/*
---------------------------------------------------------------------------
Open Asset Import Library (assimp)
---------------------------------------------------------------------------

Copyright (c) 2006-2025, assimp team

All rights reserved.

Redistribution and use of this software in source and binary forms,
with or without modification, are permitted provided that the following
conditions are met:

* Redistributions of source code must retain the above
  copyright notice, this list of conditions and the
  following disclaimer.

* Redistributions in binary form must reproduce the above
  copyright notice, this list of conditions and the
  following disclaimer in the documentation and/or other
  materials provided with the distribution.

* Neither the name of the assimp team, nor the names of its
  contributors may be used to endorse or promote products
  derived from this software without specific prior
  written permission of the assimp team.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
---------------------------------------------------------------------------
*/

/** @file  3DSLoader.cpp
 *  @brief Implementation of the 3ds importer class
 *
 *  http://www.the-labs.com/Blender/3DS-details.html
 */

#ifndef ASSIMP_BUILD_NO_3DS_IMPORTER

#include "3DSLoader.h"
#include <assimp/StringComparison.h>
#include <assimp/importerdesc.h>
#include <assimp/scene.h>
#include <assimp/DefaultLogger.hpp>
#include <assimp/IOSystem.hpp>

namespace Assimp {

static constexpr aiImporterDesc desc = {
    "Discreet 3DS Importer",
    "",
    "",
    "Limited animation support",
    aiImporterFlags_SupportBinaryFlavour,
    0,
    0,
    0,
    0,
    "3ds prj"
};

// ------------------------------------------------------------------------------------------------
// Begins a new parsing block
// - Reads the current chunk and validates it
// - computes its length
#define ASSIMP_3DS_BEGIN_CHUNK()                                              \
    while (true) {                                                            \
        if (stream->GetRemainingSizeToLimit() < sizeof(Discreet3DS::Chunk)) { \
            return;                                                           \
        }                                                                     \
        Discreet3DS::Chunk chunk;                                             \
        ReadChunk(&chunk);                                                    \
        int chunkSize = chunk.Size - sizeof(Discreet3DS::Chunk);              \
        if (chunkSize <= 0)                                                   \
            continue;                                                         \
        const unsigned int oldReadLimit = stream->SetReadLimit(               \
                stream->GetCurrentPos() + chunkSize);

// ------------------------------------------------------------------------------------------------
// End a parsing block
// Must follow at the end of each parsing block, reset chunk end marker to previous value
#define ASSIMP_3DS_END_CHUNK()                  \
    stream->SkipToReadLimit();                  \
    stream->SetReadLimit(oldReadLimit);         \
    if (stream->GetRemainingSizeToLimit() == 0) \
        return;                                 \
    }

// ------------------------------------------------------------------------------------------------
// Constructor to be privately used by Importer
Discreet3DSImporter::Discreet3DSImporter() :
        stream(), mLastNodeIndex(), mCurrentNode(), mRootNode(), mScene(), mMasterScale(), bHasBG(), bIsPrj() {
    // empty
}

// ------------------------------------------------------------------------------------------------
// Returns whether the class can handle the format of the given file.
bool Discreet3DSImporter::CanRead(const std::string &pFile, IOSystem *pIOHandler, bool /*checkSig*/) const {
    static constexpr uint16_t token[] = { 0x4d4d, 0x3dc2 /*, 0x3daa */ };
    return CheckMagicToken(pIOHandler, pFile, token, AI_COUNT_OF(token), 0, sizeof token[0]);
}

// ------------------------------------------------------------------------------------------------
// Loader registry entry
const aiImporterDesc *Discreet3DSImporter::GetInfo() const {
    return &desc;
}

// ------------------------------------------------------------------------------------------------
// Setup configuration properties
void Discreet3DSImporter::SetupProperties(const Importer * /*pImp*/) {
    // nothing to be done for the moment
}

// ------------------------------------------------------------------------------------------------
// Imports the given file into the given scene structure.
void Discreet3DSImporter::InternReadFile(const std::string &pFile,
        aiScene *pScene, IOSystem *pIOHandler) {

    auto theFile = pIOHandler->Open(pFile, "rb");
    if (!theFile) {
        throw DeadlyImportError("3DS: Could not open ", pFile);
    }

    StreamReaderLE theStream(theFile);

    // We should have at least one chunk
    if (theStream.GetRemainingSize() < 16) {
        throw DeadlyImportError("3DS file is either empty or corrupt: ", pFile);
    }
    this->stream = &theStream;

    // Allocate our temporary 3DS representation
    D3DS::Scene _scene;
    mScene = &_scene;

    // Initialize members
    D3DS::Node _rootNode("UNNAMED");
    mLastNodeIndex = -1;
    mCurrentNode = &_rootNode;
    mRootNode = mCurrentNode;
    mRootNode->mHierarchyPos = -1;
    mRootNode->mHierarchyIndex = -1;
    mRootNode->mParent = nullptr;
    mMasterScale = 1.0f;
    mBackgroundImage = std::string();
    bHasBG = false;
    bIsPrj = false;

    // Parse the file
    ParseMainChunk();

    // Process all meshes in the file. First check whether all
    // face indices have valid values. The generate our
    // internal verbose representation. Finally compute normal
    // vectors from the smoothing groups we read from the
    // file.
    for (auto &mesh : mScene->mMeshes) {
        if (mesh.mFaces.size() > 0 && mesh.mPositions.size() == 0) {
            throw DeadlyImportError("3DS file contains faces but no vertices: ", pFile);
        }
        CheckIndices(mesh);
        MakeUnique(mesh);
        ComputeNormalsWithSmoothingsGroups<D3DS::Face>(mesh);
    }

    // Replace all occurrences of the default material with a
    // valid material. Generate it if no material containing
    // DEFAULT in its name has been found in the file
    ReplaceDefaultMaterial();

    // Convert the scene from our internal representation to an
    // aiScene object. This involves copying all meshes, lights
    // and cameras to the scene
    ConvertScene(pScene);

    // Generate the node graph for the scene. This is a little bit
    // tricky since we'll need to split some meshes into sub-meshes
    GenerateNodeGraph(pScene);

    // Now apply the master scaling factor to the scene
    ApplyMasterScale(pScene);

    // Our internal scene representation and the root
    // node will be automatically deleted, so the whole hierarchy will follow

    AI_DEBUG_INVALIDATE_PTR(mRootNode);
    AI_DEBUG_INVALIDATE_PTR(mScene);
    AI_DEBUG_INVALIDATE_PTR(this->stream);
}

// ------------------------------------------------------------------------------------------------
// Applies a master-scaling factor to the imported scene
void Discreet3DSImporter::ApplyMasterScale(aiScene *pScene) {
    // There are some 3DS files with a zero scaling factor
    if (!mMasterScale)
        mMasterScale = 1.0f;
    else
        mMasterScale = 1.0f / mMasterScale;

    // Construct an uniform scaling matrix and multiply with it
    pScene->mRootNode->mTransformation *= aiMatrix4x4(
            mMasterScale, 0.0f, 0.0f, 0.0f,
            0.0f, mMasterScale, 0.0f, 0.0f,
            0.0f, 0.0f, mMasterScale, 0.0f,
            0.0f, 0.0f, 0.0f, 1.0f);

    // Check whether a scaling track is assigned to the root node.
}

// ------------------------------------------------------------------------------------------------
// Reads a new chunk from the file
void Discreet3DSImporter::ReadChunk(Discreet3DS::Chunk *pcOut) {
    ai_assert(pcOut != nullptr);

    pcOut->Flag = stream->GetI2();
    pcOut->Size = stream->GetI4();

    if (pcOut->Size - sizeof(Discreet3DS::Chunk) > stream->GetRemainingSize()) {
        throw DeadlyImportError("Chunk is too large");
    }

    if (pcOut->Size - sizeof(Discreet3DS::Chunk) > stream->GetRemainingSizeToLimit()) {
        ASSIMP_LOG_ERROR("3DS: Chunk overflow");
    }
}

// ------------------------------------------------------------------------------------------------
// Skip a chunk
void Discreet3DSImporter::SkipChunk() {
    Discreet3DS::Chunk psChunk;
    ReadChunk(&psChunk);

    stream->IncPtr(psChunk.Size - sizeof(Discreet3DS::Chunk));
    return;
}

// ------------------------------------------------------------------------------------------------
// Process the primary chunk of the file
void Discreet3DSImporter::ParseMainChunk() {
    ASSIMP_3DS_BEGIN_CHUNK();

    // get chunk type
    switch (chunk.Flag) {

    case Discreet3DS::CHUNK_PRJ:
        bIsPrj = true;
        break;
    case Discreet3DS::CHUNK_MAIN:
        ParseEditorChunk();
        break;
    };

    ASSIMP_3DS_END_CHUNK();
#if defined(__clang__)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunreachable-code-return"
#endif
    // recursively continue processing this hierarchy level
    return ParseMainChunk();
#if defined(__clang__)
#pragma clang diagnostic pop
#endif
}

// ------------------------------------------------------------------------------------------------
void Discreet3DSImporter::ParseEditorChunk() {
    ASSIMP_3DS_BEGIN_CHUNK();

    // get chunk type
    switch (chunk.Flag) {
    case Discreet3DS::CHUNK_OBJMESH:

        ParseObjectChunk();
        break;

    // NOTE: In several documentations in the internet this
    // chunk appears at different locations
    case Discreet3DS::CHUNK_KEYFRAMER:

        ParseKeyframeChunk();
        break;

    case Discreet3DS::CHUNK_VERSION: {
        // print the version number
        char buff[10];
        ASSIMP_itoa10(buff, stream->GetI2());
        ASSIMP_LOG_INFO("3DS file format version: ", buff);
    } break;
    };
    ASSIMP_3DS_END_CHUNK();
}

// ------------------------------------------------------------------------------------------------
void Discreet3DSImporter::ParseObjectChunk() {
    ASSIMP_3DS_BEGIN_CHUNK();

    // get chunk type
    switch (chunk.Flag) {
    case Discreet3DS::CHUNK_OBJBLOCK: {
        unsigned int cnt = 0;
        const char *sz = (const char *)stream->GetPtr();

        // Get the name of the geometry object
        while (stream->GetI1())
            ++cnt;
        ParseChunk(sz, cnt);
    } break;

    case Discreet3DS::CHUNK_MAT_MATERIAL:

        // Add a new material to the list
        mScene->mMaterials.emplace_back(std::string("UNNAMED_" + ai_to_string(mScene->mMaterials.size())));
        ParseMaterialChunk();
        break;

    case Discreet3DS::CHUNK_AMBCOLOR:

        // This is the ambient base color of the scene.
        // We add it to the ambient color of all materials
        ParseColorChunk(&mClrAmbient, true);
        if (is_qnan(mClrAmbient.r)) {
            // We failed to read the ambient base color.
            ASSIMP_LOG_ERROR("3DS: Failed to read ambient base color");
            mClrAmbient.r = mClrAmbient.g = mClrAmbient.b = 0.0f;
        }
        break;

    case Discreet3DS::CHUNK_BIT_MAP: {
        // Specifies the background image. The string should already be
        // properly 0 terminated but we need to be sure
        unsigned int cnt = 0;
        const char *sz = (const char *)stream->GetPtr();
        while (stream->GetI1())
            ++cnt;
        mBackgroundImage = std::string(sz, cnt);
    } break;

    case Discreet3DS::CHUNK_BIT_MAP_EXISTS:
        bHasBG = true;
        break;

    case Discreet3DS::CHUNK_MASTER_SCALE:
        // Scene master scaling factor
        mMasterScale = stream->GetF4();
        break;
    };
    ASSIMP_3DS_END_CHUNK();
}

// ------------------------------------------------------------------------------------------------
void Discreet3DSImporter::ParseChunk(const char *name, unsigned int num) {
    ASSIMP_3DS_BEGIN_CHUNK();

    // IMPLEMENTATION NOTE;
    // Cameras or lights define their transformation in their parent node and in the
    // corresponding light or camera chunks. However, we read and process the latter
    // to be able to return valid cameras/lights even if no scenegraph is given.

    // get chunk type
    switch (chunk.Flag) {
    case Discreet3DS::CHUNK_TRIMESH: {
        // this starts a new triangle mesh
        mScene->mMeshes.emplace_back(std::string(name, num));

        // Read mesh chunks
        ParseMeshChunk();
    } break;

    case Discreet3DS::CHUNK_LIGHT: {
        // This starts a new light
        aiLight *light = new aiLight();
        mScene->mLights.push_back(light);

        light->mName.Set(std::string(name, num));

        // First read the position of the light
        light->mPosition.x = stream->GetF4();
        light->mPosition.y = stream->GetF4();
        light->mPosition.z = stream->GetF4();

        light->mColorDiffuse = aiColor3D(1.f, 1.f, 1.f);

        // Now check for further subchunks
        if (!bIsPrj) /* fixme */
            ParseLightChunk();

        // The specular light color is identical the the diffuse light color. The ambient light color
        // is equal to the ambient base color of the whole scene.
        light->mColorSpecular = light->mColorDiffuse;
        light->mColorAmbient = mClrAmbient;

        if (light->mType == aiLightSource_UNDEFINED) {
            // It must be a point light
            light->mType = aiLightSource_POINT;
        }
    } break;

    case Discreet3DS::CHUNK_CAMERA: {
        // This starts a new camera
        aiCamera *camera = new aiCamera();
        mScene->mCameras.push_back(camera);
        camera->mName.Set(std::string(name, num));

        // First read the position of the camera
        camera->mPosition.x = stream->GetF4();
        camera->mPosition.y = stream->GetF4();
        camera->mPosition.z = stream->GetF4();

        // Then the camera target
        camera->mLookAt.x = stream->GetF4() - camera->mPosition.x;
        camera->mLookAt.y = stream->GetF4() - camera->mPosition.y;
        camera->mLookAt.z = stream->GetF4() - camera->mPosition.z;
        ai_real len = camera->mLookAt.Length();
        if (len < 1e-5) {

            // There are some files with lookat == position. Don't know why or whether it's ok or not.
            ASSIMP_LOG_ERROR("3DS: Unable to read proper camera look-at vector");
            camera->mLookAt = aiVector3D(0.0, 1.0, 0.0);

        } else
            camera->mLookAt /= len;

        // And finally - the camera rotation angle, in counter clockwise direction
        const ai_real angle = AI_DEG_TO_RAD(stream->GetF4());
        aiQuaternion quat(camera->mLookAt, angle);
        camera->mUp = quat.GetMatrix() * aiVector3D(0.0, 1.0, 0.0);

        // Read the lense angle
        camera->mHorizontalFOV = AI_DEG_TO_RAD(stream->GetF4());
        if (camera->mHorizontalFOV < 0.001f) {
            camera->mHorizontalFOV = float(AI_DEG_TO_RAD(45.f));
        }

        // Now check for further subchunks
        if (!bIsPrj) /* fixme */ {
            ParseCameraChunk();
        }
    } break;
    };
    ASSIMP_3DS_END_CHUNK();
}

// ------------------------------------------------------------------------------------------------
void Discreet3DSImporter::ParseLightChunk() {
    ASSIMP_3DS_BEGIN_CHUNK();
    aiLight *light = mScene->mLights.back();

    // get chunk type
    switch (chunk.Flag) {
    case Discreet3DS::CHUNK_DL_SPOTLIGHT:
        // Now we can be sure that the light is a spot light
        light->mType = aiLightSource_SPOT;

        // We wouldn't need to normalize here, but we do it
        light->mDirection.x = stream->GetF4() - light->mPosition.x;
        light->mDirection.y = stream->GetF4() - light->mPosition.y;
        light->mDirection.z = stream->GetF4() - light->mPosition.z;
        light->mDirection.Normalize();

        // Now the hotspot and falloff angles - in degrees
        light->mAngleInnerCone = AI_DEG_TO_RAD(stream->GetF4());

        // FIX: the falloff angle is just an offset
        light->mAngleOuterCone = light->mAngleInnerCone + AI_DEG_TO_RAD(stream->GetF4());
        break;

        // intensity multiplier
    case Discreet3DS::CHUNK_DL_MULTIPLIER:
        light->mColorDiffuse = light->mColorDiffuse * stream->GetF4();
        break;

        // light color
    case Discreet3DS::CHUNK_RGBF:
    case Discreet3DS::CHUNK_LINRGBF:
        light->mColorDiffuse.r *= stream->GetF4();
        light->mColorDiffuse.g *= stream->GetF4();
        light->mColorDiffuse.b *= stream->GetF4();
        break;

        // light attenuation
    case Discreet3DS::CHUNK_DL_ATTENUATE:
        light->mAttenuationLinear = stream->GetF4();
        break;
    };

    ASSIMP_3DS_END_CHUNK();
}

// ------------------------------------------------------------------------------------------------
void Discreet3DSImporter::ParseCameraChunk() {
    ASSIMP_3DS_BEGIN_CHUNK();
    aiCamera *camera = mScene->mCameras.back();

    // get chunk type
    switch (chunk.Flag) {
        // near and far clip plane
    case Discreet3DS::CHUNK_CAM_RANGES:
        camera->mClipPlaneNear = stream->GetF4();
        camera->mClipPlaneFar = stream->GetF4();
        break;
    }

    ASSIMP_3DS_END_CHUNK();
}

// ------------------------------------------------------------------------------------------------
void Discreet3DSImporter::ParseKeyframeChunk() {
    ASSIMP_3DS_BEGIN_CHUNK();

    // get chunk type
    switch (chunk.Flag) {
    case Discreet3DS::CHUNK_TRACKCAMTGT:
    case Discreet3DS::CHUNK_TRACKSPOTL:
    case Discreet3DS::CHUNK_TRACKCAMERA:
    case Discreet3DS::CHUNK_TRACKINFO:
    case Discreet3DS::CHUNK_TRACKLIGHT:
    case Discreet3DS::CHUNK_TRACKLIGTGT:

        // this starts a new mesh hierarchy chunk
        ParseHierarchyChunk(chunk.Flag);
        break;
    };

    ASSIMP_3DS_END_CHUNK();
}

// ------------------------------------------------------------------------------------------------
// Little helper function for ParseHierarchyChunk
void Discreet3DSImporter::InverseNodeSearch(D3DS::Node *pcNode, D3DS::Node *pcCurrent) {
    if (!pcCurrent) {
        mRootNode->push_back(pcNode);
        return;
    }

    if (pcCurrent->mHierarchyPos == pcNode->mHierarchyPos) {
        if (pcCurrent->mParent) {
            pcCurrent->mParent->push_back(pcNode);
        } else
            pcCurrent->push_back(pcNode);
        return;
    }
    return InverseNodeSearch(pcNode, pcCurrent->mParent);
}

// ------------------------------------------------------------------------------------------------
// Find a node with a specific name in the import hierarchy
D3DS::Node *FindNode(D3DS::Node *root, const std::string &name) {
    if (root->mName == name) {
        return root;
    }

    for (std::vector<D3DS::Node *>::iterator it = root->mChildren.begin(); it != root->mChildren.end(); ++it) {
        D3DS::Node *nd = FindNode(*it, name);
        if (nullptr != nd) {
            return nd;
        }
    }

    return nullptr;
}

// ------------------------------------------------------------------------------------------------
// Binary predicate for std::unique()
template <class T>
bool KeyUniqueCompare(const T &first, const T &second) {
    return first.mTime == second.mTime;
}

// ------------------------------------------------------------------------------------------------
// Skip some additional import data.
void Discreet3DSImporter::SkipTCBInfo() {
    unsigned int flags = stream->GetI2();

    if (!flags) {
        // Currently we can't do anything with these values. They occur
        // quite rare, so it wouldn't be worth the effort implementing
        // them. 3DS is not really suitable for complex animations,
        // so full support is not required.
        ASSIMP_LOG_WARN("3DS: Skipping TCB animation info");
    }

    if (flags & Discreet3DS::KEY_USE_TENS) {
        stream->IncPtr(4);
    }
    if (flags & Discreet3DS::KEY_USE_BIAS) {
        stream->IncPtr(4);
    }
    if (flags & Discreet3DS::KEY_USE_CONT) {
        stream->IncPtr(4);
    }
    if (flags & Discreet3DS::KEY_USE_EASE_FROM) {
        stream->IncPtr(4);
    }
    if (flags & Discreet3DS::KEY_USE_EASE_TO) {
        stream->IncPtr(4);
    }
}

// ------------------------------------------------------------------------------------------------
// Read hierarchy and keyframe info
void Discreet3DSImporter::ParseHierarchyChunk(uint16_t parent) {
    ASSIMP_3DS_BEGIN_CHUNK();

    // get chunk type
    switch (chunk.Flag) {
    case Discreet3DS::CHUNK_TRACKOBJNAME:

        // This is the name of the object to which the track applies. The chunk also
        // defines the position of this object in the hierarchy.
        {

            // First of all: get the name of the object
            unsigned int cnt = 0;
            const char *sz = (const char *)stream->GetPtr();

            while (stream->GetI1())
                ++cnt;
            std::string name = std::string(sz, cnt);

            // Now find out whether we have this node already (target animation channels
            // are stored with a separate object ID)
            D3DS::Node *pcNode = FindNode(mRootNode, name);
            int instanceNumber = 1;

            if (pcNode) {
                // if the source is not a CHUNK_TRACKINFO block it won't be an object instance
                if (parent != Discreet3DS::CHUNK_TRACKINFO) {
                    mCurrentNode = pcNode;
                    break;
                }
                pcNode->mInstanceCount++;
                instanceNumber = pcNode->mInstanceCount;
            }
            pcNode = new D3DS::Node(name);
            pcNode->mInstanceNumber = instanceNumber;

            // There are two unknown values which we can safely ignore
            stream->IncPtr(4);

            // Now read the hierarchy position of the object
            uint16_t hierarchy = stream->GetI2() + 1;
            pcNode->mHierarchyPos = hierarchy;
            pcNode->mHierarchyIndex = mLastNodeIndex;

            // And find a proper position in the graph for it
            if (mCurrentNode && mCurrentNode->mHierarchyPos == hierarchy) {

                // add to the parent of the last touched node
                mCurrentNode->mParent->push_back(pcNode);
                mLastNodeIndex++;
            } else if (hierarchy >= mLastNodeIndex) {

                // place it at the current position in the hierarchy
                mCurrentNode->push_back(pcNode);
                mLastNodeIndex = hierarchy;
            } else {
                // need to go back to the specified position in the hierarchy.
                InverseNodeSearch(pcNode, mCurrentNode);
                mLastNodeIndex++;
            }
            // Make this node the current node
            mCurrentNode = pcNode;
        }
        break;

    case Discreet3DS::CHUNK_TRACKDUMMYOBJNAME:

        // This is the "real" name of a $$$DUMMY object
        {
            const char *sz = (const char *)stream->GetPtr();
            while (stream->GetI1())
                ;

            // If object name is DUMMY, take this one instead
            if (mCurrentNode->mName == "$$$DUMMY") {
                mCurrentNode->mName = std::string(sz);
                break;
            }
        }
        break;

    case Discreet3DS::CHUNK_TRACKPIVOT:

        if (Discreet3DS::CHUNK_TRACKINFO != parent) {
            ASSIMP_LOG_WARN("3DS: Skipping pivot subchunk for non usual object");
            break;
        }

        // Pivot = origin of rotation and scaling
        mCurrentNode->vPivot.x = stream->GetF4();
        mCurrentNode->vPivot.y = stream->GetF4();
        mCurrentNode->vPivot.z = stream->GetF4();
        break;

        // ////////////////////////////////////////////////////////////////////
        // POSITION KEYFRAME
    case Discreet3DS::CHUNK_TRACKPOS: {
        stream->IncPtr(10);
        const unsigned int numFrames = stream->GetI4();
        bool sortKeys = false;

        // This could also be meant as the target position for
        // (targeted) lights and cameras
        std::vector<aiVectorKey> *l;
        if (Discreet3DS::CHUNK_TRACKCAMTGT == parent || Discreet3DS::CHUNK_TRACKLIGTGT == parent) {
            l = &mCurrentNode->aTargetPositionKeys;
        } else
            l = &mCurrentNode->aPositionKeys;

        l->reserve(numFrames);
        for (unsigned int i = 0; i < numFrames; ++i) {
            const unsigned int fidx = stream->GetI4();

            // Setup a new position key
            aiVectorKey v;
            v.mTime = (double)fidx;

            SkipTCBInfo();
            v.mValue.x = stream->GetF4();
            v.mValue.y = stream->GetF4();
            v.mValue.z = stream->GetF4();

            // check whether we'll need to sort the keys
            if (!l->empty() && v.mTime <= l->back().mTime)
                sortKeys = true;

            // Add the new keyframe to the list
            l->push_back(v);
        }

        // Sort all keys with ascending time values and remove duplicates?
        if (sortKeys) {
            std::stable_sort(l->begin(), l->end());
            l->erase(std::unique(l->begin(), l->end(), &KeyUniqueCompare<aiVectorKey>), l->end());
        }
    }

    break;

        // ////////////////////////////////////////////////////////////////////
        // CAMERA ROLL KEYFRAME
    case Discreet3DS::CHUNK_TRACKROLL: {
        // roll keys are accepted for cameras only
        if (parent != Discreet3DS::CHUNK_TRACKCAMERA) {
            ASSIMP_LOG_WARN("3DS: Ignoring roll track for non-camera object");
            break;
        }
        bool sortKeys = false;
        std::vector<aiFloatKey> *l = &mCurrentNode->aCameraRollKeys;

        stream->IncPtr(10);
        const unsigned int numFrames = stream->GetI4();
        l->reserve(numFrames);
        for (unsigned int i = 0; i < numFrames; ++i) {
            const unsigned int fidx = stream->GetI4();

            // Setup a new position key
            aiFloatKey v;
            v.mTime = (double)fidx;

            // This is just a single float
            SkipTCBInfo();
            v.mValue = stream->GetF4();

            // Check whether we'll need to sort the keys
            if (!l->empty() && v.mTime <= l->back().mTime)
                sortKeys = true;

            // Add the new keyframe to the list
            l->push_back(v);
        }

        // Sort all keys with ascending time values and remove duplicates?
        if (sortKeys) {
            std::stable_sort(l->begin(), l->end());
            l->erase(std::unique(l->begin(), l->end(), &KeyUniqueCompare<aiFloatKey>), l->end());
        }
    } break;

        // ////////////////////////////////////////////////////////////////////
        // CAMERA FOV KEYFRAME
    case Discreet3DS::CHUNK_TRACKFOV: {
        ASSIMP_LOG_ERROR("3DS: Skipping FOV animation track. "
                         "This is not supported");
    } break;

        // ////////////////////////////////////////////////////////////////////
        // ROTATION KEYFRAME
    case Discreet3DS::CHUNK_TRACKROTATE: {
        stream->IncPtr(10);
        const unsigned int numFrames = stream->GetI4();

        bool sortKeys = false;
        std::vector<aiQuatKey> *l = &mCurrentNode->aRotationKeys;
        l->reserve(numFrames);

        for (unsigned int i = 0; i < numFrames; ++i) {
            const unsigned int fidx = stream->GetI4();
            SkipTCBInfo();

            aiQuatKey v;
            v.mTime = (double)fidx;

            // The rotation keyframe is given as an axis-angle pair
            const float rad = stream->GetF4();
            aiVector3D axis;
            axis.x = stream->GetF4();
            axis.y = stream->GetF4();
            axis.z = stream->GetF4();

            if (!axis.x && !axis.y && !axis.z)
                axis.y = 1.f;

            // Construct a rotation quaternion from the axis-angle pair
            v.mValue = aiQuaternion(axis, rad);

            // Check whether we'll need to sort the keys
            if (!l->empty() && v.mTime <= l->back().mTime)
                sortKeys = true;

            // add the new keyframe to the list
            l->push_back(v);
        }
        // Sort all keys with ascending time values and remove duplicates?
        if (sortKeys) {
            std::stable_sort(l->begin(), l->end());
            l->erase(std::unique(l->begin(), l->end(), &KeyUniqueCompare<aiQuatKey>), l->end());
        }
    } break;

        // ////////////////////////////////////////////////////////////////////
        // SCALING KEYFRAME
    case Discreet3DS::CHUNK_TRACKSCALE: {
        stream->IncPtr(10);
        const unsigned int numFrames = stream->GetI2();
        stream->IncPtr(2);

        bool sortKeys = false;
        std::vector<aiVectorKey> *l = &mCurrentNode->aScalingKeys;
        l->reserve(numFrames);

        for (unsigned int i = 0; i < numFrames; ++i) {
            const unsigned int fidx = stream->GetI4();
            SkipTCBInfo();

            // Setup a new key
            aiVectorKey v;
            v.mTime = (double)fidx;

            // ... and read its value
            v.mValue.x = stream->GetF4();
            v.mValue.y = stream->GetF4();
            v.mValue.z = stream->GetF4();

            // check whether we'll need to sort the keys
            if (!l->empty() && v.mTime <= l->back().mTime)
                sortKeys = true;

            // Remove zero-scalings on singular axes - they've been reported to be there erroneously in some strange files
            if (!v.mValue.x) v.mValue.x = 1.f;
            if (!v.mValue.y) v.mValue.y = 1.f;
            if (!v.mValue.z) v.mValue.z = 1.f;

            l->push_back(v);
        }
        // Sort all keys with ascending time values and remove duplicates?
        if (sortKeys) {
            std::stable_sort(l->begin(), l->end());
            l->erase(std::unique(l->begin(), l->end(), &KeyUniqueCompare<aiVectorKey>), l->end());
        }
    } break;
    };

    ASSIMP_3DS_END_CHUNK();
}

// ------------------------------------------------------------------------------------------------
// Read a face chunk - it contains smoothing groups and material assignments
void Discreet3DSImporter::ParseFaceChunk() {
    ASSIMP_3DS_BEGIN_CHUNK();

    // Get the mesh we're currently working on
    D3DS::Mesh &mMesh = mScene->mMeshes.back();

    // Get chunk type
    switch (chunk.Flag) {
    case Discreet3DS::CHUNK_SMOOLIST: {
        // This is the list of smoothing groups - a bitfield for every face.
        // Up to 32 smoothing groups assigned to a single face.
        unsigned int num = chunkSize / 4, m = 0;
        if (num > mMesh.mFaces.size()) {
            throw DeadlyImportError("3DS: More smoothing groups than faces");
        }
        for (std::vector<D3DS::Face>::iterator i = mMesh.mFaces.begin(); m != num; ++i, ++m) {
            // nth bit is set for nth smoothing group
            (*i).iSmoothGroup = stream->GetI4();
        }
    } break;

    case Discreet3DS::CHUNK_FACEMAT: {
        // at fist an asciiz with the material name
        const char *sz = (const char *)stream->GetPtr();
        while (stream->GetI1())
            ;

        // find the index of the material
        unsigned int idx = 0xcdcdcdcd, cnt = 0;
        for (std::vector<D3DS::Material>::const_iterator i = mScene->mMaterials.begin(); i != mScene->mMaterials.end(); ++i, ++cnt) {
            // use case independent comparisons. hopefully it will work.
            if ((*i).mName.length() && !ASSIMP_stricmp(sz, (*i).mName.c_str())) {
                idx = cnt;
                break;
            }
        }
        if (0xcdcdcdcd == idx) {
            ASSIMP_LOG_ERROR("3DS: Unknown material: ", sz);
        }

        // Now continue and read all material indices
        cnt = (uint16_t)stream->GetI2();
        for (unsigned int i = 0; i < cnt; ++i) {
            unsigned int fidx = (uint16_t)stream->GetI2();

            // check range
            if (fidx >= mMesh.mFaceMaterials.size()) {
                ASSIMP_LOG_ERROR("3DS: Invalid face index in face material list");
            } else
                mMesh.mFaceMaterials[fidx] = idx;
        }
    } break;
    };
    ASSIMP_3DS_END_CHUNK();
}

// ------------------------------------------------------------------------------------------------
// Read a mesh chunk. Here's the actual mesh data
void Discreet3DSImporter::ParseMeshChunk() {
    ASSIMP_3DS_BEGIN_CHUNK();

    // Get the mesh we're currently working on
    D3DS::Mesh &mMesh = mScene->mMeshes.back();

    // get chunk type
    switch (chunk.Flag) {
    case Discreet3DS::CHUNK_VERTLIST: {
        // This is the list of all vertices in the current mesh
        int num = (int)(uint16_t)stream->GetI2();
        mMesh.mPositions.reserve(num);
        while (num-- > 0) {
            aiVector3D v;
            v.x = stream->GetF4();
            v.y = stream->GetF4();
            v.z = stream->GetF4();
            mMesh.mPositions.push_back(v);
        }
    } break;
    case Discreet3DS::CHUNK_TRMATRIX: {
        // This is the RLEATIVE transformation matrix of the current mesh. Vertices are
        // pretransformed by this matrix wonder.
        mMesh.mMat.a1 = stream->GetF4();
        mMesh.mMat.b1 = stream->GetF4();
        mMesh.mMat.c1 = stream->GetF4();
        mMesh.mMat.a2 = stream->GetF4();
        mMesh.mMat.b2 = stream->GetF4();
        mMesh.mMat.c2 = stream->GetF4();
        mMesh.mMat.a3 = stream->GetF4();
        mMesh.mMat.b3 = stream->GetF4();
        mMesh.mMat.c3 = stream->GetF4();
        mMesh.mMat.a4 = stream->GetF4();
        mMesh.mMat.b4 = stream->GetF4();
        mMesh.mMat.c4 = stream->GetF4();
    } break;

    case Discreet3DS::CHUNK_MAPLIST: {
        // This is the list of all UV coords in the current mesh
        int num = (int)(uint16_t)stream->GetI2();
        mMesh.mTexCoords.reserve(num);
        while (num-- > 0) {
            aiVector3D v;
            v.x = stream->GetF4();
            v.y = stream->GetF4();
            mMesh.mTexCoords.push_back(v);
        }
    } break;

    case Discreet3DS::CHUNK_FACELIST: {
        // This is the list of all faces in the current mesh
        int num = (int)(uint16_t)stream->GetI2();
        mMesh.mFaces.reserve(num);
        while (num-- > 0) {
            // 3DS faces are ALWAYS triangles
            mMesh.mFaces.emplace_back();
            D3DS::Face &sFace = mMesh.mFaces.back();

            sFace.mIndices[0] = (uint16_t)stream->GetI2();
            sFace.mIndices[1] = (uint16_t)stream->GetI2();
            sFace.mIndices[2] = (uint16_t)stream->GetI2();

            stream->IncPtr(2); // skip edge visibility flag
        }

        // Resize the material array (0xcdcdcdcd marks the default material; so if a face is
        // not referenced by a material, $$DEFAULT will be assigned to it)
        mMesh.mFaceMaterials.resize(mMesh.mFaces.size(), 0xcdcdcdcd);

        // Larger 3DS files could have multiple FACE chunks here
        chunkSize = (int)stream->GetRemainingSizeToLimit();
        if (chunkSize > (int)sizeof(Discreet3DS::Chunk))
            ParseFaceChunk();
    } break;
    };
    ASSIMP_3DS_END_CHUNK();
}

// ------------------------------------------------------------------------------------------------
// Read a 3DS material chunk
void Discreet3DSImporter::ParseMaterialChunk() {
    ASSIMP_3DS_BEGIN_CHUNK();
    switch (chunk.Flag) {
    case Discreet3DS::CHUNK_MAT_MATNAME:

    {
        // The material name string is already zero-terminated, but we need to be sure ...
        const char *sz = (const char *)stream->GetPtr();
        unsigned int cnt = 0;
        while (stream->GetI1())
            ++cnt;

        if (!cnt) {
            // This may not be, we use the default name instead
            ASSIMP_LOG_ERROR("3DS: Empty material name");
        } else
            mScene->mMaterials.back().mName = std::string(sz, cnt);
    } break;

    case Discreet3DS::CHUNK_MAT_DIFFUSE: {
        // This is the diffuse material color
        aiColor3D *pc = &mScene->mMaterials.back().mDiffuse;
        ParseColorChunk(pc);
        if (is_qnan(pc->r)) {
            // color chunk is invalid. Simply ignore it
            ASSIMP_LOG_ERROR("3DS: Unable to read DIFFUSE chunk");
            pc->r = pc->g = pc->b = 1.0f;
        }
    } break;

    case Discreet3DS::CHUNK_MAT_SPECULAR: {
        // This is the specular material color
        aiColor3D *pc = &mScene->mMaterials.back().mSpecular;
        ParseColorChunk(pc);
        if (is_qnan(pc->r)) {
            // color chunk is invalid. Simply ignore it
            ASSIMP_LOG_ERROR("3DS: Unable to read SPECULAR chunk");
            pc->r = pc->g = pc->b = 1.0f;
        }
    } break;

    case Discreet3DS::CHUNK_MAT_AMBIENT: {
        // This is the ambient material color
        aiColor3D *pc = &mScene->mMaterials.back().mAmbient;
        ParseColorChunk(pc);
        if (is_qnan(pc->r)) {
            // color chunk is invalid. Simply ignore it
            ASSIMP_LOG_ERROR("3DS: Unable to read AMBIENT chunk");
            pc->r = pc->g = pc->b = 0.0f;
        }
    } break;

    case Discreet3DS::CHUNK_MAT_SELF_ILLUM: {
        // This is the emissive material color
        aiColor3D *pc = &mScene->mMaterials.back().mEmissive;
        ParseColorChunk(pc);
        if (is_qnan(pc->r)) {
            // color chunk is invalid. Simply ignore it
            ASSIMP_LOG_ERROR("3DS: Unable to read EMISSIVE chunk");
            pc->r = pc->g = pc->b = 0.0f;
        }
    } break;

    case Discreet3DS::CHUNK_MAT_TRANSPARENCY: {
        // This is the material's transparency
        ai_real *pcf = &mScene->mMaterials.back().mTransparency;
        *pcf = ParsePercentageChunk();

        // NOTE: transparency, not opacity
        if (is_qnan(*pcf))
            *pcf = ai_real(1.0);
        else
            *pcf = ai_real(1.0) - *pcf * (ai_real)0xFFFF / ai_real(100.0);
    } break;

    case Discreet3DS::CHUNK_MAT_SHADING:
        // This is the material shading mode
        mScene->mMaterials.back().mShading = (D3DS::Discreet3DS::shadetype3ds)stream->GetI2();
        break;

    case Discreet3DS::CHUNK_MAT_TWO_SIDE:
        // This is the two-sided flag
        mScene->mMaterials.back().mTwoSided = true;
        break;

    case Discreet3DS::CHUNK_MAT_SHININESS: { // This is the shininess of the material
        ai_real *pcf = &mScene->mMaterials.back().mSpecularExponent;
        *pcf = ParsePercentageChunk();
        if (is_qnan(*pcf))
            *pcf = 0.0;
        else
            *pcf *= (ai_real)0xFFFF;
    } break;

    case Discreet3DS::CHUNK_MAT_SHININESS_PERCENT: { // This is the shininess strength of the material
        ai_real *pcf = &mScene->mMaterials.back().mShininessStrength;
        *pcf = ParsePercentageChunk();
        if (is_qnan(*pcf))
            *pcf = ai_real(0.0);
        else
            *pcf *= (ai_real)0xffff / ai_real(100.0);
    } break;

    case Discreet3DS::CHUNK_MAT_SELF_ILPCT: { // This is the self illumination strength of the material
        ai_real f = ParsePercentageChunk();
        if (is_qnan(f))
            f = ai_real(0.0);
        else
            f *= (ai_real)0xFFFF / ai_real(100.0);
        mScene->mMaterials.back().mEmissive = aiColor3D(f, f, f);
    } break;

    // Parse texture chunks
    case Discreet3DS::CHUNK_MAT_TEXTURE:
        // Diffuse texture
        ParseTextureChunk(&mScene->mMaterials.back().sTexDiffuse);
        break;
    case Discreet3DS::CHUNK_MAT_BUMPMAP:
        // Height map
        ParseTextureChunk(&mScene->mMaterials.back().sTexBump);
        break;
    case Discreet3DS::CHUNK_MAT_OPACMAP:
        // Opacity texture
        ParseTextureChunk(&mScene->mMaterials.back().sTexOpacity);
        break;
    case Discreet3DS::CHUNK_MAT_MAT_SHINMAP:
        // Shininess map
        ParseTextureChunk(&mScene->mMaterials.back().sTexShininess);
        break;
    case Discreet3DS::CHUNK_MAT_SPECMAP:
        // Specular map
        ParseTextureChunk(&mScene->mMaterials.back().sTexSpecular);
        break;
    case Discreet3DS::CHUNK_MAT_SELFIMAP:
        // Self-illumination (emissive) map
        ParseTextureChunk(&mScene->mMaterials.back().sTexEmissive);
        break;
    case Discreet3DS::CHUNK_MAT_REFLMAP:
        // Reflection map
        ParseTextureChunk(&mScene->mMaterials.back().sTexReflective);
        break;
    };
    ASSIMP_3DS_END_CHUNK();
}

// ------------------------------------------------------------------------------------------------
void Discreet3DSImporter::ParseTextureChunk(D3DS::Texture *pcOut) {
    ASSIMP_3DS_BEGIN_CHUNK();

    // get chunk type
    switch (chunk.Flag) {
    case Discreet3DS::CHUNK_MAPFILE: {
        // The material name string is already zero-terminated, but we need to be sure ...
        const char *sz = (const char *)stream->GetPtr();
        unsigned int cnt = 0;
        while (stream->GetI1())
            ++cnt;
        pcOut->mMapName = std::string(sz, cnt);
    } break;

    case Discreet3DS::CHUNK_PERCENTD:
        // Manually parse the blend factor
        pcOut->mTextureBlend = ai_real(stream->GetF8());
        break;

    case Discreet3DS::CHUNK_PERCENTF:
        // Manually parse the blend factor
        pcOut->mTextureBlend = stream->GetF4();
        break;

    case Discreet3DS::CHUNK_PERCENTW:
        // Manually parse the blend factor
        pcOut->mTextureBlend = (ai_real)((uint16_t)stream->GetI2()) / ai_real(100.0);
        break;

    case Discreet3DS::CHUNK_MAT_MAP_USCALE:
        // Texture coordinate scaling in the U direction
        pcOut->mScaleU = stream->GetF4();
        if (0.0f == pcOut->mScaleU) {
            ASSIMP_LOG_WARN("Texture coordinate scaling in the x direction is zero. Assuming 1.");
            pcOut->mScaleU = 1.0f;
        }
        break;
    case Discreet3DS::CHUNK_MAT_MAP_VSCALE:
        // Texture coordinate scaling in the V direction
        pcOut->mScaleV = stream->GetF4();
        if (0.0f == pcOut->mScaleV) {
            ASSIMP_LOG_WARN("Texture coordinate scaling in the y direction is zero. Assuming 1.");
            pcOut->mScaleV = 1.0f;
        }
        break;

    case Discreet3DS::CHUNK_MAT_MAP_UOFFSET:
        // Texture coordinate offset in the U direction
        pcOut->mOffsetU = -stream->GetF4();
        break;

    case Discreet3DS::CHUNK_MAT_MAP_VOFFSET:
        // Texture coordinate offset in the V direction
        pcOut->mOffsetV = stream->GetF4();
        break;

    case Discreet3DS::CHUNK_MAT_MAP_ANG:
        // Texture coordinate rotation, CCW in DEGREES
        pcOut->mRotation = -AI_DEG_TO_RAD(stream->GetF4());
        break;

    case Discreet3DS::CHUNK_MAT_MAP_TILING: {
        const uint16_t iFlags = stream->GetI2();

        // Get the mapping mode (for both axes)
        if (iFlags & 0x2u)
            pcOut->mMapMode = aiTextureMapMode_Mirror;

        else if (iFlags & 0x10u)
            pcOut->mMapMode = aiTextureMapMode_Decal;

        // wrapping in all remaining cases
        else
            pcOut->mMapMode = aiTextureMapMode_Wrap;
    } break;
    };

    ASSIMP_3DS_END_CHUNK();
}

// ------------------------------------------------------------------------------------------------
// Read a percentage chunk
ai_real Discreet3DSImporter::ParsePercentageChunk() {
    Discreet3DS::Chunk chunk;
    ReadChunk(&chunk);

    if (Discreet3DS::CHUNK_PERCENTF == chunk.Flag) {
        return stream->GetF4() * ai_real(100) / ai_real(0xFFFF);
    } else if (Discreet3DS::CHUNK_PERCENTW == chunk.Flag) {
        return (ai_real)((uint16_t)stream->GetI2()) / (ai_real)0xFFFF;
    }

    return get_qnan();
}

// ------------------------------------------------------------------------------------------------
// Read a color chunk. If a percentage chunk is found instead it is read as a grayscale color
void Discreet3DSImporter::ParseColorChunk(aiColor3D *out, bool acceptPercent) {
    ai_assert(out != nullptr);

    // error return value
    const ai_real qnan = get_qnan();
    static const aiColor3D clrError = aiColor3D(qnan, qnan, qnan);

    Discreet3DS::Chunk chunk;
    ReadChunk(&chunk);
    const unsigned int diff = chunk.Size - sizeof(Discreet3DS::Chunk);

    bool bGamma = false;

    // Get the type of the chunk
    switch (chunk.Flag) {
    case Discreet3DS::CHUNK_LINRGBF:
        bGamma = true;
    // fallthrough
    case Discreet3DS::CHUNK_RGBF:
        if (sizeof(float) * 3 > diff) {
            *out = clrError;
            return;
        }
        out->r = stream->GetF4();
        out->g = stream->GetF4();
        out->b = stream->GetF4();
        break;

    case Discreet3DS::CHUNK_LINRGBB:
        bGamma = true;
            // fallthrough
    case Discreet3DS::CHUNK_RGBB: {
        if (sizeof(char) * 3 > diff) {
            *out = clrError;
            return;
        }
        const ai_real invVal = ai_real(1.0) / ai_real(255.0);
        out->r = (ai_real)(uint8_t)stream->GetI1() * invVal;
        out->g = (ai_real)(uint8_t)stream->GetI1() * invVal;
        out->b = (ai_real)(uint8_t)stream->GetI1() * invVal;
    } break;

    // Percentage chunks are accepted, too.
    case Discreet3DS::CHUNK_PERCENTF:
        if (acceptPercent && 4 <= diff) {
            out->g = out->b = out->r = stream->GetF4();
            break;
        }
        *out = clrError;
        return;

    case Discreet3DS::CHUNK_PERCENTW:
        if (acceptPercent && 1 <= diff) {
            out->g = out->b = out->r = (ai_real)(uint8_t)stream->GetI1() / ai_real(255.0);
            break;
        }
        *out = clrError;
        return;

    default:
        stream->IncPtr(diff);
        // Skip unknown chunks, hope this won't cause any problems.
        return ParseColorChunk(out, acceptPercent);
    };
    (void)bGamma;
}

} // namespace Assimp

#endif // !! ASSIMP_BUILD_NO_3DS_IMPORTER
