using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Xml;
using System.Windows.Forms;
using System.Threading;
using DataStructures;

namespace Sentience
{
    public delegate void ConsoleHandler(String e);

    /// <summary>
    /// The map class is the main mediator between the components of the engine. It contains the main
    /// graph data structure, the entities, teams, blocks and anything else that makes up the non-application
    /// side of the program.
    /// </summary>
    public class Map
    {
        public const Char mapDataSplit = '.';
        public const Char mapDataLine = '|';

        public event ConsoleHandler Console;

        public Map()
        {
            // Instantiate all the objects.. 
            Graph = new Graph<IBlock>();
            Entities = new List<Entity>();
            Teams = new List<Team>();
            Blocks = new List<IBlock>();
            Behaviors = new List<IBehavior>();

            // Set some properties..
            Size = Defaults.DefaultMapSize;

            NodeSize = Defaults.DefaultNodeSize;
            NodeSpacing = Defaults.DefaultNodeSpacing;
            NodeOffset = Defaults.DefaultNodeOffset;
            Offset = Size.Empty;

            // Add the usable blocks for this map and pass itself to them..
            Blocks.Add(new FloorBlock(this));
            Blocks.Add(new WallBlock(this));
            Blocks.Add(new TeleporterBlock(this));
            Blocks.Add(new GoalBlock(this));
            Blocks.Add(new FireBlock(this));

            // Add the usable behaviors for this map and pass itself to them..
            Behaviors.Add(new Void(this));
            Behaviors.Add(new Guard(this));
            Behaviors.Add(new GoalSeeker(this));
            Behaviors.Add(new Zombie(this));

            randomNumber = new Random();
        }

        private Random randomNumber;

        public String Name { get; set; }
        public String File { get; set; }

        public String RawData { get; set; }

        public int NodeSize { get; set; }
        public int NodeSpacing { get; set; }
        public Size NodeOffset { get; set; }
        public Size Offset { get; set; }

        public Size Size { get; set; }

        public bool ShowConnections { get; set; }
        public bool ShowNodes { get; set; }
        public bool ShowPaths { get; set; }
        public bool ShowEntityData { get; set; }
        public bool ShowViewRadius { get; set; }

        public Graph<IBlock> Graph { get; set; }
        public List<Team> Teams { get; set; }
        public List<Entity> Entities { get; set; }
        public List<IBlock> Blocks { get; set; }
        public List<IBehavior> Behaviors { get; set; }

        /// <summary>
        /// Create the map graph from scratch or from imported map data.
        /// </summary>
        public void Create()
        {
            Graph = new Graph<IBlock>();

            // If the map is being loaded from imported data..
            if (RawData != null)
            {
                foreach (Team Team in Teams)
                    Blocks.Add(new SpawnBlock(this, Team));

                String[] gridRows = RawData.Split(new Char[] { mapDataSplit }, StringSplitOptions.RemoveEmptyEntries);

                int j = 0;
                int k = 0;

                for (int i = 0; i < gridRows.Length; i++)
                {
                    // Find the block from string identifer.
                    IBlock gridBlock = FindBlock(gridRows[i]);

                    // If the block cannot be found, throw a loading exception as this map contains invalid data.
                    if (gridBlock == null)
                        throw new Exception(String.Format("The block identifier '{0}' was not found.", gridRows[i]));

                    Graph.Add(new PointF((j * NodeSize) + NodeOffset.Width, (k * NodeSize) + NodeOffset.Height), gridBlock);
                    j++;

                    if (j >= Size.Width)
                    {
                        j = 0;
                        k++;
                    }
                }
            }
            // Else, create a new map..
            else
            {
                for (int i = 0; i < Size.Height; i++)
                    for (int j = 0; j < Size.Width; j++)
                        Graph.Add(new PointF((j * NodeSize) + NodeOffset.Width, (i * NodeSize) + NodeOffset.Height), FindBlock("f"));

                Console(String.Format("Map: Blank Map Created ({0} x {1})", Size.Width, Size.Height));
            }

            // Compute the connections between the nodes.
            ComputeConnections();

            // Create and spawn the entities.
            UpdateEntities();
            SpawnEntities(null);
        }

        /// <summary>
        /// Spawn a specific entity (or an entire team) at a random spawn point.
        /// </summary>
        /// <param name="spawnEntity">The entity to spawn. (NULL to spawn entire team)</param>
        public void SpawnEntities(Entity spawnEntity)
        {
            foreach (Entity Entity in Entities)
            {
                if (spawnEntity != null)
                    if (Entity != spawnEntity)
                        continue;

                Entity.CurrentNode = FindSpawn(Entity.Team);

                if (!Entity.HasSpawned())
                {
                    Console(String.Format("Warning: No spawn found for {0}", Entity.Name));
                    continue;
                }

                Entity.Spawn = DateTime.Now;

                Entity.Position = Entity.CurrentNode.Location;
                Entity.State = States.Idle;
                Entity.PerformBehavior();

                Console(String.Format("Map: {0} spawned at {1}", Entity.Name, Entity.CurrentNode.Data.Name));
            }
        }


        /// <summary>
        /// Finds a random spawn point for the supplied team and returns it.
        /// </summary>
        /// <param name="teamSpawn">The team to find a spawn point for.</param>
        /// <returns>A spawn node.</returns>
        public Node<IBlock> FindSpawn(Team teamSpawn)
        {
            List<Node<IBlock>> spawnPoints = new List<Node<IBlock>>();

            foreach (Node<IBlock> Node in Graph.Nodes)
                if ((Node.Data is SpawnBlock))
                    if ((Node.Data as SpawnBlock).Team == teamSpawn)
                        spawnPoints.Add(Node);

            if (spawnPoints.Count <= 0)
                return null;

            return spawnPoints[randomNumber.Next(spawnPoints.Count)];
        }

        /// <summary>
        /// Finds a random goal point on the map and returns it.
        /// </summary>
        /// <returns>A goal node.</returns>
        public Node<IBlock> FindGoal()
        {
            List<Node<IBlock>> goalPoints = new List<Node<IBlock>>();

            foreach (Node<IBlock> Node in Graph.Nodes)
                if ((Node.Data is GoalBlock))
                    goalPoints.Add(Node);

            if (goalPoints.Count <= 0)
                return null;

            return goalPoints[randomNumber.Next(goalPoints.Count)];
        }


        /// <summary>
        /// Makes sure the number of entities in a team is correct and spawns any entities
        /// that haven't been spawned yet.
        /// </summary>
        public void UpdateEntities()
        {
            foreach (Team Team in Teams)
            {
                // Get the number of entities in this team.
                int numEntities = Team.GetMemberCount(Entities);

                // If the number is greater than the current count, create and spawn the remainder..
                if (Team.Members > numEntities)
                {
                    for (int i = 0; i < Team.Members - numEntities; i++)
                    {
                        Entity newEntity = new Entity(Entities.Count, Team);
                        Entities.Add(newEntity);

                        SpawnEntities(newEntity);
                    }
                }
                // Else if, the number is less, remove the remainder..
                else if (Team.Members < numEntities)
                {
                    for (int i = 0; i < numEntities - Team.Members; i++)
                        Entities.Remove(Entities.Find(delegate(Entity findEntity) { return findEntity.Team == Team; }));
                }

                // Spawn any entities that haven't been spawned yet..
                foreach (Entity Entity in Entities)
                    if (Entity.Team == Team && !Entity.HasSpawned())
                        SpawnEntities(Entity);
            }

            // Remove any entities that have no team assigned to them (e.g. the team has just been removed)
            for (int i = 0; i < Entities.Count; i++)
                if (Entities[i].Team == null)
                    Entities.RemoveAt(i);

            Console(String.Format("Map: Entity Data Updated ({0} Entities in {1} Teams)", Entities.Count, Teams.Count));
        }

        /// <summary>
        /// Computes all the connections between each node of the graph.
        /// </summary>
        public void ComputeConnections()
        {
            // Isolate everything.
            foreach (Node<IBlock> Node in Graph.Nodes)
                Graph.Isolate(Node);

            for (int i = 0; i < Graph.Nodes.Count; i++)
            {
                for (int l = 0; l < Graph.Nodes.Count; l++)
                {
                    float nodeCost;

                    // If the two blocks are diagonal to each other, get a diagonal cost value.
                    if (Graph.Nodes[i].Location.X != Graph.Nodes[l].Location.X
                        && Graph.Nodes[i].Location.Y != Graph.Nodes[l].Location.Y)
                        nodeCost = Defaults.GraphDiagonalCost;
                    // Else If, the two blocks are side-by-side to each other, get a linear cost value.
                    else
                        nodeCost = Defaults.GraphLinearCost;

                    // Calculate the distance between the nodes.
                    float nodeDistance = Graph.Nodes[i].DistanceTo(Graph.Nodes[l]);

                    // If it is less than the set amount, connect them.
                    if (nodeDistance < Properties.Settings.Default.ConnectionDistance && i != l)
                        Graph.Connect(Graph.Nodes[i], Graph.Nodes[l], nodeCost);
                }
            }

            // Isolate any blockers from the graph (e.g. Walls)
            foreach (Node<IBlock> Node in Graph.Nodes)
                if (Node.Data.Blocker)
                    Graph.Isolate(Node);

            // Connect up all the teleporter nodes and give them a cost of 0.
            foreach (Node<IBlock> NodeA in Graph.Nodes)
            {
                if ((NodeA.Data is TeleporterBlock))
                    foreach (Node<IBlock> NodeB in Graph.Nodes)
                    {
                        if ((NodeB.Data is TeleporterBlock) && NodeB != NodeA)
                        {
                            Graph.Connect(NodeA, NodeB, 0);
                            Graph.Connect(NodeB, NodeA, 0);
                        }
                    }
            }

            Console(String.Format("Map: Successfully Computed {0} Node Connections", Graph.Connections.Count));
        }

        /// <summary>
        /// Output a string to the console.
        /// </summary>
        /// <param name="output">The string to output.</param>
        public void Report(String output)
        {
            Console(output);
        }

        /// <summary>
        /// Output an action to the console. (e.g. "Guard killed Goal-Seeker").
        /// </summary>
        /// <param name="subjectA">The first subject.</param>
        /// <param name="verb">The verb.</param>
        /// <param name="subjectB">The second subject.</param>
        public void Report(IName subjectA, String verb, IName subjectB)
        {
            Console(String.Format("{0} {1} {2}", subjectA.Name, verb, subjectB.Name));
        }

        /// <summary>
        /// Applies the changes made in the Designer to the main graph.
        /// </summary>
        /// <param name="mapGrid">The DataGridView to get the data from.</param>
        public void ApplyDesign(DataGridView mapGrid)
        {
            int nodeID = 0;

            for (int j = 0; j < mapGrid.RowCount; j++)
            {
                for (int i = 0; i < mapGrid.ColumnCount; i++)
                {
                    // Get the node to modify..
                    Node<IBlock> modNode = Graph.FindNodeFromID(nodeID);
                    // Find the block from the color of the cell. Because of this, all blocks *MUST* have
                    // a unique color.
                    IBlock newBlock = FindBlock(mapGrid[i, j].Style.BackColor);

                    // If any entities reside on a blocker, set them to not spawned.
                    if (newBlock.Blocker)
                        foreach (Entity Entity in Entities)
                            if (Entity.CurrentNode == modNode)
                            {
                                Entity.CurrentNode = null;
                                Entity.Waypoints.Clear();
                                Console(String.Format("Error: {0} was on inaccessible block and has been respawned.", Entity.Name));
                            }

                    modNode.Data = newBlock;

                    // Isolate it if it is a blocker.
                    if (newBlock.Blocker)
                        Graph.Isolate(modNode);

                    nodeID++;
                }
            }

            // Update the graph to the new state.
            ComputeConnections();
            UpdateEntities();
        }

        /// <summary>
        /// Removes a team and any associated entities and blocks.
        /// </summary>
        /// <param name="removeTeam">The team to remove.</param>
        public void RemoveTeam(Team removeTeam)
        {
            List<Entity> removeEntities = new List<Entity>();

            // Remove linked entities.
            foreach (Entity Entity in Entities)
                if (Entity.Team == removeTeam)
                    removeEntities.Add(Entity);

            foreach (Entity Entity in removeEntities)
                Entities.Remove(Entity);

            // Remove linked spawn blocks.
            for (int i = 0; i < Blocks.Count; i++)
			{
                if (Blocks[i] is SpawnBlock)
                    if ((Blocks[i] as SpawnBlock).Team == removeTeam)
                        Blocks.Remove(Blocks[i]);
			}

            // Remove linked spawn nodes in the graph.
            foreach (Node<IBlock> Node in Graph.Nodes)
                if (Node.Data is SpawnBlock)
                    if ((Node.Data as SpawnBlock).Team == removeTeam)
                        Node.Data = Blocks[0];

            Teams.Remove(removeTeam);

            Console(String.Format("{0} Removed", removeTeam.Name));
        }

        /// <summary>
        /// Find a block based on its string identifier.
        /// </summary>
        /// <param name="id">The string to find.</param>
        /// <returns>The IBlock object.</returns>
        public IBlock FindBlock(String id)
        {
            foreach (IBlock Block in Blocks)
                if (Block.ID == id)
                    return Block;

            return null;
        }

        /// <summary>
        /// Find an entity based on its ID.
        /// </summary>
        /// <param name="id">The ID to find.</param>
        /// <returns>The found entity.</returns>
        public Entity FindEntity(int id)
        {
            foreach (Entity Entity in Entities)
                if (Entity.ID == id)
                    return Entity;

            return null;
        }

        /// <summary>
        /// Find an team based on its name.
        /// </summary>
        /// <param name="id">The string to find.</param>
        /// <returns>The found team.</returns>
        public Team FindTeam(String name)
        {
            foreach (Team Team in Teams)
                if (Team.Name == name)
                    return Team;

            return null;
        }

        /// <summary>
        /// Find a block based on its color identifier.
        /// </summary>
        /// <param name="color">The color to find.</param>
        /// <returns>The IBlock object.</returns>
        public IBlock FindBlock(Color color)
        {
            foreach (IBlock Block in Blocks)
                if (Block.Color == color)
                    return Block;

            return null;
        }

        /// <summary>
        /// Checks if the map is empty of anything other than floor blocks. 
        /// </summary>
        /// <returns>True if empty, false if not.</returns>
        public bool IsMapBlank()
        {
            foreach (Node<IBlock> Node in Graph.Nodes)
                if (!(Node.Data is FloorBlock))
                        return false;

            return true;
        }

        /// <summary>
        /// Draws the map.
        /// </summary>
        /// <param name="graphics">The graphics object to draw with.</param>
        public void Draw(Graphics graphics)
        {
            // Nodes..
            if (ShowNodes)
            {
                foreach (Node<IBlock> Node in Graph.Nodes)
                {
                    Color nodeColor = Color.LightGray;

                    if (ShowPaths)
                        foreach (Entity Entity in Entities)
                            foreach (Node<IBlock> Waypoint in Entity.Waypoints)
                                if (Waypoint == Node)
                                    nodeColor = Entity.Team.Color;

                    Drawing.Box(graphics, Node.Location, "", new Size(NodeSize - NodeSpacing, NodeSize - NodeSpacing), nodeColor, Node.Data.Color);
                }
            }
            
            // Connections..
            if (ShowConnections)
            {
                foreach (Node<IBlock> Node in Graph.Nodes)
                    foreach (Node<IBlock> Connection in Graph.FindChildNodes(Node))
                        if ((Node.Data is TeleporterBlock) && (Connection.Data is TeleporterBlock))
                            Drawing.Line(graphics, Node.Location, Connection.Location, Node.Data.Color);
                        else
                            Drawing.Line(graphics, Node.Location, Connection.Location, Color.Silver);
            }

            // Entities..
            foreach (Entity Entity in Entities)
                if (Entity.HasSpawned())
                    Entity.Draw(graphics, ShowEntityData, ShowViewRadius);
        }

        /// <summary>
        /// Computes the A* path for an entity to get to a specific node.
        /// </summary>
        /// <param name="entity">The entity to move.</param>
        /// <param name="targetNode">The target block.</param>
        public void Move(Entity entity, Node<IBlock> targetNode)
        {
            List<Node<IBlock>> waypointNodes = Graph.Path(PathType.AStar, Properties.Settings.Default.Heuristic, entity.CurrentNode, targetNode, Properties.Settings.Default.FixPathErrors);

            targetNode.Selected = true;

            entity.Waypoints.Clear();
            entity.Waypoints.AddRange(waypointNodes);
        }

        /// <summary>
        /// Kills a specific entity.
        /// </summary>
        /// <param name="entity">The entity to kill.</param>
        /// <param name="killer">The killer of the entity.</param>
        public void Kill(Entity entity, IName killer)
        {
            entity.State = States.Idle;
            entity.Waypoints.Clear();
            Report(killer, "killed", entity);
            SpawnEntities(entity);
        }

        /// <summary>
        /// Performs simple checks to make sure the map data is valid.
        /// </summary>
        /// <param name="map">The map to check.</param>
        public static void Checker(Map map)
        {
            // Map Dimensions
            if (map.Size.Width > Defaults.MaxMapSize.Width || map.Size.Height > Defaults.MaxMapSize.Height)
                throw new Exception("Maximum Size Exceeded");

            if (map.Size.Width < Defaults.MinMapSize.Width || map.Size.Height < Defaults.MinMapSize.Height)
                throw new Exception("Minimum Size Exceeded");

            if (map.Teams.Count > Defaults.MaxTeams)
                throw new Exception("Team Limit Exceeded");

            if (map.RawData.Split(mapDataSplit).Length != (map.Size.Width * map.Size.Height) + 1)
                throw new Exception("Data Block Count is Invalid");

            // Team Checks
            foreach (Team Team in map.Teams)
            {
                if (Team.GetMemberCount(map.Entities) > Defaults.MaxTeamSize)
                    throw new Exception("Team '" + Team.Name + "' Exceeds Member Limit");

                foreach (Team NameCheck in map.Teams)
                    if (Team.Name == NameCheck.Name && Team != NameCheck)
                        throw new Exception("Duplicated Team Name '" + Team.Name + "'");
            }
        }

        /// <summary>
        /// Loads XML map data from a file.
        /// </summary>
        /// <param name="readPath">The path of the file to load.</param>
        /// <returns>The new map object.</returns>
        public static Map Load(String readPath)
        {
            Map newMap = new Map();
            newMap.Teams.Clear();

            XmlDocument xmlRead = new XmlDocument();

            xmlRead.Load(readPath);

            XmlNode xmlName = xmlRead.GetElementsByTagName("name")[0];
            XmlNode xmlSize = xmlRead.GetElementsByTagName("size")[0];
            XmlNodeList xmlTeams = xmlRead.GetElementsByTagName("team");

            XmlNode xmlData = xmlRead.GetElementsByTagName("data")[0];

            newMap.Name = xmlName.InnerText;
            newMap.File = readPath;

            newMap.Size = new Size(int.Parse(xmlSize.Attributes["width"].Value), 
                int.Parse(xmlSize.Attributes["height"].Value));

            foreach (XmlNode xmlNode in xmlTeams)
            {
                Color teamColor = Color.FromName(xmlNode.Attributes["color"].Value);

                if (!teamColor.IsKnownColor)
                    teamColor = ColorTranslator.FromHtml("#" + xmlNode.Attributes["color"].Value);

                Team newTeam = new Team(int.Parse(xmlNode.Attributes["id"].Value), xmlNode.Attributes["name"].Value,
                    teamColor);

                newTeam.Members = int.Parse(xmlNode.Attributes["members"].Value);
                int newBehavior = int.Parse(xmlNode.Attributes["behavior"].Value);

                foreach (IBehavior Behavior in newMap.Behaviors)
                {
                    if (Behavior.ID == newBehavior)
                    {
                        newTeam.Behavior = Behavior;
                        break;
                    }
                }

                if (newTeam.Behavior == null)
                    newTeam.Behavior = newMap.Behaviors[0];
                
                newMap.Teams.Add(newTeam);
            }

            newMap.RawData = xmlData.InnerText;

            return newMap;
        }

        /// <summary>
        /// Writes map data to a specific file.
        /// </summary>
        /// <param name="map">The map to write.</param>
        /// <param name="writePath">The file to save the data in.</param>
        public static void Write(Map map, String writePath)
        {
            XmlTextWriter xmlWrite = new XmlTextWriter(writePath, null);
            xmlWrite.Formatting = Formatting.Indented;

            xmlWrite.WriteStartDocument();
            xmlWrite.WriteStartElement("map");

                xmlWrite.WriteStartElement("name");
                    xmlWrite.WriteString(map.Name);
                xmlWrite.WriteEndElement();

                xmlWrite.WriteStartElement("size");
                    xmlWrite.WriteAttributeString("width", map.Size.Width.ToString());
                    xmlWrite.WriteAttributeString("height", map.Size.Height.ToString());
                xmlWrite.WriteEndElement();

                xmlWrite.WriteStartElement("teams");

                foreach (Team Team in map.Teams)
                {
                    int teamMembers = 0;

                    xmlWrite.WriteStartElement("team");

                        xmlWrite.WriteAttributeString("id", Team.ID.ToString());
                        xmlWrite.WriteAttributeString("name", Team.Name);

                        foreach (Entity Entity in map.Entities)
                            if (Entity.Team == Team)
                                teamMembers++;

                        xmlWrite.WriteAttributeString("members", teamMembers.ToString());
                        xmlWrite.WriteAttributeString("behavior", Team.Behavior.ID.ToString());
                        xmlWrite.WriteAttributeString("color", Team.Color.Name);

                    xmlWrite.WriteEndElement();
                }

                xmlWrite.WriteEndElement();

                xmlWrite.WriteStartElement("data");

                    StringBuilder mapData = new StringBuilder();

                    foreach (Node<IBlock> Node in map.Graph.Nodes)
                        mapData.Append(Node.Data.ID + mapDataSplit);

                    xmlWrite.WriteString(mapData.ToString());

                xmlWrite.WriteEndElement();

            xmlWrite.WriteEndElement();
            xmlWrite.WriteEndDocument();

            xmlWrite.Close();
        }
    }
}
