package org.python.modules._collections;

import org.python.core.PyIterator;
import org.python.core.PyObject;
import org.python.core.PyTuple;
import org.python.core.PyType;
import org.python.core.Py;
import org.python.core.PyException;
import org.python.core.PyBuiltinCallable;
import org.python.core.ThreadState;
import org.python.expose.ExposedMethod;
import org.python.expose.ExposedNew;
import org.python.expose.ExposedType;
import org.python.expose.MethodType;

/**
 * PyDeque - This class implements the functionalities of Deque data structure. Deques are a
 * generalization of stacks and queues (the name is pronounced 'deck' and is short for 'double-ended
 * queue'). Deques support thread-safe, memory efficient appends and pops from either side of the
 * deque with approximately the same O(1) performance in either direction.
 * 
 * Though list objects support similar operations, they are optimized for fast fixed-length
 * operations and incur O(n) memory movement costs for pop(0) and insert(0, v) operations which
 * change both the size and position of the underlying data representation.
 * 
 * collections.deque([iterable]) - returns a new deque object initialized left-to-right (using
 * append()) with data from iterable. If iterable is not specified, the new deque is empty.
 */
@ExposedType(name = "collections.deque")
public class PyDeque extends PyObject {

    public static final PyType TYPE = PyType.fromClass(PyDeque.class);

    private int size = 0;

    private Node header = new Node(null, null, null); 
    
    public PyDeque() {
        this(TYPE);
    }

    public PyDeque(PyType subType) {
        super(subType);
        header.left = header.right = header;
    }

    @ExposedNew
    @ExposedMethod
    final void deque___init__(PyObject[] args, String[] kwds) {
        if (kwds.length > 0) {
            throw Py.TypeError("deque() does not take keyword arguments");
        }
        int nargs = args.length;
        if (nargs > 1) {
            throw PyBuiltinCallable.DefaultInfo.unexpectedCall(nargs, false, "deque", 0, 1);
        } 
        if (nargs == 0) {
            return;
        }
        deque_extend(args[0]);
    }

    /**
     * Add obj to the right side of the deque.
     */	
    @ExposedMethod
    final void deque_append(PyObject obj) {
        addBefore(obj, header);		
    }

    /**
     * Add obj to the left side of the deque.
     */
    @ExposedMethod
    final void deque_appendleft(PyObject obj) {		
        addBefore(obj, header.right);
    }

    private Node addBefore(PyObject obj, Node node) {
        Node newNode = new Node(obj, node, node.left);
        newNode.left.right = newNode;
        newNode.right.left = newNode;
        size++;
        return newNode;
    }

    /**
     * Remove all elements from the deque leaving it with length 0.
     */
    @ExposedMethod
    final void deque_clear() {
        Node node = header.right;
        while (node != header) {
            Node right = node.right;
            node.left = null;
            node.right = null;
            node.data = null;
            node = right;
        }
        header.right = header.left = header;
        size = 0;
    }

    /**
     * Extend the right side of the deque by appending elements from the 
     * iterable argument.
     */
    @ExposedMethod
    final void deque_extend(PyObject iterable) {
        for (PyObject item : iterable.asIterable()) {
            deque_append(item);			
        } 
    }

    /**
     * Extend the left side of the deque by appending elements from iterable. 
     * Note, the series of left appends results in reversing the order of 
     * elements in the iterable argument.
     */
    @ExposedMethod
    final void deque_extendleft(PyObject iterable) {
        for (PyObject item : iterable.asIterable()) {
            deque_appendleft(item);
        }
    }

    /**
     * Remove and return an element from the right side of the deque. If no 
     * elements are present, raises an IndexError.
     */
    @ExposedMethod
    final PyObject deque_pop() {
        return removeNode(header.left);
    }

    /**
     * Remove and return an element from the left side of the deque. If no 
     * elements are present, raises an IndexError.
     */
    @ExposedMethod
    final PyObject deque_popleft() {
        return removeNode(header.right);
    }

    private PyObject removeNode(Node node) {
        if (node == header) {
            throw Py.IndexError("pop from an empty deque");
        }
        PyObject obj = node.data;
        node.left.right = node.right;
        node.right.left = node.left;
        node.right = null;
        node.left = null;
        node.data = null;
        size--;
        return obj;
    } 

    /**
     * Removed the first occurrence of value. If not found, raises a 
     * ValueError.
     */
    @ExposedMethod
    final PyObject deque_remove(PyObject value) {
        int n = size;
        Node tmp = header.right;
        boolean match = false;
        for (int i = 0; i < n; i++) {
            if (tmp.data.equals(value)) { 
                match = true;
            }
            if (n != size) { 
                throw Py.IndexError("deque mutated during remove().");
            }
            if (match) {
                return removeNode(tmp);
            }
            tmp = tmp.right;
        }
        throw Py.ValueError("deque.remove(x): x not in deque");
    }

    /**
     * Rotate the deque n steps to the right. If n is negative, rotate to the 
     * left. Rotating one step to the right is equivalent to: d.appendleft(d.pop()).
     */
    @ExposedMethod(defaults = {"1"})
    final void deque_rotate(int steps) {
        if (size == 0) {
            return;
        }

        int halfsize = (size + 1) >> 1; 
        if (steps > halfsize || steps < -halfsize) {
            steps %= size;
            if (steps > halfsize) { 
                steps -= size;
            } else if (steps < -halfsize) {
                steps += size;
            }
        }

        //rotate right 
        for (int i = 0; i < steps; i++) {
            deque_appendleft(deque_pop());
        } 
        //rotate left
        for (int i = 0; i > steps; i--) {
            deque_append(deque_popleft());
        } 
    }

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

    @ExposedMethod(names = "__repr__")
    final String deque_toString() {
        ThreadState ts = Py.getThreadState();
        if (!ts.enterRepr(this)) { 
            return "[...]";
        }
        StringBuilder buf = new StringBuilder("deque").append("([");
        for (Node tmp = header.right; tmp != header; tmp = tmp.right) {
            buf.append(tmp.data.__repr__().toString());
            if (tmp.right != header) {
                buf.append(", ");
            }
        }
        buf.append("])");
        ts.exitRepr(this);
        return buf.toString();
    }

    public int __len__() {
        return deque___len__();
    }

    @ExposedMethod
    final int deque___len__() {
        return size;
    }

    public boolean __nonzero__() {
        return deque___nonzero__();
    }

    @ExposedMethod
    final boolean deque___nonzero__() {
        return size != 0;
    }

    public PyObject __finditem__(PyObject key) {
        try {
            return deque___getitem__(key);
        } catch (PyException pe) {
            if (pe.match(Py.KeyError)) {
                return null;
            }
            throw pe;
        }
    }

    @ExposedMethod
    final PyObject deque___getitem__(PyObject index) {
        return getNode(index).data;				
    }	

    public void __setitem__(PyObject index, PyObject value) {
        deque___setitem__(index, value);
    }

    @ExposedMethod
    final void deque___setitem__(PyObject index, PyObject value) {
        Node node = getNode(index).right;
        removeNode(node.left);
        addBefore(value, node);
    }	

    public void __delitem__(PyObject key) {
        deque___delitem__(key);
    }

    @ExposedMethod
    final void deque___delitem__(PyObject key) {
        removeNode(getNode(key));
    }

    private Node getNode(PyObject index) {
        int pos = 0;
        if (!index.isIndex()) {
            throw Py.TypeError(String.format("sequence index must be integer, not '%.200s'",
                                             index.getType().fastGetName()));
        }
        pos = index.asIndex(Py.IndexError);

        if (pos < 0) {
            pos += size;
        }
        if (pos < 0 || pos >= size) {
            throw Py.IndexError("index out of range: " + index);
        }

        Node tmp = header;
        if (pos < (size >> 1)) {
            for (int i = 0; i <= pos; i++) {
                tmp = tmp.right;
            }
        } else {
            for (int i = size - 1; i >= pos; i--) {
                tmp = tmp.left;
            }
        }
        return tmp;
    }

    public PyObject __iter__() {
        return deque___iter__();
    }

    @ExposedMethod
    final PyObject deque___iter__() {
        return new PyDequeIter();
    }

    public synchronized PyObject __eq__(PyObject o) {
        return deque___eq__(o);
    }

    @ExposedMethod(type = MethodType.BINARY)
    final synchronized PyObject deque___eq__(PyObject o) {
        if (!(getType() == o.getType()) && !(getType().isSubType(o.getType()))) {
            return null;
        }
        int tl = __len__();
        int ol = o.__len__();
        if (tl != ol) {
            return Py.False;
        }
        int i = cmp(this, tl, o, ol);
        return (i < 0) ? Py.True : Py.False;
    }

    public synchronized PyObject __ne__(PyObject o) {
        return deque___ne__(o);
    }

    @ExposedMethod(type = MethodType.BINARY)
    final synchronized PyObject deque___ne__(PyObject o) {
        if (!(getType() == o.getType()) && !(getType().isSubType(o.getType()))) {
            return null;
        }
        int tl = __len__();
        int ol = o.__len__();
        if (tl != ol) {
            return Py.True;
        }
        int i = cmp(this, tl, o, ol);
        return (i < 0) ? Py.False : Py.True;
    }

    public synchronized PyObject __lt__(PyObject o) {
        return deque___lt__(o);
    }

    @ExposedMethod(type = MethodType.BINARY)
    final synchronized PyObject deque___lt__(PyObject o) {
        if (!(getType() == o.getType()) && !(getType().isSubType(o.getType()))) {
            return null;
        }
        int i = cmp(this, -1, o, -1);
        if (i < 0) {
            return (i == -1) ? Py.True : Py.False;
        }
        return __finditem__(i)._lt(o.__finditem__(i));
    }

    public synchronized PyObject __le__(PyObject o) {
        return deque___le__(o);
    }

    @ExposedMethod(type = MethodType.BINARY)
    final synchronized PyObject deque___le__(PyObject o) {
        if (!(getType() == o.getType()) && !(getType().isSubType(o.getType()))) {
            return null;
        }
        int i = cmp(this, -1, o, -1);
        if (i < 0) {
            return (i == -1 || i == -2) ? Py.True : Py.False;
        }
        return __finditem__(i)._le(o.__finditem__(i));
    }

    public synchronized PyObject __gt__(PyObject o) {
        return deque___gt__(o);
    }

    @ExposedMethod(type = MethodType.BINARY)
    final synchronized PyObject deque___gt__(PyObject o) {
        if (!(getType() == o.getType()) && !(getType().isSubType(o.getType()))) {
            return null;
        }
        int i = cmp(this, -1, o, -1);
        if (i < 0) {
            return (i == -3) ? Py.True : Py.False;
        }
        return __finditem__(i)._gt(o.__finditem__(i));
    }

    public synchronized PyObject __ge__(PyObject o) {
        return deque___ge__(o);
    }

    @ExposedMethod(type = MethodType.BINARY)
    final synchronized PyObject deque___ge__(PyObject o) {
        if (!(getType() == o.getType()) && !(getType().isSubType(o.getType()))) {
            return null;
        }
        int i = cmp(this, -1, o, -1);
        if (i < 0) {
            return (i == -3 || i == -2) ? Py.True : Py.False;
        }
        return __finditem__(i)._ge(o.__finditem__(i));
    }

    // Return value >= 0 is the index where the sequences differs.
    // -1: reached the end of o1 without a difference
    // -2: reached the end of both seqeunces without a difference
    // -3: reached the end of o2 without a difference
    protected static int cmp(PyObject o1, int ol1, PyObject o2, int ol2) {
        if (ol1 < 0) {
            ol1 = o1.__len__();
        }
        if (ol2 < 0) {
            ol2 = o2.__len__();
        }
        for (int i = 0 ; i < ol1 && i < ol2; i++) {
            if (!o1.__getitem__(i).equals(o2.__getitem__(i))) {
                return i;
            }
        }
        if (ol1 == ol2) {
            return -2;
        }
        return (ol1 < ol2) ? -1 : -3;
    }

    public int hashCode() {
        return deque_hashCode();
    }

    @ExposedMethod(names = "__hash__")
    final int deque_hashCode() {
        throw Py.TypeError("deque objects are unhashable");
    }

    public PyObject __reduce__() {
        return deque___reduce__();
    }

    @ExposedMethod
    final PyObject deque___reduce__() {
        PyObject dict = getDict();
        if (dict == null) {
            dict = Py.None;
        }
        return new PyTuple(getType(), Py.EmptyTuple, dict, __iter__());
    }

    @ExposedMethod
    final PyObject deque___copy__() {
        PyDeque pd = (PyDeque)this.getType().__call__();	
        pd.deque_extend(this);
        return pd;
    }

    @Override
    public boolean isMappingType() {
        return false;
    }

    @Override
    public boolean isSequenceType() {
        return true;
    }

    private static class Node {
        private Node left;
        private Node right;
        private PyObject data;

        Node(PyObject data, Node right, Node left) {
            this.data = data;
            this.right = right;
            this.left = left;
        }
    }

    private class PyDequeIter extends PyIterator {

        private Node lastReturned = header;
        private int itersize;

        public PyDequeIter() {
            itersize = size;
        }

        public PyObject __iternext__() {        	   		
            if (itersize != size) { 
                throw Py.RuntimeError("deque changed size during iteration");
            }
            if (lastReturned.right != header) {
                lastReturned = lastReturned.right;
                return lastReturned.data;
            }
            return null;
        }
    }
}
