<?php

declare(strict_types=1);

namespace PpPhp\Core\Theme;

/**
 * Small colour utility used to DERIVE theme tokens.
 *
 * A theme author sets a handful of base colours (primary, success, surface…)
 * and the compiler derives the rest — soft tints, borders, readable on-colours,
 * hover/selected row states. That keeps every theme complete by construction:
 * no theme can be "missing" a token, so no component ever has to fall back to a
 * hardcoded colour.
 *
 * Maths is done in PHP rather than CSS color-mix() so the output is a plain
 * colour value that works in every browser and can be inspected in the
 * generated stylesheet.
 */
final class Color
{
    /** Parse #rgb/#rrggbb/#rrggbbaa/rgb()/rgba() → [r, g, b, a] or null. */
    public static function parse(string $c): ?array
    {
        $c = trim($c);
        if ($c === '' || str_starts_with($c, 'var(')) return null;

        if ($c[0] === '#') {
            $h = substr($c, 1);
            if (strlen($h) === 3) $h = $h[0] . $h[0] . $h[1] . $h[1] . $h[2] . $h[2];
            if (strlen($h) === 4) $h = $h[0] . $h[0] . $h[1] . $h[1] . $h[2] . $h[2] . $h[3] . $h[3];
            if (!preg_match('/^[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$/', $h)) return null;
            return [
                hexdec(substr($h, 0, 2)),
                hexdec(substr($h, 2, 2)),
                hexdec(substr($h, 4, 2)),
                strlen($h) === 8 ? round(hexdec(substr($h, 6, 2)) / 255, 3) : 1.0,
            ];
        }
        if (preg_match('/^rgba?\(([^)]+)\)$/i', $c, $m)) {
            $p = array_map('trim', preg_split('/[,\s\/]+/', trim($m[1])) ?: []);
            if (count($p) < 3) return null;
            return [(float) $p[0], (float) $p[1], (float) $p[2], isset($p[3]) ? (float) $p[3] : 1.0];
        }
        return null;
    }

    /** [r,g,b,a] → the most compact CSS representation. */
    public static function css(array $c): string
    {
        [$r, $g, $b, $a] = [max(0, min(255, (int) round($c[0]))), max(0, min(255, (int) round($c[1]))), max(0, min(255, (int) round($c[2]))), $c[3] ?? 1.0];
        if ($a >= 1) return sprintf('#%02x%02x%02x', $r, $g, $b);
        return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . rtrim(rtrim(number_format($a, 3, '.', ''), '0'), '.') . ')';
    }

    /** Blend $a over $b; $w is the weight of $a (0..1). Returns '' if unparseable. */
    public static function mix(string $a, string $b, float $w): string
    {
        $ca = self::parse($a); $cb = self::parse($b);
        if (!$ca || !$cb) return '';
        $w = max(0.0, min(1.0, $w));
        return self::css([
            $ca[0] * $w + $cb[0] * (1 - $w),
            $ca[1] * $w + $cb[1] * (1 - $w),
            $ca[2] * $w + $cb[2] * (1 - $w),
            1.0,
        ]);
    }

    /** Same colour at a given opacity. */
    public static function alpha(string $c, float $a): string
    {
        $p = self::parse($c);
        if (!$p) return '';
        return self::css([$p[0], $p[1], $p[2], max(0.0, min(1.0, $a))]);
    }

    /** WCAG relative luminance (0 = black, 1 = white). */
    public static function luminance(string $c): float
    {
        $p = self::parse($c);
        if (!$p) return 1.0;
        $ch = [];
        foreach ([$p[0], $p[1], $p[2]] as $v) {
            $v /= 255;
            $ch[] = $v <= 0.03928 ? $v / 12.92 : (($v + 0.055) / 1.055) ** 2.4;
        }
        return 0.2126 * $ch[0] + 0.7152 * $ch[1] + 0.0722 * $ch[2];
    }

    public static function isDark(string $c): bool
    {
        return self::luminance($c) < 0.4;
    }

    /** Whichever of black/white reads better on $bg — used for on-colour text. */
    public static function readable(string $bg, string $onLight = '#0f172a', string $onDark = '#ffffff'): string
    {
        return self::isDark($bg) ? $onDark : $onLight;
    }

    /** Move a colour toward white (positive) or black (negative) by $amount (0..1). */
    public static function shade(string $c, float $amount): string
    {
        return $amount >= 0 ? self::mix('#ffffff', $c, $amount) : self::mix('#000000', $c, -$amount);
    }

    /**
     * A version of $c guaranteed to read against $bg — nudges toward the
     * opposite end until it clears a contrast ratio, so status text stays
     * legible in both light and dark themes.
     */
    public static function contrastOn(string $c, string $bg, float $ratio = 4.0): string
    {
        if (!self::parse($c) || !self::parse($bg)) return $c;
        $towardWhite = self::isDark($bg);
        $out = $c;
        for ($i = 0; $i < 12; $i++) {
            if (self::ratio($out, $bg) >= $ratio) return $out;
            $out = self::shade($out, $towardWhite ? 0.08 : -0.08);
        }
        return $out;
    }

    public static function ratio(string $a, string $b): float
    {
        $la = self::luminance($a); $lb = self::luminance($b);
        [$hi, $lo] = $la > $lb ? [$la, $lb] : [$lb, $la];
        return ($hi + 0.05) / ($lo + 0.05);
    }
}
