<?php

/**
 * MIT License. This file is part of the Propel package.
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace Propel\Generator\Behavior\I18n;

use Propel\Generator\Behavior\Validate\ValidateBehavior;
use Propel\Generator\Builder\Om\AbstractOMBuilder;
use Propel\Generator\Exception\EngineException;
use Propel\Generator\Model\Behavior;
use Propel\Generator\Model\Column;
use Propel\Generator\Model\ForeignKey;
use Propel\Generator\Model\PropelTypes;
use Propel\Generator\Model\Table;

/**
 * Allows translation of text columns through transparent one-to-many
 * relationship.
 *
 * @author Francois Zaninotto
 */
class I18nBehavior extends Behavior
{
    /**
     * @var string
     */
    public const DEFAULT_LOCALE = 'en_US';

    /**
     * Default parameters value
     *
     * @var array<string, mixed>
     */
    protected $parameters = [
        'i18n_table' => '%TABLE%_i18n',
        'i18n_phpname' => '%PHPNAME%I18n',
        'i18n_columns' => '',
        'i18n_pk_column' => null,
        'locale_column' => 'locale',
        'locale_length' => 5,
        'default_locale' => null,
        'locale_alias' => '',
    ];

    /**
     * @var int
     */
    protected $tableModificationOrder = 70;

    /**
     * @var \Propel\Generator\Behavior\I18n\I18nBehaviorObjectBuilderModifier|null
     */
    protected $objectBuilderModifier;

    /**
     * @var \Propel\Generator\Behavior\I18n\I18nBehaviorQueryBuilderModifier|null
     */
    protected $queryBuilderModifier;

    /**
     * @var \Propel\Generator\Model\Table
     */
    protected $i18nTable;

    /**
     * @return void
     */
    #[\Override]
    public function modifyDatabase(): void
    {
        foreach ($this->getDatabase()->getTables() as $table) {
            if ($table->hasBehavior('i18n') && !$table->getBehavior('i18n')->getParameter('default_locale')) {
                $table->getBehavior('i18n')->addParameter([
                    'name' => 'default_locale',
                    'value' => $this->getParameter('default_locale'),
                ]);
            }
        }
    }

    /**
     * @return string
     */
    public function getDefaultLocale(): string
    {
        $defaultLocale = $this->getParameter('default_locale');
        if (!$defaultLocale) {
            $defaultLocale = self::DEFAULT_LOCALE;
        }

        return $defaultLocale;
    }

    /**
     * @return \Propel\Generator\Model\Table
     */
    public function getI18nTable(): Table
    {
        return $this->i18nTable;
    }

    /**
     * @return \Propel\Generator\Model\ForeignKey|null
     */
    public function getI18nForeignKey(): ?ForeignKey
    {
        foreach ($this->i18nTable->getForeignKeys() as $fk) {
            if ($fk->getForeignTableName() == $this->table->getName()) {
                return $fk;
            }
        }

        return null;
    }

    /**
     * @return \Propel\Generator\Model\Column|null
     */
    public function getLocaleColumn(): ?Column
    {
        return $this->getI18nTable()->getColumn($this->getLocaleColumnName());
    }

    /**
     * @return list<\Propel\Generator\Model\Column>
     */
    public function getI18nColumns(): array
    {
        $columns = [];
        $i18nTable = $this->getI18nTable();
        $columnNames = $this->getI18nColumnNamesFromConfig();

        if ($columnNames) {
            // Strategy 1: use the i18n_columns parameter
            foreach ($columnNames as $columnName) {
                /** @var \Propel\Generator\Model\Column $column */
                $column = $i18nTable->getColumn($columnName);
                $columns[] = $column;
            }
        } else {
            // strategy 2: use the columns of the i18n table
            // warning: does not work when database behaviors add columns to all tables
            // (such as timestampable behavior)
            foreach ($i18nTable->getColumns() as $column) {
                if (!$column->isPrimaryKey()) {
                    $columns[] = $column;
                }
            }
        }

        return $columns;
    }

    /**
     * @param string $string
     *
     * @return string
     */
    public function replaceTokens(string $string): string
    {
        $table = $this->getTable();

        return strtr($string, [
            '%TABLE%' => $table->getOriginCommonName(),
            '%PHPNAME%' => $table->getPhpName(),
        ]);
    }

    /**
     * @return $this|\Propel\Generator\Behavior\I18n\I18nBehaviorObjectBuilderModifier
     */
    #[\Override]
    public function getObjectBuilderModifier()
    {
        if ($this->objectBuilderModifier === null) {
            $this->objectBuilderModifier = new I18nBehaviorObjectBuilderModifier($this);
        }

        return $this->objectBuilderModifier;
    }

    /**
     * @return $this|\Propel\Generator\Behavior\I18n\I18nBehaviorQueryBuilderModifier
     */
    #[\Override]
    public function getQueryBuilderModifier()
    {
        if ($this->queryBuilderModifier === null) {
            $this->queryBuilderModifier = new I18nBehaviorQueryBuilderModifier($this);
        }

        return $this->queryBuilderModifier;
    }

    /**
     * @param \Propel\Generator\Builder\Om\AbstractOMBuilder $builder
     *
     * @return string
     */
    public function staticAttributes(AbstractOMBuilder $builder): string
    {
        return $this->renderTemplate('staticAttributes', [
            'defaultLocale' => $this->getDefaultLocale(),
        ]);
    }

    /**
     * @return void
     */
    #[\Override]
    public function modifyTable(): void
    {
        $this->addI18nTable();
        $this->relateI18nTableToMainTable();
        $this->addLocaleColumnToI18n();
        $this->moveI18nColumns();
    }

    /**
     * @return void
     */
    protected function addI18nTable(): void
    {
        $table = $this->getTable();
        $database = $table->getDatabase();
        $i18nTableName = $this->getI18nTableName();

        if ($database->hasTable($i18nTableName)) {
            $this->i18nTable = $database->getTable($i18nTableName);
        } else {
            $this->i18nTable = $database->addTable([
                'name' => $i18nTableName,
                'phpName' => $this->getI18nTablePhpName(),
                'package' => $table->getPackage(),
                'schema' => $table->getSchema(),
                'namespace' => $table->getNamespace() ? '\\' . $table->getNamespace() : null,
                'skipSql' => $table->isSkipSql(),
                'identifierQuoting' => $table->isIdentifierQuotingEnabled(),
            ]);

            // every behavior adding a table should re-execute database behaviors
            foreach ($database->getBehaviors() as $behavior) {
                $behavior->modifyDatabase();
            }
        }
    }

    /**
     * @throws \Propel\Generator\Exception\EngineException
     *
     * @return void
     */
    protected function relateI18nTableToMainTable(): void
    {
        $table = $this->getTable();
        $i18nTable = $this->i18nTable;
        $pks = $this->getTable()->getPrimaryKey();

        if (count($pks) > 1) {
            throw new EngineException('The i18n behavior does not support tables with composite primary keys');
        }

        $column = $pks[0];
        $i18nColumn = clone $column;

        if ($this->getParameter('i18n_pk_column')) {
            // custom i18n table pk name
            $i18nColumn->setName($this->getParameter('i18n_pk_column'));
        } elseif (in_array($table->getName(), $i18nTable->getForeignTableNames(), true)) {
            // custom i18n table pk name not set, but some fk already exists
            return;
        }

        if (!$i18nTable->hasColumn($i18nColumn->getName())) {
            $i18nColumn->setAutoIncrement(false);
            $i18nTable->addColumn($i18nColumn);
        }

        $fk = new ForeignKey();
        $fk->setForeignTableCommonName($table->getCommonName());
        $fk->setForeignSchemaName($table->getSchema());
        $fk->setDefaultJoin('LEFT JOIN');
        $fk->setOnDelete(ForeignKey::CASCADE);
        $fk->setOnUpdate(ForeignKey::NONE);
        $fk->addReference($i18nColumn->getName(), $column->getName());

        $i18nTable->addForeignKey($fk);
    }

    /**
     * @return void
     */
    protected function addLocaleColumnToI18n(): void
    {
        $localeColumnName = $this->getLocaleColumnName();

        if (!$this->i18nTable->hasColumn($localeColumnName)) {
            $this->i18nTable->addColumn([
                'name' => $localeColumnName,
                'type' => PropelTypes::VARCHAR,
                'size' => $this->getParameter('locale_length') ? (int)$this->getParameter('locale_length') : 5,
                'default' => $this->getDefaultLocale(),
                'primaryKey' => 'true',
            ]);
        }
    }

    /**
     * Moves i18n columns from the main table to the i18n table
     *
     * @throws \Propel\Generator\Exception\EngineException
     *
     * @return void
     */
    protected function moveI18nColumns(): void
    {
        $table = $this->getTable();
        $i18nTable = $this->i18nTable;

        $i18nValidateParams = [];
        foreach ($this->getI18nColumnNamesFromConfig() as $columnName) {
            if (!$i18nTable->hasColumn($columnName)) {
                if (!$table->hasColumn($columnName)) {
                    throw new EngineException(sprintf('No column named `%s` found in table `%s`', $columnName, $table->getName()));
                }

                $column = $table->getColumn($columnName);
                $i18nTable->addColumn(clone $column);

                // validate behavior: move rules associated to the column
                if ($table->hasBehavior('validate')) {
                    /** @var \Propel\Generator\Behavior\Validate\ValidateBehavior $validateBehavior */
                    $validateBehavior = $table->getBehavior('validate');
                    $params = $validateBehavior->getParametersFromColumnName($columnName);
                    $i18nValidateParams = array_merge($i18nValidateParams, $params);
                    $validateBehavior->removeParametersFromColumnName($columnName);
                }
                // FIXME: also move FKs, and indices on this column
            }

            if ($table->hasColumn($columnName)) {
                $table->removeColumn($columnName);
            }
        }

        // validate behavior
        if (count($i18nValidateParams) > 0) {
            $i18nVbehavior = new ValidateBehavior();
            $i18nVbehavior->setName('validate');
            $i18nVbehavior->setParameters($i18nValidateParams);
            $i18nTable->addBehavior($i18nVbehavior);

            // current table must have almost 1 validation rule
            /** @var \Propel\Generator\Behavior\Validate\ValidateBehavior $validate */
            $validate = $table->getBehavior('validate');
            $validate->addRuleOnPk();
        }
    }

    /**
     * @return string
     */
    protected function getI18nTableName(): string
    {
        return $this->replaceTokens($this->getParameter('i18n_table'));
    }

    /**
     * @return string
     */
    protected function getI18nTablePhpName(): string
    {
        return $this->replaceTokens($this->getParameter('i18n_phpname'));
    }

    /**
     * @return string
     */
    protected function getLocaleColumnName(): string
    {
        return $this->replaceTokens($this->getParameter('locale_column'));
    }

    /**
     * @return array<string>
     */
    protected function getI18nColumnNamesFromConfig(): array
    {
        $columnNames = explode(',', $this->getParameter('i18n_columns'));

        foreach ($columnNames as $key => $columnName) {
            $columnName = trim($columnName);

            if ($columnName) {
                $columnNames[$key] = $columnName;
            } else {
                unset($columnNames[$key]);
            }
        }

        return $columnNames;
    }
}
