<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2015 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: yunwuxin <448901948@qq.com>
// +----------------------------------------------------------------------

namespace think\console\input;

class Definition
{

    /**
     * @var Argument[]
     */
    private $arguments;

    private $requiredCount;
    private $hasAnArrayArgument = false;
    private $hasOptional;

    /**
     * @var Option[]
     */
    private $options;
    private $shortcuts;

    /**
     * 构造方法
     * @param array $definition
     * @api
     */
    public function __construct(array $definition = [])
    {
        $this->setDefinition($definition);
    }

    /**
     * 设置指令的定义
     * @param array $definition 定义的数组
     */
    public function setDefinition(array $definition): void
    {
        $arguments = [];
        $options   = [];
        foreach ($definition as $item) {
            if ($item instanceof Option) {
                $options[] = $item;
            } else {
                $arguments[] = $item;
            }
        }

        $this->setArguments($arguments);
        $this->setOptions($options);
    }

    /**
     * 设置参数
     * @param Argument[] $arguments 参数数组
     */
    public function setArguments(array $arguments = []): void
    {
        $this->arguments          = [];
        $this->requiredCount      = 0;
        $this->hasOptional        = false;
        $this->hasAnArrayArgument = false;
        $this->addArguments($arguments);
    }

    /**
     * 添加参数
     * @param Argument[] $arguments 参数数组
     * @api
     */
    public function addArguments(array $arguments = []): void
    {
        if (null !== $arguments) {
            foreach ($arguments as $argument) {
                $this->addArgument($argument);
            }
        }
    }

    /**
     * 添加一个参数
     * @param Argument $argument 参数
     * @throws \LogicException
     */
    public function addArgument(Argument $argument): void
    {
        if (isset($this->arguments[$argument->getName()])) {
            throw new \LogicException(sprintf('An argument with name "%s" already exists.', $argument->getName()));
        }

        if ($this->hasAnArrayArgument) {
            throw new \LogicException('Cannot add an argument after an array argument.');
        }

        if ($argument->isRequired() && $this->hasOptional) {
            throw new \LogicException('Cannot add a required argument after an optional one.');
        }

        if ($argument->isArray()) {
            $this->hasAnArrayArgument = true;
        }

        if ($argument->isRequired()) {
            ++$this->requiredCount;
        } else {
            $this->hasOptional = true;
        }

        $this->arguments[$argument->getName()] = $argument;
    }

    /**
     * 根据名称或者位置获取参数
     * @param string|int $name 参数名或者位置
     * @return Argument 参数
     * @throws \InvalidArgumentException
     */
    public function getArgument($name): Argument
    {
        if (!$this->hasArgument($name)) {
            throw new \InvalidArgumentException(sprintf('The "%s" argument does not exist.', $name));
        }

        $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments;

        return $arguments[$name];
    }

    /**
     * 根据名称或位置检查是否具有某个参数
     * @param string|int $name 参数名或者位置
     * @return bool
     * @api
     */
    public function hasArgument($name): bool
    {
        $arguments = is_int($name) ? array_values($this->arguments) : $this->arguments;

        return isset($arguments[$name]);
    }

    /**
     * 获取所有的参数
     * @return Argument[] 参数数组
     */
    public function getArguments(): array
    {
        return $this->arguments;
    }

    /**
     * 获取参数数量
     * @return int
     */
    public function getArgumentCount(): int
    {
        return $this->hasAnArrayArgument ? PHP_INT_MAX : count($this->arguments);
    }

    /**
     * 获取必填的参数的数量
     * @return int
     */
    public function getArgumentRequiredCount(): int
    {
        return $this->requiredCount;
    }

    /**
     * 获取参数默认值
     * @return array
     */
    public function getArgumentDefaults(): array
    {
        $values = [];
        foreach ($this->arguments as $argument) {
            $values[$argument->getName()] = $argument->getDefault();
        }

        return $values;
    }

    /**
     * 设置选项
     * @param Option[] $options 选项数组
     */
    public function setOptions(array $options = []): void
    {
        $this->options   = [];
        $this->shortcuts = [];
        $this->addOptions($options);
    }

    /**
     * 添加选项
     * @param Option[] $options 选项数组
     * @api
     */
    public function addOptions(array $options = []): void
    {
        foreach ($options as $option) {
            $this->addOption($option);
        }
    }

    /**
     * 添加一个选项
     * @param Option $option 选项
     * @throws \LogicException
     * @api
     */
    public function addOption(Option $option): void
    {
        if (isset($this->options[$option->getName()]) && !$option->equals($this->options[$option->getName()])) {
            throw new \LogicException(sprintf('An option named "%s" already exists.', $option->getName()));
        }

        if ($option->getShortcut()) {
            foreach (explode('|', $option->getShortcut()) as $shortcut) {
                if (isset($this->shortcuts[$shortcut])
                    && !$option->equals($this->options[$this->shortcuts[$shortcut]])
                ) {
                    throw new \LogicException(sprintf('An option with shortcut "%s" already exists.', $shortcut));
                }
            }
        }

        $this->options[$option->getName()] = $option;
        if ($option->getShortcut()) {
            foreach (explode('|', $option->getShortcut()) as $shortcut) {
                $this->shortcuts[$shortcut] = $option->getName();
            }
        }
    }

    /**
     * 根据名称获取选项
     * @param string $name 选项名
     * @return Option
     * @throws \InvalidArgumentException
     * @api
     */
    public function getOption(string $name): Option
    {
        if (!$this->hasOption($name)) {
            throw new \InvalidArgumentException(sprintf('The "--%s" option does not exist.', $name));
        }

        return $this->options[$name];
    }

    /**
     * 根据名称检查是否有这个选项
     * @param string $name 选项名
     * @return bool
     * @api
     */
    public function hasOption(string $name): bool
    {
        return isset($this->options[$name]);
    }

    /**
     * 获取所有选项
     * @return Option[]
     * @api
     */
    public function getOptions(): array
    {
        return $this->options;
    }

    /**
     * 根据名称检查某个选项是否有短名称
     * @param string $name 短名称
     * @return bool
     */
    public function hasShortcut(string $name): bool
    {
        return isset($this->shortcuts[$name]);
    }

    /**
     * 根据短名称获取选项
     * @param string $shortcut 短名称
     * @return Option
     */
    public function getOptionForShortcut(string $shortcut): Option
    {
        return $this->getOption($this->shortcutToName($shortcut));
    }

    /**
     * 获取所有选项的默认值
     * @return array
     */
    public function getOptionDefaults(): array
    {
        $values = [];
        foreach ($this->options as $option) {
            $values[$option->getName()] = $option->getDefault();
        }

        return $values;
    }

    /**
     * 根据短名称获取选项名
     * @param string $shortcut 短名称
     * @return string
     * @throws \InvalidArgumentException
     */
    private function shortcutToName(string $shortcut): string
    {
        if (!isset($this->shortcuts[$shortcut])) {
            throw new \InvalidArgumentException(sprintf('The "-%s" option does not exist.', $shortcut));
        }

        return $this->shortcuts[$shortcut];
    }

    /**
     * 获取该指令的介绍
     * @param bool $short 是否简洁介绍
     * @return string
     */
    public function getSynopsis(bool $short = false): string
    {
        $elements = [];

        if ($short && $this->getOptions()) {
            $elements[] = '[options]';
        } elseif (!$short) {
            foreach ($this->getOptions() as $option) {
                $value = '';
                if ($option->acceptValue()) {
                    $value = sprintf(' %s%s%s', $option->isValueOptional() ? '[' : '', strtoupper($option->getName()), $option->isValueOptional() ? ']' : '');
                }

                $shortcut   = $option->getShortcut() ? sprintf('-%s|', $option->getShortcut()) : '';
                $elements[] = sprintf('[%s--%s%s]', $shortcut, $option->getName(), $value);
            }
        }

        if (count($elements) && $this->getArguments()) {
            $elements[] = '[--]';
        }

        foreach ($this->getArguments() as $argument) {
            $element = '<' . $argument->getName() . '>';
            if (!$argument->isRequired()) {
                $element = '[' . $element . ']';
            } elseif ($argument->isArray()) {
                $element .= ' (' . $element . ')';
            }

            if ($argument->isArray()) {
                $element .= '...';
            }

            $elements[] = $element;
        }

        return implode(' ', $elements);
    }
}
