<?php

/**
 * Interface that all minion tasks must implement
 *
 * @package    KO7/Minion
 * @author     Kohana Team
 * @copyright  (c) Kohana Team
 * @license    https://koseven.ga/LICENSE.md
 */
abstract class KO7_Minion_Task {

	/**
	 * The separator used to separate different levels of tasks
	 * @var string
	 */
	public static $task_separator = ':';

	/**
	 * Converts a task (e.g. db:migrate to a class name)
	 *
	 * @param string  Task name
	 * @return string Class name
	 */
	public static function convert_task_to_class_name($task)
	{
		$task = trim($task);

		if (empty($task))
			return '';

		return 'Task_'.implode('_', array_map('ucfirst', explode(Minion_Task::$task_separator, $task)));
	}

	/**
	 * Gets the task name of a task class / task object
	 *
	 * @param  string|Minion_Task The task class / object
	 * @return string             The task name
	 */
	public static function convert_class_to_task($class)
	{
		if (is_object($class))
		{
			$class = get_class($class);
		}

		return strtolower(str_replace('_', Minion_Task::$task_separator, substr($class, 5)));
	}

	/**
	 * Factory for loading minion tasks
	 *
	 * @param  array An array of command line options. It should contain the 'task' key
	 * @throws Minion_Exception_InvalidTask
	 * @return Minion_Task The Minion task
	 */
	public static function factory($options)
	{
		if (($task = Arr::get($options, 'task')) !== NULL)
		{
			unset($options['task']);
		}
		else if (($task = Arr::get($options, 0)) !== NULL)
		{
			// The first positional argument (aka 0) may be the task name
			unset($options[0]);
		}
		else
		{
			// If we didn't get a valid task, generate the help
			$task = 'help';
		}

		$class = Minion_Task::convert_task_to_class_name($task);

		if ( ! class_exists($class))
		{
			throw new Minion_Exception_InvalidTask(
				"Task ':task' is not a valid minion task",
				[':task' => $class]
			);
		}

		$class = new $class;

		if ( ! $class instanceof Minion_Task)
		{
			throw new Minion_Exception_InvalidTask(
				"Task ':task' is not a valid minion task",
				[':task' => $class]
			);
		}

		$class->set_options($options);

		// Show the help page for this task if requested
		if (array_key_exists('help', $options))
		{
			$class->_method = '_help';
		}

		return $class;
	}

	/**
	 * The list of options this task accepts and their default values.
	 *
	 *     protected $_options = array(
	 *         'limit' => 4,
	 *         'table' => NULL,
	 *     );
	 *
	 * @var array
	 */
	protected $_options = [];

	/**
	 * Populated with the accepted options for this task.
	 * This array is automatically populated based on $_options.
	 *
	 * @var array
	 */
	protected $_accepted_options = [];

	protected $_method = '_execute';

	protected function __construct()
	{
		// Populate $_accepted_options based on keys from $_options
		$this->_accepted_options = array_keys($this->_options);
	}

	/**
	 * The file that get's passes to Validation::errors() when validation fails
	 * @var string|NULL
	 */
	protected $_errors_file = 'validation';

	/**
	 * Gets the task name for the task
	 *
	 * @return string
	 */
	public function __toString()
	{
		static $task_name = NULL;

		if ($task_name === NULL)
		{
			$task_name = Minion_Task::convert_class_to_task($this);
		}

		return $task_name;
	}

	/**
	 * Sets options for this task
	 *
	 * $param  array  the array of options to set
	 * @return this
	 */
	public function set_options(array $options)
	{
		foreach ($options as $key => $value)
		{
			$this->_options[$key] = $value;
		}

		return $this;
	}

	/**
	 * Get the options that were passed into this task with their defaults
	 *
	 * @return array
	 */
	public function get_options()
	{
		return (array) $this->_options;
	}

	/**
	 * Get a set of options that this task can accept
	 *
	 * @return array
	 */
	public function get_accepted_options()
	{
		return (array) $this->_accepted_options;
	}

	/**
	 * Adds any validation rules/labels for validating _options
	 *
	 *     public function build_validation(Validation $validation)
	 *     {
	 *         return parent::build_validation($validation)
	 *             ->rule('paramname', 'not_empty'); // Require this param
	 *     }
	 *
	 * @param  Validation   the validation object to add rules to
	 *
	 * @return Validation
	 */
	public function build_validation(Validation $validation)
	{
		// Add a rule to each key making sure it's in the task
		foreach ($validation->data() as $key => $value)
		{
			$validation->rule($key, [$this, 'valid_option'], [':validation', ':field']);
		}

		return $validation;
	}

	/**
	 * Returns $_errors_file
	 *
	 * @return string
	 */
	public function get_errors_file()
	{
		return $this->_errors_file;
	}

	/**
	 * Execute the task with the specified set of options
	 *
	 * @return null
	 */
	public function execute()
	{
		$options = $this->get_options();

		// Validate $options
		$validation = Validation::factory($options);
		$validation = $this->build_validation($validation);

		if ( $this->_method != '_help' AND ! $validation->check())
		{
			echo View::factory('minion/error/validation')
				->set('task', Minion_Task::convert_class_to_task($this))
				->set('errors', $validation->errors($this->get_errors_file()));
		}
		else
		{
			// Finally, run the task
			$method = $this->_method;
			echo $this->{$method}($options);
		}
	}

	abstract protected function _execute(array $params);

	/**
	 * Outputs help for this task
	 *
	 * @return null
	 */
	protected function _help(array $params)
	{
		$tasks = $this->_compile_task_list(KO7::list_files('classes/task'));

		$inspector = new ReflectionClass($this);

		list($description, $tags) = $this->_parse_doccomment($inspector->getDocComment());

		$view = View::factory('minion/help/task')
			->set('description', $description)
			->set('tags', (array) $tags)
			->set('task', Minion_Task::convert_class_to_task($this));

		echo $view;
	}


	public function valid_option(Validation $validation, $option)
	{
		if ( ! in_array($option, $this->_accepted_options))
		{
			$validation->error($option, 'minion_option');
		}
	}

	/**
	 * Parses a doccomment, extracting both the comment and any tags associated
	 *
	 * Based on the code in Kodoc::parse()
	 *
	 * @param string The comment to parse
	 * @return array First element is the comment, second is an array of tags
	 */
	protected function _parse_doccomment($comment)
	{
		// Normalize all new lines to \n
		$comment = str_replace(["\r\n", "\n"], "\n", $comment);

		// Remove the phpdoc open/close tags and split
		$comment = array_slice(explode("\n", $comment), 1, -1);

		// Tag content
		$tags = [];

		foreach ($comment as $i => $line)
		{
			// Remove all leading whitespace
			$line = preg_replace('/^\s*\* ?/m', '', $line);

			// Search this line for a tag
			if (preg_match('/^@(\S+)(?:\s*(.+))?$/', $line, $matches))
			{
				// This is a tag line
				unset($comment[$i]);

				$name = $matches[1];
				$text = isset($matches[2]) ? $matches[2] : '';

				$tags[$name] = $text;
			}
			else
			{
				$comment[$i] = (string) $line;
			}
		}

		$comment = trim(implode("\n", $comment));

		return [$comment, $tags];
	}

	/**
	 * Compiles a list of available tasks from a directory structure
	 *
	 * @param  array Directory structure of tasks
	 * @param  string prefix
	 * @return array Compiled tasks
	 */
	protected function _compile_task_list(array $files, $prefix = '')
	{
		$output = [];

		foreach ($files as $file => $path)
		{
			$file = substr($file, strrpos($file, DIRECTORY_SEPARATOR) + 1);

			if (is_array($path) AND count($path))
			{
				$task = $this->_compile_task_list($path, $prefix.$file.Minion_Task::$task_separator);

				if ($task)
				{
					$output = array_merge($output, $task);
				}
			}
			else
			{
				$output[] = strtolower($prefix.substr($file, 0, -strlen(EXT)));
			}
		}

		return $output;
	}

}
