"""Slice and dice 3DS files.

Provides for reading, writing, and manipulating 3DS files.  It's
called dom3ds because it's reminiscent of XML-DOM: it converts the 3DS
file into a hierarchy of objects, in much the same way XML-DOM
converts an XML file into a hierarchy of objects called the Document
Object Model.  The dom3ds module creates an object for each chunk in
the 3DS file, which can be accessed hierarchially as attributes.

For example, once a 3DS file is loaded, you could access the smoothing
data of the second object like this:

    dom.mdata.objects[2].ntri.faces.smoothing.array

"""

import sys
import struct

import numpy


# Exceptions


class File3dsFormatError(Exception):
    "Exception in the 3DS file format"

    def __init__(self, msg, tight=False):
        self.msg = msg
        self.tight = tight

    def __str__(self):
        return repr(self.msg)


class FBufError(Exception):
    "Exception when reading from FileLikeBuffers"


# Numeric/string conversion functions, where the string uses
# little-endian regardless of host byte order


def string_to_array(s, typ):
    """Convert data block from a 3DS file to a numpy array."""
    a = numpy.fromstring(s, typ)
    if sys.byteorder == "big":
        a.byteswap(True)
    return a


def array_to_string(a):
    """Convert a numpy array to data block for a 3DS file."""
    if sys.byteorder == "big":
        a = a.byteswap()
    return a.tostring()


def array_to_string_destructive(a):
    """Destructively convert a numpy array to data block for a 3DS file."""
    if sys.byteorder == "big":
        a.byteswap(True)
    return a.tostring()


# Utility classes


class FileLikeBuffer(object):
    """Treat a range of a string as a quasi-file-like object.

    This is a quasi-file like object that represents a certain
    range of a string.  It behaves kind of like a file object,
    but has some specialized methods.

    """

    def __init__(self, s, start=0, end=None):
        self.buf = s
        self.start = start
        if end is None:
            self.end = len(s)
        else:
            if end > len(s):
                raise FBufError("End of fbuf object set past end of buffer")
            self.end = end
        assert 0 <= start <= self.end, (start, self.end)
        self.index = start

    def tell(self):
        return self.index

    def seek(self, pos):
        self.index = pos
        if self.index < 0 or self.index > self.end:
            raise FBufError("Invalid position in fbuf")

    def advance(self, n):
        self.index += n
        if self.index > self.end:
            raise FBufError("End of fbuf reached")

    def read(self, length):
        r = self.buf[self.index : self.index + length]
        self.advance(length)
        return r

    def read_fbuf(self, length):
        r = FileLikeBuffer(self.buf, self.index, self.index + length)
        self.advance(length)
        return r

    def read_to_nul(self):
        try:
            i = self.buf.index("\0", self.index, self.end)
        except IndexError:
            raise FBufError("End of fbuf reached in string")
        r = self.buf[self.index : i]
        self.index = i + 1
        return r

    def read_rest(self):
        r = self.buf[self.index : self.end]
        self.index = self.end
        return r

    def room_for_chunks(self):
        return self.index + 6 <= self.end


# Utility functions


def _decls_to_list(decls):
    decllist = [tuple(x.strip().split()) for x in decls.split(",")]
    return decllist


def _decls_to_vars(decls):
    if not decls:
        return ()
    vars = [x.strip().split(None, 1)[1] for x in decls.split(",")]
    return vars


# Chunk metaclasses


class ChunkMetaclass(type):
    """Metaclass of chunk types.

    This class takes some static data and defines how to read in
    the chunks, and preprocesses it.  It also creates a dictionary
    of chunk classes keyed by tag.

    """

    chunk_taghash = {}
    chunk_labelhash = {}

    def __new__(metatype, name, bases, clsdict):
        vars = []
        vars.extend(_decls_to_vars(clsdict.get("struct", "")))
        vars.extend(_decls_to_vars(clsdict.get("single", "")))
        vars.extend(_decls_to_vars(clsdict.get("multiple", "")))
        if clsdict.get("freechunks", False):
            vars.append("subchunks")
        clsdict.setdefault("__slots__", []).extend(vars)

        return type.__new__(metatype, name, bases, clsdict)

    def __init__(self, name, bases, clsdict):
        if hasattr(self, "struct"):
            self.struct_fields = _decls_to_list(self.struct)
        else:
            self.struct_fields = []

        self.single_types = {}
        self.single_order = []
        if hasattr(self, "single"):
            attrs = {}
            for typ, attr in _decls_to_list(self.single):
                self.single_types[typ] = attr
                if attr not in attrs:
                    attrs[attr] = 1
                    self.single_order.append(attr)

        self.multiple_types = {}
        self.multiple_order = []
        if hasattr(self, "multiple"):
            attrs = {}
            for typ, attr in _decls_to_list(self.multiple):
                self.multiple_types[typ] = attr
                if attr not in attrs:
                    attrs[attr] = 1
                    self.multiple_order.append(attr)

        if hasattr(self, "keyframe"):
            self.keyframe_fields = _decls_to_list(self.keyframe)
            slots = ["frameno", "flags", "tension", "continuity", "bias", "ease_to", "ease_from"]
            for typ, attr in self.keyframe_fields:
                slots.append(attr)
            kfdict = {"__slots__": slots}
            kfbases = (object,)
            kfname = "%s.Key" % name
            self.Key = type(kfname, kfbases, kfdict)
        else:
            self.keyframe_fields = []

        if hasattr(self, "tag"):
            self.chunk_taghash[self.tag] = self
            self.chunk_labelhash[name] = self
            if not hasattr(self, "label"):
                self.label = name

        type.__init__(self, name, bases, clsdict)


class UndefinedChunkMetaclass(ChunkMetaclass):
    """Metaclass of undefined chunks.

    Automatically inserts a data attribute into __slots__ to hold
    the chunk data.

    """

    def __new__(metatype, name, bases, clsdict):
        if "__slots__" in clsdict:
            if "data" not in clsdict["__slots__"]:
                clsdict["__slots__"].append("data")
        else:
            clsdict["__slots__"] = ["data"]
        return ChunkMetaclass.__new__(metatype, name, bases, clsdict)


# Base class of chunks.


class ChunkBase(object):
    """Base class of chunks.

    The methods of this class are mostly for input/output.  When
    manipulating a 3ds model, one would access subchunks as
    attributes.

    """

    __metaclass__ = ChunkMetaclass

    # Defaults for some parameters

    freechunks = True
    swallow = False

    # Define constructor to accept keyword arguments, as a convenience
    # Init chunk arguments to nothing

    def __init__(self, **kwargs):
        """Construct a chunk.

        This is an abstract base constructor; normally you'd
        construct a subclass of this.  As a convenience, you
        can set attributes via keyword arguments when creating
        a chunk.

        """

        if self.freechunks:
            self.subchunks = []
        for v in self.single_order:
            setattr(self, v, None)
        for v in self.multiple_order:
            setattr(self, v, [])
        for attr, value in kwargs.items():
            setattr(self, attr, value)

    # -------- SECTION 1: INITIALIZATION FROM CHUNK DATA --------

    # These get a specific object from the fbuf

    @staticmethod
    def get_byte(fbuf, flags):
        return ord(fbuf.read(1))

    @staticmethod
    def get_short(fbuf, flags):
        return struct.unpack("<H", fbuf.read(2))[0]

    @staticmethod
    def get_long(fbuf, flags):
        return struct.unpack("<L", fbuf.read(4))[0]

    @staticmethod
    def get_float(fbuf, flags):
        return struct.unpack("<f", fbuf.read(4))[0]

    @staticmethod
    def get_string(fbuf, flags):
        return fbuf.read_to_nul()

    @staticmethod
    def get_chunk(fbuf, flags):
        tag, length = struct.unpack("<HL", fbuf.read(6))
        pos = fbuf.tell()
        cls = ChunkMetaclass.chunk_taghash.get(tag)
        try:
            if cls is not None:
                ch = cls()
            else:
                ch = UnknownChunk(tag)
            ch.read(fbuf.read_fbuf(length - 6), flags)
        except (File3dsFormatError, FBufError) as fe:
            if flags["recover"]:
                if cls is not None:
                    label = cls.label
                else:
                    label = "UNKNOWN"
                ch = ErrorChunk(tag, label, str(fe))
                fbuf.seek(pos)
                ch.read(fbuf.read_fbuf(length - 6), flags)
            else:
                raise
        return ch

    # These read sections of the chunk

    def read_struct(self, fbuf, flags):
        for typ, attr in self.struct_fields:
            setattr(self, attr, getattr(self, "get_" + typ)(fbuf, flags))

    def read_array(self, fbuf, flags):
        # Expect subclass to override this
        pass

    def read_chunks(self, fbuf, flags):
        if self.swallow:
            return

        while fbuf.room_for_chunks():
            ch = self.get_chunk(fbuf, flags)
            label = ch.label
            if label == "ERROR":
                label = ch.intended_label
                if label is None:
                    label = "ERROR"
            if label in self.single_types:
                attr = self.single_types[label]
                if flags["tight"] and getattr(self, attr) is not None:
                    raise File3dsFormatError("Chunk %s appeared twice" % label, True)
                setattr(self, attr, ch)
            elif label in self.multiple_types:
                attr = self.multiple_types[label]
                getattr(self, attr).append(ch)
            elif label == "DUMMY":
                pass
            elif self.freechunks:
                self.subchunks.append(ch)
            elif flags["tight"]:
                raise File3dsFormatError("Illegal context for chunk %s" % label, True)

    # Initialize self by scanning a chunk

    def read(self, fbuf, flags):
        self.read_struct(fbuf, flags)
        self.read_array(fbuf, flags)
        self.read_chunks(fbuf, flags)

    # -------- SECTION 2: DUMPING TO OUTPUT --------

    # Dump individual attributes

    def out_attr(self, attr, flo, indent, flags):
        flo.write("%s%s = %r\n" % (indent, attr, getattr(self, attr)))

    def out_chunk(self, attr, flo, indent, flags):
        g = getattr(self, attr)
        if g is not None:
            flo.write("%s%s = " % (indent, attr))
            g.dump(flo, indent, flags)

    def out_chunk_index(self, attr, i, flo, indent, flags):
        g = getattr(self, attr)[i]
        if g is not None:
            flo.write("%s%s[%d] = " % (indent, attr, i))
            g.dump(flo, indent, flags)

    # Dump sections of the file

    def dump_header(self, flo, flags):
        flo.write("%s (0x%04X)\n" % (self.label, self.tag))

    def dump_struct(self, flo, indent, flags):
        for typ, attr in self.struct_fields:
            self.out_attr(attr, flo, indent, flags)

    def dump_array(self, flo, indent, flags):
        # Expect subclass to override this
        pass

    def dump_chunks(self, flo, indent, flags):
        if self.swallow:
            return
        for attr in self.single_order:
            self.out_chunk(attr, flo, indent, flags)
        for attr in self.multiple_order:
            for i in range(len(getattr(self, attr))):
                self.out_chunk_index(attr, i, flo, indent, flags)
        if self.freechunks:
            attr = "subchunks"
            for i in range(len(self.subchunks)):
                self.out_chunk_index(attr, i, flo, indent, flags)

    # Dump self to stream flo at a certain indentation

    def dump(self, flo, indent, flags):
        indent += "    "
        self.dump_header(flo, flags)
        self.dump_struct(flo, indent, flags)
        self.dump_array(flo, indent, flags)
        self.dump_chunks(flo, indent, flags)

    # -------- SECTION #3: WRITING TO OUTPUT --------

    # These set a specific object from the fbuf

    @staticmethod
    def set_byte(value):
        return chr(value)

    @staticmethod
    def set_short(value):
        return struct.pack("<H", value)

    @staticmethod
    def set_long(value):
        return struct.pack("<L", value)

    @staticmethod
    def set_float(value):
        return struct.pack("<f", value)

    @staticmethod
    def set_string(value):
        return value + "\0"

    @staticmethod
    def set_chunk(value):
        s = value.write()
        length = len(s) + 6
        return struct.pack("<HL", value.tag, length) + s

    # These write sections of the chunk

    def write_struct(self):
        s = []
        for typ, attr in self.struct_fields:
            s.append(getattr(self, "set_" + typ)(getattr(self, attr)))
        return "".join(s)

    def write_array(self):
        # Expect subclass to override this
        return ""

    def write_chunks(self):
        if self.swallow:
            return ""
        s = []
        for attr in self.single_order:
            v = getattr(self, attr)
            if v is not None:
                s.append(self.set_chunk(v))
        for attr in self.multiple_order:
            for v in getattr(self, attr):
                s.append(self.set_chunk(v))
        if self.freechunks:
            for v in self.subchunks:
                s.append(self.set_chunk(v))
        return "".join(s)

    # Write out contents (but not header) to chunk

    def write(self):
        s = []
        s.append(self.write_struct())
        s.append(self.write_array())
        s.append(self.write_chunks())
        return "".join(s)

    # -------- SECTION #4: OUTPUT DOCUMENTATION --------

    def document_html(cls, flo, done):
        if cls.label in done:
            return
        flo.write(
            '<dt>\n<a name="%s"></a><b><tt>%s (0x%04X)</tt></b></dt>\n'
            % (cls.label, cls.label, cls.tag)
        )
        flo.write("<dd>\n")

        i = 0

        for typ, attr in cls.struct_fields:
            flo.write("<tt>%s %s</tt><br>\n" % (typ, attr))
            i += 1

        tagorder = []

        d = {}
        for typ, attr in cls.single_types.items():
            if attr not in d:
                d[attr] = []
            d[attr].append(typ)
        for attr in cls.single_order:
            typs = d[attr]
            for typ in typs:
                tagorder.append(typ)
                flo.write('<tt><a href="#%s">%s</a> %s</tt><br>\n' % (typ, typ, attr))
                i += 1

        d = {}
        for typ, attr in cls.multiple_types.items():
            if attr not in d:
                d[attr] = []
            d[attr].append(typ)
        for attr in cls.multiple_order:
            typs = d[attr]
            for typ in typs:
                tagorder.append(typ)
                flo.write('<tt><a href="#%s">%s[]</a> %s</tt><br>\n' % (typ, typ, attr))
                i += 1

        if not i:
            flo.write("No fields<br>\n")

        flo.write("&nbsp;</dd>\n")
        done[cls.label] = 1

        for typ in tagorder:
            type(cls).chunk_labelhash[typ].document_html(flo, done)

    document_html = classmethod(document_html)


# Chunks we don't know anything about, maybe not even the tag


class UndefinedChunk(ChunkBase):
    __metaclass__ = UndefinedChunkMetaclass

    def read(self, fbuf, flags):
        self.data = fbuf.read_rest()

    def dump(self, flo, indent, flags):
        self.dump_header(flo, flags)

    def write(self):
        return self.data


class UnknownChunk(UndefinedChunk):
    __slots__ = ["tag"]
    label = "UNKNOWN"

    def __init__(self, tag):
        super().__init__(tag=tag)


class ErrorChunk(UndefinedChunk):
    __slots__ = ["intended_tag", "intended_label", "error_msg"]
    label = "ERROR"
    tag = 0xEEEE

    def __init__(self, intended_tag=None, intended_label=None, error_msg=None):
        super().__init__(
            intended_tag=intended_tag, intended_label=intended_label, error_msg=error_msg
        )

    def dump(self, flo, indent, flags):
        super(ErrorChunk, self).dump(flo, indent, flags)
        if self.intended_tag is not None:
            flo.write("%s    intended_tag = 0x%04X\n" % (indent, self.intended_tag))
        if self.intended_label is not None:
            self.out_attr("intended_label", flo, indent + "    ", flags)
        if self.error_msg is not None:
            self.out_attr("error_msg", flo, indent + "    ", flags)

    def write(self):
        raise TypeError("Attempt to write an Error Chunk")


# Chunks with common sets of attributes


class OneColorChunk(ChunkBase):
    single = "COLOR_F color, COLOR_24 color," "LIN_COLOR_F lincolor, LIN_COLOR_24 lincolor"


class OnePercentageChunk(ChunkBase):
    single = "INT_PERCENTAGE pct, FLOAT_PERCENTAGE pct"


class OneShortValueChunk(ChunkBase):
    struct = "short value"


class OneFloatValueChunk(ChunkBase):
    struct = "float value"


class Color24Chunk(ChunkBase):
    struct = "byte red, byte green, byte blue"


class TextureChunk(ChunkBase):
    single = (
        "INT_PERCENTAGE pct,"
        "MAT_MAPNAME filename,"
        "MAT_MAP_TILING tiling,"
        "MAT_MAP_TEXBLUR blur,"
        "MAT_MAP_USCALE uscale,"
        "MAT_MAP_VSCALE vscale,"
        "MAT_MAP_UOFFSET uoffset,"
        "MAT_MAP_VOFFSET voffset,"
        "MAT_MAP_ANG angle,"
        "MAT_MAP_COL1 color1,"
        "MAT_MAP_COL2 color2,"
        "MAT_MAP_RCOL rtint,"
        "MAT_MAP_GCOL gtint,"
        "MAT_MAP_BCOL btint"
    )


class TextureMaskChunk(ChunkBase):
    single = (
        "INT_PERCENTAGE pct,"
        "MAT_MAPNAME filename,"
        "MAT_MAP_TILING tiling,"
        "MAT_MAP_TEXBLUR blur,"
        "MAT_MAP_USCALE uscale,"
        "MAT_MAP_VSCALE vscale,"
        "MAT_MAP_UOFFSET uoffset,"
        "MAT_MAP_VOFFSET voffset,"
        "MAT_MAP_ANG angle"
    )


class ArrayChunk(ChunkBase):
    __slots__ = ["array"]

    def dump_array(self, flo, indent, flags):
        flo.write("%s%s = <array data %s>\n" % (indent, "array", repr(self.array.shape)))


class MatrixChunk(ArrayChunk):
    def read_array(self, fbuf, flags):
        size = 48
        a = string_to_array(fbuf.read(size), numpy.float32)
        a = numpy.reshape(a, (4, 3))
        m = numpy.zeros((4, 4), numpy.float32)
        m[:, 0:3] = a
        m[3, 3] = 1.0
        self.array = numpy.array(numpy.transpose(m))

    def dump_array(self, flo, indent, flags):
        super(MatrixChunk, self).dump_array(flo, indent, flags)
        if flags["arraylines"] == 0:
            return
        for i in range(4):
            flo.write(
                "%s    %12.4g%12.4g%12.4g%12.4g\n"
                % (indent, self.array[i, 0], self.array[i, 1], self.array[i, 2], self.array[i, 3])
            )

    def write_array(self):
        a = self.array[0:3, :]
        a = numpy.array(numpy.transpose(a)).astype(numpy.float32)
        return array_to_string_destructive(a)


class TrackChunk(ChunkBase):
    __slots__ = ["keys"]
    struct = "short flags, long unused1, long unused2, long nkeys"

    def read_array(self, fbuf, flags):
        self.keys = []
        for i in range(self.nkeys):
            kf = self.Key()
            kf.frameno = self.get_long(fbuf, flags)
            kf.flags = self.get_short(fbuf, flags)
            if kf.flags & 1:
                kf.tension = self.get_float(fbuf, flags)
            if kf.flags & 2:
                kf.continuity = self.get_float(fbuf, flags)
            if kf.flags & 4:
                kf.bias = self.get_float(fbuf, flags)
            if kf.flags & 8:
                kf.ease_to = self.get_float(fbuf, flags)
            if kf.flags & 16:
                kf.ease_from = self.get_float(fbuf, flags)
            for typ, attr in self.keyframe_fields:
                setattr(kf, attr, getattr(self, "get_" + typ)(fbuf, flags))
            self.keys.append(kf)

    def write_array(self):
        s = []
        for kf in self.keys:
            s.append(self.set_long(kf.frameno))
            s.append(self.set_short(kf.flags))
            if kf.flags & 1:
                s.append(self.set_float(kf.tension))
            if kf.flags & 2:
                s.append(self.set_float(kf.continuity))
            if kf.flags & 4:
                s.append(self.set_float(kf.bias))
            if kf.flags & 8:
                s.append(self.set_float(kf.ease_to))
            if kf.flags & 16:
                s.append(self.set_float(kf.ease_from))
            for typ, attr in self.keyframe_fields:
                s.append(getattr(self, "set_" + typ)(getattr(kf, attr)))
        return "".join(s)

    def dump_array(self, flo, indent, flags):
        super(TrackChunk, self).dump_array(flo, indent, flags)
        if flags["arraylines"] == 0:
            return
        if flags["arraylines"] < 0:
            n = self.nkeys
        else:
            n = min(flags["arraylines"], self.nkeys)
        for i in range(n):
            kf = self.keys[i]
            flo.write("%skeys[0] = %s.Key\n" % (indent, self.label))
            flo.write("%s    frameno = %r\n" % (indent, kf.frameno))
            flo.write("%s    flags = %r\n" % (indent, kf.flags))
            if kf.flags & 1:
                flo.write("%s    tension = %r\n" % (indent, kf.tension))
            if kf.flags & 2:
                flo.write("%s    continuity = %r\n" % (indent, kf.continuity))
            if kf.flags & 4:
                flo.write("%s    bias = %r\n" % (indent, kf.bias))
            if kf.flags & 8:
                flo.write("%s    ease_to = %r\n" % (indent, kf.ease_to))
            if kf.flags & 16:
                flo.write("%s    ease_from = %r\n" % (indent, kf.ease_from))
            for typ, attr in self.keyframe_fields:
                flo.write("%s    %s = %r\n" % (indent, attr, getattr(kf, attr)))
        if n < self.nkeys:
            flo.write("%s    …\n" % indent)


#
# Magic and Version Chunks
#


class M3DMAGIC(ChunkBase):
    tag = 0x4D4D
    single = "M3D_VERSION version, MDATA mdata, KFDATA kfdata"


class M3D_VERSION(ChunkBase):
    tag = 0x0002
    struct = "long number"


class MDATA(ChunkBase):
    tag = 0x3D3D
    single = (
        "MESH_VERSION version,"
        "MASTER_SCALE scale,"
        "VIEWPORT_LAYOUT layout,"
        "LO_SHADOW_BIAS lo_shadow_bias,"
        "HI_SHADOW_BIAS hi_shadow_bias,"
        "SHADOW_MAP_SIZE shadow_map_size,"
        "SHADOW_SAMPLES shadow_samples,"
        "SHADOW_RANGE shadow_range,"
        "SHADOW_FILTER shadow_filter,"
        # "RAY_BIAS ray_bias,"
        "O_CONSTS o_consts,"
        "AMBIENT_LIGHT ambient_light,"
        "SOLID_BGND solid_background,"
        "BIT_MAP bitmap,"
        "V_GRADIENT v_gradient,"
        "USE_BIT_MAP use_bitmap,"
        "USE_SOLID_BGND use_solid_background,"
        "USE_V_GRADIENT use_v_gradient,"
        "FOG fog,"
        "LAYER_FOG layer_fog,"
        "DISTANCE_CUE distance_cue,"
        "USE_FOG use_fog,"
        "USE_LAYER_FOG use_layer_fog,"
        "USE_DISTANCE_CUE use_distance_cue,"
        "DEFAULT_VIEW default_view,"
        "MARKER marker"
    )
    multiple = "MAT_ENTRY materials, NAMED_OBJECT objects"


class MESH_VERSION(ChunkBase):
    tag = 0x3D3E
    struct = "long number"


class SMAGIC(UndefinedChunk):
    tag = 0x2D2D


class LMAGIC(UndefinedChunk):
    tag = 0x2D3D


class MLIBMAGIC(UndefinedChunk):
    tag = 0x2DAA


class PRJMAGIC(UndefinedChunk):
    tag = 0x3D2C


class MATMAGIC(UndefinedChunk):
    tag = 0x3DFF


#
# Miscellaneous, Global data
#


class MARKER(ChunkBase):
    tag = 0x0001
    struct = "long sum"


class COLOR_F(ChunkBase):
    tag = 0x0010
    struct = "float red, float green, float blue"


class COLOR_24(Color24Chunk):
    tag = 0x0011


class LIN_COLOR_24(Color24Chunk):
    tag = 0x0012


class LIN_COLOR_F(ChunkBase):
    tag = 0x0013
    struct = "float red, float green, float blue"


class INT_PERCENTAGE(OneShortValueChunk):
    tag = 0x0030


class FLOAT_PERCENTAGE(OneFloatValueChunk):
    tag = 0x0031


class MASTER_SCALE(OneFloatValueChunk):
    tag = 0x0100


#
# Coloring, Lighting, Atmosphers Stuff
#


class BIT_MAP(ChunkBase):
    tag = 0x1100
    struct = "string filename"
    swallow = True


class USE_BIT_MAP(ChunkBase):
    tag = 0x1101


class SOLID_BGND(OneColorChunk):
    tag = 0x1200


class USE_SOLID_BGND(ChunkBase):
    tag = 0x1201


class V_GRADIENT(ChunkBase):
    tag = 0x1300
    struct = "float midpoint"
    multiple = "COLOR_F color, LIN_COLOR_F lincolor," "COLOR_24 color, LIN_COLOR_24 lincolor"


class USE_V_GRADIENT(ChunkBase):
    tag = 0x1301


class LO_SHADOW_BIAS(OneFloatValueChunk):
    tag = 0x1400


class HI_SHADOW_BIAS(OneFloatValueChunk):
    tag = 0x1410


class SHADOW_MAP_SIZE(OneShortValueChunk):
    tag = 0x1420


class SHADOW_SAMPLES(OneShortValueChunk):
    tag = 0x1430


class SHADOW_RANGE(OneShortValueChunk):
    tag = 0x1440


class SHADOW_FILTER(OneFloatValueChunk):
    tag = 0x1450


class RAW_BIAS(OneFloatValueChunk):
    tag = 0x1460


class O_CONSTS(ChunkBase):
    tag = 0x1500
    struct = "float plane_x, float plane_y, float plane_z"


class AMBIENT_LIGHT(OneColorChunk):
    tag = 0x2100


class FOG(ChunkBase):
    tag = 0x2200
    struct = "float near_plane, float near_density," "float far_plane, float far_density"
    single = "COLOR_F color, FOG_BGND fog_background"


class USE_FOG(ChunkBase):
    tag = 0x2201


class FOG_BGND(ChunkBase):
    tag = 0x2210


class DISTANCE_CUE(ChunkBase):
    tag = 0x2300
    struct = "float near_plane, float near_density," "float far_plane, float far_density"
    single = "DCUE_BGND dcue_background"


class USE_DISTANCE_CUE(ChunkBase):
    tag = 0x2301


class LAYER_FOG(ChunkBase):
    tag = 0x2302
    struct = "float fog_z_from, float fog_z_to," "float fog_density, long fog_type"
    single = "COLOR_F color"


class USE_LAYER_FOG(ChunkBase):
    tag = 0x2303


class DCUE_BGND(ChunkBase):
    tag = 0x2310


#
# View Data
#


class DEFAULT_VIEW(ChunkBase):
    tag = 0x3000
    single = (
        "VIEW_TOP view, VIEW_BOTTOM view, VIEW_LEFT view,"
        "VIEW_RIGHT view, VIEW_FRONT view, VIEW_BACK view,"
        "VIEW_USER view, VIEW_CAMERA camera"
    )


class ViewChunk(ChunkBase):
    struct = "float target_x, float target_y," "float target_z, float view_width"


class VIEW_TOP(ViewChunk):
    tag = 0x3010


class VIEW_BOTTOM(ViewChunk):
    tag = 0x3020


class VIEW_LEFT(ViewChunk):
    tag = 0x3030


class VIEW_RIGHT(ViewChunk):
    tag = 0x3040


class VIEW_FRONT(ViewChunk):
    tag = 0x3050


class VIEW_BACK(ViewChunk):
    tag = 0x3060


class VIEW_USER(ChunkBase):
    tag = 0x3070
    struct = (
        "float target_x, float target_y,"
        "float target_z, float view_width,"
        "float horiz_angle, float vert_angle,"
        "float back_angle"
    )


class VIEW_CAMERA(ChunkBase):
    tag = 0x3080
    struct = "string name"
    swallow = True


class VIEW_WINDOW(UndefinedChunk):
    tag = 0x3090


#
# Objects
#


class NAMED_OBJECT(ChunkBase):
    tag = 0x4000
    struct = "string name"
    single = (
        "N_TRI_OBJECT obj,"
        "N_DIRECT_LIGHT obj,"
        "N_CAMERA obj,"
        "OBJ_HIDDEN hidden,"
        "OBJ_VIS_LOFTER vis_lofter,"
        "OBJ_DOESNT_CAST doesnt_cast,"
        "OBJ_MATTE matte,"
        "OBJ_DONT_RCVSHADOW dont_rcvshadow,"
        "OBJ_FAST fast,"
        "OBJ_PROCEDURAL procedural,"
        "OBJ_FROZEN frozen"
    )


class OBJ_HIDDEN(ChunkBase):
    tag = 0x4010


class OBJ_VIS_LOFTER(ChunkBase):
    tag = 0x4011


class OBJ_DOESNT_CAST(ChunkBase):
    tag = 0x4012


class OBJ_MATTE(ChunkBase):
    tag = 0x4013


class OBJ_FAST(ChunkBase):
    tag = 0x4014


class OBJ_PROCEDURAL(ChunkBase):
    tag = 0x4015


class OBJ_FROZEN(ChunkBase):
    tag = 0x4016


class OBJ_DONT_RCVSHADOW(ChunkBase):
    tag = 0x4017


class N_TRI_OBJECT(ChunkBase):
    tag = 0x4100
    single = (
        "POINT_ARRAY points,"
        "POINT_FLAG_ARRAY flags,"
        "FACE_ARRAY faces,"
        "TEX_VERTS texverts,"
        "MESH_MATRIX matrix,"
        "MESH_COLOR color,"
        "MESH_TEXTURE_INFO texinfo,"
        "PROC_NAME proc_name,"
        "PROC_DATA proc_data"
    )
    multiple = "MSH_MAT_GROUP matlist"


class POINT_ARRAY(ArrayChunk):
    tag = 0x4110
    struct = "short npoints"

    def read_array(self, fbuf, flags):
        size = 12 * self.npoints
        a = string_to_array(fbuf.read(size), numpy.float32)
        a = numpy.reshape(a, (self.npoints, 3))
        self.array = numpy.array(a)

    def dump_array(self, flo, indent, flags):
        super(POINT_ARRAY, self).dump_array(flo, indent, flags)
        if flags["arraylines"] == 0:
            return
        if flags["arraylines"] < 0:
            n = self.npoints
        else:
            n = min(flags["arraylines"], self.npoints)
        for i in range(n):
            flo.write(
                "%s    %12.4g%12.4g%12.4g\n"
                % (indent, self.array[i, 0], self.array[i, 1], self.array[i, 2])
            )
        if n < self.npoints:
            flo.write("%s            …\n" % indent)

    def write_array(self):
        s = numpy.array(self.array).astype(numpy.float32)
        return array_to_string_destructive(s)

    swallow = True


class POINT_FLAG_ARRAY(ArrayChunk):
    tag = 0x4111
    struct = "short npoints"

    def read_array(self, fbuf, flags):
        size = 2 * self.npoints
        self.array = string_to_array(fbuf.read(size), numpy.uint16)

    def write_array(self):
        s = numpy.array(self.array).astype(numpy.uint16)
        return array_to_string_destructive(s)

    swallow = True


class FACE_ARRAY(ArrayChunk):
    tag = 0x4120
    struct = "short nfaces"
    multiple = "MSH_MAT_GROUP materials"
    single = "SMOOTH_GROUP smoothing, MSH_BOXMAP box"

    def read_array(self, fbuf, flags):
        size = 8 * self.nfaces
        a = string_to_array(fbuf.read(size), numpy.uint16)
        a = numpy.reshape(a, (self.nfaces, 4))
        self.array = numpy.array(a)

    def dump_array(self, flo, indent, flags):
        super(FACE_ARRAY, self).dump_array(flo, indent, flags)
        if flags["arraylines"] == 0:
            return
        if flags["arraylines"] < 0:
            n = self.nfaces
        else:
            n = min(flags["arraylines"], self.nfaces)
        for i in range(n):
            flo.write(
                "%s    %10d%10d%10d%10d\n"
                % (indent, self.array[i, 0], self.array[i, 1], self.array[i, 2], self.array[i, 3])
            )
        if n < self.nfaces:
            flo.write("%s            …\n" % indent)

    def write_array(self):
        s = numpy.array(self.array).astype(numpy.uint16)
        return array_to_string_destructive(s)


class MSH_MAT_GROUP(ArrayChunk):
    tag = 0x4130
    struct = "string name, short mfaces"

    def read_array(self, fbuf, flags):
        size = 2 * self.mfaces
        self.array = string_to_array(fbuf.read(size), numpy.uint16)

    def dump_array(self, flo, indent, flags):
        super(MSH_MAT_GROUP, self).dump_array(flo, indent, flags)
        if flags["arraylines"] == 0:
            return
        if flags["arraylines"] < 0:
            n = self.mfaces
        else:
            n = min(flags["arraylines"], self.mfaces)
        for i in range(n):
            flo.write("%s    %10d\n" % (indent, self.array[i]))
        if n < self.mfaces:
            flo.write("%s            …\n" % indent)

    def write_array(self):
        s = numpy.array(self.array).astype(numpy.uint16)
        return array_to_string_destructive(s)

    swallow = True


class TEX_VERTS(ArrayChunk):
    tag = 0x4140
    struct = "short npoints"

    def read_array(self, fbuf, flags):
        size = 8 * self.npoints
        a = string_to_array(fbuf.read(size), numpy.float32)
        a = numpy.reshape(a, (self.npoints, 2))
        self.array = numpy.array(a)

    def dump_array(self, flo, indent, flags):
        super(TEX_VERTS, self).dump_array(flo, indent, flags)
        if flags["arraylines"] == 0:
            return
        if flags["arraylines"] < 0:
            n = self.npoints
        else:
            n = min(flags["arraylines"], self.npoints)
        for i in range(n):
            flo.write("%s    %12.4g%12.4g\n" % (indent, self.array[i, 0], self.array[i, 1]))
        if n < self.npoints:
            flo.write("%s            …\n" % indent)

    def write_array(self):
        s = numpy.array(self.array).astype(numpy.float32)
        return array_to_string_destructive(s)

    swallow = True


class SMOOTH_GROUP(ArrayChunk):
    tag = 0x4150

    def read_array(self, fbuf, flags):
        self.array = string_to_array(fbuf.read_rest(), numpy.uint32)

    def dump_array(self, flo, indent, flags):
        super(SMOOTH_GROUP, self).dump_array(flo, indent, flags)
        if flags["arraylines"] == 0:
            return
        if flags["arraylines"] < 0:
            n = len(self.array)
        else:
            n = min(flags["arraylines"], len(self.array))
        for i in range(n):
            flo.write("%s    %10d\n" % (indent, self.array[i]))
        if n < len(self.array):
            flo.write("%s            …\n" % indent)

    def write_array(self):
        s = numpy.array(self.array).astype(numpy.uint32)
        return array_to_string_destructive(s)

    swallow = True


class MESH_MATRIX(MatrixChunk):
    tag = 0x4160


class MESH_COLOR(ChunkBase):
    tag = 0x4165
    struct = "byte color_index"


class MESH_TEXTURE_INFO(MatrixChunk):
    tag = 0x4170
    __slots__ = ["icon_width", "icon_height", "cyl_height"]
    struct = (
        "float x_tiling, float y_tiling, float icon_x," "float icon_y, float icon_z, float scaling"
    )

    def read(self, fbuf, flags):
        self.read_struct(fbuf, flags)
        self.read_array(fbuf, flags)
        self.icon_width = self.get_float(fbuf, flags)
        self.icon_height = self.get_float(fbuf, flags)
        self.cyl_height = self.get_float(fbuf, flags)
        self.read_chunks(fbuf, flags)

    def dump(self, flo, indent, flags):
        indent += "    "
        self.dump_header(flo, flags)
        self.dump_struct(flo, indent, flags)
        self.dump_array(flo, indent, flags)
        self.out_attr("icon_width", flo, indent, flags)
        self.out_attr("icon_height", flo, indent, flags)
        self.out_attr("cyl_height", flo, indent, flags)
        self.dump_chunks(flo, indent, flags)

    def write(self):
        s = []
        s.append(self.write_struct())
        s.append(self.write_array())
        s.append(self.out_float(self.icon_width))
        s.append(self.out_float(self.icon_height))
        s.append(self.out_float(self.cyl_height))
        s.append(self.write_chunks())
        return "".join(s)


class PROC_NAME(ChunkBase):
    tag = 0x4181
    struct = "string value"
    swallow = True


class PROC_DATA(UndefinedChunk):
    tag = 0x4181
    struct = "string value"
    swallow = True


class MSH_BOXMAP(ChunkBase):
    tag = 0x4190
    struct = "string front, string back, string left," "string right, string top, string bottom"
    swallow = True


class N_DIRECT_LIGHT(ChunkBase):
    tag = 0x4600
    struct = "float light_x, float light_y, float light_z"
    single = (
        "COLOR_F color, COLOR_24 color,"
        "DL_OFF switch,"
        "DL_OUTER_RANGE outer_range,"
        "DL_INNER_RANGE inner_range,"
        "DL_MULTIPLIER multiplier,"
        "DL_SPOTLIGHT spotlight,"
        "DL_ATTENUATE attenuate"
    )
    multiple = "DL_EXCLUDE excludes"


class DL_SPOTLIGHT(ChunkBase):
    tag = 0x4610
    struct = "float spot_x, float spot_y, float spot_z," "float hotspot_angle, float falloff_angle"
    single = (
        "DL_SPOT_ROLL roll_angle,"
        "DL_SHADOWED shadowed,"
        "DL_LOCAL_SHADOW2 local_shadow,"
        "DL_SEE_CONE see_cone,"
        "DL_SPOT_RECTANGULAR rectangular,"
        "DL_SPOT_ASPECT aspect,"
        "DL_SPOT_PROJECTOR projector,"
        "DL_SPOT_OVERSHOOT overshoot,"
        "DL_RAY_BIAS bias,"
        "DL_RAYSHADE rayshade"
    )


class DL_OFF(ChunkBase):
    tag = 0x4620


class DL_ATTENUATE(ChunkBase):
    tag = 0x4625


class DL_RAYSHADE(ChunkBase):
    tag = 0x4627


class DL_SHADOWED(ChunkBase):
    tag = 0x4630


class DL_LOCAL_SHADOW2(ChunkBase):
    tag = 0x4630
    struct = "float low_bias, float filter, short mapsize"


class DL_SEE_CONE(ChunkBase):
    tag = 0x4650


class DL_SPOT_RECTANGULAR(ChunkBase):
    tag = 0x4651


class DL_SPOT_OVERSHOOT(ChunkBase):
    tag = 0x4652


class DL_SPOT_PROJECTOR(ChunkBase):
    tag = 0x4653
    struct = "string filename"
    swallow = True


class DL_EXCLUDE(ChunkBase):
    tag = 0x4654
    struct = "string value"
    swallow = True


class DL_SPOT_ROLL(OneFloatValueChunk):
    tag = 0x4656


class DL_SPOT_ASPECT(OneFloatValueChunk):
    tag = 0x4657


class DL_RAY_BIAS(OneFloatValueChunk):
    tag = 0x4658


class DL_INNER_RANGE(OneFloatValueChunk):
    tag = 0x4659


class DL_OUTER_RANGE(OneFloatValueChunk):
    tag = 0x465A


class DL_MULTIPLIER(OneFloatValueChunk):
    tag = 0x465B


class N_CAMERA(ChunkBase):
    tag = 0x4700
    struct = (
        "float camera_x, float camera_y, float camera_z,"
        "float target_x, float target_y, float target_z,"
        "float bank_angle, float focal_length"
    )
    single = "CAM_SEE_CONE see_cone, CAM_RANGES ranges"


class CAM_SEE_CONE(ChunkBase):
    tag = 0x4710


class CAM_RANGES(ChunkBase):
    tag = 0x4720
    struct = "float near, float far"


#
# Viewport Data
#


class VIEWPORT_LAYOUT(ChunkBase):
    tag = 0x7001
    struct = (
        "short form, short top, short ready,"
        "short wstate, short swapws, short swapport,"
        "short swapcur"
    )
    single = "VIEWPORT_SIZE size"
    multiple = "VIEWPORT_DATA data, VIEWPORT_DATA_3 data"


class ViewportDataChunk(ChunkBase):
    struct = (
        "short flags, short axis_lockout, short win_x,"
        "short win_y, short win_w, short win_h,"
        "short win_view, float zoom, float worldcenter_x,"
        "float worldcenter_y, float worldcenter_z,"
        "float horiz_ang, float vert_ang,"
        "string cameraname"
    )
    swallow = True


class VIEWPORT_DATA(ViewportDataChunk):
    tag = 0x7011


class VIEWPORT_DATA_3(ViewportDataChunk):
    tag = 0x7012


class VIEWPORT_SIZE(ChunkBase):
    tag = 0x7020
    struct = "short x, short y, short w, short h"


class NETWORK_VIEW(UndefinedChunk):
    tag = 0x7030


#
# Material Data
#


class MAT_NAME(ChunkBase):
    tag = 0xA000
    struct = "string value"
    swallow = True


class MAT_AMBIENT(OneColorChunk):
    tag = 0xA010


class MAT_DIFFUSE(OneColorChunk):
    tag = 0xA020


class MAT_SPECULAR(OneColorChunk):
    tag = 0xA030


class MAT_SHININESS(OnePercentageChunk):
    tag = 0xA040


class MAT_SHIN2PCT(OnePercentageChunk):
    tag = 0xA041


class MAT_SHIN3PCT(OnePercentageChunk):
    tag = 0xA042


class MAT_TRANSPARENCY(OnePercentageChunk):
    tag = 0xA050


class MAT_XPFALL(OnePercentageChunk):
    tag = 0xA052


class MAT_REFBLUR(OnePercentageChunk):
    tag = 0xA053


class MAT_SELF_ILLUM(ChunkBase):
    tag = 0xA080


class MAT_TWO_SIDE(ChunkBase):
    tag = 0xA081


class MAT_DECAL(ChunkBase):
    tag = 0xA082


class MAT_ADDITIVE(ChunkBase):
    tag = 0xA083


class MAT_SELF_ILPCT(OnePercentageChunk):
    tag = 0xA084


class MAT_WIRE(ChunkBase):
    tag = 0xA085


class MAT_SUPERSMP(UndefinedChunk):
    tag = 0xA086


class MAT_WIRESIZE(OneFloatValueChunk):
    tag = 0xA087


class MAT_FACEMAP(ChunkBase):
    tag = 0xA088


class MAT_XPFALLIN(UndefinedChunk):
    tag = 0xA08A


class MAT_PHONGSOFT(ChunkBase):
    tag = 0xA08C


class MAT_WIREABS(ChunkBase):
    tag = 0xA08E


class MAT_SHADING(OneShortValueChunk):
    tag = 0xA100


class MAT_TEXMAP(TextureChunk):
    tag = 0xA200


class MAT_SPECMAP(TextureChunk):
    tag = 0xA204


class MAT_OPACMAP(TextureMaskChunk):
    tag = 0xA210


class MAT_REFLMAP(ChunkBase):
    tag = 0xA220
    single = "INT_PERCENTAGE pct, MAT_MAPNAME filename"


class MAT_BUMPMAP(TextureMaskChunk):
    tag = 0xA230


class MAT_USE_XPFALL(ChunkBase):
    tag = 0xA240


class MAT_USE_REFBLUR(ChunkBase):
    tag = 0xA250


class MAT_BUMP_PERCENT(ChunkBase):
    tag = 0xA252
    struct = "short value"


class MAT_MAPNAME(ChunkBase):
    tag = 0xA300
    struct = "string value"
    swallow = True


class MAT_ACUBIC(ChunkBase):
    tag = 0xA310
    struct = "byte shade, byte antialias, short rflags," "long mapsize, long frame"


class MAT_TEX2MAP(TextureChunk):
    tag = 0xA33A


class MAT_SHINMAP(TextureMaskChunk):
    tag = 0xA33C


class MAT_SELFIMAP(TextureChunk):
    tag = 0xA33D


class MAT_TEXMASK(TextureMaskChunk):
    tag = 0xA33E


class MAT_TEXT2MASK(TextureMaskChunk):
    tag = 0xA340


class MAT_OPACMASK(TextureMaskChunk):
    tag = 0xA342


class MAT_BUMPMASK(TextureMaskChunk):
    tag = 0xA344


class MAT_SHINMASK(TextureMaskChunk):
    tag = 0xA346


class MAT_SPECMASK(TextureMaskChunk):
    tag = 0xA348


class MAT_SELFIMASK(TextureChunk):
    tag = 0xA34A


class MAT_REFLMASK(TextureChunk):
    tag = 0xA34C


class MAT_MAP_TILING(OneShortValueChunk):
    tag = 0xA351


class MAT_MAP_TEXBLUR(OneFloatValueChunk):
    tag = 0xA353


class MAT_MAP_USCALE(OneFloatValueChunk):
    tag = 0xA354


class MAT_MAP_VSCALE(OneFloatValueChunk):
    tag = 0xA356


class MAT_MAP_UOFFSET(OneFloatValueChunk):
    tag = 0xA358


class MAT_MAP_VOFFSET(OneFloatValueChunk):
    tag = 0xA35A


class MAT_MAP_ANG(OneFloatValueChunk):
    tag = 0xA35C


class MAT_MAP_COL1(Color24Chunk):
    tag = 0xA360


class MAT_MAP_COL2(Color24Chunk):
    tag = 0xA362


class MAT_MAP_RCOL(Color24Chunk):
    tag = 0xA364


class MAT_MAP_GCOL(Color24Chunk):
    tag = 0xA366


class MAT_MAP_BCOL(Color24Chunk):
    tag = 0xA368


class MAT_ENTRY(ChunkBase):
    tag = 0xAFFF
    single = (
        "MAT_NAME name,"
        "MAT_AMBIENT ambient,"
        "MAT_DIFFUSE diffuse,"
        "MAT_SPECULAR specular,"
        "MAT_SHININESS shininess,"
        "MAT_SHIN2PCT shin2pct,"
        "MAT_TRANSPARENCY transparency,"
        "MAT_XPFALL xpfall,"
        "MAT_REFBLUR refblur,"
        "MAT_SELF_ILLUM self_illum,"
        "MAT_SHADING shading,"
        "MAT_SELF_ILPCT self_ilpct,"
        "MAT_USE_XPFALL use_xpfall,"
        "MAT_USE_REFBLUR use_refblur,"
        "MAT_TWO_SIDE two_side,"
        "MAT_ADDITIVE additive,"
        "MAT_WIRE wire,"
        "MAT_FACEMAP facemap,"
        "MAT_PHONGSOFT phongsoft,"
        "MAT_WIRESIZE wiresize,"
        "MAT_DECAL decal,"
        "MAT_TEXMAP texmap,"
        "MAT_SXP_TEXT_DATA sxp_text_data,"
        "MAT_TEXMASK texmask,"
        "MAT_SXP_TEXT_MASKDATA sxp_text_maskdata,"
        "MAT_TEX2MAP tex2map,"
        "MAT_SXP_TEXT2_DATA sxp_text2_data,"
        "MAT_TEXT2MASK text2mask,"
        "MAT_SXP_TEXT2_MASKDATA sxp_text2_maskdata,"
        "MAT_OPACMAP opacmap,"
        "MAT_SXP_OPAC_DATA sxp_opac_data,"
        "MAT_OPACMASK opac_mask,"
        "MAT_SXP_OPAC_MASKDATA sxp_opac_maskdata,"
        "MAT_BUMPMAP bumpmap,"
        "MAT_SXP_BUMP_DATA sxp_bump_data,"
        "MAT_BUMPMASK bumpmask,"
        "MAT_SXP_BUMP_MASKDATA sxp_bump_maskdata,"
        "MAT_SPECMAP specmap,"
        "MAT_SXP_SPEC_DATA sxp_spec_data,"
        "MAT_SPECMASK specmask,"
        "MAT_SXP_SPEC_MASKDATA sxp_spec_maskdata,"
        "MAT_SHINMAP shinmap,"
        "MAT_SXP_SHIN_DATA sxp_shin_data,"
        "MAT_SHINMASK shinmask,"
        "MAT_SXP_SHIN_MASKDATA sxp_shin_maskdata,"
        "MAT_SELFIMAP selfimap,"
        "MAT_SXP_SELFI_DATA sxp_selfi_data,"
        "MAT_SELFIMASK selfimask,"
        "MAT_SXP_SELFI_MASKDATA sxp_selfi_maskdata,"
        "MAT_REFLMAP reflmap,"
        "MAT_REFLMASK reflmask,"
        "MAT_SXP_REFL_MASKDATA sxp_refl_maskdata,"
        "MAT_ACUBIC acubic"
    )


class MAT_SXP_TEXT_DATA(UndefinedChunk):
    tag = 0xA320


class MAT_SXP_TEXT2_DATA(UndefinedChunk):
    tag = 0xA321


class MAT_SXP_OPAC_DATA(UndefinedChunk):
    tag = 0xA322


class MAT_SXP_BUMP_DATA(UndefinedChunk):
    tag = 0xA324


class MAT_SXP_SPEC_DATA(UndefinedChunk):
    tag = 0xA325


class MAT_SXP_SHIN_DATA(UndefinedChunk):
    tag = 0xA326


class MAT_SXP_SELFI_DATA(UndefinedChunk):
    tag = 0xA328


class MAT_SXP_TEXT_MASKDATA(UndefinedChunk):
    tag = 0xA32A


class MAT_SXP_TEXT2_MASKDATA(UndefinedChunk):
    tag = 0xA32C


class MAT_SXP_OPAC_MASKDATA(UndefinedChunk):
    tag = 0xA32E


class MAT_SXP_BUMP_MASKDATA(UndefinedChunk):
    tag = 0xA330


class MAT_SXP_SPEC_MASKDATA(UndefinedChunk):
    tag = 0xA332


class MAT_SXP_SHIN_MASKDATA(UndefinedChunk):
    tag = 0xA334


class MAT_SXP_SELFI_MASKDATA(UndefinedChunk):
    tag = 0xA336


class MAT_SXP_REFL_MASKDATA(UndefinedChunk):
    tag = 0xA338


#
# Keyframe Section
#


class KFDATA(ChunkBase):
    tag = 0xB000
    single = "KFHDR kfhdr, KFSEG kfseg, KFCURTIME kfcurtime"
    multiple = (
        "OBJECT_NODE_TAG object_nodes,"
        "CAMERA_NODE_TAG camera_nodes,"
        "TARGET_NODE_TAG target_nodes,"
        "LIGHT_NODE_TAG light_nodes,"
        "SPOTLIGHT_NODE_TAG spotlight_nodes,"
        "L_TARGET_NODE_TAG l_target_nodes,"
        "AMBIENT_NODE_TAG ambient_nodes"
    )


class KFHDR(ChunkBase):
    tag = 0xB00A
    struct = "short revision, string filename, long anim_length"


class KFSEG(ChunkBase):
    tag = 0xB008
    struct = "long first_frame, long last_frame"


class KFCURTIME(ChunkBase):
    tag = 0xB009
    struct = "long current_frame"


class AMBIENT_NODE_TAG(ChunkBase):
    tag = 0xB001
    single = "NODE_ID node_id, NODE_HDR node_hdr, COL_TRACK_TAG col_track"


class OBJECT_NODE_TAG(ChunkBase):
    tag = 0xB002
    single = (
        "NODE_ID node_id,"
        "NODE_HDR node_hdr,"
        "PIVOT pivot,"
        "INSTANCE_NAME instance_name,"
        "BOUNDBOX bounding_box,"
        "POS_TRACK_TAG pos_track,"
        "ROT_TRACK_TAG rot_track,"
        "SCL_TRACK_TAG scl_track,"
        "MORPH_TRACK_TAG morph_track,"
        "HIDE_TRACK_TAG hide_track,"
        "MORPH_SMOOTH morph_smooth"
    )


class CAMERA_NODE_TAG(ChunkBase):
    tag = 0xB003
    single = (
        "NODE_ID node_id,"
        "NODE_HDR node_hdr,"
        "POS_TRACK_TAG pos_track,"
        "FOV_TRACK_TAG fov_track,"
        "ROLL_TRACK_TAG roll_track"
    )


class TARGET_NODE_TAG(ChunkBase):
    tag = 0xB004
    single = "NODE_ID node_id," "NODE_HDR node_hdr," "POS_TRACK_TAG pos_track"


class LIGHT_NODE_TAG(ChunkBase):
    tag = 0xB005
    single = (
        "NODE_ID node_id," "NODE_HDR node_hdr," "POS_TRACK_TAG pos_track," "COL_TRACK_TAG col_track"
    )


class SPOTLIGHT_NODE_TAG(ChunkBase):
    tag = 0xB007
    single = (
        "NODE_ID node_id,"
        "NODE_HDR node_hdr,"
        "POS_TRACK_TAG pos_track,"
        "COL_TRACK_TAG col_track,"
        "HOT_TRACK_TAG hot_track,"
        "FALL_TRACK_TAG fall_track,"
        "ROLL_TRACK_TAG roll_track"
    )


class L_TARGET_NODE_TAG(ChunkBase):
    tag = 0xB006
    single = (
        "NODE_ID node_id," "NODE_HDR node_hdr," "POS_TRACK_TAG pos_track," "COL_TRACK_TAG col_track"
    )


class NODE_ID(ChunkBase):
    tag = 0xB030
    struct = "short id"


class NODE_HDR(ChunkBase):
    tag = 0xB010
    struct = "string name, short flags1, short flags2, short parent"


class PIVOT(ChunkBase):
    tag = 0xB013
    struct = "float pivot_x, float pivot_y, float pivot_z"


class INSTANCE_NAME(ChunkBase):
    tag = 0xB011
    struct = "string name"


class MORPH_SMOOTH(ChunkBase):
    tag = 0xB015
    struct = "float smooth_angle"


class BOUNDBOX(ChunkBase):
    tag = 0xB014
    struct = "float min_x, float min_y, float min_z," "float max_x, float max_y, float max_z"


class POS_TRACK_TAG(TrackChunk):
    tag = 0xB020
    keyframe = "float pos_x, float pos_y, float pos_z"


class COL_TRACK_TAG(TrackChunk):
    tag = 0xB025
    keyframe = "float red, float green, float blue"


class ROT_TRACK_TAG(TrackChunk):
    tag = 0xB021
    keyframe = "float angle, float axis_x, float axis_y, float axis_z"


class SCL_TRACK_TAG(TrackChunk):
    tag = 0xB022
    keyframe = "float scl_x, float scl_y, float scl_z"


class MORPH_TRACK_TAG(TrackChunk):
    tag = 0xB026
    keyframe = "string name"


class FOV_TRACK_TAG(TrackChunk):
    tag = 0xB023
    keyframe = "float angle"


class ROLL_TRACK_TAG(TrackChunk):
    tag = 0xB024
    keyframe = "float angle"


class HOT_TRACK_TAG(TrackChunk):
    tag = 0xB027
    keyframe = "float angle"


class FALL_TRACK_TAG(TrackChunk):
    tag = 0xB028
    keyframe = "float angle"


class HIDE_TRACK_TAG(UndefinedChunk):
    tag = 0xB029


#
# Misc
#


class DUMMY(ChunkBase):
    tag = 0xFFFF


#
# Obsolete Chunks
#


class VIEWPORT_LAYOUT_OLD(UndefinedChunk):
    tag = 0x7000


class VIEWPORT_DATA_OLD(UndefinedChunk):
    tag = 0x7010


class OLD_MAT_GROUP(UndefinedChunk):
    tag = 0x4131


class MAT_MAP_TILING_OLD(UndefinedChunk):
    tag = 0xA350


class MAT_MAP_TEXBLUR_OLD(UndefinedChunk):
    tag = 0xA352


#
# Functions to operate on chunks
#


def read_3ds_mem(membuf, check_magic=True, tight=False, recover=True):
    """Create a 3DS DOM from a memory buffer.

        dom = read_3ds_mem(buffer,check_magic=True,tight=False,
                           recover=True)

    buffer: is an image of the 3DS file in memory.  It could be
    a string, a mapped file, or something else.

    check_magic: If true, this function checks that the top level
    chunk is the 3DS magic chunk (0x4D4D), and raises an exception
    if it is not.

    tight: Whether to use tighter error checking.  Try disabling
    if getting 3DS format errors.

    recover: Whether to emit an Error chunk when an error is found;
    otherwise raise an exception.

    """

    if check_magic:
        tag, length = struct.unpack("<HL", membuf[:6])
        if tag != 0x4D4D:
            raise File3dsFormatError("Not a 3D Studio file.")
    fbuf = FileLikeBuffer(membuf, 0, len(membuf))
    flags = {"tight": tight, "recover": recover}
    return ChunkBase.get_chunk(fbuf, flags)


def read_3ds_file(filename, check_magic=True, tight=False, recover=True):
    """Create a 3DS DOM from a file.

        dom = read_3ds_file(filename,check_magic=True,tight=False,
                            recover=True)

    filename: name of a 3DS file.

    check_magic: If true, this function checks that the top level
    chunk is the 3DS magic chunk (0x4D4D), and raises an exception
    if it is not.

    tight: Whether to use tighter error checking.  Try disabling
    if getting 3DS format errors.

    recover: Whether to emit an Error chunk when an error is found;
    otherwise raise an exception.

    """

    with open(filename, "rb") as flo:
        return read_3ds_mem(flo.read(), check_magic, tight, recover)


def write_3ds_mem(dom, check_magic=True):
    """Output a 3DS DOM as a string.

        buf = write_3ds_mem(dom,check_magic=True)

    dom: the 3DS dom

    check_magic: If true, this function checks that the top level
    chunk is the 3DS magic chunk (0x4D4D), and raises an exception
    if it is not.

    """

    if check_magic and dom.tag != 0x4D4D:
        raise File3dsFormatError("Not a 3D Studio file.")
    s = dom.write()
    length = len(s) + 6
    return struct.pack("<HL", dom.tag, length) + s


def write_3ds_file(filename, dom, check_magic=True):
    """Write a 3DS file.

        buf = write_3ds_file(filename,dom,check_magic=True)

    filename: name of a 3DS file to write.

    dom: the 3DS dom

    check_magic: If true, this function checks that the top level
    chunk is the 3DS magic chunk (0x4D4D), and raises an exception
    if it is not.

    """

    if check_magic and dom.tag != 0x4D4D:
        raise File3dsFormatError("Not a 3D Studio file.")
    with open(filename, "wb") as flo:
        flo.write(write_3ds_mem(dom, False))


def dump_3ds_chunk(chunk, flo, arraylines=10, indent=""):
    """Dump a 3DS DOM to a file stream.

        dump_3ds_chunk(filename,flo,arraylines=10,indent='')

    chunk: The 3DS chunk to dump

    flo: The file-like-object to dump output to (for example, sys.stdout)

    arraylines: Max number of lines of array data to dump. If negative,
    dump the whole array.

    indent: Prefix string to all lines dumped; used to indent.

    """

    flags = {"arraylines": arraylines}
    chunk.dump(flo, indent, flags)


def dump_3ds_file(filename, flo, arraylines=10, tight=False, recover=True):
    """Dump a text representation of 3DS DOM to a file stream.

        dump_3ds_file(filename,flo,arraylines=2,tight=False,
                      recover=True)

    filename: The 3DS file to dump

    flo: The file-like-object to dump output to (for example, sys.stdout)

    arraylines: Max number of lines of array data to dump. If negative,
    dump the whole array.

    tight: Whether to use tighter error checking.  Try disabling
    if getting 3DS format errors.

    recover: Whether to emit an Error chunk when an error is found;
    otherwise raise an exception.

    """

    check_magic = False
    dom = read_3ds_file(filename, check_magic, tight, recover)
    dump_3ds_chunk(dom, flo, arraylines)


def remove_errant_chunks(chunk):
    """Recursively remove any errant chunks.

        remove_errant_chunks(chunk)

    This recursively removes chunks that might prevent
    successfully writing the DOM.

    """

    for attr in chunk.single_order:
        c = getattr(chunk, attr)
        if isinstance(c, ErrorChunk):
            setattr(chunk, attr, None)
        elif c:
            remove_errant_chunks(c)
    mchunks = chunk.multiple_order[:]
    if chunk.freechunks:
        mchunks.append("subchunks")
    for attr in mchunks:
        cl = [x for x in getattr(chunk, attr) if not isinstance(x, ErrorChunk)]
        setattr(chunk, attr, cl)
        for c in cl:
            remove_errant_chunks(c)
