#region Using directives

using System;
using System.IO;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;
using System.Drawing;
using System.Windows.Forms;
using System.Xml;



#endregion


namespace PunkMan.Shared
{
    /// <summary>
    /// Map cell container
    /// </summary>
    public class Map
    {
        /// <summary>
        /// Type of box side
        /// </summary>
        private enum BoxSide{
            Bottom = 0, Front = 1, Right = 2, Back = 3, Left = 4, Top = 5
        }

        /// <summary>
        /// Vertex buffer for unit cube
        /// </summary>
        private VertexBuffer vertexBuffer = null;

        /// <summary>
        /// Direct3D device
        /// </summary>
        private Device dev = null;
        /// <summary>
        /// Real container
        /// </summary>
        private MapCell[,] data = null;
        /// <summary>
        /// Access real container of MapCell
        /// </summary>
        /// <value></value>
        public MapCell[,] Data
        {
            get { return data; }
            set { data = value; }
        }

        /// <summary>
        /// width of map
        /// </summary>
        private int dataWidth = 0;
        /// <summary>
        /// height of map
        /// </summary>
        private int dataHeight = 0;
        /// <summary>
        /// width of map
        /// </summary>
        /// <value></value>
        public int DataWidth
        {
            get { return dataWidth; }
            //set { dataWidth = value; }
        }

        /// <summary>
        /// height of map
        /// </summary>
        /// <value></value>
        public int DataHeight
        {
            get { return dataHeight; }
        }

        /// <summary>
        /// texture manager object
        /// </summary>
        private TextureManager texMgr;


        /// <summary>
        /// mighty constructor
        /// </summary>
        /// <param name="dev">Direct3D device</param>
        /// <param name="mgr">texture manager object</param>
        /// <param name="levelPath">path of map file</param>
        public Map(Device dev, TextureManager mgr, string levelPath)
        {
            this.texMgr = mgr;
            this.dev = dev;
            //dev.DeviceReset += new EventHandler(this.CreateBuffer);
            vertexBuffer = new VertexBuffer(typeof(CustomVertex.PositionNormalTextured), 24, dev, Usage.WriteOnly,
                CustomVertex.PositionNormalTextured.Format, Pool.Default);
            vertexBuffer.Created += new EventHandler(OnCreateBuffer);
            OnCreateBuffer(vertexBuffer, null);
            
            LoadXML(levelPath);
        }

        /// <summary>
        /// Create empty map
        /// </summary>
        /// <param name="width">width of map</param>
        /// <param name="height">height of map</param>
        public Map(int width, int height)
        {
            dev = null;
            dataWidth = width;
            dataHeight = height;
            data = new MapCell[width, height];
            for (int x = 0; x < width; x++)
                for (int y = 0; y < height; y++)
                    data[x, y] = new MapCell(false, 0);
            data[0, 0].Entity = MapCell.EntityType.Start;
            data[dataWidth - 1, dataHeight - 1].Entity = MapCell.EntityType.End;

            
        }

        /// <summary>
        /// Load map form file
        /// </summary>
        /// <param name="fileName">map file path</param>
        public void Load(string fileName)
        {
            BinaryReader reader = new BinaryReader(new FileStream(fileName, FileMode.Open));
            
            dataWidth = reader.ReadInt32();
            dataHeight = reader.ReadInt32();
            data = new MapCell[dataWidth, dataHeight];
            
            for (int x = 0; x < dataWidth; x++)
                for (int y = 0; y < dataHeight; y++)
                {
                    data[x, y] = new MapCell();
                    //byte w = reader.ReadByte();
                    data[x, y].Walkable = reader.ReadBoolean();
                    data[x, y].Index = reader.ReadInt32();
                    data[x, y].Entity = (MapCell.EntityType)reader.ReadInt32();
                }
            reader.Close();
        }

		/// <summary>
		/// XML Format of map exception
		/// </summary>
		public class FormatException : ApplicationException
		{
			/// <summary>
			/// Dummy constructor with base message "Wrong map file format"
			/// </summary>
			public FormatException() : base("Wrong map file format"){}
			/// <summary>
			/// More sophisticated consturctor. Sets message.
			/// </summary>
			/// <param name="message">Description of exception</param>
			public FormatException(string message) : base(message){}
		}

		/// <summary>
		/// Returns integer value of XmlNode attribute 
		/// </summary>
		/// <param name="node">The XML node</param>
		/// <param name="attrName">Name of attribute to be returned</param>
		/// <returns>Value of attribute</returns>
		private int GetAttrInt(XmlNode node, string attrName)
		{
			XmlAttribute attr = node.Attributes[attrName];
			if (attr == null) 
				throw new FormatException("Attribute " + attrName + " in node " + node.Name + " not found.");
			return Int32.Parse(attr.Value);
		}

		/// <summary>
		/// Loads map from XML file
		/// </summary>
		/// <param name="fileName">Path to XML file</param>
		public void LoadXML(string fileName)
		{
			int x,y;
			XmlDocument xml = new XmlDocument();
			xml.Load(fileName);

			XmlElement el = xml.DocumentElement;

			// load header
			if (el.Name.CompareTo("map") != 0)
			{
				throw new FormatException();
			}
			dataWidth = Int32.Parse(el.Attributes["width"].Value);
			dataHeight = Int32.Parse(el.Attributes["height"].Value);

			data = new MapCell[dataWidth,dataHeight];
			for (x = 0; x < dataWidth; x++)
				for (y = 0; y < dataHeight; y++)
					data[x,y] = new MapCell();


			foreach (XmlNode node in el.ChildNodes)
			{
				switch (node.Name)
				{
					case "playerinfo":
						foreach (XmlNode node2 in node.ChildNodes)
						{
							switch (node2.Name)
							{
								case "start":
									x = GetAttrInt(node2,"x");
									y = GetAttrInt(node2,"y");
									if (x < 0 || x >= dataWidth || y < 0 || y >= dataHeight)
									{
										throw new FormatException("Player start coordinates are out of map range.");
									}
									data[x,y].Entity = MapCell.EntityType.Start;
									break;

								case "end":
									x = GetAttrInt(node2,"x");
									y = GetAttrInt(node2,"y");
									if (x < 0 || x >= dataWidth || y < 0 || y >= dataHeight)
									{
										throw new FormatException("Player end coordinates are out of map range.");
									}
									data[x,y].Entity = MapCell.EntityType.End;
									break;

							}
						}
						break;

					case "monsters":
						foreach (XmlNode node2 in node.ChildNodes)
						{
							if (node2.Name.CompareTo("monster") != 0)
								throw new FormatException();
							x = GetAttrInt(node2,"x");
							y = GetAttrInt(node2,"y");
							data[x,y].Entity = MapCell.EntityType.Monster;
						}
						break;

					case "bottles":
						foreach (XmlNode node2 in node.ChildNodes)
						{
							switch (node2.Name)
							{
								case "rum":
									x = GetAttrInt(node2,"x");
									y = GetAttrInt(node2,"y");
									data[x,y].Entity = MapCell.EntityType.Rum;
									break;

								case "absinth":
									x = GetAttrInt(node2,"x");
									y = GetAttrInt(node2,"y");
									data[x,y].Entity = MapCell.EntityType.Absinth;
									break;
							}
						}
						break;

					case "cells":
						foreach (XmlNode node2 in node.ChildNodes)
						{
							if (node2.Name.CompareTo("cell") != 0)
								throw new FormatException();
							x = GetAttrInt(node2,"x");
							y = GetAttrInt(node2,"y");
							int tex = GetAttrInt(node2,"texture");
							int walkable = GetAttrInt(node2,"walkable");
							data[x,y].Index = tex;
							data[x,y].Walkable = walkable != 0;
						}
						break;
				}
			}
		}


        /// <summary>
        /// Save map into file
        /// </summary>
        /// <param name="fileName">map file path</param>
        public void Save(string fileName)
        {
            BinaryWriter writer = new BinaryWriter(new FileStream(fileName, FileMode.Create));
            writer.Write(dataWidth);
            writer.Write(dataHeight);
            
            for (int x = 0; x < dataWidth; x++)
                for (int y = 0; y < dataHeight; y++)
                {
                    //byte w = (data[x, y].Walkable) ? (byte)1 : (byte)0;
                    //writer.Write(w);
                    writer.Write(data[x, y].Walkable);
                    writer.Write(data[x, y].Index);
                    writer.Write((int)data[x, y].Entity);
                }
            writer.Close();
        }

		/// <summary>
		/// Creates attribute of XmlNode
		/// </summary>
		/// <param name="xml">XML Document</param>
		/// <param name="name">Name of attribute</param>
		/// <param name="value">Value of attribute</param>
		/// <returns>Created attribute</returns>
		private XmlAttribute CreateAttr(XmlDocument xml, string name, string value)
		{
			XmlAttribute attr = xml.CreateAttribute(name);
			attr.Value = value;
			return attr;
		}

		/// <summary>
		/// Creates attribute of XmlNode
		/// </summary>
		/// <param name="xml">XML Document</param>
		/// <param name="name">Name of attribute</param>
		/// <param name="value">Value of attribute</param>
		/// <returns>Created attribute</returns>
		private XmlAttribute CreateAttr(XmlDocument xml, string name, int value)
		{
			XmlAttribute attr = xml.CreateAttribute(name);
			attr.Value = value.ToString();
			return attr;
		}

		/// <summary>
		/// Saves map as XML file
		/// </summary>
		/// <param name="fileName">Path of file to be saved</param>
		public void SaveXML(string fileName)
		{
			XmlDocument xml = new XmlDocument();
			xml.LoadXml("<?xml version=\"1.0\"?>\n<map>\n</map>");
			XmlElement root = xml.DocumentElement;
			
			root.Attributes.Append(CreateAttr(xml, "width", dataWidth));
			root.Attributes.Append(CreateAttr(xml, "height", dataHeight));

			// store info
			XmlElement infoEl = xml.CreateElement("playerinfo");

			XmlElement ieStart = xml.CreateElement("start");
			XmlElement ieEnd = xml.CreateElement("end");

			Point start = FindStartPoint();
			Point end = FindEndPoint();

			ieStart.Attributes.Append(CreateAttr(xml, "x", start.X));
			ieStart.Attributes.Append(CreateAttr(xml, "y", start.Y));

			ieEnd.Attributes.Append(CreateAttr(xml, "x", end.X));
			ieEnd.Attributes.Append(CreateAttr(xml, "y", end.Y));

			infoEl.AppendChild(ieStart);
			infoEl.AppendChild(ieEnd);

			root.AppendChild(infoEl);

			// store monsters
			XmlElement monstersEl = xml.CreateElement("monsters");
			for (int x = 0; x < dataWidth; x++)
			{
				for (int y = 0; y < dataHeight; y++)
				{
					if (data[x,y].Entity == MapCell.EntityType.Monster)
					{
						XmlElement monsterEl = xml.CreateElement("monster");
						
						monsterEl.Attributes.Append(CreateAttr(xml, "x", x));
						monsterEl.Attributes.Append(CreateAttr(xml, "y", y));
						
						monstersEl.AppendChild(monsterEl);
					}
				}
			}
			
			root.AppendChild(monstersEl);

			// store bottles
			XmlElement bottlesEl = xml.CreateElement("bottles");
			for (int x = 0; x < dataWidth; x++)
			{
				for (int y = 0; y < dataHeight; y++)
				{
					XmlElement bottleEl = null;
					switch (data[x,y].Entity)
					{
						case MapCell.EntityType.Rum:
							bottleEl = xml.CreateElement("rum");

							bottleEl.Attributes.Append(CreateAttr(xml, "x", x));
							bottleEl.Attributes.Append(CreateAttr(xml, "y", y));

							bottlesEl.AppendChild(bottleEl);
							break;


						case MapCell.EntityType.Absinth:
							bottleEl = xml.CreateElement("absinth");

							bottleEl.Attributes.Append(CreateAttr(xml, "x", x));
							bottleEl.Attributes.Append(CreateAttr(xml, "y", y));

							bottlesEl.AppendChild(bottleEl);
							break;

					}
				}
			}

			root.AppendChild(bottlesEl);

			// store map cells
			XmlElement cellsEl = xml.CreateElement("cells");

			for (int x = 0; x < dataWidth; x++)
			{
				for (int y = 0; y < dataHeight; y++)
				{
					XmlElement cellEl = xml.CreateElement("cell");

					cellEl.Attributes.Append(CreateAttr(xml, "x", x));
					cellEl.Attributes.Append(CreateAttr(xml, "y", y));
					
					int walkable = (data[x,y].Walkable) ? 1 : 0;
					cellEl.Attributes.Append(CreateAttr(xml, "walkable", walkable));
					
					cellEl.Attributes.Append(CreateAttr(xml, "texture", data[x,y].Index));

					cellsEl.AppendChild(cellEl);
				}
			}

			root.AppendChild(cellsEl);


			xml.Save(fileName);
		}

		/// <summary>
		/// Safe empty point constant. It is used by FindStartPoint and FindEndPoint method
		/// </summary>
		public static readonly Point EmptyPoint = new Point(-1,-1);
        /// <summary>
        /// Find start point
        /// </summary>
        /// <returns>point of player start</returns>
        public Point FindStartPoint()
        {
            for (int x = 0; x < dataWidth; x++)
            {
                for (int y = 0; y < dataHeight; y++)
                {
                    if (data[x, y].Entity == MapCell.EntityType.Start)
                        return new Point(x, y);
                }
            }
            return new Point(-1,-1);
        }

        /// <summary>
        /// Find end point
        /// </summary>
        /// <returns>point of level exit</returns>
        public Point FindEndPoint()
        {
            for (int x = 0; x < dataWidth; x++)
            {
                for (int y = 0; y < dataHeight; y++)
                {
                    if (data[x, y].Entity == MapCell.EntityType.End)
                        return new Point(x, y);
                }
            }
            return Point.Empty;
        }



        /// <summary>
        /// Device Reset resurection
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnCreateBuffer(object sender, EventArgs args)
        {
            VertexBuffer vertexBuffer = (VertexBuffer)sender;

            CustomVertex.PositionNormalTextured[] vert = (CustomVertex.PositionNormalTextured[])vertexBuffer.Lock(0, 0);
            Vector3[] v = new Vector3[8];
            v[0] = new Vector3(-1, -1, -1); v[1] = new Vector3(+1, -1, -1); v[2] = new Vector3(+1, -1, +1); v[3] = new Vector3(-1, -1, +1);
            v[4] = new Vector3(-1, +1, -1); v[5] = new Vector3(+1, +1, -1); v[6] = new Vector3(+1, +1, +1); v[7] = new Vector3(-1, +1, +1);
            Vector3[] n = new Vector3[6];
            n[0] = new Vector3(0, +1, 0); n[1] = new Vector3(0, 0, -1); n[2] = new Vector3(+1, 0, 0);
            n[3] = new Vector3(0, 0, +1); n[4] = new Vector3(-1, 0, 0); n[5] = new Vector3(0, +1, 0);
            Vector2[] t = new Vector2[4];
            t[0] = new Vector2(0, 0); t[1] = new Vector2(0, 1); t[2] = new Vector2(1, 1); t[3] = new Vector2(1, 0);
            int[] idx = {0,1,3,2, 0,1,4,5, 1,2,5,6, 2,3,6,7, 0,3,4,7, 4,5,7,6};

            for (int i = 0; i < 24; i++)
            {
                vert[i].Position = (v[idx[i]]);
                int ni = i / 4;
                int ti = i % 4;
                vert[i].Tu = t[ti].X;
                vert[i].Tv = t[ti].Y;
                vert[i].Normal = (n[ni]);
            }
            vertexBuffer.Unlock();

        }

        /// <summary>
        /// Draw part of unit cube
        /// </summary>
        /// <param name="partNo"></param>
        public void Render(int partNo)
        {
            if (partNo > 5 || partNo < 0) return;

            dev.SetStreamSource(0, vertexBuffer, 0);
            dev.VertexFormat = CustomVertex.PositionNormalTextured.Format;
            dev.DrawPrimitives(PrimitiveType.TriangleStrip, partNo * 4, 2);
        }

        /// <summary>
        /// Draw part of map cell
        /// </summary>
        /// <param name="partNo">number of part</param>
        /// <param name="x">postion of map cell</param>
        /// <param name="y">postion of map cell</param>
        public void RenderCell(int partNo, int x, int y)
        {
            Matrix oldWorld = dev.Transform.World;
            dev.Transform.World = Matrix.Translation(new Vector3(x * 2.0f, 0, y * 2.0f)) * oldWorld;
            Render(partNo);
            dev.Transform.World = oldWorld;
        }

        /// <summary>
        /// Draw map range
        /// </summary>
        /// <param name="x1">start position</param>
        /// <param name="y1">start position</param>
        /// <param name="x2">end position</param>
        /// <param name="y2">end position</param>
        public void Render(int x1, int y1, int x2, int y2)
        {
            for (int y = y1; y <= y2; y++)
            {
                for (int x = x1; x <= x2; x++)
                {
                    texMgr.Activate(data[x, y].Index);
                    if (data[x, y].Walkable)
                    {
                        //for (int i = 0; i < 6; i++) RenderCell(i, x, y);
                        RenderCell((int)BoxSide.Bottom, x, y);
                    }
                    else
                    {
                        RenderCell((int)BoxSide.Top, x, y);
                        
                        if (x <= 0) RenderCell((int)BoxSide.Left, x, y);
                        else if (data[x - 1, y].Walkable) RenderCell((int)BoxSide.Left, x, y);
                        
                        if (y <= 0) RenderCell((int)BoxSide.Front, x, y);
                        else if (data[x, y - 1].Walkable) RenderCell((int)BoxSide.Front, x, y);
                        
                        if (x >= dataWidth - 1) RenderCell((int)BoxSide.Right, x, y);
                        else if (data[x + 1, y].Walkable) RenderCell((int)BoxSide.Right, x, y);
                        
                        if (y >= dataHeight - 1) RenderCell((int)BoxSide.Back, x, y);
                        else if (data[x, y + 1].Walkable) RenderCell((int)BoxSide.Back, x, y);
                    }
                }
            }
        }

        /// <summary>
        /// Draw map layout
        /// </summary>
        /// <param name="g">graphics object</param>
        /// <param name="imgs">list of texture thumbnails</param>
        /// <param name="gridWidth">width of layout grid</param>
        /// <param name="gridHeight">height of layout grid</param>
        public void RenderToGraphics(Graphics g, ImageList imgs, int gridWidth, int gridHeight)
        {
            Bitmap bmp = new Bitmap(gridWidth * dataWidth, gridHeight * dataHeight);
            for (int x = 0; x < dataWidth; x++)
            {
                for (int y = 0; y < dataHeight; y++)
                {
                    g.DrawImageUnscaled(imgs.Images[data[x, y].Index], x * gridWidth, y * gridHeight);
                    
                    //g.DrawImage(imgs.Images[data[x, y].Index], new Point(x * gridWidth, y * gridHeight));

                }
            }
            int ex = gridWidth * dataWidth;
            int ey = gridHeight * dataHeight;
            for (int i = 0; i < dataWidth; i++)
            {
                g.DrawLine(Pens.Black, i * gridWidth, 0, i * gridWidth, ex);
            }
            for (int i = 0; i < dataHeight; i++)
                g.DrawLine(Pens.Black, 0, i * gridHeight, ey, i * gridHeight);
        }


        /// <summary>
        /// Copy cells
        /// </summary>
        /// <param name="cells">source map data</param>
        /// <param name="width">width of source, destination</param>
        /// <param name="height">height of source, destination</param>
        public void SetCells(MapCell[,] cells, int width, int height)
        {
            dataWidth = width;
            dataHeight = height;
            data = new MapCell[width, height];
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    data[x, y] = cells[x, y];
                }
            }
        }

        /// <summary>
        /// Converte world-coordinate to map-coordinate
        /// </summary>
        /// <param name="v">world coordinate</param>
        /// <returns>map coordinate</returns>
        public static Point Convert(Vector3 v)
        {
            
            int x = (int)v.X / 2;
            int y = (int)v.Z / 2;
            return new Point(x, y);
        }

        /// <summary>
        /// Convert map-coordinate to world-coordinate
        /// </summary>
        /// <param name="p">map coordinate</param>
        /// <returns>world coordinate</returns>
        public static Vector3 Convert(Point p)
        {
            return new Vector3(p.X * 2, 0, p.Y * 2);
        }

        /// <summary>
        /// Access cell by Point
        /// </summary>
        /// <param name="p">pointer</param>
        /// <returns></returns>
        public MapCell this[Point p]
        {
            get
            {
                if (p.X < 0 || p.X >= dataWidth || p.Y < 0 || p.Y >= dataHeight) 
                    return null;
                return data[p.X, p.Y];
            }
            set
            {
                if (!(p.X < 0 || p.X >= dataWidth || p.Y < 0 || p.Y >= dataHeight))
                    data[p.X, p.Y] = value;
            }
        }

        /// <summary>
        /// Count of pickable items
        /// </summary>
        /// <returns>count of ...</returns>
        public int PickableItemCount()
        {
            int n = 0;
            for (int x = 0; x < dataWidth; x++)
                for (int y = 0; y < dataHeight; y++)
                    if ((data[x, y].Entity == MapCell.EntityType.Absinth ||
                        data[x, y].Entity == MapCell.EntityType.Rum) && data[x,y].Walkable) 
                        n++;
            return n;
        }

        /// <summary>
        /// Has pickable items
        /// </summary>
        /// <returns>has?</returns>
        public bool HasItem()
        {
            int n = PickableItemCount();
            return n > 0;
        }

        /// <summary>
        /// Count of monsters
        /// </summary>
        /// <returns>count of monsters in map</returns>
        public uint MonsterCount()
        {
            uint n = 0;
            for (int i = 0; i < dataWidth; i++)
            {
                for (int j = 0; j < dataHeight; j++)
                    if (data[i, j].Entity == MapCell.EntityType.Monster) n++;
            }
            return n;
        }
    }
}
