
#region ================== Copyright (c) 2007 Pascal vd Heiden

/*
 * Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com
 * This program is released under GNU General Public License
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 */

#endregion

#region ================== Namespaces

using System;
using System.Collections.Generic;
using CodeImp.DoomBuilder.Geometry;
using System.Drawing;

#endregion

namespace CodeImp.DoomBuilder.Map
{
	public class BlockMap<BE> where BE : BlockEntry, new()
	{
		#region ================== Constants
		
		#endregion
		
		#region ================== Variables
		
		// Blocks
		protected BE[,] blockmap;
		protected int blocksizeshift;
		protected int blocksize;
		protected Size size;
		protected RectangleF range;
		protected Vector2D rangelefttop;
		
		// State
		private bool isdisposed;
		
		#endregion
		
		#region ================== Properties
		
		public bool IsDisposed { get { return isdisposed; } }
		public Size Size { get { return size; } }
		public RectangleF Range { get { return range; } }
		public int BlockSize { get { return blocksize; } }
		internal BE[,] Map { get { return blockmap; } }
		
		#endregion
		
		#region ================== Constructor / Disposer
		
		// Constructor
		public BlockMap(RectangleF range)
		{
			Initialize(range, 128);
		}

		// Constructor
		public BlockMap(RectangleF range, int blocksize)
		{
			Initialize(range, blocksize);
		}

		// This initializes the blockmap
		private void Initialize(RectangleF range, int blocksize)
		{
			// Initialize
			this.range = range;
			this.blocksizeshift = General.BitsForInt(blocksize);
			this.blocksize = 1 << blocksizeshift;
			if((this.blocksize != blocksize) || (this.blocksize <= 1)) throw new ArgumentException("Block size must be a power of 2 greater than 1");
			rangelefttop = new Vector2D(range.Left, range.Top);

			// Simply casting to int can result int the blockmap being too small (for example for off-grid vertices), so round the dimensions
			int left = range.Left < 0 ? (int)Math.Floor(range.Left) : (int)Math.Ceiling(range.Left);
			int right = range.Right < 0 ? (int)Math.Floor(range.Right) : (int)Math.Ceiling(range.Right);
			int top = range.Top < 0 ? (int)Math.Floor(range.Top) : (int)Math.Ceiling(range.Top);
			int bottom = range.Bottom < 0 ? (int)Math.Floor(range.Bottom) : (int)Math.Ceiling(range.Bottom);

			Point lefttop = new Point(left >> blocksizeshift, top >> blocksizeshift);
			Point rightbottom = new Point(right >> blocksizeshift, bottom >> blocksizeshift);
			size = new Size((rightbottom.X - lefttop.X) + 1, (rightbottom.Y - lefttop.Y) + 1);
			blockmap = new BE[size.Width, size.Height];
			Clear();
		}
		
		// Disposer
		public void Dispose()
		{
			// Not already disposed?
			if(!isdisposed)
			{
				// Clean up
				blockmap = null;
				
				// Done
				isdisposed = true;
			}
		}
		
		#endregion
		
		#region ================== Methods
		
		// This returns the block coordinates
		internal Point GetBlockCoordinates(Vector2D v)
		{
			return new Point((int)(v.x - range.Left) >> blocksizeshift,
							 (int)(v.y - range.Top) >> blocksizeshift);
		}

		// This returns the block center in world coordinates
		protected Vector2D GetBlockCenter(Point p)
		{
			return new Vector2D(((p.X << blocksizeshift) + (blocksize >> 1)) + range.Left,
								((p.Y << blocksizeshift) + (blocksize >> 1)) + range.Top);
		}
		
		// This returns true when the given block is inside range
		internal bool IsInRange(Point p)
		{
			return (p.X >= 0) && (p.X < size.Width) && (p.Y >= 0) && (p.Y < size.Height);
		}
		
		// This returns true when the given block is inside range
		public bool IsInRange(Vector2D p)
		{
			return (p.x >= range.Left) && (p.x < range.Right) && (p.y >= range.Top) && (p.y < range.Bottom);
		}
		
		// This crops a point into the range
		protected Point CropToRange(Point p)
		{
			return new Point(Math.Min(Math.Max(p.X, 0), size.Width - 1),
							 Math.Min(Math.Max(p.Y, 0), size.Height - 1));
		}

		// This crops a point into the range
		protected int CropToRangeX(int x)
		{
			return Math.Min(Math.Max(x, 0), size.Width - 1);
		}

		// This crops a point into the range
		protected int CropToRangeY(int y)
		{
			return Math.Min(Math.Max(y, 0), size.Height - 1);
		}
		
		// This clears the blockmap
		public virtual void Clear()
		{
			for(int x = 0; x < size.Width; x++)
			{
				for(int y = 0; y < size.Height; y++)
				{
					blockmap[x, y] = new BE();
				}
			}
		}

		// This returns a blocks at the given coordinates, if any
		// Returns null when out of range
		public virtual BE GetBlockAt(Vector2D pos)
		{
			// Calculate block coordinates
			Point p = GetBlockCoordinates(pos);
			return IsInRange(p) ? blockmap[p.X, p.Y] : null;
		}

		// This returns a range of blocks in a square
		public virtual List<BE> GetSquareRange(RectangleF rect)
		{
			// Calculate block coordinates
			Point lt = GetBlockCoordinates(new Vector2D(rect.Left, rect.Top));
			Point rb = GetBlockCoordinates(new Vector2D(rect.Right, rect.Bottom));
			
			// Crop coordinates to range
			lt = CropToRange(lt);
			rb = CropToRange(rb);
			
			// Go through the range to make a list
			int entriescount = ((rb.X - lt.X) + 1) * ((rb.Y - lt.Y) + 1);
			List<BE> entries = new List<BE>(entriescount);
			for(int x = lt.X; x <= rb.X; x++)
			{
				for(int y = lt.Y; y <= rb.Y; y++)
				{
					entries.Add(blockmap[x, y]);
				}
			}
			
			// Return list
			return entries;
		}

		// This returns a range of blocks in a rectangle. Pretty much the same like GetSquareRange(RectangleF rect),
		// just without the overhead to create a RectangleF first
		public virtual HashSet<BE> GetSquareRange(double left, double top, double width, double height)
		{
			// Calculate block coordinates
			Point lt = GetBlockCoordinates(new Vector2D(left, top));
			Point rb = GetBlockCoordinates(new Vector2D(left+width, top+height));

			// Crop coordinates to range
			lt = CropToRange(lt);
			rb = CropToRange(rb);

			// Go through the range to make a list
			int entriescount = ((rb.X - lt.X) + 1) * ((rb.Y - lt.Y) + 1);
			HashSet<BE> entries = new HashSet<BE>(entriescount);
			for (int x = lt.X; x <= rb.X; x++)
			{
				for (int y = lt.Y; y <= rb.Y; y++)
				{
					entries.Add(blockmap[x, y]);
				}
			}

			return entries;
		}

		// This returns all blocks along the given line
		public virtual List<BE> GetLineBlocks(Vector2D v1, Vector2D v2)
		{
			int dirx, diry;
			
			// Estimate number of blocks we will go through and create list
			int entriescount = (int)(Vector2D.ManhattanDistance(v1, v2) * 2.0f) / blocksize;
			List<BE> entries = new List<BE>(entriescount);

			// Find start and end block
			Point pos = GetBlockCoordinates(v1);
			Point end = GetBlockCoordinates(v2);
			v1 -= rangelefttop;
			v2 -= rangelefttop;

			// Horizontal straight line?
			if(pos.Y == end.Y)
			{
				// Simple loop
				pos.X = CropToRangeX(pos.X);
				end.X = CropToRangeX(end.X);
				if(IsInRange(new Point(pos.X, pos.Y)))
				{
					dirx = Math.Sign(v2.x - v1.x);
					if(dirx != 0)
					{
						for(int x = pos.X; x != end.X; x += dirx)
						{
							entries.Add(blockmap[x, pos.Y]);
						}
					}
					entries.Add(blockmap[end.X, end.Y]);
				}
			}
			// Vertical straight line?
			else if(pos.X == end.X)
			{
				// Simple loop
				pos.Y = CropToRangeY(pos.Y);
				end.Y = CropToRangeY(end.Y);
				if(IsInRange(new Point(pos.X, pos.Y)))
				{
					diry = Math.Sign(v2.y - v1.y);
					if(diry != 0)
					{
						for(int y = pos.Y; y != end.Y; y += diry)
						{
							entries.Add(blockmap[pos.X, y]);
						}
					}
					entries.Add(blockmap[end.X, end.Y]);
				}
			}
			else
			{
				// Add this block
				if(IsInRange(pos)) entries.Add(blockmap[pos.X, pos.Y]);

				// Moving outside the block?
				if(pos != end)
				{
					// Calculate current block edges
					double cl = pos.X * blocksize;
					double cr = (pos.X + 1) * blocksize;
					double ct = pos.Y * blocksize;
					double cb = (pos.Y + 1) * blocksize;

					// Line directions
					dirx = Math.Sign(v2.x - v1.x);
					diry = Math.Sign(v2.y - v1.y);

					// Calculate offset and delta movement over x
					double posx, deltax;
					if(dirx >= 0)
					{
						posx = (cr - v1.x) / (v2.x - v1.x);
						deltax = blocksize / (v2.x - v1.x);
					}
					else
					{
						// Calculate offset and delta movement over x
						posx = (v1.x - cl) / (v1.x - v2.x);
						deltax = blocksize / (v1.x - v2.x);
					}

					// Calculate offset and delta movement over y
					double posy, deltay;
					if(diry >= 0)
					{
						posy = (cb - v1.y) / (v2.y - v1.y);
						deltay = blocksize / (v2.y - v1.y);
					}
					else
					{
						posy = (v1.y - ct) / (v1.y - v2.y);
						deltay = blocksize / (v1.y - v2.y);
					}

					// Continue while not reached the end
					while(pos != end)
					{
						// Check in which direction to move
						if(posx < posy)
						{
							// Move horizontally
							posx += deltax;
							if(pos.X != end.X) pos.X += dirx;
						}
						else
						{
							// Move vertically
							posy += deltay;
							if(pos.Y != end.Y) pos.Y += diry;
						}
						
						// Add lines to this block
						if(IsInRange(pos)) entries.Add(blockmap[pos.X, pos.Y]);
					}
				}
			}
			
			// Return list
			return entries;
		}

		// This puts things in the blockmap
		public virtual void AddThingsSet(ICollection<Thing> things)
		{
			foreach(Thing t in things) AddThing(t);
		}
		
		// This puts a thing in the blockmap
		public virtual void AddThing(Thing t)
		{
			Point p = GetBlockCoordinates(t.Position);
			if(IsInRange(p)) blockmap[p.X, p.Y].Things.Add(t);
		}

		//mxd. This puts vertices in the blockmap
		public virtual void AddVerticesSet(ICollection<Vertex> verts) 
		{
			foreach(Vertex v in verts) AddVertex(v);
		}

		//mxd. This puts a vertex in the blockmap
		public virtual void AddVertex(Vertex v) 
		{
			Point p = GetBlockCoordinates(v.Position);
			if(IsInRange(p)) blockmap[p.X, p.Y].Vertices.Add(v);
		}

		// This puts sectors in the blockmap
		public virtual void AddSectorsSet(ICollection<Sector> sectors)
		{
			foreach(Sector s in sectors) AddSector(s);
		}

		// This puts a sector in the blockmap
		public virtual void AddSector(Sector s)
		{
			//mxd. Check range. Sector can be bigger than blockmap range
			if(!range.IntersectsWith(s.BBox)) return;
			
			Point p1 = GetBlockCoordinates(new Vector2D(s.BBox.Left, s.BBox.Top));
			Point p2 = GetBlockCoordinates(new Vector2D(s.BBox.Right, s.BBox.Bottom));
			p1 = CropToRange(p1);
			p2 = CropToRange(p2);
			for(int x = p1.X; x <= p2.X; x++)
			{
				for(int y = p1.Y; y <= p2.Y; y++)
				{
					blockmap[x, y].Sectors.Add(s);
				}
			}
		}
		
		// This puts a whole set of linedefs in the blocks they cross
		public virtual void AddLinedefsSet(ICollection<Linedef> lines)
		{
			foreach(Linedef l in lines) AddLinedef(l);
		}
		
		// This puts a single linedef in all blocks it crosses
		public virtual void AddLinedef(Linedef line)
		{
			int dirx, diry;

			// Get coordinates
			Vector2D v1 = line.Start.Position;
			Vector2D v2 = line.End.Position;
			
			// Find start and end block
			Point pos = GetBlockCoordinates(v1);
			Point end = GetBlockCoordinates(v2);
			v1 -= rangelefttop;
			v2 -= rangelefttop;
			
			// Horizontal straight line?
			if(pos.Y == end.Y)
			{
				// Simple loop
				pos.X = CropToRangeX(pos.X);
				end.X = CropToRangeX(end.X);
				if(IsInRange(new Point(pos.X, pos.Y)))
				{
					dirx = Math.Sign(v2.x - v1.x);
					if(dirx != 0)
					{
						for(int x = pos.X; x != end.X; x += dirx)
						{
							blockmap[x, pos.Y].Lines.Add(line);
						}
					}
					blockmap[end.X, end.Y].Lines.Add(line);
				}
			}
			// Vertical straight line?
			else if(pos.X == end.X)
			{
				// Simple loop
				pos.Y = CropToRangeY(pos.Y);
				end.Y = CropToRangeY(end.Y);
				if(IsInRange(new Point(pos.X, pos.Y)))
				{
					diry = Math.Sign(v2.y - v1.y);
					if(diry != 0)
					{
						for(int y = pos.Y; y != end.Y; y += diry)
						{
							blockmap[pos.X, y].Lines.Add(line);
						}
					}
					blockmap[end.X, end.Y].Lines.Add(line);
				}
			}
			else
			{
				// Add lines to this block
				if(IsInRange(pos)) blockmap[pos.X, pos.Y].Lines.Add(line);
				
				// Moving outside the block?
				if(pos != end)
				{
					// Calculate current block edges
					double cl = pos.X * blocksize;
					double cr = (pos.X + 1) * blocksize;
					double ct = pos.Y * blocksize;
					double cb = (pos.Y + 1) * blocksize;
					
					// Line directions
					dirx = Math.Sign(v2.x - v1.x);
					diry = Math.Sign(v2.y - v1.y);

					// Calculate offset and delta movement over x
					double posx, deltax;
					if(dirx == 0)
					{
						posx = float.MaxValue;
						deltax = float.MaxValue;
					}
					else if(dirx > 0)
					{
						posx = (cr - v1.x) / (v2.x - v1.x);
						deltax = blocksize / (v2.x - v1.x);
					}
					else
					{
						// Calculate offset and delta movement over x
						posx = (v1.x - cl) / (v1.x - v2.x);
						deltax = blocksize / (v1.x - v2.x);
					}

					// Calculate offset and delta movement over y
					double posy, deltay;
					if(diry == 0)
					{
						posy = float.MaxValue;
						deltay = float.MaxValue;
					}
					else if(diry > 0)
					{
						posy = (cb - v1.y) / (v2.y - v1.y);
						deltay = blocksize / (v2.y - v1.y);
					}
					else
					{
						posy = (v1.y - ct) / (v1.y - v2.y);
						deltay = blocksize / (v1.y - v2.y);
					}
					
					// Continue while not reached the end
					while(pos != end)
					{
						// Check in which direction to move
						if(posx < posy)
						{
							// Move horizontally
							posx += deltax;
							if(pos.X != end.X) pos.X += dirx;
						}
						else
						{
							// Move vertically
							posy += deltay;
							if(pos.Y != end.Y) pos.Y += diry;
						}
						
						// Add lines to this block
						if(IsInRange(pos)) blockmap[pos.X, pos.Y].Lines.Add(line);
					}
				}
			}
		}
		
		#endregion
	}
}
