<?php
/**
 * CCodeModel class file.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
 * @copyright Copyright &copy; 2008-2011 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

/**
 * CCodeModel is the base class for model classes that are used to generate code.
 *
 * Each code generator should have at least one code model class that extends from this class.
 * The purpose of a code model is to represent user-supplied parameters and use them to
 * generate customized code.
 *
 * Derived classes should implement the {@link prepare} method whose main task is to
 * fill up the {@link files} property based on the user parameters.
 *
 * The {@link files} property should be filled with a set of {@link CCodeFile} instances,
 * each representing a single code file to be generated.
 *
 * CCodeModel implements the feature of "sticky attributes". A sticky attribute is an attribute
 * that can remember its last valid value, even if the user closes his browser window
 * and reopen it. To declare an attribute is sticky, simply list it in a validation rule with
 * the validator name being "sticky".
 *
 * @property array $templates A list of available code templates (name=>directory).
 * @property string $templatePath The directory that contains the template files.
 * @property string $stickyFile The file path that stores the sticky attribute values.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id: CCodeModel.php 3426 2011-10-25 00:01:09Z alexander.makarow $
 * @package system.gii
 * @since 1.1.2
 */
abstract class CCodeModel extends CFormModel
{
	const STATUS_NEW=1;
	const STATUS_PREVIEW=2;
	const STATUS_SUCCESS=3;
	const STATUS_ERROR=4;

	/**
	 * @var array user confirmations on whether to overwrite existing code files with the newly generated ones.
	 * The value of this property is internally managed by this class and {@link CCodeGenerator}.
	 */
	public $answers;
	/**
	 * @var string the name of the code template that the user has selected.
	 * The value of this property is internally managed by this class and {@link CCodeGenerator}.
	 */
	public $template;
	/**
	 * @var array a list of {@link CCodeFile} objects that represent the code files to be generated.
	 * The {@link prepare()} method is responsible to populate this property.
	 */
	public $files=array();
	/**
	 * @var integer the status of this model. T
	 * The value of this property is internally managed by {@link CCodeGenerator}.
	 */
	public $status=self::STATUS_NEW;

	private $_stickyAttributes=array();

	/**
	 * Prepares the code files to be generated.
	 * This is the main method that child classes should implement. It should contain the logic
	 * that populates the {@link files} property with a list of code files to be generated.
	 */
	abstract public function prepare();

	/**
	 * Declares the model validation rules.
	 * Child classes must override this method in the following format:
	 * <pre>
	 * return array_merge(parent::rules(), array(
	 *     ...rules for the child class...
	 * ));
	 * </pre>
	 * @return array validation rules
	 */
	public function rules()
	{
		return array(
			array('template', 'required'),
			array('template', 'validateTemplate', 'skipOnError'=>true),
			array('template', 'sticky'),
		);
	}

	/**
	 * 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 {@link requiredTemplates}.
	 * @param string $attribute the attribute to be validated
	 * @param array $params validation parameters
	 */
	public function validateTemplate($attribute,$params)
	{
		$templates=$this->templates;
		if(!isset($templates[$this->template]))
			$this->addError('template', 'Invalid template selection.');
		else
		{
			$templatePath=$this->templatePath;
			foreach($this->requiredTemplates() as $template)
			{
				if(!is_file($templatePath.'/'.$template))
					$this->addError('template', "Unable to find the required code template file '$template'.");
			}
		}
	}

	/**
	 * Checks if the named class exists (in a case sensitive manner).
	 * @param string $name class name to be checked
	 * @return boolean whether the class exists
	 */
	public function classExists($name)
	{
		return class_exists($name,false) && in_array($name, get_declared_classes());
	}

	/**
	 * Declares the model attribute labels.
	 * Child classes must override this method in the following format:
	 * <pre>
	 * return array_merge(parent::attributeLabels(), array(
	 *     ...labels for the child class attributes...
	 * ));
	 * </pre>
	 * @return array the attribute labels
	 */
	public function attributeLabels()
	{
		return array(
			'template'=>'Code Template',
		);
	}

	/**
	 * Returns a list of code templates that are required.
	 * Derived classes usually should override this method.
	 * @return array list of code templates that are required. They should be file paths
	 * relative to {@link templatePath}.
	 */
	public function requiredTemplates()
	{
		return array();
	}

	/**
	 * Saves the generated code into files.
	 */
	public function save()
	{
		$result=true;
		foreach($this->files as $file)
		{
			if($this->confirmed($file))
				$result=$file->save() && $result;
		}
		return $result;
	}

	/**
	 * Returns the message to be displayed when the newly generated code is saved successfully.
	 * Child classes should override this method if the message needs to be customized.
	 * @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 message to be displayed when some error occurred during code file saving.
	 * Child classes should override this method if the message needs to be customized.
	 * @return string the message to be displayed when some error occurred during code file saving.
	 */
	public function errorMessage()
	{
		return 'There was some error when generating the code. Please check the following messages.';
	}

	/**
	 * Returns a list of available code templates (name=>directory).
	 * This method simply returns the {@link CCodeGenerator::templates} property value.
	 * @return array a list of available code templates (name=>directory).
	 */
	public function getTemplates()
	{
		return Yii::app()->controller->templates;
	}

	/**
	 * @return string the directory that contains the template files.
	 * @throw CException if {@link templates} is empty or template selection is invalid
	 */
	public function getTemplatePath()
	{
		$templates=$this->getTemplates();
		if(isset($templates[$this->template]))
			return $templates[$this->template];
		else if(empty($templates))
			throw new CHttpException(500,'No templates are available.');
		else
			throw new CHttpException(500,'Invalid template selection.');

	}

	/**
	 * @param CCodeFile $file whether the code file should be saved
	 */
	public function confirmed($file)
	{
		return $this->answers===null && $file->operation===CCodeFile::OP_NEW
			|| is_array($this->answers) && isset($this->answers[md5($file->path)]);
	}

	/**
	 * Generates the code using the specified code template file.
	 * This method is manly used in {@link generate} to generate code.
	 * @param string $templateFile the code template file path
	 * @param array $_params_ a set of parameters to be extracted and made available in the code template
	 * @return string the generated code
	 */
	public function render($templateFile,$_params_=null)
	{
		if(!is_file($templateFile))
			throw new CException("The template file '$templateFile' does not exist.");

		if(is_array($_params_))
			extract($_params_,EXTR_PREFIX_SAME,'params');
		else
			$params=$_params_;
		ob_start();
		ob_implicit_flush(false);
		require($templateFile);
		return ob_get_clean();
	}

	/**
	 * @return string the code generation result log.
	 */
	public function renderResults()
	{
		$output='Generating code using template "'.$this->templatePath."\"...\n";
		foreach($this->files as $file)
		{
			if($file->error!==null)
				$output.="<span class=\"error\">generating {$file->relativePath}<br/>           {$file->error}</span>\n";
			else if($file->operation===CCodeFile::OP_NEW && $this->confirmed($file))
				$output.=' generated '.$file->relativePath."\n";
			else if($file->operation===CCodeFile::OP_OVERWRITE && $this->confirmed($file))
				$output.=' overwrote '.$file->relativePath."\n";
			else
				$output.='   skipped '.$file->relativePath."\n";
		}
		$output.="done!\n";
		return $output;
	}

	/**
	 * The "sticky" validator.
	 * This validator does not really validate the attributes.
	 * It actually saves the attribute value in a file to make it sticky.
	 * @param string $attribute the attribute to be validated
	 * @param array $params the validation parameters
	 */
	public function sticky($attribute,$params)
	{
		if(!$this->hasErrors())
			$this->_stickyAttributes[$attribute]=$this->$attribute;
	}

	/**
	 * Loads sticky attributes from a file and populates them into the model.
	 */
	public function loadStickyAttributes()
	{
		$this->_stickyAttributes=array();
		$path=$this->getStickyFile();
		if(is_file($path))
		{
			$result=@include($path);
			if(is_array($result))
			{
				$this->_stickyAttributes=$result;
				foreach($this->_stickyAttributes as $name=>$value)
				{
					if(property_exists($this,$name) || $this->canSetProperty($name))
						$this->$name=$value;
				}
			}
		}
	}

	/**
	 * Saves sticky attributes into a file.
	 */
	public function saveStickyAttributes()
	{
		$path=$this->getStickyFile();
		@mkdir(dirname($path),0755,true);
		file_put_contents($path,"<?php\nreturn ".var_export($this->_stickyAttributes,true).";\n");
	}

	/**
	 * @return string the file path that stores the sticky attribute values.
	 */
	public function getStickyFile()
	{
		return Yii::app()->runtimePath.'/gii-'.Yii::getVersion().'/'.get_class($this).'.php';
	}

	/**
	 * Converts a word to its plural form.
	 * Note that this is for English only!
	 * For example, 'apple' will become 'apples', and 'child' will become 'children'.
	 * @param string $name the word to be pluralized
	 * @return string the pluralized word
	 */
	public function pluralize($name)
	{
		$rules=array(
			'/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es',
			'/(?:([^f])fe|([lr])f)$/i' => '\1\2ves',
			'/(m)an$/i' => '\1en',
			'/(child)$/i' => '\1ren',
			'/(r|t)y$/i' => '\1ies',
			'/s$/' => 's',
		);
		foreach($rules as $rule=>$replacement)
		{
			if(preg_match($rule,$name))
				return preg_replace($rule,$replacement,$name);
		}
		return $name.'s';
	}

	/**
	 * Converts a class name into a HTML ID.
	 * For example, 'PostTag' will be converted as 'post-tag'.
	 * @param string $name the string to be converted
	 * @return string the resulting ID
	 */
	public function class2id($name)
	{
		return trim(strtolower(str_replace('_','-',preg_replace('/(?<![A-Z])[A-Z]/', '-\0', $name))),'-');
	}

	/**
	 * Converts a class name into space-separated words.
	 * For example, 'PostTag' will be converted as 'Post Tag'.
	 * @param string $name the string to be converted
	 * @param boolean $ucwords whether to capitalize the first letter in each word
	 * @return string the resulting words
	 */
	public function class2name($name,$ucwords=true)
	{
		$result=trim(strtolower(str_replace('_',' ',preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $name))));
		return $ucwords ? ucwords($result) : $result;
	}

	/**
	 * Converts a class name into a variable name with the first letter in lower case.
	 * This method is provided because lcfirst() PHP function is only available for PHP 5.3+.
	 * @param string $name the class name
	 * @return string the variable name converted from the class name
	 * @since 1.1.4
	 */
	public function class2var($name)
	{
		$name[0]=strtolower($name[0]);
		return $name;
	}

	/**
	 * Validates an attribute to make sure it is not taking a PHP reserved keyword.
	 * @param string $attribute the attribute to be validated
	 * @param array $params validation parameters
	 */
	public function validateReservedWord($attribute,$params)
	{
		static $keywords=array(
			'__class__',
			'__dir__',
			'__file__',
			'__function__',
			'__line__',
			'__method__',
			'__namespace__',
			'abstract',
			'and',
			'array',
			'as',
			'break',
			'case',
			'catch',
			'cfunction',
			'class',
			'clone',
			'const',
			'continue',
			'declare',
			'default',
			'die',
			'do',
			'echo',
			'else',
			'elseif',
			'empty',
			'enddeclare',
			'endfor',
			'endforeach',
			'endif',
			'endswitch',
			'endwhile',
			'eval',
			'exception',
			'exit',
			'extends',
			'final',
			'final',
			'for',
			'foreach',
			'function',
			'global',
			'goto',
			'if',
			'implements',
			'include',
			'include_once',
			'instanceof',
			'interface',
			'isset',
			'list',
			'namespace',
			'new',
			'old_function',
			'or',
			'parent',
			'php_user_filter',
			'print',
			'private',
			'protected',
			'public',
			'require',
			'require_once',
			'return',
			'static',
			'switch',
			'this',
			'throw',
			'try',
			'unset',
			'use',
			'var',
			'while',
			'xor',
		);
		$value=$this->$attribute;
		if(in_array(strtolower($value),$keywords))
			$this->addError($attribute, $this->getAttributeLabel($attribute).' cannot take a reserved PHP keyword.');
	}
}