package me.koba1.led.utils.gradient;

import me.koba1.led.model.colors.KobaColor;
import me.koba1.led.model.colors.KobaGradient;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

/**
 * Utility class for interpolating colors along a gradient using the OKLab color space.
 *
 * OKLab interpolates in Cartesian coordinates (L, a, b) — straight-line blending
 * that is perceptually uniform. Compared to OKLCH (polar form), transitions are
 * more neutral and predictable; highly saturated color pairs may pass through a
 * slightly desaturated midpoint, but there is no hue-spin artifact near greys.
 *
 * Usage:
 *   List<GradientStop> stops = List.of(
 *       new GradientStop(0.0f, new KobaColor("#ff0000")),
 *       new GradientStop(1.0f, new KobaColor("#0000ff"))
 *   );
 *   KobaColor color = OklabGradientUtils.getColorAt(stops, 150, 300);
 */
public class ClaudeOklabGradientUtils {

    // -------------------------------------------------------------------------
    // Public API
    // -------------------------------------------------------------------------

    /**
     * Returns the interpolated color for a given LED index on a strip.
     *
     * @param stops     List of GradientStop objects (position 0.0–1.0, any order)
     * @param ledIndex  0-based index of the LED (0 to ledCount-1)
     * @param ledCount  Total number of LEDs on the strip (e.g. 300)
     * @return          Interpolated KobaColor at that position
     */
    public static KobaColor getColorAt(KobaGradient gradient, int ledIndex, int ledCount) {
        if (gradient.getStops() == null || gradient.getStops().isEmpty()) {
            return new KobaColor("#000000");
        }
        if (ledCount <= 1) {
            return getColorAtPosition(gradient, 0.0);
        }
        double position = (double) ledIndex / (double) (ledCount - 1);
        return getColorAtPosition(gradient, position);
    }

    /**
     * Returns the interpolated color at a normalized position (0.0–1.0).
     *
     * @param stops    List of GradientStop objects
     * @param position Normalized position along the gradient (0.0 = start, 1.0 = end)
     * @return         Interpolated KobaColor
     */
    public static KobaColor getColorAtPosition(KobaGradient gradient, double position) {
        if (gradient.getStops() == null || gradient.getStops().isEmpty()) {
            return new KobaColor("#000000");
        }

        // Sort stops by position
        List<KobaGradient.GradientStop> sorted = new ArrayList<>(gradient.getStops());
        sorted.sort(Comparator.comparingDouble(KobaGradient.GradientStop::position));

        // Clamp position
        position = Math.max(0.0, Math.min(1.0, position));

        // Exact match or out-of-bounds → return edge color
        if (position <= sorted.get(0).position()) {
            return sorted.get(0).color();
        }
        if (position >= sorted.get(sorted.size() - 1).position()) {
            return sorted.get(sorted.size() - 1).color();
        }

        // Find the two surrounding stops
        KobaGradient.GradientStop lower = sorted.get(0);
        KobaGradient.GradientStop upper = sorted.get(sorted.size() - 1);
        for (int i = 0; i < sorted.size() - 1; i++) {
            if (sorted.get(i).position() <= position && sorted.get(i + 1).position() >= position) {
                lower = sorted.get(i);
                upper = sorted.get(i + 1);
                break;
            }
        }

        // Local t between the two stops
        double range = upper.position() - lower.position();
        double t = (range == 0.0) ? 0.0 : (position - lower.position()) / range;

        return interpolateOklab(lower.color(), upper.color(), t);
    }

    /**
     * Pre-bakes the full gradient into an array of KobaColors, one per LED.
     * Use this for performance-critical paths (e.g. live LED refresh).
     *
     * @param stops    List of GradientStop objects
     * @param ledCount Number of LEDs
     * @return         Array of KobaColor, indexed by LED position
     */
    public static KobaColor[] bakeGradient(KobaGradient gradient, int ledCount) {
        KobaColor[] result = new KobaColor[ledCount];
        for (int i = 0; i < ledCount; i++) {
            result[i] = getColorAt(gradient, i, ledCount);
        }
        return result;
    }

    // -------------------------------------------------------------------------
    // OKLab Interpolation
    // -------------------------------------------------------------------------

    /**
     * Interpolates between two KobaColors in OKLab space (L, a, b — Cartesian).
     * All three axes are linearly interpolated; no hue angle computation needed.
     */
    private static KobaColor interpolateOklab(KobaColor colorA, KobaColor colorB, double t) {
        double[] labA = hexToOklab(colorA.getHex());
        double[] labB = hexToOklab(colorB.getHex());

        double L = lerp(labA[0], labB[0], t);
        double a = lerp(labA[1], labB[1], t);
        double b = lerp(labA[2], labB[2], t);

        int[] rgb = oklabToRgb(L, a, b);
        return new KobaColor(toHex(rgb[0], rgb[1], rgb[2]));
    }

    // -------------------------------------------------------------------------
    // Color Space Conversions: HEX → sRGB → Linear RGB → OKLab  (and back)
    // -------------------------------------------------------------------------

    /** Hex string (with or without #) → OKLab [L, a, b] */
    private static double[] hexToOklab(String hex) {
        int[] rgb = hexToRgb(hex);
        double[] linear = srgbToLinear(rgb[0] / 255.0, rgb[1] / 255.0, rgb[2] / 255.0);
        return linearRgbToOklab(linear[0], linear[1], linear[2]);
    }

    /** OKLab [L, a, b] → Linear RGB → sRGB → int[R, G, B] */
    private static int[] oklabToRgb(double L, double a, double b) {
        double[] linear = oklabToLinearRgb(L, a, b);
        double[] srgb = linearToSrgb(linear[0], linear[1], linear[2]);
        return new int[]{
                clamp((int) Math.round(srgb[0] * 255), 0, 255),
                clamp((int) Math.round(srgb[1] * 255), 0, 255),
                clamp((int) Math.round(srgb[2] * 255), 0, 255)
        };
    }

    // -------------------------------------------------------------------------
    // sRGB ↔ Linear RGB
    // -------------------------------------------------------------------------

    private static double[] srgbToLinear(double r, double g, double b) {
        return new double[]{srgbChannelToLinear(r), srgbChannelToLinear(g), srgbChannelToLinear(b)};
    }

    private static double srgbChannelToLinear(double c) {
        return (c <= 0.04045) ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
    }

    private static double[] linearToSrgb(double r, double g, double b) {
        return new double[]{linearChannelToSrgb(r), linearChannelToSrgb(g), linearChannelToSrgb(b)};
    }

    private static double linearChannelToSrgb(double c) {
        c = Math.max(0.0, Math.min(1.0, c));
        return (c <= 0.0031308) ? c * 12.92 : 1.055 * Math.pow(c, 1.0 / 2.4) - 0.055;
    }

    // -------------------------------------------------------------------------
    // Linear RGB ↔ OKLab
    // Based on Björn Ottosson's spec: https://bottosson.github.io/posts/oklab/
    // -------------------------------------------------------------------------

    private static double[] linearRgbToOklab(double r, double g, double b) {
        double l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
        double m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
        double s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;

        double l_ = Math.cbrt(l);
        double m_ = Math.cbrt(m);
        double s_ = Math.cbrt(s);

        return new double[]{
                0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
                1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
                0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
        };
    }

    private static double[] oklabToLinearRgb(double L, double a, double b) {
        double l_ = L + 0.3963377774 * a + 0.2158037573 * b;
        double m_ = L - 0.1055613458 * a - 0.0638541728 * b;
        double s_ = L - 0.0894841775 * a - 1.2914855480 * b;

        double l = l_ * l_ * l_;
        double m = m_ * m_ * m_;
        double s = s_ * s_ * s_;

        return new double[]{
                +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
                -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
                -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
        };
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private static double lerp(double a, double b, double t) {
        return a + (b - a) * t;
    }

    private static int[] hexToRgb(String hex) {
        String clean = hex.startsWith("#") ? hex.substring(1) : hex;
        if (clean.length() == 3) {
            clean = "" + clean.charAt(0) + clean.charAt(0)
                    + clean.charAt(1) + clean.charAt(1)
                    + clean.charAt(2) + clean.charAt(2);
        }
        return new int[]{
                Integer.parseInt(clean.substring(0, 2), 16),
                Integer.parseInt(clean.substring(2, 4), 16),
                Integer.parseInt(clean.substring(4, 6), 16)
        };
    }

    private static String toHex(int r, int g, int b) {
        return String.format("#%02x%02x%02x", r, g, b);
    }

    private static int clamp(int value, int min, int max) {
        return Math.max(min, Math.min(max, value));
    }
}