<?php
namespace Concrete\Core\Console\Command;

use Concrete\Core\Config\DirectFileSaver;
use Concrete\Core\Config\FileSaver;
use Concrete\Core\Config\FileLoader;
use Concrete\Core\Config\Repository\Repository;
use Illuminate\Filesystem\Filesystem;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Config;
use Exception;

class ConfigCommand extends Command
{
    const OPERATION_GET = 'get';
    const OPERATION_SET = 'set';

    /**
     * @var Repository
     */
    protected $repository;

    protected function configure()
    {
        $this
            ->setName('c5:config')
            ->setDescription('Set or get configuration parameters.')
            ->addArgument('operation', InputArgument::REQUIRED, 'The operation to accomplish ('.implode('|', $this->getAllowedOperations()).')')
            ->addArgument('item', InputArgument::REQUIRED, 'The configuration item (eg: concrete.debug.display_errors)')
            ->addArgument('value', InputArgument::OPTIONAL, 'The new value of the configuration item')
            ->addOption('environment', 'e', InputOption::VALUE_REQUIRED, 'The environment (if not specified, we\'ll work with the configuration item valid for all environments)')
            ->addOption('generated-overrides', 'g', InputOption::VALUE_NONE, 'Set this option to save configurations to the generated_overrides folder')
        ;
        $this->setHelp(<<<EOT
When setting values that may be evaluated as boolean (true/false), null or numbers, but you want to store them as strings, you can enclose those values in single or double quotes.
For instance, with
concrete5 %command.name% set concrete.test_item 1
The new configuration item will have a numeric value of 1. If you want to save the string "1" you have to write
concrete5 %command.name% set concrete.test_item '1'

Returns codes:
  0 operation completed successfully
  1 errors occurred
EOT
        );
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $rc = 0;

        try {
            $default_environment = \Config::getEnvironment();

            $environment = $input->getOption('environment') ?: $default_environment;

            $file_system = new Filesystem();
            $file_loader = new FileLoader($file_system);
            if ($input->getOption('generated-overrides')) {
                $file_saver = new FileSaver($file_system, $environment == $default_environment ? null : $environment);
            } else {
                $file_saver = new DirectFileSaver($file_system, $environment == $default_environment ? null : $environment);
            }
            $this->repository = new Repository($file_loader, $file_saver, $environment);

            $item = $input->getArgument('item');
            switch ($input->getArgument('operation')) {
                case self::OPERATION_GET:
                    $output->writeln($this->serialize($this->repository->get($item)));
                    break;

                case self::OPERATION_SET:
                    $value = $input->getArgument('value');
                    if (!isset($value)) {
                        throw new Exception('Missing new configuration value');
                    }

                    $this->repository->save($item, $this->unserialize($value));
                    break;

                default:
                    throw new Exception('Invalid operation specified. Allowed operations: '.implode(', ', $this->getAllowedOperations()));
            }
        } catch (Exception $x) {
            $output->writeln('<error>'.$x->getMessage().'</error>');
            $rc = 1;
        }

        return $rc;
    }

    /**
     * @return string[]
     */
    protected function getAllowedOperations()
    {
        return array(
            self::OPERATION_GET,
            self::OPERATION_SET,
        );
    }

    /**
     * @param mixed $value
     *
     * @return string
     *
     * @throws Exception
     */
    protected function serialize($value)
    {
        $jsonOptions = JSON_PRETTY_PRINT;
        if (defined('JSON_UNESCAPED_SLASHES')) {
            $jsonOptions |= JSON_UNESCAPED_SLASHES;
        }
        $type = gettype($value);
        $result = null;
        switch ($type) {
            case 'array':
                $result = json_encode($value, $jsonOptions);
                break;

            case 'boolean':
                $result = $value ? 'true' : 'false';
                break;

            case 'NULL':
                $result = 'null';
                break;

            case 'integer':
            case 'double':
                $result = (string) $value;
                break;

            case 'string':
                $enquote = false;
                switch ($value) {
                    case 'true':
                    case 'false':
                    case 'null':
                        $enquote = true;
                        break;

                    default:
                        if (preg_match('/^-?\d+(\.\d*)?$/', $value)) {
                            $enquote = true;
                        }
                        break;

                }
                $result = $enquote ? "\"$value\"" : $value;
                break;

        }
        if (!isset($result)) {
            throw new Exception("Unable to represent variable of type '$type'");
        }

        return $result;
    }

    /**
     * @param string $value
     *
     * @return mixed
     *
     * @throws Exception
     */
    protected function unserialize($value)
    {
        $result = json_decode($value, true);
        if (is_null($result) && trim(strtolower($value)) !== 'null') {
            return (string) $value;
        }

        return $result;
    }
}
