# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Copyright 2021 Daniel Mark Gass, see __about__.py for license information.
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
"""Bit field structure member definition."""

# pylint: disable=too-many-arguments

from dataclasses import dataclass
from enum import IntEnum, EnumMeta
from typing import Any, Dict, Optional, Tuple

from .._code_injector import CodeInjector
from .._getbytes import getbytes
from .._typing import ByteOrderHint
from ..bitfields import BitFields
from ..dump import Record
from ..enum import EnumX
from ..flag import FlagX
from ..exceptions import InsufficientMemoryError
from ..none import NoneX
from ._member import Member


@dataclass
class BitFieldsStore:

    """Temporary information store for generating implementation code."""

    is_first: bool = False
    is_last: bool = False
    group_name: str = ""
    nbytes: Optional[int] = None
    byteorder: str = "little"


class BitFieldMember(Member):

    """Bit field structure member definition.

    :param doc: accessor documentation string
    :param size: bit field width (in bits)
    :param lsb: least significant bit position
    :param typ: field type (e.g. ``IntEnum`` subclass), default uses type annotation
    :param default: initializer default value
    :param ignore: ignore member during comparisons
    :param readonly: block setting member attribute
    :param compute: initializer defaults to compute based on another member
    :param nbytes: bytes occupied by this bit field and ones that follow (0 => calculate)
    :param argrepr: format to represent member argument in structure repr

    """

    aux_store: BitFieldsStore

    _group_name: str
    _typ: Any

    _attributes_to_copy: Tuple[str, ...] = Member._attributes_to_copy + (
        "_typ",
        "_size",
        "_lsb",
        "_signed",
        "_group_name",
    )

    def __init__(
        self,
        doc: str,
        size: int,
        lsb: Optional[int],
        typ: type,
        signed: bool,
        default: Any,
        ignore: bool,
        readonly: bool,
        compute: bool,
        nbytes: int,
        argrepr: Optional[str],
    ) -> None:
        if lsb is not None:
            lsb = int(lsb)
            if lsb < 0:
                raise TypeError(
                    "bit field position must be greater than or equal to zero"
                )

        if signed and size < 2:
            raise ValueError("'size' must be 2 or greater for signed bit field")

        if size < 1:
            raise ValueError("'size' must be 1 or greater for unsigned bit field")

        super().__init__(
            doc,
            default,
            ignore,
            readonly,
            compute,
            argrepr,
            fmt=NoneX("none"),
            fmt_arg=None,
        )

        self._set_typ(typ)
        self._size = size
        self._lsb = lsb
        self._nbytes = nbytes
        self._signed = signed
        self._group_name = ""

    @property
    def lsb(self) -> int:
        """Bit field bit position."""
        return -1 if self._lsb is None else self._lsb

    @property
    def nbytes(self) -> int:
        """Number of bytes allocated for this and following bit fields.

        Return zero for bit fields included in previous bit field allocation.

        """
        if self._nbytes is None:  # pragma: no cover
            raise RuntimeError("internal error")
        return self._nbytes

    @property
    def signed(self) -> bool:
        """Bit field is a signed integer."""
        return self._signed

    @property
    def size(self) -> int:
        """Bit field width (in bits)."""
        return self._size

    @property
    def typ(self) -> type:
        """Field type (e.g. ``IntEnum`` subclass), default uses type annotation."""
        if self._typ is None:  # pragma: no cover
            raise RuntimeError("internal error")

        return self._typ

    def _set_typ(self, typ: type):
        try:
            is_int_subclass = issubclass(typ, (int, BitFields))

        except TypeError:
            is_int_subclass = False  # must not be class

        if isinstance(typ, (FlagX, EnumX)) or is_int_subclass:
            pass

        else:
            raise TypeError("bit field type must be int-like")

        self._typ = typ

    @property
    def argrepr(self) -> str:
        """Format string to represent member argument in structure repr."""
        if isinstance(self._argrepr, str):
            repr_format = self._argrepr

        elif isinstance(self._typ, EnumMeta):
            repr_format = f"{self.name}={{str(self.{self.name})}}"

        else:
            repr_format = f"{self.name}={{self.{self.name}!r}}"

        return repr_format

    @classmethod
    def organize(cls, members: Dict[str, Member], byteorder: str, fieldorder: str):
        """Organize bit field members into groups."""
        collection = []
        group = {}

        for name, member in members.items():
            if isinstance(member, BitFieldMember) and not member.nbytes:
                group[name] = member
                continue

            if group:
                collection.append(group)
                group = {}

            if isinstance(member, BitFieldMember):
                group[name] = member

        if group:
            collection.append(group)

        for i, group in enumerate(collection):
            nbytes = cls.position_and_size(f"fields_{i}", group, fieldorder)
            for member in group.values():
                member.aux_store = BitFieldsStore(
                    byteorder=byteorder, nbytes=nbytes, group_name=f"bitfields_{i}"
                )
            list(group.values())[0].aux_store.is_first = True
            list(group.values())[-1].aux_store.is_last = True

    @staticmethod
    def position_and_size(
        group_name: str, members: Dict[str, "BitFieldMember"], fieldorder: str
    ) -> int:
        """Fill in any unassigned positions, determine overall size."""
        # fill in missing positions (and assign group name)
        lsb = 0
        field_member_properties = (
            members.values()
            if fieldorder == "least_to_most"
            else reversed(list(members.values()))
        )
        for field_member in field_member_properties:
            # pylint: disable=protected-access
            field_member._group_name = group_name
            if field_member._lsb is None:
                field_member._lsb = lsb
            lsb = field_member.lsb + field_member.size

        # check for overlap
        claimed_bits: Dict[int, str] = {}
        for name, member in members.items():
            for i in range(member.lsb, member.lsb + member.size):
                if i in claimed_bits:
                    raise TypeError(
                        f"bit field {name!r} overlaps with {claimed_bits[i]!r}"
                    )

                claimed_bits[i] = name

        # default/validate 'nbytes'
        numbits = max(member.lsb + member.size for member in members.values())
        nbytes_needed = (numbits + 7) // 8

        first_bitfield_name, first_bitfield = list(members.items())[0]
        nbytes = first_bitfield.nbytes

        if not nbytes:
            nbytes = first_bitfield._nbytes = nbytes_needed

        elif nbytes_needed > nbytes:
            raise TypeError(
                f"'nbytes' must be at least {nbytes_needed} for bitfield "
                f"{first_bitfield_name!r} (to have sufficient room for it "
                f"and those that follow)"
            )

        return nbytes

    @staticmethod
    def __unpack__(
        buffer: bytes,
        offset: int,
        dump: Optional[Record],
        byteorder: ByteOrderHint,
        nbytes: int,
        signed: bool,
    ) -> Tuple[int, int]:
        if dump is None:
            end = offset + nbytes

            if len(buffer) < end:
                raise InsufficientMemoryError(
                    "too few bytes to unpack, retry with dump to follow"
                )

            value = int.from_bytes(buffer[offset:end], byteorder, signed=signed)

            return value, end

        chunk, offset = getbytes(buffer, offset, dump, nbytes)

        value = int.from_bytes(chunk, byteorder, signed=signed)

        dump.value = value

        return value, offset

    def add_name_index(
        self,
        name: str,
        index: int,
        code_injector: CodeInjector,
    ) -> int:
        """Assign name, index number, and prepare for code generation."""
        # pylint: disable=too-many-statements, too-many-branches, too-many-locals
        index = super().add_name_index(name, index, code_injector)

        store = self.temp_store
        aux = self.aux_store

        # Erase normal member packing lines (from super)
        store.unpack = []
        store.unpack_and_dump = []
        store.pack = []
        store.pack_and_dump = []

        lo_mask = (1 << self.size) - 1
        sign_bit = 1 << (self.size - 1)

        cls_name = self.typ.__name__
        try:
            is_same = code_injector.evaluate(cls_name) is self.typ
        except NameError:  # pragma: no cover
            is_same = False
        cls_name = cls_name if is_same else f"cls.{self.name}.typ"

        dump_name = f"{aux.group_name}_dump"
        dump_group = [f"{dump_name} = dump.add_record()"]

        if issubclass(self.typ, BitFields):
            dump_member = [
                # line 1
                f"dump_m_{self.name} = dump.add_record("
                f'access="{name}", value=int(m_{self.name}), fmt={cls_name})',
                # line 2
                f"{cls_name}.__add_bitfields_to_dump__("
                f"m_{self.name}, dump_m_{self.name}, bitoffset={self.lsb})",
            ]
        else:
            dump_member = [
                f"dump.add_record("
                f'access="{name}", bits={self.lsb, self.size}, '
                f"value=m_{self.name}, fmt={cls_name})"
            ]

        if aux.is_first:
            store.unpack += [
                f"{aux.group_name}, offset = "
                f"cls.{name}.__unpack__(buffer, offset, dump, "
                f'"{aux.byteorder}", nbytes={aux.nbytes}, signed={self.signed})',
                "",
            ]

            store.unpack_and_dump += dump_group + [
                f"{aux.group_name}, offset = "
                f"cls.{name}.__unpack__(buffer, offset, {dump_name}, "
                f'"{aux.byteorder}", nbytes={aux.nbytes}, signed={self.signed})',
                "",
            ]

        lines = [f"m_{name} = ({aux.group_name} >> {self.lsb}) & 0x{lo_mask:x}"]

        if self.signed:
            lines += [
                f"if m_{name} & 0x{sign_bit:x}:",
                f"    m_{name} -= 0x{1 << self.size:x}",
            ]

        if self.typ is not int:
            if issubclass(self.typ, BitFields):
                lines.append(f"m_{name} = {cls_name}.from_int(m_{name})")
            else:
                lines.append(f"m_{name} = {cls_name}(m_{name})")

        store.unpack += lines
        store.unpack_and_dump += lines + dump_member

        if aux.is_first:
            lines = [f"{aux.group_name} = 0", ""]
            store.pack += lines
            store.pack_and_dump += dump_group + lines

        assert self.lsb is not None, "internal error"
        hi_mask = ~(lo_mask << self.lsb)
        member_name = f"m_{self.name}"
        if self.typ is bool:
            # don't let an integer > 1 be masked off and become False
            member_name = f"int(bool({member_name}))"
        op2 = f"({member_name} & 0x{lo_mask:x})"
        if self.lsb:
            op2 = f"({op2} << {self.lsb})"
        lines = [f"{aux.group_name} |= ({aux.group_name} & {hi_mask}) | {op2}"]
        store.pack += lines
        store.pack_and_dump += dump_member + lines

        if aux.is_last:
            store.pack += [
                "",
                f"pieces.append({aux.group_name}.to_bytes("
                f'{aux.nbytes}, "{aux.byteorder}", signed={self.signed}))',
            ]
            store.pack_and_dump += [
                "",
                f"pieces.append({aux.group_name}.to_bytes("
                f'{aux.nbytes}, "{aux.byteorder}", signed={self.signed}))',
                f"{dump_name}.value = str({aux.group_name})",
                f"{dump_name}.memory = pieces[-1]",
            ]

        if self.signed:
            neg_limit = -(1 << (self.size - 1))
            maxvalue = f"{-(1 + neg_limit)}"
            minvalue = f"-{-neg_limit}"
        else:
            minvalue = "0"
            maxvalue = f"{(1 << self.size) - 1}"

        if self.fset is None and not self.readonly:
            # override lines put in by super()
            if issubclass(self.typ, IntEnum):
                store.setter = (
                    []
                )  # no need for range check, enum instantiation checks validity
            else:
                store.setter = [
                    f"if not {minvalue} <= value <= {maxvalue}:",
                    f'   raise ValueError("out of range, {minvalue} <= value <= {maxvalue}")',
                ]
            if self.typ is int:
                store.setter.append(f"self[{self.index}] = value")
            else:
                store.setter.append(f"self[{self.index}] = {cls_name}(value)")

        if not self.default_is_a_factory:
            store.init_begin += [
                f"if not {minvalue} <= {self.name} <= {maxvalue}:",
                f'   raise ValueError("{self.name!r} out of range, {minvalue} <= {self.name} <= {maxvalue}")',
            ]

        return index


def bitfield_member(
    doc: str = "",
    *,
    size: int,
    lsb: Optional[int] = None,
    typ: type = int,
    signed: bool = False,
    default: Any = None,
    ignore: bool = False,
    readonly: bool = False,
    compute: bool = False,
    nbytes: int = 0,
    argrepr: Optional[str] = None,
) -> Any:
    """Bit field structure member definition.

    :param doc: accessor documentation string
    :param size: bit field width (in bits)
    :param lsb: least significant bit position
    :param signed: interpret as signed integer
    :param typ: field type (e.g. ``IntEnum`` subclass)
    :param default: initializer default value
    :param ignore: ignore member during comparisons
    :param readonly: block setting member attribute
    :param compute: initializer defaults to compute based on another member
    :param nbytes: bytes occupied by this bit field and ones that follow (0 => calculate)
    :param argrepr: format to represent member argument in structure repr

    """
    return BitFieldMember(
        doc, size, lsb, typ, signed, default, ignore, readonly, compute, nbytes, argrepr
    )
