package it.cavallium.warppi.math;

import it.cavallium.warppi.math.rules.Rule;
import it.cavallium.warppi.util.Error;
import it.cavallium.warppi.util.Errors;
import it.cavallium.warppi.util.Utils;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;

import java.util.Objects;

public abstract class FunctionOperator implements Function {
	
	/**
	 * Create a new instance of FunctionOperator. The Math Context will be the
	 * same of <strong>value1</strong>'s.
	 *
	 * @throws NullPointerException
	 *             when value1 is null.
	 * @param value1
	 *            The parameter of this function.
	 * @param value2
	 *            The parameter of this function.
	 */
	public FunctionOperator(final Function value1, final Function value2) throws NullPointerException {
		mathContext = value1.getMathContext();
		parameter1 = value1;
		parameter2 = value2;
	}

	/**
	 * Create a new instance of FunctionOperator.
	 *
	 * @param value1
	 *            The parameter of this function.
	 * @param value2
	 *            The parameter of this function.
	 */
	public FunctionOperator(final MathContext mc, final Function value1, final Function value2) {
		mathContext = mc;
		parameter1 = value1;
		parameter2 = value2;
	}

	protected final MathContext mathContext;

	protected Function parameter1 = null;
	protected Function parameter2 = null;

	/**
	 *
	 * @return First parameter.
	 */
	public Function getParameter1() {
		return parameter1;
	}

	/**
	 *
	 * @return Second parameter.
	 */
	public Function getParameter2() {
		return parameter2;
	}

	/**
	 *
	 * @param var
	 *            First parameter.
	 * @return A new instance of this function.
	 */
	public FunctionOperator setParameter1(final Function var) {
		final FunctionOperator s = clone();
		s.parameter1 = var;
		return s;
	}

	/**
	 *
	 * @param var
	 *            Second parameter.
	 * @return A new instance of this function.
	 */
	public FunctionOperator setParameter2(final Function var) {
		final FunctionOperator s = clone();
		s.parameter2 = var;
		return s;
	}

	@Override
	public FunctionOperator setParameter(final int index, final Function var) throws IndexOutOfBoundsException {
		switch (index) {
			case 0:
				return setParameter1(var);
			case 1:
				return setParameter2(var);
			default:
				throw new IndexOutOfBoundsException();
		}
	}

	@Override
	public Function getParameter(final int index) throws IndexOutOfBoundsException {
		switch (index) {
			case 0:
				return getParameter1();
			case 1:
				return getParameter2();
			default:
				throw new IndexOutOfBoundsException();
		}
	}

	@Override
	public MathContext getMathContext() {
		return mathContext;
	}

	@Override
	public final ObjectArrayList<Function> simplify(final Rule rule) throws Error, InterruptedException {
		if (Thread.interrupted()) {
			throw new InterruptedException();
		}

		final ObjectArrayList<Function> simplifiedParam1 = parameter1.simplify(rule);
		final ObjectArrayList<Function> simplifiedParam2 = parameter2.simplify(rule);
		try {
			if (simplifiedParam1 == null & simplifiedParam2 == null) {
				return rule.execute(this);
			}
		} catch (final Exception e) {
			final Error err = new Error(Errors.ERROR, "Error while executing rule '" + rule.getRuleName() + "'!\n" + e.getMessage());
			err.initCause(e);
			throw err;
		}

		if (Thread.interrupted()) {
			throw new InterruptedException();
		}
		final ObjectArrayList<Function> result = new ObjectArrayList<>();

		final ObjectArrayList<Function> l1 = new ObjectArrayList<>();
		final ObjectArrayList<Function> l2 = new ObjectArrayList<>();
		if (Thread.interrupted()) {
			throw new InterruptedException();
		}
		if (simplifiedParam1 == null) {
			l1.add(parameter1);
		} else {
			if (Thread.interrupted()) {
				throw new InterruptedException();
			}
			l1.addAll(simplifiedParam1);
		}
		if (Thread.interrupted()) {
			throw new InterruptedException();
		}
		if (simplifiedParam2 == null) {
			l2.add(parameter2);
		} else {
			if (Thread.interrupted()) {
				throw new InterruptedException();
			}
			l2.addAll(simplifiedParam2);
		}

		final Function[][] results = Utils.joinFunctionsResults(l1, l2);

		for (final Function[] f : results) {
			result.add(setParameter1(f[0]).setParameter2(f[1]));
		}

		return result;
	}

	@Override
	public abstract FunctionOperator clone();

	@Override
	public int hashCode() {
		return Objects.hash(parameter1, parameter2);
	}

	@Override
	public abstract boolean equals(Object o);

	@Override
	public String toString() {
		return this.getClass().getSimpleName() + "(" + getParameter1() + "," + getParameter2() + ")";
	}
}