"""
Layer and mask data structures.

This module implements the low-level binary structures for PSD layers and masks,
corresponding to the "Layer and Mask Information" section of PSD files. This is
one of the most complex parts of the PSD format.

Key classes:

- :py:class:`LayerAndMaskInformation`: Top-level container for all layer data
- :py:class:`LayerInfo`: Contains layer records and channel image data
- :py:class:`LayerRecords`: List of individual layer records
- :py:class:`LayerRecord`: Single layer metadata (name, bounds, blend mode, etc.)
- :py:class:`ChannelInfo`: Channel metadata within a layer record
- :py:class:`ChannelImageData`: Compressed pixel data for all channels
- :py:class:`ChannelData`: Single channel's compressed pixel data
- :py:class:`MaskData`: Layer mask parameters
- :py:class:`GlobalLayerMaskInfo`: Document-wide mask settings
- :py:class:`TaggedBlocks`: Extended layer metadata (see :py:mod:`psd_tools.psd.tagged_blocks`)

The layer structure in PSD files is stored as a flat list with implicit hierarchy.
Group boundaries are marked by special layers with ``SectionDivider`` tagged blocks:

- ``BOUNDING_SECTION_DIVIDER``: Marks the start of a group (the layer that opens the group)
- ``OPEN_FOLDER`` or ``CLOSED_FOLDER``: Marks the end of a group (the closing divider layer)
  - ``OPEN_FOLDER``: Group was open in Photoshop UI
  - ``CLOSED_FOLDER``: Group was closed in Photoshop UI

The high-level API (:py:mod:`psd_tools.api`) reconstructs this into a proper
tree structure with parent-child relationships.

Each layer record contains:

1. **Metadata**: Rectangle bounds, blend mode, opacity, flags
2. **Channel info**: List of channels (R, G, B, A, masks, etc.) with byte offsets
3. **Blend ranges**: Advanced blending parameters
4. **Layer name**: Pascal string (legacy, inaccurate for Unicode names)
5. **Tagged blocks**: Extended metadata in key-value format

The channel image data section follows all layer records and contains the actual
compressed pixel data for each channel, referenced by the channel info structures.

Example of reading layer metadata::

    from psd_tools.psd import PSD

    with open('file.psd', 'rb') as f:
        psd = PSD.read(f)

    layer_info = psd.layer_and_mask_information.layer_info
    for record in layer_info.layer_records:
        print(f"Layer: {record.name}")
        print(f"  Bounds: {record.top}, {record.left}, {record.bottom}, {record.right}")
        print(f"  Blend mode: {record.blend_mode}")
        print(f"  Channels: {len(record.channel_info)}")

For most use cases, prefer the high-level :py:class:`~psd_tools.api.layers.Layer`
API which provides easier access to this data.
"""

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

from attrs import define, field, astuple

from psd_tools.compression import compress, decompress
from psd_tools.constants import (
    BlendMode,
    ChannelID,
    Clipping,
    Compression,
    GlobalLayerMaskKind,
    Tag,
)
from psd_tools.psd.base import BaseElement, ListElement
from psd_tools.psd.tagged_blocks import TaggedBlocks, register
from psd_tools.psd.bin_utils import (
    is_readable,
    read_fmt,
    read_length_block,
    read_pascal_string,
    write_bytes,
    write_fmt,
    write_length_block,
    write_padding,
    write_pascal_string,
)
from psd_tools.validators import in_, range_

logger = logging.getLogger(__name__)

T_LayerAndMaskInformation = TypeVar(
    "T_LayerAndMaskInformation", bound="LayerAndMaskInformation"
)
T_LayerInfo = TypeVar("T_LayerInfo", bound="LayerInfo")
T_ChannelInfo = TypeVar("T_ChannelInfo", bound="ChannelInfo")
T_LayerFlags = TypeVar("T_LayerFlags", bound="LayerFlags")
T_LayerBlendingRanges = TypeVar("T_LayerBlendingRanges", bound="LayerBlendingRanges")
T_LayerRecords = TypeVar("T_LayerRecords", bound="LayerRecords")
T_LayerRecord = TypeVar("T_LayerRecord", bound="LayerRecord")
T_MaskFlags = TypeVar("T_MaskFlags", bound="MaskFlags")
T_MaskData = TypeVar("T_MaskData", bound="MaskData")
T_MaskParameters = TypeVar("T_MaskParameters", bound="MaskParameters")
T_ChannelImageData = TypeVar("T_ChannelImageData", bound="ChannelImageData")
T_ChannelDataList = TypeVar("T_ChannelDataList", bound="ChannelDataList")
T_ChannelData = TypeVar("T_ChannelData", bound="ChannelData")
T_GlobalLayerMaskInfo = TypeVar("T_GlobalLayerMaskInfo", bound="GlobalLayerMaskInfo")


@define(repr=False)
class LayerAndMaskInformation(BaseElement):
    """
    Layer and mask information section.

    .. py:attribute:: layer_info

        See :py:class:`.LayerInfo`.

    .. py:attribute:: global_layer_mask_info

        See :py:class:`.GlobalLayerMaskInfo`.

    .. py:attribute:: tagged_blocks

        See :py:class:`.TaggedBlocks`.
    """

    layer_info: "LayerInfo | None" = None
    global_layer_mask_info: "GlobalLayerMaskInfo | None" = None
    tagged_blocks: "TaggedBlocks | None" = None

    @classmethod
    def read(
        cls: type[T_LayerAndMaskInformation],
        fp: IO[bytes],
        encoding: str = "macroman",
        version: int = 1,
        **kwargs: Any,
    ) -> T_LayerAndMaskInformation:
        start_pos = fp.tell()
        length = read_fmt(("I", "Q")[version - 1], fp)[0]
        end_pos = fp.tell() + length
        logger.debug(
            "reading layer and mask info, len=%d, offset=%d" % (length, start_pos)
        )
        if length == 0:
            self = cls()
        else:
            self = cls._read_body(fp, end_pos, encoding, version)
        if fp.tell() > end_pos:
            logger.warning(
                "LayerAndMaskInformation is broken: current fp=%d, expected=%d"
                % (fp.tell(), end_pos)
            )
        fp.seek(end_pos, 0)
        return self

    @classmethod
    def _read_body(
        cls: type[T_LayerAndMaskInformation],
        fp: IO[bytes],
        end_pos: int,
        encoding: str,
        version: int,
    ) -> T_LayerAndMaskInformation:
        layer_info = LayerInfo.read(fp, encoding, version)

        global_layer_mask_info = None
        if is_readable(fp, 17) and fp.tell() < end_pos:
            global_layer_mask_info = GlobalLayerMaskInfo.read(fp)

        tagged_blocks = None
        if is_readable(fp):
            # For some reason, global tagged blocks aligns 4 byte
            tagged_blocks = TaggedBlocks.read(
                fp, version=version, padding=4, end_pos=end_pos
            )

        return cls(layer_info, global_layer_mask_info, tagged_blocks)

    def write(
        self,
        fp: IO[bytes],
        encoding: str = "macroman",
        version: int = 1,
        padding: int = 4,
        **kwargs: Any,
    ) -> int:
        def writer(f: IO[bytes]) -> int:
            written = self._write_body(f, encoding, version, padding)
            logger.debug("writing layer and mask info, len=%d" % (written))
            return written

        fmt = ("I", "Q")[version - 1]
        return write_length_block(fp, writer, fmt=fmt)

    def _write_body(
        self, fp: IO[bytes], encoding: str, version: int, padding: int
    ) -> int:
        written = 0
        if self.layer_info:
            written += self.layer_info.write(fp, encoding, version, padding)
        if self.global_layer_mask_info:
            written += self.global_layer_mask_info.write(fp)
        if self.tagged_blocks:
            written += self.tagged_blocks.write(fp, version=version, padding=4)
        return written


@define(repr=False)
class LayerInfo(BaseElement):
    """
    High-level organization of the layer information.

    .. py:attribute:: layer_count

        Layer count. If it is a negative number, its absolute value is the
        number of layers and the first alpha channel contains the transparency
        data for the merged result.

    .. py:attribute:: layer_records

        Information about each layer. See :py:class:`.LayerRecords`.

    .. py:attribute:: channel_image_data

        Channel image data. See :py:class:`.ChannelImageData`.
    """

    layer_count: int = 0
    layer_records: "LayerRecords" = field(factory=lambda: LayerRecords())
    channel_image_data: "ChannelImageData" = field(factory=lambda: ChannelImageData())

    @classmethod
    def read(
        cls: type[T_LayerInfo],
        fp: IO[bytes],
        encoding: str = "macroman",
        version: int = 1,
        **kwargs: Any,
    ) -> T_LayerInfo:
        length = read_fmt(("I", "Q")[version - 1], fp)[0]
        logger.debug("reading layer info, len=%d" % length)
        end_pos = fp.tell() + length
        if length == 0:
            self = LayerInfo()
        else:
            self = cls._read_body(fp, encoding, version)
        assert fp.tell() <= end_pos
        fp.seek(end_pos, 0)
        return self  # type: ignore[return-value]

    @classmethod
    def _read_body(
        cls: type[T_LayerInfo], fp: IO[bytes], encoding: str, version: int
    ) -> T_LayerInfo:
        start_pos = fp.tell()
        layer_count = read_fmt("h", fp)[0]
        layer_records = LayerRecords.read(fp, layer_count, encoding, version)
        logger.debug("  read layer records, len=%d" % (fp.tell() - start_pos))
        channel_image_data = ChannelImageData.read(fp, layer_records)
        return cls(
            layer_count=layer_count,
            layer_records=layer_records,
            channel_image_data=channel_image_data,
        )

    def write(
        self,
        fp: IO[bytes],
        encoding: str = "macroman",
        version: int = 1,
        padding: int = 4,
        **kwargs: Any,
    ) -> int:
        def writer(f: IO[bytes]) -> int:
            written = self._write_body(f, encoding, version, padding)
            logger.debug("writing layer info, len=%d" % (written))
            return written

        fmt = ("I", "Q")[version - 1]
        if self.layer_count == 0:
            return write_fmt(fp, fmt, 0)
        return write_length_block(fp, writer, fmt=fmt)

    def _write_body(
        self, fp: IO[bytes], encoding: str, version: int, padding: int
    ) -> int:
        start_pos = fp.tell()
        written = write_fmt(fp, "h", self.layer_count)
        if self.layer_records:
            self._update_channel_length()
            written += self.layer_records.write(fp, encoding, version)
        logger.debug("  wrote layer records, len=%d" % (fp.tell() - start_pos))
        if self.channel_image_data:
            written += self.channel_image_data.write(fp)
        # Seems the padding size here is different between Photoshop and GIMP.
        written += write_padding(fp, written, padding)
        return written

    def _update_channel_length(self) -> None:
        if not self.layer_records or not self.channel_image_data:
            return

        for layer, lengths in zip(self.layer_records, self.channel_image_data._lengths):
            for channel_info, length in zip(layer.channel_info, lengths):
                channel_info.length = length


@register(Tag.LAYER_16)
@register(Tag.LAYER_32)
@define(repr=False)
class LayerInfoBlock(LayerInfo):
    """ """

    @classmethod
    def read(
        cls: type[T_LayerInfo],
        fp: IO[bytes],
        encoding: str = "macroman",
        version: int = 1,
        **kwargs: Any,
    ) -> T_LayerInfo:
        return cls._read_body(fp, encoding, version)

    def write(
        self,
        fp: IO[bytes],
        encoding: str = "macroman",
        version: int = 1,
        padding: int = 4,
        **kwargs: Any,
    ) -> int:
        return self._write_body(fp, encoding, version, padding)


@define(repr=False)
class ChannelInfo(BaseElement):
    """
    Channel information.

    .. py:attribute:: id

        Channel ID: 0 = red, 1 = green, etc.; -1 = transparency mask; -2 =
        user supplied layer mask, -3 real user supplied layer mask (when both
        a user mask and a vector mask are present). See
        :py:class:`~psd_tools.constants.ChannelID`.

    .. py:attribute:: length

        Length of the corresponding channel data.
    """

    id: ChannelID = field(
        default=ChannelID.CHANNEL_0, converter=ChannelID, validator=in_(ChannelID)
    )
    length: int = 0

    @classmethod
    def read(
        cls: type[T_ChannelInfo], fp: IO[bytes], version: int = 1, **kwargs: Any
    ) -> T_ChannelInfo:
        values = read_fmt(("hI", "hQ")[version - 1], fp)
        return cls(id=values[0], length=values[1])

    def write(self, fp: IO[bytes], version: int = 1, **kwargs: Any) -> int:
        return write_fmt(fp, ("hI", "hQ")[version - 1], *astuple(self))


@define(repr=False)
class LayerFlags(BaseElement):
    """
    Layer flags.

    Note there are undocumented flags. Maybe photoshop version.

    .. py:attribute:: transparency_protected
    .. py:attribute:: visible
    .. py:attribute:: pixel_data_irrelevant
    """

    transparency_protected: bool = False
    visible: bool = True
    obsolete: bool = field(default=False, repr=False)
    photoshop_v5_later: bool = field(default=True, repr=False)
    pixel_data_irrelevant: bool = False
    undocumented_1: bool = field(default=False, repr=False)
    undocumented_2: bool = field(default=False, repr=False)
    undocumented_3: bool = field(default=False, repr=False)

    @classmethod
    def read(cls: type[T_LayerFlags], fp: IO[bytes], **kwargs: Any) -> T_LayerFlags:
        flags = read_fmt("B", fp)[0]
        return cls(
            bool(flags & 1),
            not bool(flags & 2),  # why "not"?
            bool(flags & 4),
            bool(flags & 8),
            bool(flags & 16),
            bool(flags & 32),
            bool(flags & 64),
            bool(flags & 128),
        )

    def write(self, fp: IO[bytes], **kwargs: Any) -> int:
        flags = (
            (self.transparency_protected * 1)
            | ((not self.visible) * 2)
            | (self.obsolete * 4)
            | (self.photoshop_v5_later * 8)
            | (self.pixel_data_irrelevant * 16)
            | (self.undocumented_1 * 32)
            | (self.undocumented_2 * 64)
            | (self.undocumented_3 * 128)
        )
        return write_fmt(fp, "B", flags)


@define(repr=False)
class LayerBlendingRanges(BaseElement):
    """
    Layer blending ranges.

    All ranges contain 2 black values followed by 2 white values.

    .. py:attribute:: composite_ranges

        List of composite gray blend source and destination ranges.

    .. py:attribute:: channel_ranges

        List of channel source and destination ranges.
    """

    composite_ranges: list[tuple[int, int]] = field(
        factory=lambda: [(0, 65535), (0, 65535)]
    )
    channel_ranges: list[list[tuple[int, int]]] = field(
        factory=lambda: [
            [(0, 65535), (0, 65535)],
            [(0, 65535), (0, 65535)],
            [(0, 65535), (0, 65535)],
            [(0, 65535), (0, 65535)],
        ]
    )

    @classmethod
    def read(
        cls: type[T_LayerBlendingRanges], fp: IO[bytes], **kwargs: Any
    ) -> T_LayerBlendingRanges:
        data = read_length_block(fp)
        if len(data) == 0:
            return cls(None, None)  # type: ignore[arg-type]

        with io.BytesIO(data) as f:
            return cls._read_body(f)

    @classmethod
    def _read_body(
        cls: type[T_LayerBlendingRanges], fp: IO[bytes]
    ) -> T_LayerBlendingRanges:
        def read_channel_range(f: IO[bytes]) -> list[tuple[int, int]]:
            values = read_fmt("4H", f)
            return [values[0:2], values[2:4]]  # type: ignore[return-value]

        composite_ranges = read_channel_range(fp)
        channel_ranges = []
        while is_readable(fp, 8):
            channel_ranges.append(read_channel_range(fp))
        return cls(composite_ranges, channel_ranges)  # type: ignore[arg-type]

    def write(self, fp: IO[bytes], **kwargs: Any) -> int:
        return write_length_block(fp, lambda f: self._write_body(f))

    def _write_body(self, fp: IO[bytes]) -> int:
        written = 0
        if self.composite_ranges is not None:
            for x in self.composite_ranges:
                written += write_fmt(fp, "2H", *x)
        if self.channel_ranges is not None:
            for channel in self.channel_ranges:
                for x in channel:
                    written += write_fmt(fp, "2H", *x)
        return written


class LayerRecords(ListElement):
    """
    List of layer records. See :py:class:`.LayerRecord`.
    """

    @classmethod
    def read(  # type: ignore[override]
        cls: type[T_LayerRecords],
        fp: IO[bytes],
        layer_count: int,
        encoding: str = "macroman",
        version: int = 1,
        **kwargs: Any,
    ) -> T_LayerRecords:  # type: ignore[override]
        items = []
        for _ in range(abs(layer_count)):
            items.append(LayerRecord.read(fp, encoding, version))
        return cls(items)  # type: ignore[arg-type]


@define(repr=False)
class LayerRecord(BaseElement):
    """
    Layer record.

    .. py:attribute:: top

        Top position.

    .. py:attribute:: left

        Left position.

    .. py:attribute:: bottom

        Bottom position.

    .. py:attribute:: right

        Right position.

    .. py:attribute:: channel_info

        List of :py:class:`.ChannelInfo`.

    .. py:attribute:: signature

        Blend mode signature ``b'8BIM'``.

    .. py:attribute:: blend_mode

        Blend mode key. See :py:class:`~psd_tools.constants.BlendMode`.

    .. py:attribute:: opacity

        Opacity, 0 = transparent, 255 = opaque.

    .. py:attribute:: clipping

        Clipping, 0 = base, 1 = non-base. See
        :py:class:`~psd_tools.constants.Clipping`.

    .. py:attribute:: flags

        See :py:class:`.LayerFlags`.

    .. py:attribute:: mask_data

        :py:class:`.MaskData` or None.

    .. py:attribute:: blending_ranges

        See :py:class:`~psd_tools.constants.LayerBlendingRanges`.

    .. py:attribute:: name

        Layer name.

    .. py:attribute:: tagged_blocks

        See :py:class:`.TaggedBlocks`.
    """

    top: int = 0
    left: int = 0
    bottom: int = 0
    right: int = 0
    channel_info: list[ChannelInfo] = field(factory=list)
    signature: bytes = field(default=b"8BIM", repr=False, validator=in_((b"8BIM",)))
    blend_mode: BlendMode = field(
        default=BlendMode.NORMAL, converter=BlendMode, validator=in_(BlendMode)
    )
    opacity: int = field(default=255, validator=range_(0, 255))
    clipping: Clipping = field(
        default=Clipping.BASE, converter=Clipping, validator=in_(Clipping)
    )
    flags: LayerFlags = field(factory=LayerFlags)
    mask_data: object = None
    blending_ranges: LayerBlendingRanges = field(factory=LayerBlendingRanges)
    name: str = ""
    tagged_blocks: TaggedBlocks = field(factory=TaggedBlocks)

    @classmethod
    def read(
        cls: type[T_LayerRecord],
        fp: IO[bytes],
        encoding: str = "macroman",
        version: int = 1,
        **kwargs: Any,
    ) -> T_LayerRecord:
        start_pos = fp.tell()
        top, left, bottom, right, num_channels = read_fmt("4iH", fp)
        channel_info = [ChannelInfo.read(fp, version) for i in range(num_channels)]
        signature, blend_mode, opacity, clipping = read_fmt("4s4sBB", fp)
        flags = LayerFlags.read(fp)

        data = read_length_block(fp, fmt="xI")
        logger.debug("  read layer record, len=%d" % (fp.tell() - start_pos))
        with io.BytesIO(data) as f:
            mask_data, blending_ranges, name, tagged_blocks = cls._read_extra(
                f, encoding, version
            )
            self = cls(
                top=top,
                left=left,
                bottom=bottom,
                right=right,
                channel_info=channel_info,
                signature=signature,
                blend_mode=blend_mode,
                opacity=opacity,
                clipping=clipping,
                flags=flags,
                mask_data=mask_data,
                blending_ranges=blending_ranges,
                name=name,
                tagged_blocks=tagged_blocks,
            )

        # with io.BytesIO() as f:
        #     self._write_extra(f, encoding, version)
        #     assert data == f.getvalue()

        return self

    @classmethod
    def _read_extra(
        cls, fp: IO[bytes], encoding: str, version: int
    ) -> tuple["MaskData | None", LayerBlendingRanges, str, TaggedBlocks]:
        mask_data = MaskData.read(fp)
        blending_ranges = LayerBlendingRanges.read(fp)
        name = read_pascal_string(fp, encoding, padding=4)
        tagged_blocks = TaggedBlocks.read(fp, version=version, padding=1)
        return mask_data, blending_ranges, name, tagged_blocks

    def write(
        self, fp: IO[bytes], encoding: str = "macroman", version: int = 1, **kwargs: Any
    ) -> int:
        start_pos = fp.tell()
        written = write_fmt(
            fp,
            "4iH",
            self.top,
            self.left,
            self.bottom,
            self.right,
            len(self.channel_info),
        )
        written += sum(c.write(fp, version) for c in self.channel_info)
        written += write_fmt(
            fp,
            "4s4sBB",
            self.signature,
            self.blend_mode.value,
            self.opacity,
            self.clipping.value,
        )
        written += self.flags.write(fp)

        def writer(f: IO[bytes]) -> int:
            written = self._write_extra(f, encoding, version)
            logger.debug("  wrote layer record, len=%d" % (fp.tell() - start_pos))
            return written

        written += write_length_block(fp, writer, fmt="xI")
        return written

    def _write_extra(self, fp: IO[bytes], encoding: str, version: int) -> int:
        written = 0
        if self.mask_data and hasattr(self.mask_data, "write"):
            written += self.mask_data.write(fp)  # type: ignore[attr-defined]
        else:
            written += write_fmt(fp, "I", 0)

        written += self.blending_ranges.write(fp)
        written += write_pascal_string(fp, self.name, encoding, padding=4)
        written += self.tagged_blocks.write(fp, version, padding=1)
        written += write_padding(fp, written, 2)
        return written

    @property
    def width(self) -> int:
        """Width of the layer."""
        return max(self.right - self.left, 0)

    @property
    def height(self) -> int:
        """Height of the layer."""
        return max(self.bottom - self.top, 0)

    @property
    def channel_sizes(self) -> list[tuple[int, int]]:
        """List of channel sizes: [(width, height)]."""
        sizes = []
        for channel in self.channel_info:
            if channel.id == ChannelID.USER_LAYER_MASK:
                sizes.append((self.mask_data.width, self.mask_data.height))  # type: ignore[attr-defined]
            elif channel.id == ChannelID.REAL_USER_LAYER_MASK:
                sizes.append((self.mask_data.real_width, self.mask_data.real_height))  # type: ignore[attr-defined]
            else:
                sizes.append((self.width, self.height))
        return sizes


@define(repr=False)
class MaskFlags(BaseElement):
    """
    Mask flags.

    .. py:attribute:: pos_relative_to_layer

        Position relative to layer.

    .. py:attribute:: mask_disabled

        Layer mask disabled.

    .. py:attribute:: invert_mask

        Invert layer mask when blending (Obsolete).

    .. py:attribute:: user_mask_from_render

        The user mask actually came from rendering other data.

    .. py:attribute:: parameters_applied

        The user and/or vector masks have parameters applied to them.
    """

    pos_relative_to_layer: bool = False
    mask_disabled: bool = False
    invert_mask: bool = False
    user_mask_from_render: bool = False
    parameters_applied: bool = False
    undocumented_1: bool = field(default=False, repr=False)
    undocumented_2: bool = field(default=False, repr=False)
    undocumented_3: bool = field(default=False, repr=False)

    @classmethod
    def read(cls: type[T_MaskFlags], fp: IO[bytes], **kwargs: Any) -> T_MaskFlags:
        flags = read_fmt("B", fp)[0]
        return cls(
            bool(flags & 1),
            bool(flags & 2),
            bool(flags & 4),
            bool(flags & 8),
            bool(flags & 16),
            bool(flags & 32),
            bool(flags & 64),
            bool(flags & 128),
        )

    def write(self, fp: IO[bytes], **kwargs: Any) -> int:
        flags = (
            (self.pos_relative_to_layer * 1)
            | (self.mask_disabled * 2)
            | (self.invert_mask * 4)
            | (self.user_mask_from_render * 8)
            | (self.parameters_applied * 16)
            | (self.undocumented_1 * 32)
            | (self.undocumented_2 * 64)
            | (self.undocumented_3 * 128)
        )
        return write_fmt(fp, "B", flags)


@define(repr=False)
class MaskData(BaseElement):
    """
    Mask data.

    Real user mask is a final composite mask of vector and pixel masks.

    .. py:attribute:: top

        Top position.

    .. py:attribute:: left

        Left position.

    .. py:attribute:: bottom

        Bottom position.

    .. py:attribute:: right

        Right position.

    .. py:attribute:: background_color

        Default color. 0 or 255.

    .. py:attribute:: flags

        See :py:class:`.MaskFlags`.

    .. py:attribute:: parameters

        :py:class:`.MaskParameters` or None.

    .. py:attribute:: real_flags

        Real user mask flags. See :py:class:`.MaskFlags`.

    .. py:attribute:: real_background_color

        Real user mask background. 0 or 255.

    .. py:attribute:: real_top

        Top position of real user mask.

    .. py:attribute:: real_left

        Left position of real user mask.

    .. py:attribute:: real_bottom

        Bottom position of real user mask.

    .. py:attribute:: real_right

        Right position of real user mask.
    """

    top: int = 0
    left: int = 0
    bottom: int = 0
    right: int = 0
    background_color: int = 0
    flags: MaskFlags = field(factory=MaskFlags)
    parameters: "MaskParameters | None" = None
    real_flags: MaskFlags | None = None
    real_background_color: int | None = None
    real_top: int | None = None
    real_left: int | None = None
    real_bottom: int | None = None
    real_right: int | None = None

    @classmethod
    def read(cls: type[T_MaskData], fp: IO[bytes], **kwargs: Any) -> T_MaskData:  # type: ignore[return]
        data = read_length_block(fp)
        if len(data) == 0:
            return None  # type: ignore[return-value]

        with io.BytesIO(data) as f:
            return cls._read_body(f, len(data))

    @classmethod
    def _read_body(cls: type[T_MaskData], fp: IO[bytes], length: int) -> T_MaskData:
        top, left, bottom, right, background_color = read_fmt("4iB", fp)
        flags = MaskFlags.read(fp)

        # Order is based on tests. The specification is messed up here...

        # if length == 20:
        #     read_fmt('2x', fp)
        #     return cls(top, left, bottom, right, background_color, flags)

        real_flags, real_background_color = None, None
        real_top, real_left, real_bottom, real_right = None, None, None, None
        if length >= 36:
            real_flags = MaskFlags.read(fp)
            real_background_color = read_fmt("B", fp)[0]
            real_top, real_left, real_bottom, real_right = read_fmt("4i", fp)

        parameters = None
        if flags.parameters_applied:
            parameters = MaskParameters.read(fp)

        # logger.debug('    skipping %d' % (len(fp.read())))
        return cls(
            top=top,
            left=left,
            bottom=bottom,
            right=right,
            background_color=background_color,
            flags=flags,
            parameters=parameters,
            real_flags=real_flags,
            real_background_color=real_background_color,
            real_top=real_top,
            real_left=real_left,
            real_bottom=real_bottom,
            real_right=real_right,
        )

    def write(self, fp: IO[bytes], **kwargs: Any) -> int:
        return write_length_block(fp, lambda f: self._write_body(f))

    def _write_body(self, fp: IO[bytes]) -> int:
        written = write_fmt(
            fp,
            "4iB",
            self.top,
            self.left,
            self.bottom,
            self.right,
            self.background_color,
        )
        written += self.flags.write(fp)

        # if self.real_flags is None and self.parameters is None:
        #     written += write_fmt(fp, '2x')
        #     assert written == 20

        if self.real_flags is not None:
            written += self.real_flags.write(fp)
            written += write_fmt(
                fp,
                "B4i",
                self.real_background_color,
                self.real_top,
                self.real_left,
                self.real_bottom,
                self.real_right,
            )

        if self.flags.parameters_applied and self.parameters is not None:
            written += self.parameters.write(fp)

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

    @property
    def width(self) -> int:
        """Width of the mask."""
        return max(self.right - self.left, 0)

    @property
    def height(self) -> int:
        """Height of the mask."""
        return max(self.bottom - self.top, 0)

    @property
    def real_width(self) -> int:
        """Width of real user mask."""
        return max((self.real_right or 0) - (self.real_left or 0), 0)

    @property
    def real_height(self) -> int:
        """Height of real user mask."""
        return max((self.real_bottom or 0) - (self.real_top or 0), 0)


@define(repr=False)
class MaskParameters(BaseElement):
    """
    Mask parameters.

    .. py:attribute:: user_mask_density
    .. py:attribute:: user_mask_feather
    .. py:attribute:: vector_mask_density
    .. py:attribute:: vector_mask_feather
    """

    user_mask_density: int | None = None
    user_mask_feather: float | None = None
    vector_mask_density: int | None = None
    vector_mask_feather: float | None = None

    @classmethod
    def read(
        cls: type[T_MaskParameters], fp: IO[bytes], **kwargs: Any
    ) -> T_MaskParameters:
        parameters = read_fmt("B", fp)[0]
        user_mask_density = None
        user_mask_feather = None
        vector_mask_density = None
        vector_mask_feather = None
        try:
            if bool(parameters & 1):
                user_mask_density = read_fmt("B", fp)[0]
            if bool(parameters & 2):
                user_mask_feather = read_fmt("d", fp)[0]
            if bool(parameters & 4):
                vector_mask_density = read_fmt("B", fp)[0]
            if bool(parameters & 8):
                vector_mask_feather = read_fmt("d", fp)[0]
        except OSError as exc:
            logger.warning(
                "Truncated MaskParameters data (parameters=0x%02x); some fields will be missing: %s",
                parameters,
                exc,
            )
        return cls(
            user_mask_density,
            user_mask_feather,
            vector_mask_density,
            vector_mask_feather,
        )

    def write(self, fp: IO[bytes], **kwargs: Any) -> int:
        written = 0
        written += write_fmt(
            fp,
            "B",
            (
                (1 if self.user_mask_density is not None else 0)
                | (2 if self.user_mask_feather is not None else 0)
                | (4 if self.vector_mask_density is not None else 0)
                | (8 if self.vector_mask_feather is not None else 0)
            ),
        )
        if self.user_mask_density is not None:
            written += write_fmt(fp, "B", self.user_mask_density)
        if self.user_mask_feather is not None:
            written += write_fmt(fp, "d", self.user_mask_feather)
        if self.vector_mask_density is not None:
            written += write_fmt(fp, "B", self.vector_mask_density)
        if self.vector_mask_feather is not None:
            written += write_fmt(fp, "d", self.vector_mask_feather)
        return written


class ChannelImageData(ListElement):
    """
    List of channel data list.

    This size of this list corresponds to the size of
    :py:class:`LayerRecords`. Each item corresponds to the channels of each
    layer.

    See :py:class:`.ChannelDataList`.
    """

    @classmethod
    def read(
        cls: type[T_ChannelImageData],
        fp: IO[bytes],
        layer_records: "LayerRecords | None" = None,
        **kwargs: Any,
    ) -> T_ChannelImageData:
        start_pos = fp.tell()
        items = []
        if layer_records:
            for layer in layer_records:
                items.append(ChannelDataList.read(fp, layer.channel_info))
        logger.debug("  read channel image data, len=%d" % (fp.tell() - start_pos))
        return cls(items)  # type: ignore[arg-type]

    def write(self, fp: IO[bytes], **kwargs: Any) -> int:
        start_pos = fp.tell()
        written = sum(item.write(fp) for item in self)
        logger.debug("  wrote channel image data, len=%d" % (fp.tell() - start_pos))
        return written

    @property
    def _lengths(self) -> list[list[int]]:
        """List of layer channel lengths."""
        return [item._lengths for item in self]


class ChannelDataList(ListElement):
    """
    List of channel image data, corresponding to each color or alpha.

    See :py:class:`.ChannelData`.
    """

    @classmethod
    def read(  # type: ignore[override]
        cls: type[T_ChannelDataList],
        fp: IO[bytes],
        channel_info: list["ChannelInfo"],
        **kwargs: Any,
    ) -> T_ChannelDataList:
        items = []
        for c in channel_info:
            if c.length == 0:
                # length=0 means no channel data in the standard location
                # (e.g. pixel data is stored in a tagged block). Don't read.
                # Note: on save, this channel will be written as a minimal
                # ChannelData (compression header only, length=2), which
                # differs from the original length=0. This is still a valid
                # PSD representation per spec; byte-identical round-tripping
                # is not guaranteed for these channels.
                logger.debug("  channel %s: length=0, skipping read", c.id)
                items.append(ChannelData())
            elif c.length == 1:
                # length=1 is malformed (spec requires 0 or >=2). Consume the
                # stray byte to keep the file pointer aligned, then fall back
                # to an empty ChannelData so parsing can continue.
                logger.warning(
                    "  channel %s: length=1 is invalid, skipping 1 byte", c.id
                )
                fp.read(1)
                items.append(ChannelData())
            else:
                items.append(ChannelData.read(fp, c.length - 2, **kwargs))
        return cls(items)  # type: ignore[arg-type]

    @property
    def _lengths(self) -> list[int]:
        """List of channel lengths."""
        return [item._length for item in self]


@define(repr=False)
class ChannelData(BaseElement):
    """
    Channel data.

    .. py:attribute:: compression

        Compression type. See :py:class:`~psd_tools.constants.Compression`.

    .. py:attribute:: data

        Data.
    """

    compression: Compression = field(
        default=Compression.RAW, converter=Compression, validator=in_(Compression)
    )
    data: bytes = b""

    @classmethod
    def read(
        cls: type[T_ChannelData], fp: IO[bytes], length: int = 0, **kwargs: Any
    ) -> T_ChannelData:
        compression = Compression(read_fmt("H", fp)[0])
        # length is c.length - 2 (the 2-byte compression header is excluded).
        # A negative value here indicates an upstream logic error; warn and
        # clamp to 0 rather than letting fp.read(negative) consume until EOF.
        if length < 0:
            logger.warning(
                "ChannelData.read: negative length %d, clamping to 0", length
            )
            length = 0
        data = fp.read(length)
        return cls(compression=compression, data=data)

    def write(self, fp: IO[bytes], **kwargs: Any) -> int:
        written = write_fmt(fp, "H", self.compression.value)
        written += write_bytes(fp, self.data)
        # written += write_padding(fp, written, 2)  # Seems no padding here.
        return written

    def get_data(self, width: int, height: int, depth: int, version: int = 1) -> bytes:
        """Get decompressed channel data.

        :param width: width.
        :param height: height.
        :param depth: bit depth of the pixel.
        :param version: psd file version.
        :rtype: bytes
        """
        return decompress(self.data, self.compression, width, height, depth, version)

    def set_data(
        self, data: bytes, width: int, height: int, depth: int, version: int = 1
    ) -> int:
        """Set raw channel data and compress to store.

        :param data: raw data bytes to write.
        :param compression: compression type,
            see :py:class:`~psd_tools.constants.Compression`.
        :param width: width.
        :param height: height.
        :param depth: bit depth of the pixel.
        :param version: psd file version.
        """
        self.data = compress(data, self.compression, width, height, depth, version)
        return len(self.data)

    @property
    def _length(self) -> int:
        """Length of channel data block."""
        return 2 + len(self.data)


@define(repr=False)
class GlobalLayerMaskInfo(BaseElement):
    """
    Global mask information.

    .. py:attribute:: overlay_color

        Overlay color space (undocumented) and color components.

    .. py:attribute:: opacity

        Opacity. 0 = transparent, 100 = opaque.

    .. py:attribute:: kind

        Kind.
        0 = Color selected--i.e. inverted;
        1 = Color protected;
        128 = use value stored per layer. This value is preferred. The others
        are for backward compatibility with beta versions.
    """

    overlay_color: list[int] | None = None
    opacity: int = 0
    kind: GlobalLayerMaskKind = field(
        default=GlobalLayerMaskKind.PER_LAYER,
        converter=GlobalLayerMaskKind,
        validator=in_(GlobalLayerMaskKind),
    )

    @classmethod
    def read(
        cls: type[T_GlobalLayerMaskInfo], fp: IO[bytes], **kwargs: Any
    ) -> T_GlobalLayerMaskInfo:
        pos = fp.tell()
        data = read_length_block(fp)  # fmt?
        logger.debug("reading global layer mask info, len=%d" % (len(data)))
        if len(data) == 0:
            return cls(overlay_color=None)
        elif len(data) < 13:
            logger.warning(
                "global layer mask info is broken, expected 13 bytes but found"
                " only %d" % (len(data))
            )
            fp.seek(pos)
            return cls(overlay_color=None)

        with io.BytesIO(data) as f:
            return cls._read_body(f)

    @classmethod
    def _read_body(
        cls: type[T_GlobalLayerMaskInfo], fp: IO[bytes]
    ) -> T_GlobalLayerMaskInfo:
        overlay_color = list(read_fmt("5H", fp))
        opacity, kind = read_fmt("HB", fp)
        return cls(overlay_color, opacity, kind)

    def write(self, fp: IO[bytes], **kwargs: Any) -> int:
        return write_length_block(fp, lambda f: self._write_body(f))

    def _write_body(self, fp: IO[bytes]) -> int:
        written = 0
        if self.overlay_color is not None:
            written = write_fmt(fp, "5H", *self.overlay_color)
            written += write_fmt(fp, "HB", self.opacity, self.kind.value)
            written += write_padding(fp, written, 4)
        logger.debug("writing global layer mask info, len=%d" % (written))
        return written
