using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MonoGame.FourEngine.Services;
using System;
using System.Collections.Generic;
using System.Linq;

namespace MonoGame.FourEngine.Raycast
{
    public partial class RaycastComponent : Component, IDisposable
    {
        public const bool OptimizeIdle = true;

        private Point FloorPixelization = new Point(4, 2);

        public List<IRaycastObject> Objects { get; internal set; } = new List<IRaycastObject>();

        public IRaycastMap Map { get; }
        public IRaycastCamera Camera { get; }

        #region Textures
        protected Texture2D FloorShadow { get; }
        #endregion

        protected virtual ISpriteBatchFactory SpriteBatchFactory => ServiceLocator.SpriteBatchFactory;
        
        public readonly int Width;
        public readonly int Height;

        protected GraphicsDevice GraphicsDevice;
        protected SpriteBatch Batch;

        protected readonly int WallTextureSize;
        protected readonly int FloorTextureSize;
        protected float[,] TextureOffsets;

        protected RaycastWallLayer[] WallLayers;
        private Rectangle[] _slices;
        private float[] _cameraX;

        private float[] _floorDistance;
        private Texture2D _floorCanvas;
        private Color[] _floorData;
        private int _floorHeight;
        private Color[] _floorProjection;

        private Dictionary<int, Rectangle> _objectOnScreen = new Dictionary<int, Rectangle>();
        private Dictionary<int, Color> _objectColor = new Dictionary<int, Color>();
        private Queue<int> _objectStack = new Queue<int>();

        private Vector2 _prevCameraPosition;
        private Vector2 _prevCameraPlane;
        private Vector2 _prevCameraDirection;

        private Scene _scene;

        private bool _drawn = true;
        private bool _hasFloor;

        public RaycastComponent(IRaycastMap map, Scene scene, IRaycastCamera camera, Point resolution)
        {
            GraphicsDevice = FourEngineGame.Current.GraphicsDevice;
            Batch = SpriteBatchFactory.Create(GraphicsDevice);

            Map = map;
            Camera = camera;

            Width = resolution.X;
            Height = resolution.Y;
            _floorHeight = Height / 2;

            _scene = scene;

            _hasFloor = Map.Floor != null;

            foreach (var layer in Map.Layers)
            {
                if (layer.TileSet.Size.X != layer.TileSet.Size.Y)
                {
                    throw new ArgumentException($"Wall textures should have a square size");
                }
            }

            if (_hasFloor && Map.Floor.TileSet.Size.X != Map.Floor.TileSet.Size.Y)
            {
                throw new ArgumentException($"Floor textures should have a square size");
            }

            WallTextureSize = map.Layers[0].TileSet.Size.X;

            if (_hasFloor)
            {
                FloorTextureSize = map.Floor.TileSet.Size.X;
                FloorShadow = GenerateFloorTexture();
            }

            InitializeArrays();
        }

        private void InitializeArrays()
        {
            TextureOffsets = new float[Map.Layers[0].Data.GetLength(0), Map.Layers[0].Data.GetLength(1)];
            WallLayers = CreateLevels().ToArray();
            _slices = CreateSlices().ToArray();

            if (_hasFloor)
            {
                _floorCanvas = new Texture2D(GraphicsDevice, Width / FloorPixelization.X, _floorHeight / FloorPixelization.Y);

                _floorData = new Color[Map.Floor.TileSet.Texture.Width * Map.Floor.TileSet.Texture.Height];
                Map.Floor.TileSet.Texture.GetData(_floorData);

                _floorDistance = new float[_floorHeight];
                for (var y = 0; y < _floorHeight; y++)
                {
                    _floorDistance[y] = Height / (2f * y + Height - Height);
                }

                _floorProjection = new Color[Width / FloorPixelization.X * _floorHeight / FloorPixelization.Y];
                for (var i = 0; i < _floorProjection.Length; i++)
                {
                    _floorProjection[i] = _floorData[FloorTextureSize + 1];
                }
            }

            _cameraX = new float[Width];
            for (var i = 0; i < Width; i++)
            {
                // From screen coordinate to camera coordinate
                _cameraX[i] = 2 * i / (float)Width - 1;
            }
        }

        private Texture2D GenerateFloorTexture()
        {
            var texture = new Texture2D(GraphicsDevice, Width, _floorHeight);
            var data = new Color[texture.Width * texture.Height];

            for (var y = 0; y < texture.Height; y++)
            {
                var color = Color.Black * (1 - y / (float)texture.Height);
                for (var x = 0; x < texture.Width; x++)
                {
                    var index = x + y * texture.Width;
                    data[index] = color;
                }
            }

            texture.SetData(data);
            return texture;
        }

        private IEnumerable<RaycastWallLayer> CreateLevels()
        {
            IEnumerable<Rectangle> CreateRects()
            {
                for (var i = 0; i < Width; i++)
                    yield return new Rectangle(i, 0, 1, Height);
            }

            for (var i = 0; i < Map.Layers.Count; i++)
            {
                var layer = new RaycastWallLayer
                {
                    DestinationRect = CreateRects().ToArray(),
                    SourceRect = new Rectangle[Width],
                    Color = new Color[Width],
                    IsEmpty = new bool[Width]
                };

                yield return layer;
            }
        }

        private IEnumerable<Rectangle> CreateSlices()
        {
            for (var i = 0; i < WallTextureSize; i++)
                yield return new Rectangle(i, 0, 1, WallTextureSize);
        }

        public void Dispose()
        {
            if (_floorCanvas != null)
            {
                _floorCanvas.Dispose();
                _floorCanvas = null;
            }

            if (Batch != null)
            {
                Batch.Dispose();
                Batch = null;
            }
        }
    }
}