<?php
/*
 *  $Id: Builder.php 10831 2013-05-29 19:32:17Z btowles $
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.doctrine-project.org>.
 */

/**
 * Doctrine_Migration_Builder
 *
 * @package     Doctrine
 * @subpackage  Migration
 * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
 * @author      Jonathan H. Wage <jwage@mac.com>
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @link        www.doctrine-project.org
 * @since       1.0
 * @version     $Revision: 2939 $
 */
class Doctrine_Migration_Builder extends Doctrine_Builder
{
    /**
     * The path to your migration classes directory
     *
     * @var string
     */
    private $migrationsPath = '';

    /**
     * File suffix to use when writing class definitions
     *
     * @var string $suffix
     */
    private $suffix = '.php';

    /**
     * Instance of the migration class for the migration classes directory
     *
     * @var Doctrine_Migration $migration
     */
    private $migration;

    /**
     * Class template used for writing classes
     *
     * @var $tpl
     */
    private static $tpl;

    /**
     * Instantiate new instance of the Doctrine_Migration_Builder class
     *
     * <code>
     * $builder = new Doctrine_Migration_Builder('/path/to/migrations');
     * </code>
     *
     * @return void
     */
    public function __construct($migrationsPath = null)
    {
        if ($migrationsPath instanceof Doctrine_Migration) {
            $this->setMigrationsPath($migrationsPath->getMigrationClassesDirectory());
            $this->migration = $migrationsPath;
        } else if (is_dir($migrationsPath)) {
            $this->setMigrationsPath($migrationsPath);
            $this->migration = new Doctrine_Migration($migrationsPath);
        }

        $this->loadTemplate();
    }

    /**
     * Set the path to write the generated migration classes
     *
     * @param string path   the path where migration classes are stored and being generated
     * @return void
     */
    public function setMigrationsPath($path)
    {
        Doctrine_Lib::makeDirectories($path);

        $this->migrationsPath = $path;
    }

    /**
     * Get the path where generated migration classes are written to
     *
     * @return string       the path where migration classes are stored and being generated
     */
    public function getMigrationsPath()
    {
        return $this->migrationsPath;
    }

    /**
     * Loads the class template used for generating classes
     *
     * @return void
     */
    protected function loadTemplate()
    {
        if (isset(self::$tpl)) {
            return;
        }

        self::$tpl =<<<END
/**
 * This class has been auto-generated by the Doctrine ORM Framework
 */
class %s extends %s
{
    public function up()
    {
%s
    }

    public function down()
    {
%s
    }
}
END;
    }

    /**
     * Generate migrations from a Doctrine_Migration_Diff instance
     *
     * @param  Doctrine_Migration_Diff $diff Instance to generate changes from
     * @return array $changes  Array of changes produced from the diff
     */
    public function generateMigrationsFromDiff(Doctrine_Migration_Diff $diff, array $buildoptions = array())
    {
        $changes = $diff->generateChanges();

        $up = array();
        $down = array();

        if ( ! empty($changes['dropped_tables'])) {
            foreach ($changes['dropped_tables'] as $tableName => $table) {
                $up[] = $this->buildDropTable($table);
                $down[] = $this->buildCreateTable($table);
            }
        }

        if ( ! empty($changes['created_tables'])) {
            foreach ($changes['created_tables'] as $tableName => $table) {
                $up[] = $this->buildCreateTable($table);
                $down[] = $this->buildDropTable($table);
            }
        }

        if ( ! empty($changes['dropped_columns'])) {
            foreach ($changes['dropped_columns'] as $tableName => $removedColumns) {
                foreach ($removedColumns as $name => $column) {
                    $up[] = $this->buildRemoveColumn($tableName, $name, $column);
                    $down[] = $this->buildAddColumn($tableName, $name, $column);
                }
            }
        }

        if ( ! empty($changes['created_columns'])) {
            foreach ($changes['created_columns'] as $tableName => $addedColumns) {
                foreach ($addedColumns as $name => $column) {
                    $up[] = $this->buildAddColumn($tableName, $name, $column);
                    $down[] = $this->buildRemoveColumn($tableName, $name, $column);
                }
            }
        }

        if ( ! empty($changes['changed_columns'])) {
            foreach ($changes['changed_columns'] as $tableName => $changedColumns) {
                foreach ($changedColumns as $name => $column) {
                    $up[] = $this->buildChangeColumn($tableName, $name, $column);
                }
            }
        }

        if ( ! empty($up) || ! empty($down)) {
            $up = implode("\n", $up);
            $down = implode("\n", $down);
            $className = 'Version' . $this->migration->getNextMigrationClassVersion();
            $this->generateMigrationClass($className, $buildoptions, $up, $down);
        }

        $up = array();
        $down = array();
        if ( ! empty($changes['dropped_foreign_keys'])) {
            foreach ($changes['dropped_foreign_keys'] as $tableName => $droppedFks) {
                if ( ! empty($changes['dropped_tables']) && isset($changes['dropped_tables'][$tableName])) { 
                    continue; 
                }

                foreach ($droppedFks as $name => $foreignKey) {
                    $up[] = $this->buildDropForeignKey($tableName, $foreignKey);
                    $down[] = $this->buildCreateForeignKey($tableName, $foreignKey);
                }
            }
        }

        if ( ! empty($changes['dropped_indexes'])) {
            foreach ($changes['dropped_indexes'] as $tableName => $removedIndexes) {
                if ( ! empty($changes['dropped_tables']) && isset($changes['dropped_tables'][$tableName])) { 
                    continue; 
                }

                foreach ($removedIndexes as $name => $index) {
                    $up[] = $this->buildRemoveIndex($tableName, $name, $index);
                    $down[] = $this->buildAddIndex($tableName, $name, $index);
                }
            }
        }

        if ( ! empty($changes['created_foreign_keys'])) {
            foreach ($changes['created_foreign_keys'] as $tableName => $createdFks) {
                if ( ! empty($changes['dropped_tables']) && isset($changes['dropped_tables'][$tableName])) { 
                    continue; 
                }

                foreach ($createdFks as $name => $foreignKey) {
                    $up[] = $this->buildCreateForeignKey($tableName, $foreignKey);
                    $down[] = $this->buildDropForeignKey($tableName, $foreignKey);
                }
            }
        }

        if ( ! empty($changes['created_indexes'])) {
            foreach ($changes['created_indexes'] as $tableName => $addedIndexes) {
                if ( ! empty($changes['dropped_tables']) && isset($changes['dropped_tables'][$tableName])) { 
                    continue; 
                }

                foreach ($addedIndexes as $name => $index) {
                    if (isset($changes['created_tables'][$tableName]['options']['indexes'][$name])) {
                        continue;
                    }
                    $up[] = $this->buildAddIndex($tableName, $name, $index);
                    $down[] = $this->buildRemoveIndex($tableName, $name, $index);
                }
            }
        }

        if ( ! empty($up) || ! empty($down)) {
            $up = implode("\n", $up);
            $down = implode("\n", $down);
            $className = 'Version' . $this->migration->getNextMigrationClassVersion();
            $this->generateMigrationClass($className, $buildoptions, $up, $down);
        }
        return $changes;
    }

    /**
     * Generate a set of migration classes from the existing databases
     *
     * @return void
     */
    public function generateMigrationsFromDb()
    {
        $directory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'tmp_doctrine_models';

        Doctrine_Core::generateModelsFromDb($directory);

        $result = $this->generateMigrationsFromModels($directory, Doctrine_Core::MODEL_LOADING_CONSERVATIVE);

        Doctrine_Lib::removeDirectories($directory);

        return $result;
    }

    /**
     * Generate a set of migrations from a set of models
     *
     * @param  string $modelsPath    Path to models
     * @param  string $modelLoading  What type of model loading to use when loading the models
     * @return boolean
     */
    public function generateMigrationsFromModels($modelsPath = null, $modelLoading = null, Array $buildoptions = array())
    {
        if ($modelsPath !== null) {
            $models = Doctrine_Core::filterInvalidModels(Doctrine_Core::loadModels($modelsPath, $modelLoading));
        } else {
            $models = Doctrine_Core::getLoadedModels();
        }

        $models = Doctrine_Core::initializeModels($models);

        $foreignKeys = array();

        foreach ($models as $model) {
            $table = Doctrine_Core::getTable($model);
            if ($table->getTableName() !== $this->migration->getTableName()) {
                $export = $table->getExportableFormat();

                $foreignKeys[$export['tableName']] = $export['options']['foreignKeys'];

                $up = $this->buildCreateTable($export);
                $down = $this->buildDropTable($export);

                $className = 'Add' . Doctrine_Inflector::classify($export['tableName']);

                $this->generateMigrationClass($className, $buildoptions, $up, $down);
            }
        }

        if ( ! empty($foreignKeys)) {
            $className = 'AddFks';

            $up = array();
            $down = array();
            foreach ($foreignKeys as $tableName => $definitions)    {
                $tableForeignKeyNames[$tableName] = array();

                foreach ($definitions as $definition) {
                    $up[] = $this->buildCreateForeignKey($tableName, $definition);
                    $down[] = $this->buildDropForeignKey($tableName, $definition);
                }
            }

            $up = implode("\n", $up);
            $down = implode("\n", $down);
            if ($up || $down) {
                $this->generateMigrationClass($className, $buildoptions, $up, $down);
            }
        }

        return true;
    }

    /**
     * Build the code for creating foreign keys
     *
     * @param  string $tableName
     * @param  array  $definition
     * @return string $code
     */
    public function buildCreateForeignKey($tableName, $definition)
    {
        return "        \$this->createForeignKey('" . $tableName . "', '" . $definition['name'] . "', " . $this->varExport($definition, true) . ");";
    }

    /**
     * Build the code for dropping foreign keys
     *
     * @param  string $tableName
     * @param  array  $definition
     * @return string $code
     */
    public function buildDropForeignKey($tableName, $definition)
    {
        return "        \$this->dropForeignKey('" . $tableName . "', '" . $definition['name'] . "');";
    }

    /**
     * Build the code for creating tables
     *
     * @param  string $tableData
     * @return string $code
     */
    public function buildCreateTable($tableData)
    {
        $code  = "        \$this->createTable('" . $tableData['tableName'] . "', ";

        $code .= $this->varExport($tableData['columns'], true) . ", ";

        $optionsWeNeed = array('type', 'indexes', 'primary', 'collate', 'charset');

        $options = array();
        foreach ($optionsWeNeed as $option) {
            if (isset($tableData['options'][$option])) {
                $options[$option] = $tableData['options'][$option];
            }
        }

        $code .= $this->varExport($options, true);

        $code .= ");";

        return $code;
    }

    /**
     * Build the code for dropping tables
     *
     * @param  string $tableData
     * @return string $code
     */
    public function buildDropTable($tableData)
    {
        return "        \$this->dropTable('" . $tableData['tableName'] . "');";
    }

    /**
     * Build the code for adding columns
     *
     * @param string $tableName
     * @param string $columnName
     * @param string $column
     * @return string $code
     */
    public function buildAddColumn($tableName, $columnName, $column)
    {
        $length = $column['length'];
        $type = $column['type'];
        unset($column['length'], $column['type']);
        return "        \$this->addColumn('" . $tableName . "', '" . $columnName. "', '" . $type . "', '" . $length . "', " . $this->varExport($column) . ");";
    }

    /**
     * Build the code for removing columns
     *
     * @param string $tableName
     * @param string $columnName
     * @param string $column
     * @return string $code
     */
    public function buildRemoveColumn($tableName, $columnName, $column)
    {
        return "        \$this->removeColumn('" . $tableName . "', '" . $columnName. "');";
    }

    /**
     * Build the code for changing columns
     *
     * @param string $tableName
     * @param string $columnName
     * @param string $column
     * @return string $code
     */
    public function buildChangeColumn($tableName, $columnName, $column)
    {
        $length = $column['length'];
        $type = $column['type'];
        unset($column['length'], $column['type']);
        return "        \$this->changeColumn('" . $tableName . "', '" . $columnName. "', '" . $type . "', '" . $length . "', " . $this->varExport($column) . ");";
    }

    /**
     * Build the code for adding indexes
     *
     * @param string $tableName
     * @param string $indexName
     * @param string $index
     * @return sgtring $code
     */
    public function buildAddIndex($tableName, $indexName, $index)
    {
        return "        \$this->addIndex('$tableName', '$indexName', " . $this->varExport($index) . ");";
    }

    /**
     * Build the code for removing indexes
     *
     * @param string $tableName
     * @param string $indexName
     * @param string $index
     * @return string $code
     */
    public function buildRemoveIndex($tableName, $indexName, $index)
    {
        return "        \$this->removeIndex('$tableName', '$indexName', " . $this->varExport($index) . ");";
    }

    /**
     * Generate a migration class
     *
     * @param string  $className   Class name to generate
     * @param array   $options     Options for the migration class
     * @param string  $up          The code for the up function
     * @param string  $down        The code for the down function
     * @param boolean $return      Whether or not to return the code.
     *                             If true return and false it writes the class to disk.
     * @return mixed
     */
    public function generateMigrationClass($className, $options = array(), $up = null, $down = null, $return = false)
    {
        $className = Doctrine_Inflector::urlize($className);
        $className = str_replace('-', '_', $className);
        $className = Doctrine_Inflector::classify($className);

        if ($return || ! $this->getMigrationsPath()) {
            return $this->buildMigrationClass($className, null, $options, $up, $down);
        } else {
            if ( ! $this->getMigrationsPath()) {
                throw new Doctrine_Migration_Exception('You must specify the path to your migrations.');
            }

            $next = time() + $this->migration->getNextMigrationClassVersion();
            $fileName = $next . '_' . Doctrine_Inflector::tableize($className) . $this->suffix;

            $class = $this->buildMigrationClass($className, $fileName, $options, $up, $down);

            $path = $this->getMigrationsPath() . DIRECTORY_SEPARATOR . $fileName;
            if (class_exists($className) || file_exists($path)) {
                $this->migration->loadMigrationClass($className);
                return false;
            }

            file_put_contents($path, $class);
            require_once($path);
            $this->migration->loadMigrationClass($className);

            return true;
        }
    }

    /**
     * Build the code for a migration class
     *
     * @param string  $className   Class name to generate
     * @param string  $fileName    File name to write the class to
     * @param array   $options     Options for the migration class
     * @param string  $up          The code for the up function
     * @param string  $down        The code for the down function
     * @return string $content     The code for the generated class
     */
    public function buildMigrationClass($className, $fileName = null, $options = array(), $up = null, $down = null)
    {
        $extends = isset($options['extends']) ? $options['extends']:'Doctrine_Migration_Base';

        $content  = '<?php' . PHP_EOL;

        $content .= sprintf(self::$tpl, $className,
                                       $extends,
                                       $up,
                                       $down);

        return $content;
    }
}