import abc
from dataclasses import dataclass
from enum import Enum
from typing import Sequence, Any, Iterator, List, Dict, Optional

from wapiti_arsenic import constants
from wapiti_arsenic.session import Element


@dataclass
class Action:
    source: "Device"
    payload: Dict[str, Any]


class Tick:
    def __init__(self, *actions: Action):
        self.actions: Dict["Device", "Action"] = {
            action.source: action for action in actions
        }

    def __and__(self, other: "Tick") -> "Tick":
        overlap = set(self.actions.keys()) & set(other.actions.keys())
        if overlap:
            raise ValueError(
                f"Devices {overlap} have more than one action in this tick"
            )
        return Tick(*self.actions.values(), *other.actions.values())

    def encode(self, device: "Device") -> Dict[str, Any]:
        if device not in self.actions:
            return {"type": "pause", "duration": 0}

        return self.actions[device].payload


class DeviceType(Enum):
    keyboard = "key"
    pointer = "pointer"


class PointerType(Enum):
    mouse = "mouse"
    pen = "pen"
    touch = "touch"


class Button(Enum):
    left = 0
    middle = 1
    right = 2


class Device(metaclass=abc.ABCMeta):
    type: DeviceType = abc.abstractproperty()

    def __init__(self, device_id: Optional[str] = None):
        self.device_id = device_id

    def info(self, index: int) -> Dict[str, Any]:
        device_id = self.device_id or f"{self.type.value}{index}"
        return {"id": device_id, "type": self.type.value}

    def _tick(self, **payload: Any) -> Tick:
        return Tick(Action(self, payload))

    def pause(self, duration: int) -> Tick:
        return self._tick(type="pause", duration=duration)


class Pointer(Device, metaclass=abc.ABCMeta):
    type = DeviceType.pointer
    pointer_type: PointerType = abc.abstractproperty()

    def info(self, index: int) -> Dict[str, Any]:
        return {
            "parameters": {"pointerType": self.pointer_type.value},
            **super().info(index),
        }

    def move_to(self, element: Element, duration: int = 250) -> Tick:
        return self._tick(
            type="pointerMove",
            duration=duration,
            origin={constants.WEB_ELEMENT: element.id},
            x=0,
            y=0,
        )

    def move_by(self, x: int, y: int, duration: int = 250) -> Tick:
        return self._tick(
            type="pointerMove", duration=duration, origin="pointer", x=x, y=y
        )

    def down(self) -> Tick:
        return self._tick(type="pointerDown", duration=0, button=0)

    def up(self) -> Tick:
        return self._tick(type="pointerUp", duration=0, button=0)


class Mouse(Pointer):
    pointer_type = PointerType.mouse

    def down(self, button: Button = Button.left) -> Tick:
        return self._tick(type="pointerDown", duration=0, button=button.value)

    def up(self, button: Button = Button.left) -> Tick:
        return self._tick(type="pointerUp", duration=0, button=button.value)


class Pen(Pointer):
    pointer_type = PointerType.pen


class Touch(Pointer):
    pointer_type = PointerType.touch


class Keyboard(Device):
    type = DeviceType.keyboard

    def down(self, key: str) -> Tick:
        return self._tick(type="keyDown", value=key)

    def up(self, key: str) -> Tick:
        return self._tick(type="keyUp", value=key)


def gather_devices(ticks: Sequence[Tick]) -> Iterator[Device]:
    found = set()
    for tick in ticks:
        devices = set(tick.actions.keys())
        for device in devices - found:
            yield device
        found.update(devices)


def chain(*ticks: Tick) -> Dict[str, List[Dict[str, Any]]]:
    devices = list(gather_devices(ticks))
    return {
        "actions": [
            {**device.info(index), "actions": [tick.encode(device) for tick in ticks]}
            for index, device in enumerate(devices, start=1)
        ]
    }
