package io.noties.markwon.html.jsoup.nodes;

import java.util.Arrays;
import java.util.Iterator;

import io.noties.markwon.html.jsoup.helper.Validate;

import static io.noties.markwon.html.jsoup.helper.Normalizer.lowerCase;

/**
 * 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-";
    private static final int InitialCapacity = 4; // todo - analyze Alexa 1MM sites, determine best setting

    // 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 without checking if this key exists
    private void add(String key, String value) {
        checkCapacity(size + 1);
        keys[size] = key;
        vals[size] = value;
        size++;
    }

    /**
     * Set a new attribute, or replace an existing one by key.
     * @param key case sensitive attribute key
     * @param value attribute value
     * @return these attributes, for chaining
     */
    public Attributes put(String key, String value) {
        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;
    }

    /**
     Get the number of attributes in this set.
     @return size
     */
    public int size() {
        return size;
    }

    /**
     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) {
            // todo - should this be case insensitive?
            put(attr);
        }

    }

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

            @Override
            public boolean hasNext() {
                return i < size;
            }

            @Override
            public Attribute next() {
                final String val = vals[i];
                final Attribute attr = new Attribute(keys[i], val == null ? "" : val, 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 unmodifialbe List.
//     */
//    public List<Attribute> asList() {
//        ArrayList<Attribute> list = new ArrayList<>(size);
//        for (int i = 0; i < size; i++) {
////            Attribute attr = vals[i] == null ?
////                    new BooleanAttribute(keys[i]) : // deprecated class, but maybe someone still wants it
////                    new Attribute(keys[i], vals[i], Attributes.this);
////            list.add(attr);
//            list.add(new Attribute(keys[i], vals[i], Attributes.this));
//        }
//        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
//     @throws SerializationException if the HTML representation of the attributes cannot be constructed.
//     */
//    public String html() {
//        StringBuilder accum = new StringBuilder();
//        try {
//            html(accum, (new Document("")).outputSettings()); // output settings a bit funky, but this html() seldom used
//        } catch (IOException e) { // ought never happen
//            throw new SerializationException(e);
//        }
//        return accum.toString();
//    }
//
//    final void html(final Appendable accum, final Document.OutputSettings out) throws IOException {
//        final int sz = size;
//        for (int i = 0; i < sz; i++) {
//            // 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]);
        }
    }

//    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;
//    }
}