<?php
/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd New BSD License
 */

namespace Zend\Code\Generator;

use 
Zend\Code\Reflection\FileReflection;

class 
FileGenerator extends AbstractGenerator
{
    
/**
     * @var string
     */
    
protected $filename null;

    
/**
     * @var DocBlockGenerator
     */
    
protected $docBlock null;

    
/**
     * @var array
     */
    
protected $requiredFiles = array();

    
/**
     * @var string
     */
    
protected $namespace null;

    
/**
     * @var array
     */
    
protected $uses = array();

    
/**
     * @var array
     */
    
protected $classes = array();

    
/**
     * @var string
     */
    
protected $body null;

    
/**
     * Passes $options to {@link setOptions()}.
     *
     * @param  array|\Traversable $options
     */
    
public function __construct($options null)
    {
        if (
null !== $options) {
            
$this->setOptions($options);
        }
    }

    
/**
     * 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 $includeIfNotAlreadyIncluded
     * @throws Exception\InvalidArgumentException
     * @return FileGenerator
     */
    
public static function fromReflectedFileName($filePath$includeIfNotAlreadyIncluded true)
    {
        
$realpath realpath($filePath);

        if (
            
$realpath === false
            
&& ($realpath FileReflection::findRealpathInIncludePath($filePath)) === false
        
) {
            throw new 
Exception\InvalidArgumentException(sprintf(
                
'No file for %s was found.',
                
$realpath
            
));
        }

        if (
$includeIfNotAlreadyIncluded && !in_array($realpathget_included_files())) {
            include 
$realpath;
        }

        
$fileReflector = new FileReflection($realpath);
        
$codeGenerator = static::fromReflection($fileReflector);

        return 
$codeGenerator;
    }

    
/**
     * @param  FileReflection $fileReflection
     * @return FileGenerator
     */
    
public static function fromReflection(FileReflection $fileReflection)
    {
        
$file = new static();

        
$file->setSourceContent($fileReflection->getContents());
        
$file->setSourceDirty(false);

        
$body $fileReflection->getContents();

        foreach (
$fileReflection->getClasses() as $class) {
            
$phpClass ClassGenerator::fromReflection($class);
            
$phpClass->setContainingFileGenerator($file);

            
$file->setClass($phpClass);

            
$classStartLine $class->getStartLine(true);
            
$classEndLine   $class->getEndLine();

            
$bodyLines  explode("\n"$body);
            
$bodyReturn = array();
            for (
$lineNum 1$count count($bodyLines); $lineNum <= $count$lineNum++) {
                if (
$lineNum == $classStartLine) {
                    
$bodyReturn[] = str_replace(
                        
'?',
                        
$class->getName(),
                        
'/* Zend_Code_Generator_Php_File-ClassMarker: {?} */'
                    
);

                    
$lineNum $classEndLine;
                } else {
                    
$bodyReturn[] = $bodyLines[$lineNum 1]; // adjust for index -> line conversion
                
}
            }
            
$body implode("\n"$bodyReturn);
            unset(
$bodyLines$bodyReturn$classStartLine$classEndLine);
        }

        
$namespace $fileReflection->getNamespace();

        if (
$namespace != '') {
            
$file->setNamespace($namespace);
        }

        
$uses $fileReflection->getUses();
        if (
$uses) {
            
$file->setUses($uses);
        }

        if ((
$fileReflection->getDocComment() != '')) {
            
$docBlock $fileReflection->getDocBlock();
            
$file->setDocBlock(DocBlockGenerator::fromReflection($docBlock));

            
$bodyLines  explode("\n"$body);
            
$bodyReturn = array();
            for (
$lineNum 1$count count($bodyLines); $lineNum <= $count$lineNum++) {
                if (
$lineNum == $docBlock->getStartLine()) {
                    
$bodyReturn[] = str_replace(
                        
'?',
                        
$class->getName(),
                        
'/* Zend_Code_Generator_FileGenerator-DocBlockMarker */'
                    
);
                    
$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;
    }

    
/**
     * @param  array $values
     * @return FileGenerator
     */
    
public static function fromArray(array $values)
    {
        
$fileGenerator = new static;
        foreach (
$values as $name => $value) {
            switch (
strtolower(str_replace(array('.''-''_'), ''$name))) {
                case 
'filename':
                    
$fileGenerator->setFilename($value);
                    continue;
                case 
'class':
                    
$fileGenerator->setClass(($value instanceof ClassGenerator) ? $value ClassGenerator::fromArray($value));
                    continue;
                case 
'requiredfiles':
                    
$fileGenerator->setRequiredFiles($value);
                    continue;
                default:
                    if (
property_exists($fileGenerator$name)) {
                        
$fileGenerator->{$name} = $value;
                    } elseif (
method_exists($fileGenerator'set' $name)) {
                        
$fileGenerator->{'set' $name}($value);
                    }
            }
        }

        return 
$fileGenerator;
    }

    
/**
     * @param  DocBlockGenerator|string $docBlock
     * @throws Exception\InvalidArgumentException
     * @return FileGenerator
     */
    
public function setDocBlock($docBlock)
    {
        if (
is_string($docBlock)) {
            
$docBlock = array('shortDescription' => $docBlock);
        }

        if (
is_array($docBlock)) {
            
$docBlock = new DocBlockGenerator($docBlock);
        } elseif (!
$docBlock instanceof DocBlockGenerator) {
            throw new 
Exception\InvalidArgumentException(sprintf(
                
'%s is expecting either a string, array or an instance of %s\DocBlockGenerator',
                
__METHOD__,
                
__NAMESPACE__
            
));
        }

        
$this->docBlock $docBlock;
        return 
$this;
    }

    
/**
     * @return DocBlockGenerator
     */
    
public function getDocBlock()
    {
        return 
$this->docBlock;
    }

    
/**
     * @param  array $requiredFiles
     * @return FileGenerator
     */
    
public function setRequiredFiles(array $requiredFiles)
    {
        
$this->requiredFiles $requiredFiles;
        return 
$this;
    }

    
/**
     * @return array
     */
    
public function getRequiredFiles()
    {
        return 
$this->requiredFiles;
    }

    
/**
     * @param  array $classes
     * @return FileGenerator
     */
    
public function setClasses(array $classes)
    {
        foreach (
$classes as $class) {
            
$this->setClass($class);
        }

        return 
$this;
    }

    
/**
     * @return string
     */
    
public function getNamespace()
    {
        return 
$this->namespace;
    }

    
/**
     * @param  string $namespace
     * @return FileGenerator
     */
    
public function setNamespace($namespace)
    {
        
$this->namespace = (string) $namespace;
        return 
$this;
    }

    
/**
     * Returns an array with the first element the use statement, second is the as part.
     * If $withResolvedAs is set to true, there will be a third element that is the
     * "resolved" as statement, as the second part is not required in use statements
     *
     * @param  bool $withResolvedAs
     * @return array
     */
    
public function getUses($withResolvedAs false)
    {
        
$uses $this->uses;
        if (
$withResolvedAs) {
            for (
$useIndex 0$count count($uses); $useIndex $count$useIndex++) {
                if (
$uses[$useIndex][1] == '') {
                    if ((
$lastSeparator strrpos($uses[$useIndex][0], '\\')) !== false) {
                        
$uses[$useIndex][2] = substr($uses[$useIndex][0], $lastSeparator 1);
                    } else {
                        
$uses[$useIndex][2] = $uses[$useIndex][0];
                    }
                } else {
                    
$uses[$useIndex][2] = $uses[$useIndex][1];
                }
            }
        }

        return 
$uses;
    }

    
/**
     * @param  array $uses
     * @return FileGenerator
     */
    
public function setUses(array $uses)
    {
        foreach (
$uses as $use) {
            
$use = (array)$use;
            if (
array_key_exists('use'$use) && array_key_exists('as'$use)) {
                
$import $use['use'];
                
$alias  $use['as'];
            } elseif (
count($use) == 2) {
                list(
$import$alias) = $use;
            } else {
                
$import current($use);
                
$alias  null;
            }
            
$this->setUse($import$alias);
        }
        return 
$this;
    }

    
/**
     * @param  string $use
     * @param  null|string $as
     * @return FileGenerator
     */
    
public function setUse($use$as null)
    {
        if (!
in_array(array($use$as), $this->uses)) {
            
$this->uses[] = array($use$as);
        }
        return 
$this;
    }

    
/**
     * @param  string $name
     * @return ClassGenerator
     */
    
public function getClass($name null)
    {
        if (
$name == null) {
            
reset($this->classes);

            return 
current($this->classes);
        }

        return 
$this->classes[(string) $name];
    }

    
/**
     * @param  array|string|ClassGenerator $class
     * @throws Exception\InvalidArgumentException
     * @return FileGenerator
     */
    
public function setClass($class)
    {
        if (
is_array($class)) {
            
$class ClassGenerator::fromArray($class);
        } elseif (
is_string($class)) {
            
$class = new ClassGenerator($class);
        } elseif (!
$class instanceof ClassGenerator) {
            throw new 
Exception\InvalidArgumentException(sprintf(
                
'%s is expecting either a string, array or an instance of %s\ClassGenerator',
                
__METHOD__,
                
__NAMESPACE__
            
));
        }

        
// @todo check for dup here
        
$className                 $class->getName();
        
$this->classes[$className] = $class;

        return 
$this;
    }

    
/**
     * @param  string $filename
     * @return FileGenerator
     */
    
public function setFilename($filename)
    {
        
$this->filename = (string) $filename;
        return 
$this;
    }

    
/**
     * @return string
     */
    
public function getFilename()
    {
        return 
$this->filename;
    }

    
/**
     * @return ClassGenerator[]
     */
    
public function getClasses()
    {
        return 
$this->classes;
    }

    
/**
     * @param  string $body
     * @return FileGenerator
     */
    
public function setBody($body)
    {
        
$this->body = (string) $body;
        return 
$this;
    }

    
/**
     * @return string
     */
    
public function getBody()
    {
        return 
$this->body;
    }

    
/**
     * @return bool
     */
    
public function isSourceDirty()
    {
        
$docBlock $this->getDocBlock();
        if (
$docBlock && $docBlock->isSourceDirty()) {
            return 
true;
        }

        foreach (
$this->classes as $class) {
            if (
$class->isSourceDirty()) {
                return 
true;
            }
        }

        return 
parent::isSourceDirty();
    }

    
/**
     * @return string
     */
    
public function generate()
    {
        if (
$this->isSourceDirty() === false) {
            return 
$this->sourceContent;
        }

        
$output '';

        
// start with the body (if there), or open tag
        
$body $this->getBody();
        if (
preg_match('#(?:\s*)<\?php#'$body) == false) {
            
$output '<?php' self::LINE_FEED;
        }

        
// if there are markers, put the body into the output
        
if (preg_match('#/\* Zend_Code_Generator_FileGenerator-(.*?)Marker:#'$body)) {
            
$tokens token_get_all($body);
            foreach (
$tokens as $token) {
                if (
is_array($token) && in_array($token[0], array(T_OPEN_TAGT_COMMENTT_DOC_COMMENTT_WHITESPACE))
                ) {
                    
$output .= $token[1];
                }
            }
            
$body '';
        }

        
// Add file DocBlock, if any
        
if (null !== ($docBlock $this->getDocBlock())) {
            
$docBlock->setIndentation('');

            if (
preg_match('#/* Zend_Code_Generator_FileGenerator-DocBlockMarker */#'$output)) {
                
$output preg_replace('#/* Zend_CodeGenerator_Php_File-DocBlockMarker */#'$docBlock->generate(),
                                       
$output1);
            } else {
                
$output .= $docBlock->generate() . self::LINE_FEED;
            }
        }

        
// newline
        
$output .= self::LINE_FEED;

        
// namespace, if any
        
$namespace $this->getNamespace();
        if (
$namespace) {
            
$output .= sprintf('namespace %s;%s'$namespacestr_repeat(self::LINE_FEED2));
        }

        
// 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 import statements
        
$uses $this->getUses();
        if (!empty(
$uses)) {
            foreach (
$uses as $use) {
                list(
$import$alias) = $use;
                if (
null === $alias) {
                    
$output .= sprintf('use %s;%s'$importself::LINE_FEED);
                } else {
                    
$output .= sprintf('use %s as %s;%s'$import$aliasself::LINE_FEED);
                }
            }
            
$output .= self::LINE_FEED;
        }

        
// process classes
        
$classes $this->getClasses();
        if (!empty(
$classes)) {
            foreach (
$classes as $class) {
                
$regex str_replace('?'$class->getName(),
                                     
'/* Zend_Code_Generator_FileGenerator-ClassMarker: {?} */');
                
$regex preg_quote($regex'#');
                if (
preg_match('#' $regex '#'$output)) {
                    
$output preg_replace('#' $regex '#'$class->generate(), $output1);
                } else {
                    if (
$namespace) {
                        
$class->setNamespaceName(null);
                    }
                    
$output .= $class->generate() . self::LINE_FEED;
                }
            }
        }

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

            
$output .= $body;
        }

        return 
$output;
    }

    
/**
     * @return FileGenerator
     * @throws Exception\RuntimeException
     */
    
public function write()
    {
        if (
$this->filename == '' || !is_writable(dirname($this->filename))) {
            throw new 
Exception\RuntimeException('This code generator object is not writable.');
        }
        
file_put_contents($this->filename$this->generate());

        return 
$this;
    }
}