//BSD License  
//DXGfx® - http://www.eteractions.com
//Copyright (c) 2005
//by Guillaume Randon
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software
//and associated documentation files (the "Software"), to deal in the Software without restriction,
//including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
//and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
//subject to the following conditions:
//The above copyright notice and this permission notice shall be included in all copies or substantial
//portions of the Software.
//THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 
//INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE 
//AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
//DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
//OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
using Microsoft.Samples.DirectX.UtilityToolkit;

namespace DXGfxLib
{
    /// <summary>
    /// We want to update world position of each frame in the Update phase of our scene graph.
    /// The base Frame doesn't keep track of that worldPosition so we need to derive from Frame and
    /// add members to our class in order to be able to store that matrix.
    /// </summary>
    internal class EnhancedFrame : Frame
    {
        /// <summary>
        /// World matrix of each EnhancedFrame. That value is updated during the update of the scenegraph
        /// based on the animationController advancement and the worldmat of the ArticulatedModel.
        /// </summary>
		public Matrix frameWorldMat;

        /// <summary>
        /// It is interesting to have a bounding box for our frame!
        /// 
        /// </summary>
        public AABBox boundingBox;

        /// <summary>
        /// At creation time we affect the identity matrix to frameWorldMat. That value is overwritten on the first
        /// update of the scene graph.
        /// </summary>
        public EnhancedFrame()
		{
            frameWorldMat = Matrix.Identity;
		}
    }

    /// <summary>
    /// We want to keep track of quite a lot of information that are not kept by MeshContainer. So
    /// we derive our own class from it and add members to keep track of every single information we need.
    /// These information are populated when loading a hierarchy from file by EnhancedAllocateHierarchy.
    /// </summary>
    internal class EnhancedMeshContainer : MeshContainer
    {
        /// <summary>
        /// The original mesh.
        /// </summary>
        public Mesh oriMesh = null;

        /// <summary>
        /// This property flags whether or not we have skinning information for this mesh.
        /// </summary>
        public bool hasSkin = false;

        /// <summary>
        /// The blended mesh to draw
        /// </summary>
        public Mesh BlendedMesh;

        /// <summary>
        /// The bone combinations for this mesh. Each bone combination represents a mesh subset
        /// with the same attribute and bone combination, and can be drawn in a single drawing call.
        /// </summary>
        public BoneCombination[] BoneCombinations;

        public int numMaterials = 0;
        /// <summary>
        /// Materials for the blended mesh
        /// </summary>
        public Microsoft.DirectX.Direct3D.Material[] materials;

        /// <summary>
        /// Textures for the blended mesh
        /// </summary>
        public Texture[] textures;

        /// <summary>
        /// Maximum number of influences effecting any single face in the skin mesh.
        /// </summary>
        public int MaxFaceInfluences = 0;

        /// <summary>
        /// Bone offset matrices for all bones that affect this mesh. Stored here for fast lookup while drawing.
        /// Fetched from SkinInformation.GetBoneOffsetMatrix(matrixId)
        /// </summary>
        public Matrix[] BoneOffsetMatrices;

        /// <summary>
        /// Bones (frames) that affect this mesh. Stored here for fast lookup while drawing.
        /// Fetched from Frame.Find(root, SkinInformation.GetBoneName(i))
        /// </summary>
        public EnhancedFrame[] AffectingBones;

        /// <summary>
        /// In case our blended mesh needs effects for its rendering we would store them here.
        /// </summary>
        public EffectInstance[] EffectInstances;

        /// <summary>
        /// It is interesting to calculate and keep a local bounding box for each container.
        /// </summary>
        public AABBox localBoundingBox;
    }

    /// <summary>
    /// In order to create derived classes instead of base Frame and MeshContainer we need to provide our own
    /// class deriving from AllocateHierarchy. This is EnhancedAllocateHierarchy.
    /// </summary>
    internal class EnhancedAllocateHierarchy : AllocateHierarchy
    {
        public override Frame CreateFrame(string name)
        {
            EnhancedFrame frame = new EnhancedFrame();
            frame.Name = name;

            return frame;
        }

        public override MeshContainer CreateMeshContainer(string name, MeshData meshData, ExtendedMaterial[] materials, EffectInstance[] effectInstances, GraphicsStream adjacency, SkinInformation skinInfo)
        {
            EnhancedMeshContainer container = new EnhancedMeshContainer();

            container.Name = name;

            // meshData
            container.MeshData = meshData;


            container.oriMesh = meshData.Mesh;
            container.localBoundingBox = AABBox.CalculateLocalBBox(container.oriMesh);
           

            // materials
            if (materials != null)
                container.SetMaterials(materials);

            // skinInfo
            if (skinInfo != null)
                container.SkinInformation = skinInfo;

            // effectIntances
            if (effectInstances != null)
                container.EffectInstances = effectInstances;

            // adjacency
            if (adjacency != null)
                container.SetAdjacency(adjacency);
            else
            {
                throw new NullReferenceException("adjancy null!");
            }

            // materials and textures
            if (materials != null)
            {
                container.materials = new Microsoft.DirectX.Direct3D.Material[materials.Length];
                container.textures = new Texture[materials.Length];
                container.numMaterials = materials.Length;

                for (int i = 0; i < materials.Length; i++)
                {
                    // Copy the material
                    container.materials[i] = materials[i].Material3D;

                    // Set the ambient color for the material (D3DX does not do this)
                    container.materials[i].Ambient = container.materials[i].Diffuse;

                    // Create the texture
                    if (materials[i].TextureFilename != null)
                    {
                        if (File.Exists(materials[i].TextureFilename))
                            container.textures[i] = ResourceCache.GetGlobalInstance().CreateTextureFromFile(ArticulatedModel.currentDevice, materials[i].TextureFilename);
                        else
                        {
                            throw new Exception("Texture file for material not found!");
                        }
                    }
                }
            }

            if (skinInfo != null)
            {
                container.BoneOffsetMatrices = new Matrix[skinInfo.NumberBones];

                for (int i = 0; i < skinInfo.NumberBones; i++)
                    container.BoneOffsetMatrices[i] = skinInfo.GetBoneOffsetMatrix(i);

                container.BlendedMesh = skinInfo.ConvertToBlendedMesh(
                    container.MeshData.Mesh, MeshFlags.Managed, container.GetAdjacencyStream(),
                    out container.MaxFaceInfluences, out container.BoneCombinations);
            }

            return container;
        }
    }

    /// <summary>
    /// Each time you want to add an animated model to your scene you should use this class or a class that derives from this.
    /// The model and associated animations are going to be loaded from a X file. By default all animations will be played as
    /// soon as the instance of this class is added to the scene.
    /// </summary>
    public class ArticulatedModel : MeshObject
    {
        /// <summary>
        /// EnhancedMeshContainer need to know the current D3DDevice to be alble to load textures associated with the x file.
        /// So for convenience I have added that information as a static fielf of this class.
        /// </summary>
        public static Device currentDevice = null;

        /// <summary>
        /// There is no articulated model without a frame hierarchy and thus without an AnimationRootFrame. The rootFrame
        /// member is just that. Updates and draw calls start from it and are propagated to children and sibling frames.
        /// </summary>
        protected AnimationRootFrame rootFrame;

        /// <summary>
        /// We want to know if our ArticulatedModel has any associated skin information. This boolean is setted when
        /// loading the x file and can be checked whenever it is needed afterwards.
        /// </summary>
        public bool HasSkin = false;

        /// <summary>
        /// We switch that one from 0 to 1....
        /// </summary>
        private int currentAnimationTrack = 1;

        /// <summary>
        /// Each time you start or stop an animation that value is used to generate keys to perform the transition from
        /// current state to desired state. Animations will be smoothly started or stoped over a period of time matching with this value.
        /// </summary>
        public float animationTransitionTime = 0.5f;

        /// <summary>
        /// We also want to keep track of the current time. This is needed by method in charge of stoping or starting animation
        /// progressively. They use that value to generate keys at time value calculated from this one. This value is updated
        /// in the Update method as might be expect. This member variable is public in case you might need for the same
        /// kind of operation in your own code controlling an instance of this class.
        /// </summary>
        public double currentTime;

        /// <summary>
        /// Default constructor, this one doesn't do anything at all except creating an instance of the class. If you don't
        /// call LoadFromFile afterward you won't have anything in this class and if you had that one to the scene before
        /// loading anything you are going to create havoc!! Of course I could more time in my code checking for nulls but at
        /// some point I can see with the profiler that I am spending a lot of time checking for nulls and this is not necessary
        /// if you follow an expected order in the operations (loading before adding to scene!).
        /// </summary>
        public ArticulatedModel()
        {
        }

        /// <summary>
        /// This will load all information from an x file. First it will populate basic information needed by SceneObject, then
        /// it will move on and load the FrameHierarchy. This method might look sub efficient as we load two time the mesh
        /// information, a first time to populate what is needed by SceneObject and another time in the FrameHierarchy.
        /// This might change in the future.
        /// </summary>
        /// <param name="d3ddevice">Self explicit I hope.</param>
        /// <param name="fileName">Idem</param>
        /// <returns>If everything goes right it should return true.</returns>
        public override void LoadFromFile(Device d3ddevice, string fileName)
        {
            // This will be used to populate fields used by SceneObject code (intersections and everything which at the moment
            // have not been overriden (yet)).
            //Attach(new FrameworkMesh(d3ddevice, fileName));

            currentDevice = d3ddevice;
            rootFrame = Mesh.LoadHierarchyFromFile(fileName, MeshFlags.Managed, d3ddevice, new EnhancedAllocateHierarchy(), null);

            SetupBonePointersOnMesh(rootFrame.FrameHierarchy);
        }

        /// <summary>
        /// This method is used to setup bone information from X file. This function calls itself passing sibling and
        /// child frame as parameter to subsequent calls.
        /// </summary>
        /// <param name="frame">frame on which to perform the operation. Will automatically be performed
        /// on child frame and sibling frames of the frame hierarchy.</param>
        protected void SetupBonePointersOnMesh(Frame frame)
        {
            // If this frame has a mesh container, store pointers to all frames (bones) that affect this mesh.
            if (frame.MeshContainer != null)
            {
                EnhancedMeshContainer meshContainer = (EnhancedMeshContainer)frame.MeshContainer;
                SkinInformation skinInformation = meshContainer.SkinInformation;

                if (skinInformation != null)
                {
                    HasSkin = true;
                    meshContainer.AffectingBones = new EnhancedFrame[skinInformation.NumberBones];

                    for (int i = 0; i < skinInformation.NumberBones; i++)
                    {
                        EnhancedFrame affectingBone = (EnhancedFrame)Frame.Find(rootFrame.FrameHierarchy,
                            skinInformation.GetBoneName(i));
                        meshContainer.AffectingBones[i] = affectingBone;
                    }
                }
            }

            // Perform this operation recursively
            if (frame.FrameSibling != null) SetupBonePointersOnMesh(frame.FrameSibling);

            if (frame.FrameFirstChild != null) SetupBonePointersOnMesh(frame.FrameFirstChild);
        }

        /// <summary>
        /// This method is the method through which the animation is progressed. By default as soon as your instance
        /// of ArticulatedModel will be added to the scene this method will be called and thus the animation will be progressed
        /// by default.
        /// </summary>
        /// <param name="frustrum">Viewing frustrum at the time of the update. Check description of that specific class
        /// for further details.</param>
        /// <param name="appTime">Absolute time when Update is called.</param>
        /// <param name="elapsedTime">Elapsed time since last update. This value is used in a lot of places so we pass
        /// it as a parameter.</param>
        public override void Update(Frustrum frustrum, double appTime, float elapsedTime)
        {
            base.Update(frustrum, appTime, elapsedTime);

            rootFrame.AnimationController.AdvanceTime(elapsedTime, null);
            UpdateFrameMatrices(rootFrame.FrameHierarchy, WorldMat);
        }

        /// <summary>
        /// We update each frame world matrice from the ArticulatedModel world mat and current advancement of the
        /// animation controller. The animation controller used is the one associated with the rootframe.
        /// </summary>
        /// <param name="frame">Frame on which to perform the operation. In fact an EnhancedFramed is expected here.</param>
        /// <param name="parentMat">world mat of the parent Frame or of the ArticulatedModel for the root frame.</param>
        protected void UpdateFrameMatrices(Frame frame, Matrix parentMat)
        {
            BBox framebbox = null;

            EnhancedFrame enhancedFrame = frame as EnhancedFrame;
            if (enhancedFrame != null)
            {
                enhancedFrame.frameWorldMat = Matrix.Multiply(frame.TransformationMatrix, parentMat);
                if (enhancedFrame.MeshContainer != null)
                {
                    EnhancedMeshContainer enMeshCont = enhancedFrame.MeshContainer as EnhancedMeshContainer;
                    if (enMeshCont != null)
                    {
                        framebbox = enMeshCont.localBoundingBox.Transform(enhancedFrame.frameWorldMat);
                    }
                }
            }

            if (frame.FrameSibling != null)
            {
                UpdateFrameMatrices(frame.FrameSibling, parentMat);
                EnhancedFrame enFrame = frame.FrameSibling as EnhancedFrame;
                if (enFrame != null)
                {
                    if (framebbox != null)
                    {
                        framebbox = AABBox.BoxFromBoxes(new AABBox(framebbox), enFrame.boundingBox);
                    }
                    else
                    {
                        framebbox = enFrame.boundingBox;
                    }
                }
            }

            if ((frame.FrameFirstChild != null) && (enhancedFrame != null))
            {
                UpdateFrameMatrices(frame.FrameFirstChild, enhancedFrame.frameWorldMat);
                EnhancedFrame enFrame = frame.FrameFirstChild as EnhancedFrame;
                if (enFrame != null)
                {
                    if (framebbox != null)
                    {
                        framebbox = AABBox.BoxFromBoxes(new AABBox(framebbox), enFrame.boundingBox);
                    }
                    else
                    {
                        framebbox = enFrame.boundingBox;
                    }
                }
            }

            if (enhancedFrame != null)
            {
                if (framebbox == null)
                {
                    framebbox = new BBox();
                }
                enhancedFrame.boundingBox = new AABBox(framebbox);
            }
        }

        /// <summary>
        /// This is where actual DX draw calls are performed. This method calls itself on sibling and child frame of the
        /// frame passed as argument and thus will render the whole frame hierarchy below the frame passed as argument.
        /// </summary>
        /// <param name="d3ddevice">The current device on which the rendering will be performed.</param>
        /// <param name="frame">The frame on which rendering should be started. As stated in the description rendering for
        /// the whole frame hierarchy below or at the same level as this frame will also be performed from here.</param>
        protected void RenderFrame(Device d3ddevice, Frame frame)
        {
            // Check if this is a mesh frame
            if (frame.MeshContainer != null)
            {
                EnhancedMeshContainer meshContainer = (EnhancedMeshContainer)frame.MeshContainer;

                // Standard mesh. Draw it the simple way.
                if (meshContainer.SkinInformation == null)
                {
                    EnhancedFrame frameDerived = (EnhancedFrame)frame;
                    d3ddevice.SetTransform(TransformType.World, frameDerived.frameWorldMat);
                    for (int i = 0; i < meshContainer.materials.Length; i++)
                    {
                        d3ddevice.Material = meshContainer.materials[i];
                        if (meshContainer.textures[i] != null)
                            d3ddevice.SetTexture(0, meshContainer.textures[i]);
                        meshContainer.MeshData.Mesh.DrawSubset(i);
                    }
                }
                // Animated mesh.
                else
                {
                    // Loop all mesh subsets (bone combinations)
                    int lastAttribId = -1;
                    for (int j = 0; j < meshContainer.BoneCombinations.Length; j++)
                    {
                        BoneCombination boneCombination = meshContainer.BoneCombinations[j];

                        d3ddevice.SetTransform(TransformType.World, Matrix.Identity);
                        d3ddevice.SetTransform(TransformType.World1, Matrix.Identity);
                        d3ddevice.SetTransform(TransformType.World2, Matrix.Identity);
                        d3ddevice.SetTransform(TransformType.World3, Matrix.Identity);

                        // Set the matrices for this subset
                        int blendCount = 0;
                        for (int i = 0; i < meshContainer.MaxFaceInfluences; i++)
                        {
                            int boneMatrixId = boneCombination.BoneId[i];

                            if (boneMatrixId != -1)
                            {
                                blendCount = i;

                                Matrix transformMatrix = Matrix.Multiply(meshContainer.BoneOffsetMatrices[boneMatrixId],
                                    meshContainer.AffectingBones[boneMatrixId].frameWorldMat);

                                if (i == 0) d3ddevice.SetTransform(TransformType.World, transformMatrix);
                                else if (i == 1) d3ddevice.SetTransform(TransformType.World1, transformMatrix);
                                else if (i == 2) d3ddevice.SetTransform(TransformType.World2, transformMatrix);
                                else if (i == 3) d3ddevice.SetTransform(TransformType.World3, transformMatrix);
                            }
                        }

                        // Set vertex blend count
                        if (blendCount == 1) d3ddevice.RenderState.VertexBlend = VertexBlend.OneWeights;
                        else if (blendCount == 2) d3ddevice.RenderState.VertexBlend = VertexBlend.TwoWeights;
                        else if (blendCount == 3) d3ddevice.RenderState.VertexBlend = VertexBlend.ThreeWeights;
                        else d3ddevice.RenderState.VertexBlend = VertexBlend.Disable;

                        // If the material is different than the last subset, update the device material
                        if (lastAttribId != boneCombination.AttributeId)
                        {
                            lastAttribId = boneCombination.AttributeId;
                            d3ddevice.Material = meshContainer.materials[lastAttribId];
                            d3ddevice.SetTexture(0, meshContainer.textures[lastAttribId]);
                        }

                        // Render the mesh
                        meshContainer.BlendedMesh.DrawSubset(j);
                    }

                    // Rendering is finished, set the vertex blending back to disabled mode
                    d3ddevice.RenderState.VertexBlend = VertexBlend.Disable;
                }
            }

            // Do this recursively on all child Frames and all Frames at the same level.
            if (frame.FrameSibling != null) RenderFrame(d3ddevice, frame.FrameSibling);
            if (frame.FrameFirstChild != null) RenderFrame(d3ddevice, frame.FrameFirstChild);
        }

        /// <summary>
        /// Draw call for the whole ArticulatedModel. This will render the whole associated Frame hierarchy in its current state.
        /// </summary>
        /// <param name="device">The device on which the rendering will be performed.</param>
        public override void Draw(Device device)
        {
            device.SetTransform(TransformType.World, worldMat);
            RenderFrame(device, rootFrame.FrameHierarchy);
        }

        /// <summary>
        /// This method generate a List of string with all the name of the animations associated with the ArticulatedModel.
        /// </summary>
        /// <returns>List of strings with all the animations' name.</returns>
        public List<String> GetAnimationList()
        {
            List<String> animations = new List<string>();
            for (int i = 0; i < rootFrame.AnimationController.MaxNumberAnimationSets; i++)
            {
                animations.Add(rootFrame.AnimationController.GetAnimationSet(i).Name);
            }
            return animations;
        }

        /// <summary>
        /// This method will do everything that is needed to get one specific animation smoothly started. Subsequent
        /// calls to the update method will then progress this specific animation. The way things are implement here means
        /// any other running animation for the the ArticulatedModel will be stop. This might change in the future if the functionnality
        /// is needed.
        /// </summary>
        /// <param name="animationName">The name of the animation to be started. </param>
        public void StartAnimation(string animationName)
        {
            AnimationSet animSet = rootFrame.AnimationController.GetAnimationSet(animationName);

            if (animSet == null)
            {
                return;
            }

            int newAnimationTrack = (currentAnimationTrack == 0 ? 1 : 0);

            rootFrame.AnimationController.SetTrackAnimationSet(newAnimationTrack, animSet);

            rootFrame.AnimationController.UnkeyAllTrackEvents(currentAnimationTrack);
            rootFrame.AnimationController.UnkeyAllTrackEvents(newAnimationTrack);

            rootFrame.AnimationController.KeyTrackEnable(currentAnimationTrack, false, currentTime + animationTransitionTime);
            rootFrame.AnimationController.KeyTrackSpeed(currentAnimationTrack, 0.0f, currentTime, animationTransitionTime, TransitionType.Linear);
            rootFrame.AnimationController.KeyTrackWeight(currentAnimationTrack, 0.0f, currentTime, animationTransitionTime, TransitionType.Linear);

            rootFrame.AnimationController.KeyTrackEnable(newAnimationTrack, true, currentTime + animationTransitionTime);
            rootFrame.AnimationController.KeyTrackSpeed(newAnimationTrack, 1.0f, currentTime, animationTransitionTime, TransitionType.Linear);
            rootFrame.AnimationController.KeyTrackWeight(newAnimationTrack, 1.0f, currentTime, animationTransitionTime, TransitionType.Linear);

            currentAnimationTrack = newAnimationTrack;

        }

        /// <summary>
        /// This method will smoothly stop all animations. The effect is immediate but the animations won't be still just after
        /// the call to this method. Instead keys are generated to stop them progressively (this concept is all relative to the animationTransitionTime
        /// so if you put a very small value there you can stop the animation nearly instantenously).
        /// </summary>
        public void StopAll()
        {
            rootFrame.AnimationController.UnkeyAllTrackEvents(currentAnimationTrack);

            rootFrame.AnimationController.KeyTrackEnable(currentAnimationTrack, false, currentTime + animationTransitionTime);
            rootFrame.AnimationController.KeyTrackSpeed(currentAnimationTrack, 0.0f, currentTime, animationTransitionTime, TransitionType.Linear);
            rootFrame.AnimationController.KeyTrackWeight(currentAnimationTrack, 0.0f, currentTime, animationTransitionTime, TransitionType.Linear);
        }
    }
}
