import sys

from typing import Any
from typing import List
from typing import Optional
from typing import Union

from cleo.exceptions import NoSuchOptionException
from cleo.exceptions import RuntimeException
from cleo.io.inputs.definition import Definition

from .input import Input


class ArgvInput(Input):
    """
    Represents an input coming from the command line.
    """

    def __init__(
        self, argv: Optional[List[str]] = None, definition: Optional[Definition] = None
    ) -> None:
        if argv is None:
            argv = sys.argv

        argv = argv[:]

        # Strip the application name
        try:
            self._script_name = argv.pop(0)
        except IndexError:
            self._script_name = None

        self._tokens = argv
        self._parsed = []

        super().__init__(definition=definition)

    @property
    def first_argument(self) -> Optional[str]:
        is_option = False

        for i, token in enumerate(self._tokens):
            if token and token[0] == "-":
                if token.find("=") != -1 or len(self._tokens) <= (i + 1):
                    continue

                # If it's a long option, consider that
                # everything after "--" is the option name.
                # Otherwise, use the last character
                # (if it's a short option set, only the last one
                # can take a value with space separator).
                if len(token) > 1 and token[1] == "-":
                    name = token[2:]
                else:
                    name = token[-1]

                if name not in self._options and not self._definition.has_shortcut(
                    name
                ):
                    # noop
                    continue

                if name not in self._options:
                    name = self._definition.shortcut_to_name(name)

                if name in self._options and self._tokens[i + 1] == self._options[name]:
                    is_option = True

                continue

            if is_option:
                is_option = False
                continue

            return token

        return

    @property
    def script_name(self) -> Optional[str]:
        return self._script_name

    def has_parameter_option(
        self, values: Union[str, List[str]], only_params: bool = False
    ) -> bool:
        """
        Returns true if the raw parameters (not parsed) contain a value.
        """
        if not isinstance(values, list):
            values = [values]

        for token in self._tokens:
            if only_params and token == "--":
                return False

            for value in values:
                # Options with values:
                # For long options, test for '--option=' at beginning
                # For short options, test for '-o' at beginning
                if value.find("--") == 0:
                    leading = value + "="
                else:
                    leading = value

                if token == value or leading != "" and token.find(leading) == 0:
                    return True

        return False

    def parameter_option(
        self,
        values: Union[str, List[str]],
        default: Any = False,
        only_params: bool = False,
    ) -> Any:
        if not isinstance(values, list):
            values = [values]

        tokens = self._tokens[:]
        while len(tokens) > 0:
            token = tokens.pop(0)
            if only_params and token == "--":
                return default

            for value in values:
                if token == value:
                    try:
                        return tokens.pop(0)
                    except IndexError:
                        return

                # Options with values:
                # For long options, test for '--option=' at beginning
                # For short options, test for '-o' at beginning
                if value.find("--") == 0:
                    leading = value + "="
                else:
                    leading = value

                if token == value or leading != "" and token.find(leading) == 0:
                    return token[len(leading)]

        return False

    def _set_tokens(self, tokens: List[str]) -> None:
        self._tokens = tokens

    def _parse(self) -> None:
        parse_options = True
        self._parsed = self._tokens[:]

        try:
            token = self._parsed.pop(0)
        except IndexError:
            token = None

        while token is not None:
            if parse_options and token == "":
                self._parse_argument(token)
            elif parse_options and token == "--":
                parse_options = False
            elif parse_options and token.find("--") == 0:
                self._parse_long_option(token)
            elif parse_options and token[0] == "-" and token != "-":
                self._parse_short_option(token)
            else:
                self._parse_argument(token)

            try:
                token = self._parsed.pop(0)
            except IndexError:
                token = None

    def _parse_short_option(self, token: str) -> None:
        name = token[1:]

        if len(name) > 1:
            if (
                self._definition.has_shortcut(name[0])
                and self._definition.option_for_shortcut(name[0]).accepts_value()
            ):
                # An option with a value and no space
                self._add_short_option(name[0], name[1:])
            else:
                self._parse_short_option_set(name)
        else:
            self._add_short_option(name, None)

    def _parse_short_option_set(self, name: str) -> None:
        length = len(name)
        for i in range(length):
            if not self._definition.has_shortcut(name[i]):
                raise RuntimeException(f'The option "{name[i]}" does not exist')

            option = self._definition.option_for_shortcut(name[i])
            if option.accepts_value():
                self._add_long_option(
                    option.name, None if i == length - 1 else name[i + 1 :]
                )

                break

            self._add_long_option(option.name, None)

    def _parse_long_option(self, token: str) -> None:
        name = token[2:]

        pos = name.find("=")
        if pos != -1:
            value = name[pos + 1 :]
            if not value:
                self._parsed.insert(0, value)

            self._add_long_option(name[0:pos], value)
        else:
            self._add_long_option(name, None)

    def _parse_argument(self, token: str) -> None:
        count = len(self._arguments)

        # If the input is expecting another argument, add it
        if self._definition.has_argument(count):
            argument = self._definition.argument(count)
            self._arguments[argument.name] = [token] if argument.is_list() else token
        # If the last argument is a list, append the token to it
        elif (
            self._definition.has_argument(count - 1)
            and self._definition.argument(count - 1).is_list()
        ):
            argument = self._definition.argument(count - 1)
            self._arguments[argument.name].append(token)
        # Unexpected argument
        else:
            all = self._definition.arguments.copy()
            command_name = None
            argument = all[0]
            if argument and argument.name == "command":
                command_name = self._arguments.get("command")
                del all[0]

            if all:
                if command_name:
                    message = 'Too many arguments to "{}" command, expected arguments "{}"'.format(
                        command_name, '" "'.join([a.name for a in all])
                    )
                else:
                    message = 'Too many arguments, expected arguments "{}"'.format(
                        '" "'.join([a.name for a in all])
                    )
            elif command_name:
                message = 'No arguments expected for "{}" command, got "{}"'.format(
                    command_name, token
                )
            else:
                message = 'No arguments expected, got "{}"'.format(token)

            raise RuntimeException(message)

    def _add_short_option(self, shortcut: str, value: Any) -> None:
        if not self._definition.has_shortcut(shortcut):
            raise NoSuchOptionException(f'The option "-{shortcut}" does not exist')

        self._add_long_option(
            self._definition.option_for_shortcut(shortcut).name, value
        )

    def _add_long_option(self, name: str, value: Any) -> None:
        if not self._definition.has_option(name):
            raise NoSuchOptionException(f'The option "--{name}" does not exist')

        option = self._definition.option(name)

        if value is not None and not option.accepts_value():
            raise RuntimeException(f'The "--{name}" option does not accept a value')

        if value in ["", None] and option.accepts_value() and self._parsed:
            # If the option accepts a value, either required or optional,
            # we check if there is one
            next_token = self._parsed.pop(0)
            if (next_token and next_token[0] != "-") or next_token in ["", None]:
                value = next_token
            else:
                self._parsed.insert(0, next_token)

        if value is None:
            if option.requires_value():
                raise RuntimeException(f'The "--{name}" option requires a value')

            if not option.is_list() and option.is_flag():
                value = True

        if option.is_list():
            if name not in self._options:
                self._options[name] = []

            self._options[name].append(value)
        else:
            self._options[name] = value
