using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using Audio3D;

namespace FredriksdalAssault2
{
    /// <summary>
    /// This abstract class represents methods that are common for a map.
    /// </summary>
    public abstract class IMap : IAudioEmitter
    {
        #region Variables

        // The worldMatrix for the map.
        public Matrix worldMatrix;

        // Models
        public Model mapModel;
        public Sky sky;
        public Water water;
        public Model barrelModel;
        public Model treeModel;
        public Model grassModel;

        // Heightmap information
        public HeightMapInfo heightMapInfo;

        // Textures
        public Texture2D grassTexture;
        public Texture2D sandTexture;
        public Texture2D rockTexture;
        public Texture2D roadTexture;
        public Texture2D floorTexture;
        public Texture2D wallTexture;
        public Texture2D roofTexture;
        public Texture2D minimap;

        // List of resources
        public List<Building> buildings;
        public int numberOfBuildings;
        public Tree[] trees;
        public byte numberOfBarrels;
        public Item[] items;
        public List<Vegetation> grass;

        // Vegetation map
        public Texture2D vegetationMap;
        public float[,] colorHeightData;

        // Effects
        public Effect graphicEffect;

        // Content manager and graphics device.
        public ContentManager content;
        public GraphicsDevice graphicsDevice;

        // Random generator used to generate randomvalues.
        public Random rnd;

        // Audio variables
        private Vector3 position;
        private Vector3 forward;
        private Vector3 up;
        private Vector3 velocity;

        #endregion

        #region Properties
        
        /// <summary>
        /// Gets or sets the 3D position of the entity.
        /// </summary>
        public Vector3 Position
        {
            get { return position; }
            set { position = value; }
        }

        /// <summary>
        /// Gets or sets which way the entity is facing.
        /// </summary>
        public Vector3 Forward
        {
            get { return forward; }
            set { forward = value; }
        }

        /// <summary>
        /// Gets or sets the orientation of this entity.
        /// </summary>
        public Vector3 Up
        {
            get { return up; }
            set { up = value; }
        }

        /// <summary>
        /// Gets or sets how fast this entity is moving.
        /// </summary>
        public Vector3 Velocity
        {
            get { return velocity; }
            protected set { velocity = value; }
        }

        #endregion

        #region Constructors

        /// <summary>
        /// Create a new map
        /// </summary>
        /// <param name="contentManager"></param>
        /// <param name="graphicsDevice"></param>
        /// <param name="position"></param>
        public IMap(ContentManager contentManager, GraphicsDevice graphicsDevice, Vector3 position)
        {
            // Set the position of the terrain, it will be centered to the (0,0,0) 3D origin point.
            this.worldMatrix = Matrix.CreateTranslation(position);

            // Set the content manager and the graphics device.
            this.content = contentManager;
            this.graphicsDevice = graphicsDevice;

            this.rnd = new Random();
        }

        #endregion

        #region Methods

        // Methods that need to be implemented for a map that inherits from this class.
        public abstract void Update(GameTime gameTime);
        public abstract void Draw(GameTime gameTime, Matrix view, Matrix projection, Matrix viewProj, Vector3 cameraPosition);

        /// <summary>
        /// This method will fill a height 2D array with data from the color information in a 
        /// vegetation map.
        /// </summary>
        public void LoadVegetationHeights()
        {
            // Create arrays.
            this.colorHeightData = new float[129, 129];
            Color[] vegetationMapColors = new Color[129 * 129];

            // Get color data from the vegetation map and store this information
            // in the vegetationMapColors array.
            this.vegetationMap.GetData(vegetationMapColors);

            // Loop through each pixel.
            for (int x = 0; x < 129; x++)
                for (int z = 0; z < 129; z++)
                    this.colorHeightData[x, z] = vegetationMapColors[x + z * 129].R;

        } // End of the LoadVegetationHeights method

        /// <summary>
        /// This method will add vegetation to the terrain according to the information in the
        /// vegetation map.
        /// </summary>
        public void AddVegetation()
        {
            // A local variable to hold height information.
            float height = 0;

            // Loop through the colorHeightData array.
            for (int x = 1; x < 128; x++)
            {
                for (int z = 1; z < 128; z++)
                {
                    // Get the height
                    this.heightMapInfo.GetHeight(new Vector3((x - 64.5f) * 100, 0, (z - 64.5f) * 100), out height);

                    // Add grass or a tree.
                    if (this.colorHeightData[x, z] == 255f)
                    {
                        float randomValue = (float)(rnd.NextDouble() * 100 - 50);
                        this.grass.Add(new Vegetation(new Vector3((x - 64.5f) * 100 + randomValue, height, (z - 64.5f) * 100 + 0), new Vector3(MathHelper.ToRadians(-90f), 0, 0), 1.0f));
                        randomValue = (float)(rnd.NextDouble() * 100 - 50);
                        this.grass.Add(new Vegetation(new Vector3((x - 64.5f) * 100 + randomValue, height, (z - 64.5f) * 100 - 33), new Vector3(MathHelper.ToRadians(-90f), 0, 0), 1.0f));
                        randomValue = (float)(rnd.NextDouble() * 100 - 50);
                        this.grass.Add(new Vegetation(new Vector3((x - 64.5f) * 100 + randomValue, height, (z - 64.5f) * 100 - 66), new Vector3(MathHelper.ToRadians(-90f), 0, 0), 1.0f));

                    }
                    else if (this.colorHeightData[x, z] == 160f)
                    {
                        this.buildings.Add(new Tree(new Vector3((x - 64.5f) * 100, height, (z - 64.5f) * 100), new Vector3(0, 0, 0), 15f));
                    }

                }
            }

            // Trim lists to the correct size
            this.grass.TrimExcess();
            this.buildings.TrimExcess();

            // Copy trees from buildings to the trees array
            this.trees = new Tree[this.buildings.Count - this.numberOfBuildings];
            this.buildings.CopyTo(this.numberOfBuildings, this.trees, 0, this.buildings.Count - this.numberOfBuildings);

        } // End of the AddVegetation method.

        /// <summary>
        /// Helper for drawing the terrain model.
        /// </summary>
        public void DrawTerrain(Matrix view, Matrix projection)
        {

            // Set graphic states before the draw method.
            graphicsDevice.BlendState = BlendState.Opaque;
            graphicsDevice.DepthStencilState = DepthStencilState.Default;
            graphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;

            // Loop through each mesh in the terrain model.
            foreach (ModelMesh mesh in mapModel.Meshes)
            {
                // Use the matrices provided by the camera
                this.graphicEffect.CurrentTechnique = graphicEffect.Techniques["MultiTextured"];
                this.graphicEffect.Parameters["xView"].SetValue(view);
                this.graphicEffect.Parameters["xProjection"].SetValue(projection);
                this.graphicEffect.Parameters["xWorld"].SetValue(worldMatrix);
                this.graphicEffect.Parameters["xTexture0"].SetValue(roadTexture);
                this.graphicEffect.Parameters["xTexture1"].SetValue(sandTexture);
                this.graphicEffect.Parameters["xTexture2"].SetValue(grassTexture);
                this.graphicEffect.Parameters["xTexture3"].SetValue(rockTexture);
                this.graphicEffect.Parameters["xLightDirection"].SetValue(new Vector3(0.25f, -1.0f, -0.25f));
                this.graphicEffect.Parameters["xAmbient"].SetValue(0.1f);

                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    meshPart.Effect = this.graphicEffect;

                }

                foreach (EffectPass pass in this.graphicEffect.CurrentTechnique.Passes)
                {
                    pass.Apply();

                }

                mesh.Draw();

            }

        } // End of the DrawTerrain method.

        /// <summary>
        /// Helper method to draw trees.
        /// </summary>
        /// <param name="view"></param>
        /// <param name="projection"></param>
        public void DrawTrees(Matrix view, Matrix projection, Vector3 cameraPosition)
        {

            // Set depthStencilState and blendState before the loop
            graphicsDevice.DepthStencilState = DepthStencilState.DepthRead;
            graphicsDevice.BlendState = BlendState.AlphaBlend;
            graphicsDevice.RasterizerState = RasterizerState.CullNone;

            // Sort the tree array on the distance to the camera, closest trees should be drawn last.
            Tree.cameraPosition = cameraPosition;
            Array.Sort(this.trees);

            foreach (ModelMesh mesh in this.treeModel.Meshes)
            {
                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    this.graphicsDevice.SetVertexBuffer(meshPart.VertexBuffer, meshPart.VertexOffset);
                    this.graphicsDevice.Indices = meshPart.IndexBuffer;

                    // Set up the rendering effect.
                    Effect effect = meshPart.Effect;

                    effect.CurrentTechnique = effect.Techniques["NoInstancing"];

                    effect.Parameters["View"].SetValue(view);
                    effect.Parameters["Projection"].SetValue(projection);

                    EffectParameter transformParameter = effect.Parameters["World"];

                    // Draw a single instance copy each time around this loop.
                    for (int i = 0; i < this.trees.Length; i++)
                    {
                        transformParameter.SetValue(this.trees[i].world);

                        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                        {
                            pass.Apply();

                            this.graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0,
                                                                    meshPart.NumVertices, meshPart.StartIndex,
                                                                    meshPart.PrimitiveCount);
                        }
                    }
                }
            }

            // Reset states to default values
            graphicsDevice.BlendState = BlendState.Opaque;
            graphicsDevice.DepthStencilState = DepthStencilState.Default;
            graphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;

        } // End of the DrawTrees method

        /// <summary>
        /// Helper method to draw grass.
        /// </summary>
        /// <param name="view"></param>
        /// <param name="projection"></param>
        public void DrawGrass(Matrix view, Matrix projection, GameTime gameTime)
        {
            // Set depthStencilState and blendState before the loop
            graphicsDevice.DepthStencilState = DepthStencilState.DepthRead;
            graphicsDevice.BlendState = BlendState.AlphaBlend;
            graphicsDevice.RasterizerState = RasterizerState.CullNone;

            float time = 0;

            // Time is scaled down to make things wave in the wind more slowly.
            if (gameTime != null)
                time = (float)gameTime.TotalGameTime.TotalSeconds * 0.333f;

            foreach (ModelMesh mesh in this.grassModel.Meshes)
            {
                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    this.graphicsDevice.SetVertexBuffer(meshPart.VertexBuffer, meshPart.VertexOffset);
                    this.graphicsDevice.Indices = meshPart.IndexBuffer;

                    // Set up the rendering effect.
                    Effect effect = meshPart.Effect;

                    effect.CurrentTechnique = effect.Techniques["NoInstancing"];

                    effect.Parameters["View"].SetValue(view);
                    effect.Parameters["Projection"].SetValue(projection);
                    effect.Parameters["WindTime"].SetValue(time);

                    EffectParameter transformParameter = effect.Parameters["World"];

                    // Draw a single instance copy each time around this loop.
                    for (int i = 0; i < grass.Count; i++)
                    {
                        // Set the world parameter.
                        transformParameter.SetValue(grass[i].world);

                        foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                        {
                            pass.Apply();

                            this.graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0,
                                                                    meshPart.NumVertices, meshPart.StartIndex,
                                                                    meshPart.PrimitiveCount);
                        }
                    }
                }
            }

            // Reset states to default values
            graphicsDevice.BlendState = BlendState.Opaque;
            graphicsDevice.DepthStencilState = DepthStencilState.Default;
            graphicsDevice.RasterizerState = RasterizerState.CullCounterClockwise;

        } // End of the DrawGrass method

        /// <summary>
        /// Draw all the barrels in the barrels list.
        /// </summary>
        /// <param name="view"></param>
        /// <param name="projection"></param>
        public void DrawItems(Matrix view, Matrix projection)
        {
            // Loop through each mesh in the barrel model.
            foreach (ModelMesh mesh in barrelModel.Meshes)
            {
                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    this.graphicsDevice.SetVertexBuffer(meshPart.VertexBuffer, meshPart.VertexOffset);
                    this.graphicsDevice.Indices = meshPart.IndexBuffer;

                    // Set up the rendering effect.
                    Effect effect = meshPart.Effect;

                    effect.CurrentTechnique = effect.Techniques["NoInstancing"];

                    effect.Parameters["View"].SetValue(view);
                    effect.Parameters["Projection"].SetValue(projection);

                    EffectParameter transformParameter = effect.Parameters["World"];

                    // Draw a single instance copy each time around this loop.
                    for (int i = 0; i < items.Length; i++)
                    {
                        if (items[i] != null)
                        {
                            transformParameter.SetValue(items[i].world);

                            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                            {
                                pass.Apply();

                                this.graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0,
                                                                     meshPart.NumVertices, meshPart.StartIndex,
                                                                     meshPart.PrimitiveCount);
                            }
                        }
                    }
                }
            }

        } // End of DrawItems method

        /// <summary>
        /// Check for a collision with buildings.
        /// </summary>
        /// <param name="boundingBox"></param>
        /// <returns>A custom bounding box with a name of none, wall, roof or floor.</returns>
        public CustomBoundingBox CollisionWithBuildings(BoundingBox boundingBox)
        {
            CustomBoundingBox collisionBox = new CustomBoundingBox("none", new BoundingBox(Vector3.Zero, Vector3.Zero));

            foreach (Building building in buildings)
            {
                // If there is a collision with the whole building then continue.
                if (building.boundingBoxesList[0].boundingBox.Intersects(boundingBox))
                {
                    // Check for a collision with each custom bounding box in the house.
                    for (int i = 1; i < building.boundingBoxesList.Count; i++)
                    {
                        if (building.boundingBoxesList[i].boundingBox.Intersects(boundingBox))
                        {
                            // Set the bounding box to return to the current bounding box.
                            collisionBox = building.boundingBoxesList[i];

                            // Break from the loop
                            break;
                        }
                    }
                }
            }

            // Return the collisionBox
            return collisionBox;

        } // End of the CollisionWithBuildings method.

        /// <summary>
        /// Check for collision with buildings (float).
        /// </summary>
        /// <param name="ray">A ray for a bullet</param>
        /// <returns>A float? for the distance where the collision occured</returns>
        public float? CollisionWithBuildings(Ray ray)
        {
            float? collisionWithBuilding = float.MaxValue;

            foreach (Building building in buildings)
            {

                // If there is a collision with the house then continue.
                if (building.boundingBoxesList[0].boundingBox.Intersects(ray) != null)
                {
                    // Check for a collision with each custom bounding box in the house.
                    for (int i = 1; i < building.boundingBoxesList.Count; i++)
                    {
                        if (building.boundingBoxesList[i].boundingBox.Intersects(ray) != null)
                        {
                            float? minDistance = building.boundingBoxesList[i].boundingBox.Intersects(ray);

                            // A collision has happened.
                            if(minDistance < collisionWithBuilding)
                                collisionWithBuilding = minDistance;

                        }
                    }
                }
            }

            // Return the collision float? value.
            return collisionWithBuilding;

        } // End of the CollisionWithBuildings method

        /// <summary>
        /// Check if an attacker has collected a item.
        /// </summary>
        /// <param name="player"></param>
        public byte CollisionCheckItemOnServer(PlayerProfile player, AudioManager audioManager)
        {
            byte barrelToRemove = 100;

            // For each gamer, check for collision with barrels
            // if the player is on the attacking team.
            if (player.isAttacking == true)
            {
                for (int i = 0; i < items.Length; i++)
                {
                    // Only check for collision if there is a barrel.
                    if (items[i] != null)
                    {
                        foreach (BoundingSphere sphere in player.soldier.boundingSpheres)
                        {
                            // Check if there is a collision between player and barrel.
                            if (sphere.Intersects(items[i].boundingSpheres[0]))
                            {
                                // Update the number of barrels collected for the player
                                player.collectedBarrels++;

                                // Set position data for the sound and play the sound.
                                this.Position = items[i].boundingSpheres[0].Center;
                                this.Forward = Vector3.Zero;
                                this.Up = Vector3.Up;
                                this.Velocity = Vector3.Zero;
                                audioManager.Play3DSound("barrelCollection", false, this);

                                // Remove the barrel on the server.
                                items[i] = null;
                                this.numberOfBarrels--;

                                // Set the index number for the barrel to remove.
                                barrelToRemove = (byte)i;

                                // Break from the loop
                                break;
                            }
                        }
                    }
                }
            }

            // Return the barrel to remove
            return barrelToRemove;

        } // End of the CollisionCheckBarrelOnServer method

        /// <summary>
        /// Check if an attacker has collected an item in a local network game.
        /// </summary>
        /// <param name="player"></param>
        public void CollisionCheckItemSplitScreen(LocalPlayerProfile player, AudioManager audioManager)
        {
            // For each gamer, check for collision with nuclear waste barrels
            // if the player is on the attacking team.
            if (player.isAttacking == true)
            {
                for (int i = 0; i < items.Length; i++)
                {
                    // Only check for collision if there is a barrel.
                    if (items[i] != null)
                    {
                        foreach (BoundingSphere sphere in player.soldier.boundingSpheres)
                        {
                            // Check if there is a collision between player and barrel.
                            if (sphere.Intersects(items[i].boundingSpheres[0]))
                            {
                                // Update the number of barrels collected for the player
                                player.collectedBarrels++;

                                // Set position data for the sound and play the sound.
                                this.Position = items[i].boundingSpheres[0].Center;
                                this.Forward = Vector3.Zero;
                                this.Up = Vector3.Up;
                                this.Velocity = Vector3.Zero;
                                audioManager.Play3DSound("barrelCollection", false, this);

                                // Remove the barrel on the server and play the sound.
                                items[i] = null;
                                this.numberOfBarrels--;

                                // Break from the loop
                                break;
                            }
                        }
                    }
                }
            }

        } // End of the CollisionCheckBarrelsSplitScreen method

        /// <summary>
        /// Check for barrels to remove.
        /// </summary>
        public void CheckForItemToRemove(byte itemToRemove, AudioManager audioManager)
        {

            if (items[itemToRemove] != null)
            {

                // Set position data for the sound and play the sound.
                this.Position = items[itemToRemove].boundingSpheres[0].Center;
                this.Forward = Vector3.Zero;
                this.Up = Vector3.Up;
                this.Velocity = Vector3.Zero;
                audioManager.Play3DSound("barrelCollection", false, this);

                // Remove the barrel.
                items[itemToRemove] = null;
                this.numberOfBarrels--;

            }

        } // End of the CheckForItemToRemove method.

        #endregion

    } // End of class

} // End of namespace
