/**
 * Design tab - colour pickers and live preview.
 *
 * Two jobs, one file, because they watch the same inputs: the picker writes a
 * value and the preview has to show it within the same frame.
 */
(function () {
    'use strict';

    /**
     * Splits #rgb or #rrggbb into three 0-255 channels.
     *
     * @return {number[]|null} Null when the value is not a hex colour.
     */
    function toRgb(hex) {
        var clean = String(hex || '').trim().replace(/^#/, '');

        if (/^[0-9a-f]{3}$/i.test(clean)) {
            clean = clean[0] + clean[0] + clean[1] + clean[1] + clean[2] + clean[2];
        }

        if (!/^[0-9a-f]{6}$/i.test(clean)) {
            return null;
        }

        return [
            parseInt(clean.slice(0, 2), 16),
            parseInt(clean.slice(2, 4), 16),
            parseInt(clean.slice(4, 6), 16)
        ];
    }



    /**
     * What a typed value means as a colour.
     *
     * People paste "E30613" as often as "#E30613", and both are the colour
     * they meant. Anything else returns null and is left alone - rewriting
     * half-typed input under someone's cursor is worse than waiting.
     *
     * @return {string|null} A #rrggbb string, or null.
     */
    function normaliseHex(value) {
        var rgb = toRgb(value);

        if (!rgb) {
            return null;
        }

        return '#' + rgb.map(function (n) {
            return ('0' + n.toString(16)).slice(-2);
        }).join('');
    }

    /* ── Wiring ─────────────────────────────────────────────────────────── */

    var l10n = window.glseDesignL10n || {};
    var vars = l10n.vars || {};

    function byId(id) {
        return document.getElementById(id);
    }


    /** Pushes the current values onto the preview as CSS variables. */
    function refreshPreview() {
        var preview = byId('glse-preview');

        if (!preview) {
            return;
        }

        Object.keys(vars).forEach(function (key) {
            var field = byId('glse-field-' + key.replace(/_/g, '-'));

            if (!field) {
                return;
            }

            var value = field.value;

            if (vars[key].unit) {
                value = String(parseInt(value, 10) || 0) + vars[key].unit;
            } else if (!normaliseHex(value)) {
                // Mid-typing, "#3b8" is a valid colour and "#3b" is not. Holding
                // the last good value beats flashing the preview black.
                return;
            }

            preview.style.setProperty(vars[key].css, value);
        });
    }

    /**
     * Hands one hex box to WordPress' colour picker.
     *
     * The field is captured in a closure rather than read from the callback's
     * arguments. Iris calls back with its own event, whose target is one of its
     * internal nodes, not this input - so `event.target` is an assumption, and
     * a closure is a fact.
     *
     * `change` fires while a colour is being dragged, which is exactly when the
     * preview earns its keep. The value is written to the field first because
     * the readouts below re-read the DOM.
     */
    function bindPicker(field) {
        if (field.dataset.glseBound) {
            return;
        }

        if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.wpColorPicker) {
            // No picker available: the field is still a plain hex box that
            // saves correctly, so keep it live rather than leaving it inert.
            field.dataset.glseBound = '1';
            field.addEventListener('input', refresh);
            return;
        }

        window.jQuery(field).wpColorPicker({
            change: function (event, ui) {
                field.value = ui.color.toString();
                refreshPreview();
            }
        });

        // Marked once it worked, never before: a failure here must not leave
        // the field looking handled.
        field.dataset.glseBound = '1';
    }

    /**
     * Creates each picker the moment its field is actually on screen.
     *
     * Iris measures its square and its slider when it is created. Created
     * inside a hidden tab panel it measures zero and the panel stays unusable
     * once opened - and the design settings sit on a tab that is not the one
     * WordPress opens on, so every picker was being built blind.
     *
     * An observer rather than a listener, because the field can appear for
     * reasons this script should not have to enumerate: a tab clicked, a tab
     * reached with the arrow keys, a preset that shows the rows back. Listening
     * for clicks covered the first and missed the others.
     */
    function watchForVisibleFields() {
        var fields = document.querySelectorAll('.glse-color-hex');

        if (!fields.length) {
            return;
        }

        if (typeof window.IntersectionObserver !== 'function') {
            // Old browser: bind now and accept the measuring risk, which is
            // still better than a picker that is never created at all.
            fields.forEach(bindPicker);
            return;
        }

        var observer = new window.IntersectionObserver(function (entries) {
            entries.forEach(function (entry) {
                if (!entry.isIntersecting) {
                    return;
                }

                bindPicker(entry.target);
                observer.unobserve(entry.target);
            });
        });

        fields.forEach(function (field) {
            observer.observe(field);
        });
    }


    /**
     * Closes every open picker except the one the click belongs to.
     *
     * One rule covers both cases, because they are the same question asked of
     * the same event: which picker, if any, does this click belong to?
     *
     *   - clicked outside every picker  -> none is spared, all close
     *   - clicked inside picker B       -> B is spared, the rest close
     *
     * The library means to do this itself: open() fires a click on `body`,
     * which every other open picker listens for (color-picker.js:302), and a
     * click inside a picker is stopped before it gets there (:226). On this
     * screen that chain does not hold - pickers stay open, and opening a second
     * leaves the first up. I could not pin down what swallows the event, so
     * this asks directly rather than relying on it.
     *
     * The closing goes through the library's own button, never its close():
     * that method toggles rather than hides, so calling it on an already-closed
     * picker would open it. Reading `wp-picker-open` and letting the toggler
     * act cannot desynchronise anything.
     */
    function closeOtherPickers(event) {
        var owner = event.target.closest ? event.target.closest('.wp-picker-container') : null;

        document.querySelectorAll('.wp-color-result.wp-picker-open').forEach(function (toggler) {
            // Le sien reste ouvert : c'est celui qu'on vient de manipuler.
            if (owner && owner.contains(toggler)) {
                return;
            }

            toggler.click();
        });
    }

    /** Escape closes whatever is open, as any panel should. */
    function closePickersOnEscape(event) {
        if ('Escape' !== event.key) {
            return;
        }

        document.querySelectorAll('.wp-color-result.wp-picker-open').forEach(function (toggler) {
            toggler.click();
        });
    }

    document.addEventListener('DOMContentLoaded', function () {
        watchForVisibleFields();
        document.addEventListener('click', closeOtherPickers);
        document.addEventListener('keydown', closePickersOnEscape);

        Object.keys(vars).forEach(function (key) {
            var field = byId('glse-field-' + key.replace(/_/g, '-'));

            if (field && !field.classList.contains('glse-color-hex')) {
                field.addEventListener('input', refreshPreview);
            }
        });

        var placeholder = byId('glse-field-placeholder');
        if (placeholder) {
            placeholder.addEventListener('input', function () {
                document.querySelectorAll('.glse-preview-text').forEach(function (node) {
                    node.textContent = placeholder.value.trim() || (l10n.placeholder || '');
                });
            });
        }

        refreshPreview();
    });
})();
