#include "CScriptNode.h"
#include "CScene.h"
#include "Core/GameProject/CResourceStore.h"
#include "Core/Render/CDrawUtil.h"
#include "Core/Render/CGraphics.h"
#include "Core/Render/CRenderer.h"
#include "Core/Resource/Animation/CAnimSet.h"
#include "Core/Resource/Script/CGameTemplate.h"
#include "Core/Resource/Script/CScriptLayer.h"
#include "Core/ScriptExtra/CScriptExtra.h"
#include <Common/Macros.h>
#include <Common/Math/MathUtil.h>

CScriptNode::CScriptNode(CScene *pScene, uint32 NodeID, CSceneNode *pParent, CScriptObject *pInstance)
    : CSceneNode(pScene, NodeID, pParent)
    , mGameModeVisibility(EGameModeVisibility::Untested)
    , mpVolumePreviewNode(nullptr)
    , mHasVolumePreview(false)
    , mpInstance(pInstance)
    , mpExtra(nullptr)
{
    ASSERT(pInstance);

    // Evaluate instance
    SetDisplayAsset(nullptr);
    mpCollisionNode = new CCollisionNode(pScene, -1, this);
    mpCollisionNode->SetInheritance(true, true, false);

    if (mpInstance)
    {
        CScriptTemplate *pTemp = Template();

        // Determine transform
        mHasValidPosition = pTemp->PositionProperty() != nullptr;
        mPosition = mpInstance->Position();
        mRotation = CQuaternion::FromEuler(mpInstance->Rotation());
        mScale = mpInstance->Scale();
        MarkTransformChanged();

        SetName("[" + pTemp->Name() + "] " + mpInstance->InstanceName());

        // Determine display assets
        SetDisplayAsset(mpInstance->DisplayAsset());
        mpCollisionNode->SetCollision(mpInstance->Collision());

        // Create preview volume node
        mpVolumePreviewNode = new CModelNode(pScene, -1, this, nullptr);

        if (pTemp->ScaleType() == CScriptTemplate::EScaleType::ScaleVolume)
        {
            UpdatePreviewVolume();

            if (mHasVolumePreview)
            {
                mpVolumePreviewNode->SetInheritance(true, (mpInstance->VolumeShape() != EVolumeShape::AxisAlignedBoxShape), true);
                mpVolumePreviewNode->ForceAlphaEnabled(true);
            }
        }

        // Create attachment nodes
        for (uint32 iAttach = 0; iAttach < pTemp->NumAttachments(); iAttach++)
        {
            const SAttachment& rkAttach = pTemp->Attachment(iAttach);
            CScriptAttachNode *pAttach = new CScriptAttachNode(pScene, rkAttach, this);
            mAttachments.push_back(pAttach);
        }

        // Fetch LightParameters
        mpLightParameters = std::make_unique<CLightParameters>(mpInstance->LightParameters(), mpInstance->GameTemplate()->Game());
        SetLightLayerIndex(mpLightParameters->LightLayerIndex());
    }

    else
    {
        // Shouldn't ever happen
        SetName("ScriptNode - NO INSTANCE");
    }

    mpExtra = CScriptExtra::CreateExtra(this);
}

ENodeType CScriptNode::NodeType()
{
    return ENodeType::Script;
}

void CScriptNode::PostLoad()
{
    CModel *pModel = ActiveModel();

    if (pModel)
    {
        pModel->BufferGL();
        pModel->GenerateMaterialShaders();
    }
}

void CScriptNode::OnTransformed()
{
    if (mpInstance)
    {
        if (LocalPosition() != mpInstance->Position())
            mpInstance->SetPosition(LocalPosition());

        if (LocalRotation().ToEuler() != mpInstance->Rotation())
            mpInstance->SetRotation(LocalRotation().ToEuler());

        if (LocalScale() != mpInstance->Scale())
            mpInstance->SetScale(LocalScale());
    }

    if (mpExtra)
        mpExtra->OnTransformed();
}

void CScriptNode::AddToRenderer(CRenderer *pRenderer, const SViewInfo& rkViewInfo)
{
    if (!mpInstance) return;

    // Add script extra to renderer first
    if (mpExtra) mpExtra->AddToRenderer(pRenderer, rkViewInfo);

    // If we're in game mode, then override other visibility settings.
    if (rkViewInfo.GameMode)
    {
        if (mGameModeVisibility == EGameModeVisibility::Untested)
            TestGameModeVisibility();

        if (mGameModeVisibility != EGameModeVisibility::Visible)
            return;
    }

    // Check whether the script extra wants us to render before we render.
    bool ShouldDraw = (!mpExtra || mpExtra->ShouldDrawNormalAssets());

    if (ShouldDraw)
    {
        // Otherwise, we proceed as normal
        if ((rkViewInfo.ShowFlags & EShowFlag::ObjectCollision) && (!rkViewInfo.GameMode))
            mpCollisionNode->AddToRenderer(pRenderer, rkViewInfo);

        if (rkViewInfo.ShowFlags & EShowFlag::ObjectGeometry || rkViewInfo.GameMode)
        {
            for (uint32 iAttach = 0; iAttach < mAttachments.size(); iAttach++)
                mAttachments[iAttach]->AddToRenderer(pRenderer, rkViewInfo);

            if (rkViewInfo.ViewFrustum.BoxInFrustum(AABox()))
            {
                CModel *pModel = ActiveModel();

                if (!pModel)
                    pRenderer->AddMesh(this, -1, AABox(), false, ERenderCommand::DrawMesh);
                else
                    AddModelToRenderer(pRenderer, pModel, 0);
            }
        }
    }

    if (IsSelected() && !rkViewInfo.GameMode)
    {
        // Script nodes always draw their selections regardless of frustum planes
        // in order to ensure that script connection lines don't get improperly culled.
       if (ShouldDraw)
           pRenderer->AddMesh(this, -1, AABox(), false, ERenderCommand::DrawSelection);

        if (mHasVolumePreview && (!mpExtra || mpExtra->ShouldDrawVolume()))
            mpVolumePreviewNode->AddToRenderer(pRenderer, rkViewInfo);
    }
}

void CScriptNode::Draw(FRenderOptions Options, int /*ComponentIndex*/, ERenderCommand Command, const SViewInfo& rkViewInfo)
{
    if (!mpInstance) return;

    // Draw model
    if (UsesModel())
    {
        EWorldLightingOptions LightingOptions = (mpLightParameters ? mpLightParameters->WorldLightingOptions() : eNormalLighting);

        if (CGraphics::sLightMode == CGraphics::ELightingMode::World && LightingOptions == eDisableWorldLighting)
        {
            CGraphics::sNumLights = 0;
            CGraphics::sVertexBlock.COLOR0_Amb = CColor::skTransparentBlack;
            CGraphics::sVertexBlock.COLOR0_Mat = CColor::skTransparentWhite;
            CGraphics::sPixelBlock.LightmapMultiplier = 1.f;
            CGraphics::UpdateLightBlock();
        }

        else
        {
            // DKCR doesn't support world lighting yet, so light nodes that don't have ingame models with default lighting
            if (Template()->Game() == EGame::DKCReturns && !mpInstance->HasInGameModel() && CGraphics::sLightMode == CGraphics::ELightingMode::World)
            {
                CGraphics::SetDefaultLighting();
                CGraphics::sVertexBlock.COLOR0_Amb = CGraphics::skDefaultAmbientColor;
                CGraphics::sVertexBlock.COLOR0_Mat = CColor::skTransparentWhite;
            }

            else
                LoadLights(rkViewInfo);
        }

        LoadModelMatrix();

        // Draw model if possible!
        CModel *pModel = ActiveModel();

        if (pModel)
        {
            if (pModel->IsSkinned()) CGraphics::LoadIdentityBoneTransforms();

            if (mpExtra) CGraphics::sPixelBlock.SetAllTevColors(mpExtra->TevColor());
            else CGraphics::sPixelBlock.SetAllTevColors(CColor::skWhite);

            CGraphics::sPixelBlock.TintColor = TintColor(rkViewInfo);
            CGraphics::UpdatePixelBlock();
            DrawModelParts(pModel, Options, 0, Command);
        }

        // If no model or billboard, default to drawing a purple box
        else
        {
            glBlendFuncSeparate(GL_ONE, GL_ZERO, GL_ZERO, GL_ZERO);
            glDepthMask(GL_TRUE);
            CGraphics::UpdateVertexBlock();
            CGraphics::UpdatePixelBlock();
            CDrawUtil::DrawShadedCube(CColor::skTransparentPurple * TintColor(rkViewInfo));
        }
    }

    // Draw billboard
    else if (mpDisplayAsset->Type() == EResourceType::Texture)
    {
        CDrawUtil::DrawBillboard(ActiveBillboard(), mPosition, BillboardScale(), TintColor(rkViewInfo));
    }
}

void CScriptNode::DrawSelection()
{
    glBlendFunc(GL_ONE, GL_ZERO);
    LoadModelMatrix();

    // Draw wireframe for models
    if (UsesModel())
    {
        CModel *pModel = ActiveModel();
        if (!pModel) pModel = CDrawUtil::GetCubeModel();
        pModel->DrawWireframe(ERenderOption::None, WireframeColor());
    }

    // Draw rotation arrow for billboards
    else
    {
        // Create model matrix that doesn't take scaling into account, and draw
        CTransform4f Transform;
        Transform.Rotate(AbsoluteRotation());
        Transform.Translate(AbsolutePosition());
        CGraphics::sMVPBlock.ModelMatrix = Transform;
        CGraphics::UpdateMVPBlock();

        CGraphics::sPixelBlock.TintColor = CColor::skWhite;
        CGraphics::UpdatePixelBlock();

        DrawRotationArrow();
    }

    if (mpInstance)
    {
        CGraphics::sMVPBlock.ModelMatrix = CMatrix4f::skIdentity;
        CGraphics::UpdateMVPBlock();

        for (uint32 iIn = 0; iIn < mpInstance->NumLinks(ELinkType::Incoming); iIn++)
        {
            // Don't draw in links if the other object is selected.
            CLink *pLink = mpInstance->Link(ELinkType::Incoming, iIn);
            CScriptNode *pLinkNode = mpScene->NodeForInstanceID(pLink->SenderID());
            if (pLinkNode && !pLinkNode->IsSelected()) CDrawUtil::DrawLine(CenterPoint(), pLinkNode->CenterPoint(), CColor::skTransparentRed);
        }

        for (uint32 iOut = 0; iOut < mpInstance->NumLinks(ELinkType::Outgoing); iOut++)
        {
            CLink *pLink = mpInstance->Link(ELinkType::Outgoing, iOut);
            CScriptNode *pLinkNode = mpScene->NodeForInstanceID(pLink->ReceiverID());
            if (pLinkNode) CDrawUtil::DrawLine(CenterPoint(), pLinkNode->CenterPoint(), CColor::skTransparentGreen);
        }
    }
}

void CScriptNode::RayAABoxIntersectTest(CRayCollisionTester& rTester, const SViewInfo& rkViewInfo)
{
    if (!mpInstance)
        return;

    // Let script extra do ray check first
    if (mpExtra)
    {
        mpExtra->RayAABoxIntersectTest(rTester, rkViewInfo);

        // If the extra doesn't want us rendering, then don't do the ray test either
        if (!mpExtra->ShouldDrawNormalAssets())
            return;
    }

    // If we're in game mode, then check whether we're visible before proceeding with the ray test.
    if (rkViewInfo.GameMode)
    {
        if (mGameModeVisibility == EGameModeVisibility::Untested)
            TestGameModeVisibility();

        if (mGameModeVisibility != EGameModeVisibility::Visible)
            return;
    }

    // Otherwise, proceed with the ray test as normal...
    const CRay& rkRay = rTester.Ray();

    if (UsesModel())
    {
        std::pair<bool,float> BoxResult = AABox().IntersectsRay(rkRay);

        if (BoxResult.first)
        {
            CModel *pModel = ActiveModel();
            if (pModel) rTester.AddNodeModel(this, pModel);
            else rTester.AddNode(this, 0, BoxResult.second);
        }
    }

    else
    {
        // Because the billboard rotates a lot, expand the AABox on the X/Y axes to cover any possible orientation
        CVector2f BillScale = BillboardScale();
        float ScaleXY = (BillScale.X > BillScale.Y ? BillScale.X : BillScale.Y);

        CAABox BillBox = CAABox(mPosition + CVector3f(-ScaleXY, -ScaleXY, -BillScale.Y),
                                mPosition + CVector3f( ScaleXY,  ScaleXY,  BillScale.Y));

        std::pair<bool,float> BoxResult = BillBox.IntersectsRay(rkRay);
        if (BoxResult.first) rTester.AddNode(this, 0, BoxResult.second);
    }

    // Run ray check on child nodes as well
    mpCollisionNode->RayAABoxIntersectTest(rTester, rkViewInfo);

    for (uint32 iAttach = 0; iAttach < mAttachments.size(); iAttach++)
        mAttachments[iAttach]->RayAABoxIntersectTest(rTester, rkViewInfo);
}

SRayIntersection CScriptNode::RayNodeIntersectTest(const CRay& rkRay, uint32 AssetID, const SViewInfo& rkViewInfo)
{
    FRenderOptions Options = rkViewInfo.pRenderer->RenderOptions();

    SRayIntersection Out;
    Out.pNode = this;
    Out.ComponentIndex = AssetID;

    // Model test
    if (UsesModel())
    {
        CModel *pModel = ActiveModel();
        if (!pModel) pModel = CDrawUtil::GetCubeModel();

        CRay TransformedRay = rkRay.Transformed(Transform().Inverse());
        std::pair<bool,float> Result = pModel->GetSurface(AssetID)->IntersectsRay(TransformedRay, ((Options & ERenderOption::EnableBackfaceCull) == 0));

        if (Result.first)
        {
            Out.Hit = true;

            CVector3f HitPoint = TransformedRay.PointOnRay(Result.second);
            CVector3f WorldHitPoint = Transform() * HitPoint;
            Out.Distance = Math::Distance(rkRay.Origin(), WorldHitPoint);
        }

        else
            Out.Hit = false;
    }

    // Billboard test
    // todo: come up with a better way to share this code between CScriptNode and CLightNode
    else
    {
        // Step 1: check whether the ray intersects with the plane the billboard is on
        CPlane BillboardPlane(-rkViewInfo.pCamera->Direction(), mPosition);
        std::pair<bool,float> PlaneTest = Math::RayPlaneIntersection(rkRay, BillboardPlane);

        if (PlaneTest.first)
        {
            // Step 2: transform the hit point into the plane's local space
            CVector3f PlaneHitPoint = rkRay.PointOnRay(PlaneTest.second);
            CVector3f RelHitPoint = PlaneHitPoint - mPosition;

            CVector3f PlaneForward = -rkViewInfo.pCamera->Direction();
            CVector3f PlaneRight = -rkViewInfo.pCamera->RightVector();
            CVector3f PlaneUp = rkViewInfo.pCamera->UpVector();
            CQuaternion PlaneRot = CQuaternion::FromAxes(PlaneRight, PlaneForward, PlaneUp);

            CVector3f RotatedHitPoint = PlaneRot.Inverse() * RelHitPoint;
            CVector2f LocalHitPoint = RotatedHitPoint.XZ() / BillboardScale();

            // Step 3: check whether the transformed hit point is in the -1 to 1 range
            if ((LocalHitPoint.X >= -1.f) && (LocalHitPoint.X <= 1.f) && (LocalHitPoint.Y >= -1.f) && (LocalHitPoint.Y <= 1.f))
            {
                // Step 4: look up the hit texel and check whether it's transparent or opaque
                CVector2f TexCoord = (LocalHitPoint + CVector2f(1.f)) * 0.5f;
                TexCoord.X = -TexCoord.X + 1.f;
                float TexelAlpha = ActiveBillboard()->ReadTexelAlpha(TexCoord);

                if (TexelAlpha < 0.25f)
                    Out.Hit = false;

                else
                {
                    // It's opaque... we have a hit!
                    Out.Hit = true;
                    Out.Distance = PlaneTest.second;
                }
            }

            else
                Out.Hit = false;
        }

        else
            Out.Hit = false;
    }

    return Out;
}

bool CScriptNode::AllowsRotate() const
{
    return (Template()->RotationType() == CScriptTemplate::ERotationType::RotationEnabled);
}

bool CScriptNode::AllowsScale() const
{
    return (Template()->ScaleType() != CScriptTemplate::EScaleType::ScaleDisabled);
}

bool CScriptNode::IsVisible() const
{
    // Reimplementation of CSceneNode::IsVisible() to allow for layer and template visiblity to be taken into account
    return (mVisible && mpInstance->Layer()->IsVisible() && Template()->IsVisible());
}

CColor CScriptNode::TintColor(const SViewInfo &ViewInfo) const
{
    CColor BaseColor = CSceneNode::TintColor(ViewInfo);
    if (mpExtra) mpExtra->ModifyTintColor(BaseColor);
    return BaseColor;
}

void CScriptNode::LinksModified()
{
    if (mpExtra) mpExtra->LinksModified();
}

CStructRef CScriptNode::GetProperties() const
{
    return CStructRef(mpInstance->PropertyData(), mpInstance->Template()->Properties());
}

void CScriptNode::PropertyModified(IProperty* pProp)
{
    // Update volume
    EPropertyType Type = pProp->Type();

    if ( Type == EPropertyType::Bool || Type == EPropertyType::Byte || Type == EPropertyType::Short ||
         Type == EPropertyType::Int || Type == EPropertyType::Choice || Type == EPropertyType::Enum )
    {
        mpInstance->EvaluateVolume();
        UpdatePreviewVolume();
    }

    // Update resources
    else if (Type == EPropertyType::AnimationSet)
    {
        mpInstance->EvaluateDisplayAsset();
        SetDisplayAsset(mpInstance->DisplayAsset());
    }

    else if (Type == EPropertyType::Asset)
    {
        CAssetProperty* pAssetProperty = TPropCast<CAssetProperty>(pProp);
        const CResTypeFilter& rkFilter = pAssetProperty->GetTypeFilter();

        if (rkFilter.Accepts(EResourceType::Model) || rkFilter.Accepts(EResourceType::Texture) ||
            rkFilter.Accepts(EResourceType::AnimSet) || rkFilter.Accepts(EResourceType::Character))
        {
            mpInstance->EvaluateDisplayAsset();
            SetDisplayAsset(mpInstance->DisplayAsset());
        }
        else if (rkFilter.Accepts(EResourceType::DynamicCollision))
        {
            mpInstance->EvaluateCollisionModel();
            mpCollisionNode->SetCollision(mpInstance->Collision());
        }
    }

    // Update other editor properties
    CScriptTemplate *pTemplate = Template();

    if (pProp == pTemplate->NameProperty())
        SetName("[" + mpInstance->Template()->Name() + "] " + mpInstance->InstanceName());

    else if (pProp == pTemplate->PositionProperty() || pProp->Parent() == pTemplate->PositionProperty())
        mPosition = mpInstance->Position();

    else if (pProp == pTemplate->RotationProperty() || pProp->Parent() == pTemplate->RotationProperty())
        mRotation = CQuaternion::FromEuler(mpInstance->Rotation());

    else if (pProp == pTemplate->ScaleProperty() || pProp->Parent() == pTemplate->ScaleProperty())
        mScale = mpInstance->Scale();

    MarkTransformChanged();
    SetLightLayerIndex(mpLightParameters->LightLayerIndex());

    // Notify attachments
    for (uint32 AttachIdx = 0; AttachIdx < mAttachments.size(); AttachIdx++)
    {
        CScriptAttachNode* pAttachNode = mAttachments[AttachIdx];

        if (pAttachNode->AttachProperty() == pProp)
            pAttachNode->AttachPropertyModified();
    }

    // Notify script extra
    if (mpExtra) mpExtra->PropertyModified(pProp);

    // Update game mode visibility
    if (pProp && pProp == pTemplate->ActiveProperty())
        TestGameModeVisibility();
}

void CScriptNode::UpdatePreviewVolume()
{
    EVolumeShape Shape = mpInstance->VolumeShape();
    TResPtr<CModel> pVolumeModel = nullptr;

    switch (Shape)
    {
    case EVolumeShape::AxisAlignedBoxShape:
    case EVolumeShape::BoxShape:
        pVolumeModel = gpEditorStore->LoadResource("VolumeBox.CMDL");
        break;

    case EVolumeShape::EllipsoidShape:
        pVolumeModel = gpEditorStore->LoadResource("VolumeSphere.CMDL");
        break;

    case EVolumeShape::CylinderShape:
        pVolumeModel = gpEditorStore->LoadResource("VolumeCylinder.CMDL");
        break;
    default: break;
    }

    mHasVolumePreview = (pVolumeModel != nullptr);

    if (mHasVolumePreview)
    {
        mpVolumePreviewNode->SetModel(pVolumeModel);
        mpVolumePreviewNode->SetScale(mpInstance->VolumeScale());
    }
}

void CScriptNode::GeneratePosition()
{
    if (!mHasValidPosition)
    {
        // Default to center of the active area; this is to prevent recursion issues
        CTransform4f AreaTransform = mpScene->ActiveArea()->Transform();
        mPosition = CVector3f(AreaTransform[0][3], AreaTransform[1][3], AreaTransform[2][3]);
        mHasValidPosition = true;
        MarkTransformChanged();

        // Ideal way to generate the position is to find a spot close to where it's being used.
        // To do this I check the location of the objects that this one is linked to.
        uint32 NumLinks = mpInstance->NumLinks(ELinkType::Incoming) + mpInstance->NumLinks(ELinkType::Outgoing);

        // In the case of one link, apply an offset so the new position isn't the same place as the object it's linked to
        if (NumLinks == 1)
        {
            uint32 LinkedID = (mpInstance->NumLinks(ELinkType::Incoming) > 0 ? mpInstance->Link(ELinkType::Incoming, 0)->SenderID() : mpInstance->Link(ELinkType::Outgoing, 0)->ReceiverID());
            CScriptNode *pNode = mpScene->NodeForInstanceID(LinkedID);
            pNode->GeneratePosition();
            mPosition = pNode->AbsolutePosition();
            mPosition.Z += (pNode->AABox().Size().Z / 2.f);
            mPosition.Z += (AABox().Size().Z / 2.f);
            mPosition.Z += 2.f;
        }

        // For two or more links, average out the position of the connected objects.
        else if (NumLinks >= 2)
        {
            CVector3f NewPos = CVector3f::skZero;

            for (uint32 iIn = 0; iIn < mpInstance->NumLinks(ELinkType::Incoming); iIn++)
            {
                CScriptNode *pNode = mpScene->NodeForInstanceID(mpInstance->Link(ELinkType::Incoming, iIn)->SenderID());

                if (pNode)
                {
                    pNode->GeneratePosition();
                    NewPos += pNode->AABox().Center();
                }
            }

            for (uint32 iOut = 0; iOut < mpInstance->NumLinks(ELinkType::Outgoing); iOut++)
            {
                CScriptNode *pNode = mpScene->NodeForInstanceID(mpInstance->Link(ELinkType::Outgoing, iOut)->ReceiverID());

                if (pNode)
                {
                    pNode->GeneratePosition();
                    NewPos += pNode->AABox().Center();
                }
            }

            mPosition = NewPos / (float) NumLinks;
            mPosition.X += 2.f;
        }

        MarkTransformChanged();
    }
}

void CScriptNode::TestGameModeVisibility()
{
    // Don't render if we don't have an ingame model, or if this is the Prime series and the instance is not active.
    if ((Template()->Game() < EGame::DKCReturns && !mpInstance->IsActive()) || !mpInstance->HasInGameModel())
        mGameModeVisibility = EGameModeVisibility::NotVisible;

    // If this is Returns, only render if the instance is active OR if it has a near visible activation.
    else
        mGameModeVisibility = (mpInstance->IsActive() || mpInstance->HasNearVisibleActivation()) ? EGameModeVisibility::Visible : EGameModeVisibility::NotVisible;
}

CColor CScriptNode::WireframeColor() const
{
    return CColor::Integral(12, 135, 194);
}

CScriptObject* CScriptNode::Instance() const
{
    return mpInstance;
}

CScriptTemplate* CScriptNode::Template() const
{
    return mpInstance->Template();
}

CScriptExtra* CScriptNode::Extra() const
{
    return mpExtra;
}

CModel* CScriptNode::ActiveModel() const
{
    if (mpDisplayAsset)
    {
        if (mpDisplayAsset->Type() == EResourceType::Model)
            return static_cast<CModel*>(mpDisplayAsset.RawPointer());
        else if (mpDisplayAsset->Type() == EResourceType::AnimSet || mpDisplayAsset->Type() == EResourceType::Character)
            return static_cast<CAnimSet*>(mpDisplayAsset.RawPointer())->Character(mCharIndex)->pModel;
    }

    return nullptr;
}

CAnimSet* CScriptNode::ActiveAnimSet() const
{
    if (mpDisplayAsset && (mpDisplayAsset->Type() == EResourceType::AnimSet || mpDisplayAsset->Type() == EResourceType::Character))
        return static_cast<CAnimSet*>(mpDisplayAsset.RawPointer());
    else
        return nullptr;
}

CSkeleton* CScriptNode::ActiveSkeleton() const
{
    CAnimSet *pSet = ActiveAnimSet();
    if (pSet) return pSet->Character(mCharIndex)->pSkeleton;
    else return nullptr;
}

CAnimation* CScriptNode::ActiveAnimation() const
{
    CAnimSet *pSet = ActiveAnimSet();
    return pSet ? pSet->FindAnimationAsset(mAnimIndex) : nullptr;
}

CTexture* CScriptNode::ActiveBillboard() const
{
    if (mpDisplayAsset && mpDisplayAsset->Type() == EResourceType::Texture)
        return static_cast<CTexture*>(mpDisplayAsset.RawPointer());
    else
        return nullptr;
}

bool CScriptNode::UsesModel() const
{
    return (mpDisplayAsset == nullptr || ActiveModel() != nullptr);
}

bool CScriptNode::HasPreviewVolume() const
{
    return mHasVolumePreview;
}

CAABox CScriptNode::PreviewVolumeAABox() const
{
    if (!mHasVolumePreview)
        return CAABox::skZero;
    else
        return mpVolumePreviewNode->AABox();
}

CVector2f CScriptNode::BillboardScale() const
{
    CVector2f Out = (Template()->ScaleType() == CScriptTemplate::EScaleType::ScaleEnabled ? AbsoluteScale().XZ() : CVector2f(1.f));
    return Out * 0.5f * Template()->PreviewScale();
}

CTransform4f CScriptNode::BoneTransform(uint32 BoneID, EAttachType AttachType, bool Absolute) const
{
    CTransform4f Out;
    CSkeleton *pSkel = ActiveSkeleton();

    if (pSkel)
    {
        CBone *pBone = pSkel->BoneByID(BoneID);
        ASSERT(pBone);

        if (AttachType == eAttach) Out.Rotate(pBone->Rotation());
        Out.Translate(pBone->Position());
    }

    if (Absolute) Out = Transform() * Out;

    return Out;
}

// ************ PROTECTED ************
void CScriptNode::SetDisplayAsset(CResource *pRes)
{
    mpDisplayAsset = pRes;

    bool IsAnimSet = (pRes && (pRes->Type() == EResourceType::AnimSet || pRes->Type() == EResourceType::Character));
    mCharIndex = (IsAnimSet ? mpInstance->ActiveCharIndex() : -1);
    mAnimIndex = (IsAnimSet ? mpInstance->ActiveAnimIndex() : -1);

    CModel *pModel = ActiveModel();
    mLocalAABox = (pModel ? pModel->AABox() : CAABox::skOne);
    MarkTransformChanged();

    for (uint32 iAttach = 0; iAttach < mAttachments.size(); iAttach++)
        mAttachments[iAttach]->ParentDisplayAssetChanged(pRes);

    if (mpExtra)
        mpExtra->DisplayAssetChanged(pRes);
}

void CScriptNode::CalculateTransform(CTransform4f& rOut) const
{
    CScriptTemplate *pTemp = Template();

    if (pTemp->ScaleType() != CScriptTemplate::EScaleType::ScaleDisabled)
    {
        CVector3f Scale = (HasPreviewVolume() ? CVector3f::skOne : AbsoluteScale());
        rOut.Scale(Scale * pTemp->PreviewScale());
    }

    if (pTemp->RotationType() == CScriptTemplate::ERotationType::RotationEnabled)
        rOut.Rotate(AbsoluteRotation());

    rOut.Translate(AbsolutePosition());
}
