#include "nifloader.hpp"

#include <osg/Matrixf>
#include <osg/MatrixTransform>
#include <osg/Geometry>
#include <osg/Array>
#include <osg/LOD>
#include <osg/Switch>
#include <osg/TexGen>
#include <osg/ValueObject>

// resource
#include <components/debug/debuglog.hpp>
#include <components/misc/constants.hpp>
#include <components/misc/stringops.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/resource/imagemanager.hpp>
#include <components/sceneutil/util.hpp>

// particle
#include <osgParticle/ParticleSystem>
#include <osgParticle/ParticleSystemUpdater>
#include <osgParticle/ConstantRateCounter>
#include <osgParticle/BoxPlacer>
#include <osgParticle/ModularProgram>

#include <osg/BlendFunc>
#include <osg/AlphaFunc>
#include <osg/Depth>
#include <osg/PolygonMode>
#include <osg/FrontFace>
#include <osg/Stencil>
#include <osg/Material>
#include <osg/Texture2D>
#include <osg/TexEnv>
#include <osg/TexEnvCombine>

#include <components/nif/node.hpp>
#include <components/nif/effect.hpp>
#include <components/sceneutil/skeleton.hpp>
#include <components/sceneutil/riggeometry.hpp>
#include <components/sceneutil/morphgeometry.hpp>

#include "particle.hpp"
#include "userdata.hpp"

namespace
{

    void getAllNiNodes(const Nif::Node* node, std::vector<int>& outIndices)
    {
        const Nif::NiNode* ninode = dynamic_cast<const Nif::NiNode*>(node);
        if (ninode)
        {
            outIndices.push_back(ninode->recIndex);
            for (unsigned int i=0; i<ninode->children.length(); ++i)
                if (!ninode->children[i].empty())
                    getAllNiNodes(ninode->children[i].getPtr(), outIndices);
        }
    }

    // Collect all properties affecting the given drawable that should be handled on drawable basis rather than on the node hierarchy above it.
    void collectDrawableProperties(const Nif::Node* nifNode, std::vector<const Nif::Property*>& out)
    {
        if (nifNode->parent)
            collectDrawableProperties(nifNode->parent, out);
        const Nif::PropertyList& props = nifNode->props;
        for (size_t i = 0; i <props.length();++i)
        {
            if (!props[i].empty())
            {
                switch (props[i]->recType)
                {
                case Nif::RC_NiMaterialProperty:
                case Nif::RC_NiVertexColorProperty:
                case Nif::RC_NiSpecularProperty:
                case Nif::RC_NiAlphaProperty:
                    out.push_back(props[i].getPtr());
                    break;
                default:
                    break;
                }
            }
        }
    }

    // NodeCallback used to have a node always oriented towards the camera. The node can have translation and scale
    // set just like a regular MatrixTransform, but the rotation set will be overridden in order to face the camera.
    // Must be set as a cull callback.
    class BillboardCallback : public osg::NodeCallback
    {
    public:
        BillboardCallback()
        {
        }
        BillboardCallback(const BillboardCallback& copy, const osg::CopyOp& copyop)
            : osg::NodeCallback(copy, copyop)
        {
        }

        META_Object(NifOsg, BillboardCallback)

        virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
        {
            osgUtil::CullVisitor* cv = static_cast<osgUtil::CullVisitor*>(nv);

            osg::Matrix modelView = *cv->getModelViewMatrix();

            // attempt to preserve scale
            float mag[3];
            for (int i=0;i<3;++i)
            {
                mag[i] = std::sqrt(modelView(0,i) * modelView(0,i) + modelView(1,i) * modelView(1,i) + modelView(2,i) * modelView(2,i));
            }

            modelView.setRotate(osg::Quat());
            modelView(0,0) = mag[0];
            modelView(1,1) = mag[1];
            modelView(2,2) = mag[2];

            cv->pushModelViewMatrix(new osg::RefMatrix(modelView), osg::Transform::RELATIVE_RF);

            traverse(node, nv);

            cv->popModelViewMatrix();
        }
    };

    void extractTextKeys(const Nif::NiTextKeyExtraData *tk, NifOsg::TextKeyMap &textkeys)
    {
        for(size_t i = 0;i < tk->list.size();i++)
        {
            const std::string &str = tk->list[i].text;
            std::string::size_type pos = 0;
            while(pos < str.length())
            {
                if(::isspace(str[pos]))
                {
                    pos++;
                    continue;
                }

                std::string::size_type nextpos = std::min(str.find('\r', pos), str.find('\n', pos));
                if(nextpos != std::string::npos)
                {
                    do {
                        nextpos--;
                    } while(nextpos > pos && ::isspace(str[nextpos]));
                    nextpos++;
                }
                else if(::isspace(*str.rbegin()))
                {
                    std::string::const_iterator last = str.end();
                    do {
                        --last;
                    } while(last != str.begin() && ::isspace(*last));
                    nextpos = std::distance(str.begin(), ++last);
                }
                std::string result = str.substr(pos, nextpos-pos);
                Misc::StringUtils::lowerCaseInPlace(result);
                textkeys.emplace(tk->list[i].time, std::move(result));

                pos = nextpos;
            }
        }
    }
}

namespace NifOsg
{
    class CollisionSwitch : public osg::MatrixTransform
    {
    public:
        CollisionSwitch() : osg::MatrixTransform()
        {
        }

        CollisionSwitch(const CollisionSwitch& copy, const osg::CopyOp& copyop)
            : osg::MatrixTransform(copy, copyop)
        {
        }

        META_Node(NifOsg, CollisionSwitch)

        CollisionSwitch(const osg::Matrixf& transformations, bool enabled) : osg::MatrixTransform(transformations)
        {
            setEnabled(enabled);
        }

        void setEnabled(bool enabled)
        {
            setNodeMask(enabled ? ~0 : Loader::getIntersectionDisabledNodeMask());
        }
    };

    bool Loader::sShowMarkers = false;

    void Loader::setShowMarkers(bool show)
    {
        sShowMarkers = show;
    }

    bool Loader::getShowMarkers()
    {
        return sShowMarkers;
    }

    unsigned int Loader::sHiddenNodeMask = 0;

    void Loader::setHiddenNodeMask(unsigned int mask)
    {
        sHiddenNodeMask = mask;
    }
    unsigned int Loader::getHiddenNodeMask()
    {
        return sHiddenNodeMask;
    }

    unsigned int Loader::sIntersectionDisabledNodeMask = ~0;

    void Loader::setIntersectionDisabledNodeMask(unsigned int mask)
    {
        sIntersectionDisabledNodeMask = mask;
    }

    unsigned int Loader::getIntersectionDisabledNodeMask()
    {
        return sIntersectionDisabledNodeMask;
    }

    class LoaderImpl
    {
    public:
        /// @param filename used for warning messages.
        LoaderImpl(const std::string& filename, unsigned int ver, unsigned int userver, unsigned int bethver)
            : mFilename(filename), mVersion(ver), mUserVersion(userver), mBethVersion(bethver)
        {

        }
        std::string mFilename;
        unsigned int mVersion, mUserVersion, mBethVersion;

        size_t mFirstRootTextureIndex = -1;
        bool mFoundFirstRootTexturingProperty = false;

        // This is used to queue emitters that weren't attached to their node yet.
        std::vector<std::pair<size_t, osg::ref_ptr<Emitter>>> mEmitterQueue;

        static void loadKf(Nif::NIFFilePtr nif, KeyframeHolder& target)
        {
            const Nif::NiSequenceStreamHelper *seq = nullptr;
            const size_t numRoots = nif->numRoots();
            for (size_t i = 0; i < numRoots; ++i)
            {
                const Nif::Record *r = nif->getRoot(i);
                assert(r != nullptr);
                if (r->recType == Nif::RC_NiSequenceStreamHelper)
                {
                    seq = static_cast<const Nif::NiSequenceStreamHelper*>(r);
                    break;
                }
            }

            if (!seq)
            {
                nif->warn("Found no NiSequenceStreamHelper root record");
                return;
            }

            Nif::ExtraPtr extra = seq->extra;
            if(extra.empty() || extra->recType != Nif::RC_NiTextKeyExtraData)
            {
                nif->warn("First extra data was not a NiTextKeyExtraData, but a "+
                          (extra.empty() ? std::string("nil") : extra->recName)+".");
                return;
            }

            extractTextKeys(static_cast<const Nif::NiTextKeyExtraData*>(extra.getPtr()), target.mTextKeys);

            extra = extra->next;
            Nif::ControllerPtr ctrl = seq->controller;
            for(;!extra.empty() && !ctrl.empty();(extra=extra->next),(ctrl=ctrl->next))
            {
                if(extra->recType != Nif::RC_NiStringExtraData || ctrl->recType != Nif::RC_NiKeyframeController)
                {
                    nif->warn("Unexpected extra data "+extra->recName+" with controller "+ctrl->recName);
                    continue;
                }

                // Vanilla seems to ignore the "active" flag for NiKeyframeController,
                // so we don't want to skip inactive controllers here.

                const Nif::NiStringExtraData *strdata = static_cast<const Nif::NiStringExtraData*>(extra.getPtr());
                const Nif::NiKeyframeController *key = static_cast<const Nif::NiKeyframeController*>(ctrl.getPtr());

                if(key->data.empty())
                    continue;

                osg::ref_ptr<NifOsg::KeyframeController> callback(new NifOsg::KeyframeController(key->data.getPtr()));
                callback->setFunction(std::shared_ptr<NifOsg::ControllerFunction>(new NifOsg::ControllerFunction(key)));

                if (!target.mKeyframeControllers.emplace(strdata->string, callback).second)
                    Log(Debug::Verbose) << "Controller " << strdata->string << " present more than once in " << nif->getFilename() << ", ignoring later version";
            }
        }

        osg::ref_ptr<osg::Node> load(Nif::NIFFilePtr nif, Resource::ImageManager* imageManager)
        {
            const Nif::Node* nifNode = nullptr;
            const size_t numRoots = nif->numRoots();
            for (size_t i = 0; i < numRoots; ++i)
            {
                const Nif::Record* r = nif->getRoot(i);
                if ((nifNode = dynamic_cast<const Nif::Node*>(r)))
                    break;
            }
            if (!nifNode)
                nif->fail("Found no root nodes");

            osg::ref_ptr<TextKeyMapHolder> textkeys (new TextKeyMapHolder);

            osg::ref_ptr<osg::Node> created = handleNode(nifNode, nullptr, imageManager, std::vector<unsigned int>(), 0, false, false, false, &textkeys->mTextKeys);

            // Attach particle emitters to their nodes which should all be loaded by now.
            handleQueuedParticleEmitters(created, nif);

            if (nif->getUseSkinning())
            {
                osg::ref_ptr<SceneUtil::Skeleton> skel = new SceneUtil::Skeleton;

                osg::Group* root = created->asGroup();
                if (root && root->getDataVariance() == osg::Object::STATIC && !root->asTransform())
                {
                    skel->setStateSet(root->getStateSet());
                    skel->setName(root->getName());
                    for (unsigned int i=0; i<root->getNumChildren(); ++i)
                        skel->addChild(root->getChild(i));
                    root->removeChildren(0, root->getNumChildren());
                }
                else
                    skel->addChild(created);
                created = skel;
            }

            if (!textkeys->mTextKeys.empty())
                created->getOrCreateUserDataContainer()->addUserObject(textkeys);

            return created;
        }

        void applyNodeProperties(const Nif::Node *nifNode, osg::Node *applyTo, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector<unsigned int>& boundTextures, int animflags)
        {
            const Nif::PropertyList& props = nifNode->props;
            for (size_t i = 0; i <props.length(); ++i)
            {
                if (!props[i].empty())
                {
                    // Get the lowest numbered recIndex of the NiTexturingProperty root node.
                    // This is what is overridden when a spell effect "particle texture" is used.
                    if (nifNode->parent == nullptr && !mFoundFirstRootTexturingProperty && props[i].getPtr()->recType == Nif::RC_NiTexturingProperty)
                    {
                        mFirstRootTextureIndex = props[i].getPtr()->recIndex;
                        mFoundFirstRootTexturingProperty = true;
                    }
                    else if (props[i].getPtr()->recType == Nif::RC_NiTexturingProperty)
                    {
                        if (props[i].getPtr()->recIndex == mFirstRootTextureIndex)
                            applyTo->setUserValue("overrideFx", 1);                
                    }
                    handleProperty(props[i].getPtr(), applyTo, composite, imageManager, boundTextures, animflags);
                }              
            }
        }

        void setupController(const Nif::Controller* ctrl, SceneUtil::Controller* toSetup, int animflags)
        {
            bool autoPlay = animflags & Nif::NiNode::AnimFlag_AutoPlay;
            if (autoPlay)
                toSetup->setSource(std::shared_ptr<SceneUtil::ControllerSource>(new SceneUtil::FrameTimeSource));

            toSetup->setFunction(std::shared_ptr<ControllerFunction>(new ControllerFunction(ctrl)));
        }

        osg::ref_ptr<osg::LOD> handleLodNode(const Nif::NiLODNode* niLodNode)
        {
            osg::ref_ptr<osg::LOD> lod (new osg::LOD);
            lod->setName(niLodNode->name);
            lod->setCenterMode(osg::LOD::USER_DEFINED_CENTER);
            lod->setCenter(niLodNode->lodCenter);
            for (unsigned int i=0; i<niLodNode->lodLevels.size(); ++i)
            {
                const Nif::NiLODNode::LODRange& range = niLodNode->lodLevels[i];
                lod->setRange(i, range.minRange, range.maxRange);
            }
            lod->setRangeMode(osg::LOD::DISTANCE_FROM_EYE_POINT);
            return lod;
        }

        osg::ref_ptr<osg::Switch> handleSwitchNode(const Nif::NiSwitchNode* niSwitchNode)
        {
            osg::ref_ptr<osg::Switch> switchNode (new osg::Switch);
            switchNode->setName(niSwitchNode->name);
            switchNode->setNewChildDefaultValue(false);
            switchNode->setSingleChildOn(niSwitchNode->initialIndex);
            return switchNode;
        }

        osg::ref_ptr<osg::Image> handleSourceTexture(const Nif::NiSourceTexture* st, Resource::ImageManager* imageManager)
        {
            if (!st)
                return nullptr;

            osg::ref_ptr<osg::Image> image;
            if (!st->external && !st->data.empty())
            {
                image = handleInternalTexture(st->data.getPtr());
            }
            else
            {
                std::string filename = Misc::ResourceHelpers::correctTexturePath(st->filename, imageManager->getVFS());
                image = imageManager->getImage(filename);
            }
            return image;
        }

        void handleEffect(const Nif::Node* nifNode, osg::Node* node, Resource::ImageManager* imageManager)
        {
            if (nifNode->recType != Nif::RC_NiTextureEffect)
            {
                Log(Debug::Info) << "Unhandled effect " << nifNode->recName << " in " << mFilename;
                return;
            }

            const Nif::NiTextureEffect* textureEffect = static_cast<const Nif::NiTextureEffect*>(nifNode);
            if (textureEffect->textureType != Nif::NiTextureEffect::Environment_Map)
            {
                Log(Debug::Info) << "Unhandled NiTextureEffect type " << textureEffect->textureType << " in " << mFilename;
                return;
            }

            if (textureEffect->texture.empty())
            {
                Log(Debug::Info) << "NiTextureEffect missing source texture in " << mFilename;
                return;
            }

            osg::ref_ptr<osg::TexGen> texGen (new osg::TexGen);
            switch (textureEffect->coordGenType)
            {
            case Nif::NiTextureEffect::World_Parallel:
                texGen->setMode(osg::TexGen::OBJECT_LINEAR);
                break;
            case Nif::NiTextureEffect::World_Perspective:
                texGen->setMode(osg::TexGen::EYE_LINEAR);
                break;
            case Nif::NiTextureEffect::Sphere_Map:
                texGen->setMode(osg::TexGen::SPHERE_MAP);
                break;
            default:
                Log(Debug::Info) << "Unhandled NiTextureEffect coordGenType " << textureEffect->coordGenType << " in " << mFilename;
                return;
            }

            osg::ref_ptr<osg::Image> image (handleSourceTexture(textureEffect->texture.getPtr(), imageManager));
            osg::ref_ptr<osg::Texture2D> texture2d (new osg::Texture2D(image));
            if (image)
                texture2d->setTextureSize(image->s(), image->t());
            texture2d->setName("envMap");
            bool wrapT = textureEffect->clamp & 0x1;
            bool wrapS = (textureEffect->clamp >> 1) & 0x1;
            texture2d->setWrap(osg::Texture::WRAP_S, wrapS ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE);
            texture2d->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE);

            osg::ref_ptr<osg::TexEnvCombine> texEnv = new osg::TexEnvCombine;
            texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE);
            texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS);
            texEnv->setCombine_RGB(osg::TexEnvCombine::ADD);
            texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS);
            texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE);

            int texUnit = 3; // FIXME

            osg::StateSet* stateset = node->getOrCreateStateSet();
            stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON);
            stateset->setTextureAttributeAndModes(texUnit, texGen, osg::StateAttribute::ON);
            stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON);

            stateset->addUniform(new osg::Uniform("envMapColor", osg::Vec4f(1,1,1,1)));
        }

        // Get a default dataVariance for this node to be used as a hint by optimization (post)routines
        osg::ref_ptr<osg::Group> createNode(const Nif::Node* nifNode)
        {
            osg::ref_ptr<osg::Group> node;
            osg::Object::DataVariance dataVariance = osg::Object::UNSPECIFIED;

            switch (nifNode->recType)
            {
            case Nif::RC_NiAutoNormalParticles:
            case Nif::RC_NiRotatingParticles:
                // Leaf nodes in the NIF hierarchy, so won't be able to dynamically attach children.
                // No support for keyframe controllers (just crashes in the original engine).
                if (nifNode->trafo.isIdentity())
                    node = new osg::Group;
                dataVariance = osg::Object::STATIC;
                break;
            case Nif::RC_NiBillboardNode:
                dataVariance = osg::Object::DYNAMIC;
                break;
            case Nif::RC_NiCollisionSwitch:
            {
                bool enabled = nifNode->flags & Nif::NiNode::Flag_ActiveCollision;
                node = new CollisionSwitch(nifNode->trafo.toMatrix(), enabled);
                // This matrix transform must not be combined with another matrix transform.
                dataVariance = osg::Object::DYNAMIC;
                break;
            }
            default:
                // The Root node can be created as a Group if no transformation is required.
                // This takes advantage of the fact root nodes can't have additional controllers
                // loaded from an external .kf file (original engine just throws "can't find node" errors if you try).
                if (!nifNode->parent && nifNode->controller.empty() && nifNode->trafo.isIdentity())
                {
                    node = new osg::Group;
                    dataVariance = osg::Object::STATIC;
                }
                else
                {
                    dataVariance = (nifNode->controller.empty() ? osg::Object::STATIC : osg::Object::DYNAMIC);
                }

                if (nifNode->isBone)
                    dataVariance = osg::Object::DYNAMIC;

                break;
            }
            if (!node)
                node = new osg::MatrixTransform(nifNode->trafo.toMatrix());

            node->setDataVariance(dataVariance);

            return node;
        }

        osg::ref_ptr<osg::Node> handleNode(const Nif::Node* nifNode, osg::Group* parentNode, Resource::ImageManager* imageManager,
                                std::vector<unsigned int> boundTextures, int animflags, bool skipMeshes, bool hasMarkers, bool isAnimated, TextKeyMap* textKeys, osg::Node* rootNode=nullptr)
        {
            if (rootNode != nullptr && Misc::StringUtils::ciEqual(nifNode->name, "Bounding Box"))
                return nullptr;

            osg::ref_ptr<osg::Group> node = createNode(nifNode);

            if (nifNode->recType == Nif::RC_NiBillboardNode)
            {
                node->addCullCallback(new BillboardCallback);
            }

            if (!nifNode->controller.empty() && nifNode->controller->recType == Nif::RC_NiKeyframeController)
                isAnimated = true;

            node->setName(nifNode->name);

            if (parentNode)
                parentNode->addChild(node);

            if (!rootNode)
                rootNode = node;

            // UserData used for a variety of features:
            // - finding the correct emitter node for a particle system
            // - establishing connections to the animated collision shapes, which are handled in a separate loader
            // - finding a random child NiNode in NiBspArrayController
            // - storing the previous 3x3 rotation and scale values for when a KeyframeController wants to
            //   change only certain elements of the 4x4 transform
            node->getOrCreateUserDataContainer()->addUserObject(
                new NodeUserData(nifNode->recIndex, nifNode->trafo.scale, nifNode->trafo.rotation));

            for (Nif::ExtraPtr e = nifNode->extra; !e.empty(); e = e->next)
            {
                if(e->recType == Nif::RC_NiTextKeyExtraData && textKeys)
                {
                    const Nif::NiTextKeyExtraData *tk = static_cast<const Nif::NiTextKeyExtraData*>(e.getPtr());
                    extractTextKeys(tk, *textKeys);
                }
                else if(e->recType == Nif::RC_NiStringExtraData)
                {
                    const Nif::NiStringExtraData *sd = static_cast<const Nif::NiStringExtraData*>(e.getPtr());
                    // String markers may contain important information
                    // affecting the entire subtree of this obj
                    if(sd->string == "MRK" && !Loader::getShowMarkers())
                    {
                        // Marker objects. These meshes are only visible in the editor.
                        hasMarkers = true;
                    }
                    else if(sd->string == "BONE")
                    {
                        node->getOrCreateUserDataContainer()->addDescription("CustomBone");
                    }
                }
            }

            if (nifNode->recType == Nif::RC_NiBSAnimationNode || nifNode->recType == Nif::RC_NiBSParticleNode)
                animflags = nifNode->flags;

            // Hide collision shapes, but don't skip the subgraph
            // We still need to animate the hidden bones so the physics system can access them
            if (nifNode->recType == Nif::RC_RootCollisionNode)
            {
                skipMeshes = true;
                node->setNodeMask(Loader::getHiddenNodeMask());
            }

            // We can skip creating meshes for hidden nodes if they don't have a VisController that
            // might make them visible later
            if (nifNode->flags & Nif::NiNode::Flag_Hidden)
            {
                bool hasVisController = false;
                for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next)
                {
                    if ((hasVisController |= (ctrl->recType == Nif::RC_NiVisController)))
                        break;
                }

                if (!hasVisController)
                    skipMeshes = true; // skip child meshes, but still create the child node hierarchy for animating collision shapes

                node->setNodeMask(Loader::getHiddenNodeMask());
            }

            if ((skipMeshes || hasMarkers) && isAnimated) // make sure the empty node is not optimized away so the physicssystem can find it.
            {
                node->setDataVariance(osg::Object::DYNAMIC);
            }

            if ((nifNode->recType == Nif::RC_NiTriShape || nifNode->recType == Nif::RC_NiTriStrips) && isAnimated) // Same thing for animated shapes
            {
                node->setDataVariance(osg::Object::DYNAMIC);
            }

            osg::ref_ptr<SceneUtil::CompositeStateSetUpdater> composite = new SceneUtil::CompositeStateSetUpdater;

            applyNodeProperties(nifNode, node, composite, imageManager, boundTextures, animflags);

            if ((nifNode->recType == Nif::RC_NiTriShape || nifNode->recType == Nif::RC_NiTriStrips) && !skipMeshes)
            {
                const std::string nodeName = Misc::StringUtils::lowerCase(nifNode->name);
                static const std::string markerName = "tri editormarker";
                static const std::string shadowName = "shadow";
                static const std::string shadowName2 = "tri shadow";
                const bool isMarker = hasMarkers && !nodeName.compare(0, markerName.size(), markerName);
                if (!isMarker && nodeName.compare(0, shadowName.size(), shadowName) && nodeName.compare(0, shadowName2.size(), shadowName2))
                {
                    Nif::NiSkinInstancePtr skin = static_cast<const Nif::NiGeometry*>(nifNode)->skin;

                    if (skin.empty())
                        handleTriShape(nifNode, node, composite, boundTextures, animflags);
                    else
                        handleSkinnedTriShape(nifNode, node, composite, boundTextures, animflags);

                    if (!nifNode->controller.empty())
                        handleMeshControllers(nifNode, node, composite, boundTextures, animflags);
                }
            }

            if(nifNode->recType == Nif::RC_NiAutoNormalParticles || nifNode->recType == Nif::RC_NiRotatingParticles)
                handleParticleSystem(nifNode, node, composite, animflags, rootNode);

            if (composite->getNumControllers() > 0)
                node->addUpdateCallback(composite);

            if (nifNode->recType != Nif::RC_NiTriShape && nifNode->recType != Nif::RC_NiTriStrips
                    && !nifNode->controller.empty() && node->getDataVariance() == osg::Object::DYNAMIC)
                handleNodeControllers(nifNode, static_cast<osg::MatrixTransform*>(node.get()), animflags);

            // LOD and Switch nodes must be wrapped by a transform (the current node) to support transformations properly
            // and we need to attach their children to the osg::LOD/osg::Switch nodes
            // but we must return that transform to the caller of handleNode instead of the actual LOD/Switch nodes.
            osg::ref_ptr<osg::Group> currentNode = node;

            if (nifNode->recType == Nif::RC_NiSwitchNode)
            {
                const Nif::NiSwitchNode* niSwitchNode = static_cast<const Nif::NiSwitchNode*>(nifNode);
                osg::ref_ptr<osg::Switch> switchNode = handleSwitchNode(niSwitchNode);
                node->addChild(switchNode);
                if (niSwitchNode->name == Constants::NightDayLabel && !SceneUtil::hasUserDescription(rootNode, Constants::NightDayLabel))
                    rootNode->getOrCreateUserDataContainer()->addDescription(Constants::NightDayLabel);
                else if (niSwitchNode->name == Constants::HerbalismLabel && !SceneUtil::hasUserDescription(rootNode, Constants::HerbalismLabel))
                    rootNode->getOrCreateUserDataContainer()->addDescription(Constants::HerbalismLabel);

                currentNode = switchNode;
            }
            else if (nifNode->recType == Nif::RC_NiLODNode)
            {
                const Nif::NiLODNode* niLodNode = static_cast<const Nif::NiLODNode*>(nifNode);
                osg::ref_ptr<osg::LOD> lodNode = handleLodNode(niLodNode);
                node->addChild(lodNode);
                currentNode = lodNode;
            }

            const Nif::NiNode *ninode = dynamic_cast<const Nif::NiNode*>(nifNode);
            if(ninode)
            {
                const Nif::NodeList &effects = ninode->effects;
                for (size_t i = 0; i < effects.length(); ++i)
                {
                    if (!effects[i].empty())
                        handleEffect(effects[i].getPtr(), currentNode, imageManager);
                }

                const Nif::NodeList &children = ninode->children;
                for(size_t i = 0;i < children.length();++i)
                {
                    if(!children[i].empty())
                        handleNode(children[i].getPtr(), currentNode, imageManager, boundTextures, animflags, skipMeshes, hasMarkers, isAnimated, textKeys, rootNode);
                }
            }

            return node;
        }

        void handleMeshControllers(const Nif::Node *nifNode, osg::Node* node, SceneUtil::CompositeStateSetUpdater* composite, const std::vector<unsigned int> &boundTextures, int animflags)
        {
            for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next)
            {
                if (!(ctrl->flags & Nif::NiNode::ControllerFlag_Active))
                    continue;
                if (ctrl->recType == Nif::RC_NiUVController)
                {
                    const Nif::NiUVController *niuvctrl = static_cast<const Nif::NiUVController*>(ctrl.getPtr());
                    if (niuvctrl->data.empty())
                        continue;
                    const unsigned int uvSet = niuvctrl->uvSet;
                    std::set<int> texUnits;
                    // UVController should work only for textures which use a given UV Set, usually 0.
                    for (unsigned int i=0; i<boundTextures.size(); ++i)
                    {
                        if (boundTextures[i] == uvSet)
                            texUnits.insert(i);
                    }

                    osg::ref_ptr<UVController> uvctrl = new UVController(niuvctrl->data.getPtr(), texUnits);
                    setupController(niuvctrl, uvctrl, animflags);
                    composite->addController(uvctrl);
                }
                else if (ctrl->recType == Nif::RC_NiKeyframeController)
                {
                    const Nif::NiKeyframeController *key = static_cast<const Nif::NiKeyframeController*>(ctrl.getPtr());
                    if(!key->data.empty())
                    {
                        osg::ref_ptr<KeyframeController> callback(new KeyframeController(key->data.getPtr()));

                        setupController(key, callback, animflags);
                        node->addUpdateCallback(callback);
                    }
                }
                else if (ctrl->recType == Nif::RC_NiPathController)
                {
                    const Nif::NiPathController *path = static_cast<const Nif::NiPathController*>(ctrl.getPtr());
                    if (!path->posData.empty() && !path->floatData.empty())
                    {
                        osg::ref_ptr<PathController> callback(new PathController(path));

                        setupController(path, callback, animflags);
                        node->addUpdateCallback(callback);
                    }
                }
                else if (ctrl->recType == Nif::RC_NiVisController)
                {
                    handleVisController(static_cast<const Nif::NiVisController*>(ctrl.getPtr()), node, animflags);
                }
                else if(ctrl->recType == Nif::RC_NiGeomMorpherController)
                {} // handled in handleTriShape
                else
                    Log(Debug::Info) << "Unhandled controller " << ctrl->recName << " on node " << nifNode->recIndex << " in " << mFilename;
            }
        }

        void handleNodeControllers(const Nif::Node* nifNode, osg::MatrixTransform* transformNode, int animflags)
        {
            for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next)
            {
                if (!(ctrl->flags & Nif::NiNode::ControllerFlag_Active))
                    continue;
                if (ctrl->recType == Nif::RC_NiKeyframeController)
                {
                    const Nif::NiKeyframeController *key = static_cast<const Nif::NiKeyframeController*>(ctrl.getPtr());
                    if(!key->data.empty())
                    {
                        osg::ref_ptr<KeyframeController> callback(new KeyframeController(key->data.getPtr()));

                        setupController(key, callback, animflags);
                        transformNode->addUpdateCallback(callback);
                    }
                }
                else if (ctrl->recType == Nif::RC_NiPathController)
                {
                    const Nif::NiPathController *path = static_cast<const Nif::NiPathController*>(ctrl.getPtr());
                    if (!path->posData.empty() && !path->floatData.empty())
                    {
                        osg::ref_ptr<PathController> callback(new PathController(path));

                        setupController(path, callback, animflags);
                        transformNode->addUpdateCallback(callback);
                    }
                }
                else if (ctrl->recType == Nif::RC_NiVisController)
                {
                    handleVisController(static_cast<const Nif::NiVisController*>(ctrl.getPtr()), transformNode, animflags);
                }
                else if (ctrl->recType == Nif::RC_NiRollController)
                {
                    handleRollController(static_cast<const Nif::NiRollController*>(ctrl.getPtr()), transformNode, animflags);
                }
                else
                    Log(Debug::Info) << "Unhandled controller " << ctrl->recName << " on node " << nifNode->recIndex << " in " << mFilename;
            }
        }

        void handleVisController(const Nif::NiVisController* visctrl, osg::Node* node, int animflags)
        {
            if (visctrl->data.empty())
                return;
            osg::ref_ptr<VisController> callback(new VisController(visctrl->data.getPtr(), Loader::getHiddenNodeMask()));
            setupController(visctrl, callback, animflags);
            node->addUpdateCallback(callback);
        }

        void handleRollController(const Nif::NiRollController* rollctrl, osg::Node* node, int animflags)
        {
            if (rollctrl->data.empty())
                return;
            osg::ref_ptr<RollController> callback(new RollController(rollctrl->data.getPtr()));
            setupController(rollctrl, callback, animflags);
            node->addUpdateCallback(callback);
        }

        void handleMaterialControllers(const Nif::Property *materialProperty, SceneUtil::CompositeStateSetUpdater* composite, int animflags)
        {
            for (Nif::ControllerPtr ctrl = materialProperty->controller; !ctrl.empty(); ctrl = ctrl->next)
            {
                if (!(ctrl->flags & Nif::NiNode::ControllerFlag_Active))
                    continue;
                if (ctrl->recType == Nif::RC_NiAlphaController)
                {
                    const Nif::NiAlphaController* alphactrl = static_cast<const Nif::NiAlphaController*>(ctrl.getPtr());
                    if (alphactrl->data.empty())
                        continue;
                    osg::ref_ptr<AlphaController> osgctrl(new AlphaController(alphactrl->data.getPtr()));
                    setupController(alphactrl, osgctrl, animflags);
                    composite->addController(osgctrl);
                }
                else if (ctrl->recType == Nif::RC_NiMaterialColorController)
                {
                    const Nif::NiMaterialColorController* matctrl = static_cast<const Nif::NiMaterialColorController*>(ctrl.getPtr());
                    if (matctrl->data.empty())
                        continue;
                    auto targetColor = static_cast<MaterialColorController::TargetColor>(matctrl->targetColor);
                    osg::ref_ptr<MaterialColorController> osgctrl(new MaterialColorController(matctrl->data.getPtr(), targetColor));
                    setupController(matctrl, osgctrl, animflags);
                    composite->addController(osgctrl);
                }
                else
                    Log(Debug::Info) << "Unexpected material controller " << ctrl->recType << " in " << mFilename;
            }
        }

        void handleTextureControllers(const Nif::Property *texProperty, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, osg::StateSet *stateset, int animflags)
        {
            for (Nif::ControllerPtr ctrl = texProperty->controller; !ctrl.empty(); ctrl = ctrl->next)
            {
                if (!(ctrl->flags & Nif::NiNode::ControllerFlag_Active))
                    continue;
                if (ctrl->recType == Nif::RC_NiFlipController)
                {
                    const Nif::NiFlipController* flipctrl = static_cast<const Nif::NiFlipController*>(ctrl.getPtr());
                    std::vector<osg::ref_ptr<osg::Texture2D> > textures;
                    for (unsigned int i=0; i<flipctrl->mSources.length(); ++i)
                    {
                        Nif::NiSourceTexturePtr st = flipctrl->mSources[i];
                        if (st.empty())
                            continue;

                        // inherit wrap settings from the target slot
                        osg::Texture2D* inherit = dynamic_cast<osg::Texture2D*>(stateset->getTextureAttribute(flipctrl->mTexSlot, osg::StateAttribute::TEXTURE));
                        osg::Texture2D::WrapMode wrapS = osg::Texture2D::CLAMP_TO_EDGE;
                        osg::Texture2D::WrapMode wrapT = osg::Texture2D::CLAMP_TO_EDGE;
                        if (inherit)
                        {
                            wrapS = inherit->getWrap(osg::Texture2D::WRAP_S);
                            wrapT = inherit->getWrap(osg::Texture2D::WRAP_T);
                        }

                        osg::ref_ptr<osg::Image> image (handleSourceTexture(st.getPtr(), imageManager));
                        osg::ref_ptr<osg::Texture2D> texture (new osg::Texture2D(image));
                        if (image)
                            texture->setTextureSize(image->s(), image->t());
                        texture->setWrap(osg::Texture::WRAP_S, wrapS);
                        texture->setWrap(osg::Texture::WRAP_T, wrapT);
                        textures.push_back(texture);
                    }
                    osg::ref_ptr<FlipController> callback(new FlipController(flipctrl, textures));
                    setupController(ctrl.getPtr(), callback, animflags);
                    composite->addController(callback);
                }
                else
                    Log(Debug::Info) << "Unexpected texture controller " << ctrl->recName << " in " << mFilename;
            }
        }

        void handleParticlePrograms(Nif::NiParticleModifierPtr affectors, Nif::NiParticleModifierPtr colliders, osg::Group *attachTo, osgParticle::ParticleSystem* partsys, osgParticle::ParticleProcessor::ReferenceFrame rf)
        {
            osgParticle::ModularProgram* program = new osgParticle::ModularProgram;
            attachTo->addChild(program);
            program->setParticleSystem(partsys);
            program->setReferenceFrame(rf);
            for (; !affectors.empty(); affectors = affectors->next)
            {
                if (affectors->recType == Nif::RC_NiParticleGrowFade)
                {
                    const Nif::NiParticleGrowFade *gf = static_cast<const Nif::NiParticleGrowFade*>(affectors.getPtr());
                    program->addOperator(new GrowFadeAffector(gf->growTime, gf->fadeTime));
                }
                else if (affectors->recType == Nif::RC_NiGravity)
                {
                    const Nif::NiGravity* gr = static_cast<const Nif::NiGravity*>(affectors.getPtr());
                    program->addOperator(new GravityAffector(gr));
                }
                else if (affectors->recType == Nif::RC_NiParticleColorModifier)
                {
                    const Nif::NiParticleColorModifier *cl = static_cast<const Nif::NiParticleColorModifier*>(affectors.getPtr());
                    const Nif::NiColorData *clrdata = cl->data.getPtr();
                    program->addOperator(new ParticleColorAffector(clrdata));
                }
                else if (affectors->recType == Nif::RC_NiParticleRotation)
                {
                    // unused
                }
                else
                    Log(Debug::Info) << "Unhandled particle modifier " << affectors->recName << " in " << mFilename;
            }
            for (; !colliders.empty(); colliders = colliders->next)
            {
                if (colliders->recType == Nif::RC_NiPlanarCollider)
                {
                    const Nif::NiPlanarCollider* planarcollider = static_cast<const Nif::NiPlanarCollider*>(colliders.getPtr());
                    program->addOperator(new PlanarCollider(planarcollider));
                }
                else if (colliders->recType == Nif::RC_NiSphericalCollider)
                {
                    const Nif::NiSphericalCollider* sphericalcollider = static_cast<const Nif::NiSphericalCollider*>(colliders.getPtr());
                    program->addOperator(new SphericalCollider(sphericalcollider));
                }
                else
                    Log(Debug::Info) << "Unhandled particle collider " << colliders->recName << " in " << mFilename;
            }
        }

        // Load the initial state of the particle system, i.e. the initial particles and their positions, velocity and colors.
        void handleParticleInitialState(const Nif::Node* nifNode, osgParticle::ParticleSystem* partsys, const Nif::NiParticleSystemController* partctrl)
        {
            const Nif::NiAutoNormalParticlesData *particledata = nullptr;
            if(nifNode->recType == Nif::RC_NiAutoNormalParticles)
                particledata = static_cast<const Nif::NiAutoNormalParticles*>(nifNode)->data.getPtr();
            else if(nifNode->recType == Nif::RC_NiRotatingParticles)
                particledata = static_cast<const Nif::NiRotatingParticles*>(nifNode)->data.getPtr();
            else
                return;

            osg::BoundingBox box;

            int i=0;
            for (std::vector<Nif::NiParticleSystemController::Particle>::const_iterator it = partctrl->particles.begin();
                 i<particledata->activeCount && it != partctrl->particles.end(); ++it, ++i)
            {
                const Nif::NiParticleSystemController::Particle& particle = *it;

                ParticleAgeSetter particletemplate(std::max(0.f, particle.lifetime));

                osgParticle::Particle* created = partsys->createParticle(&particletemplate);
                created->setLifeTime(std::max(0.f, particle.lifespan));

                // Note this position and velocity is not correct for a particle system with absolute reference frame,
                // which can not be done in this loader since we are not attached to the scene yet. Will be fixed up post-load in the SceneManager.
                created->setVelocity(particle.velocity);
                const osg::Vec3f& position = particledata->vertices.at(particle.vertex);
                created->setPosition(position);

                osg::Vec4f partcolor (1.f,1.f,1.f,1.f);
                if (particle.vertex < int(particledata->colors.size()))
                    partcolor = particledata->colors.at(particle.vertex);

                float size = partctrl->size;
                if (particle.vertex < int(particledata->sizes.size()))
                    size *= particledata->sizes.at(particle.vertex);

                created->setSizeRange(osgParticle::rangef(size, size));
                box.expandBy(osg::BoundingSphere(position, size));
            }

            // radius may be used to force a larger bounding box
            box.expandBy(osg::BoundingSphere(osg::Vec3(0,0,0), particledata->radius));

            partsys->setInitialBound(box);
        }

        osg::ref_ptr<Emitter> handleParticleEmitter(const Nif::NiParticleSystemController* partctrl)
        {
            std::vector<int> targets;
            if (partctrl->recType == Nif::RC_NiBSPArrayController)
            {
                getAllNiNodes(partctrl->emitter.getPtr(), targets);
            }

            osg::ref_ptr<Emitter> emitter = new Emitter(targets);

            osgParticle::ConstantRateCounter* counter = new osgParticle::ConstantRateCounter;
            if (partctrl->emitFlags & Nif::NiParticleSystemController::NoAutoAdjust)
                counter->setNumberOfParticlesPerSecondToCreate(partctrl->emitRate);
            else
                counter->setNumberOfParticlesPerSecondToCreate(partctrl->numParticles / (partctrl->lifetime + partctrl->lifetimeRandom/2));

            emitter->setCounter(counter);

            ParticleShooter* shooter = new ParticleShooter(partctrl->velocity - partctrl->velocityRandom*0.5f,
                                                           partctrl->velocity + partctrl->velocityRandom*0.5f,
                                                           partctrl->horizontalDir, partctrl->horizontalAngle,
                                                           partctrl->verticalDir, partctrl->verticalAngle,
                                                           partctrl->lifetime, partctrl->lifetimeRandom);
            emitter->setShooter(shooter);

            osgParticle::BoxPlacer* placer = new osgParticle::BoxPlacer;
            placer->setXRange(-partctrl->offsetRandom.x() / 2.f, partctrl->offsetRandom.x() / 2.f);
            placer->setYRange(-partctrl->offsetRandom.y() / 2.f, partctrl->offsetRandom.y() / 2.f);
            placer->setZRange(-partctrl->offsetRandom.z() / 2.f, partctrl->offsetRandom.z() / 2.f);

            emitter->setPlacer(placer);
            return emitter;
        }

        void handleQueuedParticleEmitters(osg::Node* rootNode, Nif::NIFFilePtr nif)
        {
            for (const auto& emitterPair : mEmitterQueue)
            {
                size_t recIndex = emitterPair.first;
                FindGroupByRecIndex findEmitterNode(recIndex);
                rootNode->accept(findEmitterNode);
                osg::Group* emitterNode = findEmitterNode.mFound;
                if (!emitterNode)
                {
                    nif->warn("Failed to find particle emitter emitter node (node record index " + std::to_string(recIndex) + ")");
                    continue;
                }

                // Emitter attached to the emitter node. Note one side effect of the emitter using the CullVisitor is that hiding its node
                // actually causes the emitter to stop firing. Convenient, because MW behaves this way too!
                emitterNode->addChild(emitterPair.second);
            }
            mEmitterQueue.clear();
        }

        void handleParticleSystem(const Nif::Node *nifNode, osg::Group *parentNode, SceneUtil::CompositeStateSetUpdater* composite, int animflags, osg::Node* rootNode)
        {
            osg::ref_ptr<ParticleSystem> partsys (new ParticleSystem);
            partsys->setSortMode(osgParticle::ParticleSystem::SORT_BACK_TO_FRONT);

            const Nif::NiParticleSystemController* partctrl = nullptr;
            for (Nif::ControllerPtr ctrl = nifNode->controller; !ctrl.empty(); ctrl = ctrl->next)
            {
                if (!(ctrl->flags & Nif::NiNode::ControllerFlag_Active))
                    continue;
                if(ctrl->recType == Nif::RC_NiParticleSystemController || ctrl->recType == Nif::RC_NiBSPArrayController)
                    partctrl = static_cast<Nif::NiParticleSystemController*>(ctrl.getPtr());
                else
                    Log(Debug::Info) << "Unhandled controller " << ctrl->recName << " on node " << nifNode->recIndex << " in " << mFilename;
            }
            if (!partctrl)
            {
                Log(Debug::Info) << "No particle controller found in " << mFilename;
                return;
            }

            osgParticle::ParticleProcessor::ReferenceFrame rf = (animflags & Nif::NiNode::ParticleFlag_LocalSpace)
                    ? osgParticle::ParticleProcessor::RELATIVE_RF
                    : osgParticle::ParticleProcessor::ABSOLUTE_RF;

            // HACK: ParticleSystem has no setReferenceFrame method
            if (rf == osgParticle::ParticleProcessor::ABSOLUTE_RF)
            {
                partsys->getOrCreateUserDataContainer()->addDescription("worldspace");
            }

            partsys->setParticleScaleReferenceFrame(osgParticle::ParticleSystem::LOCAL_COORDINATES);

            handleParticleInitialState(nifNode, partsys, partctrl);

            partsys->setQuota(partctrl->numParticles);

            partsys->getDefaultParticleTemplate().setSizeRange(osgParticle::rangef(partctrl->size, partctrl->size));
            partsys->getDefaultParticleTemplate().setColorRange(osgParticle::rangev4(osg::Vec4f(1.f,1.f,1.f,1.f), osg::Vec4f(1.f,1.f,1.f,1.f)));
            partsys->getDefaultParticleTemplate().setAlphaRange(osgParticle::rangef(1.f, 1.f));

            partsys->setFreezeOnCull(true);

            if (!partctrl->emitter.empty())
            {
                osg::ref_ptr<Emitter> emitter = handleParticleEmitter(partctrl);
                emitter->setParticleSystem(partsys);
                emitter->setReferenceFrame(osgParticle::ParticleProcessor::RELATIVE_RF);

                // The emitter node may not actually be handled yet, so let's delay attaching the emitter to a later moment.
                // If the emitter node is placed later than the particle node, it'll have a single frame delay in particle processing.
                // But that shouldn't be a game-breaking issue.
                mEmitterQueue.emplace_back(partctrl->emitter->recIndex, emitter);

                osg::ref_ptr<ParticleSystemController> callback(new ParticleSystemController(partctrl));
                setupController(partctrl, callback, animflags);
                emitter->setUpdateCallback(callback);

                if (!(animflags & Nif::NiNode::ParticleFlag_AutoPlay))
                {
                    partsys->setFrozen(true);
                }

                // Due to odd code in the ParticleSystemUpdater, particle systems will not be updated in the first frame
                // So do that update manually
                osg::NodeVisitor nv;
                partsys->update(0.0, nv);
            }

            // affectors should be attached *after* the emitter in the scene graph for correct update order
            // attach to same node as the ParticleSystem, we need osgParticle Operators to get the correct
            // localToWorldMatrix for transforming to particle space
            handleParticlePrograms(partctrl->affectors, partctrl->colliders, parentNode, partsys.get(), rf);

            std::vector<const Nif::Property*> drawableProps;
            collectDrawableProperties(nifNode, drawableProps);
            applyDrawableProperties(parentNode, drawableProps, composite, true, animflags);

            // particle system updater (after the emitters and affectors in the scene graph)
            // I think for correct culling needs to be *before* the ParticleSystem, though osg examples do it the other way
            osg::ref_ptr<osgParticle::ParticleSystemUpdater> updater = new osgParticle::ParticleSystemUpdater;
            updater->addParticleSystem(partsys);
            parentNode->addChild(updater);

            osg::Node* toAttach = partsys.get();

            if (rf == osgParticle::ParticleProcessor::RELATIVE_RF)
                parentNode->addChild(toAttach);
            else
            {
                osg::MatrixTransform* trans = new osg::MatrixTransform;
                trans->setUpdateCallback(new InverseWorldMatrix);
                trans->addChild(toAttach);
                parentNode->addChild(trans);
            }
            // create partsys stateset in order to pass in ShaderVisitor like all other Drawables
            partsys->getOrCreateStateSet();
        }

        void triCommonToGeometry(osg::Geometry *geometry, const std::vector<osg::Vec3f>& vertices, const std::vector<osg::Vec3f>& normals, const std::vector<std::vector<osg::Vec2f>>& uvlist, const std::vector<osg::Vec4f>& colors, const std::vector<unsigned int>& boundTextures, const std::string& name)
        {
            if (!vertices.empty())
                geometry->setVertexArray(new osg::Vec3Array(vertices.size(), vertices.data()));
            if (!normals.empty())
                geometry->setNormalArray(new osg::Vec3Array(normals.size(), normals.data()), osg::Array::BIND_PER_VERTEX);
            if (!colors.empty())
                geometry->setColorArray(new osg::Vec4Array(colors.size(), colors.data()), osg::Array::BIND_PER_VERTEX);

            int textureStage = 0;
            for (const unsigned int uvSet : boundTextures)
            {
                if (uvSet >= uvlist.size())
                {
                    Log(Debug::Verbose) << "Out of bounds UV set " << uvSet << " on shape \"" << name << "\" in " << mFilename;
                    if (!uvlist.empty())
                        geometry->setTexCoordArray(textureStage, new osg::Vec2Array(uvlist[0].size(), uvlist[0].data()), osg::Array::BIND_PER_VERTEX);
                    continue;
                }

                geometry->setTexCoordArray(textureStage, new osg::Vec2Array(uvlist[uvSet].size(), uvlist[uvSet].data()), osg::Array::BIND_PER_VERTEX);
                textureStage++;
            }
        }

        void triShapeToGeometry(const Nif::Node *nifNode, osg::Geometry *geometry, osg::Node* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector<unsigned int>& boundTextures, int animflags)
        {
            bool vertexColorsPresent = false;
            if (nifNode->recType == Nif::RC_NiTriShape)
            {
                const Nif::NiTriShape* triShape = static_cast<const Nif::NiTriShape*>(nifNode);
                if (!triShape->data.empty())
                {
                    const Nif::NiTriShapeData* data = triShape->data.getPtr();
                    vertexColorsPresent = !data->colors.empty();
                    triCommonToGeometry(geometry, data->vertices, data->normals, data->uvlist, data->colors, boundTextures, triShape->name);
                    if (!data->triangles.empty())
                        geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLES, data->triangles.size(),
                                                                                (unsigned short*)data->triangles.data()));
                }
            }
            else
            {
                const Nif::NiTriStrips* triStrips = static_cast<const Nif::NiTriStrips*>(nifNode);
                if (!triStrips->data.empty())
                {
                    const Nif::NiTriStripsData* data = triStrips->data.getPtr();
                    vertexColorsPresent = !data->colors.empty();
                    triCommonToGeometry(geometry, data->vertices, data->normals, data->uvlist, data->colors, boundTextures, triStrips->name);
                    if (!data->strips.empty())
                    {
                        for (const std::vector<unsigned short>& strip : data->strips)
                        {
                            // Can't make a triangle from less than three vertices.
                            if (strip.size() < 3)
                                continue;
                            geometry->addPrimitiveSet(new osg::DrawElementsUShort(osg::PrimitiveSet::TRIANGLE_STRIP, strip.size(), 
                                                                                (unsigned short*)strip.data()));
                        }
                    }
                }
            }

            // osg::Material properties are handled here for two reasons:
            // - if there are no vertex colors, we need to disable colorMode.
            // - there are 3 "overlapping" nif properties that all affect the osg::Material, handling them
            //   above the actual renderable would be tedious.
            std::vector<const Nif::Property*> drawableProps;
            collectDrawableProperties(nifNode, drawableProps);
            applyDrawableProperties(parentNode, drawableProps, composite, vertexColorsPresent, animflags);
        }

        void handleTriShape(const Nif::Node* nifNode, osg::Group* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector<unsigned int>& boundTextures, int animflags)
        {
            assert(nifNode->recType == Nif::RC_NiTriShape || nifNode->recType == Nif::RC_NiTriStrips);
            osg::ref_ptr<osg::Drawable> drawable;
            osg::ref_ptr<osg::Geometry> geom (new osg::Geometry);
            triShapeToGeometry(nifNode, geom, parentNode, composite, boundTextures, animflags);
            Nif::ControllerPtr ctrl;
            if (nifNode->recType == Nif::RC_NiTriShape)
                ctrl = static_cast<const Nif::NiTriShape*>(nifNode)->controller;
            else
                ctrl = static_cast<const Nif::NiTriStrips*>(nifNode)->controller;
            for (; !ctrl.empty(); ctrl = ctrl->next)
            {
                if (!(ctrl->flags & Nif::NiNode::ControllerFlag_Active))
                    continue;
                if(ctrl->recType == Nif::RC_NiGeomMorpherController)
                {
                    const Nif::NiGeomMorpherController* nimorphctrl = static_cast<const Nif::NiGeomMorpherController*>(ctrl.getPtr());
                    if (nimorphctrl->data.empty())
                        continue;
                    drawable = handleMorphGeometry(nimorphctrl, geom, parentNode, composite, boundTextures, animflags);

                    osg::ref_ptr<GeomMorpherController> morphctrl = new GeomMorpherController(nimorphctrl->data.getPtr());
                    setupController(ctrl.getPtr(), morphctrl, animflags);
                    drawable->setUpdateCallback(morphctrl);
                    break;
                }
            }
            if (!drawable.get())
                drawable = geom;
            drawable->setName(nifNode->name);
            parentNode->addChild(drawable);
        }

        osg::ref_ptr<osg::Drawable> handleMorphGeometry(const Nif::NiGeomMorpherController* morpher, osg::ref_ptr<osg::Geometry> sourceGeometry, osg::Node* parentNode, SceneUtil::CompositeStateSetUpdater* composite, const std::vector<unsigned int>& boundTextures, int animflags)
        {
            osg::ref_ptr<SceneUtil::MorphGeometry> morphGeom = new SceneUtil::MorphGeometry;
            morphGeom->setSourceGeometry(sourceGeometry);

            const std::vector<Nif::NiMorphData::MorphData>& morphs = morpher->data.getPtr()->mMorphs;
            if (morphs.empty())
                return morphGeom;
            // Note we are not interested in morph 0, which just contains the original vertices
            for (unsigned int i = 1; i < morphs.size(); ++i)
                morphGeom->addMorphTarget(new osg::Vec3Array(morphs[i].mVertices.size(), morphs[i].mVertices.data()), 0.f);

            return morphGeom;
        }

        void handleSkinnedTriShape(const Nif::Node *nifNode, osg::Group *parentNode, SceneUtil::CompositeStateSetUpdater* composite,
                                          const std::vector<unsigned int>& boundTextures, int animflags)
        {
            assert(nifNode->recType == Nif::RC_NiTriShape || nifNode->recType == Nif::RC_NiTriStrips);
            osg::ref_ptr<osg::Geometry> geometry (new osg::Geometry);
            triShapeToGeometry(nifNode, geometry, parentNode, composite, boundTextures, animflags);
            osg::ref_ptr<SceneUtil::RigGeometry> rig(new SceneUtil::RigGeometry);
            rig->setSourceGeometry(geometry);
            rig->setName(nifNode->name);

            // Assign bone weights
            osg::ref_ptr<SceneUtil::RigGeometry::InfluenceMap> map (new SceneUtil::RigGeometry::InfluenceMap);

            const Nif::NiSkinInstance *skin = static_cast<const Nif::NiGeometry*>(nifNode)->skin.getPtr();
            const Nif::NiSkinData *data = skin->data.getPtr();
            const Nif::NodeList &bones = skin->bones;
            for(size_t i = 0;i < bones.length();i++)
            {
                std::string boneName = Misc::StringUtils::lowerCase(bones[i].getPtr()->name);

                SceneUtil::RigGeometry::BoneInfluence influence;
                const std::vector<Nif::NiSkinData::VertWeight> &weights = data->bones[i].weights;
                for(size_t j = 0;j < weights.size();j++)
                {
                    influence.mWeights.emplace_back(weights[j].vertex, weights[j].weight);
                }
                influence.mInvBindMatrix = data->bones[i].trafo.toMatrix();
                influence.mBoundSphere = osg::BoundingSpheref(data->bones[i].boundSphereCenter, data->bones[i].boundSphereRadius);

                map->mData.emplace_back(boneName, influence);
            }
            rig->setInfluenceMap(map);

            parentNode->addChild(rig);
        }

        osg::BlendFunc::BlendFuncMode getBlendMode(int mode)
        {
            switch(mode)
            {
            case 0: return osg::BlendFunc::ONE;
            case 1: return osg::BlendFunc::ZERO;
            case 2: return osg::BlendFunc::SRC_COLOR;
            case 3: return osg::BlendFunc::ONE_MINUS_SRC_COLOR;
            case 4: return osg::BlendFunc::DST_COLOR;
            case 5: return osg::BlendFunc::ONE_MINUS_DST_COLOR;
            case 6: return osg::BlendFunc::SRC_ALPHA;
            case 7: return osg::BlendFunc::ONE_MINUS_SRC_ALPHA;
            case 8: return osg::BlendFunc::DST_ALPHA;
            case 9: return osg::BlendFunc::ONE_MINUS_DST_ALPHA;
            case 10: return osg::BlendFunc::SRC_ALPHA_SATURATE;
            default:
                Log(Debug::Info) << "Unexpected blend mode: "<< mode << " in " << mFilename;
                return osg::BlendFunc::SRC_ALPHA;
            }
        }

        osg::AlphaFunc::ComparisonFunction getTestMode(int mode)
        {
            switch (mode)
            {
            case 0: return osg::AlphaFunc::ALWAYS;
            case 1: return osg::AlphaFunc::LESS;
            case 2: return osg::AlphaFunc::EQUAL;
            case 3: return osg::AlphaFunc::LEQUAL;
            case 4: return osg::AlphaFunc::GREATER;
            case 5: return osg::AlphaFunc::NOTEQUAL;
            case 6: return osg::AlphaFunc::GEQUAL;
            case 7: return osg::AlphaFunc::NEVER;
            default:
                Log(Debug::Info) << "Unexpected blend mode: " << mode << " in " << mFilename;
                return osg::AlphaFunc::LEQUAL;
            }
        }

        osg::Stencil::Function getStencilFunction(int func)
        {
            switch (func)
            {
            case 0: return osg::Stencil::NEVER;
            case 1: return osg::Stencil::LESS;
            case 2: return osg::Stencil::EQUAL;
            case 3: return osg::Stencil::LEQUAL;
            case 4: return osg::Stencil::GREATER;
            case 5: return osg::Stencil::NOTEQUAL;
            case 6: return osg::Stencil::GEQUAL;
            case 7: return osg::Stencil::NEVER; // NifSkope says this is GL_ALWAYS, but in MW it's GL_NEVER
            default:
                Log(Debug::Info) << "Unexpected stencil function: " << func << " in " << mFilename;
                return osg::Stencil::NEVER;
            }
        }

        osg::Stencil::Operation getStencilOperation(int op)
        {
            switch (op)
            {
            case 0: return osg::Stencil::KEEP;
            case 1: return osg::Stencil::ZERO;
            case 2: return osg::Stencil::REPLACE;
            case 3: return osg::Stencil::INCR;
            case 4: return osg::Stencil::DECR;
            case 5: return osg::Stencil::INVERT;
            default:
                Log(Debug::Info) << "Unexpected stencil operation: " << op << " in " << mFilename;
                return osg::Stencil::KEEP;
            }
        }

        osg::ref_ptr<osg::Image> handleInternalTexture(const Nif::NiPixelData* pixelData)
        {
            osg::ref_ptr<osg::Image> image (new osg::Image);

            GLenum pixelformat = 0;
            switch (pixelData->fmt)
            {
            case Nif::NiPixelData::NIPXFMT_RGB8:
            case Nif::NiPixelData::NIPXFMT_PAL8:
                pixelformat = GL_RGB;
                break;
            case Nif::NiPixelData::NIPXFMT_RGBA8:
            case Nif::NiPixelData::NIPXFMT_PALA8:
                pixelformat = GL_RGBA;
                break;
            default:
                Log(Debug::Info) << "Unhandled internal pixel format " << pixelData->fmt << " in " << mFilename;
                return nullptr;
            }

            if (pixelData->mipmaps.empty())
                return nullptr;

            int width = 0;
            int height = 0;

            std::vector<unsigned int> mipmapVector;
            for (unsigned int i=0; i<pixelData->mipmaps.size(); ++i)
            {
                const Nif::NiPixelData::Mipmap& mip = pixelData->mipmaps[i];

                size_t mipSize = mip.height * mip.width * pixelData->bpp / 8;
                if (mipSize + mip.dataOffset > pixelData->data.size())
                {
                    Log(Debug::Info) << "Internal texture's mipmap data out of bounds, ignoring texture";
                    return nullptr;
                }

                if (i != 0)
                    mipmapVector.push_back(mip.dataOffset);
                else
                {
                    width = mip.width;
                    height = mip.height;
                }
            }

            if (width <= 0 || height <= 0)
            {
                Log(Debug::Info) << "Internal Texture Width and height must be non zero, ignoring texture";
                return nullptr;
            }

            const std::vector<unsigned char>& pixels = pixelData->data;
            switch (pixelData->fmt)
            {
            case Nif::NiPixelData::NIPXFMT_RGB8:
            case Nif::NiPixelData::NIPXFMT_RGBA8:
            {
                unsigned char* data = new unsigned char[pixels.size()];
                memcpy(data, pixels.data(), pixels.size());
                image->setImage(width, height, 1, pixelformat, pixelformat, GL_UNSIGNED_BYTE, data, osg::Image::USE_NEW_DELETE);
                break;
            }
            case Nif::NiPixelData::NIPXFMT_PAL8:
            case Nif::NiPixelData::NIPXFMT_PALA8:
            {
                if (pixelData->palette.empty() || pixelData->bpp != 8)
                {
                    Log(Debug::Info) << "Palettized texture in " << mFilename << " is invalid, ignoring";
                    return nullptr;
                }
                // We're going to convert the indices that pixel data contains
                // into real colors using the palette.
                const std::vector<unsigned int>& palette = pixelData->palette->colors;
                if (pixelData->fmt == Nif::NiPixelData::NIPXFMT_PAL8)
                {
                    unsigned char* data = new unsigned char[pixels.size() * 3];
                    for (size_t i = 0; i < pixels.size(); i++)
                    {
                        unsigned int color = palette[pixels[i]];
                        data[i * 3 + 0] = (color >>  0) & 0xFF;
                        data[i * 3 + 1] = (color >>  8) & 0xFF;
                        data[i * 3 + 2] = (color >> 16) & 0xFF;
                    }
                    image->setImage(width, height, 1, pixelformat, pixelformat, GL_UNSIGNED_BYTE, data, osg::Image::USE_NEW_DELETE);
                }
                else // if (fmt = NIPXFMT_PALA8)
                {
                    unsigned char* data = new unsigned char[pixels.size() * 4];
                    for (size_t i = 0; i < pixels.size(); i++)
                    {
                        unsigned int color = palette[pixels[i]];
                        data[i * 4 + 0] = (color >>  0) & 0xFF;
                        data[i * 4 + 1] = (color >>  8) & 0xFF;
                        data[i * 4 + 2] = (color >> 16) & 0xFF;
                        data[i * 4 + 3] = (color >> 24) & 0xFF;
                    }
                    image->setImage(width, height, 1, pixelformat, pixelformat, GL_UNSIGNED_BYTE, data, osg::Image::USE_NEW_DELETE);
                }
                break;
            }
            default:
                return nullptr;
            }

            image->setMipmapLevels(mipmapVector);
            image->flipVertical();

            return image;
        }

        void handleTextureProperty(const Nif::NiTexturingProperty* texprop, const std::string& nodeName, osg::StateSet* stateset, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector<unsigned int>& boundTextures, int animflags)
        {
            if (!boundTextures.empty())
            {
                // overriding a parent NiTexturingProperty, so remove what was previously bound
                for (unsigned int i=0; i<boundTextures.size(); ++i)
                    stateset->setTextureMode(i, GL_TEXTURE_2D, osg::StateAttribute::OFF);
                boundTextures.clear();
            }

            // If this loop is changed such that the base texture isn't guaranteed to end up in texture unit 0, the shadow casting shader will need to be updated accordingly.
            for (size_t i=0; i<texprop->textures.size(); ++i)
            {
                if (texprop->textures[i].inUse)
                {
                    switch(i)
                    {
                        //These are handled later on
                        case Nif::NiTexturingProperty::BaseTexture:
                        case Nif::NiTexturingProperty::GlowTexture:
                        case Nif::NiTexturingProperty::DarkTexture:
                        case Nif::NiTexturingProperty::BumpTexture:
                        case Nif::NiTexturingProperty::DetailTexture:
                        case Nif::NiTexturingProperty::DecalTexture:
                            break;
                        case Nif::NiTexturingProperty::GlossTexture:
                        {
                            // Not used by the vanilla engine. MCP (Morrowind Code Patch) adds an option to use Gloss maps:
                            // "- Gloss map fix. Morrowind removed gloss map entries from model files after loading them. This stops Morrowind from removing them."
                            // Log(Debug::Info) << "NiTexturingProperty::GlossTexture in " << mFilename << " not currently used.";
                            continue;
                        }
                        default:
                        {
                            Log(Debug::Info) << "Unhandled texture stage " << i << " on shape \"" << nodeName << "\" in " << mFilename;
                            continue;
                        }
                    }

                    const Nif::NiTexturingProperty::Texture& tex = texprop->textures[i];
                    if(tex.texture.empty() && texprop->controller.empty())
                    {
                        if (i == 0)
                            Log(Debug::Warning) << "Base texture is in use but empty on shape \"" << nodeName << "\" in " << mFilename;
                        continue;
                    }

                    // create a new texture, will later attempt to share using the SharedStateManager
                    osg::ref_ptr<osg::Texture2D> texture2d;
                    if (!tex.texture.empty())
                    {
                        const Nif::NiSourceTexture *st = tex.texture.getPtr();
                        osg::ref_ptr<osg::Image> image = handleSourceTexture(st, imageManager);
                        texture2d = new osg::Texture2D(image);
                        if (image)
                            texture2d->setTextureSize(image->s(), image->t());
                    }
                    else
                        texture2d = new osg::Texture2D;

                    bool wrapT = tex.clamp & 0x1;
                    bool wrapS = (tex.clamp >> 1) & 0x1;

                    texture2d->setWrap(osg::Texture::WRAP_S, wrapS ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE);
                    texture2d->setWrap(osg::Texture::WRAP_T, wrapT ? osg::Texture::REPEAT : osg::Texture::CLAMP_TO_EDGE);

                    unsigned int texUnit = boundTextures.size();

                    stateset->setTextureAttributeAndModes(texUnit, texture2d, osg::StateAttribute::ON);

                    if (i == Nif::NiTexturingProperty::GlowTexture)
                    {
                        osg::TexEnvCombine* texEnv = new osg::TexEnvCombine;
                        texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE);
                        texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS);
                        texEnv->setCombine_RGB(osg::TexEnvCombine::ADD);
                        texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS);
                        texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE);

                        stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON);
                    }
                    else if (i == Nif::NiTexturingProperty::DarkTexture)
                    {
                        osg::TexEnv* texEnv = new osg::TexEnv;
                        texEnv->setMode(osg::TexEnv::MODULATE);
                        stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON);
                    }
                    else if (i == Nif::NiTexturingProperty::DetailTexture)
                    {
                        osg::TexEnvCombine* texEnv = new osg::TexEnvCombine;
                        texEnv->setScale_RGB(2.f);
                        texEnv->setCombine_Alpha(osg::TexEnvCombine::MODULATE);
                        texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA);
                        texEnv->setOperand1_Alpha(osg::TexEnvCombine::SRC_ALPHA);
                        texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS);
                        texEnv->setSource1_Alpha(osg::TexEnvCombine::TEXTURE);
                        texEnv->setCombine_RGB(osg::TexEnvCombine::MODULATE);
                        texEnv->setOperand0_RGB(osg::TexEnvCombine::SRC_COLOR);
                        texEnv->setOperand1_RGB(osg::TexEnvCombine::SRC_COLOR);
                        texEnv->setSource0_RGB(osg::TexEnvCombine::PREVIOUS);
                        texEnv->setSource1_RGB(osg::TexEnvCombine::TEXTURE);
                        stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON);
                    }
                    else if (i == Nif::NiTexturingProperty::BumpTexture)
                    {
                        // Set this texture to Off by default since we can't render it with the fixed-function pipeline
                        stateset->setTextureMode(texUnit, GL_TEXTURE_2D, osg::StateAttribute::OFF);
                        osg::Matrix2 bumpMapMatrix(texprop->bumpMapMatrix.x(), texprop->bumpMapMatrix.y(),
                                                   texprop->bumpMapMatrix.z(), texprop->bumpMapMatrix.w());
                        stateset->addUniform(new osg::Uniform("bumpMapMatrix", bumpMapMatrix));
                        stateset->addUniform(new osg::Uniform("envMapLumaBias", texprop->envMapLumaBias));
                    }
                    else if (i == Nif::NiTexturingProperty::DecalTexture)
                    {
                         osg::TexEnvCombine* texEnv = new osg::TexEnvCombine;
                         texEnv->setCombine_RGB(osg::TexEnvCombine::INTERPOLATE);
                         texEnv->setSource0_RGB(osg::TexEnvCombine::TEXTURE);
                         texEnv->setOperand0_RGB(osg::TexEnvCombine::SRC_COLOR);
                         texEnv->setSource1_RGB(osg::TexEnvCombine::PREVIOUS);
                         texEnv->setOperand1_RGB(osg::TexEnvCombine::SRC_COLOR);
                         texEnv->setSource2_RGB(osg::TexEnvCombine::TEXTURE);
                         texEnv->setOperand2_RGB(osg::TexEnvCombine::SRC_ALPHA);
                         texEnv->setCombine_Alpha(osg::TexEnvCombine::REPLACE);
                         texEnv->setSource0_Alpha(osg::TexEnvCombine::PREVIOUS);
                         texEnv->setOperand0_Alpha(osg::TexEnvCombine::SRC_ALPHA);
                         stateset->setTextureAttributeAndModes(texUnit, texEnv, osg::StateAttribute::ON);
                    }

                    switch (i)
                    {
                    case Nif::NiTexturingProperty::BaseTexture:
                        texture2d->setName("diffuseMap");
                        break;
                    case Nif::NiTexturingProperty::BumpTexture:
                        texture2d->setName("bumpMap");
                        break;
                    case Nif::NiTexturingProperty::GlowTexture:
                        texture2d->setName("emissiveMap");
                        break;
                    case Nif::NiTexturingProperty::DarkTexture:
                        texture2d->setName("darkMap");
                        break;
                    case Nif::NiTexturingProperty::DetailTexture:
                        texture2d->setName("detailMap");
                        break;
                    case Nif::NiTexturingProperty::DecalTexture:
                        texture2d->setName("decalMap");
                        break;
                    default:
                        break;
                    }

                    boundTextures.push_back(tex.uvSet);
                }
            }
            handleTextureControllers(texprop, composite, imageManager, stateset, animflags);
        }

        void handleProperty(const Nif::Property *property,
                            osg::Node *node, SceneUtil::CompositeStateSetUpdater* composite, Resource::ImageManager* imageManager, std::vector<unsigned int>& boundTextures, int animflags)
        {
            switch (property->recType)
            {
            case Nif::RC_NiStencilProperty:
            {
                const Nif::NiStencilProperty* stencilprop = static_cast<const Nif::NiStencilProperty*>(property);
                osg::ref_ptr<osg::FrontFace> frontFace = new osg::FrontFace;
                switch (stencilprop->data.drawMode)
                {
                case 2:
                    frontFace->setMode(osg::FrontFace::CLOCKWISE);
                    break;
                case 0:
                case 1:
                default:
                    frontFace->setMode(osg::FrontFace::COUNTER_CLOCKWISE);
                    break;
                }
                frontFace = shareAttribute(frontFace);

                osg::StateSet* stateset = node->getOrCreateStateSet();
                stateset->setAttribute(frontFace, osg::StateAttribute::ON);
                stateset->setMode(GL_CULL_FACE, stencilprop->data.drawMode == 3 ? osg::StateAttribute::OFF
                                                                                : osg::StateAttribute::ON);

                if (stencilprop->data.enabled != 0)
                {
                    osg::ref_ptr<osg::Stencil> stencil = new osg::Stencil;
                    stencil->setFunction(getStencilFunction(stencilprop->data.compareFunc), stencilprop->data.stencilRef, stencilprop->data.stencilMask);
                    stencil->setStencilFailOperation(getStencilOperation(stencilprop->data.failAction));
                    stencil->setStencilPassAndDepthFailOperation(getStencilOperation(stencilprop->data.zFailAction));
                    stencil->setStencilPassAndDepthPassOperation(getStencilOperation(stencilprop->data.zPassAction));
                    stencil = shareAttribute(stencil);

                    stateset->setAttributeAndModes(stencil, osg::StateAttribute::ON);
                }
                break;
            }
            case Nif::RC_NiWireframeProperty:
            {
                const Nif::NiWireframeProperty* wireprop = static_cast<const Nif::NiWireframeProperty*>(property);
                osg::ref_ptr<osg::PolygonMode> mode = new osg::PolygonMode;
                mode->setMode(osg::PolygonMode::FRONT_AND_BACK, wireprop->flags == 0 ? osg::PolygonMode::FILL
                                                                                     : osg::PolygonMode::LINE);
                mode = shareAttribute(mode);
                node->getOrCreateStateSet()->setAttributeAndModes(mode, osg::StateAttribute::ON);
                break;
            }
            case Nif::RC_NiZBufferProperty:
            {
                const Nif::NiZBufferProperty* zprop = static_cast<const Nif::NiZBufferProperty*>(property);
                // VER_MW doesn't support a DepthFunction according to NifSkope
                osg::ref_ptr<osg::Depth> depth = new osg::Depth;
                depth->setWriteMask((zprop->flags>>1)&1);
                depth = shareAttribute(depth);
                node->getOrCreateStateSet()->setAttributeAndModes(depth, osg::StateAttribute::ON);
                break;
            }
            // OSG groups the material properties that NIFs have separate, so we have to parse them all again when one changed
            case Nif::RC_NiMaterialProperty:
            case Nif::RC_NiVertexColorProperty:
            case Nif::RC_NiSpecularProperty:
            {
                // Handled on drawable level so we know whether vertex colors are available
                break;
            }
            case Nif::RC_NiAlphaProperty:
            {
                // Handled on drawable level to prevent RenderBin nesting issues
                break;
            }
            case Nif::RC_NiTexturingProperty:
            {
                const Nif::NiTexturingProperty* texprop = static_cast<const Nif::NiTexturingProperty*>(property);
                osg::StateSet* stateset = node->getOrCreateStateSet();
                handleTextureProperty(texprop, node->getName(), stateset, composite, imageManager, boundTextures, animflags);
                break;
            }
            // unused by mw
            case Nif::RC_NiShadeProperty:
            case Nif::RC_NiDitherProperty:
            case Nif::RC_NiFogProperty:
            {
                break;
            }
            default:
                Log(Debug::Info) << "Unhandled " << property->recName << " in " << mFilename;
                break;
            }
        }

        struct CompareStateAttribute
        {
            bool operator() (const osg::ref_ptr<osg::StateAttribute>& left, const osg::ref_ptr<osg::StateAttribute>& right) const
            {
                return left->compare(*right) < 0;
            }
        };

        // global sharing of State Attributes will reduce the number of GL calls as the osg::State will check by pointer to see if state is the same
        template <class Attribute>
        Attribute* shareAttribute(const osg::ref_ptr<Attribute>& attr)
        {
            typedef std::set<osg::ref_ptr<Attribute>, CompareStateAttribute> Cache;
            static Cache sCache;
            static OpenThreads::Mutex sMutex;
            OpenThreads::ScopedLock<OpenThreads::Mutex> lock(sMutex);
            typename Cache::iterator found = sCache.find(attr);
            if (found == sCache.end())
                found = sCache.insert(attr).first;
            return *found;
        }

        void applyDrawableProperties(osg::Node* node, const std::vector<const Nif::Property*>& properties, SceneUtil::CompositeStateSetUpdater* composite,
                                             bool hasVertexColors, int animflags)
        {
            osg::StateSet* stateset = node->getOrCreateStateSet();

            // Specular lighting is enabled by default, but there's a quirk...
            int specFlags = 1;
            osg::ref_ptr<osg::Material> mat (new osg::Material);
            mat->setColorMode(hasVertexColors ? osg::Material::AMBIENT_AND_DIFFUSE : osg::Material::OFF);

            // NIF material defaults don't match OpenGL defaults
            mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1));
            mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1));

            bool hasMatCtrl = false;

            int lightmode = 1;

            for (const Nif::Property* property : properties)
            {
                switch (property->recType)
                {
                case Nif::RC_NiSpecularProperty:
                {
                    // Specular property can turn specular lighting off.
                    specFlags = property->flags;
                    break;
                }
                case Nif::RC_NiMaterialProperty:
                {
                    const Nif::NiMaterialProperty* matprop = static_cast<const Nif::NiMaterialProperty*>(property);

                    mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(matprop->data.diffuse, matprop->data.alpha));
                    mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(matprop->data.ambient, 1.f));
                    mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(matprop->data.emissive, 1.f));

                    mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(matprop->data.specular, 1.f));
                    mat->setShininess(osg::Material::FRONT_AND_BACK, matprop->data.glossiness);

                    if (!matprop->controller.empty())
                    {
                        hasMatCtrl = true;
                        handleMaterialControllers(matprop, composite, animflags);
                    }

                    break;
                }
                case Nif::RC_NiVertexColorProperty:
                {
                    const Nif::NiVertexColorProperty* vertprop = static_cast<const Nif::NiVertexColorProperty*>(property);
                    lightmode = vertprop->data.lightmode;

                    switch (vertprop->data.vertmode)
                    {
                        case 0:
                            mat->setColorMode(osg::Material::OFF);
                            break;
                        case 1:
                            mat->setColorMode(osg::Material::EMISSION);
                            break;
                        case 2:
                            if (lightmode != 0)
                                mat->setColorMode(osg::Material::AMBIENT_AND_DIFFUSE);
                            else
                                mat->setColorMode(osg::Material::OFF);
                            break;
                    }
                    break;
                }
                case Nif::RC_NiAlphaProperty:
                {
                    const Nif::NiAlphaProperty* alphaprop = static_cast<const Nif::NiAlphaProperty*>(property);
                    if (alphaprop->flags&1)
                    {
                        osg::ref_ptr<osg::BlendFunc> blendFunc (new osg::BlendFunc(getBlendMode((alphaprop->flags>>1)&0xf),
                                                                                   getBlendMode((alphaprop->flags>>5)&0xf)));
                        blendFunc = shareAttribute(blendFunc);
                        stateset->setAttributeAndModes(blendFunc, osg::StateAttribute::ON);

                        bool noSort = (alphaprop->flags>>13)&1;
                        if (!noSort)
                            stateset->setRenderingHint(osg::StateSet::TRANSPARENT_BIN);
                        else
                            stateset->setRenderBinToInherit();
                    }
                    else
                    {
                        stateset->removeAttribute(osg::StateAttribute::BLENDFUNC);
                        stateset->removeMode(GL_BLEND);
                        stateset->setRenderBinToInherit();
                    }

                    if((alphaprop->flags>>9)&1)
                    {
                        osg::ref_ptr<osg::AlphaFunc> alphaFunc (new osg::AlphaFunc(getTestMode((alphaprop->flags>>10)&0x7), alphaprop->data.threshold/255.f));
                        alphaFunc = shareAttribute(alphaFunc);
                        stateset->setAttributeAndModes(alphaFunc, osg::StateAttribute::ON);
                    }
                    else
                    {
                        stateset->removeAttribute(osg::StateAttribute::ALPHAFUNC);
                        stateset->removeMode(GL_ALPHA_TEST);
                    }
                    break;
                }
                }
            }

            // While NetImmerse and Gamebryo support specular lighting, Morrowind has its support disabled.
            if (mVersion <= Nif::NIFFile::NIFVersion::VER_MW || specFlags == 0)
                mat->setSpecular(osg::Material::FRONT_AND_BACK, osg::Vec4f(0.f,0.f,0.f,0.f));

            if (lightmode == 0)
            {
                osg::Vec4f diffuse = mat->getDiffuse(osg::Material::FRONT_AND_BACK);
                diffuse = osg::Vec4f(0,0,0,diffuse.a());
                mat->setDiffuse(osg::Material::FRONT_AND_BACK, diffuse);
                mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f());
            }

            // If we're told to use vertex colors but there are none to use, use a default color instead.
            if (!hasVertexColors)
            {
                switch (mat->getColorMode())
                {
                case osg::Material::AMBIENT:
                    mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1));
                    break;
                case osg::Material::AMBIENT_AND_DIFFUSE:
                    mat->setAmbient(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1));
                    mat->setDiffuse(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1));
                    break;
                case osg::Material::EMISSION:
                    mat->setEmission(osg::Material::FRONT_AND_BACK, osg::Vec4f(1,1,1,1));
                    break;
                default:
                    break;
                }
                mat->setColorMode(osg::Material::OFF);
            }

            if (!hasMatCtrl && mat->getColorMode() == osg::Material::OFF
                    && mat->getEmission(osg::Material::FRONT_AND_BACK) == osg::Vec4f(0,0,0,1)
                    && mat->getDiffuse(osg::Material::FRONT_AND_BACK) == osg::Vec4f(1,1,1,1)
                    && mat->getAmbient(osg::Material::FRONT_AND_BACK) == osg::Vec4f(1,1,1,1)
                    && mat->getShininess(osg::Material::FRONT_AND_BACK) == 0
                    && mat->getSpecular(osg::Material::FRONT_AND_BACK) == osg::Vec4f(0.f, 0.f, 0.f, 0.f))
            {
                // default state, skip
                return;
            }

            mat = shareAttribute(mat);

            stateset->setAttributeAndModes(mat, osg::StateAttribute::ON);
        }

    };

    osg::ref_ptr<osg::Node> Loader::load(Nif::NIFFilePtr file, Resource::ImageManager* imageManager)
    {
        LoaderImpl impl(file->getFilename(), file->getVersion(), file->getUserVersion(), file->getBethVersion());
        return impl.load(file, imageManager);
    }

    void Loader::loadKf(Nif::NIFFilePtr kf, KeyframeHolder& target)
    {
        LoaderImpl impl(kf->getFilename(), kf->getVersion(), kf->getUserVersion(), kf->getBethVersion());
        impl.loadKf(kf, target);
    }

}