from collections import defaultdict

from typing import (Any,
                    ClassVar,
                    List,
                    Mapping,
                    MutableMapping,
                    Optional,
                    Sequence,
                    Set,
                    Type,
                    TypeVar,
                    Union)

from ._module import BidiModule, command

InputSourceType = TypeVar('InputSourceType', bound="InputSource")


class Action:
    action_type: ClassVar[str]

    def to_json(self) -> MutableMapping[str, Any]:
        return {"type": self.action_type}


class PauseAction(Action):
    action_type = "pause"

    def __init__(self, duration: Optional[int] = None):
        self.duration = duration

    def to_json(self) -> MutableMapping[str, Any]:
        rv = super().to_json()
        if self.duration is not None:
            rv["duration"] = self.duration
        return rv


class KeyAction(Action):

    def __init__(self, key: str):
        self.value = key

    def to_json(self) -> MutableMapping[str, Any]:
        rv = super().to_json()
        rv["value"] = self.value
        return rv


class KeyUpAction(KeyAction):
    action_type = "keyUp"


class KeyDownAction(KeyAction):
    action_type = "keyDown"


class PointerAction(Action):

    def __init__(self,
                 button: Optional[int] = None,
                 x: Optional[int] = None,
                 y: Optional[int] = None,
                 duration: Optional[int] = None,
                 origin: Optional[Union[str, Mapping[str, Any]]] = None,
                 width: Optional[int] = None,
                 height: Optional[int] = None,
                 pressure: Optional[float] = None,
                 tangential_pressure: Optional[float] = None,
                 tilt_x: Optional[int] = None,
                 tilt_y: Optional[int] = None,
                 twist: Optional[int] = None,
                 altitude_angle: Optional[float] = None,
                 azimuth_angle: Optional[float] = None):
        self.button = button
        self.x = x
        self.y = y
        self.duration = duration
        self.origin = origin
        self.width = width
        self.height = height
        self.pressure = pressure
        self.tangential_pressure = tangential_pressure
        self.tilt_x = tilt_x
        self.tilt_y = tilt_y
        self.twist = twist
        self.altitude_angle = altitude_angle
        self.azimuth_angle = azimuth_angle

    def to_json(self) -> MutableMapping[str, Any]:
        rv = super().to_json()
        if self.button is not None:
            rv["button"] = self.button
        if self.x is not None:
            rv["x"] = self.x
        if self.y is not None:
            rv["y"] = self.y
        if self.duration is not None:
            rv["duration"] = self.duration
        if self.origin is not None:
            rv["origin"] = self.origin
        if self.width is not None:
            rv["width"] = self.width
        if self.height is not None:
            rv["height"] = self.height
        if self.pressure is not None:
            rv["pressure"] = self.pressure
        if self.tangential_pressure is not None:
            rv["tangentialPressure"] = self.tangential_pressure
        if self.tilt_x is not None:
            rv["tiltX"] = self.tilt_x
        if self.tilt_y is not None:
            rv["tiltY"] = self.tilt_y
        if self.twist is not None:
            rv["twist"] = self.twist
        if self.altitude_angle is not None:
            rv["altitudeAngle"] = self.altitude_angle
        if self.azimuth_angle is not None:
            rv["azimuthAngle"] = self.azimuth_angle
        return rv


class PointerDownAction(PointerAction):
    action_type = "pointerDown"

    def __init__(self,
                 button: int,
                 width: Optional[int] = None,
                 height: Optional[int] = None,
                 pressure: Optional[float] = None,
                 tangential_pressure: Optional[float] = None,
                 tilt_x: Optional[int] = None,
                 tilt_y: Optional[int] = None,
                 twist: Optional[int] = None,
                 altitude_angle: Optional[float] = None,
                 azimuth_angle: Optional[float] = None):
        super().__init__(button=button,
                         x=None,
                         y=None,
                         duration=None,
                         origin=None,
                         width=width,
                         height=height,
                         pressure=pressure,
                         tangential_pressure=tangential_pressure,
                         tilt_x=tilt_x,
                         tilt_y=tilt_y,
                         twist=twist,
                         altitude_angle=altitude_angle,
                         azimuth_angle=azimuth_angle)


class PointerUpAction(PointerAction):
    action_type = "pointerUp"

    def __init__(self,
                 button: int,
                 width: Optional[int] = None,
                 height: Optional[int] = None,
                 pressure: Optional[float] = None,
                 tangential_pressure: Optional[float] = None,
                 tilt_x: Optional[int] = None,
                 tilt_y: Optional[int] = None,
                 twist: Optional[int] = None,
                 altitude_angle: Optional[float] = None,
                 azimuth_angle: Optional[float] = None):
        super().__init__(button=button,
                         x=None,
                         y=None,
                         duration=None,
                         origin=None,
                         width=width,
                         height=height,
                         pressure=pressure,
                         tangential_pressure=tangential_pressure,
                         tilt_x=tilt_x,
                         tilt_y=tilt_y,
                         twist=twist,
                         altitude_angle=altitude_angle,
                         azimuth_angle=azimuth_angle)


class PointerMoveAction(PointerAction):
    action_type = "pointerMove"

    def __init__(self,
                 x: int,
                 y: int,
                 duration: Optional[int] = None,
                 origin: Optional[Union[str, Mapping[str, Any]]] = None,
                 width: Optional[int] = None,
                 height: Optional[int] = None,
                 pressure: Optional[float] = None,
                 tangential_pressure: Optional[float] = None,
                 tilt_x: Optional[int] = None,
                 tilt_y: Optional[int] = None,
                 twist: Optional[int] = None,
                 altitude_angle: Optional[float] = None,
                 azimuth_angle: Optional[float] = None):
        super().__init__(button=None,
                         x=x,
                         y=y,
                         duration=duration,
                         origin=origin,
                         width=width,
                         height=height,
                         pressure=pressure,
                         tangential_pressure=tangential_pressure,
                         tilt_x=tilt_x,
                         tilt_y=tilt_y,
                         twist=twist,
                         altitude_angle=altitude_angle,
                         azimuth_angle=azimuth_angle)


class WheelScrollAction(Action):
    action_type = "scroll"

    def __init__(self,
                 x: int,
                 y: int,
                 delta_x: int,
                 delta_y: int,
                 duration: Optional[int] = None,
                 origin: Optional[Union[str, Mapping[str, Any]]] = None):
        self.x = x
        self.y = y
        self.delta_x = delta_x
        self.delta_y = delta_y
        self.duration = duration
        self.origin = origin

    def to_json(self) -> MutableMapping[str, Any]:
        rv = super().to_json()
        rv.update({
            "x": self.x,
            "y": self.y,
            "deltaX": self.delta_x,
            "deltaY": self.delta_y
        })
        if self.duration is not None:
            rv["duration"] = self.duration
        if self.origin is not None:
            rv["origin"] = self.origin
        return rv


class InputSource:
    input_type: ClassVar[str]

    def __init__(self, input_id: str, **kwargs: Any):
        """Represents a sequence of actions of one type for one input source.

        :param input_id: ID of input source.
        """
        self.id = input_id
        self.actions: List[Action] = []

    def __len__(self) -> int:
        return len(self.actions)

    def to_json(self, total_ticks: int) -> MutableMapping[str, Any]:
        actions = [item.to_json() for item in self.actions]
        for i in range(total_ticks - len(self)):
            actions.append(PauseAction().to_json())

        return {"id": self.id, "type": self.input_type, "actions": actions}

    def done(self) -> List[Action]:
        return self.actions

    def pause(self: InputSourceType,
              duration: Optional[int] = None) -> InputSourceType:
        self.actions.append(PauseAction(duration))
        return self


class KeyInputSource(InputSource):
    input_type = "key"

    def key_down(self, key: str) -> "KeyInputSource":
        self.actions.append(KeyDownAction(key))
        return self

    def key_up(self, key: str) -> "KeyInputSource":
        self.actions.append(KeyUpAction(key))
        return self

    def send_keys(self, keys: str) -> "KeyInputSource":
        for c in keys:
            self.key_down(c)
            self.key_up(c)
        return self


class PointerInputSource(InputSource):
    input_type = "pointer"

    def __init__(self, input_id: str, pointer_type: str = "mouse"):
        super().__init__(input_id)
        self.parameters = {"pointerType": pointer_type}

    def to_json(self, total_ticks: int) -> MutableMapping[str, Any]:
        rv = super().to_json(total_ticks)
        rv["parameters"] = self.parameters
        return rv

    def pointer_down(self, button: int, **kwargs: Any) -> "PointerInputSource":
        self.actions.append(PointerDownAction(button, **kwargs))
        return self

    def pointer_up(self, button: int, **kwargs: Any) -> "PointerInputSource":
        self.actions.append(PointerUpAction(button, **kwargs))
        return self

    def pointer_move(self,
                     x: int,
                     y: int,
                     duration: Optional[int] = None,
                     origin: Union[str, Mapping[str, Any]] = "viewport",
                     **kwargs: Any) -> "PointerInputSource":
        self.actions.append(
            PointerMoveAction(x, y, duration=duration, origin=origin,
                              **kwargs))
        return self


class WheelInputSource(InputSource):
    input_type = "wheel"

    def scroll(self,
               x: int,
               y: int,
               delta_x: int = 0,
               delta_y: int = 0,
               duration: Optional[int] = None,
               origin: Union[str, Mapping[str, Any]] = "viewport") -> "WheelInputSource":
        self.actions.append(WheelScrollAction(x,
                                              y,
                                              delta_x=delta_x,
                                              delta_y=delta_y,
                                              duration=duration,
                                              origin=origin))
        return self


class Actions:

    def __init__(self) -> None:
        self.input_sources: List[InputSource] = []
        self.seen_names: MutableMapping[str, Set[str]] = defaultdict(set)

    def _add_source(self,
                    cls: Type[InputSourceType],
                    input_id: Optional[str] = None,
                    **kwargs: Any) -> InputSourceType:
        input_type = cls.input_type
        if input_id is None:
            i = 0
            input_id = f"{input_type}-{i}"
            while input_id in self.seen_names[input_type]:
                i += 1
                input_id = f"{input_type}-{i}"
        else:
            if input_id in self.seen_names[input_type]:
                raise ValueError(f"Duplicate input id ${input_id}")

        self.seen_names[input_type].add(input_id)
        rv = cls(input_id, **kwargs)
        self.input_sources.append(rv)
        return rv

    def add_key(self, input_id: Optional[str] = None) -> "KeyInputSource":
        return self._add_source(KeyInputSource, input_id)

    def add_pointer(self,
                    input_id: Optional[str] = None,
                    pointer_type: str = "mouse") -> "PointerInputSource":
        return self._add_source(PointerInputSource,
                                input_id,
                                pointer_type=pointer_type)

    def add_wheel(self, input_id: Optional[str] = None) -> "WheelInputSource":
        return self._add_source(WheelInputSource, input_id)

    def to_json(self) -> Sequence[Mapping[str, Any]]:
        num_ticks = max(len(item) for item in self.input_sources)
        return [item.to_json(num_ticks) for item in self.input_sources]


class Input(BidiModule):

    @command
    def perform_actions(self,
                        actions: Union[Actions, List[Any]],
                        context: str
                        ) -> Mapping[str, Any]:
        params: MutableMapping[str, Any] = {
            "context": context
        }
        if isinstance(actions, Actions):
            params["actions"] = actions.to_json()
        else:
            params["actions"] = actions
        return params

    @command
    def release_actions(self, context: str) -> Mapping[str, Any]:
        params: MutableMapping[str, Any] = {"context": context}
        return params

    @command
    def set_files(
        self, context: str, element: Any, files: List[str]
    ) -> Mapping[str, Any]:
        params: MutableMapping[str, Any] = {
            "context": context,
            "element": element,
            "files": files,
        }
        return params


def get_element_origin(element: Any) -> Mapping[str, Any]:
    return {"type": "element", "element": {"sharedId": element["sharedId"]}}
