#!/usr/pkg/bin/python3.13
# coding=utf-8
#
# Copyright (c) 2024 jonathan.neuhauser@outlook.com
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
"""
Parse binary CGM files
"""

from dataclasses import dataclass, fields, is_dataclass
from io import BufferedIOBase, BytesIO
import struct
from typing import (
    ClassVar,
    Dict,
    List,
    Optional,
    Tuple,
    Type,
    Union,
    get_type_hints,
    get_origin,
    get_args,
)
import warnings
import cgm_enums
import inkex


class FunctionRegistry(dict):
    def register(self, key):
        def decorator(func):
            self[key] = func
            return func

        return decorator


class BinaryCGMCommandParser:
    type_registry: Dict[Tuple[int, int], Type] = {}
    unsigned_readers = FunctionRegistry()
    signed_readers = FunctionRegistry()
    real_readers = FunctionRegistry()
    readers = FunctionRegistry()
    sizers = FunctionRegistry()

    def __init__(self, data: Union[bytes, BufferedIOBase]):
        if isinstance(data, bytes):
            data = BytesIO(data)
        # The stream we currently operate on. For long form commands its
        # easier to temporarily exchange this reference
        self.stream = data
        # The main stream of the file
        self.outer_stream = data
        # Reader functions for basic data types, where precision can be changed
        # in the file.
        self.integer_precision = 16
        self.index_precision = 16
        self.real_type = cgm_enums.RealTypeEnum.FIXED_POINT
        self.real_precision = (16, 16)
        self.vdc_integer_precision = 16
        self.vdc_real_type = cgm_enums.RealTypeEnum.FIXED_POINT
        self.vdc_real_precision = (16, 16)
        self.float_precision = (9, 23)
        self.colour_component_precision = 8
        self.colour_index_precision = 8
        self.vdc_type = cgm_enums.VDCTypeEnum.INTEGER
        self.vdc_real_type = cgm_enums.RealTypeEnum.FIXED_POINT
        self.vdc_real_precision = (16, 16)
        self.colour_model = cgm_enums.ColourModelEnum.RGB
        self.colour_selection_mode = cgm_enums.ColourSelectionModeEnum.INDEXED
        self.line_width_specification_mode = cgm_enums.WidthSpecificationModeEnum.SCALED
        self.marker_size_specification_mode = (
            cgm_enums.WidthSpecificationModeEnum.SCALED
        )
        self.edge_width_specification_mode = cgm_enums.WidthSpecificationModeEnum.SCALED
        self.interior_style_specification_mode = (
            cgm_enums.WidthSpecificationModeEnum.ABSOLUTE
        )

    @staticmethod
    def register_target_datatype(cls):
        if cls.id is not None:
            BinaryCGMCommandParser.type_registry[cls.id] = cls

    def read(self, bytes):
        res = self.stream.read(bytes)
        if len(res) != bytes:
            raise ValueError("EOF")
        return res

    def read_padded(self, length):
        # An uneven length of a command is padded with a null byte
        if length % 2 == 1:
            arg_bytes = self.read(length + 1)
            assert arg_bytes[-1] == 0
            return arg_bytes[:-1]
        return self.read(length)

    @unsigned_readers.register(8)
    def read_u8(self) -> int:
        return int.from_bytes(self.read(1), byteorder="big")

    @unsigned_readers.register(16)
    def read_u16(self) -> int:
        return int.from_bytes(self.read(2), byteorder="big")

    @unsigned_readers.register(24)
    def read_u24(self) -> int:
        return int.from_bytes(self.read(3), byteorder="big")

    @unsigned_readers.register(32)
    def read_u32(self) -> int:
        return int.from_bytes(self.read(4), byteorder="big")

    @signed_readers.register(8)
    def read_i8(self) -> int:
        return int.from_bytes(self.read(1), byteorder="big", signed=True)

    @signed_readers.register(16)
    @readers.register("E")
    def read_i16(self) -> int:
        return int.from_bytes(self.read(2), byteorder="big", signed=True)

    @signed_readers.register(24)
    def read_i24(self) -> int:
        return int.from_bytes(self.read(3), byteorder="big", signed=True)

    @signed_readers.register(32)
    def read_i32(self) -> int:
        return int.from_bytes(self.read(4), byteorder="big", signed=True)

    @real_readers.register((cgm_enums.RealTypeEnum.FLOATING_POINT, 9, 23))
    def read_float(self) -> float:
        return struct.unpack(">f", self.read(4))[0]

    @real_readers.register((cgm_enums.RealTypeEnum.FLOATING_POINT, 12, 52))
    def read_double(self) -> float:
        return struct.unpack(">d", self.read(8))[0]

    @real_readers.register((cgm_enums.RealTypeEnum.FIXED_POINT, 16, 16))
    def read_fixed_single(self) -> float:
        # Some sample files appear to not be using
        # two's complement for signed integers if they occur in a flot.
        # IMO that's incorrect according to the specification.
        return self.read_i16() + self.read_u16() / 2**16

    @real_readers.register((cgm_enums.RealTypeEnum.FIXED_POINT, 32, 32))
    def read_fixed_double(self) -> float:
        return self.read_i32() + self.read_u32() / 2**32

    @readers.register("S")
    @readers.register("D")
    @readers.register("SF")
    def read_str(self) -> str:
        """The first byte indicates the size.
        If the size is 255, the first two bytes indicate the size.
        From the new size, the first bit is a continuation flag
        and the remaining 15 bits are the size"""
        data: bytes
        size = self.read_u8()
        if size != 0xFF:
            data = self.read(size)
        else:
            # Long-form string
            self.stream.seek(0)
            _data = bytearray()
            while True:
                cur_size = self.read_u16()
                length = cur_size & 0b0111111111111111
                _data.extend(self.read(length))
                if not cur_size & 0b1000000000000000:
                    break
            data = bytes(_data)
        # TODO implement proper string encoding
        return data.decode("latin-1")

    def get_single_value(self, type_str: Union[str, Tuple[str]]):
        if type_str in self.readers:
            return BinaryCGMCommandParser.readers[type_str](self)
        else:
            raise ValueError(f"Unknown data type {type_str}")

    def get_value(
        self,
        type_str: Union[str, Tuple[str, ...]],
        target_types: Union[type, Tuple[type]],
        length: Optional[int] = None,
    ):
        """This function reads a value from the stream.
        The result can either be:
         - a primitive type (e.g. "I", <type int> -> int)
         - a tuple of primitive types
           (e.g. ("I", "P"), (<type int>, <type Point>) -> (int, Point))
         - a dataclass
           (e.g. ("P", "E"), <type PolygonLineSetEntry> -> PolygonLineSetEntry)
           in this case the type_str applies to the fields of the dataclass,
           and the target types of those fields are inferred from the
           type hint of the dataclass field
         - a list of any of the previous
           (e.g. ("I", "R"), typing.List[<type int>, <type float>]
           -> List[Tuple[int, float]])
           length must be defined for lists
           This just calls get_value with the argument of the List typehint,
           and collects the result until we've read length bytes
        """
        res = None
        initial_pos = self.stream.tell()

        if isinstance(type_str, str) and isinstance(target_types, type):
            if target_types == str and length == 0:
                return ""
            res = self.get_single_value(type_str)
            if type_str not in ("P", "CO", "CD", "CI"):
                res = target_types(res)

        elif isinstance(type_str, tuple):
            if get_origin(target_types) == tuple:
                target_types = get_args(target_types)
            if isinstance(target_types, tuple):
                assert len(target_types) == len(type_str)
                res = tuple(
                    self.get_value(i, t) for t, i in zip(target_types, type_str)
                )

            elif is_dataclass(target_types):
                res = target_types(
                    *self.get_value(
                        type_str,
                        tuple(
                            get_type_hints(target_types)[t.name]
                            for t in fields(target_types)
                        ),
                    )
                )
        if res is None and get_origin(target_types) == list:
            assert length is not None
            target_type = get_args(target_types)[0]
            try:
                field_size = self.get_size(type_str)
                # Assert that the remaining bytes fit into the list without residue
                assert length % field_size == 0
            except ValueError:
                # just an assertion here, continue
                pass
            res = []
            while True:
                if (self.stream.tell() - initial_pos) >= length:
                    break
                res.append(self.get_value(type_str, target_type))

        assert res is not None, "invalid specification for get_value"
        # assert isinstance(res, target_types), "bad return type"
        return res

    @readers.register("IX")
    def read_index(self):
        return BinaryCGMCommandParser.signed_readers[self.index_precision](self)

    @readers.register("SI")
    @readers.register("I")
    def read_signed(self):
        return BinaryCGMCommandParser.signed_readers[self.integer_precision](self)

    @readers.register("VDC")
    def read_vdc(self):
        return (
            BinaryCGMCommandParser.signed_readers[self.vdc_integer_precision](self)
            if self.vdc_type == cgm_enums.VDCTypeEnum.INTEGER
            else BinaryCGMCommandParser.real_readers[
                (self.vdc_real_type, *self.vdc_real_precision)
            ](self)
        )

    @readers.register("R")
    def read_real(self):
        return BinaryCGMCommandParser.real_readers[
            (self.real_type, *self.real_precision)
        ](self)

    @readers.register("F")
    def read_floating(self):
        return BinaryCGMCommandParser.real_readers[
            (cgm_enums.RealTypeEnum.FLOATING_POINT, *self.float_precision)
        ](self)

    @readers.register("CI")
    def read_indexed_colour(self):
        return cgm_enums.IndexColour(
            self.signed_readers[self.colour_index_precision](self)
        )

    @readers.register("CCO")
    def read_colour_component(self):
        return BinaryCGMCommandParser.unsigned_readers[self.colour_component_precision](
            self
        )

    @readers.register("CD")
    def read_direct_colour(self):
        result = cgm_enums.DirectColour(
            *(
                self.read_colour_component()
                for _ in range(
                    4 if self.colour_model == cgm_enums.ColourModelEnum.CMYK else 3
                )
            )
        )
        result.colour_model = self.colour_model
        return result

    @readers.register("CO")
    def read_colour(self):
        if self.colour_selection_mode == cgm_enums.ColourSelectionModeEnum.DIRECT:
            return self.read_direct_colour()
        else:
            return self.read_indexed_colour()

    @readers.register("P")
    def read_point(self):
        return cgm_enums.Point(self.read_vdc(), self.read_vdc())

    def read_size(self, mode: cgm_enums.WidthSpecificationModeEnum):
        if mode == cgm_enums.WidthSpecificationModeEnum.ABSOLUTE:
            return self.read_vdc()
        return self.read_real()

    @readers.register("LSS")
    def read_line_size(self):
        return self.read_size(self.line_width_specification_mode)

    @readers.register("MSS")
    def read_marker_size(self):
        return self.read_size(self.marker_size_specification_mode)

    @readers.register("ESS")
    def read_edge_size(self):
        return self.read_size(self.edge_width_specification_mode)

    @readers.register("ISS")
    def read_interior_style_size(self):
        return self.read_size(self.interior_style_specification_mode)

    # SIZING FUNCTIONS

    def get_size(self, type_str: Union[str, Tuple[str, ...]]):
        if type_str in BinaryCGMCommandParser.sizers:
            return BinaryCGMCommandParser.sizers[type_str](self)
        if isinstance(type_str, tuple):
            return sum(self.get_size(i) for i in type_str)
        else:
            raise ValueError(f"No size know for type {type_str}")

    @sizers.register("CI")
    def ci_size(self):
        return self.colour_index_precision // 8

    @sizers.register("CCO")
    def cco_size(self):
        return self.colour_component_precision // 8

    @sizers.register("CD")
    def cd_size(self):
        return self.cco_size() * (
            4 if self.colour_model == cgm_enums.ColourModelEnum.CMYK else 3
        )

    @sizers.register("IX")
    def ix_size(self):
        return self.index_precision // 8

    @sizers.register("E")
    def e_size(self):
        return 2

    @sizers.register("I")
    def i_size(self):
        return self.integer_precision // 8

    @sizers.register("R")
    def r_size(self):
        return (self.real_precision[0] + self.real_precision[1]) // 8

    @sizers.register("VDC")
    def vdc_size(self):
        return (
            self.vdc_integer_precision // 8
            if self.vdc_type == cgm_enums.VDCTypeEnum.INTEGER
            else (self.vdc_real_precision[0] + self.vdc_real_precision[1]) // 8
        )

    @sizers.register("P")
    def p_size(self):
        return 2 * self.vdc_size()

    @sizers.register("CO")
    def co_size(self):
        if self.colour_selection_mode == cgm_enums.ColourSelectionModeEnum.DIRECT:
            return self.cd_size()
        else:
            return self.ci_size()

    def ss_size(self, mode: cgm_enums.WidthSpecificationModeEnum):
        if mode == cgm_enums.WidthSpecificationModeEnum.ABSOLUTE:
            return self.vdc_size()
        return self.r_size()

    @sizers.register("LSS")
    def lss_size(self):
        return self.ss_size(self.line_width_specification_mode)

    @sizers.register("MSS")
    def marker_size(self):
        return self.ss_size(self.marker_size_specification_mode)

    @sizers.register("ESS")
    def edge_size(self):
        return self.ss_size(self.edge_width_specification_mode)

    @sizers.register("ISS")
    def interior_style_size(self):
        return self.ss_size(self.interior_style_specification_mode)

    def parse(self):
        while True:
            try:
                header = self.read_u16()
            except ValueError:
                return
            clss = header >> 12
            obj_id = (header & 0b0000111111100000) >> 5
            total_length = header & 0x1F

            if total_length == 31:
                # Long-form header
                args = bytearray()
                total_length = 0
                while True:
                    data = self.read_u16()
                    length = data & 0b0111111111111111
                    total_length += length
                    args.extend(self.read_padded(length))
                    if not data & 0b1000000000000000:
                        break
                self.stream = BytesIO(bytes(args))

            old_pos = self.stream.tell()
            interpreted = self.command_factory(clss, obj_id, total_length)
            new_pos = self.stream.tell()
            if (
                new_pos - old_pos not in [total_length] + [total_length - 1]
                if total_length % 2 == 0
                else []
            ):
                warnings.warn(
                    f"{interpreted.__class__.__name__}: Expected to read {total_length} bytes, but read {new_pos - old_pos}"
                )
            new_pos = old_pos + total_length
            self.stream.seek(new_pos)
            if isinstance(interpreted, ParserModifierCommand):
                self.update_readers(interpreted)

            # Prepare the stream for the next entry
            if self.stream == self.outer_stream:
                if new_pos % 2 != 0:
                    # Fields are always aligned with 2-byte words
                    self.read(1)
            else:
                self.stream = self.outer_stream

            yield interpreted

            if isinstance(interpreted, EndMetafile):
                # Sometimes there are trailing characters afterwards, return
                return

    def parse_data(self, data: BytesIO):
        """Temporarily exchanges the stream by data to read primitives from it,
        using the same settings as the current stream"""
        outer_stream = self.outer_stream
        stream = self.stream

        self.outer_stream = data
        self.stream = data
        result = tuple(self.parse())

        self.outer_stream = outer_stream
        self.stream = stream

        return result

    def update_readers(self, interpreted):
        if isinstance(interpreted, VdcType):
            self.vdc_type = interpreted.vdc_type
        elif isinstance(interpreted, IntegerPrecision):
            self.integer_precision = interpreted.integer_precision
        elif isinstance(interpreted, RealPrecision):
            self.real_type = interpreted.representation
            self.real_precision = (
                interpreted.exponent_whole_size,
                interpreted.fraction_size,
            )
            if interpreted.representation == cgm_enums.RealTypeEnum.FLOATING_POINT:
                self.float_precision = (
                    interpreted.exponent_whole_size,
                    interpreted.fraction_size,
                )
        elif isinstance(interpreted, IndexPrecision):
            self.index_precision = interpreted.index_precision
        elif isinstance(interpreted, ColourIndexPrecision):
            self.colour_index_precision = interpreted.colour_index_precision
        elif isinstance(interpreted, ColourPrecision):
            self.colour_component_precision = interpreted.colour_precision
        elif isinstance(interpreted, ColourSelectionMode):
            self.colour_selection_mode = interpreted.colour_selection_mode
        elif isinstance(interpreted, ColourModel):
            self.colour_model = interpreted.colour_model
        elif isinstance(interpreted, VDCIntegerPrecision):
            self.vdc_integer_precision = interpreted.integer_precision
        elif isinstance(interpreted, VDCRealPrecision):
            self.vdc_real_type = interpreted.representation
            self.vdc_real_precision = (
                interpreted.exponent_whole_size,
                interpreted.fraction_size,
            )
        elif isinstance(interpreted, LineWidthSpecificationMode):
            self.line_width_specification_mode = interpreted.size_specification
        elif isinstance(interpreted, EdgeWidthSpecificationMode):
            self.edge_width_specification_mode = interpreted.size_specification
        elif isinstance(interpreted, MarkerSizeSpecificationMode):
            self.marker_size_specification_mode = interpreted.size_specification
        elif isinstance(interpreted, InteriorStyleSpecificationMode):
            self.interior_style_specification_mode = interpreted.size_specification
        else:
            inkex.errormsg(
                f"Parser modifier command {interpreted} without associated action"
            )

    def command_factory(self, group, command, length):
        cls = self.type_registry.get((group, command), UnknownCommand)
        result = cls.parse_binary(self, length)
        if isinstance(result, UnknownCommand):
            result.id = (group, command)
        return result


@dataclass
class CGMCommand:
    id: ClassVar[Tuple[int, int]] = (0, 0)
    binary_params: ClassVar[Tuple[Union[str, Tuple[str, ...]], ...]] = tuple()

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        BinaryCGMCommandParser.register_target_datatype(cls)

    @classmethod
    def get_binary_args(
        cls,
        stream: BinaryCGMCommandParser,
        length: int,
    ):
        initial_pos = stream.stream.tell()
        for i, (field, types) in enumerate(zip(fields(cls), cls.binary_params)):
            outer_type = get_type_hints(cls)[field.name]
            yield stream.get_value(
                types, outer_type, length - (stream.stream.tell() - initial_pos)
            )

    @classmethod
    def parse_binary(cls, stream: BinaryCGMCommandParser, length):
        args = list(cls.get_binary_args(stream, length))
        return cls(*args)


6


@dataclass
class NoOp(CGMCommand):
    id = (0, 0)
    data: bytes

    @classmethod
    def parse_binary(cls, stream: BinaryCGMCommandParser, length):
        return cls(stream.read(length))


@dataclass
class UnknownCommand(CGMCommand):
    data: bytes

    @classmethod
    def parse_binary(cls, stream: BinaryCGMCommandParser, length):
        return cls(stream.read(length))


@dataclass
class BeginMetafile(CGMCommand):
    id = (0, 1)
    binary_params = ("SF",)
    name: str


@dataclass
class EndMetafile(CGMCommand):
    id = (0, 2)


@dataclass
class BeginPicture(CGMCommand):
    id = (0, 3)
    binary_params = ("SF",)
    name: str


@dataclass
class BeginPictureBody(CGMCommand):
    id = (0, 4)


@dataclass
class EndPicture(CGMCommand):
    id = (0, 5)


# Metafile descriptor elements


@dataclass
class MetafileVersion(CGMCommand):
    id = (1, 1)
    binary_params = ("I",)
    version: int


@dataclass
class MetafileDescription(CGMCommand):
    id = (1, 2)
    binary_params = ("SF",)
    description: str


class ParserModifierCommand(CGMCommand):
    pass


@dataclass
class VdcType(ParserModifierCommand):
    id = (1, 3)
    binary_params = ("E",)
    vdc_type: cgm_enums.VDCTypeEnum


@dataclass
class IntegerPrecision(ParserModifierCommand):
    id = (1, 4)
    binary_params = ("I",)
    integer_precision: int


@dataclass
class RealPrecision(ParserModifierCommand):
    id = (1, 5)
    binary_params = "E", "I", "I"
    representation: cgm_enums.RealTypeEnum
    exponent_whole_size: int
    fraction_size: int


@dataclass
class IndexPrecision(ParserModifierCommand):
    id = (1, 6)
    binary_params = ("I",)
    index_precision: int


@dataclass
class ColourPrecision(ParserModifierCommand):
    id = (1, 7)
    binary_params = ("I",)
    colour_precision: int


@dataclass
class ColourIndexPrecision(ParserModifierCommand):
    id = (1, 8)
    binary_params = ("I",)
    colour_index_precision: int


@dataclass
class MaximumColourIndex(CGMCommand):
    id = (1, 9)
    binary_params = ("CI",)
    maximum_colour_index: cgm_enums.IndexColour


@dataclass
class ColourValueExtent(CGMCommand):
    id = (1, 10)

    minimum: Optional[int]
    maximum: Optional[int]

    first_component: Optional[Tuple[float, float]]
    second_component: Optional[Tuple[float, float]]
    third_component: Optional[Tuple[float, float]]

    @classmethod
    def get_binary_args(cls, stream: BinaryCGMCommandParser, length):
        if stream.colour_model in (
            cgm_enums.ColourModelEnum.CMYK,
            cgm_enums.ColourModelEnum.RGB,
        ):
            return stream.read_direct_colour(), stream.read_direct_colour(), 0, 0, 0
        else:
            return 0, 0, stream.read_real(), stream.read_real(), stream.read_real()


@dataclass
class MetafileElementList(CGMCommand):
    id = (1, 11)
    elements: List[Tuple[int, int]]

    @classmethod
    def get_binary_args(cls, stream: BinaryCGMCommandParser, length: int):
        num = stream.read_signed()
        yield [(stream.read_index(), stream.read_index()) for _ in range(num)]


@dataclass
class MetafileDefaultsReplacement(CGMCommand):
    id = (1, 12)
    data: List[CGMCommand]

    @classmethod
    def get_binary_args(cls, stream: BinaryCGMCommandParser, length: int):
        data = stream.read(length)

        yield stream.parse_data(BytesIO(data))


@dataclass
class FontList(CGMCommand):
    id = (1, 13)
    binary_params = ("SF",)

    fonts: List[str]


@dataclass
class CharacterSetList(CGMCommand):
    id = (1, 14)
    binary_params = (("E", "SF"),)

    specification: List[Tuple[cgm_enums.CharacterSetTypeEnum, str]]


@dataclass
class CharacterCodingAnnouncer(CGMCommand):
    id = (1, 15)
    binary_params = ("E",)

    encoding: cgm_enums.CharacterCodingEnum


@dataclass
class MaximumVDCExtent(CGMCommand):
    id = (1, 17)
    binary_params = "P", "P"

    first_corner: cgm_enums.Point
    second_corner: cgm_enums.Point


@dataclass
class ColourModel(ParserModifierCommand):
    id = (1, 19)
    binary_params = ("IX",)

    colour_model: cgm_enums.ColourModelEnum


@dataclass
class ScalingMode(CGMCommand):
    id = (2, 1)
    binary_params = "E", "F"

    scaling_mode: cgm_enums.ScalingModeEnum
    metric_scaling_factor: float


@dataclass
class ColourSelectionMode(ParserModifierCommand):
    id = (2, 2)
    binary_params = ("E",)

    colour_selection_mode: cgm_enums.ColourSelectionModeEnum


@dataclass
class LineWidthSpecificationMode(ParserModifierCommand):
    id = (2, 3)
    binary_params = ("E",)

    size_specification: cgm_enums.WidthSpecificationModeEnum


@dataclass
class MarkerSizeSpecificationMode(ParserModifierCommand):
    id = (2, 4)

    binary_params = ("E",)
    size_specification: cgm_enums.WidthSpecificationModeEnum


@dataclass
class EdgeWidthSpecificationMode(ParserModifierCommand):
    id = (2, 5)

    binary_params = ("E",)
    size_specification: cgm_enums.WidthSpecificationModeEnum


@dataclass
class VDCExtent(CGMCommand):
    id = (2, 6)
    binary_params = "P", "P"

    first_corner: cgm_enums.Point
    second_corner: cgm_enums.Point


@dataclass
class BackgroundColour(CGMCommand):
    id = (2, 7)
    binary_params = ("CD",)

    colour: cgm_enums.DirectColour


@dataclass
class InteriorStyleSpecificationMode(ParserModifierCommand):
    id = (2, 16)

    binary_params = ("E",)
    size_specification: cgm_enums.WidthSpecificationModeEnum


@dataclass
class VDCIntegerPrecision(ParserModifierCommand):
    id = (3, 1)
    binary_params = ("I",)
    integer_precision: int


@dataclass
class VDCRealPrecision(ParserModifierCommand):
    id = (3, 2)
    binary_params = "E", "I", "I"
    representation: cgm_enums.RealTypeEnum
    exponent_whole_size: int
    fraction_size: int


@dataclass
class Transparency(CGMCommand):
    id = (3, 4)
    binary_params = ("E",)
    indicator: cgm_enums.TransparencyEnum


@dataclass
class ClipRectangle(CGMCommand):
    id = (3, 5)
    binary_params = ("P", "P")
    p1: cgm_enums.Point
    p2: cgm_enums.Point


@dataclass
class ClipIndicator(CGMCommand):
    id = (3, 6)
    binary_params = ("E",)
    clip: cgm_enums.ClipIndicatorEnum


@dataclass
class Polyline(CGMCommand):
    id = (4, 1)
    binary_params = ("P",)
    points: List[cgm_enums.Point]


@dataclass
class DisjointPolyline(CGMCommand):
    id = (4, 2)
    binary_params = ("P",)
    points: List[cgm_enums.Point]


@dataclass
class Polymarker(CGMCommand):
    id = (4, 3)
    binary_params = ("P",)
    points: List[cgm_enums.Point]


@dataclass
class Text(CGMCommand):
    id = (4, 4)
    binary_params = ("P", "E", "S")
    point: cgm_enums.Point
    flag: cgm_enums.TextFinalFlag
    string: str


@dataclass
class RestrictedText(CGMCommand):
    id = (4, 5)
    binary_params = ("VDC", "VDC", "P", "E", "S")
    width: float
    height: float
    point: cgm_enums.Point
    flag: cgm_enums.TextFinalFlag
    string: str


@dataclass
class AppendText(CGMCommand):
    id = (4, 6)
    binary_params = ("E", "S")
    flag: cgm_enums.TextFinalFlag
    string: str


@dataclass
class Polygon(CGMCommand):
    id = (4, 7)
    binary_params = ("P",)
    points: List[cgm_enums.Point]


@dataclass
class PolygonSet(CGMCommand):
    id = (4, 8)
    points: List[cgm_enums.PolygonSetEntry]
    binary_params = (("P", "E"),)


# TODO: Cell array (complicated parsing with sub-byte values)


@dataclass
class GeneralizedDrawingPrimitive(CGMCommand):
    id = (4, 10)
    identifier: int
    points: List[cgm_enums.Point]
    data: bytes

    @classmethod
    def get_binary_args(cls, stream: BinaryCGMCommandParser, length: int):
        yield stream.read_signed()
        num = stream.read_signed()
        points = [stream.read_point() for _ in range(num)]
        yield points
        read = stream.i_size() * 2 + num * stream.p_size()
        yield stream.read(length - read)


@dataclass
class Rectangle(CGMCommand):
    id = (4, 11)
    binary_params = ("P", "P")
    p1: cgm_enums.Point
    p2: cgm_enums.Point


@dataclass
class Circle(CGMCommand):
    id = (4, 12)
    binary_params = ("P", "VDC")
    center: cgm_enums.Point
    radius: float


@dataclass
class CircularArc3Point(CGMCommand):
    id = (4, 13)
    binary_params = ("P", "P", "P")
    p1: cgm_enums.Point
    p2: cgm_enums.Point
    p3: cgm_enums.Point


@dataclass
class CircularArc3PointClose(CircularArc3Point):
    id = (4, 14)
    binary_params = ("P", "P", "P", "E")  # type: ignore
    closure: cgm_enums.ArcClosureEnum


@dataclass
class CircularArcCentre(CGMCommand):
    id = (4, 15)
    binary_params = ("P", "VDC", "VDC", "VDC", "VDC", "VDC")  # type: ignore
    centre: cgm_enums.Point
    delta_x_start: float
    delta_y_start: float
    delta_x_end: float
    delta_y_end: float
    radius: float


@dataclass
class CircularArcCentreClose(CircularArcCentre):
    id = (4, 16)
    binary_params = ("P", "VDC", "VDC", "VDC", "VDC", "VDC", "E")  # type: ignore
    closure: cgm_enums.ArcClosureEnum


@dataclass
class Ellipse(CGMCommand):
    id = (4, 17)
    binary_params = ("P", "P", "P")
    center: cgm_enums.Point
    point_1: cgm_enums.Point
    point_2: cgm_enums.Point


@dataclass
class EllipticalArc(Ellipse):
    id = (4, 18)
    binary_params = ("P", "P", "P", "VDC", "VDC", "VDC", "VDC")  # type: ignore
    delta_x_start: float
    delta_y_start: float
    delta_x_end: float
    delta_y_end: float


@dataclass
class EllipticalArcClose(EllipticalArc):
    id = (4, 19)
    binary_params = ("P", "P", "P", "VDC", "VDC", "VDC", "VDC", "E")  # type: ignore
    closure: cgm_enums.ArcClosureEnum


@dataclass
class Polybezier(CGMCommand):
    id = (4, 26)
    binary_params = ("E", "P")
    continuous: cgm_enums.PolybezierContinuityEnum
    points: List[cgm_enums.Point]


@dataclass
class LineType(CGMCommand):
    id = (5, 2)
    binary_params = ("IX",)
    type: cgm_enums.LineTypeEnum


@dataclass
class LineWidth(CGMCommand):
    id = (5, 3)
    binary_params = ("LSS",)
    width: float


@dataclass
class LineColour(CGMCommand):
    id = (5, 4)
    binary_params = ("CO",)
    colour: cgm_enums.Colour


@dataclass
class MarkerType(CGMCommand):
    id = (5, 6)
    binary_params = ("E",)
    type: cgm_enums.MarkerTypeEnum


@dataclass
class MarkerSize(CGMCommand):
    id = (5, 7)
    binary_params = ("MSS",)
    size: float


@dataclass
class MarkerColour(CGMCommand):
    id = (5, 8)
    binary_params = ("CO",)
    colour: cgm_enums.Colour


@dataclass
class TextFontIndex(CGMCommand):
    id = (5, 10)
    binary_params = ("IX",)
    index: int


@dataclass
class TextPrecision(CGMCommand):
    id = (5, 11)
    binary_params = ("E",)
    text_precision: cgm_enums.TextPrecisionEnum


@dataclass
class CharacterExpansionFactor(CGMCommand):
    id = (5, 12)
    binary_params = ("R",)
    factor: float


@dataclass
class CharacterSpacing(CGMCommand):
    id = (5, 13)
    binary_params = ("R",)
    spacing: float


@dataclass
class TextColour(CGMCommand):
    id = (5, 14)
    binary_params = ("CO",)
    colour: cgm_enums.Colour


@dataclass
class CharacterHeight(CGMCommand):
    id = (5, 15)
    binary_params = ("VDC",)
    height: float


@dataclass
class CharacterOrientation(CGMCommand):
    id = (5, 16)
    binary_params = ("VDC", "VDC", "VDC", "VDC")
    x_character_up: float
    y_character_up: float
    x_character_base: float
    y_character_base: float


@dataclass
class TextPath(CGMCommand):
    id = (5, 17)
    binary_params = ("E",)
    path: cgm_enums.TextPathEnum


@dataclass
class TextAlignment(CGMCommand):
    id = (5, 18)
    binary_params = ("E", "E", "R", "R")

    horizontal_alignment: cgm_enums.TextHorizontalAlignmentEnum
    vertical_alignment: cgm_enums.TextVerticalAlignmentEnum
    continous_horizontal_alignment: float
    continous_vertical_alignment: float


@dataclass
class CharacterSetIndex(CGMCommand):
    id = (5, 19)
    binary_params = ("IX",)

    index: int


@dataclass
class AlternateCharacterSetIndex(CGMCommand):
    id = (5, 20)
    binary_params = ("IX",)

    index: int


@dataclass
class InteriorStyle(CGMCommand):
    id = (5, 22)
    binary_params = ("E",)

    interior_style: cgm_enums.InteriorStyleEnum


@dataclass
class FillColour(CGMCommand):
    id = (5, 23)
    binary_params = ("CO",)

    fill: cgm_enums.Colour


@dataclass
class HatchIndex(CGMCommand):
    id = (5, 24)
    binary_params = ("IX",)
    index: int


@dataclass
class PatternIndex(CGMCommand):
    id = (5, 25)
    binary_params = ("IX",)
    index: int


@dataclass
class EdgeType(CGMCommand):
    id = (5, 27)
    binary_params = ("IX",)
    type: cgm_enums.LineTypeEnum


@dataclass
class EdgeWidth(CGMCommand):
    id = (5, 28)
    binary_params = ("ESS",)
    width: float


@dataclass
class EdgeColour(CGMCommand):
    id = (5, 29)
    binary_params = ("CO",)
    colour: cgm_enums.Colour


@dataclass
class EdgeVisibility(CGMCommand):
    id = (5, 30)
    binary_params = ("E",)
    visibility: cgm_enums.EdgeVisibilityEnum


@dataclass
class ColourTable(CGMCommand):
    id = (5, 34)
    binary_params = ("CI", "CD")

    colour_index: cgm_enums.IndexColour
    colours: List[cgm_enums.DirectColour]


@dataclass
class AspectSourceFlags(CGMCommand):
    id = (5, 35)
    binary_params = (("E", "E"),)
    flags: List[Tuple[cgm_enums.ASFTypeEnum, cgm_enums.ASFValue]]


@dataclass
class LineCap(CGMCommand):
    id = (5, 37)
    binary_params = ("E", "E")
    cap: cgm_enums.CapIndicatorEnum
    dash_cap: cgm_enums.DashCapIndicatorEnum


@dataclass
class RestrictedTextType(CGMCommand):
    id = (5, 42)
    binary_params = ("E",)
    type: cgm_enums.RestrictionTypeEnum


@dataclass
class ApplicationData(CGMCommand):
    id = (7, 2)
    binary_params = ("I", "SF")

    identifier: int
    data: str
