

from enum import Enum, auto
from typing import Sequence, Tuple, Union

from pisat.comm.transceiver.comm_socket import CommSocket


class RequestCommandError(Exception):
    """Raised if an error about commands occurs."""
    pass


class InvalidRequestError(Exception):
    """Raised if a given request was invalid."""
    pass


class RequestParams:
    
    reception_num: int = None
    command: bytes = None
    address: Tuple[bytes] = None
    args: Tuple[bytes] = None
        
    @classmethod
    def reset(cls) -> None:
        cls.RequestParams.reception_num = None
        cls.RequestParams.command = None
        cls.RequestParams.address = None
        cls.RequestParams.args = None


class CommandParams(Enum):
    ARGS_NOTHING = auto()
    ARGS_ARBITARY = auto()


class CommandBase:
    
    COMMAND = b""
    LEN_ARGS = CommandParams.ARGS_NOTHING
    
    @classmethod
    def exec(cls, socket: CommSocket, params: RequestParams) -> None:
        pass
    

class ResponseBase(CommandBase):
    
    COMMAND = b"FF"


class RequestForm:
    
    MIN_RECEPTION = 0
    MAX_RECEPTION = 1 << 8
        
    def __init__(self) -> None:
        self._reception_num: int = None
        self._command: CommandBase = None
        self._size_args: Tuple[int] = ()
        self._args: Tuple[bytes] = ()
        
    @property
    def reception_num(self):
        return self._reception_num
    
    @reception_num.setter
    def reception_num(self, val):
        if isinstance(val, int) and self.MIN_RECEPTION <= val <= self.MAX_RECEPTION:
            self._reception_num = val
        else:
            raise ValueError(
                "'reception_num' must be int and " + 
                f"{self.MIN_RECEPTION} <= x <= {self.MAX_RECEPTION}."
            )
            
    @property
    def command(self):
        return self._command
    
    @command.setter
    def command(self, val):
        if issubclass(val, CommandBase):
            self._command = val
        else:
            raise TypeError(
                "'command' must be a subclass of CommandBase."
            )
            
    @property
    def args(self):
        return self._args
    
    @args.setter
    def args(self, val):
        if isinstance(val, (list, tuple)):
            sizes = []
            for arg in val:
                if not isinstance(arg, bytes):
                    raise TypeError(
                        "Each elements of args must be bytes."
                    )
                
                sizes.append(len(arg))
                
            self._size_args = tuple(sizes)
            self._args = tuple(val)
        else:
            raise TypeError(
                "'args' must be a list or tuple."
            )
    
    @property
    def size_args(self):
        return self._size_args


class Request:
    
    FROM_HEX = 16
    TO_HEX = "X"
    
    SEPARATOR = b"$"
    SEPARATOR_NUM = SEPARATOR[0]
    SEMI_SEPARATOR = b":"
    
    LEN_BYTE_HEAD = 4    

    @classmethod
    def make_request(cls, 
                     socket: CommSocket,
                     form: RequestForm) -> bytes:
        data = bytearray()
        
        # head
        data.extend(cls.make_head(form))
        
        # address
        data.extend(cls.make_seq_param(socket, socket.addr_mine))
        data.extend(cls.SEPARATOR)
        
        # sizes of each args
        data.extend(cls.make_seq_param(socket, form.size_args))
        data.extend(cls.SEPARATOR)
        
        # args
        for arg in form.args:
            data.extend(arg)
            
        return bytes(data)
        
    @classmethod
    def make_head(cls, form) -> bytes:
        head = bytearray()
        head.extend(cls.SEPARATOR)
        head.append(form.reception_num)
        head.extend(form.command.COMMAND)
        
        return head
    
    @classmethod
    def make_seq_param(cls, socket: CommSocket, seq: Sequence) -> bytes:
        data = []
        for val in seq:
            if isinstance(val, int):
                val = format(val, cls.TO_HEX)

            data.append(socket.encode(val))
        
        return cls.SEMI_SEPARATOR.join(data)

    @classmethod
    def parse_request(cls, socket: CommSocket):
        reception_num, command = cls.extract_head(socket)
        addr = cls.extract_param(socket).split(cls.SEMI_SEPARATOR)
        addr = tuple(map(bytes, addr))
        size_args_raw = cls.extract_param(socket).split(cls.SEMI_SEPARATOR)
        
        args = []
        for length in map(lambda x: int(x, cls.FROM_HEX), size_args_raw):
            args.append(cls.certain_recv(socket, length))
            
        RequestParams.reception_num = reception_num
        RequestParams.command = command
        RequestParams.address = addr
        RequestParams.args = tuple(args)
        
        return RequestParams
            
    @staticmethod
    def certain_recv(socket: CommSocket, count: int) -> bytes:
        count_current = 0
        data = bytearray()
        
        while True:
            data.extend(socket.recv(count - count_current))
            count_current = len(data)
            if count_current >= count:
                break
            
        return bytes(data)
        
    @classmethod
    def extract_head(cls, socket: CommSocket) -> Tuple[Union[int, bytes]]:
        
        head = cls.certain_recv(socket, cls.LEN_BYTE_HEAD)
        if head[0] != cls.SEPARATOR_NUM:
            raise InvalidRequestError(
                f"Invalid request was detected. HEAD: {head}"
            )
            
        reception_num = head[1]
        command = head[2:]
        
        return (reception_num, command)
    
    @classmethod
    def extract_param(cls, socket: CommSocket) -> bytes:
        data = bytearray()
        while True:
            char = socket.recv(1)
            if char == cls.SEPARATOR:
                break
            
            data.extend(char)
            
        return data
