import getpass
import os
import subprocess

from typing import Any

from clikit.api.formatter import Style
from clikit.api.io import IO
from clikit.utils._compat import decode


class Question(object):
    """
    A question that will be asked in a Console.
    """

    def __init__(self, question, default=None):  # type: (str, Any) -> None
        self._question = question
        self._default = default

        self._attempts = None
        self._hidden = False
        self._hidden_fallback = True
        self._autocomplete_values = None
        self._validator = None
        self._normalizer = None
        self._error_message = None

    @property
    def question(self):  # type: () -> str
        return self._question

    @property
    def default(self):  # type: () -> Any
        return self._default

    @property
    def autocomplete_values(self):
        return self._autocomplete_values

    @property
    def max_attempts(self):
        return self._attempts

    def is_hidden(self):  # type: () -> bool
        return self._hidden

    def hide(self, hidden=True):  # type: (bool) -> None
        if hidden is True and self._autocomplete_values:
            raise RuntimeError("A hidden question cannot use the autocompleter.")

        self._hidden = hidden

    def set_autocomplete_values(self, autocomplete_values):
        if self.is_hidden():
            raise RuntimeError("A hidden question cannot use the autocompleter.")

        self._autocomplete_values = autocomplete_values

    def set_max_attempts(self, attempts):
        self._attempts = attempts

    def set_validator(self, validator):
        self._validator = validator

    def ask(self, io):  # type: (IO) -> str
        """
        Asks the question to the user.
        """
        if not io.is_interactive():
            return self.default

        if not self._validator:
            return self._do_ask(io)

        interviewer = lambda: self._do_ask(io)

        return self._validate_attempts(interviewer, io)

    def _do_ask(self, io):  # type: (IO) -> str
        """
        Asks the question to the user.
        """
        self._write_prompt(io)

        autocomplete = self._autocomplete_values

        if autocomplete is None or not self._has_stty_available():
            ret = False

            if self.is_hidden():
                try:
                    ret = self._get_hidden_response(io)
                except RuntimeError:
                    if not self._hidden_fallback:
                        raise

            if not ret:
                ret = self._read_from_input(io)
        else:
            ret = self._autocomplete(io)

        if len(ret) <= 0:
            ret = self._default

        if self._normalizer:
            return self._normalizer(ret)

        return ret

    def _write_prompt(self, io):
        """
        Outputs the question prompt.
        """
        message = self._question

        io.error("<question>{}</> ".format(message))

    def _write_error(self, io, error):
        """
        Outputs an error message.
        """
        message = "<error>{}</error>".format(decode(str(error)))

        io.error_line(message)

    def _autocomplete(self, io):  # type: (IO) -> str
        """
        Autocomplete a question.
        """
        autocomplete = self._autocomplete_values

        ret = ""

        i = 0
        ofs = -1
        matches = [x for x in autocomplete]
        num_matches = len(matches)

        stty_mode = decode(subprocess.check_output(["stty", "-g"])).rstrip("\n")

        # Disable icanon (so we can read each keypress) and echo (we'll do echoing here instead)
        subprocess.check_output(["stty", "-icanon", "-echo"])

        # Add highlighted text style
        style = Style("hl").fg("black").bg("white")
        io.error_output.formatter.add_style(style)

        # Read a keypress
        while True:
            c = io.read(1)

            # Backspace character
            if c == "\177":
                if num_matches == 0 and i != 0:
                    i -= 1
                    # Move cursor backwards
                    io.error("\033[1D")

                if i == 0:
                    ofs = -1
                    matches = [x for x in autocomplete]
                    num_matches = len(matches)
                else:
                    num_matches = 0

                # Pop the last character off the end of our string
                ret = ret[:i]
            # Did we read an escape sequence
            elif c == "\033":
                c += io.read(2)

                # A = Up Arrow. B = Down Arrow
                if c[2] == "A" or c[2] == "B":
                    if c[2] == "A" and ofs == -1:
                        ofs = 0

                    if num_matches == 0:
                        continue

                    ofs += -1 if c[2] == "A" else 1
                    ofs = (num_matches + ofs) % num_matches
            elif ord(c) < 32:
                if c == "\t" or c == "\n":
                    if num_matches > 0 and ofs != -1:
                        ret = matches[ofs]
                        # Echo out remaining chars for current match
                        io.error(ret[i:])
                        i = len(ret)

                    if c == "\n":
                        io.error(c)
                        break

                    num_matches = 0

                continue
            else:
                io.error(c)
                ret += c
                i += 1

                num_matches = 0
                ofs = 0

                for value in autocomplete:
                    # If typed characters match the beginning chunk of value (e.g. [AcmeDe]moBundle)
                    if value.startswith(ret) and i != len(value):
                        num_matches += 1
                        matches[num_matches - 1] = value

            # Erase characters from cursor to end of line
            io.error("\033[K")

            if num_matches > 0 and ofs != -1:
                # Save cursor position
                io.error("\0337")
                # Write highlighted text
                io.error("<hl>" + matches[ofs][i:] + "</hl>")
                # Restore cursor position
                io.error("\0338")

        subprocess.call(["stty", "{}".format(decode(stty_mode))])

        return ret

    def _get_hidden_response(self, io):  # type: (IO) -> str
        """
        Gets a hidden response from user.
        """
        return getpass.getpass("", stream=io.error_output.stream)

    def _validate_attempts(self, interviewer, io):  # type: (Callable, IO) -> str
        """
        Validates an attempt.
        """
        error = None
        attempts = self._attempts

        while attempts is None or attempts:
            if error is not None:
                self._write_error(io, error)

            try:
                return self._validator(interviewer())
            except Exception as e:
                error = e

            if attempts is not None:
                attempts -= 1

        raise error

    def _read_from_input(self, io):
        """
        Read user input.
        """
        ret = io.read_line(4096)

        if not ret:
            raise RuntimeError("Aborted")

        return decode(ret.strip())

    def _has_stty_available(self):
        devnull = open(os.devnull, "w")

        try:
            exit_code = subprocess.call(["stty"], stdout=devnull, stderr=devnull)
        except Exception:
            exit_code = 2

        return exit_code == 0
