using System;
using Minint.Core.Models;

namespace Minint.Core.Services.Impl;

public sealed class PatternGenerator : IPatternGenerator
{
    public MinintDocument Generate(PatternType type, int width, int height, RgbaColor[] colors, int param1, int param2 = 0)
    {
        ArgumentOutOfRangeException.ThrowIfLessThan(width, 1);
        ArgumentOutOfRangeException.ThrowIfLessThan(height, 1);
        if (colors.Length < 2)
            throw new ArgumentException("At least two colors are required.", nameof(colors));

        var doc = new MinintDocument($"Pattern ({type})");
        var layer = new MinintLayer("Pattern", width * height);
        doc.Layers.Add(layer);

        int[] colorIndices = new int[colors.Length];
        for (int i = 0; i < colors.Length; i++)
            colorIndices[i] = doc.EnsureColorCached(colors[i]);

        int cellSize = Math.Max(param1, 1);

        switch (type)
        {
            case PatternType.Checkerboard:
                FillCheckerboard(layer.Pixels, width, height, colorIndices, cellSize);
                break;
            case PatternType.HorizontalGradient:
                FillGradient(layer.Pixels, width, height, colors[0], colors[1], doc, horizontal: true);
                break;
            case PatternType.VerticalGradient:
                FillGradient(layer.Pixels, width, height, colors[0], colors[1], doc, horizontal: false);
                break;
            case PatternType.HorizontalStripes:
                FillStripes(layer.Pixels, width, height, colorIndices, cellSize, horizontal: true);
                break;
            case PatternType.VerticalStripes:
                FillStripes(layer.Pixels, width, height, colorIndices, cellSize, horizontal: false);
                break;
            case PatternType.ConcentricCircles:
                FillCircles(layer.Pixels, width, height, colorIndices, cellSize);
                break;
            case PatternType.Tile:
                FillTile(layer.Pixels, width, height, colorIndices, cellSize, Math.Max(param2, 1));
                break;
        }

        return doc;
    }

    private static void FillCheckerboard(int[] pixels, int w, int h, int[] ci, int cell)
    {
        for (int y = 0; y < h; y++)
            for (int x = 0; x < w; x++)
                pixels[y * w + x] = ci[((x / cell) + (y / cell)) % 2 == 0 ? 0 : 1];
    }

    private static void FillGradient(int[] pixels, int w, int h, RgbaColor c0, RgbaColor c1,
                                      MinintDocument doc, bool horizontal)
    {
        int steps = horizontal ? w : h;
        for (int s = 0; s < steps; s++)
        {
            double t = steps > 1 ? (double)s / (steps - 1) : 0;
            var c = new RgbaColor(
                Lerp(c0.R, c1.R, t), Lerp(c0.G, c1.G, t),
                Lerp(c0.B, c1.B, t), Lerp(c0.A, c1.A, t));
            int idx = doc.EnsureColorCached(c);
            if (horizontal)
                for (int y = 0; y < h; y++) pixels[y * w + s] = idx;
            else
                for (int x = 0; x < w; x++) pixels[s * w + x] = idx;
        }
    }

    private static void FillStripes(int[] pixels, int w, int h, int[] ci, int stripe, bool horizontal)
    {
        for (int y = 0; y < h; y++)
            for (int x = 0; x < w; x++)
            {
                int coord = horizontal ? y : x;
                pixels[y * w + x] = ci[(coord / stripe) % ci.Length];
            }
    }

    private static void FillCircles(int[] pixels, int w, int h, int[] ci, int ringWidth)
    {
        double cx = (w - 1) / 2.0, cy = (h - 1) / 2.0;
        for (int y = 0; y < h; y++)
            for (int x = 0; x < w; x++)
            {
                double dist = Math.Sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy));
                int ring = (int)(dist / ringWidth);
                pixels[y * w + x] = ci[ring % ci.Length];
            }
    }

    private static void FillTile(int[] pixels, int w, int h, int[] ci, int tileW, int tileH)
    {
        for (int y = 0; y < h; y++)
            for (int x = 0; x < w; x++)
            {
                int tx = (x / tileW) % ci.Length;
                int ty = (y / tileH) % ci.Length;
                pixels[y * w + x] = ci[(tx + ty) % ci.Length];
            }
    }

    private static byte Lerp(byte a, byte b, double t)
        => (byte)Math.Clamp((int)(a + (b - a) * t + 0.5), 0, 255);
}
