using System;
using System.Linq;
using Bearded.Utilities.Geometry;
using OpenTK.Mathematics;

namespace Bearded.Utilities.Noise;

public static class PerlinNoise
{
    // Use uniformly distributed vectors to force better gradient distribution.
    private const int numVectorSamples = 12;
    private static readonly Vector2d[] vectorSamples = vectorSamples = Enumerable.Range(0, numVectorSamples)
        .Select(i => Direction2.FromRadians(MathConstants.TwoPi * i / numVectorSamples).Vector)
        .Select(vector => new Vector2d(vector.X, vector.Y))
        .ToArray();

    /// <summary>
    /// Generates a procedural texture using Perlin noise.
    /// </summary>
    /// <remarks>
    /// Perlin noise is generated by first generating a grid of gradient vectors. The texture is then filled in by
    /// for each point identifying the four surrounding gradient vectors, calculating the dot product between each
    /// of the gradient vectors and corresponding offset vector, and interpolating between them.
    /// </remarks>
    /// <param name="numCellsX">
    /// The number of cells in which the texture is divided along the horizontal axis (see remarks).
    /// </param>
    /// <param name="numCellsY">
    /// The number of cells in which the texture is divided along the vertical axis (see remarks).
    /// </param>
    /// <param name="random"></param>
    /// <returns></returns>
    /// <exception cref="ArgumentOutOfRangeException"></exception>
    public static IProceduralTexture Generate(int numCellsX, int numCellsY, Random? random)
    {
        if (numCellsX <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(numCellsX));
        }
        if (numCellsY <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(numCellsY));
        }

        var gradientArray = NoiseUtils.GenerateRandomArray(
            // We generate the corners, but to make the noise map wrap along both dimensions, we reuse the same
            // values as the left and top boundaries, so we don't have to generate the right and bottom boundaries.
            numCellsX,
            numCellsY,
            r => vectorSamples[r.Next(vectorSamples.Length)],
            random);
        return new PerlinProceduralTexture(gradientArray);
    }

    private sealed class PerlinProceduralTexture : IProceduralTexture
    {
        private readonly Vector2d[,] gradientArray;
        private readonly int width;
        private readonly int height;

        public PerlinProceduralTexture(Vector2d[,] gradientArray)
        {
            this.gradientArray = gradientArray;
            width = gradientArray.GetLength(0);
            height = gradientArray.GetLength(1);
        }

        public double ValueAt(double x, double y)
        {
            if (x < 0 || x >= 1)
            {
                throw new ArgumentOutOfRangeException(nameof(x));
            }
            if (y < 0 || y >= 1)
            {
                throw new ArgumentOutOfRangeException(nameof(y));
            }

            scaleToWidthHeight(ref x, ref y);
            var (xBelow, xAbove, yBelow, yAbove) = getGridCorners(x, y);
            var u = x - xBelow;
            var v = y - yBelow;

            var topLeft = dotProductWithGridDirection(xBelow, yAbove, x, y);
            var topRight = dotProductWithGridDirection(xAbove, yAbove, x, y);
            var bottomLeft = dotProductWithGridDirection(xBelow, yBelow, x, y);
            var bottomRight = dotProductWithGridDirection(xAbove, yBelow, x, y);

            var (tx, ty) = getInterpolationWeights(u, 1 - v);

            var r = Interpolation2d.BiLinear.Interpolate(bottomLeft, bottomRight, topLeft, topRight, tx, ty);

            return normalizeDotProductToUnitScale(r);
        }

        private void scaleToWidthHeight(ref double x, ref double y)
        {
            x *= width;
            y *= height;
        }

        private static (int xBelow, int xAbove, int yBelow, int yAbove) getGridCorners(double x, double y)
        {
            var xBelow = (int) x;
            var xAbove = xBelow + 1;

            var yBelow = (int) y;
            var yAbove = yBelow + 1;

            return (xBelow, xAbove, yBelow, yAbove);
        }

        private double dotProductWithGridDirection(int gridX, int gridY, double x, double y)
        {
            var distance = new Vector2d(x - gridX, y - gridY);
            return Vector2d.Dot(distance, gradientArray[gridX % width, gridY % height]);
        }

        private static (double tx, double ty) getInterpolationWeights(double u, double v)
        {
            var tx = Interpolation1d.SmoothStep.Interpolate(0, 1, u);
            var ty = 1 - Interpolation1d.SmoothStep.Interpolate(0, 1, v);
            return (tx, ty);
        }

        private static double normalizeDotProductToUnitScale(double dotProduct) => (dotProduct + 1) * 0.5;
    }
}
