import abc
from collections.abc import Sequence
from typing import NamedTuple, Optional

import urwid
from urwid.text_layout import calc_coords

import mitmproxy.command
import mitmproxy.flow
import mitmproxy.master
import mitmproxy.types


class Completer:
    @abc.abstractmethod
    def cycle(self, forward: bool = True) -> str:
        raise NotImplementedError()


class ListCompleter(Completer):
    def __init__(
        self,
        start: str,
        options: Sequence[str],
    ) -> None:
        self.start = start
        self.options: list[str] = []
        for o in options:
            if o.startswith(start):
                self.options.append(o)
        self.options.sort()
        self.pos = -1

    def cycle(self, forward: bool = True) -> str:
        if not self.options:
            return self.start
        if self.pos == -1:
            self.pos = 0 if forward else len(self.options) - 1
        else:
            delta = 1 if forward else -1
            self.pos = (self.pos + delta) % len(self.options)
        return self.options[self.pos]


class CompletionState(NamedTuple):
    completer: Completer
    parsed: Sequence[mitmproxy.command.ParseResult]


class CommandBuffer:
    def __init__(self, master: mitmproxy.master.Master, start: str = "") -> None:
        self.master = master
        self.text = start
        # Cursor is always within the range [0:len(buffer)].
        self._cursor = len(self.text)
        self.completion: Optional[CompletionState] = None

    @property
    def cursor(self) -> int:
        return self._cursor

    @cursor.setter
    def cursor(self, x) -> None:
        if x < 0:
            self._cursor = 0
        elif x > len(self.text):
            self._cursor = len(self.text)
        else:
            self._cursor = x

    def set_text(self, text: str) -> None:
        self.text = text
        self._cursor = len(self.text)
        self.render()

    def render(self):
        parts, remaining = self.master.commands.parse_partial(self.text)
        ret = []
        if not parts:
            # Means we just received the leader, so we need to give a blank
            # text to the widget to render or it crashes
            ret.append(("text", ""))
        else:
            for p in parts:
                if p.valid:
                    if p.type == mitmproxy.types.Cmd:
                        ret.append(("commander_command", p.value))
                    else:
                        ret.append(("text", p.value))
                elif p.value:
                    ret.append(("commander_invalid", p.value))

            if remaining:
                if parts[-1].type != mitmproxy.types.Space:
                    ret.append(("text", " "))
                for param in remaining:
                    ret.append(("commander_hint", f"{param} "))

        return ret

    def left(self) -> None:
        self.cursor = self.cursor - 1

    def right(self) -> None:
        self.cursor = self.cursor + 1

    def cycle_completion(self, forward: bool = True) -> None:
        if not self.completion:
            parts, remaining = self.master.commands.parse_partial(
                self.text[: self.cursor]
            )
            if parts and parts[-1].type != mitmproxy.types.Space:
                type_to_complete = parts[-1].type
                cycle_prefix = parts[-1].value
                parsed = parts[:-1]
            elif remaining:
                type_to_complete = remaining[0].type
                cycle_prefix = ""
                parsed = parts
            else:
                return
            ct = mitmproxy.types.CommandTypes.get(type_to_complete, None)
            if ct:
                self.completion = CompletionState(
                    completer=ListCompleter(
                        cycle_prefix,
                        ct.completion(
                            self.master.commands, type_to_complete, cycle_prefix
                        ),
                    ),
                    parsed=parsed,
                )
        if self.completion:
            nxt = self.completion.completer.cycle(forward)
            buf = "".join([i.value for i in self.completion.parsed]) + nxt
            self.text = buf
            self.cursor = len(self.text)

    def backspace(self) -> None:
        if self.cursor == 0:
            return
        self.text = self.text[: self.cursor - 1] + self.text[self.cursor :]
        self.cursor = self.cursor - 1
        self.completion = None

    def delete(self) -> None:
        if self.cursor == len(self.text):
            return
        self.text = self.text[: self.cursor] + self.text[self.cursor + 1 :]
        self.completion = None

    def insert(self, k: str) -> None:
        """
        Inserts text at the cursor.
        """

        # We don't want to insert a space before the command
        if k == " " and self.text[0 : self.cursor].strip() == "":
            return

        self.text = self.text[: self.cursor] + k + self.text[self.cursor :]
        self.cursor += len(k)
        self.completion = None


class CommandEdit(urwid.WidgetWrap):
    leader = ": "

    def __init__(self, master: mitmproxy.master.Master, text: str) -> None:
        super().__init__(urwid.Text(self.leader))
        self.master = master
        self.active_filter = False
        self.filter_str = ""
        self.cbuf = CommandBuffer(master, text)
        self.update()

    def keypress(self, size, key) -> None:
        if key == "delete":
            self.cbuf.delete()
        elif key == "ctrl a" or key == "home":
            self.cbuf.cursor = 0
        elif key == "ctrl e" or key == "end":
            self.cbuf.cursor = len(self.cbuf.text)
        elif key == "meta b":
            self.cbuf.cursor = self.cbuf.text.rfind(" ", 0, self.cbuf.cursor)
        elif key == "meta f":
            pos = self.cbuf.text.find(" ", self.cbuf.cursor + 1)
            if pos == -1:
                pos = len(self.cbuf.text)
            self.cbuf.cursor = pos
        elif key == "ctrl w":
            prev_cursor = self.cbuf.cursor
            pos = self.cbuf.text.rfind(" ", 0, self.cbuf.cursor - 1)
            if pos == -1:
                new_text = self.cbuf.text[self.cbuf.cursor :]
                cursor_pos = 0
            else:
                txt_after = self.cbuf.text[self.cbuf.cursor :]
                txt_before = self.cbuf.text[0:pos]
                new_text = f"{txt_before} {txt_after}"
                cursor_pos = prev_cursor - (prev_cursor - pos) + 1
            self.cbuf.set_text(new_text)
            self.cbuf.cursor = cursor_pos
        elif key == "backspace":
            self.cbuf.backspace()
            if self.cbuf.text == "":
                self.active_filter = False
                self.master.commands.call("commands.history.filter", "")
                self.filter_str = ""
        elif key == "left" or key == "ctrl b":
            self.cbuf.left()
        elif key == "right" or key == "ctrl f":
            self.cbuf.right()
        elif key == "up" or key == "ctrl p":
            if self.active_filter is False:
                self.active_filter = True
                self.filter_str = self.cbuf.text
                self.master.commands.call("commands.history.filter", self.cbuf.text)
            cmd = self.master.commands.execute("commands.history.prev")
            self.cbuf = CommandBuffer(self.master, cmd)
        elif key == "down" or key == "ctrl n":
            prev_cmd = self.cbuf.text
            cmd = self.master.commands.execute("commands.history.next")

            if cmd == "":
                if prev_cmd == self.filter_str:
                    self.cbuf = CommandBuffer(self.master, prev_cmd)
                else:
                    self.active_filter = False
                    self.master.commands.call("commands.history.filter", "")
                    self.filter_str = ""
                    self.cbuf = CommandBuffer(self.master, "")
            else:
                self.cbuf = CommandBuffer(self.master, cmd)
        elif key == "shift tab":
            self.cbuf.cycle_completion(False)
        elif key == "tab":
            self.cbuf.cycle_completion()
        elif len(key) == 1:
            self.cbuf.insert(key)
        self.update()

    def update(self) -> None:
        self._w.set_text([self.leader, self.cbuf.render()])

    def render(self, size, focus=False) -> urwid.Canvas:
        (maxcol,) = size
        canv = self._w.render((maxcol,))
        canv = urwid.CompositeCanvas(canv)
        canv.cursor = self.get_cursor_coords((maxcol,))
        return canv

    def get_cursor_coords(self, size) -> tuple[int, int]:
        p = self.cbuf.cursor + len(self.leader)
        trans = self._w.get_line_translation(size[0])
        x, y = calc_coords(self._w.get_text()[0], trans, p)
        return x, y

    def get_edit_text(self) -> str:
        return self.cbuf.text
