from enum import IntEnum
from typing import Any, List, Optional

from ordered_set import OrderedSet

from ymodem.Version import Version

class ProtocolType(IntEnum):
    XMODEM = 0,
    YMODEM = 1,
    # not support yet
    ZMODEM = 2

    @classmethod
    def all(cls) -> List[int]:
        return [
            cls.XMODEM,
            cls.YMODEM,
            cls.ZMODEM
        ]
    
class ProtocolSubType(IntEnum):
    YMODEM_BATCH_FILE_TRANSMISSION = 0,
    YMODEM_G_FILE_TRANSMISSION = 1
    
class ZMODEM:

    ZPAD                = '*'
    ZDLE                = 24        # 0o30
    ZDLEE               = ZDLE ^ 64 # 0o100
    ZBIN                = 'A'
    ZHEX                = 'B'
    ZBIN32              = 'C'

    #########################################
    #
    #             Frame type
    #
    #########################################

    ZRQINIT             = 0
    ZRINIT              = 1
    ZSINIT              = 2
    ZACK                = 3
    ZFILE               = 4
    ZSKIP               = 5
    ZNAK                = 6
    ZABORT              = 7
    ZFIN                = 8
    ZRPOS               = 9
    ZDATA               = 10
    ZEOF                = 11
    ZFERR               = 12
    ZCRC                = 13
    ZCHALLENGE          = 14
    ZCOMPL              = 15
    ZCAN                = 16
    ZFREECNT            = 17
    ZCOMMAND            = 18
    ZSTDERR             = 19

    #########################################
    #
    #              ZDLE sequence
    #
    #########################################

    ZCRCE               = 'h'
    ZCRCG               = 'i'
    ZCRCQ               = 'j'
    ZCRCW               = 'k'
    ZRUB0               = 'l'
    ZRUB1               = 'm'

    #########################################
    #
    #              Byte position
    #
    #########################################

    # Byte positions within header array
    ZF0                 = 3
    ZF1                 = 2
    ZF2                 = 1
    ZF3                 = 0
    ZP0                 = 0
    ZP1                 = 1
    ZP2                 = 2
    ZP3                 = 3

    #########################################
    #
    #                 ZRINIT
    #
    #########################################

    # Bit Masks for ZRINIT flags byte ZF0
    CANFDX              = 0b00000001
    CANOVIO             = 0b00000010
    CANBRK              = 0b00000100
    CANCRY              = 0b00001000
    CANLZW              = 0b00010000
    CANFC32             = 0b00100000
    ESCCTL              = 0b01000000
    ESC8                = 0b10000000

    # Bit Masks for ZRINIT flags byte ZF1
    ZF1_CANVHDR         = 0b00000001
    ZF1_TIMESYNC        = 0b00000010

    #########################################
    #
    #                 ZSINIT
    #
    #########################################

    # Parameters for ZSINIT frame
    ZATTNLEN            = 32

    # Bit Masks for ZSINIT flags byte ZF0
    TESCCTL             = 64    # 0o100
    TESC8               = 128   # 0o200

    #########################################
    #
    #                 ZFILE
    #
    #########################################

    ZCBIN               = 1
    ZCNL                = 2
    ZCRESUM             = 3

    #########################################
    #
    #                 Management
    #
    #########################################

    # Management include options, one of these ored in ZF1
    ZF1_ZMSKNOLOC       = 0x80

    # Management options, one of these ored in ZF1
    ZF1_ZMMASK          = 0x1f
    ZF1_ZMNEWL          = 1
    ZF1_ZMCRC           = 2
    ZF1_ZMAPND          = 3
    ZF1_ZMCLOB          = 4
    ZF1_ZMNEW           = 5
    ZF1_ZMDIFF          = 6
    ZF1_ZMPROT          = 7
    ZF1_ZMCHNG          = 8

    #########################################
    #
    #                 Transport
    #
    #########################################

    # Transport options, one of these in ZF2
    ZTLZW               = 1
    ZTCRYPT             = 2
    ZTRLE               = 3

    # Extended options for ZF3, bit encoded
    ZXSPARS             = 64

    #########################################
    #
    #                 ZCOMMAND
    #
    #########################################

    ZCACK1              = 1

class XMODEM:

    # Bit Masks for XMODEM features
    USE_CHECKSUM        = 0b00000001
    USE_CRC             = 0b00000010
    ALLOW_1K_PACKET     = 0b00010000
    
class YMODEM:

    #########################################
    #
    #                 SYMBOL
    #
    #########################################

    SOH                 = b'\x01'
    STX                 = b'\x02'
    EOT                 = b'\x04'
    ACK                 = b'\x06'
    NAK                 = b'\x15'
    CAN                 = b'\x18'
    CRC                 = b'\x43'

    #########################################
    #
    #                 Feature
    #
    #########################################

    # Bit Masks for YMODEM features
    USE_LENGTH_FIELD    = 0b00000001
    USE_DATE_FIELD      = 0b00000010
    USE_MODE_FIELD      = 0b00000100
    USE_SN_FIELD        = 0b00001000
    ALLOW_1K_PACKET     = 0b00010000
    ALLOW_YMODEM_G      = 0b00100000

    @classmethod
    def features(cls) -> List[int]:
        return [
            cls.USE_LENGTH_FIELD,
            cls.USE_DATE_FIELD,
            cls.USE_MODE_FIELD,
            cls.USE_SN_FIELD,
            cls.ALLOW_1K_PACKET,
            cls.ALLOW_YMODEM_G
        ]
    
    @classmethod
    def full_features(cls) -> List[int]:
        PROTOCOL_TYPE = ProtocolType.YMODEM << 8
        return [
            PROTOCOL_TYPE + cls.USE_LENGTH_FIELD,
            PROTOCOL_TYPE + cls.USE_DATE_FIELD,
            PROTOCOL_TYPE + cls.USE_MODE_FIELD,
            PROTOCOL_TYPE + cls.USE_SN_FIELD,
            PROTOCOL_TYPE + cls.ALLOW_1K_PACKET,
            PROTOCOL_TYPE + cls.ALLOW_YMODEM_G
        ]
    
class _ProtocolStyle:
    def __init__(self):
        self._feature_dict = {}

    def set_protocol_features(self, protocol_type: int, features: Any):
        self._feature_dict[protocol_type] = features
    
    def get_protocol_features(self, protocol_type: int):
        return self._feature_dict[protocol_type]
    
class ProtocolStyle:
    def __init__(self, name: str):
        self._name = name
        self._id = self._name.upper().replace(' ', '_').replace('/', '_').replace('-', '_')
        self._registered_versions = OrderedSet()
        self._deprecated_versions = set()
        self._target_version = None
        self._cores = {}
        self._enabled = True

    @property
    def id(self) -> str:
        return self._id

    @property
    def name(self) -> str:
        return self._name
    
    @name.setter
    def name(self, name: str):
        self._name = name

    @property
    def enabled(self) -> bool:
        return self._enabled
    
    @enabled.setter
    def enable(self, enabled: bool):
        self._enabled = enabled

    def is_available(self) -> bool:
        return self._enabled
    
    def get_latest_version(self) -> Optional[Version]:
        if bool(self._registered_versions):
            return max(self._registered_versions)
        else:
            return None
        
    def get_core(self, version: Version) -> Optional[_ProtocolStyle]:
        if str(version) in self._cores:
            return self._cores[str(version)]
        else:
            return None

    def register(self, versions: List[Version]) -> None:
        for version in versions:
            if version not in self._registered_versions:
                self._cores[str(version)] = _ProtocolStyle()
                self._registered_versions.add(version)
            else:
                # no warning and do nothing
                pass

    def deprecate(self, versions: List[Version]) -> None:
        for version in versions:
            if version in self._registered_versions:
                self._deprecated_versions.add(version)

    def unregister(self, versions: List[Version]) -> None:
        for version in versions:
            if version in self._registered_versions:
                del self._cores[str(version)]
                self._registered_versions.discard(version)
                # gc.collect()

    def select(self, version: Optional[Version] = None) -> None:
        if not version:
            version = self.get_latest_version()
            if version:
                self._target_version = version
            else:
                raise IndexError("No registered style!") 
        elif version not in self._registered_versions:
            raise KeyError(f"Style {self.name} - {str(version)} has not registered yet!")
        elif version in self._deprecated_versions:
            raise KeyError(f"Style {self.name} - {str(version)} has been deprecated!")
        else:
            self._target_version = version

    def update_protocol_features(self, protocol_type: int, features: Any) -> None:
        if not self._target_version:
            raise IndexError("Call select() before update!")
        elif protocol_type not in ProtocolType.all():
            raise TypeError(f"Parameter {protocol_type} does not belong to protocol type")
        else:
            self._cores[str(self._target_version)].set_protocol_features(protocol_type, features)
            

    def get_protocol_features(self, protocol_type: int) -> Any:
        if not self._target_version:
            raise IndexError("Call select() before get!")
        elif protocol_type not in ProtocolType.all():
            raise TypeError(f"Parameter {protocol_type} does not belong to protocol type")
        else:
            return self._cores[str(self._target_version)].get_protocol_features(protocol_type)


class ProtocolStyleManagement:
    def __init__(self):
        self._registered_styles = {}
        self.register_all()

    # Temporarily hard coding, change to configuration mode after the program is complete
    def register_all(self):
        '''
        YMODEM Header Information and Features
        _____________________________________________________________
        | Program   | Length | Date | Mode | S/N | 1k-Blk | YMODEM-g |
        |___________|________|______|______|_____|________|__________|
        |Unix rz/sz | yes    | yes  | yes  | no  | yes    | sb only  |
        |___________|________|______|______|_____|________|__________|
        |VMS rb/sb  | yes    | no   | no   | no  | yes    | no       |
        |___________|________|______|______|_____|________|__________|
        |Pro-YAM    | yes    | yes  | no   | yes | yes    | yes      |
        |___________|________|______|______|_____|________|__________|
        |CP/M YAM   | no     | no   | no   | no  | yes    | no       |
        |___________|________|______|______|_____|________|__________|
        |KMD/IMP    | ?      | no   | no   | no  | yes    | no       |
        |___________|________|______|______|_____|________|__________|
        '''
        p = ProtocolStyle("Unix rz/sz")
        p.register(["1.0.0"])
        p.select()
        p.update_protocol_features(ProtocolType.XMODEM, XMODEM.USE_CHECKSUM | XMODEM.USE_CRC | XMODEM.ALLOW_1K_PACKET)
        p.update_protocol_features(ProtocolType.YMODEM, YMODEM.USE_LENGTH_FIELD | YMODEM.USE_DATE_FIELD | YMODEM.USE_MODE_FIELD | YMODEM.ALLOW_1K_PACKET)
        self._registered_styles[p.id] = p

        p = ProtocolStyle("VMS rb/sb")
        p.register(["1.0.0"])
        p.select()
        p.update_protocol_features(ProtocolType.XMODEM, XMODEM.USE_CHECKSUM | XMODEM.USE_CRC | XMODEM.ALLOW_1K_PACKET)
        p.update_protocol_features(ProtocolType.YMODEM, YMODEM.USE_LENGTH_FIELD | YMODEM.ALLOW_1K_PACKET)
        self._registered_styles[p.id] = p

        p = ProtocolStyle("Pro-YAM")
        p.register(["1.0.0"])
        p.select()
        p.update_protocol_features(ProtocolType.XMODEM, XMODEM.USE_CHECKSUM | XMODEM.USE_CRC | XMODEM.ALLOW_1K_PACKET)
        p.update_protocol_features(ProtocolType.YMODEM, YMODEM.USE_LENGTH_FIELD | YMODEM.USE_DATE_FIELD | YMODEM.USE_SN_FIELD | YMODEM.ALLOW_1K_PACKET | YMODEM.ALLOW_YMODEM_G)
        self._registered_styles[p.id] = p

        p = ProtocolStyle("CP/M YAM")
        p.register(["1.0.0"])
        p.select()
        p.update_protocol_features(ProtocolType.XMODEM, XMODEM.USE_CHECKSUM | XMODEM.USE_CRC | XMODEM.ALLOW_1K_PACKET)
        p.update_protocol_features(ProtocolType.YMODEM, YMODEM.ALLOW_1K_PACKET)
        self._registered_styles[p.id] = p

        p = ProtocolStyle("KMD/IMP")
        p.register(["1.0.0"])
        p.select()
        p.update_protocol_features(ProtocolType.XMODEM, XMODEM.USE_CHECKSUM | XMODEM.USE_CRC | XMODEM.ALLOW_1K_PACKET)
        p.update_protocol_features(ProtocolType.YMODEM, YMODEM.ALLOW_1K_PACKET)
        self._registered_styles[p.id] = p

    def get_available_styles(self) -> List[ProtocolStyle]:
        available_programs = []

        for style_id, style in self._registered_styles.items():
            if style.is_available():
                available_programs.append(style_id)

        return available_programs

    def get_available_style(self, id) -> ProtocolStyle:
        if id in self.get_available_styles():
            return self._registered_styles[id]