# SPDX-License-Identifier: MIT
# Copyright (c) 2022 MBition GmbH


import abc
from typing import Optional

from ..decodestate import DecodeState
from ..encodestate import EncodeState
from ..globals import logger


class Parameter(abc.ABC):
    def __init__(self,
                 short_name,
                 parameter_type,
                 long_name=None,
                 byte_position=None,
                 bit_position=0,
                 semantic=None,
                 description=None):
        self.short_name: str = short_name
        self.long_name: Optional[str] = long_name
        self.byte_position: Optional[int] = byte_position
        self.bit_position: int = bit_position
        self.parameter_type: str = parameter_type
        self.semantic: Optional[str] = semantic
        self.description: Optional[str] = description

    @property
    def bit_length(self) -> Optional[int]:
        return None

    @abc.abstractclassmethod
    def is_required(self):
        pass

    @abc.abstractclassmethod
    def is_optional(self):
        pass

    @abc.abstractclassmethod
    def get_coded_value(self):
        pass

    @abc.abstractclassmethod
    def get_coded_value_as_bytes(self, encode_state: EncodeState) -> bytes:
        """Get the coded value of the parameter given the encode state.
        Note that this method is called by `encode_into_pdu`.
        """
        pass

    @abc.abstractclassmethod
    def decode_from_pdu(self, decode_state: DecodeState):
        """Decode the parameter value from the coded message.

        If the paramter does have a byte position property, the coded bytes the parameter covers are extracted
        at this byte position and the function parameter `default_byte_position` is ignored.

        If the parameter does not have a byte position and a byte position is passed,
        the bytes are extracted at the byte position given by the argument `default_byte_position`.

        If the parameter does not have a byte position and the argument `default_byte_position` is None,
        this function throws a `DecodeError`.

        Parameters
        ----------
        decode_state : DecodeState
            The decoding state containing
            * the byte message to be decoded
            * the parameter values that are already decoded
            * the next byte position that is used iff the parameter does not specify a byte position

        Returns
        -------
        ParameterValuePair | List[ParameterValuePair]
            the decoded parameter value (the type is defined by the DOP)
        int
            the next byte position after the extracted parameter
        """
        pass

    def encode_into_pdu(self, encode_state: EncodeState) -> bytearray:
        """Insert the encoded value of a parameter into the coded RPC.

        If the byte position of the parameter is not defined,
        the byte code is appended to the coded RPC.

        Technical note for subclasses:
        The default implementation tries to compute the coded value
        via `self.get_coded_value_as_bytes(encoded_state)` and inserts it into
        the PDU. Thus it suffices to overwrite `get_coded_value_as_bytes()`.

        Parameters:
        ----------
        encode_state: EncodeState, i.e. a named tuple with attributes
            * coded_message: bytes, the message encoded so far
            * parameter_values: List[ParameterValuePairs]
            * triggering_coded_request: bytes

        Returns:
        -------
        bytearray
            the updated, coded message after encoding the parameter into it
        """
        byte_value = self.get_coded_value_as_bytes(encode_state)

        old_rpc = encode_state.coded_message
        new_rpc = bytearray(old_rpc)

        if self.byte_position is not None:
            byte_position = self.byte_position
        else:
            byte_position = len(old_rpc)

        min_length = byte_position + len(byte_value)
        if len(old_rpc) < min_length:
            # Make byte code longer if necessary
            new_rpc += bytearray([0] * (min_length - len(old_rpc)))
        for byte_idx_val, byte_idx_rpc in enumerate(range(byte_position, byte_position + len(byte_value))):
            # insert byte value
            assert new_rpc[byte_idx_rpc] & byte_value[byte_idx_val] == 0, "Bytes are already set!"
            new_rpc[byte_idx_rpc] |= byte_value[byte_idx_val]

        logger.debug(f"Param {self.short_name} inserts"
                     f" 0x{byte_value.hex()} at byte pos {byte_position}")
        return new_rpc

    def _as_dict(self):
        """
        Mostly for pretty printing purposes (specifically not for reconstructing the object)
        """
        d = {
            'short_name': self.short_name,
            'type': self.parameter_type,
            'semantic': self.semantic
        }
        if self.byte_position is not None:
            d['byte_position'] = self.byte_position
        if self.bit_position:
            d['bit_position'] = self.bit_position
        return d

    def __repr__(self):
        repr_str = f"Parameter(parameter_type='{self.parameter_type}', short_name='{self.short_name}'"
        if self.long_name is not None:
            repr_str += f", long_name='{self.long_name}'"
        if self.byte_position is not None:
            repr_str += f", byte_position='{self.byte_position}'"
        if self.bit_position:
            repr_str += f", bit_position='{self.bit_position}'"
        if self.semantic is not None:
            repr_str += f", semantic='{self.semantic}'"
        if self.description is not None:
            repr_str += f", description='{' '.join(self.description.split())}'"
        return repr_str + ")"

    def __str__(self):
        # create list of all parameters. 'short_name' ought to be
        # first, so it needs special treatment...
        param_descs = [f"short_name='{self.short_name}'"]
        for (key, val) in self._as_dict().items():
            if key == "short_name":
                continue
            elif isinstance(val, str):
                param_descs.append(f"{key}='{val}'")
            else:
                param_descs.append(f"{key}={val}")

        return f"Parameter({', '.join(param_descs)})"
