import abc
import asyncio
import enum
import logging
from dataclasses import dataclass
from typing import cast, Tuple, Union, List, TYPE_CHECKING
from typing_extensions import Literal
from opentrons import types as top_types

if TYPE_CHECKING:
    from .dev_types import DoorStateNotificationType

MODULE_LOG = logging.getLogger(__name__)


class OutOfBoundsMove(RuntimeError):
    def __init__(self, message: str):
        self.message = message
        super().__init__()

    def __str__(self) -> str:
        return f"OutOfBoundsMove: {self.message}"

    def __repr__(self) -> str:
        return f"<{str(self.__class__)}: {self.message}>"


class MotionChecks(enum.Enum):
    NONE = 0
    LOW = 1
    HIGH = 2
    BOTH = 3


class Axis(enum.Enum):
    X = 0
    Y = 1
    Z = 2
    A = 3
    B = 4
    C = 5

    @classmethod
    def by_mount(cls, mount: top_types.Mount):
        bm = {top_types.Mount.LEFT: cls.Z, top_types.Mount.RIGHT: cls.A}
        return bm[mount]

    @classmethod
    def gantry_axes(cls) -> Tuple["Axis", "Axis", "Axis", "Axis"]:
        """The axes which are tied to the gantry and require the deck
        calibration transform
        """
        return (cls.X, cls.Y, cls.Z, cls.A)

    @classmethod
    def of_plunger(cls, mount: top_types.Mount):
        pm = {top_types.Mount.LEFT: cls.B, top_types.Mount.RIGHT: cls.C}
        return pm[mount]

    @classmethod
    def to_mount(cls, inst: "Axis"):
        return {
            cls.Z: top_types.Mount.LEFT,
            cls.A: top_types.Mount.RIGHT,
            cls.B: top_types.Mount.LEFT,
            cls.C: top_types.Mount.RIGHT,
        }[inst]

    def __str__(self):
        return self.name


class DoorState(enum.Enum):
    OPEN = False
    CLOSED = True

    def __str__(self):
        return self.name.lower()


class HardwareEventType(enum.Enum):
    DOOR_SWITCH_CHANGE = enum.auto()


@dataclass
class DoorStateNotification:
    event: "DoorStateNotificationType" = HardwareEventType.DOOR_SWITCH_CHANGE
    new_state: DoorState = DoorState.CLOSED
    blocking: bool = False


# new event types get new dataclasses
# when we add more event types we add them here
HardwareEvent = Union[DoorStateNotification]


class HardwareAPILike(abc.ABC):
    """A dummy class useful in isinstance checks to accept an API or adapter"""

    @property
    def loop(self) -> asyncio.AbstractEventLoop:
        ...

    @property
    def board_revision(self) -> str:
        ...

    @property
    def door_state(self) -> DoorState:
        ...

    @door_state.setter
    def door_state(self, door_state: DoorState) -> DoorState:
        ...

    def validate_calibration(self):
        ...


RevisionLiteral = Literal["2.1", "A", "B", "C", "UNKNOWN"]


class BoardRevision(enum.Enum):
    UNKNOWN = enum.auto()
    OG = enum.auto()
    A = enum.auto()
    B = enum.auto()
    C = enum.auto()

    @classmethod
    def by_bits(cls, rev_bits: Tuple[bool, bool]):
        br = {
            (True, True): cls.OG,
            (False, True): cls.A,
            (True, False): cls.B,
            (False, False): cls.C,
        }
        return br[rev_bits]

    def real_name(self) -> Union[RevisionLiteral, Literal["UNKNOWN"]]:
        rn = "2.1" if self.name == "OG" else self.name
        return cast(Union[RevisionLiteral, Literal["UNKNOWN"]], rn)

    def __str__(self) -> str:
        return self.real_name()


class CriticalPoint(enum.Enum):
    """Possibilities for the point to move in a move call.

    The active critical point determines the offsets that are added to the
    gantry position when moving a pipette around.
    """

    MOUNT = enum.auto()
    """
    For legacy reasons, the position of the end of a P300 single. The default
    when no pipette is attached, and used for consistent behavior in certain
    contexts (like change pipette) when a variety of different pipettes might
    be attached.
    """

    NOZZLE = enum.auto()
    """
    The end of the nozzle of a single pipette or the end of the back-most
    nozzle of a multipipette. Only relevant when a pipette is present.
    """

    TIP = enum.auto()
    """
    The end of the tip of a single pipette or the end of the back-most
    tip of a multipipette. Only relevant when a pipette is present and
    a tip with known tip length is attached.
    """

    XY_CENTER = enum.auto()
    """
    Separately from the z component of the critical point, XY_CENTER means
    the critical point under consideration is the XY center of the pipette.
    This changes nothing for single pipettes, but makes multipipettes
    move their centers - so between channels 4 and 5 - to the specified
    point.
    """

    FRONT_NOZZLE = enum.auto()
    """
    The end of the front-most nozzle of a multipipette with a tip attached.
    Only relevant when a multichannel pipette is present.
    """


class ExecutionState(enum.Enum):
    RUNNING = enum.auto()
    PAUSED = enum.auto()
    CANCELLED = enum.auto()

    def __str__(self):
        return self.name


class PipettePair(enum.Enum):
    PRIMARY_LEFT = enum.auto()
    PRIMARY_RIGHT = enum.auto()

    @property
    def primary(self) -> top_types.Mount:
        if self.name == "PRIMARY_RIGHT":
            return top_types.Mount.RIGHT
        else:
            return top_types.Mount.LEFT

    @property
    def secondary(self) -> top_types.Mount:
        if self.name == "PRIMARY_RIGHT":
            return top_types.Mount.LEFT
        else:
            return top_types.Mount.RIGHT

    @classmethod
    def of_mount(cls, mount: top_types.Mount) -> "PipettePair":
        pair = {
            top_types.Mount.LEFT: cls.PRIMARY_LEFT,
            top_types.Mount.RIGHT: cls.PRIMARY_RIGHT,
        }
        return pair[mount]


class HardwareAction(enum.Enum):
    DROPTIP = enum.auto()
    ASPIRATE = enum.auto()
    DISPENSE = enum.auto()
    BLOWOUT = enum.auto()
    PREPARE_ASPIRATE = enum.auto()

    def __str__(self):
        return self.name


class PauseType(enum.Enum):
    PAUSE = 0
    DELAY = 1


@dataclass
class AionotifyEvent:
    flags: enum.EnumMeta
    name: str

    @classmethod
    def build(cls, name: str, flags: List[enum.Enum]) -> "AionotifyEvent":
        # See https://github.com/python/mypy/issues/5317
        # as to why mypy cannot detect that list
        # comprehension or variables cannot be dynamically
        # determined to meet the argument criteria for
        # enums. Hence, the type ignore below.
        flag_list = [f.name for f in flags]
        Flag = enum.Enum("Flag", flag_list)  # type: ignore
        return cls(flags=Flag, name=name)


class PauseResumeError(RuntimeError):
    pass


class ExecutionCancelledError(RuntimeError):
    pass


class MustHomeError(RuntimeError):
    pass


class NoTipAttachedError(RuntimeError):
    pass


class TipAttachedError(RuntimeError):
    pass


class PairedPipetteConfigValueError(RuntimeError):
    pass
