<?php
declare(strict_types=1);

namespace Instinctor\Helpers;

if ( ! defined( 'ABSPATH' ) ) { exit; }

/**
 * Crop / silhouette path builder — server-side counterpart to
 * assets/js/impulse-crop-overlay.js.
 *
 * Reads the saved corner / node / tangent settings for a block and emits
 * a `<svg><clipPath/></svg>` snippet plus the matching `clip-path` CSS to
 * attach to the wrapper. The path uses closed-loop Catmull-Rom with
 * optional per-anchor tangent overrides. Mirrors the editor overlay's
 * path math byte-for-byte so the live preview equals the rendered HTML.
 *
 * Block prefix lets image and video share the same engine while storing
 * data under their own setting keys ('image_crop_' and 'video_crop_').
 *
 * Extracted from element-renderers/image.php in v2.1.1274.
 */
class CropPath
{
    /** Catmull-Rom auto-tangent magnitude factor. Mirrors AUTO_T_FACTOR
     *  in assets/js/impulse-crop-overlay.js. */
    private const AUTO_T_FACTOR = 1.0 / 20.0;

    /**
     * Build the clip-path SVG + CSS for a wrapper.
     *
     * @param array  $settings    Element settings (raw, from DB JSON).
     * @param int    $elId        Element ID (used to make a unique clipPath id).
     * @param string $keyPrefix   Setting-key prefix. Image block uses
     *                            'image_crop_', video block uses 'video_crop_'.
     * @return array { css: string, svg: string, is_full_rect: bool }
     */
    public static function build(array $settings, int $elId, string $keyPrefix = 'image_crop_'): array
    {
        $k = static fn(string $suffix): string => $keyPrefix . $suffix;

        // ── Parse corners ──────────────────────────────────────────────
        // Legacy inset insets ([0..50]%) seed corner defaults.
        $_oT = max(0, min(50, isset($settings[$k('top')])    ? (int) $settings[$k('top')]    : 0));
        $_oR = max(0, min(50, isset($settings[$k('right')])  ? (int) $settings[$k('right')]  : 0));
        $_oB = max(0, min(50, isset($settings[$k('bottom')]) ? (int) $settings[$k('bottom')] : 0));
        $_oL = max(0, min(50, isset($settings[$k('left')])   ? (int) $settings[$k('left')]   : 0));

        $_parseXY = static function ($v, array $fb): array {
            if (!is_string($v) || !str_contains($v, ',')) return $fb;
            $a = array_map('floatval', explode(',', $v, 2));
            if (count($a) < 2) return $fb;
            return [max(-100.0, min(200.0, $a[0])), max(-100.0, min(200.0, $a[1]))];
        };

        $_tl = $_parseXY($settings[$k('tl')] ?? '', [(float)$_oL,  (float)$_oT]);
        $_tr = $_parseXY($settings[$k('tr')] ?? '', [100.0 - $_oR, (float)$_oT]);
        $_br = $_parseXY($settings[$k('br')] ?? '', [100.0 - $_oR, 100.0 - $_oB]);
        $_bl = $_parseXY($settings[$k('bl')] ?? '', [(float)$_oL,  100.0 - $_oB]);

        // ── Cubic c1/c2 (legacy) ───────────────────────────────────────
        // The closed-loop Catmull-Rom engine ignores these in path output
        // but we still parse them for the full-rect detection below.
        $_third     = static fn(array $a, array $b): array =>
            [$a[0] + ($b[0] - $a[0]) / 3.0, $a[1] + ($b[1] - $a[1]) / 3.0];
        $_twoThirds = static fn(array $a, array $b): array =>
            [$a[0] + 2.0 * ($b[0] - $a[0]) / 3.0, $a[1] + 2.0 * ($b[1] - $a[1]) / 3.0];
        $_defTopC1    = $_third($_tl, $_tr);    $_defTopC2    = $_twoThirds($_tl, $_tr);
        $_defRightC1  = $_third($_tr, $_br);    $_defRightC2  = $_twoThirds($_tr, $_br);
        $_defBottomC1 = $_third($_br, $_bl);    $_defBottomC2 = $_twoThirds($_br, $_bl);
        $_defLeftC1   = $_third($_bl, $_tl);    $_defLeftC2   = $_twoThirds($_bl, $_tl);

        $_quadToCubic = static function ($oldKey, array $p0, array $p2, array $defC1, array $defC2) use ($settings, $_parseXY) {
            $oldQ = $_parseXY($settings[$oldKey] ?? '', []);
            if (count($oldQ) < 2) return [$defC1, $defC2];
            $c1 = [$p0[0] + 2.0/3.0 * ($oldQ[0] - $p0[0]), $p0[1] + 2.0/3.0 * ($oldQ[1] - $p0[1])];
            $c2 = [$p2[0] + 2.0/3.0 * ($oldQ[0] - $p2[0]), $p2[1] + 2.0/3.0 * ($oldQ[1] - $p2[1])];
            return [$c1, $c2];
        };
        [$_seedTopC1, $_seedTopC2]       = $_quadToCubic($k('top_c'),    $_tl, $_tr, $_defTopC1,    $_defTopC2);
        [$_seedRightC1, $_seedRightC2]   = $_quadToCubic($k('right_c'),  $_tr, $_br, $_defRightC1,  $_defRightC2);
        [$_seedBottomC1, $_seedBottomC2] = $_quadToCubic($k('bottom_c'), $_br, $_bl, $_defBottomC1, $_defBottomC2);
        [$_seedLeftC1, $_seedLeftC2]     = $_quadToCubic($k('left_c'),   $_bl, $_tl, $_defLeftC1,   $_defLeftC2);
        $_tC1 = $_parseXY($settings[$k('top_c1')]    ?? '', $_seedTopC1);
        $_tC2 = $_parseXY($settings[$k('top_c2')]    ?? '', $_seedTopC2);
        $_rC1 = $_parseXY($settings[$k('right_c1')]  ?? '', $_seedRightC1);
        $_rC2 = $_parseXY($settings[$k('right_c2')]  ?? '', $_seedRightC2);
        $_bC1 = $_parseXY($settings[$k('bottom_c1')] ?? '', $_seedBottomC1);
        $_bC2 = $_parseXY($settings[$k('bottom_c2')] ?? '', $_seedBottomC2);
        $_lC1 = $_parseXY($settings[$k('left_c1')]   ?? '', $_seedLeftC1);
        $_lC2 = $_parseXY($settings[$k('left_c2')]   ?? '', $_seedLeftC2);

        // ── Parse nodes (per side) ─────────────────────────────────────
        $_parseNodes = static function ($v): array {
            if (!is_string($v) || $v === '') return [];
            $arr = json_decode($v, true);
            if (!is_array($arr)) return [];
            $nodes = [];
            foreach ($arr as $n) {
                if (!is_array($n)) continue;
                if (!isset($n['x'], $n['y'])) continue;
                $node = [
                    'p' => [
                        max(0.0, min(100.0, (float) $n['x'])),
                        max(0.0, min(100.0, (float) $n['y'])),
                    ],
                    't' => null,
                ];
                if (isset($n['tx'], $n['ty']) && is_numeric($n['tx']) && is_numeric($n['ty'])) {
                    $node['t'] = [
                        max(-200.0, min(200.0, (float) $n['tx'])),
                        max(-200.0, min(200.0, (float) $n['ty'])),
                    ];
                }
                $nodes[] = $node;
            }
            return $nodes;
        };
        $_topNodes    = $_parseNodes($settings[$k('top_nodes')]    ?? '');
        $_rightNodes  = $_parseNodes($settings[$k('right_nodes')]  ?? '');
        $_bottomNodes = $_parseNodes($settings[$k('bottom_nodes')] ?? '');
        $_leftNodes   = $_parseNodes($settings[$k('left_nodes')]   ?? '');
        $_hasNodes    = !empty($_topNodes) || !empty($_rightNodes) || !empty($_bottomNodes) || !empty($_leftNodes);

        // ── Full-rect detection ────────────────────────────────────────
        $_eq = static fn(array $a, array $b): bool => abs($a[0]-$b[0]) < 0.01 && abs($a[1]-$b[1]) < 0.01;
        $_isFullRect = $_eq($_tl,[0,0]) && $_eq($_tr,[100,0]) && $_eq($_br,[100,100]) && $_eq($_bl,[0,100])
                       && $_eq($_tC1,$_defTopC1)    && $_eq($_tC2,$_defTopC2)
                       && $_eq($_rC1,$_defRightC1)  && $_eq($_rC2,$_defRightC2)
                       && $_eq($_bC1,$_defBottomC1) && $_eq($_bC2,$_defBottomC2)
                       && $_eq($_lC1,$_defLeftC1)   && $_eq($_lC2,$_defLeftC2);
        if ($_hasNodes) $_isFullRect = false;
        if (!empty($settings[$k('tl_tangent')]) || !empty($settings[$k('tr_tangent')])
            || !empty($settings[$k('br_tangent')]) || !empty($settings[$k('bl_tangent')])) {
            $_isFullRect = false;
        }

        if ($_isFullRect) {
            return ['css' => '', 'svg' => '', 'is_full_rect' => true];
        }

        // ── Parse per-corner tangent overrides ─────────────────────────
        $_parseTangent = static function ($v): ?array {
            if (!is_string($v) || !str_contains($v, ',')) return null;
            $a = array_map('floatval', explode(',', $v, 2));
            if (count($a) < 2) return null;
            return [max(-200.0, min(200.0, $a[0])), max(-200.0, min(200.0, $a[1]))];
        };
        $_tlT = $_parseTangent($settings[$k('tl_tangent')] ?? '');
        $_trT = $_parseTangent($settings[$k('tr_tangent')] ?? '');
        $_brT = $_parseTangent($settings[$k('br_tangent')] ?? '');
        $_blT = $_parseTangent($settings[$k('bl_tangent')] ?? '');

        // ── Build anchor sequence ──────────────────────────────────────
        $_anchors = [['p' => $_tl, 't' => $_tlT]];
        foreach ($_topNodes    as $_n) $_anchors[] = $_n;
        $_anchors[] = ['p' => $_tr, 't' => $_trT];
        foreach ($_rightNodes  as $_n) $_anchors[] = $_n;
        $_anchors[] = ['p' => $_br, 't' => $_brT];
        foreach ($_bottomNodes as $_n) $_anchors[] = $_n;
        $_anchors[] = ['p' => $_bl, 't' => $_blT];
        foreach ($_leftNodes   as $_n) $_anchors[] = $_n;
        $_nA = count($_anchors);

        // ── Resolve effective tangent at each anchor ──────────────────
        $_eff = [];
        for ($_i = 0; $_i < $_nA; $_i++) {
            if ($_anchors[$_i]['t'] !== null) {
                $_eff[$_i] = $_anchors[$_i]['t'];
            } else {
                $_prevP = $_anchors[($_i - 1 + $_nA) % $_nA]['p'];
                $_nextP = $_anchors[($_i + 1) % $_nA]['p'];
                $_eff[$_i] = [
                    ($_nextP[0] - $_prevP[0]) * self::AUTO_T_FACTOR,
                    ($_nextP[1] - $_prevP[1]) * self::AUTO_T_FACTOR,
                ];
            }
        }

        // ── Walk the closed loop and emit cubic segments ──────────────
        $_obb = static fn(array $pt): string =>
            number_format($pt[0]/100, 4, '.', '') . ',' . number_format($pt[1]/100, 4, '.', '');

        $_pathD = 'M ' . $_obb($_anchors[0]['p']);
        for ($_i = 0; $_i < $_nA; $_i++) {
            $_p0 = $_anchors[$_i]['p'];
            $_p1 = $_anchors[($_i + 1) % $_nA]['p'];
            $_t0 = $_eff[$_i];
            $_t1 = $_eff[($_i + 1) % $_nA];
            $_c1 = [$_p0[0] + $_t0[0], $_p0[1] + $_t0[1]];
            $_c2 = [$_p1[0] - $_t1[0], $_p1[1] - $_t1[1]];
            $_pathD .= ' C ' . $_obb($_c1) . ' ' . $_obb($_c2) . ' ' . $_obb($_p1);
        }
        $_pathD .= ' Z';

        $_clipId = 'impulse-crop-clip-' . intval($elId);
        $svg = '<svg width="0" height="0" style="position:absolute;width:0;height:0;overflow:hidden" aria-hidden="true">'
             . '<clipPath id="' . esc_attr($_clipId) . '" clipPathUnits="objectBoundingBox">'
             . '<path d="' . esc_attr($_pathD) . '"/>'
             . '</clipPath></svg>';
        $css = '-webkit-clip-path:url(#' . esc_attr($_clipId) . ');clip-path:url(#' . esc_attr($_clipId) . ');';

        return ['css' => $css, 'svg' => $svg, 'is_full_rect' => false];
    }
}
