#include "niffile.hpp"

#include <components/debug/debuglog.hpp>
#include <components/files/hash.hpp>

#include <algorithm>
#include <array>
#include <limits>
#include <map>
#include <sstream>
#include <stdexcept>

#include "controlled.hpp"
#include "controller.hpp"
#include "data.hpp"
#include "effect.hpp"
#include "exception.hpp"
#include "extra.hpp"
#include "physics.hpp"
#include "property.hpp"

namespace Nif
{

    Reader::Reader(NIFFile& file)
        : ver(file.mVersion)
        , userVer(file.mUserVersion)
        , bethVer(file.mBethVersion)
        , filename(file.mPath)
        , hash(file.mHash)
        , records(file.mRecords)
        , roots(file.mRoots)
        , mUseSkinning(file.mUseSkinning)
    {
    }

    template <typename NodeType, RecordType recordType>
    static std::unique_ptr<Record> construct()
    {
        auto result = std::make_unique<NodeType>();
        result->recType = recordType;
        return result;
    }

    using CreateRecord = std::unique_ptr<Record> (*)();

    /// These are all the record types we know how to read.
    static std::map<std::string, CreateRecord> makeFactory()
    {
        return {
            { "NiNode", &construct<NiNode, RC_NiNode> },
            { "NiSwitchNode", &construct<NiSwitchNode, RC_NiSwitchNode> },
            { "NiLODNode", &construct<NiLODNode, RC_NiLODNode> },
            { "NiFltAnimationNode", &construct<NiFltAnimationNode, RC_NiFltAnimationNode> },
            { "AvoidNode", &construct<NiNode, RC_AvoidNode> },
            { "NiCollisionSwitch", &construct<NiNode, RC_NiCollisionSwitch> },
            { "NiBSParticleNode", &construct<NiNode, RC_NiBSParticleNode> },
            { "NiBSAnimationNode", &construct<NiNode, RC_NiBSAnimationNode> },
            { "NiBillboardNode", &construct<NiBillboardNode, RC_NiBillboardNode> },
            { "NiTriShape", &construct<NiTriShape, RC_NiTriShape> },
            { "NiTriStrips", &construct<NiTriStrips, RC_NiTriStrips> },
            { "NiLines", &construct<NiLines, RC_NiLines> },
            { "NiParticles", &construct<NiParticles, RC_NiParticles> },
            { "NiRotatingParticles", &construct<NiParticles, RC_NiParticles> },
            { "NiAutoNormalParticles", &construct<NiParticles, RC_NiParticles> },
            { "NiCamera", &construct<NiCamera, RC_NiCamera> },
            { "RootCollisionNode", &construct<NiNode, RC_RootCollisionNode> },
            { "NiTexturingProperty", &construct<NiTexturingProperty, RC_NiTexturingProperty> },
            { "NiFogProperty", &construct<NiFogProperty, RC_NiFogProperty> },
            { "NiMaterialProperty", &construct<NiMaterialProperty, RC_NiMaterialProperty> },
            { "NiZBufferProperty", &construct<NiZBufferProperty, RC_NiZBufferProperty> },
            { "NiAlphaProperty", &construct<NiAlphaProperty, RC_NiAlphaProperty> },
            { "NiVertexColorProperty", &construct<NiVertexColorProperty, RC_NiVertexColorProperty> },
            { "NiShadeProperty", &construct<NiShadeProperty, RC_NiShadeProperty> },
            { "NiDitherProperty", &construct<NiDitherProperty, RC_NiDitherProperty> },
            { "NiWireframeProperty", &construct<NiWireframeProperty, RC_NiWireframeProperty> },
            { "NiSpecularProperty", &construct<NiSpecularProperty, RC_NiSpecularProperty> },
            { "NiStencilProperty", &construct<NiStencilProperty, RC_NiStencilProperty> },
            { "NiVisController", &construct<NiVisController, RC_NiVisController> },
            { "NiGeomMorpherController", &construct<NiGeomMorpherController, RC_NiGeomMorpherController> },
            { "NiKeyframeController", &construct<NiKeyframeController, RC_NiKeyframeController> },
            { "NiAlphaController", &construct<NiAlphaController, RC_NiAlphaController> },
            { "NiRollController", &construct<NiRollController, RC_NiRollController> },
            { "NiUVController", &construct<NiUVController, RC_NiUVController> },
            { "NiPathController", &construct<NiPathController, RC_NiPathController> },
            { "NiMaterialColorController", &construct<NiMaterialColorController, RC_NiMaterialColorController> },
            { "NiBSPArrayController", &construct<NiBSPArrayController, RC_NiBSPArrayController> },
            { "NiParticleSystemController", &construct<NiParticleSystemController, RC_NiParticleSystemController> },
            { "NiFlipController", &construct<NiFlipController, RC_NiFlipController> },
            { "NiAmbientLight", &construct<NiLight, RC_NiLight> },
            { "NiDirectionalLight", &construct<NiLight, RC_NiLight> },
            { "NiPointLight", &construct<NiPointLight, RC_NiLight> },
            { "NiSpotLight", &construct<NiSpotLight, RC_NiLight> },
            { "NiTextureEffect", &construct<NiTextureEffect, RC_NiTextureEffect> },
            { "NiExtraData", &construct<NiExtraData, RC_NiExtraData> },
            { "NiVertWeightsExtraData", &construct<NiVertWeightsExtraData, RC_NiVertWeightsExtraData> },
            { "NiTextKeyExtraData", &construct<NiTextKeyExtraData, RC_NiTextKeyExtraData> },
            { "NiStringExtraData", &construct<NiStringExtraData, RC_NiStringExtraData> },
            { "NiGravity", &construct<NiGravity, RC_NiGravity> },
            { "NiPlanarCollider", &construct<NiPlanarCollider, RC_NiPlanarCollider> },
            { "NiSphericalCollider", &construct<NiSphericalCollider, RC_NiSphericalCollider> },
            { "NiParticleGrowFade", &construct<NiParticleGrowFade, RC_NiParticleGrowFade> },
            { "NiParticleColorModifier", &construct<NiParticleColorModifier, RC_NiParticleColorModifier> },
            { "NiParticleRotation", &construct<NiParticleRotation, RC_NiParticleRotation> },
            { "NiFloatData", &construct<NiFloatData, RC_NiFloatData> },
            { "NiTriShapeData", &construct<NiTriShapeData, RC_NiTriShapeData> },
            { "NiTriStripsData", &construct<NiTriStripsData, RC_NiTriStripsData> },
            { "NiLinesData", &construct<NiLinesData, RC_NiLinesData> },
            { "NiVisData", &construct<NiVisData, RC_NiVisData> },
            { "NiColorData", &construct<NiColorData, RC_NiColorData> },
            { "NiPixelData", &construct<NiPixelData, RC_NiPixelData> },
            { "NiMorphData", &construct<NiMorphData, RC_NiMorphData> },
            { "NiKeyframeData", &construct<NiKeyframeData, RC_NiKeyframeData> },
            { "NiSkinData", &construct<NiSkinData, RC_NiSkinData> },
            { "NiUVData", &construct<NiUVData, RC_NiUVData> },
            { "NiPosData", &construct<NiPosData, RC_NiPosData> },
            { "NiParticlesData", &construct<NiParticlesData, RC_NiParticlesData> },
            { "NiRotatingParticlesData", &construct<NiRotatingParticlesData, RC_NiParticlesData> },
            { "NiAutoNormalParticlesData", &construct<NiParticlesData, RC_NiParticlesData> },
            { "NiSequenceStreamHelper", &construct<NiSequenceStreamHelper, RC_NiSequenceStreamHelper> },
            { "NiSourceTexture", &construct<NiSourceTexture, RC_NiSourceTexture> },
            { "NiSkinInstance", &construct<NiSkinInstance, RC_NiSkinInstance> },
            { "NiLookAtController", &construct<NiLookAtController, RC_NiLookAtController> },
            { "NiPalette", &construct<NiPalette, RC_NiPalette> },
            { "NiIntegerExtraData", &construct<NiIntegerExtraData, RC_NiIntegerExtraData> },
            { "NiIntegersExtraData", &construct<NiIntegersExtraData, RC_NiIntegersExtraData> },
            { "NiBinaryExtraData", &construct<NiBinaryExtraData, RC_NiBinaryExtraData> },
            { "NiBooleanExtraData", &construct<NiBooleanExtraData, RC_NiBooleanExtraData> },
            { "NiVectorExtraData", &construct<NiVectorExtraData, RC_NiVectorExtraData> },
            { "NiColorExtraData", &construct<NiVectorExtraData, RC_NiColorExtraData> },
            { "NiFloatExtraData", &construct<NiFloatExtraData, RC_NiFloatExtraData> },
            { "NiFloatsExtraData", &construct<NiFloatsExtraData, RC_NiFloatsExtraData> },
            { "NiStringPalette", &construct<NiStringPalette, RC_NiStringPalette> },
            { "NiBoolData", &construct<NiBoolData, RC_NiBoolData> },
            { "NiSkinPartition", &construct<NiSkinPartition, RC_NiSkinPartition> },
            { "BSXFlags", &construct<NiIntegerExtraData, RC_BSXFlags> },
            { "BSBound", &construct<BSBound, RC_BSBound> },
            { "NiTransformData", &construct<NiKeyframeData, RC_NiKeyframeData> },
            { "BSFadeNode", &construct<NiNode, RC_NiNode> },
            { "BSLeafAnimNode", &construct<NiNode, RC_NiNode> },
            { "bhkBlendController", &construct<bhkBlendController, RC_bhkBlendController> },
            { "NiFloatInterpolator", &construct<NiFloatInterpolator, RC_NiFloatInterpolator> },
            { "NiBoolInterpolator", &construct<NiBoolInterpolator, RC_NiBoolInterpolator> },
            { "NiPoint3Interpolator", &construct<NiPoint3Interpolator, RC_NiPoint3Interpolator> },
            { "NiTransformController", &construct<NiKeyframeController, RC_NiKeyframeController> },
            { "NiMultiTargetTransformController",
                &construct<NiMultiTargetTransformController, RC_NiMultiTargetTransformController> },
            { "NiTransformInterpolator", &construct<NiTransformInterpolator, RC_NiTransformInterpolator> },
            { "NiColorInterpolator", &construct<NiColorInterpolator, RC_NiColorInterpolator> },
            { "BSShaderTextureSet", &construct<BSShaderTextureSet, RC_BSShaderTextureSet> },
            { "BSLODTriShape", &construct<BSLODTriShape, RC_BSLODTriShape> },
            { "BSShaderProperty", &construct<BSShaderProperty, RC_BSShaderProperty> },
            { "BSShaderPPLightingProperty", &construct<BSShaderPPLightingProperty, RC_BSShaderPPLightingProperty> },
            { "BSShaderNoLightingProperty", &construct<BSShaderNoLightingProperty, RC_BSShaderNoLightingProperty> },
            { "BSFurnitureMarker", &construct<BSFurnitureMarker, RC_BSFurnitureMarker> },
            { "BSFurnitureMarkerNode", &construct<BSFurnitureMarker, RC_BSFurnitureMarker> },
            { "NiCollisionObject", &construct<NiCollisionObject, RC_NiCollisionObject> },
            { "bhkCollisionObject", &construct<bhkCollisionObject, RC_bhkCollisionObject> },
            { "BSDismemberSkinInstance", &construct<BSDismemberSkinInstance, RC_BSDismemberSkinInstance> },
            { "NiControllerManager", &construct<NiControllerManager, RC_NiControllerManager> },
            { "bhkMoppBvTreeShape", &construct<bhkMoppBvTreeShape, RC_bhkMoppBvTreeShape> },
            { "bhkNiTriStripsShape", &construct<bhkNiTriStripsShape, RC_bhkNiTriStripsShape> },
            { "bhkPackedNiTriStripsShape", &construct<bhkPackedNiTriStripsShape, RC_bhkPackedNiTriStripsShape> },
            { "hkPackedNiTriStripsData", &construct<hkPackedNiTriStripsData, RC_hkPackedNiTriStripsData> },
            { "bhkConvexVerticesShape", &construct<bhkConvexVerticesShape, RC_bhkConvexVerticesShape> },
            { "bhkConvexTransformShape", &construct<bhkConvexTransformShape, RC_bhkConvexTransformShape> },
            { "bhkBoxShape", &construct<bhkBoxShape, RC_bhkBoxShape> },
            { "bhkCapsuleShape", &construct<bhkCapsuleShape, RC_bhkCapsuleShape> },
            { "bhkSphereShape", &construct<bhkSphereShape, RC_bhkSphereShape> },
            { "bhkListShape", &construct<bhkListShape, RC_bhkListShape> },
            { "bhkRigidBody", &construct<bhkRigidBody, RC_bhkRigidBody> },
            { "bhkRigidBodyT", &construct<bhkRigidBody, RC_bhkRigidBodyT> },
            { "BSLightingShaderProperty", &construct<BSLightingShaderProperty, RC_BSLightingShaderProperty> },
            { "BSEffectShaderProperty", &construct<BSEffectShaderProperty, RC_BSEffectShaderProperty> },
            { "NiSortAdjustNode", &construct<NiSortAdjustNode, RC_NiSortAdjustNode> },
            { "NiClusterAccumulator", &construct<NiClusterAccumulator, RC_NiClusterAccumulator> },
            { "NiAlphaAccumulator", &construct<NiAlphaAccumulator, RC_NiAlphaAccumulator> },
            { "NiSequence", &construct<NiSequence, RC_NiSequence> },
            { "NiControllerSequence", &construct<NiControllerSequence, RC_NiControllerSequence> },
            { "NiDefaultAVObjectPalette", &construct<NiDefaultAVObjectPalette, RC_NiDefaultAVObjectPalette> },
            { "NiBlendBoolInterpolator", &construct<NiBlendBoolInterpolator, RC_NiBlendBoolInterpolator> },
            { "NiBlendFloatInterpolator", &construct<NiBlendFloatInterpolator, RC_NiBlendFloatInterpolator> },
            { "NiBlendPoint3Interpolator", &construct<NiBlendPoint3Interpolator, RC_NiBlendPoint3Interpolator> },
            { "NiBlendTransformInterpolator",
                &construct<NiBlendTransformInterpolator, RC_NiBlendTransformInterpolator> },
            { "bhkCompressedMeshShape", &construct<bhkCompressedMeshShape, RC_bhkCompressedMeshShape> },
            { "bhkCompressedMeshShapeData", &construct<bhkCompressedMeshShapeData, RC_bhkCompressedMeshShapeData> },
        };
    }

    /// Make the factory map used for parsing the file
    static const std::map<std::string, CreateRecord> factories = makeFactory();

    std::string Reader::printVersion(unsigned int version)
    {
        int major = (version >> 24) & 0xFF;
        int minor = (version >> 16) & 0xFF;
        int patch = (version >> 8) & 0xFF;
        int rev = version & 0xFF;

        std::stringstream stream;
        stream << major << "." << minor << "." << patch << "." << rev;
        return stream.str();
    }

    void Reader::parse(Files::IStreamPtr&& stream)
    {
        const std::array<std::uint64_t, 2> fileHash = Files::getHash(filename, *stream);
        hash.append(reinterpret_cast<const char*>(fileHash.data()), fileHash.size() * sizeof(std::uint64_t));

        NIFStream nif(*this, std::move(stream));

        // Check the header string
        std::string head = nif.getVersionString();
        static const std::array<std::string, 2> verStrings = {
            "NetImmerse File Format",
            "Gamebryo File Format",
        };
        const bool supportedHeader = std::any_of(verStrings.begin(), verStrings.end(),
            [&](const std::string& verString) { return head.compare(0, verString.size(), verString) == 0; });
        if (!supportedHeader)
            throw Nif::Exception("Invalid NIF header: " + head, filename);

        // Get BCD version
        ver = nif.getUInt();
        // 4.0.0.0 is an older, practically identical version of the format.
        // It's not used by Morrowind assets but Morrowind supports it.
        static const std::array<uint32_t, 2> supportedVers = {
            NIFStream::generateVersion(4, 0, 0, 0),
            NIFFile::VER_MW,
        };
        const bool supportedVersion = std::find(supportedVers.begin(), supportedVers.end(), ver) != supportedVers.end();
        const bool writeDebugLog = sWriteNifDebugLog;
        if (!supportedVersion)
        {
            if (!sLoadUnsupportedFiles)
                throw Nif::Exception("Unsupported NIF version: " + printVersion(ver), filename);
            if (writeDebugLog)
                Log(Debug::Warning) << " NIFFile Warning: Unsupported NIF version: " << printVersion(ver)
                                    << ". Proceed with caution! File: " << filename;
        }

        // NIF data endianness
        if (ver >= NIFStream::generateVersion(20, 0, 0, 4))
        {
            unsigned char endianness = nif.getChar();
            if (endianness == 0)
                throw Nif::Exception("Big endian NIF files are unsupported", filename);
        }

        // User version
        if (ver > NIFStream::generateVersion(10, 0, 1, 8))
            userVer = nif.getUInt();

        // Number of records
        const std::size_t recNum = nif.getUInt();
        records.resize(recNum);

        // Bethesda stream header
        // It contains Bethesda format version and (useless) export information
        if (ver == NIFFile::VER_OB_OLD
            || (userVer >= 3
                && ((ver == NIFFile::VER_OB || ver == NIFFile::VER_BGS)
                    || (ver >= NIFStream::generateVersion(10, 1, 0, 0) && ver <= NIFStream::generateVersion(20, 0, 0, 4)
                        && userVer <= 11))))
        {
            bethVer = nif.getUInt();
            nif.getExportString(); // Author
            if (bethVer > NIFFile::BETHVER_FO4)
                nif.getUInt(); // Unknown
            nif.getExportString(); // Process script
            nif.getExportString(); // Export script
            if (bethVer == NIFFile::BETHVER_FO4)
                nif.getExportString(); // Max file path
        }

        std::vector<std::string> recTypes;
        std::vector<unsigned short> recTypeIndices;

        const bool hasRecTypeListings = ver >= NIFStream::generateVersion(5, 0, 0, 1);
        if (hasRecTypeListings)
        {
            unsigned short recTypeNum = nif.getUShort();
            if (recTypeNum) // Record type list
                nif.getSizedStrings(recTypes, recTypeNum);
            if (recNum) // Record type mapping for each record
                nif.getUShorts(recTypeIndices, recNum);
            if (ver >= NIFStream::generateVersion(5, 0, 0, 6)) // Groups
            {
                if (ver >= NIFStream::generateVersion(20, 1, 0, 1)) // String table
                {
                    if (ver >= NIFStream::generateVersion(20, 2, 0, 5) && recNum) // Record sizes
                    {
                        std::vector<unsigned int> recSizes; // Currently unused
                        nif.getUInts(recSizes, recNum);
                    }
                    const std::size_t stringNum = nif.getUInt();
                    nif.getUInt(); // Max string length
                    if (stringNum)
                        nif.getSizedStrings(strings, stringNum);
                }
                std::vector<unsigned int> groups; // Currently unused
                unsigned int groupNum = nif.getUInt();
                if (groupNum)
                    nif.getUInts(groups, groupNum);
            }
        }

        const bool hasRecordSeparators
            = ver >= NIFStream::generateVersion(10, 0, 0, 0) && ver < NIFStream::generateVersion(10, 2, 0, 0);
        for (std::size_t i = 0; i < recNum; i++)
        {
            std::unique_ptr<Record> r;

            std::string rec = hasRecTypeListings ? recTypes[recTypeIndices[i]] : nif.getString();
            if (rec.empty())
            {
                std::stringstream error;
                error << "Record number " << i << " out of " << recNum << " is blank.";
                throw Nif::Exception(error.str(), filename);
            }

            // Record separator. Some Havok records in Oblivion do not have it.
            if (hasRecordSeparators && rec.compare(0, 3, "bhk"))
                if (nif.getInt())
                    Log(Debug::Warning) << "NIFFile Warning: Record number " << i << " out of " << recNum
                                        << " is preceded by a non-zero separator. File: " << filename;

            const auto entry = factories.find(rec);

            if (entry == factories.end())
                throw Nif::Exception("Unknown record type " + rec, filename);

            r = entry->second();

            if (!supportedVersion && writeDebugLog)
                Log(Debug::Verbose) << "NIF Debug: Reading record of type " << rec << ", index " << i << " ("
                                    << filename << ")";

            assert(r != nullptr);
            assert(r->recType != RC_MISSING);
            r->recName = rec;
            r->recIndex = i;
            r->read(&nif);
            records[i] = std::move(r);
        }

        const std::size_t rootNum = nif.getUInt();
        roots.resize(rootNum);

        // Determine which records are roots
        for (std::size_t i = 0; i < rootNum; i++)
        {
            int idx = nif.getInt();
            if (idx >= 0 && static_cast<std::size_t>(idx) < records.size())
            {
                roots[i] = records[idx].get();
            }
            else
            {
                roots[i] = nullptr;
                Log(Debug::Warning) << "NIFFile Warning: Root " << i + 1 << " does not point to a record: index " << idx
                                    << ". File: " << filename;
            }
        }

        // Once parsing is done, do post-processing.
        for (const auto& record : records)
            record->post(*this);
    }

    void Reader::setUseSkinning(bool skinning)
    {
        mUseSkinning = skinning;
    }

    std::atomic_bool Reader::sLoadUnsupportedFiles = false;
    std::atomic_bool Reader::sWriteNifDebugLog = false;

    void Reader::setLoadUnsupportedFiles(bool load)
    {
        sLoadUnsupportedFiles = load;
    }

    void Reader::setWriteNifDebugLog(bool value)
    {
        sWriteNifDebugLog = value;
    }

    std::string Reader::getString(std::uint32_t index) const
    {
        if (index == std::numeric_limits<std::uint32_t>::max())
            return std::string();
        return strings.at(index);
    }

}
