from copy import deepcopy
from dataclasses import dataclass, field
from enum import Enum
from time import time
from typing import Any, Callable, Dict, List, Optional, Union


class Mode(Enum):
    NONE = ''
    """Special mode used for the REPEAT command"""

    AT = "AT"
    """Special mode to send AT commands"""

    REQUEST = 0x01
    """Request current data"""
    FREEZE_FRAME = 0x02
    """Request freeze frame data"""
    STATUS_DTC = 0x03
    """Request stored DTCs (Diagnostic Trouble Codes)"""
    CLEAR_DTC = 0x04
    """Clear/reset DTCs (Diagnostic Trouble Codes)"""
    O2_SENSOR = 0x05
    """Request oxygen sensor monitoring test results"""
    PENDING_DTC = 0x06
    """Request DTCs (Diagnostic Trouble Codes) pending"""
    CONTROL_MODULE = 0x07
    """Request control module information"""
    O2_SENSOR_TEST = 0x08
    """Request oxygen sensor test results"""
    VEHICLE_INFO = 0x09
    """Request vehicle information"""
    PERMANENT_DTC = 0x0A
    """Request permanent DTCs (Diagnostic Trouble Codes)"""


class Protocol(Enum):
    UNKNOWN = -1
    """Unknown protocol"""

    AUTO = 0x00
    """Automatically determine the protocol"""
    SAE_J1850_PWM = 0x01
    """SAE J1850 PWM (41.6 kbaud)"""
    SAE_J1850_VPW = 0x02
    """SAE J1850 VPW (10.4 kbaud)"""
    ISO_9141_2 = 0x03
    """ISO 9141-2 (5 baud init, 10.4 kbaud)"""
    ISO_14230_4_KWP = 0x04
    """ISO 14230-4 KWP (5 baud init, 10.4 kbaud)"""
    ISO_14230_4_KWP_FAST = 0x05
    """ISO 14230-4 KWP (fast init, 10.4 kbaud)"""
    ISO_15765_4_CAN = 0x06
    """ISO 15765-4 CAN (11 bit ID, 500 kbaud)"""
    ISO_15765_4_CAN_B = 0x07
    """ISO 15765-4 CAN (29 bit ID, 500 kbaud)"""
    ISO_15765_4_CAN_C = 0x08
    """ISO 15765-4 CAN (11 bit ID, 250 kbaud)"""
    ISO_15765_4_CAN_D = 0x09
    """ISO 15765-4 CAN (29 bit ID, 250 kbaud)"""
    SAE_J1939_CAN = 0x0A
    """SAE J1939 CAN (29 bit ID, 250* kbaud) *default settings (user adjustable)"""
    USER1_CAN = 0x0B
    """USER1 CAN (11* bit ID, 125* kbaud) *default settings (user adjustable)"""
    USER2_CAN = 0x0C
    """USER2 CAN (11* bit ID, 50* kbaud) *default settings (user adjustable)"""

class Command():
    def __init__(self, 
            mode: Mode,
            pid: Union[int, str],
            n_bytes: int,
            name: str,
            description: Optional[str] = None,
            min_value: Optional[Union[int, float, str]] = None,
            max_value: Optional[Union[int, float, str]] = None,
            units: Optional[str] = None,
            formula: Optional[Callable] = None,
            command_args: Optional[Dict[str, Any]] = None,
        ) -> None:
        self.mode = mode
        self.pid = pid
        self.n_bytes = n_bytes
        self.name = name
        self.description = description
        self.min_value = min_value
        self.max_value = max_value
        self.units = units
        self.formula = formula

        self.command_args = command_args or {}
        self.is_formatted = False

    def __call__(self, *args: Any, checks: bool = True) -> "Command":
        if not self.command_args or not args or len(self.command_args) != len(args):
            raise TypeError(f"{self.__repr__()} expects {len(self.command_args)} argument(s), but got {len(args)}")
        
        fmt_command = deepcopy(self) # we don't want to modify the original command pid
        try:
            combined_args = {}
            for (arg, arg_type), value in zip(self.command_args.items(), args):
                if checks:
                    if not isinstance(value, arg_type):
                        raise TypeError(f"Argument '{arg}' should be of type {arg_type}")

                    if isinstance(value, int):
                        value = f"{value:0{len(arg)}X}"
                    elif isinstance(value, str) and len(value) != len(arg):
                        raise ValueError(f"Argument '{arg}' should have length {len(arg)}, but got {len(value)}")

                combined_args[arg] = value

            fmt_command.is_formatted = True
            fmt_command.pid = str(self.pid).format(**combined_args)
        except Exception as e:
            raise e
        return fmt_command

    def __repr__(self) -> str:
        return f"<Command {self.mode} {self.pid if isinstance(self.pid, str) else f'{self.pid:02X}'} {self.name or 'Unnamed'} [{', '.join(self.command_args.keys())}]>"
    
    def __eq__(self, value: object) -> bool:
        if not isinstance(value, Command):
            return False

        return (
            self.mode == value.mode and
            self.name == value.name and
            self.description == value.description and
            self.command_args == value.command_args
        )

    def build(self) -> bytes:
        """Builds the query to be sent.
        ELM327 is not case-sensitive, ignores spaces and all control characters."""
        if self.command_args and not self.is_formatted:
            raise ValueError(f"Command has unset arguments for '{self.pid}': {self.command_args}")

        mode = self.mode.value
        pid = self.pid
        if isinstance(mode, int):
            mode = f"{mode:02X}"
        if isinstance(pid, int):
            pid = f"{pid:02X}"
        return f"{mode} {pid}\r".encode() # \r ends each command


class BaseMode():
    def __getitem__(self, key) -> Command:
        if isinstance(key, int):
            for attr_name in dir(self):
                attr = getattr(self, attr_name)
                if hasattr(attr, "pid") and attr.pid == key:
                    return attr
        raise KeyError(f"No command found with PID {key}")
    
    def __repr__(self) -> str:
        return f"<Mode Commands: {len(self)}>"

    def __len__(self) -> int:
        return len([1 for attr_name in dir(self) if isinstance(getattr(self, attr_name), Command)])
    
    def has_command(self, command: Union[Command, str]) -> bool:
        if isinstance(command, Command):
            command = command.name

        command = command.upper()
        return hasattr(self, command) and isinstance(getattr(self, command), Command)


@dataclass
class BaseResponse():
    command: Command
    raw_response: List[bytes]
    message: List[List[bytes]]
    timestamp: float = field(default_factory=time)

@dataclass
class Response(BaseResponse):
    parsed_data: Optional[List[List[str]]] = None

    value: Optional[Any] = None

    @property
    def min_value(self) -> Optional[Union[int, float, str]]:
        return self.command.min_value
    
    @property
    def max_value(self) -> Optional[Union[int, float, str]]:
        return self.command.max_value
    
    @property
    def units(self) -> Optional[str]:
        return self.command.units