<?php

namespace XF\AddOn;

use function intval, is_array;

/**
 * @property AddOn $addOn
 */
trait StepRunnerUpgradeTrait
{
	/**
	 * @param array $stepParams
	 *
	 * @return null|StepResult
	 */
	public function upgrade(array $stepParams = [])
	{
		$stepParams = $this->getStepParams($stepParams);

		$versions = [];
		$stepsGrouped = [];

		$reflection = new \ReflectionObject($this);
		foreach ($reflection->getMethods() AS $method)
		{
			if (preg_match('/^upgrade(\d+)Step(\d+)$/', $method->name, $match))
			{
				$versionId = intval($match[1]);
				$step = intval($match[2]);

				$versions[] = $versionId;
				$stepsGrouped[$versionId][$step] = $step;
			}
		}

		if (!$versions)
		{
			return null;
		}

		if (!$stepParams['version_id'])
		{
			$resume = $this->getUpgradeResumeStep();
			$stepParams['version_id'] = $resume[0];
			$stepParams['step'] = $resume[1];
		}

		$runStep = $stepParams['step'];
		if (!$runStep)
		{
			$runStep = 1;
		}

		$versions = array_unique($versions);
		sort($versions, SORT_NUMERIC);

		foreach ($versions AS $i => $versionId)
		{
			if ($versionId >= $stepParams['version_id'])
			{
				$versionSteps = $stepsGrouped[$versionId];
				if (isset($versionSteps[$runStep]))
				{
					if ($versionId != $stepParams['version_id'] || $runStep != $stepParams['step'])
					{
						// sanity check - don't pass params if we're changing versions or steps
						$stepParams = [];
					}

					$nextVersionId = $versions[$i + 1] ?? null;

					return $this->upgradeStepRunner($versionId, $runStep, $stepParams, $nextVersionId);
				}

				// if we hit here, then the step couldn't be found, so move onto the next upgrade
				$runStep = 1;
				$stepParams = $this->getStepParams();
			}
		}

		// if we got here, we don't have any other upgrades to run
		return null;
	}

	protected function getStepParams(array $stepParams = [])
	{
		return array_replace([
			'version_id' => 0,
			'step' => 0,
		], $stepParams);
	}

	private function getUpgradeResumeStep()
	{
		$installedAddOn = $this->addOn->getInstalledAddOn();
		$lastActionStep = $installedAddOn->getLastActionStep('upgrade');
		if ($lastActionStep)
		{
			$lastActionStep[1]++; // we completed this step, so need to resume with the next
			return $lastActionStep;
		}

		return [$this->addOn->getInstalledAddOn()->version_id + 1, 1];
	}

	/**
	 * @param integer $runVersion
	 * @param integer $runStep
	 * @param array $stepParams
	 * @param integer|null $nextVersionId
	 *
	 * @return null|StepResult
	 */
	private function upgradeStepRunner($runVersion, $runStep, array $stepParams, $nextVersionId)
	{
		$fnPattern = 'upgrade%dStep%d';
		$fn = sprintf($fnPattern, $runVersion, $runStep);
		if (!method_exists($this, $fn))
		{
			throw new \LogicException("Step $fn doesn't exist. Should have been checked earlier.");
		}

		$result = $this->$fn($stepParams);
		if ($result === null || $result === true)
		{
			$result = new StepResult(true);
		}
		else if (is_array($result))
		{
			$result = new StepResult(false, $result);
		}

		if (!($result instanceof StepResult))
		{
			throw new \LogicException("Must return a StepResult object");
		}

		$result->step = $runStep;
		$result->version = $runVersion;

		if ($result->complete)
		{
			$this->addOn->updatePendingAction('upgrade', $result->version . ':' . $result->step);

			$nextStep = $runStep + 1;
			$nextStepFn = sprintf($fnPattern, $runVersion, $nextStep);
			if (method_exists($this, $nextStepFn))
			{
				$result = new StepResult(false, [], $nextStep, $runVersion);
			}
			else
			{
				// we're done this upgrade version, see if we have another
				if ($nextVersionId === null)
				{
					return null;
				}

				$nextFn = sprintf($fnPattern, $nextVersionId, 1);
				if (!method_exists($this, $nextFn))
				{
					return null;
				}

				$result = new StepResult(false, [], 1, $nextVersionId);
			}
		}

		return $result;
	}
}
