<?php
/**
 * CrudCommand 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/
 */

/**
 * CrudCommand generates code implementing CRUD operations.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @package system.cli.commands.shell
 * @since 1.0
 */
class CrudCommand extends CConsoleCommand
{
	/**
	 * @var string the directory that contains templates for crud commands.
	 * Defaults to null, meaning using 'framework/cli/views/shell/crud'.
	 * If you set this path and some views are missing in the directory,
	 * the default views will be used.
	 */
	public $templatePath;
	/**
	 * @var string the directory that contains functional test classes.
	 * Defaults to null, meaning using 'protected/tests/functional'.
	 * If this is false, it means functional test file should NOT be generated.
	 */
	public $functionalTestPath;
	/**
	 * @var array list of actions to be created. Each action must be associated with a template file with the same name.
	 */
	public $actions=array('create','update','index','view','admin','_form','_view','_search');

	public function getHelp()
	{
		return <<<EOD
USAGE
  crud <model-class> [controller-ID] ...

DESCRIPTION
  This command generates a controller and views that accomplish
  CRUD operations for the specified data model.

PARAMETERS
 * model-class: required, the name of the data model class. This can
   also be specified as a path alias (e.g. application.models.Post).
   If the model class belongs to a module, it should be specified
   as 'ModuleID.models.ClassName'.

 * controller-ID: optional, the controller ID (e.g. 'post').
   If this is not specified, the model class name will be used
   as the controller ID. In this case, if the model belongs to
   a module, the controller will also be created under the same
   module.

   If the controller should be located under a subdirectory,
   please specify the controller ID as 'path/to/ControllerID'
   (e.g. 'admin/user').

   If the controller belongs to a module (different from the module
   that the model belongs to), please specify the controller ID
   as 'ModuleID/ControllerID' or 'ModuleID/path/to/Controller'.

EXAMPLES
 * Generates CRUD for the Post model:
        crud Post

 * Generates CRUD for the Post model which belongs to module 'admin':
        crud admin.models.Post

 * Generates CRUD for the Post model. The generated controller should
   belong to module 'admin', but not the model class:
        crud Post admin/post

EOD;
	}

	/**
	 * Execute the action.
	 * @param array command line parameters specific for this command
	 * @return integer|null non zero application exit code for help or null on success
	 */
	public function run($args)
	{
		if(!isset($args[0]))
		{
			echo "Error: data model class is required.\n";
			echo $this->getHelp();
			return 1;
		}
		$module=Yii::app();
		$modelClass=$args[0];
		if(($pos=strpos($modelClass,'.'))===false)
			$modelClass='application.models.'.$modelClass;
		else
		{
			$id=substr($modelClass,0,$pos);
			if(($m=Yii::app()->getModule($id))!==null)
				$module=$m;
		}
		$modelClass=Yii::import($modelClass);

		if(isset($args[1]))
		{
			$controllerID=$args[1];
			if(($pos=strrpos($controllerID,'/'))===false)
			{
				$controllerClass=ucfirst($controllerID).'Controller';
				$controllerFile=$module->controllerPath.DIRECTORY_SEPARATOR.$controllerClass.'.php';
				$controllerID[0]=strtolower($controllerID[0]);
			}
			else
			{
				$last=substr($controllerID,$pos+1);
				$last[0]=strtolower($last);
				$pos2=strpos($controllerID,'/');
				$first=substr($controllerID,0,$pos2);
				$middle=$pos===$pos2?'':substr($controllerID,$pos2+1,$pos-$pos2);

				$controllerClass=ucfirst($last).'Controller';
				$controllerFile=($middle===''?'':$middle.'/').$controllerClass.'.php';
				$controllerID=$middle===''?$last:$middle.'/'.$last;
				if(($m=Yii::app()->getModule($first))!==null)
					$module=$m;
				else
				{
					$controllerFile=$first.'/'.$controllerFile;
					$controllerID=$first.'/'.$controllerID;
				}

				$controllerFile=$module->controllerPath.DIRECTORY_SEPARATOR.str_replace('/',DIRECTORY_SEPARATOR,$controllerFile);
			}
		}
		else
		{
			$controllerID=$modelClass;
			$controllerClass=ucfirst($controllerID).'Controller';
			$controllerFile=$module->controllerPath.DIRECTORY_SEPARATOR.$controllerClass.'.php';
			$controllerID[0]=strtolower($controllerID[0]);
		}

		$templatePath=$this->templatePath===null?YII_PATH.'/cli/views/shell/crud':$this->templatePath;
		$functionalTestPath=$this->functionalTestPath===null?Yii::getPathOfAlias('application.tests.functional'):$this->functionalTestPath;

		$viewPath=$module->viewPath.DIRECTORY_SEPARATOR.str_replace('.',DIRECTORY_SEPARATOR,$controllerID);
		$fixtureName=$this->pluralize($modelClass);
		$fixtureName[0]=strtolower($fixtureName);
		$list=array(
			basename($controllerFile)=>array(
				'source'=>$templatePath.'/controller.php',
				'target'=>$controllerFile,
				'callback'=>array($this,'generateController'),
				'params'=>array($controllerClass,$modelClass),
			),
		);

		if($functionalTestPath!==false)
		{
			$list[$modelClass.'Test.php']=array(
				'source'=>$templatePath.'/test.php',
				'target'=>$functionalTestPath.DIRECTORY_SEPARATOR.$modelClass.'Test.php',
				'callback'=>array($this,'generateTest'),
				'params'=>array($controllerID,$fixtureName,$modelClass),
			);
		}

		foreach($this->actions as $action)
		{
			$list[$action.'.php']=array(
				'source'=>$templatePath.'/'.$action.'.php',
				'target'=>$viewPath.'/'.$action.'.php',
				'callback'=>array($this,'generateView'),
				'params'=>$modelClass,
			);
		}

		$this->copyFiles($list);

		if($module instanceof CWebModule)
			$moduleID=$module->id.'/';
		else
			$moduleID='';

		echo "\nCrud '{$controllerID}' has been successfully created. You may access it via:\n";
		echo "http://hostname/path/to/index.php?r={$moduleID}{$controllerID}\n";
	}

	public function generateController($source,$params)
	{
		list($controllerClass,$modelClass)=$params;
		$model=CActiveRecord::model($modelClass);
		$id=$model->tableSchema->primaryKey;
		if($id===null)
			throw new ShellException(Yii::t('yii','Error: Table "{table}" does not have a primary key.',array('{table}'=>$model->tableName())));
		elseif(is_array($id))
			throw new ShellException(Yii::t('yii','Error: Table "{table}" has a composite primary key which is not supported by crud command.',array('{table}'=>$model->tableName())));

		if(!is_file($source))  // fall back to default ones
			$source=YII_PATH.'/cli/views/shell/crud/'.basename($source);

		return $this->renderFile($source,array(
			'ID'=>$id,
			'controllerClass'=>$controllerClass,
			'modelClass'=>$modelClass,
		),true);
	}

	public function generateView($source,$modelClass)
	{
		$model=CActiveRecord::model($modelClass);
		$table=$model->getTableSchema();
		$columns=$table->columns;
		if(!is_file($source))  // fall back to default ones
			$source=YII_PATH.'/cli/views/shell/crud/'.basename($source);
		return $this->renderFile($source,array(
			'ID'=>$table->primaryKey,
			'modelClass'=>$modelClass,
			'columns'=>$columns),true);
	}

	public function generateTest($source,$params)
	{
		list($controllerID,$fixtureName,$modelClass)=$params;
		if(!is_file($source))  // fall back to default ones
			$source=YII_PATH.'/cli/views/shell/crud/'.basename($source);
		return $this->renderFile($source, array(
			'controllerID'=>$controllerID,
			'fixtureName'=>$fixtureName,
			'modelClass'=>$modelClass,
		),true);
	}

	public function generateInputLabel($modelClass,$column)
	{
		return "CHtml::activeLabelEx(\$model,'{$column->name}')";
	}

	public function generateInputField($modelClass,$column)
	{
		if($column->type==='boolean')
			return "CHtml::activeCheckBox(\$model,'{$column->name}')";
		elseif(stripos($column->dbType,'text')!==false)
			return "CHtml::activeTextArea(\$model,'{$column->name}',array('rows'=>6, 'cols'=>50))";
		else
		{
			if(preg_match('/^(password|pass|passwd|passcode)$/i',$column->name))
				$inputField='activePasswordField';
			else
				$inputField='activeTextField';

			if($column->type!=='string' || $column->size===null)
				return "CHtml::{$inputField}(\$model,'{$column->name}')";
			else
			{
				if(($size=$maxLength=$column->size)>60)
					$size=60;
				return "CHtml::{$inputField}(\$model,'{$column->name}',array('size'=>$size,'maxlength'=>$maxLength))";
			}
		}
	}

	public function generateActiveLabel($modelClass,$column)
	{
		return "\$form->labelEx(\$model,'{$column->name}')";
	}

	public function generateActiveField($modelClass,$column)
	{
		if($column->type==='boolean')
			return "\$form->checkBox(\$model,'{$column->name}')";
		elseif(stripos($column->dbType,'text')!==false)
			return "\$form->textArea(\$model,'{$column->name}',array('rows'=>6, 'cols'=>50))";
		else
		{
			if(preg_match('/^(password|pass|passwd|passcode)$/i',$column->name))
				$inputField='passwordField';
			else
				$inputField='textField';

			if($column->type!=='string' || $column->size===null)
				return "\$form->{$inputField}(\$model,'{$column->name}')";
			else
			{
				if(($size=$maxLength=$column->size)>60)
					$size=60;
				return "\$form->{$inputField}(\$model,'{$column->name}',array('size'=>$size,'maxlength'=>$maxLength))";
			}
		}
	}

	public function guessNameColumn($columns)
	{
		foreach($columns as $column)
		{
			if(!strcasecmp($column->name,'name'))
				return $column->name;
		}
		foreach($columns as $column)
		{
			if(!strcasecmp($column->name,'title'))
				return $column->name;
		}
		foreach($columns as $column)
		{
			if($column->isPrimaryKey)
				return $column->name;
		}
		return 'id';
	}

	public function class2id($className)
	{
		return trim(strtolower(str_replace('_','-',preg_replace('/(?<![A-Z])[A-Z]/', '-\0', $className))),'-');
	}

	public function class2name($className,$pluralize=false)
	{
		if($pluralize)
			$className=$this->pluralize($className);
		return ucwords(trim(strtolower(str_replace(array('-','_'),' ',preg_replace('/(?<![A-Z])[A-Z]/', ' \0', $className)))));
	}
}
