"""
Tagged block data structure.

.. todo:: Support the following tagged blocks: ``Tag.PATTERN_DATA``,
    ``Tag.TYPE_TOOL_INFO``, ``Tag.LAYER``,
    ``Tag.ALPHA``

"""
from __future__ import absolute_import, unicode_literals
import attr
import io
import logging
from warnings import warn

from psd_tools.constants import (
    BlendMode, SectionDivider, Tag, PlacedLayerType, SheetColorType
)
from psd_tools.psd.adjustments import ADJUSTMENT_TYPES
from psd_tools.psd.base import (
    BaseElement,
    ByteElement,
    DictElement,
    EmptyElement,
    IntegerElement,
    ListElement,
    StringElement,
    ValueElement,
)
from psd_tools.psd.color import Color
from psd_tools.psd.descriptor import DescriptorBlock, DescriptorBlock2
from psd_tools.psd.effects_layer import EffectsLayer
from psd_tools.psd.engine_data import EngineData, EngineData2
from psd_tools.psd.filter_effects import FilterEffects
from psd_tools.psd.linked_layer import LinkedLayers
from psd_tools.psd.patterns import Patterns
from psd_tools.psd.vector import (
    VectorMaskSetting, VectorStrokeContentSetting
)
from psd_tools.validators import in_
from psd_tools.utils import (
    read_fmt, write_fmt, read_length_block, write_length_block, is_readable,
    write_bytes, write_padding, read_pascal_string, write_pascal_string,
    trimmed_repr, new_registry
)

logger = logging.getLogger(__name__)

TYPES, register = new_registry()

TYPES.update(ADJUSTMENT_TYPES)
TYPES.update({
    Tag.ANIMATION_EFFECTS: DescriptorBlock,
    Tag.ARTBOARD_DATA1: DescriptorBlock,
    Tag.ARTBOARD_DATA2: DescriptorBlock,
    Tag.ARTBOARD_DATA3: DescriptorBlock,
    Tag.BLEND_CLIPPING_ELEMENTS: ByteElement,
    Tag.BLEND_FILL_OPACITY: ByteElement,
    Tag.BLEND_INTERIOR_ELEMENTS: ByteElement,
    Tag.COMPOSITOR_INFO: DescriptorBlock,
    Tag.CONTENT_GENERATOR_EXTRA_DATA: DescriptorBlock,
    Tag.EFFECTS_LAYER: EffectsLayer,
    Tag.EXPORT_SETTING1: DescriptorBlock,
    Tag.EXPORT_SETTING2: DescriptorBlock,
    Tag.FILTER_EFFECTS1: FilterEffects,
    Tag.FILTER_EFFECTS2: FilterEffects,
    Tag.FILTER_EFFECTS3: FilterEffects,
    Tag.FRAMED_GROUP: DescriptorBlock,
    Tag.KNOCKOUT_SETTING: ByteElement,
    Tag.LINKED_LAYER1: LinkedLayers,
    Tag.LINKED_LAYER2: LinkedLayers,
    Tag.LINKED_LAYER3: LinkedLayers,
    Tag.LINKED_LAYER_EXTERNAL: LinkedLayers,
    Tag.LAYER_ID: IntegerElement,
    Tag.LAYER_MASK_AS_GLOBAL_MASK: ByteElement,
    Tag.UNICODE_LAYER_NAME: StringElement,
    Tag.LAYER_VERSION: IntegerElement,
    Tag.OBJECT_BASED_EFFECTS_LAYER_INFO: DescriptorBlock2,
    Tag.OBJECT_BASED_EFFECTS_LAYER_INFO_V0: DescriptorBlock2,
    Tag.OBJECT_BASED_EFFECTS_LAYER_INFO_V1: DescriptorBlock2,
    Tag.PATTERNS1: Patterns,
    Tag.PATTERNS2: Patterns,
    Tag.PATTERNS3: Patterns,
    Tag.PATT: EmptyElement,
    Tag.PIXEL_SOURCE_DATA1: DescriptorBlock,
    Tag.SAVING_MERGED_TRANSPARENCY: EmptyElement,
    Tag.SAVING_MERGED_TRANSPARENCY16: EmptyElement,
    Tag.SAVING_MERGED_TRANSPARENCY32: EmptyElement,
    Tag.TEXT_ENGINE_DATA: EngineData2,
    Tag.TRANSPARENCY_SHAPES_LAYER: ByteElement,
    Tag.UNICODE_PATH_NAME: DescriptorBlock,
    Tag.USING_ALIGNED_RENDERING: IntegerElement,
    Tag.VECTOR_MASK_AS_GLOBAL_MASK: ByteElement,
    Tag.VECTOR_MASK_SETTING1: VectorMaskSetting,
    Tag.VECTOR_MASK_SETTING2: VectorMaskSetting,
    Tag.VECTOR_ORIGINATION_DATA: DescriptorBlock2,
    Tag.VECTOR_ORIGINATION_UNKNOWN: IntegerElement,
    Tag.VECTOR_STROKE_DATA: DescriptorBlock,
    Tag.VECTOR_STROKE_CONTENT_DATA: VectorStrokeContentSetting,
})


@attr.s(repr=False, slots=True)
class TaggedBlocks(DictElement):
    """
    Dict of tagged block items.

    See :py:class:`~psd_tools.constants.Tag` for available keys.

    Example::

        from psd_tools.constants import Tag

        # Iterate over fields
        for key in tagged_blocks:
            print(key)

        # Get a field
        value = tagged_blocks.get_data(Tag.TYPE_TOOL_OBJECT_SETTING)
    """

    def get_data(self, key, default=None):
        """
        Get data from the tagged blocks.

        Shortcut for the following::

            if key in tagged_blocks:
                value = tagged_blocks[key].data
        """
        if key in self:
            value = self[key].data
            if isinstance(value, ValueElement):
                return value.value
            else:
                return value
        return default

    def set_data(self, key, *args, **kwargs):
        """
        Set data for the given key.

        Shortut for the following::

            key = getattr(Tag, key)
            kls = TYPES.get(key)
            self[key] = TaggedBlocks(key=key, data=kls(value))

        """
        key = self._key_converter(key)
        kls = TYPES.get(key)
        self[key] = TaggedBlock(key=key, data=kls(*args, **kwargs))

    @classmethod
    def read(cls, fp, version=1, padding=1, end_pos=None):
        items = []
        while is_readable(fp, 8):  # len(signature) + len(key) = 8
            if end_pos is not None and fp.tell() >= end_pos:
                break
            block = TaggedBlock.read(fp, version, padding)
            if block is None:
                break
            items.append((block.key, block))
        return cls(items)

    @classmethod
    def _key_converter(self, key):
        return getattr(key, 'value', key)

    def _repr_pretty_(self, p, cycle):
        if cycle:
            return '{{...}'

        with p.group(2, '{', '}'):
            p.breakable('')
            for idx, key in enumerate(self._items):
                if idx:
                    p.text(',')
                    p.breakable()
                value = self._items[key]
                try:
                    p.text(Tag(key).name)
                except ValueError:
                    p.pretty(key)
                p.text(': ')
                if isinstance(value.data, bytes):
                    p.text(trimmed_repr(value.data))
                else:
                    p.pretty(value.data)
            p.breakable('')


@attr.s(repr=False, slots=True)
class TaggedBlock(BaseElement):
    """
    Layer tagged block with extra info.

    .. py:attribute:: key

        4-character code. See :py:class:`~psd_tools.constants.Tag`

    .. py:attribute:: data

        Data.
    """
    _SIGNATURES = (b'8BIM', b'8B64')
    _BIG_KEYS = {
        Tag.USER_MASK,
        Tag.LAYER_16,
        Tag.LAYER_32,
        Tag.LAYER,
        Tag.SAVING_MERGED_TRANSPARENCY16,
        Tag.SAVING_MERGED_TRANSPARENCY32,
        Tag.SAVING_MERGED_TRANSPARENCY,
        Tag.SAVING_MERGED_TRANSPARENCY16,
        Tag.ALPHA,
        Tag.FILTER_MASK,
        Tag.LINKED_LAYER2,
        Tag.LINKED_LAYER3,
        Tag.LINKED_LAYER_EXTERNAL,
        Tag.FILTER_EFFECTS1,
        Tag.FILTER_EFFECTS2,
        Tag.FILTER_EFFECTS3,
        Tag.PIXEL_SOURCE_DATA2,
        Tag.UNICODE_PATH_NAME,
        Tag.EXPORT_SETTING1,
        Tag.EXPORT_SETTING2,
        Tag.COMPOSITOR_INFO,
        Tag.ARTBOARD_DATA2,
    }

    signature = attr.ib(
        default=b'8BIM', repr=False, validator=in_(_SIGNATURES)
    )
    key = attr.ib(default=b'')
    data = attr.ib(default=b'', repr=True)

    @classmethod
    def read(cls, fp, version=1, padding=1):
        signature = read_fmt('4s', fp)[0]
        if signature not in cls._SIGNATURES:
            logger.warning('Invalid signature (%r)' % (signature))
            fp.seek(-4, 1)
            return None

        key = read_fmt('4s', fp)[0]
        try:
            key = Tag(key)
        except ValueError:
            message = 'Unknown key: %r' % (key)
            warn(message)
            logger.warning(message)

        fmt = cls._length_format(key, version)
        raw_data = read_length_block(fp, fmt=fmt, padding=padding)
        kls = TYPES.get(key)
        if kls:
            data = kls.frombytes(raw_data, version=version)
            # _raw_data = data.tobytes(version=version,
            #                          padding=1 if padding == 4 else 4)
            # assert raw_data == _raw_data, '%r: %s vs %s' % (
            #     kls, trimmed_repr(raw_data), trimmed_repr(_raw_data)
            # )
        else:
            message = 'Unknown tagged block: %r, %s' % (
                key, trimmed_repr(raw_data)
            )
            warn(message)
            logger.warning(message)
            data = raw_data
        return cls(signature, key, data)

    def write(self, fp, version=1, padding=1):
        key = self.key if isinstance(self.key, bytes) else self.key.value
        written = write_fmt(fp, '4s4s', self.signature, key)

        def writer(f):
            if hasattr(self.data, 'write'):
                # It seems padding size applies at the block level here.
                inner_padding = 1 if padding == 4 else 4
                return self.data.write(
                    f, padding=inner_padding, version=version
                )
            return write_bytes(f, self.data)

        fmt = self._length_format(self.key, version)
        written += write_length_block(fp, writer, fmt=fmt, padding=padding)
        return written

    @classmethod
    def _length_format(cls, key, version):
        return ('I', 'Q')[int(version == 2 and key in cls._BIG_KEYS)]


@register(Tag.ANNOTATIONS)
@attr.s(repr=False, slots=True)
class Annotations(ListElement):
    """
    List of Annotation, see :py:class: `.Annotation`.

    .. py:attribute:: major_version
    .. py:attribute:: minor_version
    """
    major_version = attr.ib(default=2, type=int)
    minor_version = attr.ib(default=1, type=int)

    @classmethod
    def read(cls, fp, **kwargs):
        major_version, minor_version, count = read_fmt('2HI', fp)
        items = []
        for _ in range(count):
            length = read_fmt('I', fp)[0] - 4
            if length > 0:
                with io.BytesIO(fp.read(length)) as f:
                    items.append(Annotation.read(f))
        return cls(
            major_version=major_version,
            minor_version=minor_version,
            items=items
        )

    def write(self, fp, **kwargs):
        written = write_fmt(
            fp, '2HI', self.major_version, self.minor_version, len(self)
        )
        for item in self:
            data = item.tobytes()
            written += write_fmt(fp, 'I', len(data) + 4)
            written += write_bytes(fp, data)
        written += write_padding(fp, written, 4)
        return written


@attr.s(repr=False, slots=True)
class Annotation(BaseElement):
    """
    Annotation structure.

    .. py:attribute:: kind
    .. py:attribute:: is_open
    """
    kind = attr.ib(
        default=b'txtA', type=bytes, validator=in_((b'txtA', b'sndM'))
    )
    is_open = attr.ib(default=0, type=int)
    flags = attr.ib(default=0, type=int)
    optional_blocks = attr.ib(default=1, type=int)
    icon_location = attr.ib(factory=lambda: [0, 0, 0, 0], converter=list)
    popup_location = attr.ib(factory=lambda: [0, 0, 0, 0], converter=list)
    color = attr.ib(factory=Color)
    author = attr.ib(default='', type=str)
    name = attr.ib(default='', type=str)
    mod_date = attr.ib(default='', type=str)
    marker = attr.ib(
        default=b'txtC', type=bytes, validator=in_((b'txtC', b'sndM'))
    )
    data = attr.ib(default=b'', type=bytes)

    @classmethod
    def read(cls, fp, **kwargs):
        kind, is_open, flags, optional_blocks = read_fmt('4s2BH', fp)
        icon_location = read_fmt('4i', fp)
        popup_location = read_fmt('4i', fp)
        color = Color.read(fp)
        author = read_pascal_string(fp, 'macroman', padding=2)
        name = read_pascal_string(fp, 'macroman', padding=2)
        mod_date = read_pascal_string(fp, 'macroman', padding=2)
        length, marker = read_fmt('I4s', fp)
        data = read_length_block(fp)
        return cls(
            kind, is_open, flags, optional_blocks, icon_location,
            popup_location, color, author, name, mod_date, marker, data
        )

    def write(self, fp, **kwargs):
        written = write_fmt(
            fp, '4s2BH', self.kind, self.is_open, self.flags,
            self.optional_blocks
        )
        written += write_fmt(fp, '4i', *self.icon_location)
        written += write_fmt(fp, '4i', *self.popup_location)
        written += self.color.write(fp)
        written += write_pascal_string(fp, self.author, 'macroman', padding=2)
        written += write_pascal_string(fp, self.name, 'macroman', padding=2)
        written += write_pascal_string(
            fp, self.mod_date, 'macroman', padding=2
        )
        written += write_fmt(fp, 'I4s', len(self.data) + 12, self.marker)
        written += write_length_block(fp, lambda f: write_bytes(f, self.data))
        return written


@register(Tag.FOREIGN_EFFECT_ID)
@register(Tag.LAYER_NAME_SOURCE_SETTING)
@attr.s(repr=False, slots=True, eq=False, order=False)
class Bytes(ValueElement):
    """
    Bytes structure.

    .. py:attribute:: value
    """
    value = attr.ib(default=b'\x00\x00\x00\x00', type=bytes)

    @classmethod
    def read(cls, fp, **kwargs):
        return cls(fp.read(4))

    def write(self, fp, **kwargs):
        return write_bytes(fp, self.value)


@register(Tag.CHANNEL_BLENDING_RESTRICTIONS_SETTING)
@attr.s(repr=False, slots=True)
class ChannelBlendingRestrictionsSetting(ListElement):
    """
    ChannelBlendingRestrictionsSetting structure.

    List of restricted channel numbers (int).
    """

    @classmethod
    def read(cls, fp, **kwargs):
        items = []
        while is_readable(fp, 4):
            items.append(read_fmt('I', fp)[0])
        return cls(items)

    def write(self, fp, **kwargs):
        return write_fmt(fp, '%dI' % len(self), *self._items)


@register(Tag.FILTER_MASK)
@attr.s(repr=False, slots=True)
class FilterMask(BaseElement):
    """
    FilterMask structure.

    .. py:attribute:: color
    .. py:attribute:: opacity
    """
    color = attr.ib(default=None)
    opacity = attr.ib(default=0, type=int)

    @classmethod
    def read(cls, fp, **kwargs):
        color = Color.read(fp)
        opacity = read_fmt('H', fp)[0]
        return cls(color, opacity)

    def write(self, fp, **kwargs):
        written = self.color.write(fp)
        written += write_fmt(fp, 'H', self.opacity)
        return written


@register(Tag.METADATA_SETTING)
class MetadataSettings(ListElement):
    """
    MetadataSettings structure.
    """

    @classmethod
    def read(cls, fp, **kwargs):
        count = read_fmt('I', fp)[0]
        items = []
        for _ in range(count):
            items.append(MetadataSetting.read(fp))
        return cls(items)

    def write(self, fp, **kwargs):
        written = write_fmt(fp, 'I', len(self))
        written += sum(item.write(fp) for item in self)
        return written


@attr.s(repr=False, slots=True)
class MetadataSetting(BaseElement):
    """
    MetadataSetting structure.
    """
    _KNOWN_KEYS = {b'cust', b'cmls', b'extn', b'mlst', b'tmln', b'sgrp'}
    signature = attr.ib(
        default=b'8BIM', type=bytes, repr=False, validator=in_((b'8BIM', ))
    )
    key = attr.ib(default=b'', type=bytes)
    copy_on_sheet = attr.ib(default=False, type=bool)
    data = attr.ib(default=b'', type=bytes)

    @classmethod
    def read(cls, fp, **kwargs):
        signature = read_fmt('4s', fp)[0]
        assert signature == b'8BIM', 'Invalid signature %r' % signature
        key, copy_on_sheet = read_fmt("4s?3x", fp)
        data = read_length_block(fp)
        if key in (b'mdyn', b'sgrp'):
            with io.BytesIO(data) as f:
                data = read_fmt('I', f)[0]
        elif key in cls._KNOWN_KEYS:
            data = DescriptorBlock.frombytes(data, padding=4)
        else:
            message = 'Unknown metadata key %r' % (key)
            logger.warning(message)
            data = data
        return cls(signature, key, copy_on_sheet, data)

    def write(self, fp, **kwargs):
        written = write_fmt(
            fp, '4s4s?3x', self.signature, self.key, self.copy_on_sheet
        )

        def writer(f):
            if hasattr(self.data, 'write'):
                return self.data.write(f, padding=4)
            elif isinstance(self.data, int):
                return write_fmt(fp, 'I', self.data)
            return write_bytes(f, self.data)

        written += write_length_block(fp, writer)
        return written


@register(Tag.PIXEL_SOURCE_DATA2)
@attr.s(repr=False, slots=True)
class PixelSourceData2(ListElement):
    """
    PixelSourceData2 structure.
    """

    @classmethod
    def read(cls, fp, **kwargs):
        items = []
        while is_readable(fp, 8):
            items.append(read_length_block(fp, fmt='Q'))
        return cls(items)

    def write(self, fp, padding=4, **kwargs):
        written = 0
        for item in self:
            written += write_length_block(
                fp, lambda f: write_bytes(f, item), fmt='Q'
            )
        written += write_padding(fp, written, padding)
        return written


@register(Tag.PLACED_LAYER1)
@register(Tag.PLACED_LAYER2)
@attr.s(repr=False, slots=True)
class PlacedLayerData(BaseElement):
    """
    PlacedLayerData structure.
    """
    kind = attr.ib(default=b'plcL', type=bytes)
    version = attr.ib(default=3, type=int, validator=in_((3, )))
    uuid = attr.ib(default='', type=bytes)
    page = attr.ib(default=0, type=int)
    total_pages = attr.ib(default=0, type=int)
    anti_alias = attr.ib(default=0, type=int)
    layer_type = attr.ib(
        default=PlacedLayerType.UNKNOWN,
        converter=PlacedLayerType,
        validator=in_(PlacedLayerType)
    )
    transform = attr.ib(default=(0., ) * 8, type=tuple)
    warp = attr.ib(default=None)

    @classmethod
    def read(cls, fp, **kwargs):
        kind, version = read_fmt('4sI', fp)
        uuid = read_pascal_string(fp, 'macroman', padding=1)
        page, total_pages, anti_alias, layer_type = read_fmt('4I', fp)
        transform = read_fmt('8d', fp)
        warp = DescriptorBlock2.read(fp, padding=1)
        return cls(
            kind, version, uuid, page, total_pages, anti_alias, layer_type,
            transform, warp
        )

    def write(self, fp, padding=4, **kwargs):
        written = write_fmt(fp, '4sI', self.kind, self.version)
        written += write_pascal_string(fp, self.uuid, 'macroman', padding=1)
        written += write_fmt(
            fp, '4I', self.page, self.total_pages, self.anti_alias,
            self.layer_type.value
        )
        written += write_fmt(fp, '8d', *self.transform)
        written += self.warp.write(fp, padding=1)
        written += write_padding(fp, written, padding)
        return written


@register(Tag.PROTECTED_SETTING)
class ProtectedSetting(IntegerElement):
    """
    ProtectedSetting structure.
    """

    @property
    def transparency(self):
        return bool(self.value & 0x01)

    @property
    def composite(self):
        return bool(self.value & 0x02)

    @property
    def position(self):
        return bool(self.value & 0x04)


@register(Tag.REFERENCE_POINT)
@attr.s(repr=False, slots=True)
class ReferencePoint(ListElement):
    """
    ReferencePoint structure.
    """

    @classmethod
    def read(cls, fp, **kwargs):
        return cls(list(read_fmt('2d', fp)))

    def write(self, fp, **kwargs):
        return write_fmt(fp, '2d', *self._items)


@register(Tag.SECTION_DIVIDER_SETTING)
@register(Tag.NESTED_SECTION_DIVIDER_SETTING)
@attr.s(repr=False, slots=True)
class SectionDividerSetting(BaseElement):
    """
    SectionDividerSetting structure.

    .. py:attribute:: kind
    .. py:attribute:: blend_mode
    .. py:attribute:: sub_type
    """
    kind = attr.ib(
        default=SectionDivider.OTHER,
        converter=SectionDivider,
        validator=in_(SectionDivider)
    )
    signature = attr.ib(default=None, repr=False)
    blend_mode = attr.ib(default=None)
    sub_type = attr.ib(default=None)

    @classmethod
    def read(cls, fp, **kwargs):
        kind = SectionDivider(read_fmt('I', fp)[0])
        signature, blend_mode = None, None
        if is_readable(fp, 8):
            signature = read_fmt('4s', fp)[0]
            assert signature == b'8BIM', 'Invalid signature %r' % signature
            blend_mode = BlendMode(read_fmt('4s', fp)[0])
        sub_type = None
        if is_readable(fp, 4):
            sub_type = read_fmt('I', fp)[0]
        return cls(
            kind,
            signature=signature,
            blend_mode=blend_mode,
            sub_type=sub_type
        )

    def write(self, fp, **kwargs):
        written = write_fmt(fp, 'I', self.kind.value)
        if self.signature and self.blend_mode:
            written += write_fmt(
                fp, '4s4s', self.signature, self.blend_mode.value
            )
            if self.sub_type is not None:
                written += write_fmt(fp, 'I', self.sub_type)
        return written


@register(Tag.SHEET_COLOR_SETTING)
@attr.s(repr=False, slots=True, eq=False, order=False)
class SheetColorSetting(ValueElement):
    """
    SheetColorSetting value.

    This setting represents color label in the layers panel in Photoshop UI.

    .. py:attribute:: value
    """
    value = attr.ib(default=0, converter=SheetColorType, type=SheetColorType)

    @classmethod
    def read(cls, fp, **kwargs):
        return cls(SheetColorType(*read_fmt('H6x', fp)))

    def write(self, fp, **kwargs):
        return write_fmt(fp, 'H6x', self.value.value)


@register(Tag.SMART_OBJECT_LAYER_DATA1)
@register(Tag.SMART_OBJECT_LAYER_DATA2)
@attr.s(repr=False, slots=True)
class SmartObjectLayerData(BaseElement):
    """
    VersionedDescriptorBlock structure.

    .. py:attribute:: kind
    .. py:attribute:: version
    .. py:attribute:: data
    """
    kind = attr.ib(default=b'soLD', type=bytes, validator=in_((b'soLD', )))
    version = attr.ib(default=5, type=int, validator=in_((4, 5)))
    data = attr.ib(default=None, type=DescriptorBlock)

    @classmethod
    def read(cls, fp, **kwargs):
        kind, version = read_fmt('4sI', fp)
        data = DescriptorBlock.read(fp)
        return cls(kind, version, data)

    def write(self, fp, padding=4, **kwargs):
        written = write_fmt(fp, '4sI', self.kind, self.version)
        written += self.data.write(fp, padding=1)
        written += write_padding(fp, written, padding)
        return written


@register(Tag.TYPE_TOOL_OBJECT_SETTING)
@attr.s(repr=False, slots=True)
class TypeToolObjectSetting(BaseElement):
    """
    TypeToolObjectSetting structure.

    .. py:attribute:: version
    .. py:attribute:: transform

        Tuple of affine transform parameters (xx, xy, yx, yy, tx, ty).

    .. py:attribute:: text_version
    .. py:attribute:: text_data
    .. py:attribute:: warp_version
    .. py:attribute:: warp
    .. py:attribute:: left
    .. py:attribute:: top
    .. py:attribute:: right
    .. py:attribute:: bottom
    """
    version = attr.ib(default=1, type=int)
    transform = attr.ib(default=(0., ) * 6, type=tuple)
    text_version = attr.ib(default=1, type=int, validator=in_((50, )))
    text_data = attr.ib(default=None, type=DescriptorBlock)
    warp_version = attr.ib(default=1, type=int, validator=in_((1, )))
    warp = attr.ib(default=None, type=DescriptorBlock)
    left = attr.ib(default=0, type=int)
    top = attr.ib(default=0, type=int)
    right = attr.ib(default=0, type=int)
    bottom = attr.ib(default=0, type=int)

    @classmethod
    def read(cls, fp, **kwargs):
        version = read_fmt('H', fp)[0]
        transform = read_fmt('6d', fp)
        text_version = read_fmt('H', fp)[0]
        text_data = DescriptorBlock.read(fp)
        # Engine data.
        if b'EngineData' in text_data:
            try:
                engine_data = text_data[b'EngineData'].value
                engine_data = EngineData.frombytes(engine_data)
                text_data[b'EngineData'].value = engine_data
            except Exception:
                logger.warning('Failed to read engine data')
        warp_version = read_fmt('H', fp)[0]
        warp = DescriptorBlock.read(fp)
        left, top, right, bottom = read_fmt("4i", fp)
        return cls(
            version, transform, text_version, text_data, warp_version, warp,
            left, top, right, bottom
        )

    def write(self, fp, padding=4, **kwargs):
        written = write_fmt(fp, 'H6d', self.version, *self.transform)
        written += write_fmt(fp, 'H', self.text_version)
        written += self.text_data.write(fp, padding=1)
        written += write_fmt(fp, 'H', self.warp_version)
        written += self.warp.write(fp, padding=1)
        written += write_fmt(
            fp, '4i', self.left, self.top, self.right, self.bottom
        )
        written += write_padding(fp, written, padding)
        return written


@register(Tag.USER_MASK)
@attr.s(repr=False, slots=True)
class UserMask(BaseElement):
    """
    UserMask structure.

    .. py:attribute:: color
    .. py:attribute:: opacity
    .. py:attribute:: flag
    """
    color = attr.ib(default=None)
    opacity = attr.ib(default=0, type=int)
    flag = attr.ib(default=128, type=int)

    @classmethod
    def read(cls, fp, **kwargs):
        color = Color.read(fp)
        opacity, flag = read_fmt('HBx', fp)
        return cls(color, opacity, flag)

    def write(self, fp, **kwargs):
        written = self.color.write(fp)
        written += write_fmt(fp, 'HBx', self.opacity, self.flag)
        return written
