<?php

namespace Bolt\Storage\Database\Schema;

use Bolt\Common\Deprecated;
use Bolt\Events\SchemaEvent;
use Bolt\Events\SchemaEvents;
use Bolt\Storage\Database\Schema\Table\BaseTable;
use Doctrine\DBAL\Schema\Schema;
use Silex\Application;

/**
 * Manager class for Bolt database schema.
 *
 * Based on on parts of the monolithic Bolt\Database\IntegrityChecker class.
 *
 * @author Gawain Lynch <gawain.lynch@gmail.com>
 */
class Manager implements SchemaManagerInterface
{
    /** @var \Doctrine\DBAL\Connection */
    protected $connection;
    /** @var \Bolt\Config */
    protected $config;
    /** @var \Doctrine\DBAL\Schema\Schema */
    protected $schema;
    /** @var \Doctrine\DBAL\Schema\Table[] */
    protected $schemaTables;
    /** @var \Doctrine\DBAL\Schema\Table[] */
    protected $installedTables;

    /** @var \Silex\Application */
    private $app;

    /** @deprecated Deprecated since 3.0, to be removed in 4.0. */
    const INTEGRITY_CHECK_INTERVAL = 1800; // max. validity of a database integrity check, in seconds
    const INTEGRITY_CHECK_TS_FILENAME = 'dbcheck_ts'; // filename for the check timestamp file

    /**
     * Constructor.
     *
     * @param Application $app
     */
    public function __construct(Application $app)
    {
        $this->app = $app;
        $this->connection = $app['db'];
        $this->config = $app['config'];
    }

    /**
     * @deprecated Deprecated since 3.0, to be removed in 4.0. This is a place holder to prevent fatal errors.
     *
     * @param string $name
     * @param mixed  $args
     */
    public function __call($name, $args)
    {
        Deprecated::raw("An extension called an invalid, or removed, integrity checker function: $name. This will throw a fatal error in 4.0.");
    }

    /**
     * @deprecated Deprecated since 3.0, to be removed in 4.0. This is a place holder to prevent fatal errors.
     *
     * @param string $name
     */
    public function __get($name)
    {
        Deprecated::raw("An extension accessed an invalid, or removed, integrity checker property: $name. This will throw a fatal error in 4.0.");
    }

    /**
     * Get the database name of a table from an alias.
     *
     * @param string $name
     *
     * @return string|null
     */
    public function getTableName($name)
    {
        $tableName = null;
        if (isset($this->app['schema.tables'][$name])) {
            /** @var BaseTable $table */
            $table = $this->app['schema.tables'][$name];
            $tableName = $table->getTableName();
        }

        return $tableName;
    }

    /**
     * {@inheritdoc}
     */
    public function isCheckRequired()
    {
        return $this->getSchemaTimer()->isCheckRequired();
    }

    /**
     * {@inheritdoc}
     */
    public function isUpdateRequired()
    {
        $fromTables = $this->getInstalledTables();
        $toTables = $this->getSchemaTables();
        $protectedTableNames = $this->app['schema.content_tables']->keys();

        $pending = $this->getSchemaComparator()->hasPending($fromTables, $toTables, $protectedTableNames);

        if (!$pending) {
            $this->getSchemaTimer()->setCheckExpiry();
        }

        return $pending;
    }

    /**
     * Run a check against current and configured schemas.
     *
     * @return SchemaCheck
     */
    public function check()
    {
        $fromTables = $this->getInstalledTables();
        $toTables = $this->getSchemaTables();
        $protectedTableNames = $this->app['schema.content_tables']->keys();

        $response = $this->getSchemaComparator()->compare($fromTables, $toTables, $protectedTableNames);
        if (!$response->hasResponses()) {
            $this->getSchemaTimer()->setCheckExpiry();
        }

        return $response;
    }

    /**
     * Run database table updates.
     *
     * @return \Bolt\Storage\Database\Schema\SchemaCheck
     */
    public function update()
    {
        // Do the initial check
        $fromTables = $this->getInstalledTables();
        $toTables = $this->getSchemaTables();
        $protectedTableNames = $this->app['schema.content_tables']->keys();

        $this->getSchemaComparator()->compare($fromTables, $toTables, $protectedTableNames, true);
        $response = $this->getSchemaComparator()->getResponse();
        $creates = $this->getSchemaComparator()->getCreates();
        $alters = $this->getSchemaComparator()->getAlters();

        $modifier = new TableModifier($this->connection, $this->app['logger.system'], $this->app['logger.flash']);
        $modifier->createTables($creates, $response);
        $modifier->alterTables($alters, $response);

        $event = new SchemaEvent($creates, $alters);
        $this->app['dispatcher']->dispatch(SchemaEvents::UPDATE, $event);

        // Recheck now that we've processed
        $fromTables = $this->getInstalledTables();
        $toTables = $this->getSchemaTables();
        $this->getSchemaComparator()->compare($fromTables, $toTables, $protectedTableNames);
        if (!$this->getSchemaComparator()->hasPending($fromTables, $toTables, $protectedTableNames)) {
            $this->getSchemaTimer()->setCheckExpiry();
        }

        return $response;
    }

    /**
     * Check if just the users table is present.
     *
     * @return bool
     */
    public function hasUserTable()
    {
        $tables = $this->getInstalledTables();
        if (isset($tables['users'])) {
            return true;
        }

        return false;
    }

    /**
     * Get the built schema.
     *
     * @return \Doctrine\DBAL\Schema\Schema
     */
    public function getSchema()
    {
        if ($this->schema === null) {
            $this->getSchemaTables();
        }

        return $this->schema;
    }

    /**
     * Get a merged array of tables.
     *
     * @return \Doctrine\DBAL\Schema\Table[]
     */
    public function getSchemaTables()
    {
        if ($this->schemaTables !== null) {
            return $this->schemaTables;
        }
        $builder = $this->app['schema.builder'];

        /** @deprecated Deprecated since 3.0, to be removed in 4.0. */
        $builder['extensions']->addPrefix($this->app['schema.prefix']);

        $schema = new Schema();
        $tables = array_merge(
            $builder['base']->getSchemaTables($schema),
            $builder['content']->getSchemaTables($schema, $this->config),
            $builder['extensions']->getSchemaTables($schema)
        );
        $this->schema = $schema;

        return $this->schemaTables = $tables;
    }

    /**
     * Get the installed table list from Doctrine.
     *
     * @return \Doctrine\DBAL\Schema\Table[]
     */
    public function getInstalledTables()
    {
        if ($this->installedTables !== null) {
            return $this->installedTables;
        }

        /** @var $tables \Doctrine\DBAL\Schema\Table[] */
        $tables = $this->connection->getSchemaManager()->listTables();
        foreach ($tables as $table) {
            $alias = str_replace($this->app['schema.prefix'], '', $table->getName());
            $this->installedTables[$alias] = $table;
        }

        return $this->installedTables;
    }

    /**
     * This method allows extensions to register their own tables.
     *
     * @param callable $generator a generator function that takes the Schema
     *                            instance and returns a table or an array of
     *                            tables
     */
    public function registerExtensionTable(callable $generator)
    {
        $this->app['schema.builder']['extensions']->addTable($generator);
    }

    /**
     * @return \Bolt\Storage\Database\Schema\Timer
     */
    private function getSchemaTimer()
    {
        return $this->app['schema.timer'];
    }

    /**
     * @return \Bolt\Storage\Database\Schema\Comparison\BaseComparator
     */
    private function getSchemaComparator()
    {
        return $this->app['schema.comparator'];
    }
}
