<?php
/**
 * Hyvä Themes - https://hyva.io
 * Copyright © Hyvä Themes. All rights reserved.
 * See COPYING.txt for license details.
 */

declare(strict_types=1);

use Hyva\Theme\Model\ViewModelRegistry;
use Hyva\Theme\ViewModel\DeployMode;
use Hyva\Theme\ViewModel\HyvaCsp;
use Hyva\Theme\ViewModel\Modal;
use Magento\Framework\Escaper;
use Magento\Framework\View\Element\Template;

// phpcs:disable Generic.Files.LineLength.TooLong

/** @var Template $block */
/** @var Escaper $escaper */
/** @var HyvaCsp $hyvaCsp */
/** @var ViewModelRegistry $viewModels */

$deployMode = $viewModels->require(DeployMode::class);

?>
<script>
    (function () {
        const modals = [];
        const excludedFromFocusTrapping = new Set();

        function trapFocusInNextModalWithOverlay() {
            for (let idx = modals.length -1; idx >= 0; idx--) {
                const nextOnStack = modals[idx];
                const nextDialogElement = nextOnStack.instance.$refs[nextOnStack.name];
                if (! isOverlayDisabled(nextDialogElement)) {
                    hyva.trapFocus(nextDialogElement);
                    break;
                }
            }
        }

        function focusables(dialogElement) {
            const selector = 'button, [href], input, textarea, select, details, [tabindex]:not([tabindex="-1"])';
            return Array.from(dialogElement.querySelectorAll(selector))
                .filter(el => !el.hasAttribute('disabled'));
        }

        function firstVisible(elements) {
            const a = Array.from(elements);
            for (let i = 0; i < a.length; i++) {
                if (a[i].offsetWidth || a[i].offsetHeight || a[i].getClientRects().length) return a[i];
            }
            return null;
        }

        function isInViewport(element) {
            const rect = element && element.getBoundingClientRect();
            return rect &&
                rect.top >= 0 &&
                rect.left >= 0 &&
                rect.right <= window.innerWidth &&
                rect.bottom <= window.innerHeight;
        }

        function setFocusAfterTransition(dialogElement, duration) {
            <?php
            /*
             * If the dialog contains an x-focus-first element that is not within a nested dialog, set focus
             * immediately from the trapFocus default, to avoid the switch after the transition duration.
             */
            ?>
            const nested = Array.from(dialogElement.querySelectorAll('[role="dialog"]'));
            const candidates = Array.from(dialogElement.querySelectorAll('[x-focus-first]'));
            next: for (let candidate of candidates) {
                for (let child of nested) {
                    if (child.contains(candidate)) continue next;
                }
                setTimeout(() => candidate.focus(), 50);
                break;
            }
            window.setTimeout(() => {
                const focusElement = firstVisible(dialogElement.querySelectorAll('[x-focus-first]')) ||
                    focusables(dialogElement)[0] ||
                    null;
                focusElement && isInViewport(focusElement) && focusElement.focus();
            }, Math.max(1, duration));
        }

        function determineTrigger($refs, dialog, trigger) {
            <?php /* if show() was called without arguments use the event target as open trigger */ ?>
            if (typeof trigger === 'undefined' && typeof dialog === 'object' && dialog.target instanceof HTMLElement) {
                return dialog.target;
            }
            <?php /* if show('name', $event) was called with the event as the second argument use the event target as trigger */ ?>
            if (typeof dialog === 'string' && typeof trigger === 'object' && trigger.target instanceof HTMLElement) {
                return trigger.target;
            }
            <?php /* if show('name', 'trigger') was called with the ref name pr selector of the trigger element */ ?>
            if (typeof trigger === 'string') {
                try {
                    return $refs[trigger] || document.querySelector(trigger)
                } catch (e) {<?php /* Intentionally left empty because we don't know if trigger is intended as a valid selector string */ ?>}
            }
            <?php /* if show('name', document.querySelector('...')) was called, use that as the trigger */ ?>
            if (trigger instanceof Element) {
                return trigger;
            }

            <?php /* unknown trigger - no focus will be set when the dialog is hidden */ ?>
            return null;
        }

        function isOverlayDisabled(dialog) {
            return dialog && dialog.hasAttribute('x-no-overlay')
        }

        function areRemainingModalsWithoutOverlay(modals)
        {
            const overflowDisabled = modals.map(modal => modal.instance.$refs[modal.name]).filter(isOverlayDisabled);

            return overflowDisabled.length === modals.length;
        }

        window.hyva.modal = function(options = {}) {
            <?php /* $el is not defined when hyva.modal is called within Object.assign() for example */ ?>
            if (this.$el && this.$el.dataset && this.$el.dataset.options) {
                try {
                    options = Object.assign(options, JSON.parse(this.$el.dataset.options || '{}'));
                } catch (e) {
                    throw new Error('Cannot read the dialog options from the data-options attribute: not valid JSON');
                }
            }

            const config = Object.assign({
                dialog: '<?= /* @noEscape */ Modal::DEFAULT_NAME ?>', <?php /* default dialog ref name */ ?>
                duration: 300, <?php /* ms before allowing subsequent hiding of modals for nested modals (see transition duration) */ ?>
                transitionEnter: 'transition ease-out duration-300',
                transitionEnterStart: 'opacity-0',
                transitionEnterEnd: 'opacity-100',
                transitionLeave: 'transition ease-in duration-300',
                transitionLeaveStart: 'opacity-100',
                transitionLeaveEnd: 'opacity-0',
            }, options);
            let lastHide = 0;

            return {
                opened: {},
                show(dialog, trigger) {
                    const focusTargetAfterHide = determineTrigger(this.$refs, dialog, trigger);
                    const name = typeof dialog === 'string' ? dialog : config.dialog;
                    const dialogElement = this.$refs[name];
                    if (! dialogElement) {
                        <?php if ($deployMode->isDeveloperMode()): ?>
                        console.error(`Use $modal->getShowJs() in the open trigger, or specify a custom name with\n$modal->withDialogRefName("my-name") and use show("my-name", $event).`);
                        <?php endif; ?>
                        return;
                    }
                    const useOverlay = ! dialogElement.hasAttribute('x-no-overlay');

                    dialogElement.scrollTop = 0;

                    <?php /* Prevent adding the same modal on the stack twice */ ?>
                    if (this.opened[name]) {
                        return;
                    }

                    if (focusTargetAfterHide) {
                        focusTargetAfterHide.setAttribute('aria-expanded', 'true');
                    }

                    this.opened[name] = true;
                    useOverlay && this.$nextTick(() => hyva.trapFocus(dialogElement));
                    setFocusAfterTransition(dialogElement, config.duration);

                    const frame = {name, instance: this, focusTarget: focusTargetAfterHide, time: Date.now()};

                    modals.push(frame);
                    if (useOverlay) {
                        document.body.classList.add('overflow-hidden');
                    }
                    return new Promise(resolve => frame.resolve = resolve);
                },
                cancel() {
                    this.hide(false);
                },
                ok() {
                    this.hide(true);
                },
                hide(value) {
                    <?php
                    // Guard against Escape being pressed multiple times before a transition is finished, otherwise
                    // this function will pop further dialogs from the stack but the display will not update.
                    ?>
                    if (Date.now() - lastHide < config.duration) {
                        return;
                    }
                    lastHide = Date.now();

                    const modal = modals.pop() || {};
                    if (! modal.instance) {
                        return;
                    }

                    const name = modal.name;
                    this.opened[name] = false;
                    hyva.releaseFocus(modal.instance.$refs[modal.name])
                    trapFocusInNextModalWithOverlay();

                    const nextFocusAfterHide = modal.focusTarget;
                    nextFocusAfterHide && setTimeout(() => {
                        nextFocusAfterHide.setAttribute('aria-expanded', 'false');
                        nextFocusAfterHide.focus()
                    }, config.duration);

                    if (modals.length === 0 || areRemainingModalsWithoutOverlay(modals)) {
                        document.body.classList.remove('overflow-hidden');
                    }

                    modal.resolve(value);
                },
                overlay(dialog) {
                    dialog = this.$el.dataset.dialogRefName || dialog;
                    const name = typeof dialog === 'string' ? dialog : config.dialog;
                    return {
                        ['x-show']() {
                            return this.opened[name]
                        },
                        ['x-transition:enter']: config.transitionEnter,
                        ['x-transition:enter-start']: config.transitionEnterStart,
                        ['x-transition:enter-end']: config.transitionEnterEnd,
                        ['x-transition:leave']: config.transitionLeave,
                        ['x-transition:leave-start']: config.transitionLeaveStart,
                        ['x-transition:leave-end']: config.transitionLeaveEnd,
                        ['@hyva-modal-show.window'](event) {
                            event.detail && event.detail.dialog === name && this.show(name, event.detail.focusAfterHide)
                        }
                    };
                },
                showDialogOnInit() {
                    const parent = this.$el.closest('[data-dialog-ref-name]');
                    if (parent && parent.dataset.dialogRefName) {
                        this.$dispatch('hyva-modal-show', {dialog: parent.dataset.dialogRefName})
                    }
                }
            };
        }

        window.hyva.modal.peek = () => modals.length > 0 && modals[modals.length -1]

        window.hyva.modal.pop = function () {
            if (modals.length > 0) {
                const modal = modals[modals.length -1];
                modal.instance.hide();
            }
        }

        window.hyva.modal.excludeSelectorsFromFocusTrap = function (selectors) {
            typeof selectors === 'string' || selectors instanceof String
                ? excludedFromFocusTrapping.add(selectors)
                : selectors.map(selector => excludedFromFocusTrapping.add(selector));
        }

        window.hyva.modal.eventListeners = {
            keydown: event => {
                if (event.key === 'Escape') {
                    window.hyva.modal.pop();
                }
            },
            <?php /* generic modal @click.outside handler */ ?>
            click: event => {
                if (modals.length > 0) {
                    const modal = modals[modals.length -1];
                    const dialog = modal.instance.$refs[modal.name];
                    if (modal.time + 50 < Date.now() && // if last click processing is more than 50ms ago
                        ! isOverlayDisabled(dialog) && // if dialog has overlay
                        ! dialog.contains(event.target)) { // if click is outside of dialog
                        modal.instance.hide();
                    }
                }
            }
        };

        document.addEventListener('keydown', window.hyva.modal.eventListeners.keydown);

        document.addEventListener('click', window.hyva.modal.eventListeners.click);
    })();
    window.addEventListener('alpine:init', () => {
        Alpine.data('hyvaModal', window.hyva.modal);
    }, {once: true});
</script>
<?php $hyvaCsp->registerInlineScript() ?>
