"""
Serialization templates for structures used in LLUDP and HTTP bodies.
"""

import abc
import collections
import copy
import dataclasses
import datetime
import enum
import math
import zlib
from typing import *

import hippolyzer.lib.base.serialization as se
from hippolyzer.lib.base import llsd
from hippolyzer.lib.base.datatypes import UUID, IntEnum, IntFlag, Vector3, Quaternion
from hippolyzer.lib.base.namevalue import NameValuesSerializer


@se.enum_field_serializer("RequestXfer", "XferID", "VFileType")
@se.enum_field_serializer("AssetUploadRequest", "AssetBlock", "Type")
@se.enum_field_serializer("AssetUploadComplete", "AssetBlock", "Type")
@se.enum_field_serializer("UpdateCreateInventoryItem", "InventoryData", "Type")
@se.enum_field_serializer("CreateInventoryItem", "InventoryBlock", "Type")
@se.enum_field_serializer("RezObject", "InventoryData", "Type")
@se.enum_field_serializer("RezScript", "InventoryBlock", "Type")
@se.enum_field_serializer("UpdateTaskInventory", "InventoryData", "Type")
class AssetType(IntEnum):
    TEXTURE = 0
    SOUND = 1
    CALLINGCARD = 2
    LANDMARK = 3
    SCRIPT = 4
    CLOTHING = 5
    OBJECT = 6
    NOTECARD = 7
    CATEGORY = 8
    LSL_TEXT = 10
    LSL_BYTECODE = 11
    TEXTURE_TGA = 12
    BODYPART = 13
    SOUND_WAV = 17
    IMAGE_TGA = 18
    IMAGE_JPEG = 19
    ANIMATION = 20
    GESTURE = 21
    SIMSTATE = 22
    LINK = 24
    LINK_FOLDER = 25
    MARKETPLACE_FOLDER = 26
    WIDGET = 40
    PERSON = 45
    MESH = 49
    RESERVED_1 = 50
    RESERVED_2 = 51
    RESERVED_3 = 52
    RESERVED_4 = 53
    RESERVED_5 = 54
    RESERVED_6 = 55
    SETTINGS = 56
    UNKNOWN = 255
    NONE = -1

    @property
    def human_name(self):
        lower = self.name.lower()
        return {
            "animation": "animatn",
            "callingcard": "callcard",
            "texture_tga": "txtr_tga",
            "image_tga": "img_tga",
            "sound_wav": "snd_wav",
        }.get(lower, lower)

    @property
    def inventory_type(self):
        return {
            AssetType.TEXTURE: InventoryType.TEXTURE,
            AssetType.SOUND: InventoryType.SOUND,
            AssetType.CALLINGCARD: InventoryType.CALLINGCARD,
            AssetType.LANDMARK: InventoryType.LANDMARK,
            AssetType.SCRIPT: InventoryType.LSL,
            AssetType.CLOTHING: InventoryType.WEARABLE,
            AssetType.OBJECT: InventoryType.OBJECT,
            AssetType.NOTECARD: InventoryType.NOTECARD,
            AssetType.CATEGORY: InventoryType.CATEGORY,
            AssetType.LSL_TEXT: InventoryType.LSL,
            AssetType.LSL_BYTECODE: InventoryType.LSL,
            AssetType.TEXTURE_TGA: InventoryType.TEXTURE,
            AssetType.BODYPART: InventoryType.WEARABLE,
            AssetType.SOUND_WAV: InventoryType.SOUND,
            AssetType.ANIMATION: InventoryType.ANIMATION,
            AssetType.GESTURE: InventoryType.GESTURE,
            AssetType.WIDGET: InventoryType.WIDGET,
            AssetType.PERSON: InventoryType.PERSON,
            AssetType.MESH: InventoryType.MESH,
            AssetType.SETTINGS: InventoryType.SETTINGS,
        }.get(self, AssetType.NONE)


@se.enum_field_serializer("UpdateCreateInventoryItem", "InventoryData", "InvType")
@se.enum_field_serializer("CreateInventoryItem", "InventoryBlock", "InvType")
@se.enum_field_serializer("RezObject", "InventoryData", "InvType")
@se.enum_field_serializer("RezScript", "InventoryBlock", "InvType")
@se.enum_field_serializer("UpdateTaskInventory", "InventoryData", "InvType")
class InventoryType(IntEnum):
    TEXTURE = 0
    SOUND = 1
    CALLINGCARD = 2
    LANDMARK = 3
    SCRIPT = 4
    CLOTHING = 5
    OBJECT = 6
    NOTECARD = 7
    CATEGORY = 8
    ROOT_CATEGORY = 9
    LSL = 10
    LSL_BYTECODE = 11
    TEXTURE_TGA = 12
    BODYPART = 13
    TRASH = 14
    SNAPSHOT = 15
    LOST_AND_FOUND = 16
    ATTACHMENT = 17
    WEARABLE = 18
    ANIMATION = 19
    GESTURE = 20
    MESH = 22
    WIDGET = 23
    PERSON = 24
    SETTINGS = 25
    UNKNOWN = 255
    NONE = -1

    @property
    def human_name(self):
        lower = self.name.lower()
        return {
            "callingcard": "callcard",
            "none": "-1",
        }.get(lower, lower)


class FolderType(IntEnum):
    TEXTURE = 0
    SOUND = 1
    CALLINGCARD = 2
    LANDMARK = 3
    CLOTHING = 5
    OBJECT = 6
    NOTECARD = 7
    # We'd really like to change this to 9 since AT_CATEGORY is 8,
    # but "My Inventory" has been type 8 for a long time.
    ROOT_INVENTORY = 8
    LSL_TEXT = 10
    BODYPART = 13
    TRASH = 14
    SNAPSHOT_CATEGORY = 15
    LOST_AND_FOUND = 16
    ANIMATION = 20
    GESTURE = 21
    FAVORITE = 23
    ENSEMBLE_START = 26
    ENSEMBLE_END = 45
    # This range is reserved for special clothing folder types.
    CURRENT_OUTFIT = 46
    OUTFIT = 47
    MY_OUTFITS = 48
    MESH = 49
    # "received items" for MP
    INBOX = 50
    OUTBOX = 51
    BASIC_ROOT = 52
    MARKETPLACE_LISTINGS = 53
    MARKETPLACE_STOCK = 54
    # Note: We actually *never* create folders with that type. This is used for icon override only.
    MARKETPLACE_VERSION = 55
    SETTINGS = 56
    # Firestorm folders, may not actually exist
    FIRESTORM = 57
    PHOENIX = 58
    RLV = 59
    # Opensim folders
    MY_SUITCASE = 100
    NONE = -1


@se.enum_field_serializer("AgentIsNowWearing", "WearableData", "WearableType")
@se.enum_field_serializer("AgentWearablesUpdate", "WearableData", "WearableType")
@se.enum_field_serializer("CreateInventoryItem", "InventoryBlock", "WearableType")
class WearableType(IntEnum):
    SHAPE = 0
    SKIN = 1
    HAIR = 2
    EYES = 3
    SHIRT = 4
    PANTS = 5
    SHOES = 6
    SOCKS = 7
    JACKET = 8
    GLOVES = 9
    UNDERSHIRT = 10
    UNDERPANTS = 11
    SKIRT = 12
    ALPHA = 13
    TATTOO = 14
    PHYSICS = 15
    UNIVERSAL = 16


def _register_permissions_flags(message_name, block_name):
    def _wrapper(flag_cls):
        for flag_type in {"EveryoneMask", "BaseMask", "OwnerMask", "GroupMask", "NextOwnerMask"}:
            se.flag_field_serializer(message_name, block_name, flag_type)(flag_cls)
        return flag_cls
    return _wrapper


@se.flag_field_serializer("ObjectPermissions", "ObjectData", "Mask")
@_register_permissions_flags("ObjectProperties", "ObjectData")
@_register_permissions_flags("ObjectPropertiesFamily", "ObjectData")
@_register_permissions_flags("UpdateCreateInventoryItem", "InventoryData")
@_register_permissions_flags("UpdateTaskInventory", "InventoryData")
@_register_permissions_flags("CreateInventoryItem", "InventoryBlock")
@_register_permissions_flags("RezObject", "RezData")
@_register_permissions_flags("RezObject", "InventoryData")
@_register_permissions_flags("RezScript", "InventoryBlock")
@_register_permissions_flags("RezMultipleAttachmentsFromInv", "ObjectData")
class Permissions(IntFlag):
    TRANSFER = (1 << 13)
    MODIFY = (1 << 14)
    COPY = (1 << 15)
    # OpenSim export permission, per Firestorm. Deprecated parcel entry flag
    EXPORT_OR_PARCEL_ENTER = (1 << 16)
    # parcels, allow terraform, deprecated
    TERRAFORM = (1 << 17)
    # deprecated
    OWNER_DEBIT = (1 << 18)
    # objects, can grab/translate/rotate
    MOVE = (1 << 19)
    # parcels, avatars take damage, deprecated
    DAMAGE = (1 << 20)
    RESERVED = 1 << 31


@se.enum_field_serializer("ObjectSaleInfo", "ObjectData", "SaleType")
@se.enum_field_serializer("ObjectProperties", "ObjectData", "SaleType")
@se.enum_field_serializer("ObjectPropertiesFamily", "ObjectData", "SaleType")
@se.enum_field_serializer("ObjectBuy", "ObjectData", "SaleType")
@se.enum_field_serializer("RezScript", "InventoryBlock", "SaleType")
@se.enum_field_serializer("RezObject", "InventoryData", "SaleType")
@se.enum_field_serializer("UpdateTaskInventory", "InventoryData", "SaleType")
@se.enum_field_serializer("UpdateCreateInventoryItem", "InventoryData", "SaleType")
class SaleInfo(IntEnum):
    NOT = 0
    ORIGINAL = 1
    COPY = 2
    CONTENTS = 3


@se.flag_field_serializer("ParcelInfoReply", "Data", "Flags")
class ParcelInfoFlags(IntFlag):
    MATURE = 1 << 0
    # You should never see adult without mature
    ADULT = 1 << 1
    GROUP_OWNED = 1 << 2


@se.flag_field_serializer("MapItemRequest", "AgentData", "Flags")
@se.flag_field_serializer("MapNameRequest", "AgentData", "Flags")
@se.flag_field_serializer("MapBlockRequest", "AgentData", "Flags")
@se.flag_field_serializer("MapItemReply", "AgentData", "Flags")
@se.flag_field_serializer("MapNameReply", "AgentData", "Flags")
@se.flag_field_serializer("MapBlockReply", "AgentData", "Flags")
class MapImageFlags(IntFlag):
    # No clue, honestly. I guess there's potentially different image types you could request.
    LAYER = 1 << 1


@se.enum_field_serializer("MapBlockReply", "Data", "Access")
@se.enum_field_serializer("RegionInfo", "RegionInfo", "SimAccess")
class SimAccess(IntEnum):
    # Treated as 'unknown', usually ends up being SIM_ACCESS_PG
    MIN = 0
    PG = 13
    MATURE = 21
    ADULT = 42
    DOWN = 254


@se.enum_field_serializer("MapItemRequest", "RequestData", "ItemType")
@se.enum_field_serializer("MapItemReply", "RequestData", "ItemType")
class MapItemType(IntEnum):
    TELEHUB = 0x01
    PG_EVENT = 0x02
    MATURE_EVENT = 0x03
    # No longer supported, 2009-03-02 KLW
    DEPRECATED_POPULAR = 0x04
    DEPRECATED_AGENT_COUNT = 0x05
    AGENT_LOCATIONS = 0x06
    LAND_FOR_SALE = 0x07
    CLASSIFIED = 0x08
    ADULT_EVENT = 0x09
    LAND_FOR_SALE_ADULT = 0x0a


@se.flag_field_serializer("RezObject", "RezData", "ItemFlags")
@se.flag_field_serializer("RezMultipleAttachmentsFromInv", "ObjectData", "ItemFlags")
@se.flag_field_serializer("RezObject", "InventoryData", "Flags")
@se.flag_field_serializer("RezScript", "InventoryBlock", "Flags")
@se.flag_field_serializer("UpdateCreateInventoryItem", "InventoryData", "Flags")
@se.flag_field_serializer("UpdateTaskInventory", "InventoryData", "Flags")
@se.flag_field_serializer("ChangeInventoryItemFlags", "InventoryData", "Flags")
class InventoryItemFlags(IntFlag):
    # The asset has only one reference in the system. If the
    # inventory item is deleted, or the assetid updated, then we
    # can remove the old reference.
    SHARED_SINGLE_REFERENCE = 0x40000000
    # Object permissions should have next owner perm be more
    # restrictive on rez. We bump this into the second byte of the
    # flags since the low byte is used to track attachment points.
    OBJECT_SLAM_PERM = 0x100
    # The object sale information has been changed.
    OBJECT_SLAM_SALE = 0x1000
    # Specify which permissions masks to overwrite
    # upon rez.  Normally, if no permissions slam (above) or
    # overwrite flags are set, the asset's permissions are
    # used and the inventory's permissions are ignored.  If
    # any of these flags are set, the inventory's permissions
    # take precedence.
    OBJECT_PERM_OVERWRITE_BASE = 0x010000
    OBJECT_PERM_OVERWRITE_OWNER = 0x020000
    OBJECT_PERM_OVERWRITE_GROUP = 0x040000
    OBJECT_PERM_OVERWRITE_EVERYONE = 0x080000
    OBJECT_PERM_OVERWRITE_NEXT_OWNER = 0x100000
    # Whether a returned object is composed of multiple items.
    OBJECT_HAS_MULTIPLE_ITEMS = 0x200000

    @property
    def subtype(self):
        """Subtype of the given item type, could be an attachment point or setting type, etc."""
        return self & 0xFF


@se.enum_field_serializer("ObjectPermissions", "ObjectData", "Field")
class PermissionType(IntEnum):
    BASE = 0x01
    OWNER = 0x02
    GROUP = 0x04
    EVERYONE = 0x08
    NEXT_OWNER = 0x10


@se.enum_field_serializer("TransferRequest", "TransferInfo", "SourceType")
class TransferSourceType(IntEnum):
    UNKNOWN = 0
    FILE = 1
    ASSET = 2
    SIM_INV_ITEM = 3
    SIM_ESTATE = 4


class EstateAssetType(IntEnum):
    NONE = -1
    COVENANT = 0


@dataclasses.dataclass
class TransferRequestParamsBase(abc.ABC):
    pass


@dataclasses.dataclass
class TransferRequestParamsFile(TransferRequestParamsBase):
    FileName: str = se.dataclass_field(se.CStr())
    Delete: bool = se.dataclass_field(se.BOOL)


@dataclasses.dataclass
class TransferRequestParamsAsset(TransferRequestParamsFile):
    AssetID: UUID = se.dataclass_field(se.UUID)
    AssetType: "AssetType" = se.dataclass_field(se.IntEnum(AssetType, se.U32))


@dataclasses.dataclass
class TransferRequestParamsSimInvItem(TransferRequestParamsBase):
    # Will never be None on the wire, just set this way so
    # code can fill them in when creating these ourselves.
    AgentID: Optional[UUID] = se.dataclass_field(se.UUID, default=None)
    SessionID: Optional[UUID] = se.dataclass_field(se.UUID, default=None)
    OwnerID: UUID = se.dataclass_field(se.UUID)
    TaskID: UUID = se.dataclass_field(se.UUID)
    ItemID: UUID = se.dataclass_field(se.UUID)
    AssetID: UUID = se.dataclass_field(se.UUID)
    AssetType: "AssetType" = se.dataclass_field(se.IntEnum(AssetType, se.U32))


@dataclasses.dataclass
class TransferRequestParamsSimEstate(TransferRequestParamsBase):
    # See above RE Optional
    AgentID: Optional[UUID] = se.dataclass_field(se.UUID, default=None)
    SessionID: Optional[UUID] = se.dataclass_field(se.UUID, default=None)
    EstateAssetType: "EstateAssetType" = se.dataclass_field(se.IntEnum(EstateAssetType, se.U32))


@se.subfield_serializer("TransferRequest", "TransferInfo", "Params")
class TransferParamsSerializer(se.EnumSwitchedSubfieldSerializer):
    ENUM_FIELD = "SourceType"
    TEMPLATES = {
        TransferSourceType.FILE: se.Dataclass(TransferRequestParamsFile),
        TransferSourceType.ASSET: se.Dataclass(TransferRequestParamsAsset),
        TransferSourceType.SIM_INV_ITEM: se.Dataclass(TransferRequestParamsSimInvItem),
        TransferSourceType.SIM_ESTATE: se.Dataclass(TransferRequestParamsSimEstate),
    }


@se.enum_field_serializer("TransferAbort", "TransferInfo", "ChannelType")
@se.enum_field_serializer("TransferPacket", "TransferData", "ChannelType")
@se.enum_field_serializer("TransferRequest", "TransferInfo", "ChannelType")
@se.enum_field_serializer("TransferInfo", "TransferInfo", "ChannelType")
class TransferChannelType(IntEnum):
    UNKNOWN = 0
    MISC = 1
    ASSET = 2


@se.enum_field_serializer("TransferInfo", "TransferInfo", "TargetType")
class TransferTargetType(IntEnum):
    UNKNOWN = 0
    FILE = 1
    VFILE = 2


@se.enum_field_serializer("TransferInfo", "TransferInfo", "Status")
@se.enum_field_serializer("TransferPacket", "TransferData", "Status")
class TransferStatus(IntEnum):
    OK = 0
    DONE = 1
    SKIP = 2
    ABORT = 3
    ERROR = -1  # generic error
    UNKNOWN_SOURCE = -2  # not found
    INSUFFICIENT_PERMISSIONS = -3


@se.subfield_serializer("TransferInfo", "TransferInfo", "Params")
class TransferInfoSerializer(se.BaseSubfieldSerializer):
    TEMPLATES = {
        TransferTargetType.FILE: se.Template({
            "FileName": se.CStr(),
            "Delete": se.BOOL,
        }),
        TransferTargetType.VFILE: se.Template({
            "AgentID": se.UUID,
            "SessionID": se.UUID,
            "OwnerID": se.UUID,
            "TaskID": se.UUID,
            "ItemID": se.UUID,
            "AssetID": se.UUID,
            "AssetType": se.IntEnum(AssetType, se.U32),
        }),
    }

    @classmethod
    def _get_target_template(cls, block, val):
        target_type = block["TargetType"]
        if target_type != TransferTargetType.UNKNOWN:
            return cls.TEMPLATES[target_type]

        # Hard to tell what format the payload uses without tracking
        # which request this info message corresponds to. Brute force it.
        templates = TransferParamsSerializer.TEMPLATES.values()
        return cls._fuzzy_guess_template(templates, val)

    @classmethod
    def deserialize(cls, ctx_obj, val, pod=False):
        if not val:
            return se.UNSERIALIZABLE
        template = cls._get_target_template(ctx_obj, val)
        if not template:
            return se.UNSERIALIZABLE
        return cls._deserialize_template(template, val, pod)

    @classmethod
    def serialize(cls, ctx_obj, vals):
        template = cls._get_target_template(ctx_obj, vals)
        if not template:
            return se.UNSERIALIZABLE
        return cls._serialize_template(template, vals)


@se.enum_field_serializer("RequestXfer", "XferID", "FilePath")
class XferFilePath(IntEnum):
    NONE = 0
    USER_SETTINGS = 1
    APP_SETTINGS = 2
    PER_SL_ACCOUNT = 3
    CACHE = 4
    CHARACTER = 5
    HELP = 6
    LOGS = 7
    TEMP = 8
    SKINS = 9
    TOP_SKIN = 10
    CHAT_LOGS = 11
    PER_ACCOUNT_CHAT_LOGS = 12
    USER_SKIN = 14
    LOCAL_ASSETS = 15
    EXECUTABLE = 16
    DEFAULT_SKIN = 17
    FONTS = 18
    DUMP = 19


@se.enum_field_serializer("AbortXfer", "XferID", "Result")
class XferError(IntEnum):
    FILE_EMPTY = -44
    FILE_NOT_FOUND = -43
    CANNOT_OPEN_FILE = -42
    EOF = -39


@dataclasses.dataclass
class XferPacket(se.BitfieldDataclass):
    PacketID: int = se.bitfield_field(bits=31)
    IsEOF: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter())


@se.subfield_serializer("SendXferPacket", "XferID", "Packet")
class SendXferPacketIDSerializer(se.AdapterSubfieldSerializer):
    ORIG_INLINE = True
    ADAPTER = se.BitfieldDataclass(XferPacket)


@se.enum_field_serializer("ViewerEffect", "Effect", "Type")
class ViewerEffectType(IntEnum):
    TEXT = 0
    ICON = 1
    CONNECTOR = 2
    FLEXIBLE_OBJECT = 3
    ANIMAL_CONTROLS = 4
    LOCAL_ANIMATION_OBJECT = 5
    CLOTH = 6
    EFFECT_BEAM = 7
    EFFECT_GLOW = 8
    EFFECT_POINT = 9
    EFFECT_TRAIL = 10
    EFFECT_SPHERE = 11
    EFFECT_SPIRAL = 12
    EFFECT_EDIT = 13
    EFFECT_LOOKAT = 14
    EFFECT_POINTAT = 15
    EFFECT_VOICE_VISUALIZER = 16
    NAME_TAG = 17
    EFFECT_BLOB = 18


class LookAtTarget(IntEnum):
    NONE = 0
    IDLE = 1
    AUTO_LISTEN = 2
    FREELOOK = 3
    RESPOND = 4
    HOVER = 5
    CONVERSATION = 6
    SELECT = 7
    FOCUS = 8
    MOUSELOOK = 9
    CLEAR = 10


class PointAtTarget(IntEnum):
    NONE = 0
    SELECT = 1
    GRAB = 2
    CLEAR = 3


@se.subfield_serializer("ViewerEffect", "Effect", "TypeData")
class ViewerEffectDataSerializer(se.EnumSwitchedSubfieldSerializer):
    ENUM_FIELD = "Type"
    # A lot of effect types that seem to have their own payload
    # actually use spiral's. Weird.
    SPIRAL_TEMPLATE = se.Template({
        "SourceID": se.UUID,
        "TargetID": se.UUID,
        "TargetPos": se.Vector3D,
    })
    TEMPLATES = {
        ViewerEffectType.EFFECT_POINTAT: se.Template({
            "SourceID": se.UUID,
            "TargetID": se.UUID,
            "TargetPos": se.Vector3D,
            "PointTargetType": se.IntEnum(PointAtTarget, se.U8),
        }),
        ViewerEffectType.EFFECT_BEAM: SPIRAL_TEMPLATE,
        ViewerEffectType.EFFECT_EDIT: SPIRAL_TEMPLATE,
        ViewerEffectType.EFFECT_SPHERE: SPIRAL_TEMPLATE,
        ViewerEffectType.EFFECT_POINT: SPIRAL_TEMPLATE,
        ViewerEffectType.EFFECT_LOOKAT: se.Template({
            "SourceID": se.UUID,
            "TargetID": se.UUID,
            "TargetPos": se.Vector3D,
            "LookTargetType": se.IntEnum(LookAtTarget, se.U8),
        }),
        ViewerEffectType.EFFECT_SPIRAL: SPIRAL_TEMPLATE,
    }


@se.enum_field_serializer("MoneyTransferRequest", "MoneyData", "TransactionType")
@se.enum_field_serializer("MoneyBalanceReply", "TransactionInfo", "TransactionType")
class MoneyTransactionType(IntEnum):
    # _many_ of these codes haven't been used in decades.
    # Money transaction failure codes
    NULL = 0
    # Error conditions
    FAIL_SIMULATOR_TIMEOUT = 1
    FAIL_DATASERVER_TIMEOUT = 2
    FAIL_APPLICATION = 3
    # Everything past this is actual transaction types
    OBJECT_CLAIM = 1000
    LAND_CLAIM = 1001
    GROUP_CREATE = 1002
    OBJECT_PUBLIC_CLAIM = 1003
    GROUP_JOIN = 1004
    TELEPORT_CHARGE = 1100
    UPLOAD_CHARGE = 1101
    LAND_AUCTION = 1102
    CLASSIFIED_CHARGE = 1103
    OBJECT_TAX = 2000
    LAND_TAX = 2001
    LIGHT_TAX = 2002
    PARCEL_DIR_FEE = 2003
    GROUP_TAX = 2004
    CLASSIFIED_RENEW = 2005
    RECURRING_GENERIC = 2100
    GIVE_INVENTORY = 3000
    OBJECT_SALE = 5000
    GIFT = 5001
    LAND_SALE = 5002
    REFER_BONUS = 5003
    INVENTORY_SALE = 5004
    REFUND_PURCHASE = 5005
    LAND_PASS_SALE = 5006
    DWELL_BONUS = 5007
    PAY_OBJECT = 5008
    OBJECT_PAYS = 5009
    RECURRING_GENERIC_USER = 5100
    GROUP_JOIN_RESERVED = 6000
    GROUP_LAND_DEED = 6001
    GROUP_OBJECT_DEED = 6002
    GROUP_LIABILITY = 6003
    GROUP_DIVIDEND = 6004
    MEMBERSHIP_DUES = 6005
    OBJECT_RELEASE = 8000
    LAND_RELEASE = 8001
    OBJECT_DELETE = 8002
    OBJECT_PUBLIC_DECAY = 8003
    OBJECT_PUBLIC_DELETE = 8004
    LINDEN_ADJUSTMENT = 9000
    LINDEN_GRANT = 9001
    LINDEN_PENALTY = 9002
    EVENT_FEE = 9003
    EVENT_PRIZE = 9004
    STIPEND_BASIC = 10000
    STIPEND_DEVELOPER = 10001
    STIPEND_ALWAYS = 10002
    STIPEND_DAILY = 10003
    STIPEND_RATING = 10004
    STIPEND_DELTA = 10005


@se.flag_field_serializer("MoneyTransferRequest", "MoneyData", "Flags")
class MoneyTransactionFlags(IntFlag):
    SOURCE_GROUP = 1
    DEST_GROUP = 1 << 1
    OWNER_GROUP = 1 << 2
    SIMULTANEOUS_CONTRIBUTION = 1 << 3
    SIMULTANEOUS_CONTRIBUTION_REMOVAL = 1 << 4


@se.enum_field_serializer("ImprovedInstantMessage", "MessageBlock", "Dialog")
class IMDialogType(IntEnum):
    NOTHING_SPECIAL = 0
    MESSAGEBOX = 1
    GROUP_INVITATION = 3
    INVENTORY_OFFERED = 4
    INVENTORY_ACCEPTED = 5
    INVENTORY_DECLINED = 6
    GROUP_VOTE = 7
    GROUP_MESSAGE_DEPRECATED = 8
    TASK_INVENTORY_OFFERED = 9
    TASK_INVENTORY_ACCEPTED = 10
    TASK_INVENTORY_DECLINED = 11
    NEW_USER_DEFAULT = 12
    SESSION_INVITE = 13
    SESSION_P2P_INVITE = 14
    SESSION_GROUP_START = 15
    SESSION_CONFERENCE_START = 16
    SESSION_SEND = 17
    SESSION_LEAVE = 18
    FROM_TASK = 19
    DO_NOT_DISTURB_AUTO_RESPONSE = 20
    CONSOLE_AND_CHAT_HISTORY = 21
    LURE_USER = 22
    LURE_ACCEPTED = 23
    LURE_DECLINED = 24
    GODLIKE_LURE_USER = 25
    TELEPORT_REQUEST = 26
    GROUP_ELECTION_DEPRECATED = 27
    GOTO_URL = 28
    FROM_TASK_AS_ALERT = 31
    GROUP_NOTICE = 32
    GROUP_NOTICE_INVENTORY_ACCEPTED = 33
    GROUP_NOTICE_INVENTORY_DECLINED = 34
    GROUP_INVITATION_ACCEPT = 35
    GROUP_INVITATION_DECLINE = 36
    GROUP_NOTICE_REQUESTED = 37
    FRIENDSHIP_OFFERED = 38
    FRIENDSHIP_ACCEPTED = 39
    FRIENDSHIP_DECLINED_DEPRECATED = 40
    TYPING_START = 41
    TYPING_STOP = 42


@se.subfield_serializer("ImprovedInstantMessage", "MessageBlock", "BinaryBucket")
class InstantMessageBucketSerializer(se.EnumSwitchedSubfieldSerializer):
    ENUM_FIELD = "Dialog"
    # Aliases for clarity
    EMPTY_BUCKET = se.UNSERIALIZABLE
    PLAIN_STRING = se.UNSERIALIZABLE

    ACCEPTED_INVENTORY = se.Template({
        "TargetCategoryID": se.UUID,
    })
    # This is in a different, string format if it comes in through the
    # ReadOfflineMsgs Cap but we don't parse those.
    GROUP_NOTICE = se.Template({
        "HasInventory": se.BOOL,
        "AssetType": se.IntEnum(AssetType, se.U8),
        "GroupID": se.UUID,
        "ItemName": se.CStr(),
    })
    TEMPLATES = {
        IMDialogType.SESSION_GROUP_START: EMPTY_BUCKET,
        IMDialogType.INVENTORY_OFFERED: se.Template({
            "AssetType": se.IntEnum(AssetType, se.U8),
            "ItemID": se.UUID,
            # Greedy, no length field.
            "Children": se.Collection(
                None,
                se.Template({
                    "AssetType": se.IntEnum(AssetType, se.U8),
                    "ItemID": se.UUID,
                }),
            ),
        }),
        # Either binary AssetType or serialized string if from when offline. WTF?
        IMDialogType.TASK_INVENTORY_OFFERED: se.Template({
            "AssetType": se.IntEnum(AssetType, se.U8),
        }),
        IMDialogType.TASK_INVENTORY_ACCEPTED: ACCEPTED_INVENTORY,
        IMDialogType.GROUP_NOTICE_INVENTORY_ACCEPTED: ACCEPTED_INVENTORY,
        IMDialogType.INVENTORY_ACCEPTED: ACCEPTED_INVENTORY,
        IMDialogType.INVENTORY_DECLINED: EMPTY_BUCKET,
        IMDialogType.GROUP_NOTICE_INVENTORY_DECLINED: EMPTY_BUCKET,
        IMDialogType.TASK_INVENTORY_DECLINED: EMPTY_BUCKET,
        IMDialogType.NOTHING_SPECIAL: EMPTY_BUCKET,
        IMDialogType.TYPING_START: EMPTY_BUCKET,
        IMDialogType.TYPING_STOP: EMPTY_BUCKET,
        # It's a string, just read it as-is.
        IMDialogType.LURE_USER: PLAIN_STRING,
        IMDialogType.TELEPORT_REQUEST: PLAIN_STRING,
        IMDialogType.GODLIKE_LURE_USER: PLAIN_STRING,
        IMDialogType.SESSION_LEAVE: EMPTY_BUCKET,
        IMDialogType.DO_NOT_DISTURB_AUTO_RESPONSE: EMPTY_BUCKET,
        IMDialogType.FRIENDSHIP_OFFERED: EMPTY_BUCKET,
        IMDialogType.FRIENDSHIP_ACCEPTED: EMPTY_BUCKET,
        IMDialogType.FRIENDSHIP_DECLINED_DEPRECATED: EMPTY_BUCKET,
        IMDialogType.GROUP_NOTICE: GROUP_NOTICE,
        IMDialogType.GROUP_NOTICE_REQUESTED: GROUP_NOTICE,
        IMDialogType.GROUP_INVITATION: se.Template({
            "JoinCost": se.S32,
            "RoleID": se.UUID,
        }),
        # Just a string
        IMDialogType.FROM_TASK: PLAIN_STRING,
        # Session name
        IMDialogType.SESSION_SEND: PLAIN_STRING,
        # What? Is this even used?
        IMDialogType.GOTO_URL: PLAIN_STRING,
    }


@se.subfield_serializer("ObjectUpdate", "ObjectData", "ObjectData")
class ObjectUpdateDataSerializer(se.SimpleSubfieldSerializer):
    # http://wiki.secondlife.com/wiki/ObjectUpdate#ObjectData_Format
    REGION_SIZE = 256.0
    MIN_HEIGHT = -REGION_SIZE
    MAX_HEIGHT = 4096.0

    POSITION_COMPONENT_SCALES = (
        (-0.5 * REGION_SIZE, 1.5 * REGION_SIZE),
        (-0.5 * REGION_SIZE, 1.5 * REGION_SIZE),
        (MIN_HEIGHT, MAX_HEIGHT),
    )

    FLOAT_TEMPLATE = {
        "Position": se.Vector3,
        "Velocity": se.Vector3,
        "Acceleration": se.Vector3,
        "Rotation": se.PackedQuat(se.Vector3),
        "AngularVelocity": se.Vector3,
    }
    HALF_PRECISION_TEMPLATE = {
        "Position": se.Vector3U16(component_scales=POSITION_COMPONENT_SCALES),
        "Velocity": se.Vector3U16(-REGION_SIZE, REGION_SIZE),
        "Acceleration": se.Vector3U16(-REGION_SIZE, REGION_SIZE),
        "Rotation": se.PackedQuat(se.Vector4U16(-1.0, 1.0)),
        "AngularVelocity": se.Vector3U16(-REGION_SIZE, REGION_SIZE),
    }
    LOW_PRECISION_TEMPLATE = {
        "Position": se.Vector3U8(component_scales=POSITION_COMPONENT_SCALES),
        "Velocity": se.Vector3U8(-REGION_SIZE, REGION_SIZE),
        "Acceleration": se.Vector3U8(-REGION_SIZE, REGION_SIZE),
        "Rotation": se.PackedQuat(se.Vector4U8(-1.0, 1.0)),
        "AngularVelocity": se.Vector3U8(-REGION_SIZE, REGION_SIZE),
    }

    TEMPLATE = se.LengthSwitch({
        76: se.Template({"FootCollisionPlane": se.Vector4, **FLOAT_TEMPLATE}),
        60: se.Template(FLOAT_TEMPLATE),
        48: se.Template({"FootCollisionPlane": se.Vector4, **HALF_PRECISION_TEMPLATE}),
        32: se.Template(HALF_PRECISION_TEMPLATE),
        16: se.Template(LOW_PRECISION_TEMPLATE),
    })


@se.enum_field_serializer("ObjectUpdate", "ObjectData", "PCode")
@se.enum_field_serializer("ObjectAdd", "ObjectData", "PCode")
class PCode(IntEnum):
    # Should actually be a bitmask, these are just some common ones.
    PRIMITIVE = 9
    AVATAR = 47
    GRASS = 95
    NEW_TREE = 111
    PARTICLE_SYSTEM = 143
    TREE = 255


@se.enum_field_serializer("ObjectUpdate", "ObjectData", "Material")
@se.enum_field_serializer("ObjectAdd", "ObjectData", "Material")
@se.enum_field_serializer("ObjectMaterial", "ObjectData", "Material")
class MCode(IntEnum):
    # Seems like this is normally stored in a U8 with the high nybble masked off?
    # What's in the high nybble, anything?
    STONE = 0
    METAL = 1
    WOOD = 3
    FLESH = 4
    PLASTIC = 5
    RUBBER = 6
    LIGHT = 7


@se.flag_field_serializer("ObjectUpdate", "ObjectData", "UpdateFlags")
@se.flag_field_serializer("ObjectUpdateCompressed", "ObjectData", "UpdateFlags")
@se.flag_field_serializer("ObjectUpdateCached", "ObjectData", "UpdateFlags")
@se.flag_field_serializer("ObjectAdd", "ObjectData", "AddFlags")
@se.flag_field_serializer("ObjectDuplicate", "SharedData", "DuplicateFlags")
class ObjectUpdateFlags(IntFlag):
    USE_PHYSICS = 1 << 0
    CREATE_SELECTED = 1 << 1
    OBJECT_MODIFY = 1 << 2
    OBJECT_COPY = 1 << 3
    OBJECT_ANY_OWNER = 1 << 4
    OBJECT_YOU_OWNER = 1 << 5
    SCRIPTED = 1 << 6
    HANDLE_TOUCH = 1 << 7
    OBJECT_MOVE = 1 << 8
    TAKES_MONEY = 1 << 9
    PHANTOM = 1 << 10
    INVENTORY_EMPTY = 1 << 11
    AFFECTS_NAVMESH = 1 << 12
    CHARACTER = 1 << 13
    VOLUME_DETECT = 1 << 14
    INCLUDE_IN_SEARCH = 1 << 15
    ALLOW_INVENTORY_DROP = 1 << 16
    OBJECT_TRANSFER = 1 << 17
    OBJECT_GROUP_OWNED = 1 << 18
    OBJECT_YOU_OFFICER_DEPRECATED = 1 << 19
    CAMERA_DECOUPLED = 1 << 20
    ANIM_SOURCE = 1 << 21
    CAMERA_SOURCE = 1 << 22
    CAST_SHADOWS_DEPRECATED = 1 << 23
    UNUSED_002 = 1 << 24
    UNUSED_003 = 1 << 25
    UNUSED_004 = 1 << 26
    UNUSED_005 = 1 << 27
    OBJECT_OWNER_MODIFY = 1 << 28
    TEMPORARY_ON_REZ = 1 << 29
    TEMPORARY_DEPRECATED = 1 << 30
    ZLIB_COMPRESSED_REPRECATED = 1 << 31


JUST_CREATED_FLAGS = (ObjectUpdateFlags.CREATE_SELECTED | ObjectUpdateFlags.OBJECT_YOU_OWNER)


class AttachmentStateAdapter(se.Adapter):
    # Encoded attachment point ID for attached objects
    # nibbles are swapped around because old attachment nums only used to live
    # in the high bits, I guess.
    OFFSET = 4
    MASK = 0xF << OFFSET

    def _rotate_nibbles(self, val: int):
        return ((val & self.MASK) >> self.OFFSET) | ((val & ~self.MASK) << self.OFFSET)

    def decode(self, val: Any, ctx: Optional[se.ParseContext], pod: bool = False) -> Any:
        return self._rotate_nibbles(val)

    def encode(self, val: Any, ctx: Optional[se.ParseContext]) -> Any:
        # f(f(x)) = x
        return self._rotate_nibbles(val)


@se.flag_field_serializer("AgentUpdate", "AgentData", "State")
class AgentState(IntFlag):
    TYPING = 1 << 2
    EDITING = 1 << 4


class ObjectStateAdapter(se.ContextAdapter):
    # State has a different meaning depending on PCode
    def __init__(self, child_spec: Optional[se.SERIALIZABLE_TYPE]):
        super().__init__(
            lambda ctx: ctx.PCode, child_spec, {
                PCode.AVATAR: se.IntFlag(AgentState),
                PCode.PRIMITIVE: AttachmentStateAdapter(None),
                # Other cases are probably just a number (tree species ID or something.)
                se.MISSING: se.IdentityAdapter(),
            }
        )


@se.subfield_serializer("ObjectUpdate", "ObjectData", "State")
@se.subfield_serializer("ObjectAdd", "ObjectData", "State")
class ObjectStateSerializer(se.AdapterSubfieldSerializer):
    ADAPTER = ObjectStateAdapter(None)
    ORIG_INLINE = True


@se.subfield_serializer("ObjectUpdate", "RegionData", "TimeDilation")
@se.subfield_serializer("ObjectUpdateCompressed", "RegionData", "TimeDilation")
@se.subfield_serializer("ObjectUpdateCached", "RegionData", "TimeDilation")
@se.subfield_serializer("ImprovedTerseObjectUpdate", "RegionData", "TimeDilation")
class TimeDilationSerializer(se.AdapterSubfieldSerializer):
    ADAPTER = se.QuantizedFloat(se.U16, 0.0, 1.0, False)
    ORIG_INLINE = True


@se.subfield_serializer("ImprovedTerseObjectUpdate", "ObjectData", "Data")
class ImprovedTerseObjectUpdateDataSerializer(se.SimpleSubfieldSerializer):
    TEMPLATE = se.Template({
        "ID": se.U32,
        # No inline PCode, so can't make sense of State at the message level
        "State": se.U8,
        "FootCollisionPlane": se.OptionalPrefixed(se.Vector4),
        "Position": se.Vector3,
        "Velocity": se.Vector3U16(-128.0, 128.0),
        "Acceleration": se.Vector3U16(-64.0, 64.0),
        "Rotation": se.PackedQuat(se.Vector4U16(-1.0, 1.0)),
        "AngularVelocity": se.Vector3U16(-64.0, 64.0),
    })


class ShineLevel(IntEnum):
    OFF = 0
    LOW = 1
    MEDIUM = 2
    HIGH = 3


@dataclasses.dataclass(unsafe_hash=True)
class BasicMaterials:
    # Meaning is technically implementation-dependent, these are in LL data files
    Bump: int = se.bitfield_field(bits=5, default=0)
    FullBright: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter(), default=False)
    Shiny: int = se.bitfield_field(bits=2, adapter=se.IntEnum(ShineLevel), default=0)


BUMP_SHINY_FULLBRIGHT = se.BitfieldDataclass(BasicMaterials, se.U8)


class TexGen(IntEnum):
    DEFAULT = 0
    PLANAR = 0x2
    # These are unused / not supported
    SPHERICAL = 0x4
    CYLINDRICAL = 0x6


@dataclasses.dataclass(unsafe_hash=True)
class MediaFlags:
    WebPage: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter(), default=False)
    TexGen: "TexGen" = se.bitfield_field(bits=2, adapter=se.IntEnum(TexGen), default=TexGen.DEFAULT)
    # Probably unused but show it just in case
    _Unused: int = se.bitfield_field(bits=5, default=0)


# Not shifted so enum definitions can match indra
MEDIA_FLAGS = se.BitfieldDataclass(MediaFlags, se.U8, shift=False)


class Color4(se.Adapter):
    def __init__(self, invert_bytes=False, invert_alpha=False):
        # There's several different ways of representing colors, presumably
        # to allow for more efficient zerocoding in common cases.
        self.invert_bytes = invert_bytes
        self.invert_alpha = invert_alpha
        super().__init__(se.BytesFixed(4))

    def _invert(self, val: bytes) -> bytes:
        if self.invert_bytes:
            val = bytes(~x & 0xFF for x in val)
        if self.invert_alpha:
            val = val[:3] + bytes((~val[4] & 0xFF,))
        return val

    def encode(self, val: bytes, ctx: Optional[se.ParseContext]) -> bytes:
        return self._invert(val)

    def decode(self, val: bytes, ctx: Optional[se.ParseContext], pod: bool = False) -> bytes:
        return self._invert(val)


class TEFaceBitfield(se.SerializableBase):
    """
    Arbitrary-length bitfield of faces

    0x80 bit indicates bitfield has more bytes. Each byte can represent
    on / off for 7 faces.
    """

    @classmethod
    def deserialize(cls, reader: se.BufferReader, ctx=None):
        have_next = True
        val = 0
        while have_next:
            char = reader.read(se.U8, ctx=ctx)
            have_next = char & 0x80
            val |= char & 0x7F
            if have_next:
                val <<= 7

        # Bitfield of faces reconstructed, convert to tuple
        i = 0
        face_list = []
        while val:
            if val & 1:
                face_list.append(i)
            i += 1
            val >>= 1
        return tuple(face_list)

    @classmethod
    def serialize(cls, faces, writer: se.BufferWriter, ctx=None):
        packed = 0
        for face in faces:
            packed |= 1 << face

        char_arr = []
        while packed:
            # 7 faces per byte
            val = packed & 0x7F
            packed >>= 7
            char_arr.append(val)
        char_arr.reverse()

        while char_arr:
            val = char_arr.pop(0)
            # need a continuation
            if char_arr:
                val |= 0x80
            writer.write(se.U8, val, ctx=ctx)


class TEExceptionField(se.SerializableBase):
    """
    TextureEntry field with a "default" value and trailing "exception" values

    Each value that deviates from the default value is added as an "exception,"
    prefixed with a bitfield of face numbers it applies to. A field is terminated
    with a NUL, so long as it's not the last field. In that case, EOF implicitly
    terminates.
    """

    def __init__(self, spec, optional=False, first=False):
        self._spec = spec
        self._first = first
        self._optional = optional

    def serialize(self, vals, writer: se.BufferWriter, ctx=None):
        if self._optional and not vals:
            return

        # NUL needed to mark the start of a field if this isn't the first one
        if not self._first:
            writer.write_bytes(b"\x00")

        ctx = se.ParseContext(vals, parent=ctx)

        default = vals[None]
        writer.write(self._spec, default, ctx=ctx)
        for faces, val in vals.items():
            if faces is None:
                continue
            writer.write(TEFaceBitfield, faces, ctx=ctx)
            writer.write(self._spec, val, ctx=ctx)

    def deserialize(self, reader: se.BufferReader, ctx=None):
        # No bytes left and this is an optional field
        if self._optional and not reader:
            return None

        # Technically there's nothing preventing an implementation from
        # repeating a face bitfield. You can use a MultiDict to preserve
        # any duplicate keys if you care, but it's incredibly slow.
        vals = {}
        ctx = se.ParseContext(vals, parent=ctx)
        vals[None] = reader.read(self._spec, ctx=ctx)
        while reader:
            faces = reader.read(TEFaceBitfield, ctx=ctx)
            if not faces:
                break
            # Key will be a tuple of face numbers
            vals[faces] = reader.read(self._spec, ctx=ctx)
        return vals

    def default_value(self):
        return dict


_T = TypeVar("_T")
_TE_FIELD_KEY = Optional[Sequence[int]]
_TE_DICT = Dict[_TE_FIELD_KEY, _T]


def _te_field(spec: se.SERIALIZABLE_TYPE, first=False, optional=False,
              default_factory: Union[se.MissingType, Callable[[], _T]] = se.MISSING,
              default: Union[se.MissingType, _T] = se.MISSING):
    if default_factory is not se.MISSING:
        new_default_factory = lambda: {None: default_factory()}
    elif default is not None:
        new_default_factory = lambda: {None: default}
    else:
        new_default_factory = dataclasses.MISSING
    return se.dataclass_field(
        TEExceptionField(spec, first=first, optional=optional),
        default_factory=new_default_factory,
    )


# If this seems weird it's because it is. TE offsets are S16s with `0` as the actual 0
# point, and LL divides by `0x7FFF` to convert back to float. Negative S16s can
# actually go to -0x8000 due to two's complement, creating a larger range for negatives.
TE_S16_COORD = se.QuantizedFloat(se.S16, -1.000030518509476, 1.0, False)


class PackedTERotation(se.QuantizedFloat):
    """Another weird one, packed TE rotations have their own special quantization"""

    def __init__(self):
        super().__init__(se.S16, math.pi * -2, math.pi * 2, zero_median=False)
        self.step_mag = 1.0 / (se.U16.max_val + 1)

    def _float_to_quantized(self, val: float, lower: float, upper: float):
        val = math.fmod(val, upper)
        val = super()._float_to_quantized(val, lower, upper)
        if val == se.S16.max_val + 1:
            val = self.prim_min
        return val


@dataclasses.dataclass
class TextureEntry:
    """Representation of a TE for a single face. Not sent over the wire."""
    Textures: UUID = UUID('89556747-24cb-43ed-920b-47caed15465f')
    Color: bytes = b"\xff\xff\xff\xff"
    ScalesS: float = 1.0
    ScalesT: float = 1.0
    OffsetsS: float = 0.0
    OffsetsT: float = 0.0
    # In radians
    Rotation: float = 0.0
    MediaFlags: "MediaFlags" = dataclasses.field(default_factory=MediaFlags)
    BasicMaterials: "BasicMaterials" = dataclasses.field(default_factory=BasicMaterials)
    Glow: float = 0.0
    Materials: UUID = UUID.ZERO

    def st_to_uv(self, st_coord: Vector3) -> Vector3:
        """Convert OpenGL ST coordinates to UV coordinates, accounting for mapping"""
        uv = Vector3(st_coord.X - 0.5, st_coord.Y - 0.5)
        cos_rot = math.cos(self.Rotation)
        sin_rot = math.sin(self.Rotation)
        uv = Vector3(
            X=uv.X * cos_rot + uv.Y * sin_rot,
            Y=-uv.X * sin_rot + uv.Y * cos_rot
        )
        uv *= Vector3(self.ScalesS, self.ScalesT)
        return uv + Vector3(self.OffsetsS + 0.5, self.OffsetsT + 0.5)


# Max number of TEs possible according to llprimitive (but not really true!)
# Useful if you don't know how many faces / TEs an object really has because it's mesh
# or something.
MAX_TES = 45


@dataclasses.dataclass
class TextureEntryCollection:
    Textures: _TE_DICT[UUID] = _te_field(
        # Plywood texture
        se.UUID, first=True, default=UUID('89556747-24cb-43ed-920b-47caed15465f'))
    # Bytes are inverted so fully opaque white is \x00\x00\x00\x00
    Color: _TE_DICT[bytes] = _te_field(Color4(invert_bytes=True), default=b"\xff\xff\xff\xff")
    ScalesS: _TE_DICT[float] = _te_field(se.F32, default=1.0)
    ScalesT: _TE_DICT[float] = _te_field(se.F32, default=1.0)
    OffsetsS: _TE_DICT[float] = _te_field(TE_S16_COORD, default=0.0)
    OffsetsT: _TE_DICT[float] = _te_field(TE_S16_COORD, default=0.0)
    Rotation: _TE_DICT[float] = _te_field(PackedTERotation(), default=0.0)
    BasicMaterials: _TE_DICT["BasicMaterials"] = _te_field(
        BUMP_SHINY_FULLBRIGHT, default_factory=BasicMaterials,
    )
    MediaFlags: _TE_DICT["MediaFlags"] = _te_field(MEDIA_FLAGS, default_factory=MediaFlags)
    Glow: _TE_DICT[float] = _te_field(se.QuantizedFloat(se.U8, 0.0, 1.0), default=0.0)
    Materials: _TE_DICT[UUID] = _te_field(se.UUID, optional=True, default=UUID.ZERO)

    def unwrap(self):
        """Return `self` regardless of whether this is lazy wrapped object or not"""
        return self

    def realize(self, num_faces: int = MAX_TES) -> List[TextureEntry]:
        """
        Turn the "default" vs "exception cases" wire format TE representation to per-face lookups
        Makes it easier to get all TE details associated with a specific face
        """
        as_dicts = [dict() for _ in range(num_faces)]
        for field in dataclasses.fields(self):
            key = field.name
            vals = getattr(self, key)
            # Fill give all faces the default value for this key
            for te in as_dicts:
                te[key] = copy.copy(vals[None])
            # Walk over the exception cases and replace the default value
            for face_nums, val in vals.items():
                # Default case already handled
                if face_nums is None:
                    continue
                for face_num in face_nums:
                    if face_num >= num_faces:
                        raise ValueError(f"Bad value for num_faces? {face_num} >= {num_faces}")
                    as_dicts[face_num][key] = copy.copy(val)
        return [TextureEntry(**x) for x in as_dicts]

    @classmethod
    def from_tes(cls, tes: List[TextureEntry]) -> "TextureEntryCollection":
        instance = cls()
        if not tes:
            return instance

        for field in dataclasses.fields(cls):
            te_vals: Dict[Any, List[int]] = collections.defaultdict(list)
            for i, te in enumerate(tes):
                # Group values by what face they occur on
                te_vals[getattr(te, field.name)].append(i)
            # Make most common value the "default", everything else is an exception
            sorted_vals = sorted(te_vals.items(), key=lambda x: len(x[1]), reverse=True)
            default_val = sorted_vals.pop(0)[0]
            te_vals = {None: default_val}
            for val, face_nums in sorted_vals:
                te_vals[tuple(face_nums)] = val
            setattr(instance, field.name, te_vals)
        return instance


TE_SERIALIZER = se.Dataclass(TextureEntryCollection)


@se.subfield_serializer("ObjectUpdate", "ObjectData", "TextureEntry")
@se.subfield_serializer("AvatarAppearance", "ObjectData", "TextureEntry")
@se.subfield_serializer("AgentSetAppearance", "ObjectData", "TextureEntry")
@se.subfield_serializer("ObjectImage", "ObjectData", "TextureEntry")
class TextureEntrySubfieldSerializer(se.SimpleSubfieldSerializer):
    EMPTY_IS_NONE = True
    TEMPLATE = se.TypedBytesGreedy(TE_SERIALIZER, empty_is_none=True, lazy=True)


DATA_PACKER_TE_TEMPLATE = se.TypedByteArray(
    se.U32,
    TE_SERIALIZER,
    empty_is_none=True,
    # TODO: Let Readers have lazy=False prop and let addons call
    #  out what subfield serializers should not be lazy. Lazy is way
    #  more expensive if you're going to deserialize every TE anyway
    lazy=True,
)


@se.subfield_serializer("ImprovedTerseObjectUpdate", "ObjectData", "TextureEntry")
class DPTextureEntrySubfieldSerializer(se.SimpleSubfieldSerializer):
    EMPTY_IS_NONE = True
    TEMPLATE = DATA_PACKER_TE_TEMPLATE


class TextureAnimMode(IntFlag):
    ON = 0x01
    LOOP = 0x02
    REVERSE = 0x04
    PING_PONG = 0x08
    SMOOTH = 0x10
    ROTATE = 0x20
    SCALE = 0x40


@dataclasses.dataclass
class TextureAnim:
    Mode: TextureAnimMode = se.dataclass_field(se.IntFlag(TextureAnimMode, se.U8))
    Face: int = se.dataclass_field(se.S8)
    SizeX: int = se.dataclass_field(se.U8)
    SizeY: int = se.dataclass_field(se.U8)
    Start: float = se.dataclass_field(se.F32)
    Length: float = se.dataclass_field(se.F32)
    Rate: float = se.dataclass_field(se.F32)


TA_TEMPLATE = se.Dataclass(TextureAnim)


@se.subfield_serializer("ObjectUpdate", "ObjectData", "TextureAnim")
class TextureAnimSerializer(se.SimpleSubfieldSerializer):
    EMPTY_IS_NONE = True
    TEMPLATE = TA_TEMPLATE


@se.subfield_serializer("ObjectProperties", "ObjectData", "TextureID")
class TextureIDListSerializer(se.SimpleSubfieldSerializer):
    EMPTY_IS_NONE = True
    TEMPLATE = se.Collection(None, se.UUID)


class ParticleDataFlags(IntFlag):
    INTERP_COLOR = 0x001
    INTERP_SCALE = 0x002
    BOUNCE = 0x004
    WIND = 0x008
    FOLLOW_SRC = 0x010
    FOLLOW_VELOCITY = 0x020
    TARGET_POS = 0x040
    TARGET_LINEAR = 0x080
    EMISSIVE = 0x100
    BEAM = 0x200
    RIBBON = 0x400
    DATA_GLOW = 0x10000
    DATA_BLEND = 0x20000


class ParticleFlags(IntFlag):
    OBJECT_RELATIVE = 0x1
    USE_NEW_ANGLE = 0x2


class ParticleBlendFunc(IntEnum):
    ONE = 0
    ZERO = 1
    DEST_COLOR = 2
    SOURCE_COLOR = 3
    ONE_MINUS_DEST_COLOR = 4
    ONE_MINUS_SOURCE_COLOR = 5
    DEST_ALPHA = 6
    SOURCE_ALPHA = 7
    ONE_MINUS_DEST_ALPHA = 8
    ONE_MINUS_SOURCE_ALPHA = 9


PARTDATA_FLAGS = se.IntFlag(ParticleDataFlags, se.U32)


class PartDataOption(se.OptionalFlagged):
    def __init__(self, flag_val, spec):
        super().__init__("PDataFlags", PARTDATA_FLAGS, flag_val, spec)


PDATA_BLOCK_TEMPLATE = se.Template({
    "PDataFlags": PARTDATA_FLAGS,
    "PDataMaxAge": se.FixedPoint(se.U16, 8, 8),
    "StartColor": Color4(),
    "EndColor": Color4(),
    "StartScaleX": se.FixedPoint(se.U8, 3, 5),
    "StartScaleY": se.FixedPoint(se.U8, 3, 5),
    "EndScaleX": se.FixedPoint(se.U8, 3, 5),
    "EndScaleY": se.FixedPoint(se.U8, 3, 5),
    "StartGlow": PartDataOption(ParticleDataFlags.DATA_GLOW, se.QuantizedFloat(se.U8, 0.0, 1.0)),
    "EndGlow": PartDataOption(ParticleDataFlags.DATA_GLOW, se.QuantizedFloat(se.U8, 0.0, 1.0)),
    "BlendSource": PartDataOption(ParticleDataFlags.DATA_BLEND, se.IntEnum(ParticleBlendFunc, se.U8)),
    "BlendDest": PartDataOption(ParticleDataFlags.DATA_BLEND, se.IntEnum(ParticleBlendFunc, se.U8)),
})


class PartPattern(IntFlag):
    NONE = 0
    DROP = 0x1
    EXPLODE = 0x2
    ANGLE = 0x4
    ANGLE_CONE = 0x8
    ANGLE_CONE_EMPTY = 0x10


PSYS_BLOCK_TEMPLATE = se.Template({
    "CRC": se.U32,
    "PSysFlags": se.IntFlag(ParticleFlags, se.U32),
    "Pattern": se.IntFlag(PartPattern, se.U8),
    "PSysMaxAge": se.FixedPoint(se.U16, 8, 8),
    "StartAge": se.FixedPoint(se.U16, 8, 8),
    "InnerAngle": se.FixedPoint(se.U8, 3, 5),
    "OuterAngle": se.FixedPoint(se.U8, 3, 5),
    "BurstRate": se.FixedPoint(se.U16, 8, 8),
    "BurstRadius": se.FixedPoint(se.U16, 8, 8),
    "BurstSpeedMin": se.FixedPoint(se.U16, 8, 8),
    "BurstSpeedMax": se.FixedPoint(se.U16, 8, 8),
    "BurstPartCount": se.U8,
    "Vel": se.FixedPointVector3U16(8, 7, signed=True),
    "Accel": se.FixedPointVector3U16(8, 7, signed=True),
    "Texture": se.UUID,
    "Target": se.UUID,
})

PSBLOCK_TEMPLATE = se.LengthSwitch({
    0: se.Null,
    86: se.Template({"PSys": PSYS_BLOCK_TEMPLATE, "PData": PDATA_BLOCK_TEMPLATE}),
    # Catch-all, this is a variable-length psblock
    None: se.Template({
        "PSys": se.TypedByteArray(se.S32, PSYS_BLOCK_TEMPLATE),
        "PData": se.TypedByteArray(se.S32, PDATA_BLOCK_TEMPLATE),
    })
})


@se.subfield_serializer("ObjectUpdate", "ObjectData", "PSBlock")
class PSBlockSerializer(se.SimpleSubfieldSerializer):
    TEMPLATE = PSBLOCK_TEMPLATE


@se.enum_field_serializer("ObjectExtraParams", "ObjectData", "ParamType")
class ExtraParamType(IntEnum):
    FLEXIBLE = 0x10
    LIGHT = 0x20
    SCULPT = 0x30
    LIGHT_IMAGE = 0x40
    RESERVED = 0x50
    MESH = 0x60
    EXTENDED_MESH = 0x70


class ExtendedMeshFlags(IntFlag):
    ANIMATED_MESH = 0x1


class SculptType(IntEnum):
    NONE = 0
    SPHERE = 1
    TORUS = 2
    PLANE = 3
    CYLINDER = 4
    MESH = 5


@dataclasses.dataclass
class SculptTypeData:
    Type: SculptType = se.bitfield_field(bits=6, adapter=se.IntEnum(SculptType))
    Invert: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter())
    Mirror: bool = se.bitfield_field(bits=1, adapter=se.BoolAdapter())


EXTRA_PARAM_TEMPLATES = {
    ExtraParamType.FLEXIBLE: se.Template({
        "Tension": se.BitField(se.U8, {"Tension": 6, "Softness1": 2}),
        "Drag": se.BitField(se.U8, {"Drag": 7, "Softness2": 1}),
        "Gravity": se.U8,
        "Wind": se.U8,
        "UserForce": se.IfPresent(se.Vector3),
    }),
    ExtraParamType.LIGHT: se.Template({
        "Color": Color4(),
        "Radius": se.F32,
        "Cutoff": se.F32,
        "Falloff": se.F32,
    }),
    ExtraParamType.SCULPT: se.Template({
        "Texture": se.UUID,
        "TypeData": se.BitfieldDataclass(SculptTypeData, se.U8),
    }),
    ExtraParamType.LIGHT_IMAGE: se.Template({
        "Texture": se.UUID,
        "FOV": se.F32,
        "Focus": se.F32,
        "Ambiance": se.F32,
    }),
    ExtraParamType.MESH: se.Template({
        "Asset": se.UUID,
        "TypeData": se.BitfieldDataclass(SculptTypeData, se.U8),
    }),
    ExtraParamType.EXTENDED_MESH: se.Template({
        "Flags": se.IntFlag(ExtendedMeshFlags, se.U32),
    }),
}


@se.subfield_serializer("ObjectExtraParams", "ObjectData", "ParamData")
class ObjectExtraParamsDataSerializer(se.EnumSwitchedSubfieldSerializer):
    ENUM_FIELD = "ParamType"
    TEMPLATES = EXTRA_PARAM_TEMPLATES


EXTRA_PARAM_COLLECTION = se.DictAdapter(se.Collection(
    length=se.U8,
    entry_ser=se.EnumSwitch(se.IntEnum(ExtraParamType, se.U16), {
        t: se.TypedByteArray(se.U32, v) for t, v in EXTRA_PARAM_TEMPLATES.items()
    }),
))


@se.subfield_serializer("ObjectUpdate", "ObjectData", "ExtraParams")
class ObjectUpdateExtraParamsSerializer(se.SimpleSubfieldSerializer):
    TEMPLATE = EXTRA_PARAM_COLLECTION
    EMPTY_IS_NONE = True


@se.flag_field_serializer("ObjectUpdate", "ObjectData", "Flags")
class SoundFlags(IntFlag):
    LOOP = 1 << 0
    SYNC_MASTER = 1 << 1
    SYNC_SLAVE = 1 << 2
    SYNC_PENDING = 1 << 3
    QUEUE = 1 << 4
    STOP = 1 << 5


class CompressedFlags(IntFlag):
    SCRATCHPAD = 1
    TREE = 1 << 1
    TEXT = 1 << 2
    PARTICLES = 1 << 3
    SOUND = 1 << 4
    PARENT_ID = 1 << 5
    TEXTURE_ANIM = 1 << 6
    ANGULAR_VELOCITY = 1 << 7
    NAME_VALUES = 1 << 8
    MEDIA_URL = 1 << 9
    PARTICLES_NEW = 1 << 10


class CompressedOption(se.OptionalFlagged):
    def __init__(self, flag_val, spec):
        super().__init__("Flags", se.IntFlag(CompressedFlags, se.U32), flag_val, spec)


NAMEVALUES_TERMINATED_TEMPLATE = se.TypedBytesTerminated(
    NameValuesSerializer, terminators=(b"\x00",), empty_is_none=True)


@se.subfield_serializer("ObjectUpdateCompressed", "ObjectData", "Data")
class ObjectUpdateCompressedDataSerializer(se.SimpleSubfieldSerializer):
    TEMPLATE = se.Template({
        "FullID": se.UUID,
        "ID": se.U32,
        "PCode": se.IntEnum(PCode, se.U8),
        # Meaning of State is PCode dependent. Could be a bitfield of flags + shifted attachment
        # point if an object with parents set to an avatar.
        "State": ObjectStateAdapter(se.U8),
        "CRC": se.U32,
        "Material": se.IntEnum(MCode, se.U8),
        "ClickAction": se.U8,
        "Scale": se.Vector3,
        "Position": se.Vector3,
        "Rotation": se.PackedQuat(se.Vector3),
        "Flags": se.IntFlag(CompressedFlags, se.U32),
        # Only non-null if there's an attached sound
        "OwnerID": se.UUID,
        "AngularVelocity": CompressedOption(CompressedFlags.ANGULAR_VELOCITY, se.Vector3),
        # Note: missing section specifically means ParentID = 0
        "ParentID": CompressedOption(CompressedFlags.PARENT_ID, se.U32),
        # This field is strange. State == TreeSpecies in other ObjectUpdate types
        "TreeSpecies": CompressedOption(CompressedFlags.TREE, se.U8),
        # Technically only allowed if TREE not set, but I'm not convinced this is ever
        # used, or that any of the official unpackers would work correctly even if it was.
        "ScratchPad": CompressedOption(CompressedFlags.SCRATCHPAD, se.ByteArray(se.U32)),
        "Text": CompressedOption(CompressedFlags.TEXT, se.CStr()),
        "TextColor": CompressedOption(CompressedFlags.TEXT, Color4()),
        "MediaURL": CompressedOption(CompressedFlags.MEDIA_URL, se.CStr()),
        "PSBlock": CompressedOption(CompressedFlags.PARTICLES, se.TypedBytesFixed(86, PSBLOCK_TEMPLATE)),
        "ExtraParams": EXTRA_PARAM_COLLECTION,
        "Sound": CompressedOption(CompressedFlags.SOUND, se.UUID),
        "SoundGain": CompressedOption(CompressedFlags.SOUND, se.F32),
        "SoundFlags": CompressedOption(CompressedFlags.SOUND, se.IntFlag(SoundFlags, se.U8)),
        "SoundRadius": CompressedOption(CompressedFlags.SOUND, se.F32),
        "NameValue": CompressedOption(CompressedFlags.NAME_VALUES, NAMEVALUES_TERMINATED_TEMPLATE),
        # Intentionally not de-quantizing to preserve their real ranges.
        "PathCurve": se.U8,
        "ProfileCurve": se.U8,
        "PathBegin": se.U16,  # 0 to 1, quanta = 0.01
        "PathEnd": se.U16,  # 0 to 1, quanta = 0.01
        "PathScaleX": se.U8,  # 0 to 1, quanta = 0.01
        "PathScaleY": se.U8,  # 0 to 1, quanta = 0.01
        "PathShearX": se.U8,  # -.5 to .5, quanta = 0.01
        "PathShearY": se.U8,  # -.5 to .5, quanta = 0.01
        "PathTwist": se.S8,  # -1 to 1, quanta = 0.01
        "PathTwistBegin": se.S8,  # -1 to 1, quanta = 0.01
        "PathRadiusOffset": se.S8,  # -1 to 1, quanta = 0.01
        "PathTaperX": se.S8,  # -1 to 1, quanta = 0.01
        "PathTaperY": se.S8,  # -1 to 1, quanta = 0.01
        "PathRevolutions": se.U8,  # 0 to 3, quanta = 0.015
        "PathSkew": se.S8,  # -1 to 1, quanta = 0.01
        "ProfileBegin": se.U16,  # 0 to 1, quanta = 0.01
        "ProfileEnd": se.U16,  # 0 to 1, quanta = 0.01
        "ProfileHollow": se.U16,  # 0 to 1, quanta = 0.01
        "TextureEntry": DATA_PACKER_TE_TEMPLATE,
        "TextureAnim": CompressedOption(CompressedFlags.TEXTURE_ANIM, se.TypedByteArray(se.U32, TA_TEMPLATE)),
        "PSBlockNew": CompressedOption(CompressedFlags.PARTICLES_NEW, PSBLOCK_TEMPLATE),
    })


@se.flag_field_serializer("MultipleObjectUpdate", "ObjectData", "Type")
class MultipleObjectUpdateFlags(IntFlag):
    POSITION = 0x01
    ROTATION = 0x02
    SCALE = 0x04
    LINKED_SETS = 0x08
    UNIFORM = 0x10


@se.subfield_serializer("MultipleObjectUpdate", "ObjectData", "Data")
class MultipleObjectUpdateDataSerializer(se.FlagSwitchedSubfieldSerializer):
    FLAG_FIELD = "Type"
    TEMPLATES = {
        MultipleObjectUpdateFlags.POSITION: se.Vector3,
        MultipleObjectUpdateFlags.ROTATION: se.PackedQuat(se.Vector3),
        MultipleObjectUpdateFlags.SCALE: se.Vector3,
    }


@se.flag_field_serializer("AgentUpdate", "AgentData", "ControlFlags")
@se.flag_field_serializer("ScriptControlChange", "Data", "Controls")
class AgentControlFlags(IntFlag):
    AT_POS = 1
    AT_NEG = 1 << 1
    LEFT_POS = 1 << 2
    LEFT_NEG = 1 << 3
    UP_POS = 1 << 4
    UP_NEG = 1 << 5
    PITCH_POS = 1 << 6
    PITCH_NEG = 1 << 7
    YAW_POS = 1 << 8
    YAW_NEG = 1 << 9
    FAST_AT = 1 << 10
    FAST_LEFT = 1 << 11
    FAST_UP = 1 << 12
    FLY = 1 << 13
    STOP = 1 << 14
    FINISH_ANIM = 1 << 15
    STAND_UP = 1 << 16
    SIT_ON_GROUND = 1 << 17
    MOUSELOOK = 1 << 18
    NUDGE_AT_POS = 1 << 19
    NUDGE_AT_NEG = 1 << 20
    NUDGE_LEFT_POS = 1 << 21
    NUDGE_LEFT_NEG = 1 << 22
    NUDGE_UP_POS = 1 << 23
    NUDGE_UP_NEG = 1 << 24
    TURN_LEFT = 1 << 25
    TURN_RIGHT = 1 << 26
    AWAY = 1 << 27
    LBUTTON_DOWN = 1 << 28
    LBUTTON_UP = 1 << 29
    ML_LBUTTON_DOWN = 1 << 30
    ML_LBUTTON_UP = 1 << 31


@se.flag_field_serializer("AgentUpdate", "AgentData", "Flags")
class AgentUpdateFlags(IntFlag):
    HIDE_TITLE = 1
    CLIENT_AUTOPILOT = 1 << 1


@se.enum_field_serializer("ChatFromViewer", "ChatData", "Type")
@se.enum_field_serializer("ChatFromSimulator", "ChatData", "ChatType")
class ChatType(IntEnum):
    WHISPER = 0
    NORMAL = 1
    SHOUT = 2
    OOC = 3
    TYPING_START = 4
    TYPING_STOP = 5
    DEBUG_MSG = 6
    REGION = 7
    OWNER = 8
    DIRECT = 9
    IM = 10
    IM_GROUP = 11
    RADAR = 12


@se.enum_field_serializer("ChatFromSimulator", "ChatData", "SourceType")
class ChatSourceType(IntEnum):
    SYSTEM = 0
    AGENT = 1
    OBJECT = 2
    UNKNOWN = 3


@se.subfield_serializer("AgentThrottle", "Throttle", "Throttles")
class AgentThrottlesSerializer(se.SimpleSubfieldSerializer):
    TEMPLATE = se.Collection(None, se.F32)


@se.subfield_serializer("ObjectUpdate", "ObjectData", "NameValue")
class NameValueSerializer(se.SimpleSubfieldSerializer):
    TEMPLATE = NAMEVALUES_TERMINATED_TEMPLATE


@se.enum_field_serializer("SetFollowCamProperties", "CameraProperty", "Type")
class CameraPropertyType(IntEnum):
    PITCH = 0
    FOCUS_OFFSET = 1
    FOCUS_OFFSET_X = 2
    FOCUS_OFFSET_Y = 3
    FOCUS_OFFSET_Z = 4
    POSITION_LAG = 5
    FOCUS_LAG = 6
    DISTANCE = 7
    BEHINDNESS_ANGLE = 8
    BEHINDNESS_LAG = 9
    POSITION_THRESHOLD = 10
    FOCUS_THRESHOLD = 11
    ACTIVE = 12
    POSITION = 13
    POSITION_X = 14
    POSITION_Y = 15
    POSITION_Z = 16
    FOCUS = 17
    FOCUS_X = 18
    FOCUS_Y = 19
    FOCUS_Z = 20
    POSITION_LOCKED = 21
    FOCUS_LOCKED = 22


@se.enum_field_serializer("DeRezObject", "AgentBlock", "Destination")
class DeRezObjectDestination(IntEnum):
    SAVE_INTO_AGENT_INVENTORY = 0  # deprecated, disabled
    ACQUIRE_TO_AGENT_INVENTORY = 1  # try to leave copy in world
    SAVE_INTO_TASK_INVENTORY = 2
    ATTACHMENT = 3  # deprecated
    TAKE_INTO_AGENT_INVENTORY = 4  # delete from world
    FORCE_TO_GOD_INVENTORY = 5  # force take copy
    TRASH = 6
    ATTACHMENT_TO_INV = 7  # deprecated
    ATTACHMENT_EXISTS = 8  # deprecated
    RETURN_TO_OWNER = 9  # back to owner's inventory
    RETURN_TO_LAST_OWNER = 10  # deeded object back to last owner's inventory, deprecated


@se.flag_field_serializer("RegionHandshake", "RegionInfo", "RegionFlags")
@se.flag_field_serializer("RegionHandshake", "RegionInfo4", "RegionFlagsExtended")
@se.flag_field_serializer("SimStats", "Region", "RegionFlags")
@se.flag_field_serializer("SimStats", "RegionInfo", "RegionFlagsExtended")
@se.flag_field_serializer("RegionInfo", "RegionInfo", "RegionFlags")
@se.flag_field_serializer("RegionInfo", "RegionInfo3", "RegionFlagsExtended")
@se.flag_field_serializer("MapBlockReply", "Data", "RegionFlags")
class RegionFlags(IntFlag):
    ALLOW_DAMAGE = 1 << 0
    ALLOW_LANDMARK = 1 << 1
    ALLOW_SET_HOME = 1 << 2
    # Do we reset the home position when someone teleports away from here?
    RESET_HOME_ON_TELEPORT = 1 << 3
    SUN_FIXED = 1 << 4  # Does the sun move?
    # Does the estate owner allow private parcels?
    ALLOW_ACCESS_OVERRIDE = 1 << 5
    BLOCK_TERRAFORM = 1 << 6
    BLOCK_LAND_RESELL = 1 << 7
    SANDBOX = 1 << 8  # All content wiped once per night
    ALLOW_ENVIRONMENT_OVERRIDE = 1 << 9
    SKIP_COLLISIONS = 1 << 12  # Pin all non agent rigid bodies
    SKIP_SCRIPTS = 1 << 13
    SKIP_PHYSICS = 1 << 14
    EXTERNALLY_VISIBLE = 1 << 15
    ALLOW_RETURN_ENCROACHING_OBJECT = 1 << 16
    ALLOW_RETURN_ENCROACHING_ESTATE_OBJECT = 1 << 17
    BLOCK_DWELL = 1 << 18
    BLOCK_FLY = 1 << 19
    ALLOW_DIRECT_TELEPORT = 1 << 20
    # Is there an administrative override on scripts in the region at the
    # moment. This is the similar skip scripts, except this flag is
    # presisted in the database on an estate level.
    ESTATE_SKIP_SCRIPTS = 1 << 21
    RESTRICT_PUSHOBJECT = 1 << 22
    DENY_ANONYMOUS = 1 << 23
    ALLOW_PARCEL_CHANGES = 1 << 26
    BLOCK_FLYOVER = 1 << 27
    ALLOW_VOICE = 1 << 28
    BLOCK_PARCEL_SEARCH = 1 << 29
    DENY_AGEUNVERIFIED = 1 << 30


@se.flag_field_serializer("RegionHandshakeReply", "RegionInfo", "Flags")
class RegionHandshakeReplyFlags(IntFlag):
    VOCACHE_CULLING_ENABLED = 0x1  # ask sim to send all cacheable objects.
    VOCACHE_IS_EMPTY = 0x2  # the cache file is empty, no need to send cache probes.
    SUPPORTS_SELF_APPEARANCE = 0x4  # inbound AvatarAppearance for self is ok


@se.flag_field_serializer("TeleportStart", "Info", "TeleportFlags")
@se.flag_field_serializer("TeleportProgress", "Info", "TeleportFlags")
@se.flag_field_serializer("TeleportFinish", "Info", "TeleportFlags")
@se.flag_field_serializer("TeleportLocal", "Info", "TeleportFlags")
@se.flag_field_serializer("TeleportLureRequest", "Info", "TeleportFlags")
class TeleportFlags(IntFlag):
    SET_HOME_TO_TARGET = 1 << 0  # newbie leaving prelude (starter area)
    SET_LAST_TO_TARGET = 1 << 1
    VIA_LURE = 1 << 2
    VIA_LANDMARK = 1 << 3
    VIA_LOCATION = 1 << 4
    VIA_HOME = 1 << 5
    VIA_TELEHUB = 1 << 6
    VIA_LOGIN = 1 << 7
    VIA_GODLIKE_LURE = 1 << 8
    GODLIKE = 1 << 9
    NINE_ONE_ONE = 1 << 10  # What is this?
    DISABLE_CANCEL = 1 << 11  # Used for llTeleportAgentHome()
    VIA_REGION_ID = 1 << 12
    IS_FLYING = 1 << 13
    SHOW_RESET_HOME = 1 << 14
    FORCE_REDIRECT = 1 << 15
    VIA_GLOBAL_COORDS = 1 << 16
    WITHIN_REGION = 1 << 17


@se.flag_field_serializer("AvatarPropertiesReply", "PropertiesData", "Flags")
class AvatarPropertiesFlags(IntFlag):
    ALLOW_PUBLISH = 1 << 0  # whether profile is externally visible or not
    MATURE_PUBLISH = 1 << 1  # profile is "mature"
    IDENTIFIED = 1 << 2  # whether avatar has provided payment info
    TRANSACTED = 1 << 3  # whether avatar has actively used payment info
    ONLINE = 1 << 4  # the online status of this avatar, if known.
    AGEVERIFIED = 1 << 5  # whether avatar has been age-verified


@se.flag_field_serializer("AvatarGroupsReply", "GroupData", "GroupPowers")
@se.flag_field_serializer("AvatarGroupDataUpdate", "GroupData", "GroupPowers")
@se.flag_field_serializer("AvatarDataUpdate", "AgentDataData", "GroupPowers")
@se.flag_field_serializer("GroupProfileReply", "GroupData", "PowersMask")
@se.flag_field_serializer("GroupRoleDataReply", "RoleData", "Powers")
class GroupPowerFlags(IntFlag):
    MEMBER_INVITE = 1 << 1  # Invite member
    MEMBER_EJECT = 1 << 2  # Eject member from group
    MEMBER_OPTIONS = 1 << 3  # Toggle "Open enrollment" and change "Signup Fee"
    MEMBER_VISIBLE_IN_DIR = 1 << 47

    # Roles
    ROLE_CREATE = 1 << 4  # Create new roles
    ROLE_DELETE = 1 << 5  # Delete roles
    ROLE_PROPERTIES = 1 << 6  # Change Role Names, Titles, and Descriptions
    ROLE_ASSIGN_MEMBER_LIMITED = 1 << 7  # Assign Member to a Role that the assigner is in
    ROLE_ASSIGN_MEMBER = 1 << 8  # Assign Member to Role
    ROLE_REMOVE_MEMBER = 1 << 9  # Remove Member from Role
    ROLE_CHANGE_ACTIONS = 1 << 10  # Change actions a role can perform

    # Group Identity
    GROUP_CHANGE_IDENTITY = 1 << 11  # Charter, insignia, 'Show In Group List', 'Publish on the web', 'Mature', etc.

    # Parcel Management
    LAND_DEED = 1 << 12  # Deed Land and Buy Land for Group
    LAND_RELEASE = 1 << 13  # Release Land (to Gov. Linden)
    # Set for sale info (Toggle "For Sale", Set Price, Set Target, Toggle "Sell objects with the land")
    LAND_SET_SALE_INFO = 1 << 14
    LAND_DIVIDE_JOIN = 1 << 15  # Divide and Join Parcels

    # Parcel Identity
    LAND_FIND_PLACES = 1 << 17  # Toggle "Show in Find Places" and Set Category.
    # Change Parcel Identity: Parcel Name, Parcel Description, Snapshot, 'Publish on the web', and 'Mature' checkbox
    LAND_CHANGE_IDENTITY = 1 << 18
    LAND_SET_LANDING_POINT = 1 << 19  # Set Landing Point

    # Parcel Settings
    LAND_CHANGE_MEDIA = 1 << 20  # Change Media Settings
    LAND_EDIT = 1 << 21  # Toggle Edit Land
    # Toggle Set Home Point, Fly, Outside Scripts, Create/Edit Objects, Landmark, and Damage checkboxes
    LAND_OPTIONS = 1 << 22

    # Parcel Powers
    LAND_ALLOW_EDIT_LAND = 1 << 23  # Bypass Edit Land Restriction
    LAND_ALLOW_FLY = 1 << 24  # Bypass Fly Restriction
    LAND_ALLOW_CREATE = 1 << 25  # Bypass Create/Edit Objects Restriction
    LAND_ALLOW_LANDMARK = 1 << 26  # Bypass Landmark Restriction
    LAND_ALLOW_SET_HOME = 1 << 28  # Bypass Set Home Point Restriction
    LAND_ALLOW_HOLD_EVENT = 1 << 41  # Allowed to hold events on group-owned land
    LAND_ALLOW_ENVIRONMENT = 1 << 46  # Allowed to change the environment

    # Parcel Access
    LAND_MANAGE_ALLOWED = 1 << 29  # Manage Allowed List
    LAND_MANAGE_BANNED = 1 << 30  # Manage Banned List
    LAND_MANAGE_PASSES = 1 << 31  # Change Sell Pass Settings
    LAND_ADMIN = 1 << 32  # Eject and Freeze Users on the land

    # Parcel Content
    LAND_RETURN_GROUP_SET = 1 << 33  # Return objects on parcel that are set to group
    LAND_RETURN_NON_GROUP = 1 << 34  # Return objects on parcel that are not set to group
    LAND_RETURN_GROUP_OWNED = 1 << 48  # Return objects on parcel that are owned by the group

    LAND_GARDENING = 1 << 35  # Parcel Gardening - plant and move linden trees

    # Object Management
    OBJECT_DEED = 1 << 36  # Deed Object
    OBJECT_MANIPULATE = 1 << 38  # Manipulate Group Owned Objects (Move, Copy, Mod)
    OBJECT_SET_SALE = 1 << 39  # Set Group Owned Object for Sale

    # Accounting
    ACCOUNTING_ACCOUNTABLE = 1 << 40  # Pay Group Liabilities and Receive Group Dividends

    # Notices
    NOTICES_SEND = 1 << 42  # Send Notices
    NOTICES_RECEIVE = 1 << 43  # Receive Notices and View Notice History

    # Proposals
    # TODO: _DEPRECATED suffix as part of vote removal - DEV-24856:
    PROPOSAL_START = 1 << 44  # Start Proposal
    # TODO: _DEPRECATED suffix as part of vote removal - DEV-24856:
    PROPOSAL_VOTE = 1 << 45  # Vote on Proposal

    # Group chat moderation related
    SESSION_JOIN = 1 << 16  # can join session
    SESSION_VOICE = 1 << 27  # can hear/talk
    SESSION_MODERATOR = 1 << 37  # can mute people's session

    EXPERIENCE_ADMIN = 1 << 49  # has admin rights to any experiences owned by this group
    EXPERIENCE_CREATOR = 1 << 50  # can sign scripts for experiences owned by this group

    # Group Banning
    GROUP_BAN_ACCESS = 1 << 51  # Allows access to ban / un-ban agents from a group.


@se.flag_field_serializer("GrantUserRights", "Rights", "RelatedRights")
@se.flag_field_serializer("ChangeUserRights", "Rights", "RelatedRights")
class UserRelatedRights(IntFlag):
    """See lluserrelations.h for definitions"""
    ONLINE_STATUS = 1
    MAP_LOCATION = 1 << 1
    MODIFY_OBJECTS = 1 << 2


@se.flag_field_serializer("RequestObjectPropertiesFamily", "ObjectData", "RequestFlags")
@se.flag_field_serializer("ObjectPropertiesFamily", "ObjectData", "RequestFlags")
class ObjectPropertiesFamilyRequestFlags(IntFlag):
    BUG_REPORT = 1 << 0
    COMPLAINT_REPORT = 1 << 1
    OBJECT_PAY = 1 << 2


@se.enum_field_serializer("RequestImage", "RequestImage", "Type")
class RequestImageType(IntEnum):
    NORMAL = 0
    AVATAR_BAKE = 1


@se.enum_field_serializer("ImageData", "ImageID", "Codec")
class ImageCodec(IntEnum):
    INVALID = 0
    RGB = 1
    J2C = 2
    BMP = 3
    TGA = 4
    JPEG = 5
    DXT = 6
    PNG = 7


@se.enum_field_serializer("LayerData", "LayerID", "Type")
class LayerDataType(IntEnum):
    LAND_LAYER_CODE = ord('L')
    WIND_LAYER_CODE = ord('7')
    CLOUD_LAYER_CODE = ord('8')
    WATER_LAYER_CODE = ord('W')

    # <FS:CR> Aurora Sim
    # Extended land layer for Aurora Sim
    AURORA_LAND_LAYER_CODE = ord('M')
    AURORA_WATER_LAYER_CODE = ord('X')
    AURORA_WIND_LAYER_CODE = ord('9')
    AURORA_CLOUD_LAYER_CODE = ord(':')


@se.enum_field_serializer("ModifyLand", "ModifyBlock", "Action")
class ModifyLandAction(IntEnum):
    LEVEL = 0
    RAISE = 1
    LOWER = 2
    SMOOTH = 3
    NOISE = 4
    REVERT = 5


@se.flag_field_serializer("RevokePermissions", "Data", "ObjectPermissions")
@se.flag_field_serializer("ScriptQuestion", "Data", "Questions")
@se.flag_field_serializer("ScriptAnswerYes", "Data", "Questions")
class ScriptPermissions(IntFlag):
    # "1" itself seems to be unused?
    TAKE_MONEY = 1 << 1
    TAKE_CONTROLS = 1 << 2
    # Doesn't seem to be used?
    REMAP_CONTROLS = 1 << 3
    TRIGGER_ANIMATIONS = 1 << 4
    ATTACH = 1 << 5
    # Doesn't seem to be used?
    RELEASE_OWNERSHIP = 1 << 6
    CHANGE_LINKS = 1 << 7
    # Object joints don't exist anymore
    CHANGE_JOINTS = 1 << 8
    # Change its own permissions? Doesn't seem to be used.
    CHANGE_PERMISSIONS = 1 << 9
    TRACK_CAMERA = 1 << 10
    CONTROL_CAMERA = 1 << 11
    TELEPORT = 1 << 12
    JOIN_EXPERIENCE = 1 << 13
    MANAGE_ESTATE_ACCESS = 1 << 14
    ANIMATION_OVERRIDE = 1 << 15
    RETURN_OBJECTS = 1 << 16
    FORCE_SIT = 1 << 17
    CHANGE_ENVIRONMENT = 1 << 18


@se.enum_field_serializer("UpdateMuteListEntry", "MuteData", "MuteType")
class MuteType(IntEnum):
    BY_NAME = 0
    AGENT = 1
    OBJECT = 2
    GROUP = 3
    # Voice, presumably.
    EXTERNAL = 4


@se.flag_field_serializer("UpdateMuteListEntry", "MuteData", "MuteFlags")
class MuteFlags(IntFlag):
    # For backwards compatibility (since any mute list entries that were created before the flags existed
    # will have a flags field of 0), some flags are "inverted".
    # Note that it's possible, through flags, to completely disable an entry in the mute list.
    # The code should detect this case and remove the mute list entry instead.
    TEXT_CHAT = 1 << 0
    VOICE_CHAT = 1 << 1
    PARTICLES = 1 << 2
    OBJECT_SOUNDS = 1 << 3

    @property
    def DEFAULT(self):
        return 0x0

    @property
    def ALL(self):
        return 0xF


class CreationDateAdapter(se.Adapter):
    def decode(self, val: Any, ctx: Optional[se.ParseContext], pod: bool = False) -> Any:
        return datetime.datetime.fromtimestamp(val / 1_000_000).isoformat()

    def encode(self, val: Any, ctx: Optional[se.ParseContext]) -> Any:
        return int(datetime.datetime.fromisoformat(val).timestamp() * 1_000_000)


@se.subfield_serializer("ObjectProperties", "ObjectData", "CreationDate")
class CreationDateSerializer(se.AdapterSubfieldSerializer):
    ADAPTER = CreationDateAdapter(None)
    ORIG_INLINE = True


@se.http_serializer("RenderMaterials")
class RenderMaterialsSerializer(se.BaseHTTPSerializer):
    @classmethod
    def deserialize_resp_body(cls, method: str, body: bytes):
        deser = llsd.unzip_llsd(llsd.parse_xml(body)["Zipped"])
        return deser

    @classmethod
    def deserialize_req_body(cls, method: str, body: bytes):
        if not body:
            return se.UNSERIALIZABLE
        deser = llsd.unzip_llsd(llsd.parse_xml(body)["Zipped"])
        return deser

    @classmethod
    def serialize_req_body(cls, method: str, body):
        if body == b"":
            return body

        return llsd.format_xml({"Zipped": llsd.zip_llsd(body)})


@se.http_serializer("RetrieveNavMeshSrc")
class RetrieveNavMeshSrcSerializer(se.BaseHTTPSerializer):
    @classmethod
    def deserialize_resp_body(cls, method: str, body: bytes):
        deser = llsd.parse_xml(body)
        # 15 bit window size, gzip wrapped
        deser["navmesh_data"] = zlib.decompress(deser["navmesh_data"], wbits=15 | 32)
        return deser


# Beta puppetry stuff, subject to change!


class PuppetryEventMask(enum.IntFlag):
    POSITION = 1 << 0
    POSITION_IN_PARENT_FRAME = 1 << 1
    ROTATION = 1 << 2
    ROTATION_IN_PARENT_FRAME = 1 << 3
    SCALE = 1 << 4
    DISABLE_CONSTRAINT = 1 << 7


class PuppetryOption(se.OptionalFlagged):
    def __init__(self, flag_val, spec):
        super().__init__("mask", se.IntFlag(PuppetryEventMask, se.U8), flag_val, spec)


# Range to use for puppetry's quantized floats when converting to<->from U16
LL_PELVIS_OFFSET_RANGE = (-5.0, 5.0)


@dataclasses.dataclass
class PuppetryJointData:
    # Where does this number come from? `avatar_skeleton.xml`?
    joint_id: int = se.dataclass_field(se.S16)
    # Determines which fields will follow
    mask: PuppetryEventMask = se.dataclass_field(se.IntFlag(PuppetryEventMask, se.U8))
    rotation: Optional[Quaternion] = se.dataclass_field(
        # These are very odd scales for a quantized quaternion, but that's what they are.
        PuppetryOption(PuppetryEventMask.ROTATION, se.PackedQuat(se.Vector3U16(*LL_PELVIS_OFFSET_RANGE))),
    )
    position: Optional[Vector3] = se.dataclass_field(
        PuppetryOption(PuppetryEventMask.POSITION, se.Vector3U16(*LL_PELVIS_OFFSET_RANGE)),
    )
    scale: Optional[Vector3] = se.dataclass_field(
        PuppetryOption(PuppetryEventMask.SCALE, se.Vector3U16(*LL_PELVIS_OFFSET_RANGE)),
    )


@dataclasses.dataclass
class PuppetryEventData:
    time: int = se.dataclass_field(se.S32)
    # Must be set manually due to below issue
    num_joints: int = se.dataclass_field(se.U16)
    # This field is packed in the least helpful way possible. The length field
    # is in between the collection count and the collection data, but the length
    # field essentially only tells you how many bytes until the end of the buffer
    # proper, which you already know from msgsystem. Why is this here?
    joints: List[PuppetryJointData] = se.dataclass_field(se.TypedByteArray(
        se.U32,
        # Just treat contents as a greedy collection, tries to keep reading until EOF
        se.Collection(None, se.Dataclass(PuppetryJointData)),
    ))


@se.subfield_serializer("AgentAnimation", "PhysicalAvatarEventList", "TypeData")
@se.subfield_serializer("AvatarAnimation", "PhysicalAvatarEventList", "TypeData")
class PuppetryEventDataSerializer(se.SimpleSubfieldSerializer):
    # You can have multiple joint events packed in right after the other, implicitly.
    # They may _or may not_ be split into separate PhysicalAvatarEventList blocks?
    # This doesn't seem to be handled specifically in the decoder, is this a
    # serialization bug in the viewer?
    TEMPLATE = se.Collection(None, se.Dataclass(PuppetryEventData))
    EMPTY_IS_NONE = True
