/*
 * ModelAnimator.cs
 * Copyright (c) 2007 David Astle, Michael Nikonov
 * 
 * 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.
 */
#define GI
#region Using Statements
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System.Collections.Generic;
using System;
using System.Collections.ObjectModel;
#endregion
namespace Xclna.Xna.Animation
{
    /// 
    /// Animates and draws a model that was processed with AnimatedModelProcessor
    /// 
    public  class ModelAnimator
    {
        #region Member Variables
        /// Stores the world transform for the animation controller.
        public Matrix world = Matrix.Identity;
        ///Model to be animated
        public readonly Model model;
        /// This stores all of the "World" matrix parameters for an unskinned model
        public readonly EffectParameter[] worldParams, matrixPaletteParams;
        /// A flattened array of effects, one for each ModelMeshPart
        public Effect[] modelEffects;
        public ReadOnlyCollection effectCollection;
        /// Skeletal structure containg transforms
        public BonePoseCollection bonePoses;
        public AnimationInfoCollection animations;
        /// List of attached objects
        public IList attachedObjects = new List();
        /// Store the number of meshes in the model
        public readonly int numMeshes;
        
        /// 
        ///  Stores the number of effects/ModelMeshParts
        /// 
        public readonly int numEffects;
        // Used to avoid reallocation
        public static Matrix skinTransform;
        // Buffer for storing absolute bone transforms
        public Matrix[] pose;
        // Array used for the matrix palette
        public Matrix[][] palette;
        // Inverse reference pose transforms
        public SkinInfoCollection[] skinInfo;
        #endregion
        #region General Properties
        /// 
        /// Gets or sets the world matrix for the animation scene.
        /// 
        public Matrix World
        {
            get
            {
                return world;
            }
            set
            {
                world = value;
            }
        }
        /// 
        /// Returns the number of effects used by the model, one for each ModelMeshPart
        /// 
        public int EffectCount
        {
            get { return numEffects; }
        }
        /// 
        /// Gets the model associated with this controller.
        /// 
        public Model Model
        { get { return model; } }
        /// 
        /// Gets the animations that were loaded in from the content pipeline
        /// for this model.
        /// 
        public AnimationInfoCollection Animations
        { get { return animations; } }
        #endregion
        #region Constructors
        /// 
        /// Creates a new instance of ModelAnimator.
        /// 
        /// The game to which this component will belong.
        /// The model to be animated.
        public ModelAnimator(Model model) : base()
        {
            this.model = model;
            animations = AnimationInfoCollection.FromModel(model);
            bonePoses = BonePoseCollection.FromModelBoneCollection(
                model.Bones);
            numMeshes = model.Meshes.Count;
            // Find total number of effects used by the model
            numEffects = 0;
            foreach (ModelMesh mesh in model.Meshes)
                foreach (Effect effect in mesh.Effects)
                    numEffects++;
            // Initialize the arrays that store effect parameters
            modelEffects = new Effect[numEffects];
            worldParams = new EffectParameter[numEffects];
            matrixPaletteParams = new EffectParameter[numEffects];
            InitializeEffectParams();
            pose = new Matrix[model.Bones.Count];
            model.CopyAbsoluteBoneTransformsTo(pose);
            // Get all the skinning info for the model
            Dictionary modelTagInfo = (Dictionary)model.Tag;
            if (modelTagInfo == null)
                throw new Exception("Model Processor must subclass AnimatedModelProcessor.");
            skinInfo = (SkinInfoCollection[])modelTagInfo["SkinInfo"];
            if (skinInfo == null)
                throw new Exception("Model processor must pass skinning info through the tag.");
            palette = new Matrix[model.Meshes.Count][];
            for (int i = 0; i < skinInfo.Length; i++)
            {
                if (Util.IsSkinned(model.Meshes[i]))
                    palette[i] = new Matrix[skinInfo[i].Count];
                else
                    palette[i] = null;
            }
            // Update after AnimationController by default
            //base.UpdateOrder = 1;
            //game.Components.Add(this);
   
            // Test to see if model has too many bones
            for (int i = 0; i < model.Meshes.Count; i++ )
            {
                if (palette[i] != null && matrixPaletteParams[i] != null)
                {
                    Matrix[] meshPalette = palette[i];
                    try
                    {
                        matrixPaletteParams[i].SetValue(meshPalette);
                    }
                    catch
                    {
                        throw new Exception("Model has too many skinned bones for the matrix palette.");
                    }
                }
            }
        }
        #endregion
        /// 
        /// Returns skinning information for a mesh.
        /// 
        /// The index of the mesh.
        /// Skinning information for the mesh.
        public SkinInfoCollection GetMeshSkinInfo(int index)
        {
            return skinInfo[index];
        }
        /// 
        /// Called during creation and calls to InitializeEffectParams.  Returns the list of
        /// effects used during rendering.
        /// 
        /// A flattened list of effects used during rendering, one for each ModelMeshPart
        protected virtual IList CreateEffectList()
        {
            List effects = new List();
            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (ModelMeshPart part in mesh.MeshParts)
                {
                    effects.Add(part.Effect);
                }
            }
            return effects;
        }
        /// 
        /// Initializes the effect parameters.  Should be called after the effects
        /// on the model are changed.
        /// 
        public void InitializeEffectParams()
        {
            IList effects = CreateEffectList();
            if (effects.Count != numEffects)
                throw new Exception("The number of effects in the list returned by CreateEffectList "
                    + "must be equal to the number of ModelMeshParts.");
            effects.CopyTo(modelEffects, 0);
            effectCollection = new ReadOnlyCollection(modelEffects);
            // store the parameters in the arrays so the values they refer to can quickly be set
            for (int i = 0; i < numEffects; i++)
            {
                worldParams[i] = modelEffects[i].Parameters["World"];
                matrixPaletteParams[i] = modelEffects[i].Parameters["MatrixPalette"];
            }
        }
        /// 
        /// Gets a collection of effects, one per ModelMeshPart, that are used by 
        /// the ModelAnimator. The first index of the collection corresponds to the
        /// effect used to draw the first ModelMeshPart of the first Mesh, and the 
        /// last index corresponds to the effect used to drwa the last ModelMeshPart
        /// of the last Mesh.
        /// 
        public ReadOnlyCollection Effects
        {
            get { return effectCollection; }
        }
        #region Animation and Update Routines
        /// 
        /// Updates the animator by finding the current absolute transforms.
        /// 
        /// The GameTime.
        public void Update()
        {
            bonePoses.CopyAbsoluteTransformsTo(pose);
            for (int i = 0; i < skinInfo.Length; i ++) 
            {
                if (palette[i] == null)
                    continue;
                SkinInfoCollection infoCollection = skinInfo[i];
                foreach (SkinInfo info in infoCollection)
                {
                    skinTransform = info.InverseBindPoseTransform;
                    Matrix.Multiply(ref skinTransform, ref pose[info.BoneIndex],
                       out palette[i][info.PaletteIndex]);
                }
            }
            foreach (IAttachable attached in attachedObjects)
            {
                attached.CombinedTransform = attached.LocalTransform *
                    Matrix.Invert(pose[model.Meshes[0].ParentBone.Index]) *
                    pose[attached.AttachedBone.Index] * world;
            }
        }
        /// 
        /// Copies the current absolute transforms to the specified array.
        /// 
        /// The array to which the transforms will be copied.
        public void CopyAbsoluteTransformsTo(Matrix[] transforms)
        {
            pose.CopyTo(transforms, 0);
        }
        /// 
        /// Gets the current absolute transform for the given bone index.
        /// 
        /// 
        /// The current absolute transform for the bone index.
        public Matrix GetAbsoluteTransform(int boneIndex)
        {
            return pose[boneIndex];
        }
        /// 
        /// Gets a list of objects that are attached to a bone in the model.
        /// 
        public IList AttachedObjects
        {
            get { return attachedObjects; }
        }
        /// 
        /// Gets the BonePoses associated with this ModelAnimator.
        /// 
        public BonePoseCollection BonePoses
        {
            get { return bonePoses; }
        }
        /// 
        /// Draws the current frame
        /// 
        /// The game time
        public void Draw()
        {
            try
            {
                int index = 0;
                // Update all the effects with the palette and world and draw the meshes
                for (int i = 0; i < numMeshes; i++)
                {
                    ModelMesh mesh = model.Meshes[i];
                    // The starting index for the modelEffects array
                    int effectStartIndex = index;
                    if (palette[i] != null && matrixPaletteParams[index] != null)
                    {
                        foreach (Effect effect in mesh.Effects)
                        {
                   
                                worldParams[index].SetValue(
               
                                    world);
                            
                            matrixPaletteParams[index].SetValue(palette[i]);
                            index++;
                        }
                    }
                    else
                    {
                        foreach (Effect effect in mesh.Effects)
                        {
                            worldParams[index].SetValue(pose[mesh.ParentBone.Index] * world);
                            index++;
                        }
                    }
                    int numParts = mesh.MeshParts.Count;
                    GraphicsDevice device = mesh.VertexBuffer.GraphicsDevice;
                    device.Indices = mesh.IndexBuffer;
                    for (int j = 0; j < numParts; j++ )
                    {
                        ModelMeshPart currentPart = mesh.MeshParts[j];
                        if (currentPart.NumVertices == 0 || currentPart.PrimitiveCount == 0)
                            continue;
                        Effect currentEffect = modelEffects[effectStartIndex+j];
                        currentEffect.CurrentTechnique = currentEffect.Techniques["BasicLightingShaderMain"];
                        
                        device.VertexDeclaration = currentPart.VertexDeclaration;
                        device.Vertices[0].SetSource(mesh.VertexBuffer, currentPart.StreamOffset,
                            currentPart.VertexStride);
                        currentEffect.Begin();
                        EffectPassCollection passes = currentEffect.CurrentTechnique.Passes;
                        int numPasses = passes.Count;
                        for (int k = 0; k < numPasses; k++)
                        {
                            EffectPass pass = passes[k];
                            pass.Begin();
                            device.DrawIndexedPrimitives(PrimitiveType.TriangleList, currentPart.BaseVertex,
                                0, currentPart.NumVertices, currentPart.StartIndex, currentPart.PrimitiveCount);
                            pass.End();
                        }
                        currentEffect.End();
                    }
                }
            }
            catch (NullReferenceException)
            {
                throw new InvalidOperationException("The effects on the model for a " +
                    "ModelAnimator were changed without calling ModelAnimator.InitializeEffectParams().");
            }
            catch (InvalidCastException)
            {
                throw new InvalidCastException("ModelAnimator has thrown an InvalidCastException.  This is " +
                    "likely because the model uses too many bones for the matrix palette.  The default palette size "
                    + "is 56 for windows and 40 for Xbox.");
            }
            
        }
        #endregion
    }
}