/*
 * Copyright (C) 2014-2018 Mikhail Kulesh
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the GNU
 * General Public License as published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details. You should have received a copy of the GNU General
 * Public License along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package com.mkulesh.micromath.formula.terms;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.LinearLayout;

import com.mkulesh.micromath.formula.CalculaterTask;
import com.mkulesh.micromath.formula.CalculaterTask.CancelException;
import com.mkulesh.micromath.formula.Equation;
import com.mkulesh.micromath.formula.FormulaTerm;
import com.mkulesh.micromath.formula.Palette;
import com.mkulesh.micromath.formula.PaletteButton;
import com.mkulesh.micromath.formula.TermField;
import com.mkulesh.micromath.math.CalculatedValue;
import com.mkulesh.micromath.math.EquationArrayResult;
import com.mkulesh.micromath.plus.R;
import com.mkulesh.micromath.widgets.CustomEditText;
import com.mkulesh.micromath.widgets.CustomTextView;

import org.apache.commons.math3.util.FastMath;
import org.apache.commons.math3.util.Pair;

import java.util.Locale;

import javax.measure.unit.Unit;

public class Intervals extends FormulaTerm
{
    public TermTypeIf.GroupType getGroupType()
    {
        return TermTypeIf.GroupType.INTERVALS;
    }

    /**
     * Supported functions
     */
    public enum IntervalType implements TermTypeIf
    {
        EQUIDISTANT_INTERVAL(
                R.string.formula_quidistant_interval,
                R.drawable.p_equidistant_interval,
                R.string.math_equidistant_interval);

        private final int shortCutId;
        private final int imageId;
        private final int descriptionId;
        private final String lowerCaseName;

        IntervalType(int shortCutId, int imageId, int descriptionId)
        {
            this.shortCutId = shortCutId;
            this.imageId = imageId;
            this.descriptionId = descriptionId;
            this.lowerCaseName = name().toLowerCase(Locale.ENGLISH);
        }

        public GroupType getGroupType()
        {
            return GroupType.INTERVALS;
        }

        public int getShortCutId()
        {
            return shortCutId;
        }

        public int getImageId()
        {
            return imageId;
        }

        public int getDescriptionId()
        {
            return descriptionId;
        }

        public String getLowerCaseName()
        {
            return lowerCaseName;
        }

        public int getBracketId()
        {
            return Palette.NO_BUTTON;
        }

        public boolean isEnabled(CustomEditText field)
        {
            return field.isIntervalEnabled();
        }

        public PaletteButton.Category getPaletteCategory()
        {
            return PaletteButton.Category.TOP_LEVEL_TERM;
        }

        public FormulaTerm createTerm(
                TermField termField, LinearLayout layout, String s, int textIndex) throws Exception
        {
            return new Intervals(this, termField, layout, s, textIndex);
        }
    }

    /**
     * Private attributes
     */
    private TermField minValueTerm, nextValueTerm, maxValueTerm = null;

    // Attention: this is not thread-safety declaration!
    private final CalculatedValue minValue = new CalculatedValue(), nextValue = new CalculatedValue(),
            maxValue = new CalculatedValue();

    /*********************************************************
     * Constructors
     *********************************************************/

    private Intervals(IntervalType type, TermField owner, LinearLayout layout, String s, int idx) throws Exception
    {
        super(owner, layout);
        termType = type;
        inflateElements(R.layout.formula_interval, true);
        initializeElements(idx);
        if (minValueTerm == null || nextValueTerm == null || maxValueTerm == null)
        {
            throw new Exception("cannot initialize function terms");
        }
    }

    /*********************************************************
     * GUI constructors to avoid lint warning
     *********************************************************/

    public Intervals(Context context)
    {
        super();
    }

    public Intervals(Context context, AttributeSet attrs)
    {
        super();
    }

    /*********************************************************
     * Re-implementation for methods for FormulaBase and FormulaTerm superclass's
     *********************************************************/

    @Override
    public CalculatedValue.ValueType getValue(CalculaterTask thread, CalculatedValue outValue) throws CancelException
    {
        Pair<Unit, Integer> units = compareUnits(new TermField[]{ minValueTerm, nextValueTerm, maxValueTerm });
        if (units.getFirst() != null && units.getSecond() != 3)
        {
            return outValue.invalidate(CalculatedValue.ErrorType.INCOMPATIBLE_UNIT);
        }
        final Unit u = units.getFirst();

        if (getFormulaRoot() instanceof Equation)
        {
            minValue.processRealTerm(thread, minValueTerm);
            nextValue.processRealTerm(thread, nextValueTerm);
            maxValue.processRealTerm(thread, maxValueTerm);
            if (minValue.isNaN() || nextValue.isNaN() || maxValue.isNaN())
            {
                return outValue.invalidate(CalculatedValue.ErrorType.NOT_A_REAL);
            }
            final CalculatedValue calcDelta = getDelta(minValue.getReal(), nextValue.getReal(), maxValue.getReal());
            final CalculatedValue ravArg = ((Equation) getFormulaRoot()).getArgumentValue(0);
            if (calcDelta.isNaN() || ravArg.isNaN())
            {
                return outValue.invalidate(CalculatedValue.ErrorType.NOT_A_REAL);
            }
            final long idx = ravArg.getInteger();
            final int N = getNumberOfPoints(minValue.getReal(), maxValue.getReal(), calcDelta.getReal());
            if (idx == 0)
            {
                return outValue.setValue(minValue.getReal(), u);
            }
            else if (idx == N)
            {
                return outValue.setValue(maxValue.getReal(), u);
            }
            else if (idx > 0 && idx < N)
            {
                return outValue.setValue(minValue.getReal() + calcDelta.getReal() * (double) idx, u);
            }
        }
        return outValue.invalidate(CalculatedValue.ErrorType.TERM_NOT_READY);
    }

    @Override
    public DifferentiableType isDifferentiable(String var)
    {
        return DifferentiableType.NONE;
    }

    @Override
    public CalculatedValue.ValueType getDerivativeValue(String var, CalculaterTask thread, CalculatedValue outValue)
            throws CancelException
    {
        return outValue.invalidate(CalculatedValue.ErrorType.TERM_NOT_READY);
    }

    @Override
    protected CustomTextView initializeSymbol(CustomTextView v)
    {
        if (v.getText() != null)
        {
            String t = v.getText().toString();
            if (t.equals(getContext().getResources().getString(R.string.formula_left_bracket_key)))
            {
                v.prepare(CustomTextView.SymbolType.LEFT_SQR_BRACKET, getFormulaRoot().getFormulaList().getActivity(),
                        this);
                v.setText("."); // this text defines view width/height
            }
            else if (t.equals(getContext().getResources().getString(R.string.formula_right_bracket_key)))
            {
                v.prepare(CustomTextView.SymbolType.RIGHT_SQR_BRACKET, getFormulaRoot().getFormulaList().getActivity(),
                        this);
                v.setText("."); // this text defines view width/height
            }
            else if (t.equals(getContext().getResources().getString(R.string.formula_first_separator_key)))
            {
                v.prepare(CustomTextView.SymbolType.TEXT, getFormulaRoot().getFormulaList().getActivity(), this);
                v.setText(getContext().getResources().getString(R.string.formula_interval_first_separator));
            }
            else if (t.equals(getContext().getResources().getString(R.string.formula_second_separator_key)))
            {
                v.prepare(CustomTextView.SymbolType.TEXT, getFormulaRoot().getFormulaList().getActivity(), this);
                v.setText(getContext().getResources().getString(R.string.formula_interval_second_separator));
            }
        }
        return v;
    }

    @Override
    protected CustomEditText initializeTerm(CustomEditText v, LinearLayout l)
    {
        if (v.getText() != null)
        {
            if (v.getText().toString().equals(getContext().getResources().getString(R.string.formula_min_value_key)))
            {
                minValueTerm = addTerm(getFormulaRoot(), l, v, this, false);
                minValueTerm.bracketsType = TermField.BracketsType.NEVER;
            }
            else if (v.getText().toString()
                    .equals(getContext().getResources().getString(R.string.formula_next_value_key)))
            {
                nextValueTerm = addTerm(getFormulaRoot(), l, v, this, false);
                nextValueTerm.bracketsType = TermField.BracketsType.NEVER;
            }
            else if (v.getText().toString()
                    .equals(getContext().getResources().getString(R.string.formula_max_value_key)))
            {
                maxValueTerm = addTerm(getFormulaRoot(), l, v, this, false);
                maxValueTerm.bracketsType = TermField.BracketsType.NEVER;
            }
        }
        return v;
    }

    /*********************************************************
     * FormulaTermInterval-specific methods
     *********************************************************/

    private Pair<Unit, Integer> compareUnits(TermField[] terms)
    {
        Unit unit = terms[0].getParser().getUnit();
        int compatibleNumber = 0;
        for (TermField t : terms)
        {
            final Unit tp = t.getParser().getUnit();
            if (unit != null && tp != null && unit.isCompatible(tp))
            {
                compatibleNumber++;
            }
            if (unit == null)
            {
                unit = tp;
            }
        }
        return new Pair<>(unit == null ? null : unit.getStandardUnit(), compatibleNumber);
    }

    /**
     * Procedure returns declared interval if this root formula represents an interval
     */
    public void getInterval(EquationArrayResult arrayResult, CalculaterTask thread) throws CancelException
    {
        Pair<Unit, Integer> units = compareUnits(new TermField[]{ minValueTerm, nextValueTerm, maxValueTerm });
        if (units.getFirst() != null && units.getSecond() != 3)
        {
            return;
        }

        minValue.processRealTerm(thread, minValueTerm);
        nextValue.processRealTerm(thread, nextValueTerm);
        maxValue.processRealTerm(thread, maxValueTerm);
        if (minValue.isNaN() || nextValue.isNaN() || maxValue.isNaN())
        {
            return;
        }
        final CalculatedValue calcDelta = getDelta(minValue.getReal(), nextValue.getReal(), maxValue.getReal());
        if (calcDelta.isNaN())
        {
            return;
        }
        final int N = getNumberOfPoints(minValue.getReal(), maxValue.getReal(), calcDelta.getReal());
        arrayResult.resize1D(N + 1);
        for (int idx = 0; idx <= N; idx++)
        {
            if (thread != null)
            {
                thread.checkCancelation();
            }
            final CalculatedValue cv = arrayResult.getValue1D(idx);
            if (idx == 0)
            {
                cv.setValue(minValue.getReal(), units.getFirst());
            }
            else if (idx == N)
            {
                cv.setValue(maxValue.getReal(), units.getFirst());
            }
            else
            {
                final double val = minValue.getReal() + calcDelta.getReal() * (double) idx;
                cv.setValue(val, units.getFirst());
            }
        }
    }

    /**
     * Procedure checks and returns delta value
     */
    private CalculatedValue getDelta(final double min, final double next, final double max)
    {
        final CalculatedValue calcVal = new CalculatedValue();
        if (next <= min || max < next)
        {
            // error: invalid boundaries
            calcVal.invalidate(CalculatedValue.ErrorType.NOT_A_NUMBER);
        }
        else
        {
            calcVal.setValue(next - min);
        }
        return calcVal;
    }

    private int getNumberOfPoints(double min, double max, double delta)
    {
        int N = (int) FastMath.ceil(((max - min) / delta));
        if (N > 0 && min + delta * (double) N > max + delta / 2)
        {
            N--;
        }
        return N;
    }
}