package jscl.math.polynomial;

import jscl.math.Generic;
import jscl.math.JsclInteger;
import jscl.mathml.MathML;

import javax.annotation.Nonnull;
import java.util.Iterator;

final class GeoBucket extends Polynomial {
    final Polynomial factory;
    Polynomial content[];
    int size;
    boolean mutable = true;
    boolean canonicalized = true;

    GeoBucket(Polynomial factory) {
        super(factory.monomialFactory, factory.coefFactory);
        this.factory = factory;
    }

    GeoBucket(int size, Polynomial factory) {
        this(factory);
        init(size);
    }

    static int log(int n) {
        int i;
        for (i = 0; n > 3; n >>= 2) i++;
        return i;
    }

    public int size() {
        return size;
    }

    void init(int size) {
        content = new Polynomial[size];
        this.size = size;
    }

    void resize(int size) {
        Polynomial content[] = new Polynomial[size];
        System.arraycopy(this.content, 0, content, 0, Math.min(this.size, size));
        this.content = content;
        this.size = size;
    }

    public Iterator iterator(boolean direction, Monomial current) {
        return new ContentIterator(direction, current);
    }

    Term behead(Term t, int n, int i) {
        Monomial m = t.monomial();
        Polynomial p = factory.valueOf(m).multiply(t.coef());
        content[n] = content[n].subtract(p);
        content[i] = content[i].add(p);
        return new Term(m, content[i].coefficient(m));
    }

    void canonicalize() {
        Polynomial s = factory.valueOf(JsclInteger.valueOf(0));
        int sugar = 0;
        for (int i = 0; i < size; i++) {
            Polynomial p = content[i];
            if (p == null) continue;
            s = s.add(p);
            sugar = Math.max(sugar, p.sugar());
            content[i] = null;
        }
        resize(log(s.size()) + 1);
        set(s.normalize());
        canonicalized = true;
        setSugar(sugar);
        mutable = false;
    }

    Polynomial polynomial() {
        if (canonicalized) return content[size - 1];
        else throw new UnsupportedOperationException();
    }

    void set(Polynomial polynomial) {
        content[size - 1] = polynomial;
    }

    @Nonnull
    public Polynomial subtract(@Nonnull Polynomial that) {
        if (mutable) {
            Polynomial q = ((GeoBucket) that).polynomial();
            int n = log(q.size());
            if (n >= size) resize(n + 1);
            Polynomial p = content[n];
            Polynomial s = (p == null ? factory.valueOf(JsclInteger.valueOf(0)) : p).subtract(q);
            content[n] = null;
            while (n < log(s.size())) {
                n++;
                if (n >= size) resize(n + 1);
                p = content[n];
                if (p != null) s = p.add(s);
                content[n] = null;
            }
            content[n] = s;
            canonicalized = false;
            normalized = false;
            return this;
        } else return copy().subtract(that);
    }

    public Polynomial multiplyAndSubtract(Generic generic, Polynomial polynomial) {
        if (mutable) {
            Polynomial q = ((GeoBucket) polynomial).polynomial();
            int n = log(q.size());
            if (n >= size) resize(n + 1);
            Polynomial p = content[n];
            Polynomial s = (p == null ? factory.valueOf(JsclInteger.valueOf(0)) : p).multiplyAndSubtract(generic, q);
            content[n] = null;
            while (n < log(s.size())) {
                n++;
                if (n >= size) resize(n + 1);
                p = content[n];
                if (p != null) s = p.add(s);
                content[n] = null;
            }
            content[n] = s;
            canonicalized = false;
            normalized = false;
            return this;
        } else return copy().multiplyAndSubtract(generic, polynomial);
    }

    public Polynomial multiplyAndSubtract(Monomial monomial, Generic generic, Polynomial polynomial) {
        if (mutable) {
            Polynomial q = ((GeoBucket) polynomial).polynomial();
            int n = log(q.size());
            if (n >= size) resize(n + 1);
            Polynomial p = content[n];
            Polynomial s = (p == null ? factory.valueOf(JsclInteger.valueOf(0)) : p).multiplyAndSubtract(monomial, generic, q);
            content[n] = null;
            while (n < log(s.size())) {
                n++;
                if (n >= size) resize(n + 1);
                p = content[n];
                if (p != null) s = p.add(s);
                content[n] = null;
            }
            content[n] = s;
            canonicalized = false;
            normalized = false;
            return this;
        } else return copy().multiplyAndSubtract(monomial, generic, polynomial);
    }

    public Polynomial multiply(Generic generic) {
        if (mutable) {
            if (canonicalized) set(polynomial().multiply(generic));
            else for (int i = 0; i < size; i++) {
                Polynomial p = content[i];
                if (p != null) content[i] = p.multiply(generic);
            }
            normalized = false;
            return this;
        } else return copy().multiply(generic);
    }

    public Polynomial multiply(Monomial monomial) {
        if (mutable) {
            set(polynomial().multiply(monomial));
            return this;
        } else return copy().multiply(monomial);
    }

    public Polynomial divide(Generic generic) throws ArithmeticException {
        if (mutable) {
            if (canonicalized) set(polynomial().divide(generic));
            else for (int i = 0; i < size; i++) {
                Polynomial p = content[i];
                if (p != null) content[i] = p.divide(generic);
            }
            normalized = false;
            return this;
        } else return copy().divide(generic);
    }

    public Polynomial divide(Monomial monomial) throws ArithmeticException {
        if (mutable) {
            set(polynomial().divide(monomial));
            return this;
        } else return copy().divide(monomial);
    }

    public Polynomial gcd(Polynomial polynomial) {
        throw new UnsupportedOperationException();
    }

    public Generic gcd() {
        if (field) return coefficient(tail());
        return canonicalized ? polynomial().gcd() : coefficient(JsclInteger.valueOf(0));
    }

    public int degree() {
        return polynomial().degree();
    }

    public Polynomial valueof(GeoBucket bucket) {
        return valueOf(bucket.polynomial().copy());
    }

    public Polynomial valueOf(Polynomial polynomial) {
        if (polynomial instanceof GeoBucket) {
            return valueof((GeoBucket) polynomial);
        } else {
            GeoBucket b = new GeoBucket(log(polynomial.size()) + 1, factory);
            b.set(polynomial);
            return b;
        }
    }

    public Polynomial valueOf(Generic generic) {
        return valueOf(factory.valueOf(generic));
    }

    public Polynomial valueOf(Monomial monomial) {
        return valueOf(factory.valueOf(monomial));
    }

    public Polynomial freeze() {
        canonicalize();
        return this;
    }

    public Term head() {
        return canonicalized ? polynomial().head() : super.head();
    }

    public Term tail() {
        return canonicalized ? polynomial().tail() : super.tail();
    }

    public Generic coefficient(Monomial monomial) {
        return canonicalized ? polynomial().coefficient(monomial) : super.coefficient(monomial);
    }

    public int sugar() {
        return polynomial().sugar();
    }

    public int index() {
        return polynomial().index();
    }

    public void setSugar(int n) {
        polynomial().setSugar(n);
    }

    public void setIndex(int n) {
        polynomial().setIndex(n);
    }

    public Generic genericValue() {
        return polynomial().genericValue();
    }

    public Generic[] elements() {
        return polynomial().elements();
    }

    public int compareTo(GeoBucket bucket) {
        return polynomial().compareTo(bucket.polynomial());
    }

    public int compareTo(Polynomial polynomial) {
        return compareTo((GeoBucket) polynomial);
    }

    public String toString() {
        if (canonicalized) return polynomial().toString();
        else {
            StringBuffer buffer = new StringBuffer();
            buffer.append("{");
            for (int i = 0; i < size; i++) {
                Polynomial p = content[i];
                buffer.append(p == null ? factory.valueOf(JsclInteger.valueOf(0)) : p).append(i < size - 1 ? ", " : "");
            }
            buffer.append("}");
            return buffer.toString();
        }
    }

    public void toMathML(MathML element, Object data) {
        if (canonicalized) polynomial().toMathML(element, data);
        else {
            MathML e1 = element.element("mfenced");
            MathML e2 = element.element("mtable");
            for (int i = 0; i < size; i++) {
                MathML e3 = element.element("mtr");
                MathML e4 = element.element("mtd");
                Polynomial p = content[i];
                (p == null ? factory.valueOf(JsclInteger.valueOf(0)) : p).toMathML(e4, null);
                e3.appendChild(e4);
                e2.appendChild(e3);
            }
            e1.appendChild(e2);
            element.appendChild(e1);
        }
    }

    class ContentIterator implements Iterator {
        final boolean direction;
        Term term;

        ContentIterator(boolean direction, Monomial current) {
            this.direction = direction;
            term = new Term(current, coefficient(JsclInteger.valueOf(0)));
            seek();
        }

        void seek() {
            while (true) {
                int n = 0;
                Term t = null;
                for (int i = 0; i < size; i++) {
                    Polynomial p = content[i];
                    if (p == null) continue;
                    Iterator it = p.iterator(direction, term.monomial());
                    Term u = it.hasNext() ? (Term) it.next() : null;
                    if (u == null) continue;
                    if (t == null || (direction ? -1 : 1) * ordering.compare(t.monomial(), u.monomial()) > 0) {
                        t = u;
                        n = i;
                    } else if (ordering.compare(t.monomial(), u.monomial()) == 0) {
                        t = behead(t, n, i);
                        n = i;
                    }
                }
                if (t == null || t.coef().signum() != 0) {
                    term = t;
                    return;
                }
            }
        }

        public boolean hasNext() {
            return term != null;
        }

        public Object next() {
            Term t = term;
            seek();
            return t;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
}
