<?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\Command;

use Propel\Generator\Config\GeneratorConfig;
use RuntimeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;

/**
 * @author William Durand <william.durand1@gmail.com>
 */
abstract class AbstractCommand extends Command
{
    /**
     * @var string
     */
    public const DEFAULT_CONFIG_DIRECTORY = '.';

    /**
     * @var int
     */
    public const CODE_SUCCESS = 0;

    /**
     * @var int
     */
    public const CODE_ERROR = 1;

    /**
     * @var \Symfony\Component\Filesystem\Filesystem|null
     */
    protected $filesystem;

    /**
     * {@inheritDoc}
     *
     * @return void
     */
    #[\Override]
    protected function configure()
    {
        $this
            ->addOption('platform', null, InputOption::VALUE_REQUIRED, 'The platform to use. Define a full qualified class name or mysql|pgsql|sqlite|mssql|oracle.')
            ->addOption('config-dir', null, InputOption::VALUE_REQUIRED, 'The directory where the configuration file is placed.', self::DEFAULT_CONFIG_DIRECTORY)
            ->addOption('recursive', null, InputOption::VALUE_NONE, 'Search recursive for *schema.xml inside the input directory');
    }

    /**
     * Returns a new `GeneratorConfig` object with your `$properties` merged with
     * the configuration properties in the `config-dir` folder.
     *
     * @param array|null $properties Properties to add to the configuration. They usually come from command line.
     * @param \Symfony\Component\Console\Input\InputInterface|null $input
     *
     * @return \Propel\Generator\Config\GeneratorConfig
     */
    protected function getGeneratorConfig(?array $properties = null, ?InputInterface $input = null): GeneratorConfig
    {
        if ($input === null) {
            return new GeneratorConfig(null, $properties);
        }

        if ($this->hasInputOption('platform', $input)) {
            $properties['propel']['generator']['platformClass'] = $input->getOption('platform');
        }

        if ($input->hasParameterOption('--recursive')) {
            $properties['propel']['generator']['recursive'] = $input->getOption('recursive');
        }

        return new GeneratorConfig($input->getOption('config-dir'), $properties);
    }

    /**
     * Find every schema files.
     *
     * @param array<string>|string $directory Path to the input directory
     * @param bool $recursive Search for file inside the input directory and all subdirectories
     *
     * @return array List of schema files
     */
    protected function getSchemas($directory, bool $recursive = false): array
    {
        $finder = new Finder();
        $finder
            ->name('*schema.xml')
            ->sortByName()
            ->in($directory);
        if (!$recursive) {
            $finder->depth(0);
        }

        return iterator_to_array($finder->files());
    }

    /**
     * Returns a Filesystem instance.
     *
     * @return \Symfony\Component\Filesystem\Filesystem
     */
    protected function getFilesystem(): Filesystem
    {
        if ($this->filesystem === null) {
            $this->filesystem = new Filesystem();
        }

        return $this->filesystem;
    }

    /**
     * @param string $directory
     *
     * @throws \RuntimeException
     *
     * @return void
     */
    protected function createDirectory(string $directory): void
    {
        $filesystem = $this->getFilesystem();

        try {
            $filesystem->mkdir($directory);
        } catch (IOException $e) {
            throw new RuntimeException(sprintf('Unable to write the "%s" directory', $directory), 0, $e);
        }
    }

    /**
     * Parses a connection string and returns an array with name, DSN and extra information.
     *
     * @param string $connection The connection string
     *
     * @return array
     */
    protected function parseConnection(string $connection): array
    {
        $firstEqualsSignPosition = strpos($connection, '=') ?: null;
        $name = substr($connection, 0, $firstEqualsSignPosition);
        $remainder = substr($connection, $firstEqualsSignPosition + 1);

        $firstColonPosition = strpos($remainder, ':') ?: null;
        $adapter = substr($remainder, 0, $firstColonPosition);
        $remainder = substr($remainder, $firstColonPosition + 1);

        $extras = [];
        $dsn = $adapter . ':';
        foreach (explode(';', $remainder) as $element) {
            $parts = explode('=', $element);

            if (count($parts) === 2) {
                $key = strtolower($parts[0]);
                $value = urldecode($parts[1]);
                $extras[$key] = $value;

                if ($key === 'user' || $key === 'password') {
                    // dsn can't contain user or password for (at least) sql server
                    continue;
                }
            }

            $dsn .= $element . ';';
        }
        $extras['adapter'] = $adapter;

        return [$name, trim($dsn, ';'), $extras];
    }

    /**
     * Parse a connection string and return an array of properties to pass to GeneratorConfig constructor.
     * The connection must be in the following format:
     * `bookstore=mysql:host=127.0.0.1;dbname=test;user=root;password=foobar`
     * where "bookstore" is your propel database name (used in your schema.xml).
     *
     * @param string $connection The connection string
     * @param string|null $section The section where the connection must be registered in (generator, runtime...)
     *
     * @return array
     */
    protected function connectionToProperties(string $connection, ?string $section = null): array
    {
        [$name, $dsn, $infos] = $this->parseConnection($connection);
        $config['propel']['database']['connections'][$name]['classname'] = '\Propel\Runtime\Connection\ConnectionWrapper';
        $config['propel']['database']['connections'][$name]['adapter'] = strtolower($infos['adapter']);
        $config['propel']['database']['connections'][$name]['dsn'] = $dsn;
        $config['propel']['database']['connections'][$name]['user'] = isset($infos['user']) && $infos['user'] ? $infos['user'] : null;
        $config['propel']['database']['connections'][$name]['password'] = $infos['password'] ?? null;

        if ($section === null) {
            $section = 'generator';
        }

        if ($section === 'reverse') {
            $config['propel']['reverse']['connection'] = $name;
        } else {
            $config['propel'][$section]['connections'][] = $name;
        }

        return $config;
    }

    /**
     * Check if a given input option exists and it isn't null.
     *
     * @param string $option The name of the input option to check
     * @param \Symfony\Component\Console\Input\InputInterface $input object
     *
     * @return bool
     */
    protected function hasInputOption(string $option, InputInterface $input): bool
    {
        return $input->hasOption($option) && $input->getOption($option) !== null;
    }

    /**
     * Check if a given input argument exists and it isn't null.
     *
     * @param string $argument The name of the input argument to check
     * @param \Symfony\Component\Console\Input\InputInterface $input object
     *
     * @return bool
     */
    protected function hasInputArgument(string $argument, InputInterface $input): bool
    {
        return $input->hasArgument($argument) && $input->getArgument($argument) !== null;
    }
}
