package api.jsoup.nodes;

import api.jsoup.SerializationException;
import api.jsoup.helper.Validate;
import static api.jsoup.internal.Normalizer.lowerCase;
import api.jsoup.internal.StringUtil;
import api.jsoup.parser.ParseSettings;
import java.io.IOException;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * The attributes of an Element.
 * <p>
 * Attributes are treated as a map: there can be only one value associated with an attribute
 * key/name.
 * </p>
 * <p>
 * Attribute name and value comparisons are generally <b>case sensitive</b>. By default for HTML,
 * attribute names are normalized to lower-case on parsing. That means you should use lower-case
 * strings when referring to attributes by name.
 * </p>
 *
 * @author Jonathan Hedley, jonathan@hedley.net
 */
public class Attributes implements Iterable<Attribute>, Cloneable {

	protected static final String dataPrefix = "data-";
	// Indicates a jsoup internal key. Can't be set via HTML. (It could be set via accessor, but not too worried about
	// that. Suppressed from list, iter.
	static final char InternalPrefix = '/';
	private static final int InitialCapacity = 2; // sampling found mean count when attrs present = 1.49; 1.08 overall. 2.6:1 have attrs.

	// manages the key/val arrays
	private static final int GrowthFactor = 2;
	private static final String[] Empty = {};
	static final int NotFound = -1;
	private static final String EmptyString = "";

	private int size = 0; // number of slots used (not capacity, which is keys.length
	String[] keys = Empty;
	String[] vals = Empty;

	// check there's room for more
	private void checkCapacity(int minNewSize) {
		Validate.isTrue(minNewSize >= size);
		int curSize = keys.length;
		if (curSize >= minNewSize) {
			return;
		}

		int newSize = curSize >= InitialCapacity ? size * GrowthFactor : InitialCapacity;
		if (minNewSize > newSize) {
			newSize = minNewSize;
		}

		keys = copyOf(keys, newSize);
		vals = copyOf(vals, newSize);
	}

	// simple implementation of Arrays.copy, for support of Android API 8.
	private static String[] copyOf(String[] orig, int size) {
		final String[] copy = new String[size];
		System.arraycopy(orig, 0, copy, 0,
		   Math.min(orig.length, size));
		return copy;
	}

	int indexOfKey(String key) {
		Validate.notNull(key);
		for (int i = 0; i < size; i++) {
			if (key.equals(keys[i])) {
				return i;
			}
		}
		return NotFound;
	}

	private int indexOfKeyIgnoreCase(String key) {
		Validate.notNull(key);
		for (int i = 0; i < size; i++) {
			if (key.equalsIgnoreCase(keys[i])) {
				return i;
			}
		}
		return NotFound;
	}

	// we track boolean attributes as null in values - they're just keys. so returns empty for consumers
	static String checkNotNull(String val) {
		return val == null ? EmptyString : val;
	}

	/**
	 * Get an attribute value by key.
	 *
	 * @param key the (case-sensitive) attribute key
	 * @return the attribute value if set; or empty string if not set (or a boolean attribute).
	 * @see #hasKey(String)
	 */
	public String get(String key) {
		int i = indexOfKey(key);
		return i == NotFound ? EmptyString : checkNotNull(vals[i]);
	}

	/**
	 * Get an attribute's value by case-insensitive key
	 *
	 * @param key the attribute name
	 * @return the first matching attribute value if set; or empty string if not set (ora boolean
	 * attribute).
	 */
	public String getIgnoreCase(String key) {
		int i = indexOfKeyIgnoreCase(key);
		return i == NotFound ? EmptyString : checkNotNull(vals[i]);
	}

	/**
	 * Adds a new attribute. Will produce duplicates if the key already exists.
	 *
	 * @see Attributes#put(String, String)
	 */
	public Attributes add(String key, String value) {
		checkCapacity(size + 1);
		keys[size] = key;
		vals[size] = value;
		size++;
		return this;
	}

	/**
	 * Set a new attribute, or replace an existing one by key.
	 *
	 * @param key case sensitive attribute key (not null)
	 * @param value attribute value (may be null, to set a boolean attribute)
	 * @return these attributes, for chaining
	 */
	public Attributes put(String key, String value) {
		Validate.notNull(key);
		int i = indexOfKey(key);
		if (i != NotFound) {
			vals[i] = value;
		} else {
			add(key, value);
		}
		return this;
	}

	void putIgnoreCase(String key, String value) {
		int i = indexOfKeyIgnoreCase(key);
		if (i != NotFound) {
			vals[i] = value;
			if (!keys[i].equals(key)) // case changed, update
			{
				keys[i] = key;
			}
		} else {
			add(key, value);
		}
	}

	/**
	 * Set a new boolean attribute, remove attribute if value is false.
	 *
	 * @param key case <b>insensitive</b> attribute key
	 * @param value attribute value
	 * @return these attributes, for chaining
	 */
	public Attributes put(String key, boolean value) {
		if (value) {
			putIgnoreCase(key, null);
		} else {
			remove(key);
		}
		return this;
	}

	/**
	 * Set a new attribute, or replace an existing one by key.
	 *
	 * @param attribute attribute with case sensitive key
	 * @return these attributes, for chaining
	 */
	public Attributes put(Attribute attribute) {
		Validate.notNull(attribute);
		put(attribute.getKey(), attribute.getValue());
		attribute.parent = this;
		return this;
	}

	// removes and shifts up
	private void remove(int index) {
		Validate.isFalse(index >= size);
		int shifted = size - index - 1;
		if (shifted > 0) {
			System.arraycopy(keys, index + 1, keys, index, shifted);
			System.arraycopy(vals, index + 1, vals, index, shifted);
		}
		size--;
		keys[size] = null; // release hold
		vals[size] = null;
	}

	/**
	 * Remove an attribute by key. <b>Case sensitive.</b>
	 *
	 * @param key attribute key to remove
	 */
	public void remove(String key) {
		int i = indexOfKey(key);
		if (i != NotFound) {
			remove(i);
		}
	}

	/**
	 * Remove an attribute by key. <b>Case insensitive.</b>
	 *
	 * @param key attribute key to remove
	 */
	public void removeIgnoreCase(String key) {
		int i = indexOfKeyIgnoreCase(key);
		if (i != NotFound) {
			remove(i);
		}
	}

	/**
	 * Tests if these attributes contain an attribute with this key.
	 *
	 * @param key case-sensitive key to check for
	 * @return true if key exists, false otherwise
	 */
	public boolean hasKey(String key) {
		return indexOfKey(key) != NotFound;
	}

	/**
	 * Tests if these attributes contain an attribute with this key.
	 *
	 * @param key key to check for
	 * @return true if key exists, false otherwise
	 */
	public boolean hasKeyIgnoreCase(String key) {
		return indexOfKeyIgnoreCase(key) != NotFound;
	}

	/**
	 * Check if these attributes contain an attribute with a value for this key.
	 *
	 * @param key key to check for
	 * @return true if key exists, and it has a value
	 */
	public boolean hasDeclaredValueForKey(String key) {
		int i = indexOfKey(key);
		return i != NotFound && vals[i] != null;
	}

	/**
	 * Check if these attributes contain an attribute with a value for this key.
	 *
	 * @param key case-insensitive key to check for
	 * @return true if key exists, and it has a value
	 */
	public boolean hasDeclaredValueForKeyIgnoreCase(String key) {
		int i = indexOfKeyIgnoreCase(key);
		return i != NotFound && vals[i] != null;
	}

	/**
	 * Get the number of attributes in this set.
	 *
	 * @return size
	 */
	public int size() {
		int s = 0;
		for (int i = 0; i < size; i++) {
			if (!isInternalKey(keys[i])) {
				s++;
			}
		}
		return s;
	}

	/**
	 * Test if this Attributes list is empty (size==0).
	 */
	public boolean isEmpty() {
		return size == 0;
	}

	/**
	 * Add all the attributes from the incoming set to this set.
	 *
	 * @param incoming attributes to add to these attributes.
	 */
	public void addAll(Attributes incoming) {
		if (incoming.size() == 0) {
			return;
		}
		checkCapacity(size + incoming.size);

		for (Attribute attr : incoming) {
			// t o d o - should this be case insensitive?
			put(attr);
		}

	}

	public Iterator<Attribute> iterator() {
		return new Iterator<Attribute>() {
			int i = 0;

			@Override
			public boolean hasNext() {
				while (i < size) {
					if (isInternalKey(keys[i])) // skip over internal keys
					{
						i++;
					} else {
						break;
					}
				}

				return i < size;
			}

			@Override
			public Attribute next() {
				final Attribute attr = new Attribute(keys[i], vals[i], Attributes.this);
				i++;
				return attr;
			}

			@Override
			public void remove() {
				Attributes.this.remove(--i); // next() advanced, so rewind
			}
		};
	}

	/**
	 * Get the attributes as a List, for iteration.
	 *
	 * @return an view of the attributes as an unmodifiable List.
	 */
	public List<Attribute> asList() {
		ArrayList<Attribute> list = new ArrayList<>(size);
		for (int i = 0; i < size; i++) {
			if (isInternalKey(keys[i])) {
				continue; // skip internal keys
			}
			Attribute attr = new Attribute(keys[i], vals[i], Attributes.this);
			list.add(attr);
		}
		return Collections.unmodifiableList(list);
	}

	/**
	 * Retrieves a filtered view of attributes that are HTML5 custom data attributes; that is,
	 * attributes with keys starting with {@code data-}.
	 *
	 * @return map of custom data attributes.
	 */
	public Map<String, String> dataset() {
		return new Dataset(this);
	}

	/**
	 * Get the HTML representation of these attributes.
	 *
	 * @return HTML
	 */
	public String html() {
		StringBuilder sb = StringUtil.borrowBuilder();
		try {
			html(sb, (new Document("")).outputSettings()); // output settings a bit funky, but this html() seldom used
		} catch (IOException e) { // ought never happen
			throw new SerializationException(e);
		}
		return StringUtil.releaseBuilder(sb);
	}

	final void html(final Appendable accum, final Document.OutputSettings out) throws IOException {
		final int sz = size;
		for (int i = 0; i < sz; i++) {
			if (isInternalKey(keys[i])) {
				continue;
			}

			// inlined from Attribute.html()
			final String key = keys[i];
			final String val = vals[i];
			accum.append(' ').append(key);

			// collapse checked=null, checked="", checked=checked; write out others
			if (!Attribute.shouldCollapseAttribute(key, val, out)) {
				accum.append("=\"");
				Entities.escape(accum, val == null ? EmptyString : val, out, true, false, false);
				accum.append('"');
			}
		}
	}

	@Override
	public String toString() {
		return html();
	}

	/**
	 * Checks if these attributes are equal to another set of attributes, by comparing the two sets
	 *
	 * @param o attributes to compare with
	 * @return if both sets of attributes have the same content
	 */
	@Override
	public boolean equals(Object o) {
		if (this == o) {
			return true;
		}
		if (o == null || getClass() != o.getClass()) {
			return false;
		}

		Attributes that = (Attributes) o;

		if (size != that.size) {
			return false;
		}
		if (!Arrays.equals(keys, that.keys)) {
			return false;
		}
		return Arrays.equals(vals, that.vals);
	}

	/**
	 * Calculates the hashcode of these attributes, by iterating all attributes and summing their
	 * hashcodes.
	 *
	 * @return calculated hashcode
	 */
	@Override
	public int hashCode() {
		int result = size;
		result = 31 * result + Arrays.hashCode(keys);
		result = 31 * result + Arrays.hashCode(vals);
		return result;
	}

	@Override
	public Attributes clone() {
		Attributes clone;
		try {
			clone = (Attributes) super.clone();
		} catch (CloneNotSupportedException e) {
			throw new RuntimeException(e);
		}
		clone.size = size;
		keys = copyOf(keys, size);
		vals = copyOf(vals, size);
		return clone;
	}

	/**
	 * Internal method. Lowercases all keys.
	 */
	public void normalize() {
		for (int i = 0; i < size; i++) {
			keys[i] = lowerCase(keys[i]);
		}
	}

	/**
	 * Internal method. Removes duplicate attribute by name. Settings for case sensitivity of key
	 * names.
	 *
	 * @param settings case sensitivity
	 * @return number of removed dupes
	 */
	public int deduplicate(ParseSettings settings) {
		if (isEmpty()) {
			return 0;
		}
		boolean preserve = settings.preserveAttributeCase();
		int dupes = 0;
		OUTER:
		for (int i = 0; i < keys.length; i++) {
			for (int j = i + 1; j < keys.length; j++) {
				if (keys[j] == null) {
					continue OUTER; // keys.length doesn't shrink when removing, so re-test
				}
				if ((preserve && keys[i].equals(keys[j])) || (!preserve && keys[i].equalsIgnoreCase(keys[j]))) {
					dupes++;
					remove(j);
					j--;
				}
			}
		}
		return dupes;
	}

	private static class Dataset extends AbstractMap<String, String> {

		private final Attributes attributes;

		private Dataset(Attributes attributes) {
			this.attributes = attributes;
		}

		@Override
		public Set<Entry<String, String>> entrySet() {
			return new EntrySet();
		}

		@Override
		public String put(String key, String value) {
			String dataKey = dataKey(key);
			String oldValue = attributes.hasKey(dataKey) ? attributes.get(dataKey) : null;
			attributes.put(dataKey, value);
			return oldValue;
		}

		private class EntrySet extends AbstractSet<Map.Entry<String, String>> {

			@Override
			public Iterator<Map.Entry<String, String>> iterator() {
				return new DatasetIterator();
			}

			@Override
			public int size() {
				int count = 0;
				Iterator iter = new DatasetIterator();
				while (iter.hasNext()) {
					count++;
				}
				return count;
			}
		}

		private class DatasetIterator implements Iterator<Map.Entry<String, String>> {

			private Iterator<Attribute> attrIter = attributes.iterator();
			private Attribute attr;

			public boolean hasNext() {
				while (attrIter.hasNext()) {
					attr = attrIter.next();
					if (attr.isDataAttribute()) {
						return true;
					}
				}
				return false;
			}

			public Entry<String, String> next() {
				return new Attribute(attr.getKey().substring(dataPrefix.length()), attr.getValue());
			}

			public void remove() {
				attributes.remove(attr.getKey());
			}
		}
	}

	private static String dataKey(String key) {
		return dataPrefix + key;
	}

	static String internalKey(String key) {
		return InternalPrefix + key;
	}

	private boolean isInternalKey(String key) {
		return key != null && key.length() > 1 && key.charAt(0) == InternalPrefix;
	}
}
