/*!
 * Gutenberg CSS color token picker overlay for textarea.inspector-textarea-control-2
 * - Click color tokens (#rgb[a], #rrggbb[aa], rgb[a](), hsl[a]()) to open a color picker.
 * - Changing color updates the underlying textarea at the exact token range.
 * - Preserves original notation family and, when feasible, separators/units.
 *
 * External libs (loaded dynamically):
 *  - Pickr (UI): https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/pickr.min.js  (theme CSS required)
 *  - Colord (parsing/formatting): ESM at https://unpkg.com/colord@2.9.3/index.mjs
 */

(() => {
  'use strict';

  // -----------------------
  // Configuration
  // -----------------------
  const TARGET_SELECTOR = 'textarea.inspector-textarea-control-2';
  const PICKR_THEME = 'nano'; // 'classic' | 'monolith' | 'nano'
  const PICKR_THEME_CSS = `https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/${PICKR_THEME}.min.css`;
  const PICKR_SCRIPT = 'https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/pickr.min.js';
  const COLORD_ESM = 'https://unpkg.com/colord@2.9.3/index.mjs';

  // -----------------------
  // Regexes (global, case-insensitive)
  // -----------------------
  // Hex: #rgb, #rgba, #rrggbb, #rrggbbaa
  const HEX_RE = /#[a-f\d]{3}(?:[a-f\d]?|(?:[a-f\d]{3}(?:[a-f\d]{2})?)?)\b/ig;

  // rgb[a](): legacy commas or modern space-separated; optional alpha via comma or slash
  const RGB_RE = /rgba?\((?:(25[0-5]|2[0-4]\d|1?\d{1,2}|(?:\d{1,2}|100)%)\s*,\s*(25[0-5]|2[0-4]\d|1?\d{1,2}|(?:\d{1,2}|100)%)\s*,\s*(25[0-5]|2[0-4]\d|1?\d{1,2}|(?:\d{1,2}|100)%)(?:\s*,\s*((?:\d{1,2}|100)%|0(?:\.\d+)?|1))?|(25[0-5]|2[0-4]\d|1?\d{1,2}|(?:\d{1,2}|100)%)\s+(25[0-5]|2[0-4]\d|1?\d{1,2}|(?:\d{1,2}|100)%)\s+(25[0-5]|2[0-4]\d|1?\d{1,2}|(?:\d{1,2}|100)%)(?:\s*\/\s*((?:\d{1,2}|100)%|0(?:\.\d+)?|1))?)\)/ig;

  // hsl[a](): legacy commas or modern space-separated; optional alpha via comma or slash; hue units supported
  const HSL_RE = /hsla?\((?:(-?\d+(?:deg|g?rad|turn)?),\s*((?:\d{1,2}|100)%),\s*((?:\d{1,2}|100)%)(?:,\s*((?:\d{1,2}|100)%|0(?:\.\d+)?|1))?|(-?\d+(?:deg|g?rad|turn)?)\s+((?:\d{1,2}|100)%)\s+((?:\d{1,2}|100)%)(?:\s*\/\s*((?:\d{1,2}|100)%|0(?:\.\d+)?|1))?)\)/ig;

  // -----------------------
  // Utilities: loading, timing
  // -----------------------
  const loadCSS = href => new Promise((resolve, reject) => {
    const already = [...document.styleSheets].some(s => s.href && s.href.includes(href));
    if (already) return resolve();
    const link = document.createElement('link');
    link.rel = 'stylesheet'; link.href = href;
    link.onload = resolve; link.onerror = reject;
    document.head.appendChild(link);
  });

  const loadScript = src => new Promise((resolve, reject) => {
    const already = [...document.scripts].some(s => s.src && s.src.includes(src));
    if (already) return resolve();
    const s = document.createElement('script');
    s.src = src;
    // per Pickr docs: do not use defer; load synchronously
    s.async = false;
    s.onload = resolve; s.onerror = reject;
    document.head.appendChild(s);
  });

  const debounce = (fn, ms = 80) => {
    let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
  };

  const raf = cb => requestAnimationFrame(cb);

  const escapeHTML = s => s
    .replace(/&/g, '&amp;').replace(/</g, '&lt;')
    .replace(/>/g, '&gt;').replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');

  // -----------------------
  // Tokenizing and formatting helpers
  // -----------------------
  function findColorTokens(text) {
    const tokens = [];
    const addMatches = (re, type) => {
      re.lastIndex = 0;
      let m;
      while ((m = re.exec(text)) !== null) {
        tokens.push({ type, start: m.index, end: re.lastIndex, value: m[0] });
      }
    };
    addMatches(HEX_RE, 'hex');
    addMatches(RGB_RE, 'rgb');
    addMatches(HSL_RE, 'hsl');

    tokens.sort((a, b) => a.start - b.start);
    // remove overlaps conservatively
    const out = [];
    let lastEnd = -1;
    for (const t of tokens) {
      if (t.start >= lastEnd) { out.push(t); lastEnd = t.end; }
    }
    return out;
  }

  // Preserve hex case/length where possible
  const isUppercaseHex = s => /[A-F]/.test(s) && !/[a-f]/.test(s);
  function desiredHexLength(orig) {
    const n = orig.length;
    if (n === 4) return 3; // #rgb
    if (n === 5) return 4; // #rgba
    if (n === 7) return 6; // #rrggbb
    if (n === 9) return 8; // #rrggbbaa
    return 6;
  }
  function pairsToShort(R, G, B, A /* strings like 'ff' */) {
    const canShort = [R, G, B, A || ''].filter(Boolean)
      .every(p => p.length === 2 && p[0].toLowerCase() === p[1].toLowerCase());
    if (!canShort) return null;
    return [R[0], G[0], B[0]].concat(A ? [A[0]] : []).join('');
  }

  function parseRgbStyle(orig) {
    const usesCommas = orig.includes(',');
    const usesSlash = orig.includes('/');
    const inside = orig.slice(orig.indexOf('(') + 1, orig.lastIndexOf(')'));
    const parts = usesCommas ? inside.split(',') : inside.split(/\s*\/\s*|\s+/);
    const channels = parts.slice(0, 3);
    const alpha = parts[3] ?? null;
    const channelPercent = channels.some(s => /%/.test(s));
    const alphaPercent = alpha ? /%/.test(alpha) : false;
    const fnName = orig.slice(0, orig.indexOf('(')).toLowerCase(); // rgb or rgba
    return { usesCommas, usesSlash, channelPercent, alphaPercent, fnName };
  }

  function parseHslStyle(orig) {
    const usesCommas = orig.includes(',');
    const usesSlash = orig.includes('/');
    const inside = orig.slice(orig.indexOf('(') + 1, orig.lastIndexOf(')'));
    const parts = usesCommas ? inside.split(',') : inside.split(/\s*\/\s*|\s+/);
    const hueUnitMatch = (parts[0] || '').match(/(deg|rad|grad|turn)/i);
    const hueUnit = hueUnitMatch ? hueUnitMatch[1].toLowerCase() : 'deg';
    const alpha = parts[3] ?? null;
    const alphaPercent = alpha ? /%/.test(alpha) : false;
    const fnName = orig.slice(0, orig.indexOf('(')).toLowerCase(); // hsl or hsla
    return { usesCommas, usesSlash, hueUnit, alphaPercent, fnName };
  }

  // -----------------------
  // External libs on-demand
  // -----------------------
  let pickr = null;
  let colordModule = null;

  async function ensureLibs() {
    if (!window.Pickr) {
      await loadCSS(PICKR_THEME_CSS);
      await loadScript(PICKR_SCRIPT);
    }
    if (!colordModule) {
      // ESM dynamic import
      colordModule = await import(/* @vite-ignore */ COLORD_ESM);
    }
  }

  // -----------------------
  // Formatting back to original family
  // -----------------------
  function formatFromOriginal(original, colord, rgba) {
    const a = typeof rgba.a === 'number' ? rgba.a : 1;

    if (original[0] === '#') {
      const upper = isUppercaseHex(original);
      const R = Math.round(rgba.r).toString(16).padStart(2, '0');
      const G = Math.round(rgba.g).toString(16).padStart(2, '0');
      const B = Math.round(rgba.b).toString(16).padStart(2, '0');
      const A = a < 1 ? Math.round(a * 255).toString(16).padStart(2, '0') : '';
      let hex = R + G + B + (A || '');
      const want = desiredHexLength(original);
      if (want === 3 || want === 4) {
        const short = pairsToShort(R, G, B, A || null);
        if (short && short.length === want) hex = short;
      }
      const out = '#' + hex;
      return upper ? out.toUpperCase() : out.toLowerCase();
    }

    if (original.toLowerCase().startsWith('rgb')) {
      const style = parseRgbStyle(original);
      const r = Math.round(rgba.r), g = Math.round(rgba.g), b = Math.round(rgba.b);
      const pct = v => Math.round(v / 255 * 100);
      const rStr = style.channelPercent ? `${pct(r)}%` : `${r}`;
      const gStr = style.channelPercent ? `${pct(g)}%` : `${g}`;
      const bStr = style.channelPercent ? `${pct(b)}%` : `${b}`;
      const alphaIsNeeded = a < 1 || /rgba/.test(style.fnName) || style.usesSlash;
      const aStr = style.alphaPercent ? `${Math.round(a * 100)}%` : `${+a.toFixed(3)}`;

      if (style.usesCommas && !style.usesSlash) {
        const fn = alphaIsNeeded ? 'rgba' : 'rgb';
        return alphaIsNeeded
          ? `${fn}(${rStr}, ${gStr}, ${bStr}, ${aStr})`
          : `${fn}(${rStr}, ${gStr}, ${bStr})`;
      } else {
        const fn = 'rgb';
        return alphaIsNeeded
          ? `${fn}(${rStr} ${gStr} ${bStr} / ${aStr})`
          : `${fn}(${rStr} ${gStr} ${bStr})`;
      }
    }

    if (original.toLowerCase().startsWith('hsl')) {
      const { colord: c } = colordModule;
      const style = parseHslStyle(original);
      const hsl = c(rgba).toHsl(); // {h,s,l,a} with hue in degrees
      // hue unit preservation
      let hStr;
      switch (style.hueUnit) {
        case 'turn': hStr = `${+(hsl.h / 360).toFixed(4)}turn`; break;
        case 'rad':  hStr = `${+(hsl.h * Math.PI / 180).toFixed(4)}rad`; break;
        case 'grad': hStr = `${+(hsl.h * (10/9)).toFixed(2)}grad`; break;
        default:     hStr = `${Math.round(hsl.h)}deg`; break;
      }
      const sStr = `${Math.round(hsl.s)}%`;
      const lStr = `${Math.round(hsl.l)}%`;
      const alphaIsNeeded = hsl.a < 1 || /hsla/.test(style.fnName) || style.usesSlash;
      const aStr = style.alphaPercent ? `${Math.round(hsl.a * 100)}%` : `${+hsl.a.toFixed(3)}`;

      if (style.usesCommas && !style.usesSlash) {
        const fn = alphaIsNeeded ? 'hsla' : 'hsl';
        return alphaIsNeeded
          ? `${fn}(${hStr}, ${sStr}, ${lStr}, ${aStr})`
          : `${fn}(${hStr}, ${sStr}, ${lStr})`;
      } else {
        const fn = 'hsl';
        return alphaIsNeeded
          ? `${fn}(${hStr} ${sStr} ${lStr} / ${aStr})`
          : `${fn}(${hStr} ${sStr} ${lStr})`;
      }
    }

    // Fallback
    const { colord } = colordModule;
    return colord(rgba).toHex();
  }

  // -----------------------
  // Overlay/mirror DOM
  // -----------------------
  function syncMirrorStyles(ta, mirror) {
    const cs = getComputedStyle(ta);
    const props = [
      'fontFamily','fontSize','fontWeight','fontStyle','lineHeight','letterSpacing',
      'tabSize','textIndent','textTransform','textRendering',
      'paddingTop','paddingRight','paddingBottom','paddingLeft',
      'borderTopWidth','borderRightWidth','borderBottomWidth','borderLeftWidth',
      'boxSizing','whiteSpace','wordBreak','overflowWrap'
    ];
    for (const p of props) mirror.style[p] = cs[p];
    mirror.style.whiteSpace = 'pre-wrap';
  }

  function positionOverlay(ta, overlay, pre) {
    // Ensure parent can position absolute children
    const parent = ta.parentElement;
    const parentPos = getComputedStyle(parent).position;
    if (parentPos === 'static' || !parentPos) {
      parent.style.position = 'relative';
    }
    overlay.style.position = 'absolute';
    overlay.style.left = ta.offsetLeft + 'px';
    overlay.style.top = ta.offsetTop + 'px';
    overlay.style.width = ta.offsetWidth + 'px';
    overlay.style.height = ta.offsetHeight + 'px';
    overlay.style.overflow = 'hidden';
    overlay.style.pointerEvents = 'none';
    overlay.style.zIndex = 9999;

    // Align mirror content against textarea scroll
    pre.style.position = 'absolute';
    pre.style.left = -ta.scrollLeft + 'px';
    pre.style.top = -ta.scrollTop + 'px';
    pre.style.minWidth = ta.scrollWidth + 'px';
  }

  function buildOverlayHTML(text, tokens) {
    let html = '';
    let i = 0;
    for (const t of tokens) {
      if (t.start > i) html += escapeHTML(text.slice(i, t.start));
      const tokenText = escapeHTML(text.slice(t.start, t.end));
      html += `<span class="cp-token" data-start="${t.start}" data-end="${t.end}" data-type="${t.type}" style="pointer-events:auto;border-bottom:1px dashed rgba(0,0,0,.25);color:transparent;">${tokenText}</span>`;
      i = t.end;
    }
    if (i < text.length) html += escapeHTML(text.slice(i));
    return html;
  }

  // -----------------------
  // Main attach logic
  // -----------------------
  let active = { ta: null, overlay: null, pre: null, tokens: [], anchor: null };
  const rebuildOverlayDebounced = debounce(() => rebuildOverlay(active), 60);

  function attachToTextArea(ta) {
    if (!ta || ta.dataset.cpAttached === '1') return;
    ta.dataset.cpAttached = '1';

    // Overlay
    const overlay = document.createElement('div');
    overlay.className = 'cp-overlay';
    const pre = document.createElement('pre');
    pre.className = 'cp-pre';
    pre.style.margin = '0';
    pre.style.color = 'transparent'; // let underlying textarea text be visible
    overlay.appendChild(pre);

    // Anchor for Pickr positioning
    const anchor = document.createElement('div');
    anchor.style.position = 'fixed';
    anchor.style.width = '1px';
    anchor.style.height = '1px';
    anchor.style.pointerEvents = 'none';
    anchor.style.zIndex = 10000;
    document.body.appendChild(anchor);

    // Insert overlay
    ta.parentElement.appendChild(overlay);

    // Initial sync
    syncMirrorStyles(ta, pre);
    positionOverlay(ta, overlay, pre);

    // Build first time
    rebuildOverlay({ ta, overlay, pre, anchor });

    // Scroll/resize events
    ta.addEventListener('scroll', () => positionOverlay(ta, overlay, pre));
    const ro = new ResizeObserver(() => {
      syncMirrorStyles(ta, pre);
      positionOverlay(ta, overlay, pre);
    });
    ro.observe(ta);

    // Keep in active handle
    active = { ta, overlay, pre, tokens: [], anchor };

    // Update overlay on textarea input
    ta.addEventListener('input', rebuildOverlayDebounced);

    // Handle clicks on tokens -> open picker
    overlay.addEventListener('click', async (e) => {
      const tokenEl = e.target.closest('.cp-token');
      if (!tokenEl) return;
      e.preventDefault();
      e.stopPropagation();
      await ensureLibs();

      const start = +tokenEl.dataset.start;
      const end = +tokenEl.dataset.end;
      const value = ta.value.slice(start, end);

      // Position anchor near the token
      const rect = tokenEl.getBoundingClientRect();
      anchor.style.left = (rect.left + rect.width) + 'px';
      anchor.style.top = rect.top + 'px';

      // Parse current color to RGBA
      const { colord } = colordModule;
      const parsed = colord(value);
      const rgba = parsed.toRgb();

      openPickr(anchor, rgba, (newRgba) => {
        // Replace the token in the textarea
        const newText = formatFromOriginal(value, colord, newRgba);

        // Set value and dispatch input event for Gutenberg to react
        const before = ta.value.slice(0, start);
        const after = ta.value.slice(end);
        const nextValue = before + newText + after;
        const newEnd = start + newText.length;

        ta.value = nextValue;
        // Maintain caret near end of updated token
        try {
          ta.selectionStart = ta.selectionEnd = newEnd;
        } catch {}
        // Dispatch bubbling input to trigger WP state updates
        ta.dispatchEvent(new Event('input', { bubbles: true }));
        // Rebuild overlay to reflect updated indices and content
        rebuildOverlay(active);

        // Reposition anchor to stay next to updated token
        raf(() => {
          const refreshed = findSpanForRange(active.pre, start, newEnd);
          if (refreshed) {
            const r2 = refreshed.getBoundingClientRect();
            anchor.style.left = (r2.left + r2.width) + 'px';
            anchor.style.top = r2.top + 'px';
          }
        });
      });
    });
  }

  function rebuildOverlay(ctx) {
    const c = ctx || active;
    if (!c || !c.ta) return;
    const text = c.ta.value;
    const tokens = findColorTokens(text);
    c.pre.innerHTML = buildOverlayHTML(text, tokens);
    c.tokens = tokens;
    positionOverlay(c.ta, c.overlay, c.pre);
  }

  function findSpanForRange(preNode, s, e) {
    return preNode.querySelector(`.cp-token[data-start="${s}"][data-end="${e}"]`);
  }

  function openPickr(anchorEl, rgba, onChange) {
    if (!pickr) {
      pickr = Pickr.create({
        el: anchorEl,
        theme: PICKR_THEME,
        useAsButton: true,
        container: document.body,
        autoReposition: true,
        default: `rgba(${Math.round(rgba.r)}, ${Math.round(rgba.g)}, ${Math.round(rgba.b)}, ${rgba.a})`,
        components: {
          preview: true,
          opacity: true,
          hue: true,
          interaction: {
            input: true,
            hex: true,
            rgba: true,
            hsla: true,
            clear: false,
            save: false
          }
        }
      });
      pickr.on('change', (color /* HSVA */, source) => {
        // Convert to RGBA array
        const arr = color.toRGBA(); // [r,g,b,a]
        const newRgba = { r: arr[0], g: arr[1], b: arr[2], a: arr[3] };
        onChange && onChange(newRgba);
      });
    } else {
      pickr.setColor(
        `rgba(${Math.round(rgba.r)}, ${Math.round(rgba.g)}, ${Math.round(rgba.b)}, ${rgba.a})`,
        true // silent
      );
    }
    pickr.show();
  }

  // -----------------------
  // Bootstrapping / Observer
  // -----------------------
  const tryAttach = () => {
    const ta = document.querySelector(TARGET_SELECTOR);
    if (ta) attachToTextArea(ta);
  };

  // Initial attempt; then watch for Gutenberg re-renders
  tryAttach();
  const mo = new MutationObserver(debounce(tryAttach, 150));
  mo.observe(document.body, { childList: true, subtree: true });

})();