/* Copyright (c) Jython Developers */
package org.python.core;

import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;

public abstract class BaseSet extends PyObject implements Set {

    /** The underlying Set. */
    protected Set<PyObject> _set;

    /**
     * Create a new Python set of type from the specified Set object.
     */
    protected BaseSet(PyType type, Set<PyObject> set) {
        super(type);
        _set = set;
    }

    protected void _update(PyObject data) {
        _update(_set, data);
    }

    /**
     * Update the underlying set with the contents of the iterable.
     */
    protected static Set<PyObject> _update(Set<PyObject> set, PyObject data) {
        if (data == null) {
            return set;
        }
        if (data instanceof BaseSet) {
            // Skip the iteration if both are sets
            set.addAll(((BaseSet)data)._set);
            return set;
        }
        for (PyObject item : data.asIterable()) {
            set.add(item);
        }
        return set;
    }

    /**
     * The union of <code>this</code> with <code>other</code>. <p/> <br/> (I.e. all elements
     * that are in either set)
     *
     * @param other A <code>BaseSet</code> instance.
     * @return The union of the two sets as a new set.
     */
    public PyObject __or__(PyObject other) {
        return baseset___or__(other);
    }

    final PyObject baseset___or__(PyObject other) {
        if (!(other instanceof BaseSet)) {
            return null;
        }
        return baseset_union(other);
    }

    /**
     * The intersection of the <code>this</code> with <code>other</code>.
     * <p/>
     * <br/>
     * (I.e. all elements that are in both sets)
     *
     * @param other A <code>BaseSet</code> instance.
     * @return The intersection of the two sets as a new set.
     */
    public PyObject __and__(PyObject other) {
        return baseset___and__(other);
    }

    final PyObject baseset___and__(PyObject other) {
        if (!(other instanceof BaseSet)) {
            return null;
        }
        return baseset_intersection(other);
    }

    /**
     * The difference of the <code>this</code> with <code>other</code>.
     * <p/>
     * <br/>
     * (I.e. all elements that are in this set and not in the other)
     *
     * @param other A <code>BaseSet</code> instance.
     * @return The difference of the two sets as a new set.
     */
    public PyObject __sub__(PyObject other) {
        return baseset___sub__(other);
    }

    final PyObject baseset___sub__(PyObject other) {
        if (!(other instanceof BaseSet)) {
            return null;
        }
        return baseset_difference(other);
    }

    public PyObject difference(PyObject other) {
        return baseset_difference(other);
    }

    final PyObject baseset_difference(PyObject other) {
        BaseSet bs = other instanceof BaseSet ? (BaseSet)other : new PySet(other);
        Set<PyObject> set = bs._set;
        BaseSet o = BaseSet.makeNewSet(getType());

        for (PyObject p : _set) {
            if (!set.contains(p)) {
                o._set.add(p);
            }
        }
        return o;
    }

    /**
     * The symmetric difference of the <code>this</code> with <code>other</code>.
     * <p/>
     * <br/>
     * (I.e. all elements that are in exactly one of the sets)
     *
     * @param other A <code>BaseSet</code> instance.
     * @return The symmetric difference of the two sets as a new set.
     */
    public PyObject __xor__(PyObject other) {
        return baseset___xor__(other);
    }

    final PyObject baseset___xor__(PyObject other) {
        if (!(other instanceof BaseSet)) {
            return null;
        }
        return baseset_symmetric_difference(other);
    }

    public PyObject symmetric_difference(PyObject other) {
        return baseset_symmetric_difference(other);
    }

    final PyObject baseset_symmetric_difference(PyObject other) {
        BaseSet bs = other instanceof BaseSet ? (BaseSet)other : new PySet(other);
        BaseSet o = BaseSet.makeNewSet(getType());
        for (PyObject p : _set) {
            if (!bs._set.contains(p)) {
                o._set.add(p);
            }
        }
        for (PyObject p : bs._set) {
            if (!_set.contains(p)) {
                o._set.add(p);
            }
        }
        return o;
    }

    /**
     * The hashCode of the set.  Only immutable instances can be hashed.
     *
     * @return The hashCode of the set.
     */
    public abstract int hashCode();

    /**
     * The length of the set.
     *
     * @return The length of the set.
     */
    public int __len__() {
        return baseset___len__();
    }

    final int baseset___len__() {
        return _set.size();
    }

    /**
     * Determines if the instance is considered <code>true</code> by Python.
     * This implementation returns true if the set is not empty.
     *
     * @return <code>true</code> if the set is not empty, <code>false</code> otherwise
     */
    public boolean __nonzero__() {
        return !_set.isEmpty();
    }

    /**
     * Produce an iterable object.
     *
     * @return An iteration of the set.
     */
    public PyObject __iter__() {
        return baseset___iter__();
    }

    final PyObject baseset___iter__() {
        return new PyIterator() {
            private int size = size();

            private Iterator<PyObject> iterator = _set.iterator();

            @Override
            public PyObject __iternext__() {
                if (size != size()) {
                    throw Py.RuntimeError("set changed size during iteration");
                }
                if (iterator.hasNext()) {
                    return iterator.next();
                }
                return null;
            }
        };
    }

    public boolean __contains__(PyObject other) {
        return baseset___contains__(other);
    }

    final boolean baseset___contains__(PyObject other) {
        try {
            return _set.contains(other);
        } catch (PyException pye) {
            PyFrozenSet frozen = asFrozen(pye, other);
            return _set.contains(frozen);
        }
    }

    public int __cmp__(PyObject other) {
        return baseset___cmp__(other);
    }

    final int baseset___cmp__(PyObject other) {
        throw Py.TypeError("cannot compare sets using cmp()");
    }

    public PyObject __eq__(PyObject other) {
        return baseset___eq__(other);
    }

    final PyObject baseset___eq__(PyObject other) {
        if (other instanceof BaseSet) {
            return Py.newBoolean(_set.equals(((BaseSet)other)._set));
        }
        return Py.False;
    }

    public PyObject __ne__(PyObject other) {
        return baseset___ne__(other);
    }

    final PyObject baseset___ne__(PyObject other) {
        if (other instanceof BaseSet) {
            return Py.newBoolean(!_set.equals(((BaseSet)other)._set));
        }
        return Py.True;
    }

    public PyObject __le__(PyObject other) {
        return baseset___le__(other);
    }

    final PyObject baseset___le__(PyObject other) {
        return baseset_issubset(asBaseSet(other));
    }

    public PyObject __ge__(PyObject other) {
        return baseset___ge__(other);
    }

    final PyObject baseset___ge__(PyObject other) {
        return baseset_issuperset(asBaseSet(other));
    }

    public PyObject __lt__(PyObject other) {
        return baseset___lt__(other);
    }

    final PyObject baseset___lt__(PyObject other) {
        BaseSet bs = asBaseSet(other);
        return Py.newBoolean(size() < bs.size() && baseset_issubset(other).__nonzero__());
    }

    public PyObject __gt__(PyObject other) {
        return baseset___gt__(other);
    }

    final PyObject baseset___gt__(PyObject other) {
        BaseSet bs = asBaseSet(other);
        return Py.newBoolean(size() > bs.size() && baseset_issuperset(other).__nonzero__());
    }

    /**
     * Used for pickling.  Uses the module <code>setsfactory</sets> to
     * export safe constructors.
     *
     * @return a tuple of (constructor, (elements))
     */
    public PyObject __reduce__() {
        return baseset___reduce__();
    }

    final PyObject baseset___reduce__(){
        PyObject args = new PyTuple(new PyList((PyObject)this));
        PyObject dict = __findattr__("__dict__");
        if (dict == null) {
            dict = Py.None;
        }
        return new PyTuple(getType(), args, dict);
    }

    final PyObject baseset_union(PyObject other) {
        BaseSet result = BaseSet.makeNewSet(getType(), this);
        result._update(other);
        return result;
    }

    final PyObject baseset_intersection(PyObject other) {
        PyObject little, big;
        if (!(other instanceof BaseSet)) {
            other = new PySet(other);
        }

        if (size() <= __builtin__.len(other)) {
            little = this;
            big = other;
        } else {
            little = other;
            big = this;
        }

        PyObject common = __builtin__.filter(big.__getattr__("__contains__"), little);
        return BaseSet.makeNewSet(getType(), common);
    }

    final PyObject baseset_copy() {
        BaseSet copy = BaseSet.makeNewSet(getType(), this);
        return copy;
    }

    final PyObject baseset_issubset(PyObject other) {
        BaseSet bs = other instanceof BaseSet ? (BaseSet)other : new PySet(other);
        if (size() > bs.size()) {
            return Py.False;
        }
        for (Object p : _set) {
            if (!bs._set.contains(p)) {
                return Py.False;
            }
        }
        return Py.True;
    }

    final PyObject baseset_issuperset(PyObject other) {
        BaseSet bs = other instanceof BaseSet ? (BaseSet)other : new PySet(other);
        return bs.baseset_issubset(this);
    }

    public String toString() {
        return baseset_toString();
    }

    final String baseset_toString() {
        String name = getType().fastGetName();
        ThreadState ts = Py.getThreadState();
        if (!ts.enterRepr(this)) {
            return name + "(...)";
        }
        StringBuilder buf = new StringBuilder(name).append("([");
        for (Iterator<PyObject> i = _set.iterator(); i.hasNext();) {
            buf.append((i.next()).__repr__().toString());
            if (i.hasNext()) {
                buf.append(", ");
            }
        }
        buf.append("])");
        ts.exitRepr(this);
        return buf.toString();
    }

    /**
     * Casts other as BaseSet, throwing a TypeError tailored for the rich comparison
     * methods when not applicable.
     *
     * @param other a PyObject
     * @return a BaseSet
     */
    protected final BaseSet asBaseSet(PyObject other) {
        if (other instanceof BaseSet) {
            return (BaseSet)other;
        }
        throw Py.TypeError("can only compare to a set");
    }

    /**
     * Return a PyFrozenSet whose contents are shared with value when value is a BaseSet and pye is
     * a TypeError.
     *
     * WARNING: The PyFrozenSet returned is only intended to be used temporarily (and internally);
     * since its contents are shared with value, it could be mutated!
     *
     * This is better than special-casing behavior based on isinstance, because a Python subclass
     * can override, say, __hash__ and all of a sudden you can't assume that a non-PyFrozenSet is
     * unhashable anymore.
     *
     * @param pye The exception thrown from a hashable operation.
     * @param value The object which was unhashable.
     * @return A PyFrozenSet if appropriate, otherwise the pye is rethrown
     */
    protected final PyFrozenSet asFrozen(PyException pye, PyObject value) {
        if (!(value instanceof BaseSet) || !pye.match(Py.TypeError)) {
            throw pye;
        }
        PyFrozenSet tmp = new PyFrozenSet();
        tmp._set = ((BaseSet)value)._set;
        return tmp;
    }

    /**
     * Create a new set of type.
     *
     * @param type a set type
     * @return a new set
     */
    protected static BaseSet makeNewSet(PyType type) {
        return makeNewSet(type, null);
    }

    /**
     * Create a new <et of type from iterable.
     *
     * @param type a set type
     * @param iterable an iterable or null
     * @return a new set
     */
    protected static BaseSet makeNewSet(PyType type, PyObject iterable) {
        BaseSet so;
        if (type == PySet.TYPE) {
            so = new PySet(iterable);
        } else if (type == PyFrozenSet.TYPE) {
            so = new PyFrozenSet(iterable);
        } else if (Py.isSubClass(type, PySet.TYPE)) {
            so = new PySetDerived(type);
            so._update(iterable);
        } else {
            so = new PyFrozenSetDerived(type, iterable);
        }
        return so;
    }

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

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

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

    public boolean add(Object o) {
        return _set.add(Py.java2py(o));
    }

    public boolean contains(Object o) {
        return _set.contains(Py.java2py(o));
    }

    public boolean remove(Object o) {
        return _set.remove(Py.java2py(o));
    }

    public boolean addAll(Collection c) {
        boolean added = false;
        for (Object object : c) {
            added |= add(object);
        }
        return added;
    }

    public boolean containsAll(Collection c) {
        for (Object object : c) {
            if (!_set.contains(Py.java2py(object))) {
                return false;
            }
        }
        return true;
    }

    public boolean removeAll(Collection c) {
        boolean removed = false;
        for (Object object : c) {
            removed |= _set.remove(Py.java2py(object));
        }
        return removed;
    }

    public boolean retainAll(Collection c) {
        boolean modified = false;
        Iterator e = iterator();
        while (e.hasNext()) {
            if (!c.contains(e.next())) {
                e.remove();
                modified = true;
            }
        }
        return modified;
    }

    public Iterator iterator() {
        return new Iterator() {
            Iterator<PyObject> real = _set.iterator();

            public boolean hasNext() {
                return real.hasNext();
            }

            public Object next() {
                return Py.tojava(real.next(), Object.class);
            }

            public void remove() {
                real.remove();
            }
        };
    }

    public Object[] toArray() {
        return toArray(new Object[size()]);
    }

    public Object[] toArray(Object a[]) {
        int size = size();
        if (a.length < size) {
            a = (Object[])Array.newInstance(a.getClass().getComponentType(), size);
        }
        Iterator<PyObject> it = iterator();
        for (int i = 0; i < size; i++) {
            a[i] = it.next();
        }
        if (a.length > size) {
            a[size] = null;
        }
        return a;
    }
}
