package org.unicode.cldr.util;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import com.ibm.icu.dev.util.UnicodeMap;
import com.ibm.icu.text.UnicodeSet;
import com.ibm.icu.util.Freezable;

public class UnicodeRelation<T> implements Freezable<UnicodeRelation<T>> {

    final UnicodeMap<Set<T>> data = new UnicodeMap<>();
    final SetMaker<T> maker;

    public interface SetMaker<T> {
        Set<T> make();
    }

    public static SetMaker<Object> HASHSET_MAKER = new SetMaker<Object>() {
        @Override
        public Set<Object> make() {
            return new HashSet<Object>();
        }
    };

    public static final SetMaker<Object> LINKED_HASHSET_MAKER = new SetMaker<Object>() {
        public Set<Object> make() {
            return new LinkedHashSet<Object>();
        }
    };

    public UnicodeRelation(SetMaker<T> maker) {
        this.maker = maker;
    }

    public UnicodeRelation() {
        this.maker = (SetMaker<T>) HASHSET_MAKER;
    }

    public int size() {
        return data.size();
    }

    public boolean isEmpty() {
        return data.isEmpty();
    }

    public boolean containsKey(int key) {
        return data.containsKey(key);
    }

    public boolean containsKey(String key) {
        return data.containsKey(key);
    }

    public boolean containsValue(T value) {
        for (Set<T> v : data.values()) {
            if (v.contains(value)) {
                return true;
            }
        }
        return false;
    }

    public Set<T> get(int key) {
        return data.get(key);
    }

    public Set<T> get(String key) {
        return data.get((String) key);
    }

    public UnicodeSet getKeys(T value) {
        UnicodeSet result = new UnicodeSet();
        for (Entry<String, Set<T>> entry : data.entrySet()) {
            if (entry.getValue().contains(value)) {
                result.add(entry.getKey());
            }
        }
        return result;
    }

    public UnicodeRelation<T> add(String key, T value) {
        Set<T> newValues = addValue(data.get(key), value);
        if (newValues != null) {
            data.put(key, newValues);
        }
        return this;
    }

    public UnicodeRelation<T> add(int key, T value) {
        Set<T> newValues = addValue(data.get(key), value);
        if (newValues != null) {
            data.put(key, newValues);
        }
        return this;
    }

    public UnicodeRelation<T> addAll(String key, Collection<T> values) {
        Set<T> newValues = addValues(data.get(key), values);
        if (newValues != null) {
            data.put(key, newValues);
        }
        return this;
    }

    public UnicodeRelation<T> addAll(Map<String, T> m) {
        for (Entry<String, T> entry : m.entrySet()) {
            add(entry.getKey(), entry.getValue());
        }
        return this;
    }

    public UnicodeRelation<T> addAll(UnicodeSet keys, Collection<T> values) {
        for (String key : keys) {
            addAll(key, values);
        }
        return this;
    }

    public UnicodeRelation<T> addAll(UnicodeSet keys, T... values) {
        return addAll(keys, Arrays.asList(values));
    }

    public UnicodeRelation<T> addAll(UnicodeSet keys, T value) {
        for (String key : keys) {
            add(key, value);
        }
        return this;
    }

    private Set<T> addValue(Set<T> oldValues, T value) {
        if (oldValues == null) {
            return Collections.singleton(value);
        } else if (oldValues.contains(value)) {
            return null;
        } else {
            Set<T> newValues = make(oldValues);
            newValues.add(value);
            return Collections.unmodifiableSet(newValues);
        }
    }

    private final Set<T> make(Collection<T> oldValues) {
        Set<T> newValues = maker.make();
        newValues.addAll(oldValues);
        return newValues;
    }

    private Set<T> addValues(Set<T> oldValues, Collection<T> values) {
        if (oldValues == null) {
            if (values.size() == 1) {
                return Collections.singleton(values.iterator().next());
            } else {
                return Collections.unmodifiableSet(make(values));
            }
        } else if (oldValues.containsAll(values)) {
            return null;
        } else {
            Set<T> newValues = make(oldValues);
            newValues.addAll(values);
            return Collections.unmodifiableSet(newValues);
        }
    }

    private Set<T> removeValues(Set<T> oldValues, Collection<T> values) {
        if (oldValues == null) {
            return null;
        } else if (Collections.disjoint(oldValues, values)) {
            return null;
        } else {
            Set<T> newValues = make(oldValues);
            newValues.removeAll(values);
            return newValues.size() == 0 ? Collections.EMPTY_SET : Collections.unmodifiableSet(newValues);
        }
    }

    public UnicodeRelation<T> remove(int key) {
        data.remove(key);
        return this;
    }

    public UnicodeRelation<T> remove(String key) {
        data.remove(key);
        return this;
    }

    public UnicodeRelation<T> removeValue(T value) {
        UnicodeSet toChange = getKeys(value);
        for (String key : toChange) {
            remove(key, value);
        }
        return this;
    }

    public UnicodeRelation<T> remove(int key, T value) {
        Set<T> values = data.getValue(key);
        if (values != null && values.contains(value)) {
            removeExisting(key, value, values);
        }
        return this;
    }

    public UnicodeRelation<T> remove(String key, T value) {
        Set<T> values = data.getValue(key);
        if (values != null && values.contains(value)) {
            removeExisting(key, value, values);
        }
        return this;
    }

    public UnicodeRelation<T> removeAll(String key, Collection<T> values) {
        Set<T> newValues = removeValues(data.get(key), values);
        if (newValues != null) {
            if (newValues == Collections.EMPTY_SET) {
                data.remove(key);
            } else {
                data.put(key, newValues);
            }
        }
        return this;
    }

    public UnicodeRelation<T> removeAll(Map<String, T> m) {
        for (Entry<String, T> entry : m.entrySet()) {
            remove(entry.getKey(), entry.getValue());
        }
        return this;
    }

    public UnicodeRelation<T> removeAll(UnicodeSet keys, Collection<T> values) {
        for (String key : keys) {
            removeAll(key, values);
        }
        return this;
    }

    public UnicodeRelation<T> removeAll(UnicodeSet keys, T... values) {
        return removeAll(keys,  Arrays.asList(values));
    }

    public UnicodeRelation<T> removeAll(UnicodeSet keys, T value) {
        for (String key : keys) {
            remove(key, value);
        }
        return this;
    }

    private void removeExisting(int key, T value, Set<T> values) {
        if (values.size() == 1) {
            data.remove(key);
        } else {
            Set<T> newValues = make(values);
            newValues.remove(value);
            data.put(key, Collections.unmodifiableSet(newValues));
        }
    }

    private void removeExisting(String key, T value, Set<T> values) {
        if (values.size() == 1) {
            data.remove(key);
        } else {
            Set<T> newValues = make(values);
            newValues.remove(value);
            data.put(key, Collections.unmodifiableSet(newValues));
        }
    }

    public void clear() {
        data.clear();
    }

    public UnicodeSet keySet() {
        return data.keySet();
    }

    public Collection<T> values() {
        Set<T> result = maker.make();
        for (Set<T> v : data.values()) {
            result.addAll(v);
        }
        return result;
    }

    public Iterable<Entry<String, Set<T>>> keyValues() {
        return data.entrySet();
    }

    @Override
    public String toString() {
        return data.toString();
    }

    @Override
    public int hashCode() {
        return data.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        return obj instanceof UnicodeRelation && data.equals(((UnicodeRelation) obj).data);
    }

    @Override
    public boolean isFrozen() {
        return data.isFrozen();
    }

    @Override
    public UnicodeRelation<T> freeze() {
        data.freeze();
        return this;
    }

    @Override
    public UnicodeRelation<T> cloneAsThawed() {
        throw new UnsupportedOperationException();
    }
}
