package jscl.math.function;

import jscl.math.*;
import jscl.mathml.MathML;

import javax.annotation.Nonnull;

import static jscl.math.JsclInteger.ONE;
import static jscl.math.JsclInteger.ZERO;

public class Pow extends Algebraic {

    private static final int MAX_ARRAY_SIZE = 10000;

    public Pow(Generic generic, Generic exponent) {
        super("pow", new Generic[]{generic, exponent});
    }

    static Generic root_minus_1(int d) {
        switch (d) {
            case 1:
                return JsclInteger.valueOf(-1);
            case 2:
                return Constants.Generic.I;
            case 3:
                return Constants.Generic.J_BAR.negate();
            case 4:
                return new Sqrt(Constants.Generic.HALF).expressionValue().multiply(JsclInteger.valueOf(1).add(Constants.Generic.I));
            case 6:
                return Constants.Generic.HALF.multiply(new Sqrt(JsclInteger.valueOf(3)).expressionValue().add(Constants.Generic.I));
            default:
                return null;
        }
    }

    @Override
    public int getMinParameters() {
        return 2;
    }

    public Root rootValue() throws NotRootException {
        try {
            Variable v = parameters[1].variableValue();
            if (v instanceof Inverse) {
                Generic g = ((Inverse) v).parameter();
                try {
                    int d = g.integerValue().intValue();
                    if (d > 0 && d < MAX_ARRAY_SIZE) {
                        Generic a[] = new Generic[d + 1];
                        a[0] = parameters[0].negate();
                        for (int i = 1; i < d; i++) {
                            a[i] = ZERO;
                        }
                        a[d] = ONE;
                        return new Root(a, 0);
                    }
                } catch (NotIntegerException e) {
                }
            }
        } catch (NotVariableException e) {
        }
        throw new NotRootException();
    }

    public Generic antiDerivative(@Nonnull Variable variable) throws NotIntegrableException {
        try {
            Root r = rootValue();
            Generic g[] = r.getParameters();
            if (g[0].isPolynomial(variable)) {
                return AntiDerivative.compute(r, variable);
            } else throw new NotIntegrableException(this);
        } catch (NotRootException e) {
        }
        return super.antiDerivative(variable);
    }

    public Generic antiDerivative(int n) throws NotIntegrableException {
        if (n == 0) {
            return new Pow(parameters[0], parameters[1].add(JsclInteger.valueOf(1))).selfExpand().multiply(new Inverse(parameters[1].add(JsclInteger.valueOf(1))).selfExpand());
        } else {
            return new Pow(parameters[0], parameters[1]).selfExpand().multiply(new Inverse(new Ln(parameters[0]).selfExpand()).selfExpand());
        }
    }

    public Generic derivative(int n) {
        if (n == 0) {
            return new Pow(parameters[0], parameters[1].subtract(JsclInteger.valueOf(1))).selfExpand().multiply(parameters[1]);
        } else {
            return new Pow(parameters[0], parameters[1]).selfExpand().multiply(new Ln(parameters[0]).selfExpand());
        }
    }

    public Generic selfExpand() {
        if (parameters[0].compareTo(JsclInteger.valueOf(1)) == 0) {
            return JsclInteger.valueOf(1);
        }
        if (parameters[1].signum() < 0) {
            return new Pow(new Inverse(parameters[0]).selfExpand(), parameters[1].negate()).selfExpand();
        }
        try {
            int c = parameters[1].integerValue().intValue();
            return parameters[0].pow(c);
        } catch (NotIntegerException e) {
        }
        try {
            Root r = rootValue();
            int d = r.degree();
            Generic g[] = r.getParameters();
            Generic a = g[0].negate();
            try {
                JsclInteger en = a.integerValue();
                if (en.signum() < 0) ;
                else {
                    Generic rt = en.nthrt(d);
                    if (rt.pow(d).compareTo(en) == 0) return rt;
                }
            } catch (NotIntegerException e) {
            }
        } catch (NotRootException e) {
        }
        return expressionValue();
    }

    public Generic selfElementary() {
        return new Exp(
                new Ln(
                        parameters[0]
                ).selfElementary().multiply(
                        parameters[1]
                )
        ).selfElementary();
    }

    public Generic selfSimplify() {
        // a ^ b

        // a = 1 => for any b: 1 ^ b = 1
        if (parameters[0].compareTo(ONE) == 0) {
            return JsclInteger.valueOf(1);
        }

        // b < 0 => a ^ b = (1 / a) ^ (-b)
        if (parameters[1].signum() < 0) {
            return new Pow(new Inverse(parameters[0]).selfSimplify(), parameters[1].negate()).selfSimplify();
        }

        try {
            // if b is integer => just calculate the result
            int intPower = parameters[1].integerValue().intValue();
            return parameters[0].pow(intPower);
        } catch (NotIntegerException e) {
        }

        try {
            Root r = rootValue();
            int d = r.degree();
            Generic g[] = r.getParameters();
            Generic a = g[0].negate();
            try {
                JsclInteger en = a.integerValue();
                if (en.signum() < 0) ;
                else {
                    Generic rt = en.nthrt(d);
                    if (rt.pow(d).compareTo(en) == 0) return rt;
                }
            } catch (NotIntegerException e) {
            }
            switch (d) {
                case 2:
                    return new Sqrt(a).selfSimplify();
                case 3:
                case 4:
                case 6:
                    if (a.compareTo(JsclInteger.valueOf(-1)) == 0) return root_minus_1(d);
            }
        } catch (NotRootException e) {
            Generic n[] = Fraction.separateCoefficient(parameters[1]);
            if (n[0].compareTo(ONE) == 0 && n[1].compareTo(ONE) == 0) {
                // do nothing
            } else {
                return new Pow(
                        new Pow(
                                new Pow(
                                        parameters[0],
                                        n[2]
                                ).selfSimplify(),
                                new Inverse(
                                        n[1]
                                ).selfSimplify()
                        ).selfSimplify(),
                        n[0]
                ).selfSimplify();
            }
        }
        return expressionValue();
    }

    public Generic selfNumeric() {
        return ((NumericWrapper) parameters[0]).pow(parameters[1]);
    }

    public String toString() {
        StringBuffer buffer = new StringBuffer();
        try {
            JsclInteger en = parameters[0].integerValue();
            if (en.signum() < 0) buffer.append(GenericVariable.valueOf(en, true));
            else buffer.append(en);
        } catch (NotIntegerException e) {
            try {
                Variable v = parameters[0].variableValue();
                if (v instanceof Fraction || v instanceof Pow) {
                    buffer.append(GenericVariable.valueOf(parameters[0]));
                } else buffer.append(v);
            } catch (NotVariableException e2) {
                try {
                    Power o = parameters[0].powerValue();
                    if (o.exponent() == 1) buffer.append(o.value(true));
                    else buffer.append(GenericVariable.valueOf(parameters[0]));
                } catch (NotPowerException e3) {
                    buffer.append(GenericVariable.valueOf(parameters[0]));
                }
            }
        }
        buffer.append("^");
        try {
            JsclInteger en = parameters[1].integerValue();
            buffer.append(en);
        } catch (NotIntegerException e) {
            try {
                Variable v = parameters[1].variableValue();
                if (v instanceof Fraction) {
                    buffer.append(GenericVariable.valueOf(parameters[1]));
                } else buffer.append(v);
            } catch (NotVariableException e2) {
                try {
                    parameters[1].powerValue();
                    buffer.append(parameters[1]);
                } catch (NotPowerException e3) {
                    buffer.append(GenericVariable.valueOf(parameters[1]));
                }
            }
        }
        return buffer.toString();
    }

    public String toJava() {
        StringBuffer buffer = new StringBuffer();
        buffer.append(parameters[0].toJava());
        buffer.append(".pow(");
        buffer.append(parameters[1].toJava());
        buffer.append(")");
        return buffer.toString();
    }

    void bodyToMathML(MathML element, boolean fenced) {
        if (fenced) {
            MathML e1 = element.element("mfenced");
            bodyToMathML(e1);
            element.appendChild(e1);
        } else {
            bodyToMathML(element);
        }
    }

    void bodyToMathML(MathML element) {
        MathML e1 = element.element("msup");
        try {
            Variable v = parameters[0].variableValue();
            if (v instanceof Fraction || v instanceof Pow || v instanceof Exp) {
                GenericVariable.valueOf(parameters[0]).toMathML(e1, null);
            } else parameters[0].toMathML(e1, null);
        } catch (NotVariableException e2) {
            try {
                Power o = parameters[0].powerValue();
                if (o.exponent() == 1) o.value(true).toMathML(e1, null);
                else GenericVariable.valueOf(parameters[0]).toMathML(e1, null);
            } catch (NotPowerException e3) {
                GenericVariable.valueOf(parameters[0]).toMathML(e1, null);
            }
        }
        parameters[1].toMathML(e1, null);
        element.appendChild(e1);
    }

    @Nonnull
    public Variable newInstance() {
        return new Pow(null, null);
    }
}
