// MonoGame - Copyright (C) The MonoGame Team
// This file is subject to the terms and conditions defined in
// file 'LICENSE.txt', which is part of this source code package.

using System;
using System.Collections.Generic;

namespace Microsoft.Xna.Framework.Graphics
{
    /// <summary>
    /// A basic 3D model with per mesh parent bones.
    /// </summary>
	public sealed class Model
	{
		private static Matrix[] sharedDrawBoneMatrices;
		
		private GraphicsDevice graphicsDevice;

        /// <summary>
        /// A collection of <see cref="ModelBone"/> objects which describe how each mesh in the
        /// mesh collection for this model relates to its parent mesh.
        /// </summary>
        public ModelBoneCollection Bones { get; private set; }

        /// <summary>
        /// A collection of <see cref="ModelMesh"/> objects which compose the model. Each <see cref="ModelMesh"/>
        /// in a model may be moved independently and may be composed of multiple materials
        /// identified as <see cref="ModelMeshPart"/> objects.
        /// </summary>
        public ModelMeshCollection Meshes { get; private set; }

        /// <summary>
        /// Root bone for this model.
        /// </summary>
        public ModelBone Root { get; set; }

        /// <summary>
        /// Custom attached object.
        /// <remarks>
        /// Skinning data is example of attached object for model.
        /// </remarks>
        /// </summary>
        public object Tag { get; set; }

		internal Model()
		{

		}

        /// <summary>
        /// Constructs a model. 
        /// </summary>
        /// <param name="graphicsDevice">A valid reference to <see cref="GraphicsDevice"/>.</param>
        /// <param name="bones">The collection of bones.</param>
        /// <param name="meshes">The collection of meshes.</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="graphicsDevice"/> is null.
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="bones"/> is null.
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="meshes"/> is null.
        /// </exception>
        public Model(GraphicsDevice graphicsDevice, List<ModelBone> bones, List<ModelMesh> meshes)
		{
            if (graphicsDevice == null)
            {
                throw new ArgumentNullException("graphicsDevice", FrameworkResources.ResourceCreationWhenDeviceIsNull);
            }

			// TODO: Complete member initialization
			this.graphicsDevice = graphicsDevice;

			Bones = new ModelBoneCollection(bones);
			Meshes = new ModelMeshCollection(meshes);
		}

        internal void BuildHierarchy()
		{
			var globalScale = Matrix.CreateScale(0.01f);
			
			foreach(var node in this.Root.Children)
			{
				BuildHierarchy(node, this.Root.Transform * globalScale, 0);
			}
		}
		
		private void BuildHierarchy(ModelBone node, Matrix parentTransform, int level)
		{
			node.ModelTransform = node.Transform * parentTransform;
			
			foreach (var child in node.Children) 
			{
				BuildHierarchy(child, node.ModelTransform, level + 1);
			}
			
			//string s = string.Empty;
			//
			//for (int i = 0; i < level; i++) 
			//{
			//	s += "\t";
			//}
			//
			//Debug.WriteLine("{0}:{1}", s, node.Name);
		}

        /// <summary>
        /// Draws the model meshes.
        /// </summary>
        /// <param name="world">The world transform.</param>
        /// <param name="view">The view transform.</param>
        /// <param name="projection">The projection transform.</param>
        public void Draw(Matrix world, Matrix view, Matrix projection) 
		{       
            int boneCount = this.Bones.Count;
			
			if (sharedDrawBoneMatrices == null ||
				sharedDrawBoneMatrices.Length < boneCount)
			{
				sharedDrawBoneMatrices = new Matrix[boneCount];    
			}
			
			// Look up combined bone matrices for the entire model.            
			CopyAbsoluteBoneTransformsTo(sharedDrawBoneMatrices);

            // Draw the model.
            foreach (ModelMesh mesh in Meshes)
            {
                foreach (Effect effect in mesh.Effects)
                {
					IEffectMatrices effectMatricies = effect as IEffectMatrices;
					if (effectMatricies == null) {
						throw new InvalidOperationException();
					}
                    effectMatricies.World = sharedDrawBoneMatrices[mesh.ParentBone.Index] * world;
                    effectMatricies.View = view;
                    effectMatricies.Projection = projection;
                }

                mesh.Draw();
            }
		}

        /// <summary>
        /// Copies bone transforms relative to all parent bones of the each bone from this model to a given array.
        /// </summary>
        /// <param name="destinationBoneTransforms">The array receiving the transformed bones.</param>
        public void CopyAbsoluteBoneTransformsTo(Matrix[] destinationBoneTransforms)
		{
			if (destinationBoneTransforms == null)
				throw new ArgumentNullException("destinationBoneTransforms");
            if (destinationBoneTransforms.Length < this.Bones.Count)
				throw new ArgumentOutOfRangeException("destinationBoneTransforms");
            int count = this.Bones.Count;
			for (int index1 = 0; index1 < count; ++index1)
			{
                ModelBone modelBone = (this.Bones)[index1];
				if (modelBone.Parent == null)
				{
					destinationBoneTransforms[index1] = modelBone.transform;
				}
				else
				{
					int index2 = modelBone.Parent.Index;
					Matrix.Multiply(ref modelBone.transform, ref destinationBoneTransforms[index2], out destinationBoneTransforms[index1]);
				}
			}
		}

        /// <summary>
        /// Copies bone transforms relative to <see cref="Model.Root"/> bone from a given array to this model.
        /// </summary>
        /// <param name="sourceBoneTransforms">The array of prepared bone transform data.</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="sourceBoneTransforms"/> is null.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="sourceBoneTransforms"/> is invalid.
        /// </exception>
        public void CopyBoneTransformsFrom(Matrix[] sourceBoneTransforms)
        {
            if (sourceBoneTransforms == null)
                throw new ArgumentNullException("sourceBoneTransforms");
            if (sourceBoneTransforms.Length < Bones.Count)
                throw new ArgumentOutOfRangeException("sourceBoneTransforms");

            int count = Bones.Count;
            for (int i = 0; i < count; i++)
            {
                Bones[i].Transform = sourceBoneTransforms[i];
            }
        }

        /// <summary>
        /// Copies bone transforms relative to <see cref="Model.Root"/> bone from this model to a given array.
        /// </summary>
        /// <param name="destinationBoneTransforms">The array receiving the transformed bones.</param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="destinationBoneTransforms"/> is null.
        /// </exception>
        /// <exception cref="ArgumentOutOfRangeException">
        /// <paramref name="destinationBoneTransforms"/> is invalid.
        /// </exception>
        public void CopyBoneTransformsTo(Matrix[] destinationBoneTransforms)
        {
            if (destinationBoneTransforms == null)
                throw new ArgumentNullException("destinationBoneTransforms");
            if (destinationBoneTransforms.Length < Bones.Count)
                throw new ArgumentOutOfRangeException("destinationBoneTransforms");

            int count = Bones.Count;
            for (int i = 0; i < count; i++)
            {
                destinationBoneTransforms[i] = Bones[i].Transform;
            }
        }
    }
}
