import sys
from typing import TYPE_CHECKING, FrozenSet

from prompt_toolkit.data_structures import Point
from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent
from prompt_toolkit.keys import Keys
from prompt_toolkit.mouse_events import (
    MouseButton,
    MouseEvent,
    MouseEventType,
    MouseModifier,
)

from ..key_bindings import KeyBindings

if TYPE_CHECKING:
    from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone

__all__ = [
    "load_mouse_bindings",
]

E = KeyPressEvent

# fmt: off
# flake8: noqa E201
SCROLL_UP   = MouseEventType.SCROLL_UP
SCROLL_DOWN = MouseEventType.SCROLL_DOWN
MOUSE_DOWN  = MouseEventType.MOUSE_DOWN
MOUSE_MOVE  = MouseEventType.MOUSE_MOVE
MOUSE_UP    = MouseEventType.MOUSE_UP

NO_MODIFIER      : FrozenSet[MouseModifier] = frozenset()
SHIFT            : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT})
ALT              : FrozenSet[MouseModifier] = frozenset({MouseModifier.ALT})
SHIFT_ALT        : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT})
CONTROL          : FrozenSet[MouseModifier] = frozenset({MouseModifier.CONTROL})
SHIFT_CONTROL    : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.CONTROL})
ALT_CONTROL      : FrozenSet[MouseModifier] = frozenset({MouseModifier.ALT, MouseModifier.CONTROL})
SHIFT_ALT_CONTROL: FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT, MouseModifier.CONTROL})
UNKNOWN_MODIFIER : FrozenSet[MouseModifier] = frozenset()

LEFT           = MouseButton.LEFT
MIDDLE         = MouseButton.MIDDLE
RIGHT          = MouseButton.RIGHT
NO_BUTTON      = MouseButton.NONE
UNKNOWN_BUTTON = MouseButton.UNKNOWN

xterm_sgr_mouse_events = {
    ( 0, 'm') : (LEFT, MOUSE_UP, NO_MODIFIER),                # left_up                       0+ + +  =0
    ( 4, 'm') : (LEFT, MOUSE_UP, SHIFT),                      # left_up     Shift             0+4+ +  =4
    ( 8, 'm') : (LEFT, MOUSE_UP, ALT),                        # left_up           Alt         0+ +8+  =8
    (12, 'm') : (LEFT, MOUSE_UP, SHIFT_ALT),                  # left_up     Shift Alt         0+4+8+  =12
    (16, 'm') : (LEFT, MOUSE_UP, CONTROL),                    # left_up               Control 0+ + +16=16
    (20, 'm') : (LEFT, MOUSE_UP, SHIFT_CONTROL),              # left_up     Shift     Control 0+4+ +16=20
    (24, 'm') : (LEFT, MOUSE_UP, ALT_CONTROL),                # left_up           Alt Control 0+ +8+16=24
    (28, 'm') : (LEFT, MOUSE_UP, SHIFT_ALT_CONTROL),          # left_up     Shift Alt Control 0+4+8+16=28

    ( 1, 'm') : (MIDDLE, MOUSE_UP, NO_MODIFIER),              # middle_up                     1+ + +  =1
    ( 5, 'm') : (MIDDLE, MOUSE_UP, SHIFT),                    # middle_up   Shift             1+4+ +  =5
    ( 9, 'm') : (MIDDLE, MOUSE_UP, ALT),                      # middle_up         Alt         1+ +8+  =9
    (13, 'm') : (MIDDLE, MOUSE_UP, SHIFT_ALT),                # middle_up   Shift Alt         1+4+8+  =13
    (17, 'm') : (MIDDLE, MOUSE_UP, CONTROL),                  # middle_up             Control 1+ + +16=17
    (21, 'm') : (MIDDLE, MOUSE_UP, SHIFT_CONTROL),            # middle_up   Shift     Control 1+4+ +16=21
    (25, 'm') : (MIDDLE, MOUSE_UP, ALT_CONTROL),              # middle_up         Alt Control 1+ +8+16=25
    (29, 'm') : (MIDDLE, MOUSE_UP, SHIFT_ALT_CONTROL),        # middle_up   Shift Alt Control 1+4+8+16=29

    ( 2, 'm') : (RIGHT, MOUSE_UP, NO_MODIFIER),               # right_up                      2+ + +  =2
    ( 6, 'm') : (RIGHT, MOUSE_UP, SHIFT),                     # right_up    Shift             2+4+ +  =6
    (10, 'm') : (RIGHT, MOUSE_UP, ALT),                       # right_up          Alt         2+ +8+  =10
    (14, 'm') : (RIGHT, MOUSE_UP, SHIFT_ALT),                 # right_up    Shift Alt         2+4+8+  =14
    (18, 'm') : (RIGHT, MOUSE_UP, CONTROL),                   # right_up              Control 2+ + +16=18
    (22, 'm') : (RIGHT, MOUSE_UP, SHIFT_CONTROL),             # right_up    Shift     Control 2+4+ +16=22
    (26, 'm') : (RIGHT, MOUSE_UP, ALT_CONTROL),               # right_up          Alt Control 2+ +8+16=26
    (30, 'm') : (RIGHT, MOUSE_UP, SHIFT_ALT_CONTROL),         # right_up    Shift Alt Control 2+4+8+16=30

    ( 0, 'M') : (LEFT, MOUSE_DOWN, NO_MODIFIER),              # left_down                     0+ + +  =0
    ( 4, 'M') : (LEFT, MOUSE_DOWN, SHIFT),                    # left_down   Shift             0+4+ +  =4
    ( 8, 'M') : (LEFT, MOUSE_DOWN, ALT),                      # left_down         Alt         0+ +8+  =8
    (12, 'M') : (LEFT, MOUSE_DOWN, SHIFT_ALT),                # left_down   Shift Alt         0+4+8+  =12
    (16, 'M') : (LEFT, MOUSE_DOWN, CONTROL),                  # left_down             Control 0+ + +16=16
    (20, 'M') : (LEFT, MOUSE_DOWN, SHIFT_CONTROL),            # left_down   Shift     Control 0+4+ +16=20
    (24, 'M') : (LEFT, MOUSE_DOWN, ALT_CONTROL),              # left_down         Alt Control 0+ +8+16=24
    (28, 'M') : (LEFT, MOUSE_DOWN, SHIFT_ALT_CONTROL),        # left_down   Shift Alt Control 0+4+8+16=28

    ( 1, 'M') : (MIDDLE, MOUSE_DOWN, NO_MODIFIER),            # middle_down                   1+ + +  =1
    ( 5, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT),                  # middle_down Shift             1+4+ +  =5
    ( 9, 'M') : (MIDDLE, MOUSE_DOWN, ALT),                    # middle_down       Alt         1+ +8+  =9
    (13, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_ALT),              # middle_down Shift Alt         1+4+8+  =13
    (17, 'M') : (MIDDLE, MOUSE_DOWN, CONTROL),                # middle_down           Control 1+ + +16=17
    (21, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_CONTROL),          # middle_down Shift     Control 1+4+ +16=21
    (25, 'M') : (MIDDLE, MOUSE_DOWN, ALT_CONTROL),            # middle_down       Alt Control 1+ +8+16=25
    (29, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_ALT_CONTROL),      # middle_down Shift Alt Control 1+4+8+16=29

    ( 2, 'M') : (RIGHT, MOUSE_DOWN, NO_MODIFIER),             # right_down                    2+ + +  =2
    ( 6, 'M') : (RIGHT, MOUSE_DOWN, SHIFT),                   # right_down  Shift             2+4+ +  =6
    (10, 'M') : (RIGHT, MOUSE_DOWN, ALT),                     # right_down        Alt         2+ +8+  =10
    (14, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_ALT),               # right_down  Shift Alt         2+4+8+  =14
    (18, 'M') : (RIGHT, MOUSE_DOWN, CONTROL),                 # right_down            Control 2+ + +16=18
    (22, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_CONTROL),           # right_down  Shift     Control 2+4+ +16=22
    (26, 'M') : (RIGHT, MOUSE_DOWN, ALT_CONTROL),             # right_down        Alt Control 2+ +8+16=26
    (30, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_ALT_CONTROL),       # right_down  Shift Alt Control 2+4+8+16=30

    (32, 'M') : (LEFT, MOUSE_MOVE, NO_MODIFIER),              # left_drag                     32+ + +  =32
    (36, 'M') : (LEFT, MOUSE_MOVE, SHIFT),                    # left_drag   Shift             32+4+ +  =36
    (40, 'M') : (LEFT, MOUSE_MOVE, ALT),                      # left_drag         Alt         32+ +8+  =40
    (44, 'M') : (LEFT, MOUSE_MOVE, SHIFT_ALT),                # left_drag   Shift Alt         32+4+8+  =44
    (48, 'M') : (LEFT, MOUSE_MOVE, CONTROL),                  # left_drag             Control 32+ + +16=48
    (52, 'M') : (LEFT, MOUSE_MOVE, SHIFT_CONTROL),            # left_drag   Shift     Control 32+4+ +16=52
    (56, 'M') : (LEFT, MOUSE_MOVE, ALT_CONTROL),              # left_drag         Alt Control 32+ +8+16=56
    (60, 'M') : (LEFT, MOUSE_MOVE, SHIFT_ALT_CONTROL),        # left_drag   Shift Alt Control 32+4+8+16=60

    (33, 'M') : (MIDDLE, MOUSE_MOVE, NO_MODIFIER),            # middle_drag                   33+ + +  =33
    (37, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT),                  # middle_drag Shift             33+4+ +  =37
    (41, 'M') : (MIDDLE, MOUSE_MOVE, ALT),                    # middle_drag       Alt         33+ +8+  =41
    (45, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_ALT),              # middle_drag Shift Alt         33+4+8+  =45
    (49, 'M') : (MIDDLE, MOUSE_MOVE, CONTROL),                # middle_drag           Control 33+ + +16=49
    (53, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_CONTROL),          # middle_drag Shift     Control 33+4+ +16=53
    (57, 'M') : (MIDDLE, MOUSE_MOVE, ALT_CONTROL),            # middle_drag       Alt Control 33+ +8+16=57
    (61, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_ALT_CONTROL),      # middle_drag Shift Alt Control 33+4+8+16=61

    (34, 'M') : (RIGHT, MOUSE_MOVE, NO_MODIFIER),             # right_drag                    34+ + +  =34
    (38, 'M') : (RIGHT, MOUSE_MOVE, SHIFT),                   # right_drag  Shift             34+4+ +  =38
    (42, 'M') : (RIGHT, MOUSE_MOVE, ALT),                     # right_drag        Alt         34+ +8+  =42
    (46, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_ALT),               # right_drag  Shift Alt         34+4+8+  =46
    (50, 'M') : (RIGHT, MOUSE_MOVE, CONTROL),                 # right_drag            Control 34+ + +16=50
    (54, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_CONTROL),           # right_drag  Shift     Control 34+4+ +16=54
    (58, 'M') : (RIGHT, MOUSE_MOVE, ALT_CONTROL),             # right_drag        Alt Control 34+ +8+16=58
    (62, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_ALT_CONTROL),       # right_drag  Shift Alt Control 34+4+8+16=62

    (35, 'M') : (NO_BUTTON, MOUSE_MOVE, NO_MODIFIER),         # none_drag                     35+ + +  =35
    (39, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT),               # none_drag   Shift             35+4+ +  =39
    (43, 'M') : (NO_BUTTON, MOUSE_MOVE, ALT),                 # none_drag         Alt         35+ +8+  =43
    (47, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT),           # none_drag   Shift Alt         35+4+8+  =47
    (51, 'M') : (NO_BUTTON, MOUSE_MOVE, CONTROL),             # none_drag             Control 35+ + +16=51
    (55, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_CONTROL),       # none_drag   Shift     Control 35+4+ +16=55
    (59, 'M') : (NO_BUTTON, MOUSE_MOVE, ALT_CONTROL),         # none_drag         Alt Control 35+ +8+16=59
    (63, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT_CONTROL),   # none_drag   Shift Alt Control 35+4+8+16=63

    (64, 'M') : (NO_BUTTON, SCROLL_UP, NO_MODIFIER),          # scroll_up                     64+ + +  =64
    (68, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT),                # scroll_up   Shift             64+4+ +  =68
    (72, 'M') : (NO_BUTTON, SCROLL_UP, ALT),                  # scroll_up         Alt         64+ +8+  =72
    (76, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_ALT),            # scroll_up   Shift Alt         64+4+8+  =76
    (80, 'M') : (NO_BUTTON, SCROLL_UP, CONTROL),              # scroll_up             Control 64+ + +16=80
    (84, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_CONTROL),        # scroll_up   Shift     Control 64+4+ +16=84
    (88, 'M') : (NO_BUTTON, SCROLL_UP, ALT_CONTROL),          # scroll_up         Alt Control 64+ +8+16=88
    (92, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_ALT_CONTROL),    # scroll_up   Shift Alt Control 64+4+8+16=92

    (65, 'M') : (NO_BUTTON, SCROLL_DOWN, NO_MODIFIER),        # scroll_down                   64+ + +  =65
    (69, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT),              # scroll_down Shift             64+4+ +  =69
    (73, 'M') : (NO_BUTTON, SCROLL_DOWN, ALT),                # scroll_down       Alt         64+ +8+  =73
    (77, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT),          # scroll_down Shift Alt         64+4+8+  =77
    (81, 'M') : (NO_BUTTON, SCROLL_DOWN, CONTROL),            # scroll_down           Control 64+ + +16=81
    (85, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_CONTROL),      # scroll_down Shift     Control 64+4+ +16=85
    (89, 'M') : (NO_BUTTON, SCROLL_DOWN, ALT_CONTROL),        # scroll_down       Alt Control 64+ +8+16=89
    (93, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT_CONTROL),  # scroll_down Shift Alt Control 64+4+8+16=93
}

typical_mouse_events = {
    32: (LEFT           , MOUSE_DOWN , UNKNOWN_MODIFIER),
    33: (MIDDLE         , MOUSE_DOWN , UNKNOWN_MODIFIER),
    34: (RIGHT          , MOUSE_DOWN , UNKNOWN_MODIFIER),
    35: (UNKNOWN_BUTTON , MOUSE_UP   , UNKNOWN_MODIFIER),

    64: (LEFT           , MOUSE_MOVE , UNKNOWN_MODIFIER),
    65: (MIDDLE         , MOUSE_MOVE , UNKNOWN_MODIFIER),
    66: (RIGHT          , MOUSE_MOVE , UNKNOWN_MODIFIER),
    67: (NO_BUTTON      , MOUSE_MOVE , UNKNOWN_MODIFIER),

    96: (NO_BUTTON      , SCROLL_UP  , UNKNOWN_MODIFIER),
    97: (NO_BUTTON      , SCROLL_DOWN, UNKNOWN_MODIFIER),
}

urxvt_mouse_events={
    32: (UNKNOWN_BUTTON, MOUSE_DOWN , UNKNOWN_MODIFIER),
    35: (UNKNOWN_BUTTON, MOUSE_UP   , UNKNOWN_MODIFIER),
    96: (NO_BUTTON     , SCROLL_UP  , UNKNOWN_MODIFIER),
    97: (NO_BUTTON     , SCROLL_DOWN, UNKNOWN_MODIFIER),
}
# fmt:on


def load_mouse_bindings() -> KeyBindings:
    """
    Key bindings, required for mouse support.
    (Mouse events enter through the key binding system.)
    """
    key_bindings = KeyBindings()

    @key_bindings.add(Keys.Vt100MouseEvent)
    def _(event: E) -> "NotImplementedOrNone":
        """
        Handling of incoming mouse event.
        """
        # TypicaL:   "eSC[MaB*"
        # Urxvt:     "Esc[96;14;13M"
        # Xterm SGR: "Esc[<64;85;12M"

        # Parse incoming packet.
        if event.data[2] == "M":
            # Typical.
            mouse_event, x, y = map(ord, event.data[3:])

            # TODO: Is it possible to add modifiers here?
            mouse_button, mouse_event_type, mouse_modifiers = typical_mouse_events[
                mouse_event
            ]

            # Handle situations where `PosixStdinReader` used surrogateescapes.
            if x >= 0xDC00:
                x -= 0xDC00
            if y >= 0xDC00:
                y -= 0xDC00

            x -= 32
            y -= 32
        else:
            # Urxvt and Xterm SGR.
            # When the '<' is not present, we are not using the Xterm SGR mode,
            # but Urxvt instead.
            data = event.data[2:]
            if data[:1] == "<":
                sgr = True
                data = data[1:]
            else:
                sgr = False

            # Extract coordinates.
            mouse_event, x, y = map(int, data[:-1].split(";"))
            m = data[-1]

            # Parse event type.
            if sgr:
                try:
                    (
                        mouse_button,
                        mouse_event_type,
                        mouse_modifiers,
                    ) = xterm_sgr_mouse_events[mouse_event, m]
                except KeyError:
                    return NotImplemented

            else:
                # Some other terminals, like urxvt, Hyper terminal, ...
                (
                    mouse_button,
                    mouse_event_type,
                    mouse_modifiers,
                ) = urxvt_mouse_events.get(
                    mouse_event, (UNKNOWN_BUTTON, MOUSE_MOVE, UNKNOWN_MODIFIER)
                )

        x -= 1
        y -= 1

        # Only handle mouse events when we know the window height.
        if event.app.renderer.height_is_known and mouse_event_type is not None:
            # Take region above the layout into account. The reported
            # coordinates are absolute to the visible part of the terminal.
            from prompt_toolkit.renderer import HeightIsUnknownError

            try:
                y -= event.app.renderer.rows_above_layout
            except HeightIsUnknownError:
                return NotImplemented

            # Call the mouse handler from the renderer.

            # Note: This can return `NotImplemented` if no mouse handler was
            #       found for this position, or if no repainting needs to
            #       happen. this way, we avoid excessive repaints during mouse
            #       movements.
            handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x]
            return handler(
                MouseEvent(
                    position=Point(x=x, y=y),
                    event_type=mouse_event_type,
                    button=mouse_button,
                    modifiers=mouse_modifiers,
                )
            )

        return NotImplemented

    @key_bindings.add(Keys.ScrollUp)
    def _scroll_up(event: E) -> None:
        """
        Scroll up event without cursor position.
        """
        # We don't receive a cursor position, so we don't know which window to
        # scroll. Just send an 'up' key press instead.
        event.key_processor.feed(KeyPress(Keys.Up), first=True)

    @key_bindings.add(Keys.ScrollDown)
    def _scroll_down(event: E) -> None:
        """
        Scroll down event without cursor position.
        """
        event.key_processor.feed(KeyPress(Keys.Down), first=True)

    @key_bindings.add(Keys.WindowsMouseEvent)
    def _mouse(event: E) -> "NotImplementedOrNone":
        """
        Handling of mouse events for Windows.
        """
        # This key binding should only exist for Windows.
        if sys.platform == "win32":
            # Parse data.
            pieces = event.data.split(";")

            button = MouseButton(pieces[0])
            event_type = MouseEventType(pieces[1])
            x = int(pieces[2])
            y = int(pieces[3])

            # Make coordinates absolute to the visible part of the terminal.
            output = event.app.renderer.output

            from prompt_toolkit.output.win32 import Win32Output
            from prompt_toolkit.output.windows10 import Windows10_Output

            if isinstance(output, (Win32Output, Windows10_Output)):
                screen_buffer_info = output.get_win32_screen_buffer_info()
                rows_above_cursor = (
                    screen_buffer_info.dwCursorPosition.Y
                    - event.app.renderer._cursor_pos.y
                )
                y -= rows_above_cursor

                # Call the mouse event handler.
                # (Can return `NotImplemented`.)
                handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x]

                return handler(
                    MouseEvent(
                        position=Point(x=x, y=y),
                        event_type=event_type,
                        button=button,
                        modifiers=UNKNOWN_MODIFIER,
                    )
                )

        # No mouse handler found. Return `NotImplemented` so that we don't
        # invalidate the UI.
        return NotImplemented

    return key_bindings
