<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

namespace yii\gii;

use Yii;
use ReflectionClass;
use yii\base\InvalidConfigException;
use yii\base\Model;
use yii\web\View;


/**
 * This is the base class for all generator classes.
 *
 * A generator instance is responsible for taking user inputs, validating them,
 * and using them to generate the corresponding code based on a set of code template files.
 *
 * A generator class typically needs to implement the following methods:
 *
 * - [[getName()]]: returns the name of the generator
 * - [[getDescription()]]: returns the detailed description of the generator
 * - [[generate()]]: generates the code based on the current user input and the specified code template files.
 *   This is the place where main code generation code resides.
 *
 * @property string $description The detailed description of the generator. This property is read-only.
 * @property string $stickyDataFile The file path that stores the sticky attribute values. This property is
 * read-only.
 * @property string $templatePath The root path of the template files that are currently being used. This
 * property is read-only.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @since 2.0
 */
abstract class Generator extends Model
{
	/**
	 * @var array a list of available code templates. The array keys are the template names,
	 * and the array values are the corresponding template paths or path aliases.
	 */
	public $templates = [];
	/**
	 * @var string the name of the code template that the user has selected.
	 * The value of this property is internally managed by this class.
	 */
	public $template;

	/**
	 * @return string name of the code generator
	 */
	abstract public function getName();
	/**
	 * Generates the code based on the current user input and the specified code template files.
	 * This is the main method that child classes should implement.
	 * Please refer to [[\yii\gii\generators\controller\Generator::generate()]] as an example
	 * on how to implement this method.
	 * @return CodeFile[] a list of code files to be created.
	 */
	abstract public function generate();

	/**
	 * @inheritdoc
	 */
	public function init()
	{
		parent::init();
		if (!isset($this->templates['default'])) {
			$this->templates['default'] = $this->defaultTemplate();
		}
		foreach ($this->templates as $i => $template) {
			$this->templates[$i] = Yii::getAlias($template);
		}
	}

	/**
	 * Returns a list of code template files that are required.
	 * Derived classes usually should override this method if they require the existence of
	 * certain template files.
	 * @return array list of code template files that are required. They should be file paths
	 * relative to [[templatePath]].
	 */
	public function requiredTemplates()
	{
		return [];
	}

	/**
	 * Returns the list of sticky attributes.
	 * A sticky attribute will remember its value and will initialize the attribute with this value
	 * when the generator is restarted.
	 * @return array list of sticky attributes
	 */
	public function stickyAttributes()
	{
		return ['template'];
	}

	/**
	 * Returns the list of hint messages.
	 * The array keys are the attribute names, and the array values are the corresponding hint messages.
	 * Hint messages will be displayed to end users when they are filling the form for the generator.
	 * @return array the list of hint messages
	 */
	public function hints()
	{
		return [];
	}

	/**
	 * Returns the list of auto complete values.
	 * The array keys are the attribute names, and the array values are the corresponding auto complete values.
	 * Auto complete values can also be callable typed in order one want to make postponed data generation.
	 * @return array the list of auto complete values
	 */
	public function autoCompleteData()
	{
		return [];
	}

	/**
	 * Returns the message to be displayed when the newly generated code is saved successfully.
	 * Child classes may override this method to customize the message.
	 * @return string the message to be displayed when the newly generated code is saved successfully.
	 */
	public function successMessage()
	{
		return 'The code has been generated successfully.';
	}

	/**
	 * Returns the view file for the input form of the generator.
	 * The default implementation will return the "form.php" file under the directory
	 * that contains the generator class file.
	 * @return string the view file for the input form of the generator.
	 */
	public function formView()
	{
		$class = new ReflectionClass($this);
		return dirname($class->getFileName()) . '/form.php';
	}

	/**
	 * Returns the root path to the default code template files.
	 * The default implementation will return the "templates" subdirectory of the
	 * directory containing the generator class file.
	 * @return string the root path to the default code template files.
	 */
	public function defaultTemplate()
	{
		$class = new ReflectionClass($this);
		return dirname($class->getFileName()) . '/templates';
	}

	/**
	 * @return string the detailed description of the generator.
	 */
	public function getDescription()
	{
		return '';
	}

	/**
	 * @inheritdoc
	 *
	 * Child classes should override this method like the following so that the parent
	 * rules are included:
	 *
	 * ~~~
	 * return array_merge(parent::rules(), [
	 *     ...rules for the child class...
	 * ]);
	 * ~~~
	 */
	public function rules()
	{
		return [
			[['template'], 'required', 'message' => 'A code template must be selected.'],
			[['template'], 'validateTemplate'],
		];
	}

	/**
	 * Loads sticky attributes from an internal file and populates them into the generator.
	 * @internal
	 */
	public function loadStickyAttributes()
	{
		$stickyAttributes = $this->stickyAttributes();
		$attributes[] = 'template';
		$path = $this->getStickyDataFile();
		if (is_file($path)) {
			$result = json_decode(file_get_contents($path), true);
			if (is_array($result)) {
				foreach ($stickyAttributes as $name) {
					if (isset($result[$name])) {
						$this->$name = $result[$name];
					}
				}
			}
		}
	}

	/**
	 * Saves sticky attributes into an internal file.
	 * @internal
	 */
	public function saveStickyAttributes()
	{
		$stickyAttributes = $this->stickyAttributes();
		$stickyAttributes[] = 'template';
		$values = [];
		foreach ($stickyAttributes as $name) {
			$values[$name] = $this->$name;
		}
		$path = $this->getStickyDataFile();
		@mkdir(dirname($path), 0755, true);
		file_put_contents($path, json_encode($values));
	}

	/**
	 * @return string the file path that stores the sticky attribute values.
	 * @internal
	 */
	public function getStickyDataFile()
	{
		return Yii::$app->getRuntimePath() . '/gii-' . Yii::getVersion() . '/' . str_replace('\\', '-', get_class($this)) . '.json';
	}

	/**
	 * Saves the generated code into files.
	 * @param CodeFile[] $files the code files to be saved
	 * @param array $answers
	 * @param string $results this parameter receives a value from this method indicating the log messages
	 * generated while saving the code files.
	 * @return boolean whether there is any error while saving the code files.
	 */
	public function save($files, $answers, &$results)
	{
		$lines = ['Generating code using template "' . $this->getTemplatePath() . '"...'];
		$hasError = false;
		foreach ($files as $file) {
			$relativePath = $file->getRelativePath();
			if (isset($answers[$file->id]) && $file->operation !== CodeFile::OP_SKIP) {
				$error = $file->save();
				if (is_string($error)) {
					$hasError = true;
					$lines[] = "generating $relativePath\n<span class=\"error\">$error</span>";
				} else {
					$lines[] = $file->operation === CodeFile::OP_CREATE ? " generated $relativePath" : " overwrote $relativePath";
				}
			} else {
				$lines[] = "   skipped $relativePath";
			}
		}
		$lines[] = "done!\n";
		$results = implode("\n", $lines);

		return $hasError;
	}

	/**
	 * @return string the root path of the template files that are currently being used.
	 * @throws InvalidConfigException if [[template]] is invalid
	 */
	public function getTemplatePath()
	{
		if (isset($this->templates[$this->template])) {
			return $this->templates[$this->template];
		} else {
			throw new InvalidConfigException("Unknown template: {$this->template}");
		}
	}

	/**
	 * Generates code using the specified code template and parameters.
	 * Note that the code template will be used as a PHP file.
	 * @param string $template the code template file. This must be specified as a file path
	 * relative to [[templatePath]].
	 * @param array $params list of parameters to be passed to the template file.
	 * @return string the generated code
	 */
	public function render($template, $params = [])
	{
		$view = new View;
		$params['generator'] = $this;
		return $view->renderFile($this->getTemplatePath() . '/' . $template, $params, $this);
	}

	/**
	 * Validates the template selection.
	 * This method validates whether the user selects an existing template
	 * and the template contains all required template files as specified in [[requiredTemplates()]].
	 */
	public function validateTemplate()
	{
		$templates = $this->templates;
		if (!isset($templates[$this->template])) {
			$this->addError('template', 'Invalid template selection.');
		} else {
			$templatePath = $this->templates[$this->template];
			foreach ($this->requiredTemplates() as $template) {
				if (!is_file($templatePath . '/' . $template)) {
					$this->addError('template', "Unable to find the required code template file '$template'.");
				}
			}
		}
	}

	/**
	 * An inline validator that checks if the attribute value refers to an existing class name.
	 * If the `extends` option is specified, it will also check if the class is a child class
	 * of the class represented by the `extends` option.
	 * @param string $attribute the attribute being validated
	 * @param array $params the validation options
	 */
	public function validateClass($attribute, $params)
	{
		$class = $this->$attribute;
		try {
			if (class_exists($class)) {
				if (isset($params['extends'])) {
					if (ltrim($class, '\\') !== ltrim($params['extends'], '\\') && !is_subclass_of($class, $params['extends'])) {
						$this->addError($attribute, "'$class' must extend from {$params['extends']} or its child class.");
					}
				}
			} else {
				$this->addError($attribute, "Class '$class' does not exist or has syntax error.");
			}
		} catch (\Exception $e) {
			$this->addError($attribute, "Class '$class' does not exist or has syntax error.");
		}
	}

	/**
	 * An inline validator that checks if the attribute value refers to a valid namespaced class name.
	 * The validator will check if the directory containing the new class file exist or not.
	 * @param string $attribute the attribute being validated
	 * @param array $params the validation options
	 */
	public function validateNewClass($attribute, $params)
	{
		$class = ltrim($this->$attribute, '\\');
		if (($pos = strrpos($class, '\\')) === false) {
			$this->addError($attribute, "The class name must contain fully qualified namespace name.");
		} else {
			$ns = substr($class, 0, $pos);
			$path = Yii::getAlias('@' . str_replace('\\', '/', $ns), false);
			if ($path === false) {
				$this->addError($attribute, "The class namespace is invalid: $ns");
			} elseif (!is_dir($path)) {
				$this->addError($attribute, "Please make sure the directory containing this class exists: $path");
			}
		}
	}

	/**
	 * @param string $value the attribute to be validated
	 * @return boolean whether the value is a reserved PHP keyword.
	 */
	public function isReservedKeyword($value)
	{
		static $keywords = [
			'__class__',
			'__dir__',
			'__file__',
			'__function__',
			'__line__',
			'__method__',
			'__namespace__',
			'__trait__',
			'abstract',
			'and',
			'array',
			'as',
			'break',
			'case',
			'catch',
			'callable',
			'cfunction',
			'class',
			'clone',
			'const',
			'continue',
			'declare',
			'default',
			'die',
			'do',
			'echo',
			'else',
			'elseif',
			'empty',
			'enddeclare',
			'endfor',
			'endforeach',
			'endif',
			'endswitch',
			'endwhile',
			'eval',
			'exception',
			'exit',
			'extends',
			'final',
			'finally',
			'for',
			'foreach',
			'function',
			'global',
			'goto',
			'if',
			'implements',
			'include',
			'include_once',
			'instanceof',
			'insteadof',
			'interface',
			'isset',
			'list',
			'namespace',
			'new',
			'old_function',
			'or',
			'parent',
			'php_user_filter',
			'print',
			'private',
			'protected',
			'public',
			'require',
			'require_once',
			'return',
			'static',
			'switch',
			'this',
			'throw',
			'trait',
			'try',
			'unset',
			'use',
			'var',
			'while',
			'xor',
		];
		return in_array(strtolower($value), $keywords, true);
	}
}
