using Microsoft.Xna.Framework;
using MonoGame.FourEngine.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;

namespace MonoGame.FourEngine.Raycast
{
    partial class RaycastComponent
    {
        public override void OnUpdate(UpdateEventArgs args)
        {
            if (_drawn)
            {
                OnBeginCast();
                Raycast();
            }
            _drawn = false;

            _prevCameraDirection = Camera.Direction;
            _prevCameraPlane = Camera.Plane;
            _prevCameraPosition = Camera.Position;
        }

        public void Raycast()
        {
            var didCameraChange = true;
            if (OptimizeIdle)
            {
                didCameraChange =
                    Camera.Direction != _prevCameraDirection ||
                    Camera.Plane != _prevCameraPlane ||
                    Camera.Position != _prevCameraPosition;
            }

            var tasks = new Task[WallLayers.Length * 2];
            if (didCameraChange)
            {
                for (var i = WallLayers.Length - 1; i >= 0; i--)
                {
                    var level = i;
                    var widthOver2 = Width / 2;

                    var levelTask1 = tasks[level * 2] = new Task(() =>
                    {
                        for (var x = 0; x < widthOver2; x++)
                        {
                            CastLevel(x, Map.Layers[level].Data, WallLayers[level].SourceRect, WallLayers[level].DestinationRect, WallLayers[level].Color, level);
                        }
                    });
                    levelTask1.Start();

                    var levelTask2 = tasks[level * 2 + 1] = new Task(() =>
                    {
                        for (var x = widthOver2; x < Width; x++)
                        {
                            CastLevel(x, Map.Layers[level].Data, WallLayers[level].SourceRect, WallLayers[level].DestinationRect, WallLayers[level].Color, level);
                        }
                    });
                    levelTask2.Start();
                }
            }

            if (didCameraChange)
            {
                Task.WaitAll(tasks);
            }

            // TODO: Optimize somehow, cache or smth
            Objects = _scene.Entities.Filter<IRaycastObject>().ToList();
            for (var x = 0; x < Width; x++)
            {
                CastObjects(x, Map.Layers[0].Data);
            }
            _objectStack = new Queue<int>(Objects
                .Select((o, i) => new { I = i, H = _objectOnScreen[i].Height })
                .Where((o, i) => _objectOnScreen[i].Height > 0)
                .OrderBy(x => x.H)
                .Select(x => x.I));
        }

        protected void ForceRedraw()
        {
            _prevCameraDirection = default;
            _prevCameraPlane = default;
            _prevCameraPosition = default;
        }

        private void CastObjects(int x, int[,] grid)
        {
            for (var i = 0; i < Objects.Count; i++)
            {
                var obj = Objects[i];

                if (obj.Sprite == null)
                {
                    _objectOnScreen[i] = Rectangle.Empty;
                    continue;
                }

                obj.UpdateFrameByCamera(Camera);

                var diff = obj.Position - Camera.Position;

                var perpendicularDistance = 1f / (Camera.Plane.X * Camera.Direction.Y - Camera.Direction.X * Camera.Plane.Y);

                var transformX = perpendicularDistance * (Camera.Direction.Y * diff.X - Camera.Direction.X * diff.Y);
                var transformY = perpendicularDistance * (-Camera.Plane.Y * diff.X + Camera.Plane.X * diff.Y);

                if (transformY <= 0)
                {
                    _objectOnScreen[i] = Rectangle.Empty;
                    continue;
                }

                var spriteScreenX = (int)((Width / 2) * (1 + transformX / transformY));

                // Output height
                var spriteHeight = (int)(Height / transformY);
                if (spriteHeight == int.MinValue) continue;
                spriteHeight = Math.Abs(spriteHeight);

                // Output Y
                var drawStartY = -spriteHeight / 2 + _floorHeight;

                var rate = spriteHeight / (double)obj.Sprite.Size.Y;

                var spriteWidth = (int)(obj.Sprite.Size.X * rate);

                spriteScreenX -= spriteWidth / 2;

                var verticalOffset = (int)(Height * obj.VerticalPosition / transformY);

                var destinationRect = new Rectangle(spriteScreenX, drawStartY - verticalOffset, spriteWidth, spriteHeight);

                _objectOnScreen[i] = destinationRect;

                if (spriteHeight < 85)
                {
                    var light = spriteHeight * 3;
                    _objectColor[i] = new Color(light, light, light);
                }
                else
                {
                    _objectColor[i] = Color.White;
                }

                OnCastedObject(obj, destinationRect, i);
            }
        }

        private void CastLevel(int x, int[,] grid, Rectangle[] sourceRect, Rectangle[] destinationRect, Color[] color, int levelIndex, Vector2? rayStart = null, Point? ignore = null, int depth = 0)
        {
            var rayDirection = Camera.Direction + Camera.Plane * _cameraX[x];

            // Ray starts from camera position
            var rayPosition = rayStart ?? Camera.Position;

            // Ray position on the map
            var mapPoint = rayPosition.ToPoint();

            // Length of ray from current position to next x or y-side
            float sideDistanceX;
            float sideDistanceY;

            // Length of ray from one x or y-side to next x or y-side
            var deltaDistanceX = rayDirection.X == 0 ? float.MaxValue : FastSqrt(1 + (rayDirection.Y * rayDirection.Y) / (rayDirection.X * rayDirection.X));
            var deltaDistanceY = rayDirection.Y == 0 ? float.MaxValue : FastSqrt(1 + (rayDirection.X * rayDirection.X) / (rayDirection.Y * rayDirection.Y));

            // What direction to step in x or y-direction (either +1 or -1)
            sbyte stepX;
            sbyte stepY;

            var hit = Hit.None;
            var side = false; // Was a NS or a EW wall hit?

            // Calculate step and initial sideDistance
            if (rayDirection.X < 0)
            {
                stepX = -1;
                sideDistanceX = (Camera.Position.X - mapPoint.X) * deltaDistanceX;
            }
            else
            {
                stepX = +1;
                sideDistanceX = (mapPoint.X + 1f - Camera.Position.X) * deltaDistanceX;
            }

            if (rayDirection.Y < 0)
            {
                stepY = -1;
                sideDistanceY = (Camera.Position.Y - mapPoint.Y) * deltaDistanceY;
            }
            else
            {
                stepY = +1;
                sideDistanceY = (mapPoint.Y + 1f - Camera.Position.Y) * deltaDistanceY;
            }

            // Perform DDA
            while (hit == Hit.None)
            {
                // Make a 1 square step in x-direction or in y-direction
                if (sideDistanceX < sideDistanceY)
                {
                    sideDistanceX += deltaDistanceX;
                    mapPoint.X += stepX;
                    side = true;
                }
                else
                {
                    sideDistanceY += deltaDistanceY;
                    mapPoint.Y += stepY;
                    side = false;
                }

                // Check if ray hits a map boundary
                if (mapPoint.X < 0)
                {
                    hit = Hit.Bound;
                    mapPoint.X = 0;
                }
                else if (mapPoint.X > grid.GetUpperBound(0))
                {
                    hit = Hit.Bound;
                    mapPoint.X = grid.GetUpperBound(0);
                }
                if (mapPoint.Y < 0)
                {
                    hit = Hit.Bound;
                    mapPoint.Y = 0;
                }
                else if (mapPoint.Y > grid.GetUpperBound(1))
                {
                    hit = Hit.Bound;
                    mapPoint.Y = grid.GetUpperBound(1);
                }
                // Check if ray has hit a wall
                else if (grid[mapPoint.X, mapPoint.Y] > 0 && ignore != mapPoint)
                {
                    hit = Hit.Wall;
                }
            }

            // Calculate distance of perpendicular ray
            var perpendicularDistance = side
                ? (mapPoint.X - rayPosition.X + (1 - stepX) / 2) / (double)rayDirection.X
                : (mapPoint.Y - rayPosition.Y + (1 - stepY) / 2) / (double)rayDirection.Y;

            // Calculate height of line to draw on screen
            var lineHeight = (int)(Height / perpendicularDistance);

            // The top pixel
            var lineY = (-lineHeight / 2 + _floorHeight);

            var textureIndex = grid[mapPoint.X, mapPoint.Y] - 1;
            WallLayers[levelIndex].IsEmpty[x] = textureIndex < 0;

            // Calculate where exactly the wall was hit
            var wallX = side
                ? rayPosition.Y + perpendicularDistance * rayDirection.Y
                : rayPosition.X + perpendicularDistance * rayDirection.X;

            // Update hit coordinates with more precise values
            var preciseCoordinate = (int)wallX;
            if (!side && preciseCoordinate >= 0 && preciseCoordinate <= grid.GetUpperBound(0)) mapPoint.X = preciseCoordinate;
            if (side && preciseCoordinate >= 0 && preciseCoordinate <= grid.GetUpperBound(1)) mapPoint.Y = preciseCoordinate;

            wallX -= Math.Truncate(wallX);
            wallX = Math.Abs(wallX);

            // Horizontal texture coordinate
            var textureX = (int)(wallX * WallTextureSize);
            if (side && rayDirection.X > 0) textureX = WallTextureSize - textureX - 1;
            if (!side && rayDirection.Y < 0) textureX = WallTextureSize - textureX - 1;

            // Save horiontal texture coordinate
            sourceRect[x] = _slices[textureX];

            // Offset for different frames in Sprite
            sourceRect[x].X += textureIndex * WallTextureSize;
            
            // Need this to open doors and maybe moving textures
            if (TextureOffsets[mapPoint.X, mapPoint.Y] > 0)
            {
                var rightPart = (int)(TextureOffsets[mapPoint.X, mapPoint.Y] * WallTextureSize);
                var leftPart = WallTextureSize - rightPart;

                if (sourceRect[x].X % WallTextureSize < leftPart)
                {
                    sourceRect[x].X += rightPart;
                }
                else
                {
                    if (mapPoint != ignore && depth < 4)
                    {
                        CastLevel(x, grid, sourceRect, destinationRect, color, levelIndex, rayPosition, mapPoint, depth + 1);
                        return;
                    }
                }
            }

            // Vertical size of the slice
            destinationRect[x].Height = lineHeight;

            // Vertical position of the slice
            destinationRect[x].Y = lineY - lineHeight * levelIndex;

            if (lineHeight < Height / 8)
            {
                var light = (int)(lineHeight * 3.5f);
                color[x] = new Color(light, light, light);
            }
            else
            {
                color[x] = Color.White;
            }
            if (_hasFloor && levelIndex == 0)
            {
                CastFloor(x, rayDirection, mapPoint, perpendicularDistance, side, wallX);
            }
        }

        private void CastFloor(int x, Vector2 rayDirection, Point mapPoint, double perpendicularDistance, bool side, double wallX)
        {
            if (x % FloorPixelization.X != 0) return;

            var floorStart = WallLayers[0].DestinationRect[x].Bottom;

            double floorXWall, floorYWall;

            // 4 different wall directions possible
            if (side && rayDirection.X > 0)
            {
                floorXWall = mapPoint.X;
                floorYWall = mapPoint.Y + wallX;
            }
            else if (side && rayDirection.X < 0)
            {
                floorXWall = mapPoint.X + 1f;
                floorYWall = mapPoint.Y + wallX;
            }
            else if (!side && rayDirection.Y > 0)
            {
                floorXWall = mapPoint.X + wallX;
                floorYWall = mapPoint.Y;
            }
            else
            {
                floorXWall = mapPoint.X + wallX;
                floorYWall = mapPoint.Y + 1f;
            }

            double wallDistance, currentDistance;

            wallDistance = perpendicularDistance;

            // Why spend time on calculation of so long distanced pixel mess?
            var minY = _floorHeight + Height / 20;
            
            for (var y = floorStart; y < Height; y++)
            {
                if (y % FloorPixelization.Y != 0) continue;

                currentDistance = _floorDistance[y - _floorHeight];

                var weight = currentDistance / wallDistance;
                if (weight == 1f) weight += 0.0000001f;

                var currentFloorX = weight * floorXWall + (1f - weight) * Camera.Position.X;
                var currentFloorY = weight * floorYWall + (1f - weight) * Camera.Position.Y;

                var floorFrame = Map.Floor.Data[(int)currentFloorX, (int)currentFloorY] - 1;

                var projectionIndex = x / FloorPixelization.X + (y - _floorHeight) / FloorPixelization.Y * Width / FloorPixelization.X;

                if (floorFrame >= 0)
                {
                    var textureCoordinate = y < minY
                        ? new Point(
                            FloorTextureSize / 2 + floorFrame * FloorTextureSize,
                            FloorTextureSize / 2)
                        : new Point(
                            (int)(currentFloorX * FloorTextureSize) % FloorTextureSize + floorFrame * FloorTextureSize,
                            (int)(currentFloorY * FloorTextureSize) % FloorTextureSize);

                    // Write
                    var pixelIndex = textureCoordinate.X + textureCoordinate.Y * Map.Floor.TileSet.Texture.Width;

                    if (_floorData.Length > pixelIndex)
                    {
                        _floorProjection[projectionIndex] = _floorData[pixelIndex];
                    }
                }
                else
                {
                    _floorProjection[projectionIndex] = Color.Transparent;
                }
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private float FastSqrt(float number)
        {
            const float threehalfs = 1.5f;

            if (number == 0) return 0;
            var union = new EvilMagic();
            var x2 = number * 0.5f;
            union.Float = number;
            union.Int = 0x5f375a86 - (union.Int >> 1); // what the fuck? 🎮
            union.Float = union.Float * (threehalfs - x2 * union.Float * union.Float);
            return union.Float * number;
        }

        [StructLayout(LayoutKind.Explicit)]
        private struct EvilMagic
        {
            [FieldOffset(0)]
            public float Float;

            [FieldOffset(0)]
            public int Int;
        }

        enum Hit
        {
            None,
            Wall,
            Bound
        }
    }
}