using System.Collections.Generic;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Collision;
using FarseerPhysics.Factories;
namespace FarseerPhysics.Common
{
    public enum Decomposer
    {
        Bayazit,
        CDT,
        Earclip,
        Flipcode,
        Seidel,
    }
    /// 
    /// Return true if the specified color is inside the terrain.
    /// 
    public delegate bool TerrainTester(Color color);
    /// 
    /// Simple class to maintain a terrain.
    /// 
    public class MSTerrain
    {
        /// 
        /// World to manage terrain in.
        /// 
        public World World;
        /// 
        /// Center of terrain in world units.
        /// 
        public Vector2 Center;
        /// 
        /// Width of terrain in world units.
        /// 
        public float Width;
        /// 
        /// Height of terrain in world units.
        /// 
        public float Height;
        /// 
        /// Points per each world unit used to define the terrain in the point cloud.
        /// 
        public int PointsPerUnit;
        /// 
        /// Points per cell.
        /// 
        public int CellSize;
        /// 
        /// Points per sub cell.
        /// 
        public int SubCellSize;
        /// 
        /// Number of iterations to perform in the Marching Squares algorithm.
        /// Note: More then 3 has almost no effect on quality.
        /// 
        public int Iterations = 2;
        /// 
        /// Decomposer to use when regenerating terrain. Can be changed on the fly without consequence.
        /// Note: Some decomposerers are unstable.
        /// 
        public Decomposer Decomposer;
        /// 
        /// Point cloud defining the terrain.
        /// 
        private sbyte[,] _terrainMap;
        /// 
        /// Generated bodies.
        /// 
        private List
[,] _bodyMap;
        private float _localWidth;
        private float _localHeight;
        private int _xnum;
        private int _ynum;
        private AABB _dirtyArea;
        private Vector2 _topLeft;
        public MSTerrain(World world, AABB area)
        {
            World = world;
            Width = area.Extents.X * 2;
            Height = area.Extents.Y * 2;
            Center = area.Center;
        }
        /// 
        /// Initialize the terrain for use.
        /// 
        public void Initialize()
        {
            // find top left of terrain in world space
            _topLeft = new Vector2(Center.X - (Width * 0.5f), Center.Y - (-Height * 0.5f));
            // convert the terrains size to a point cloud size
            _localWidth = Width * PointsPerUnit;
            _localHeight = Height * PointsPerUnit;
            _terrainMap = new sbyte[(int)_localWidth + 1, (int)_localHeight + 1];
            for (int x = 0; x < _localWidth; x++)
            {
                for (int y = 0; y < _localHeight; y++)
                {
                    _terrainMap[x, y] = 1;
                }
            }
            _xnum = (int)(_localWidth / CellSize);
            _ynum = (int)(_localHeight / CellSize);
            _bodyMap = new List[_xnum, _ynum];
            // make sure to mark the dirty area to an infinitely small box
            _dirtyArea = new AABB(new Vector2(float.MaxValue, float.MaxValue), new Vector2(float.MinValue, float.MinValue));
        }
        /// 
        /// Apply a texture to the terrain using the specified TerrainTester.
        /// 
        /// Texture to apply.
        /// Top left position of the texture relative to the terrain.
        /// Delegate method used to determine what colors should be included in the terrain.
        public void ApplyTexture(Texture2D texture, Vector2 position, TerrainTester tester)
        {
            Color[] colorData = new Color[texture.Width * texture.Height];
            texture.GetData(colorData);
            for (int y = (int)position.Y; y < texture.Height + (int)position.Y; y++)
            {
                for (int x = (int)position.X; x < texture.Width + (int)position.X; x++)
                {
                    if (x >= 0 && x < _localWidth && y >= 0 && y < _localHeight)
                    {
                        bool inside = tester(colorData[((y - (int)position.Y) * texture.Width) + (x - (int)position.X)]);
                        if (!inside)
                            _terrainMap[x, y] = 1;
                        else
                            _terrainMap[x, y] = -1;
                    }
                }
            }
            // generate terrain
            for (int gy = 0; gy < _ynum; gy++)
            {
                for (int gx = 0; gx < _xnum; gx++)
                {
                    //remove old terrain object at grid cell
                    if (_bodyMap[gx, gy] != null)
                    {
                        for (int i = 0; i < _bodyMap[gx, gy].Count; i++)
                        {
                            World.RemoveBody(_bodyMap[gx, gy][i]);
                        }
                    }
                    _bodyMap[gx, gy] = null;
                    //generate new one
                    GenerateTerrain(gx, gy);
                }
            }
        }
        /// 
        /// Apply a texture to the terrain using the specified TerrainTester.
        /// 
        /// Top left position of the texture relative to the terrain.
        public void ApplyData(sbyte[,] data, Vector2 position)
        {
            for (int y = (int)position.Y; y < data.GetUpperBound(1) + (int)position.Y; y++)
            {
                for (int x = (int)position.X; x < data.GetUpperBound(0) + (int)position.X; x++)
                {
                    if (x >= 0 && x < _localWidth && y >= 0 && y < _localHeight)
                    {
                        _terrainMap[x, y] = data[x, y];
                    }
                }
            }
            // generate terrain
            for (int gy = 0; gy < _ynum; gy++)
            {
                for (int gx = 0; gx < _xnum; gx++)
                {
                    //remove old terrain object at grid cell
                    if (_bodyMap[gx, gy] != null)
                    {
                        for (int i = 0; i < _bodyMap[gx, gy].Count; i++)
                        {
                            World.RemoveBody(_bodyMap[gx, gy][i]);
                        }
                    }
                    _bodyMap[gx, gy] = null;
                    //generate new one
                    GenerateTerrain(gx, gy);
                }
            }
        }
        /// 
        /// Convert a texture to an sbtye array compatible with ApplyData().
        /// 
        /// Texture to convert.
        /// 
        /// 
        public static sbyte[,] ConvertTextureToData(Texture2D texture, TerrainTester tester)
        {
            sbyte[,] data = new sbyte[texture.Width, texture.Height];
            Color[] colorData = new Color[texture.Width * texture.Height];
            texture.GetData(colorData);
            for (int y = 0; y < texture.Height; y++)
            {
                for (int x = 0; x < texture.Width; x++)
                {
                    bool inside = tester(colorData[(y * texture.Width) + x]);
                    if (!inside)
                        data[x, y] = 1;
                    else
                        data[x, y] = -1;
                }
            }
            return data;
        }
        /// 
        /// Modify a single point in the terrain.
        /// 
        /// World location to modify. Automatically clipped.
        /// -1 = inside terrain, 1 = outside terrain
        public void ModifyTerrain(Vector2 location, sbyte value)
        {
            // find local position
            // make position local to map space
            Vector2 p = location - _topLeft;
            // find map position for each axis
            p.X = p.X * _localWidth / Width;
            p.Y = p.Y * -_localHeight / Height;
            if (p.X >= 0 && p.X < _localWidth && p.Y >= 0 && p.Y < _localHeight)
            {
                _terrainMap[(int)p.X, (int)p.Y] = value;
                // expand dirty area
                if (p.X < _dirtyArea.LowerBound.X) _dirtyArea.LowerBound.X = p.X;
                if (p.X > _dirtyArea.UpperBound.X) _dirtyArea.UpperBound.X = p.X;
                if (p.Y < _dirtyArea.LowerBound.Y) _dirtyArea.LowerBound.Y = p.Y;
                if (p.Y > _dirtyArea.UpperBound.Y) _dirtyArea.UpperBound.Y = p.Y;
            }
        }
        /// 
        /// Regenerate the terrain.
        /// 
        public void RegenerateTerrain()
        {
            //iterate effected cells
            var gx0 = (int)(_dirtyArea.LowerBound.X / CellSize);
            var gx1 = (int)(_dirtyArea.UpperBound.X / CellSize) + 1;
            if (gx0 < 0) gx0 = 0;
            if (gx1 > _xnum) gx1 = _xnum;
            var gy0 = (int)(_dirtyArea.LowerBound.Y / CellSize);
            var gy1 = (int)(_dirtyArea.UpperBound.Y / CellSize) + 1;
            if (gy0 < 0) gy0 = 0;
            if (gy1 > _ynum) gy1 = _ynum;
            for (int gx = gx0; gx < gx1; gx++)
            {
                for (int gy = gy0; gy < gy1; gy++)
                {
                    //remove old terrain object at grid cell
                    if (_bodyMap[gx, gy] != null)
                    {
                        for (int i = 0; i < _bodyMap[gx, gy].Count; i++)
                        {
                            World.RemoveBody(_bodyMap[gx, gy][i]);
                        }
                    }
                    _bodyMap[gx, gy] = null;
                    //generate new one
                    GenerateTerrain(gx, gy);
                }
            }
            _dirtyArea = new AABB(new Vector2(float.MaxValue, float.MaxValue), new Vector2(float.MinValue, float.MinValue));
        }
        private void GenerateTerrain(int gx, int gy)
        {
            float ax = gx * CellSize;
            float ay = gy * CellSize;
            List polys = MarchingSquares.DetectSquares(new AABB(new Vector2(ax, ay), new Vector2(ax + CellSize, ay + CellSize)), SubCellSize, SubCellSize, _terrainMap, Iterations, true);
            if (polys.Count == 0) return;
            _bodyMap[gx, gy] = new List();
            // create the scale vector
            Vector2 scale = new Vector2(1f / PointsPerUnit, 1f / -PointsPerUnit);
            // create physics object for this grid cell
            foreach (var item in polys)
            {
                // does this need to be negative?
                item.Scale(ref scale);
                item.Translate(ref _topLeft);
                item.ForceCounterClockWise();
                Vertices p = FarseerPhysics.Common.PolygonManipulation.SimplifyTools.CollinearSimplify(item);
                List decompPolys = new List();
                switch (Decomposer)
                {
                    case Decomposer.Bayazit:
                        decompPolys = Decomposition.BayazitDecomposer.ConvexPartition(p);
                        break;
                    case Decomposer.CDT:
                        decompPolys = Decomposition.CDTDecomposer.ConvexPartition(p);
                        break;
                    case Decomposer.Earclip:
                        decompPolys = Decomposition.EarclipDecomposer.ConvexPartition(p);
                        break;
                    case Decomposer.Flipcode:
                        decompPolys = Decomposition.FlipcodeDecomposer.ConvexPartition(p);
                        break;
                    case Decomposer.Seidel:
                        decompPolys = Decomposition.SeidelDecomposer.ConvexPartition(p, 0.001f);
                        break;
                    default:
                        break;
                }
                foreach (Vertices poly in decompPolys)
                {
                    if (poly.Count > 2)
                        _bodyMap[gx, gy].Add(BodyFactory.CreatePolygon(World, poly, 1));
                }
            }
        }
    }
}