<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_CodeGenerator
 * @subpackage PHP
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: File.php 16971 2009-07-22 18:05:45Z mikaelkael $
 */

/**
 * @see Zend_CodeGenerator_Php_Abstract
 */
require_once 'Zend/CodeGenerator/Php/Abstract.php';

/**
 * @see Zend_CodeGenerator_Php_Class
 */
require_once 'Zend/CodeGenerator/Php/Class.php';

/**
 * @category   Zend
 * @package    Zend_CodeGenerator
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_CodeGenerator_Php_File extends Zend_CodeGenerator_Php_Abstract
{
    
    /**
     * @var array Array of Zend_CodeGenerator_Php_File
     */
    protected static $_fileCodeGenerators = array();
    
    /**#@+
     * @var string
     */
    protected static $_markerDocblock = '/* Zend_CodeGenerator_Php_File-DocblockMarker */';
    protected static $_markerRequire = '/* Zend_CodeGenerator_Php_File-RequireMarker: {?} */';
    protected static $_markerClass = '/* Zend_CodeGenerator_Php_File-ClassMarker: {?} */';
    /**#@-*/
    
    /**
     * @var string
     */
    protected $_filename = null;
    
    /**
     * @var Zend_CodeGenerator_Php_Docblock
     */
    protected $_docblock = null;
    
    /**
     * @var array
     */
    protected $_requiredFiles = array();
    
    /**
     * @var array
     */
    protected $_classes = array();
    
    /**
     * @var string
     */
    protected $_body = null;

    public static function registerFileCodeGenerator(Zend_CodeGenerator_Php_File $fileCodeGenerator, $fileName = null)
    {
        if ($fileName == null) {
            $fileName = $fileCodeGenerator->getFilename();
        }
        
        if ($fileName == '') {
            require_once 'Zend/CodeGenerator/Php/Exception.php';
            throw new Zend_CodeGenerator_Php_Exception('FileName does not exist.');
        }
        
        // cannot use realpath since the file might not exist, but we do need to have the index
        // in the same DIRECTORY_SEPARATOR that realpath would use:
        $fileName = str_replace(array('\\', '/'), DIRECTORY_SEPARATOR, $fileName);
        
        self::$_fileCodeGenerators[$fileName] = $fileCodeGenerator;
        
    }
    
    /**
     * fromReflectedFilePath() - use this if you intend on generating code generation objects based on the same file.
     * This will keep previous changes to the file in tact during the same PHP process
     *
     * @param string $filePath
     * @param bool $usePreviousCodeGeneratorIfItExists
     * @param bool $includeIfNotAlreadyIncluded
     * @return Zend_CodeGenerator_Php_File
     */
    public static function fromReflectedFileName($filePath, $usePreviousCodeGeneratorIfItExists = true, $includeIfNotAlreadyIncluded = true)
    {
        $realpath = realpath($filePath);
        
        if ($realpath === false) {
            if ( ($realpath = Zend_Reflection_file::findRealpathInIncludePath($filePath)) === false) {
                require_once 'Zend/CodeGenerator/Php/Exception.php';
                throw new Zend_CodeGenerator_Php_Exception('No file for ' . $realpath . ' was found.');
            }
        }
        
        if ($usePreviousCodeGeneratorIfItExists && isset(self::$_fileCodeGenerators[$realpath])) {
            return self::$_fileCodeGenerators[$realpath];
        }
        
        if ($includeIfNotAlreadyIncluded && !in_array($realpath, get_included_files())) {
            include $realpath;
        }
        
        $codeGenerator = self::fromReflection(($fileReflector = new Zend_Reflection_File($realpath)));
        
        if (!isset(self::$_fileCodeGenerators[$fileReflector->getFileName()])) {
            self::$_fileCodeGenerators[$fileReflector->getFileName()] = $codeGenerator;
        }
        
        return $codeGenerator;
    }
    
    /**
     * fromReflection()
     *
     * @param Zend_Reflection_File $reflectionFile
     * @return Zend_CodeGenerator_Php_File
     */
    public static function fromReflection(Zend_Reflection_File $reflectionFile)
    {
        $file = new self();
        
        $file->setSourceContent($reflectionFile->getContents());
        $file->setSourceDirty(false);
        
        $body = $reflectionFile->getContents();
        
        // @todo this whole area needs to be reworked with respect to how body lines are processed
        foreach ($reflectionFile->getClasses() as $class) {
            $file->setClass(Zend_CodeGenerator_Php_Class::fromReflection($class));
            $classStartLine = $class->getStartLine(true);
            $classEndLine = $class->getEndLine();
            
            $bodyLines = explode("\n", $body);
            $bodyReturn = array();
            for ($lineNum = 1; $lineNum <= count($bodyLines); $lineNum++) {
                if ($lineNum == $classStartLine) { 
                    $bodyReturn[] = str_replace('?', $class->getName(), self::$_markerClass);  //'/* Zend_CodeGenerator_Php_File-ClassMarker: {' . $class->getName() . '} */';
                    $lineNum = $classEndLine;
                } else {
                    $bodyReturn[] = $bodyLines[$lineNum - 1]; // adjust for index -> line conversion
                }
            }
            $body = implode("\n", $bodyReturn);
            unset($bodyLines, $bodyReturn, $classStartLine, $classEndLine);
        }
        
        if (($reflectionFile->getDocComment() != '')) {
            $docblock = $reflectionFile->getDocblock();
            $file->setDocblock(Zend_CodeGenerator_Php_Docblock::fromReflection($docblock));
            
            $bodyLines = explode("\n", $body);
            $bodyReturn = array();
            for ($lineNum = 1; $lineNum <= count($bodyLines); $lineNum++) {
                if ($lineNum == $docblock->getStartLine()) { 
                    $bodyReturn[] = str_replace('?', $class->getName(), self::$_markerDocblock);  //'/* Zend_CodeGenerator_Php_File-ClassMarker: {' . $class->getName() . '} */';
                    $lineNum = $docblock->getEndLine();
                } else {
                    $bodyReturn[] = $bodyLines[$lineNum - 1]; // adjust for index -> line conversion
                }
            }
            $body = implode("\n", $bodyReturn);
            unset($bodyLines, $bodyReturn, $classStartLine, $classEndLine);
        }
        
        $file->setBody($body);
        
        return $file;
    }
    
    /**
     * setDocblock() Set the docblock
     *
     * @param Zend_CodeGenerator_Php_Docblock|array|string $docblock
     * @return Zend_CodeGenerator_Php_File
     */
    public function setDocblock($docblock) 
    {
        if (is_string($docblock)) {
            $docblock = array('shortDescription' => $docblock);
        }
        
        if (is_array($docblock)) {
            $docblock = new Zend_CodeGenerator_Php_Docblock($docblock);
        } elseif (!$docblock instanceof Zend_CodeGenerator_Php_Docblock) {
            require_once 'Zend/CodeGenerator/Php/Exception.php';
            throw new Zend_CodeGenerator_Php_Exception('setDocblock() is expecting either a string, array or an instance of Zend_CodeGenerator_Php_Docblock');
        }
        
        $this->_docblock = $docblock;
        return $this;
    }
    
    /**
     * Get docblock
     *
     * @return Zend_CodeGenerator_Php_Docblock
     */
    public function getDocblock() 
    {
        return $this->_docblock;
    }

    /**
     * setRequiredFiles
     *
     * @param array $requiredFiles
     * @return Zend_CodeGenerator_Php_File
     */
    public function setRequiredFiles($requiredFiles)
    {
        $this->_requiredFiles = $requiredFiles;
        return $this;
    }
    
    /**
     * getRequiredFiles()
     *
     * @return array
     */
    public function getRequiredFiles() 
    {
        return $this->_requiredFiles;
    }

    /**
     * setClasses()
     *
     * @param array $classes
     * @return Zend_CodeGenerator_Php_File
     */
    public function setClasses(Array $classes) 
    {
        foreach ($classes as $class) {
            $this->setClass($class);
        }
        return $this;
    }
    
    /**
     * getClass()
     *
     * @param string $name
     * @return Zend_CodeGenerator_Php_Class
     */
    public function getClass($name = null)
    {
        if ($name == null) {
            reset($this->_classes);
            return current($this->_classes);
        }
        
        return $this->_classes[$name];
    }
    
    /**
     * setClass()
     *
     * @param Zend_CodeGenerator_Php_Class|array $class
     * @return Zend_CodeGenerator_Php_File
     */
    public function setClass($class)
    {
        if (is_array($class)) {
            $class = new Zend_CodeGenerator_Php_Class($class);
            $className = $class->getName();
        } elseif ($class instanceof Zend_CodeGenerator_Php_Class) {
            $className = $class->getName();
        } else {
            require_once 'Zend/CodeGenerator/Php/Exception.php';
            throw new Zend_CodeGenerator_Php_Exception('Expecting either an array or an instance of Zend_CodeGenerator_Php_Class');
        }
        
        // @todo check for dup here 
        
        $this->_classes[$className] = $class;
        return $this;
    }
    
    /**
     * setFilename()
     *
     * @param string $filename
     * @return Zend_CodeGenerator_Php_File
     */
    public function setFilename($filename)
    {
        $this->_filename = $filename;
        return $this;
    }
    
    /**
     * getFilename()
     *
     * @return string
     */
    public function getFilename()
    {
        return $this->_filename;
    }
    
    /**
     * getClasses()
     *
     * @return array Array of Zend_CodeGenerator_Php_Class
     */
    public function getClasses() 
    {
        return $this->_classes;
    }

    /**
     * setBody()
     *
     * @param string $body
     * @return Zend_CodeGenerator_Php_File
     */
    public function setBody($body)
    {
        $this->_body = $body;
        return $this;
    }
    
    /**
     * getBody()
     *
     * @return string
     */
    public function getBody()
    {
        return $this->_body;
    }
    
    /**
     * isSourceDirty()
     *
     * @return bool
     */
    public function isSourceDirty()
    {
        if (($docblock = $this->getDocblock()) && $docblock->isSourceDirty()) {
            return true;
        }
        
        foreach ($this->_classes as $class) {
            if ($class->isSourceDirty()) {
                return true;
            }
        }
        
        return parent::isSourceDirty();
    }
    
    /**
     * generate()
     *
     * @return string
     */
    public function generate()
    {
        if ($this->isSourceDirty() === false) {
            return $this->_sourceContent;
        }
        
        $output = '';
        
        // start with the body (if there), or open tag
        if (preg_match('#(?:\s*)<\?php#', $this->getBody()) == false) {
            $output = '<?php' . self::LINE_FEED;
        }
        
        // if there are markers, put the body into the output
        $body = $this->getBody();
        if (preg_match('#/\* Zend_CodeGenerator_Php_File-(.*?)Marker:#', $body)) {
            $output .= $body;
            $body    = '';
        }
        
        // Add file docblock, if any
        if (null !== ($docblock = $this->getDocblock())) {
            $docblock->setIndentation('');
            $regex = preg_quote(self::$_markerDocblock, '#');
            if (preg_match('#'.$regex.'#', $output)) {
                $output  = preg_replace('#'.$regex.'#', $docblock->generate(), $output, 1);
            } else {
                $output .= $docblock->generate() . self::LINE_FEED;
            }
        }
        
        // newline
        $output .= self::LINE_FEED;
        
        // process required files
        // @todo marker replacement for required files
        $requiredFiles = $this->getRequiredFiles();
        if (!empty($requiredFiles)) {
            foreach ($requiredFiles as $requiredFile) {
                $output .= 'require_once \'' . $requiredFile . '\';' . self::LINE_FEED;
            }
            
            $output .= self::LINE_FEED;
        }
        
        // process classes
        $classes = $this->getClasses();
        if (!empty($classes)) {
            foreach ($classes as $class) {
                $regex = str_replace('?', $class->getName(), self::$_markerClass);
                $regex = preg_quote($regex, '#');
                if (preg_match('#'.$regex.'#', $output)) {
                    $output = preg_replace('#'.$regex.'#', $class->generate(), $output, 1);
                } else {
                    $output .= $class->generate() . self::LINE_FEED;
                }
            }
            
        }

        if (!empty($body)) {

            // add an extra space betwee clsses and 
            if (!empty($classes)) {
                $output .= self::LINE_FEED;
            }
        
            $output .= $body;
        }

        return $output;
    }
    
    public function write()
    {
        if ($this->_filename == '' || !is_writable(dirname($this->_filename))) {
            require_once 'Zend/CodeGenerator/Php/Exception.php';
            throw new Zend_CodeGenerator_Php_Exception('This code generator object is not writable.');
        }
        file_put_contents($this->_filename, $this->generate());
        return $this;
    }
    
}
