"""
Linked layer structure.
"""

import io
import logging
from typing import Any, BinaryIO, TypeVar

from attrs import define, field

from psd_tools.constants import LinkedLayerType
from psd_tools.psd.base import BaseElement, ListElement
from psd_tools.psd.descriptor import DescriptorBlock
from psd_tools.psd.bin_utils import (
    is_readable,
    read_fmt,
    read_length_block,
    read_pascal_string,
    read_unicode_string,
    write_bytes,
    write_fmt,
    write_length_block,
    write_padding,
    write_pascal_string,
    write_unicode_string,
)
from psd_tools.validators import in_, range_

logger = logging.getLogger(__name__)

T_LinkedLayers = TypeVar("T_LinkedLayers", bound="LinkedLayers")
T_LinkedLayer = TypeVar("T_LinkedLayer", bound="LinkedLayer")


class LinkedLayers(ListElement):
    """
    List of LinkedLayer structure. See :py:class:`.LinkedLayer`.
    """

    @classmethod
    def read(cls: type[T_LinkedLayers], fp: BinaryIO, **kwargs: Any) -> T_LinkedLayers:
        items = []
        while is_readable(fp, 8):
            data = read_length_block(fp, fmt="Q", padding=4)
            with io.BytesIO(data) as f:
                items.append(LinkedLayer.read(f))
        return cls(items)  # type: ignore[arg-type]

    def write(self, fp: BinaryIO, **kwargs: Any) -> int:
        written = 0
        for item in self:
            written += write_length_block(fp, item.write, fmt="Q", padding=4)
        return written


@define(repr=False)
class LinkedLayer(BaseElement):
    """
    LinkedLayer structure.

    .. py:attribute:: kind
    .. py:attribute:: version
    .. py:attribute:: uuid
    .. py:attribute:: filename
    .. py:attribute:: filetype
    .. py:attribute:: creator
    .. py:attribute:: filesize
    .. py:attribute:: open_file
    .. py:attribute:: linked_file
    .. py:attribute:: timestamp
    .. py:attribute:: data
    .. py:attribute:: child_id
    .. py:attribute:: mod_time
    .. py:attribute:: lock_state
    """

    kind: LinkedLayerType = field(
        default=LinkedLayerType.ALIAS, validator=in_(LinkedLayerType)
    )
    version: int = field(default=1, validator=range_(1, 8))
    uuid: str = ""
    filename: str = ""
    filetype: bytes = b"\x00\x00\x00\x00"
    creator: bytes = b"\x00\x00\x00\x00"
    filesize: int | None = None
    open_file: DescriptorBlock | None = None
    linked_file: DescriptorBlock | None = None
    timestamp: tuple | None = None
    data: bytes | None = None
    child_id: str | None = None
    mod_time: float | None = None
    lock_state: int | None = None

    @classmethod
    def read(cls: type[T_LinkedLayer], fp: BinaryIO, **kwargs: Any) -> T_LinkedLayer:
        kind = LinkedLayerType(read_fmt("4s", fp)[0])
        version = read_fmt("I", fp)[0]
        assert 1 <= version and version <= 8, "Invalid version %d" % (version)
        uuid = read_pascal_string(fp, "macroman", padding=1)
        filename = read_unicode_string(fp)
        filetype, creator, datasize, open_file = read_fmt("4s4sQB", fp)
        if open_file:
            open_file = DescriptorBlock.read(fp, padding=1)
        else:
            open_file = None

        linked_file = None
        timestamp = None
        data = None
        filesize = None
        child_id = None
        mod_time = None
        lock_state = None

        if kind == LinkedLayerType.EXTERNAL:
            linked_file = DescriptorBlock.read(fp, padding=1)
            if version > 3:
                timestamp = read_fmt("I4Bd", fp)
            filesize = read_fmt("Q", fp)[0]  # External file size.
            if version > 2:
                data = fp.read(datasize)
        elif kind == LinkedLayerType.ALIAS:
            read_fmt("8x", fp)
        if kind == LinkedLayerType.DATA:
            data = fp.read(datasize)
            assert len(data) == datasize, "(%d vs %d)" % (len(data), datasize)

        # The followings are not well documented...
        if version >= 5:
            child_id = read_unicode_string(fp)
        if version >= 6:
            mod_time = read_fmt("d", fp)[0]
        if version >= 7:
            lock_state = read_fmt("B", fp)[0]
        if kind == LinkedLayerType.EXTERNAL and version == 2:
            data = fp.read(datasize)

        return cls(
            kind,
            version,
            uuid,
            filename,
            filetype,
            creator,
            filesize,
            open_file,
            linked_file,
            timestamp,
            data,
            child_id,
            mod_time,
            lock_state,
        )

    def write(self, fp: BinaryIO, padding: int = 1, **kwargs: Any) -> int:
        written = write_fmt(fp, "4sI", self.kind.value, self.version)
        written += write_pascal_string(fp, self.uuid, "macroman", padding=1)
        written += write_unicode_string(fp, self.filename)
        written += write_fmt(
            fp,
            "4s4sQB",
            self.filetype,
            self.creator,
            len(self.data) if self.data is not None else 0,
            self.open_file is not None,
        )
        if self.open_file is not None:
            written += self.open_file.write(fp, padding=1)

        if self.kind == LinkedLayerType.EXTERNAL:
            written += self.linked_file.write(fp, padding=1)  # type: ignore[union-attr]
            if self.version > 3:
                written += write_fmt(fp, "I4Bd", *self.timestamp)  # type: ignore[misc,operator]
            written += write_fmt(fp, "Q", self.filesize)
            if self.version > 2:
                assert self.data is not None
                written += write_bytes(fp, self.data)
        elif self.kind == LinkedLayerType.ALIAS:
            written += write_fmt(fp, "8x")
        if self.kind == LinkedLayerType.DATA:
            assert self.data is not None
            written += write_bytes(fp, self.data)

        if self.child_id is not None:
            written += write_unicode_string(fp, self.child_id)
        if self.mod_time is not None:
            written += write_fmt(fp, "d", self.mod_time)
        if self.lock_state is not None:
            written += write_fmt(fp, "B", self.lock_state)

        if self.kind == LinkedLayerType.EXTERNAL and self.version == 2:
            assert self.data is not None
            written += write_bytes(fp, self.data)

        written += write_padding(fp, written, padding)
        return written
