package ee.futur.easygiantsfoundry;

//import java.util.ArrayList;
//import java.util.List;

import ee.futur.easygiantsfoundry.enums.Stage;
import lombok.Value;

/**
 * Solves the heating/cooling action and predicts tick duration (index)
 * the naming convention is focused on the algorithm rather than in-game terminology for the context.
 * <p>
 * the dx_n refers to successive derivatives of an ordinary-differential-equations
 * https://en.wikipedia.org/wiki/Ordinary_differential_equation
 * also known as position (dx0), speed (dx1), and acceleration (dx2).
 * <p>
 * dx0 - players current heat at tick
 * dx1 - dx0_current - dx0_last_tick, aka the first derivative
 * dx2 - dx1_current - dx1_last_tick, aka the second derivative
 * <p>
 * for context, here's what dx1 extracted directly from in-game dunking looks like.
 * the purpose of the HeatActionSolver.java is to accurately model this data.
 * int[] dx1 = {
 * 7,
 * 8,
 * 9,
 * 11,
 * 13,
 * 15,
 * 17,
 * 19,
 * 21,
 * 24,
 * 27, -- dunk/quench starts here
 * 30,
 * 33,
 * 37,
 * 41,
 * 45,
 * 49,
 * 53,
 * 57,
 * 62,
 * 67,
 * 72,
 * 77,
 * 83,
 * 89,
 * 95,
 * 91,
 * };
 */

/* The following code-snippet can be copy-pasted into runelite developer-tools "Shell" to extract dx1

import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Clipboard;

import java.util.concurrent.atomic.AtomicInteger;

int HEAT_ID = 13948;

AtomicInteger tickCounter = new AtomicInteger(-1);
AtomicInteger prevHeat = new AtomicInteger(client.getVarbitValue(HEAT_ID));


Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();

String output = "";

subscribe(VarbitChanged.class, ev ->
{
	if (ev.getVarbitId() == HEAT_ID)
	{
		int deltaHeat = ev.getValue() - prevHeat.getAndSet(ev.getValue());

		if (deltaHeat == -1) return; // ignore passive drain

		String str = "[" + tickCounter.incrementAndGet()
			+ "] deltaHeat: " + deltaHeat;
		log.info(str);
		output = output + deltaHeat + "\n";
		StringSelection selection = new StringSelection(output);
		clipboard.setContents(selection, selection);
	}
});
*/

public class HeatActionSolver
{

	public static final int[] DX_1 = new int[]{
		7,
		8,
		9,
		11,
		13,
		15,
		17,
		19,
		21,
		24,
		27, // -- dunk/quench starts here
		30,
		33,
		37,
		41,
		45,
		49,
		53,
		57,
		62,
		67,
		72,
		77,
		83,
		89,
		95,
		91, // last one will always overshoot 1000
	};


	// index is stage, ordinal order
	public static final int[] TOOL_TICK_CYCLE = new int[] {
		5,
		2,
		2
	};

	public static final int MAX_INDEX = DX_1.length;
	public static final int FAST_INDEX = 10;

	@Value(staticConstructor = "of")
	public static class SolveResult
	{
		int index;
		int dx0;
		int dx1;
		int dx2;
	}

	private static SolveResult heatingSolve(int start, int goal, boolean overshoot, int max, boolean isFast)
	{
		return relativeSolve(goal - start, overshoot, max - start, isFast, -1);
	}

	private static SolveResult coolingSolve(int start, int goal, boolean overshoot, int min, boolean isFast)
	{
		return relativeSolve(start - goal, overshoot, start - min, isFast, 1);
	}

	private static SolveResult relativeSolve(int goal, boolean overshoot, int max, boolean isFast, int decayValue)
	{

		int index = isFast ? FAST_INDEX : 0;
		int dx0 = 0;

		boolean decay = false;

		while (true) {

			if (index > MAX_INDEX)
			{
				break;
			}

			if (!overshoot && dx0 + DX_1[index] > goal)
			{
				break;
			}
			else if (overshoot && dx0 >= goal)
			{
				break;
			}

			if (dx0 + DX_1[index] >= max)
			{
				break;
			}

			if (decay)
			{
				dx0 -= decayValue;
			}


			dx0 += DX_1[index];
			++index;
			decay = !decay;
		}

		if (isFast)
		{
			index -= FAST_INDEX;
		}

		return SolveResult.of(index, dx0, DX_1[index], -1);
	}


	@Value(staticConstructor = "of")
	public static class DurationResult
	{
		int duration;
		boolean goalInRange;
		boolean overshooting;
		int predictedHeat;
	}

	public static DurationResult solve(
		Stage stage,
		int[] range,
		int actionLeftInStage,
		int start,
		boolean isFast,
		boolean isActionHeating,
		int paddingTicks,
		boolean isRunning)
	{

		final boolean isStageHeating = stage.isHeating();
		// adding tool cycle ticks because the first cycle at a tool is almost always nulled
		// (unless manually reaching the tile, then clicking the tool)
		final int toolDelay = TOOL_TICK_CYCLE[stage.ordinal()];
		final int travelTicks = solveTravelTicks(isRunning, stage, isActionHeating) + toolDelay;
		final int travelDecay = (int) Math.ceil((double) travelTicks / 2);

		final int paddingDecay = (int) Math.ceil((double) paddingTicks / 2);

		// adding 2.4s/8ticks worth of padding so preform doesn't decay out of range
		// average distance from lava+waterfall around 8 ticks
		// preform decays 1 heat every 2 ticks
		final int min = Math.max(0, Math.min(1000, range[0] + paddingDecay + travelDecay));
		final int max = Math.max(0, Math.min(1000, range[1] + paddingDecay + travelDecay));

		final int actionsLeft_DeltaHeat = actionLeftInStage * stage.getHeatChange();

		int estimatedDuration = 0;

		final boolean goalInRange;
		boolean overshoot = false;

		SolveResult result = null;

		// case actions are all cooling, heating is mirrored version

		// goal: in-range // stage: heating
		// overshoot goal
		//  <----------|stop|<---------------- heat
		// ------|min|----------goal-------|max|
		//                      stage ---------------->

		// goal: out-range // stage: heating
		// undershoot min
		// ...----------|stop|<--------------------- heat
		// -goal---|min|---------------------|max|
		//                      stage ----------------->

		// goal: in-range // stage: cooling
		// undershoot goal
		//   <-------------------------|stop|<--------------- heat
		// ------|min|----------goal-------|max|
		//    <---------------- stage

		// goal: out-range // stage: cooling
		// overshoot max
		//    <--------------------|stop|<--------------- heat
		// --------|min|---------------------|max|----goal
		//    <---------------- stage

		if (isActionHeating)
		{
			int goal = min - actionsLeft_DeltaHeat;
			goalInRange = goal >= min && goal <= max;

			if (isStageHeating)
			{

				if (start <= max)
				{
					overshoot = !goalInRange;

					if (!goalInRange)
					{
						goal = min;
					}

					result = heatingSolve(start, goal, overshoot, max, isFast);

					estimatedDuration = result.index;
				}
			}
			else // cooling stage
			{
				// actionsLeft_DeltaHeat is negative here
				if (start <= max)
				{
					overshoot = goalInRange;

					if (!goalInRange)
					{
						goal = max;
					}

					result = heatingSolve(start, goal, overshoot, max, isFast);

					estimatedDuration = result.index;
				}
			}
		}
		else // cooling action
		{
			int goal = max - actionsLeft_DeltaHeat;
			goalInRange = goal >= min && goal <= max;

			if (isStageHeating)
			{
				if (start >= min)
				{
					overshoot = goalInRange;

					if (!goalInRange)
					{
						goal = min;
					}

					result = coolingSolve(start, goal, overshoot, min, isFast);

					estimatedDuration = result.index;
				}
			}
			else // cooling stage cooling action
			{
				if (start >= min)
				{
					overshoot = !goalInRange;
					if (!goalInRange)
					{
						goal = max;
					}

					result = coolingSolve(start, goal, overshoot, min, isFast);

					estimatedDuration = result.index;
				}
			}
		}

		int dx0 = result == null ? 0 : result.dx0;
		if (!isActionHeating)
		{
			dx0 *= -1;
		}


		return DurationResult.of(estimatedDuration, goalInRange, overshoot, start + dx0);
	}

	private static int solveTravelTicks(boolean isRunning, Stage stage, boolean isLava)
	{
		final int distance;
		if (isLava)
		{
			distance = stage.getDistanceToLava();
		}
		else
		{
			distance = stage.getDistanceToWaterfall();
		}

		if (isRunning)
		{
			// for odd distances, like 7
			// 7 / 2 = 3.5
			// rounded to 4
			return (int) Math.ceil((double) distance / 2);
		}
		else
		{
			return distance;
		}
	}
}

