<template>
    <transition name="fade" appear @after-enter="useFocusTrap" @before-leave="clearFocusTrap">
        <div v-show="showModal" ref="mask" class="modal-mask"
            :class="{ 'modal-mask--dark': dark || !closeButtonContained || hasPrevious || hasNext }" :style="cssVariables"
            role="dialog" aria-modal="true" :aria-labelledby="'modal-name-' + randId"
            :aria-describedby="'modal-description-' + randId" tabindex="-1">
            <!-- Header -->
            <transition name="fade-visibility" appear>
                <div class="modal-header">
                    <h2 v-if="name.trim() !== ''" :id="'modal-name-' + randId" class="modal-name">
                        {{ name }}
                    </h2>
                    <div class="icons-menu">
                        <!-- Play-pause toggle -->
                        <button v-if="hasNext && enableSlideshow" :class="{ 'play-pause-icons--paused': slideshowPaused }"
                            class="play-pause-icons" type="button" @click="togglePlayPause">
                            <!-- Play/pause icons -->
                            <Play v-if="!playing" :size="iconSize" class="play-pause-icons__play" />
                            <Pause v-else :size="iconSize" class="play-pause-icons__pause" />
                            <span class="hidden-visually">
                                {{ playPauseName }}
                            </span>

                            <!-- Progress circle, css animated -->
                            <svg v-if="playing" class="progress-ring" height="50" width="50">
                                <circle class="progress-ring__circle" stroke="white" stroke-width="2" fill="transparent"
                                    r="15" cx="25" cy="25" />
                            </svg>
                        </button>

                        <!-- Actions menu -->
                        <!-- @slot List of actions to show -->
                        <NcActions class="header-actions" :inline="inlineActions">
                            <slot name="actions" />
                        </NcActions>

                        <!-- Close modal -->
                        <NcButton v-if="canClose && !closeButtonContained" :aria-label="closeButtonAriaLabel"
                            class="header-close" type="tertiary" @click="close" :icon="mdiClose">
                            <!-- <template #icon>
                                <Close :size="iconSize" />
                            </template> -->
                        </NcButton>
                        <!-- <BaseButton v-if="canClose && !closeButtonContained" :aria-label="closeButtonAriaLabel" :color="button" outline @click="cancel" >
                        </BaseButton> -->
                    </div>
                </div>
            </transition>

            <!-- modal-wrapper -->
            <transition :name="modalTransitionName" appear>
                <div v-show="showModal"
                    :class="[`modal-wrapper--${size}`, { 'modal-wrapper--spread-navigation': spreadNavigation }]"
                    class="modal-wrapper" @mousedown.self="handleClickModalWrapper">
                    
                    <!-- Navigation button left -->
                    <transition name="fade-visibility" appear>
                        <NcButton v-show="hasPrevious" type="tertiary-no-background" class="prev"
                            :aria-label="prevButtonAriaLabel" @click="previous" :icon="mdiChevronLeft" :size="40">
                        </NcButton>
                    </transition>

                    <!-- modal-container: Content with close button -->
                    <div :id="'modal-description-' + randId" class="modal-container">
                        <!-- Close modal -default hidden by FirstrunWizard.vue -->
                        <NcButton v-if="canClose && closeButtonContained" type="tertiary" class="modal-container__close"
                            :aria-label="'closeButtonAriaLabel'" @click="close" :icon="mdiClose" :size="20"
                            :rounded-full="true">
                        </NcButton>
                        <div class="modal-container__content">
                            <!-- @slot Modal content to render -->
                            <slot />
                        </div>
                    </div>

                    <!-- Navigation button right -->
                    <transition name="fade-visibility" appear>
                        <NcButton v-show="hasNext" type="tertiary-no-background" class="next"
                            :aria-label="nextButtonAriaLabel" @click="next" :icon="mdiChevronRight" :size="40">
                        </NcButton>
                    </transition>
                </div>
            </transition>
        </div>
    </transition>
</template>

<script lang="ts">
import { getTrapStack } from '../utils/focusTrap';
// import { t } from '../../l10n.js';
import GenRandomId from '../utils/GenRandomId';
// import l10n from '../../mixins/l10n.js';
import NcActions from '@/Components/NcActions.vue';
import NcButton from '@/Components/NcButton.vue';
import Timer from '../utils/Timer.js';
// import Tooltip from '../../directives/Tooltip/index.js';

import Pause from '@/Components/Icons/Pause.vue';
import Play from '@/Components/Icons/Play.vue';
import { mdiClose, mdiChevronLeft, mdiChevronRight } from '@mdi/js';

import { createFocusTrap } from 'focus-trap';
// import { useSwipe } from '@vueuse/core';

export default {
    name: 'NcModal',

    components: {
        NcActions,
        Pause,
        Play,
        NcButton,
    },

    // directives: {
    //     tooltip: Tooltip,
    // },

    // mixins: [l10n],

    props: {
        /**
         * Name to be shown with the modal
         */
        name: {
            type: String,
            default: '',
        },
        /**
         * Declare if a previous slide is available
         */
        hasPrevious: {
            type: Boolean,
            default: false,
        },
        /**
         * Declare if a next slide is available
         */
        hasNext: {
            type: Boolean,
            default: false,
        },
        /**
         * Declare if hiding the modal should be animated
         */
        outTransition: {
            type: Boolean,
            default: false,
        },
        /**
         * Declare if the slideshow functionality should be enabled
         */
        enableSlideshow: {
            type: Boolean,
            default: false,
        },
        /**
         * Declare the slide interval
         */
        slideshowDelay: {
            type: Number,
            default: 5000,
        },
        /**
         * Allow to pause an ongoing slideshow
         */
        slideshowPaused: {
            type: Boolean,
            default: false,
        },
        /**
         * Enable swipe between slides
         */
        enableSwipe: {
            type: Boolean,
            default: true,
        },
        spreadNavigation: {
            type: Boolean,
            default: false,
        },
        /**
         * Defines the modal size.
         * Default is 'normal'.
         * Available are 'small', 'normal', 'large' and 'full'.
         * All sizes except 'small' change automatically to full-screen on mobile.
         */
        size: {
            type: String,
            default: 'normal',
            validator: (size: string) => {
                return ['small', 'normal', 'large', 'full'].includes(size);
            },
        },

        /**
         * Declare if the modal can be closed
         */
        canClose: {
            type: Boolean,
            default: true,
        },

        /**
         * Close the modal if the user clicked outside of the modal
         * Only relevant if `canClose` is set to true.
         */
        closeOnClickOutside: {
            type: Boolean,
            default: false,
        },

        /**
         * Makes the modal backdrop black if true
         * Will be overwritten if some buttons are shown outside
         */
        dark: {
            type: Boolean,
            default: false,
        },

        /**
         * Selector for the modal container, pass `null` to prevent automatic container mounting
         */
        // container: {
        //     type: [String, null],
        //     default: 'body',
        // },

        /**
         * Pass in false if you want the modal 'close' button to be displayed
         * outside the modal boundaries, in the top right corner of the window
         */
        closeButtonContained: {
            type: Boolean,
            default: true,
        },

        /**
         * Additional elements to add to the focus trap
         */
        additionalTrapElements: {
            type: Array,
            default: () => [],
        },

        /**
         * Display x items inline
         *
         * @see Actions component usage
         */
        inlineActions: {
            type: Number,
            default: 0,
        },

        show: {
            type: Boolean,
            default: undefined,
        },
    },

    emits: ['previous', 'next', 'close', 'update:show'],

    data() {
        return {
            mc: null,
            playing: false,
            slideshowTimeout: null,
            iconSize: 24,
            focusTrap: null,
            randId: GenRandomId(),
            internalShow: true,
            mdiClose,
            mdiChevronLeft,
            mdiChevronRight,
        };
    },

    computed: {
        showModal() {
            return this.show === undefined ? this.internalShow : this.show;
        },
        modalTransitionName() {
            return `modal-${this.outTransition ? 'out' : 'in'}`;
        },
        playPauseName() {
            return this.playing ? 'Pause slideshow' : 'Start slideshow';
        },
        cssVariables() {
            return {
                '--slideshow-duration': this.slideshowDelay + 'ms',
                '--icon-size': this.iconSize + 'px',
            };
        },

        closeButtonAriaLabel() {
            return 'Close';
        },
        prevButtonAriaLabel() {
            return 'Previous';
        },
        nextButtonAriaLabel() {
            return 'Next';
        },
    },

    watch: {
        /**
         * Handle play/pause of an ongoing slideshow
         *
         * @param {boolean} paused is the player paused
         */
        slideshowPaused(paused) {
            if (this.slideshowTimeout) {
                if (paused) {
                    this.slideshowTimeout.pause();
                } else {
                    this.slideshowTimeout.start();
                }
            }
        },
        additionalTrapElements(elements) {
            if (this.focusTrap) {
                const contentContainer = this.$refs.mask;
                this.focusTrap.updateContainerElements([contentContainer, ...elements]);
            }
        },
    },

    beforeMount() {
        window.addEventListener('keydown', this.handleKeydown);
    },
    beforeDestroy() {
        window.removeEventListener('keydown', this.handleKeydown);
        this.mc.stop();
    },
    mounted() {
        // init clear view
        this.useFocusTrap();
        // this.mc = useSwipe(this.$refs.mask, {
        //     onSwipeEnd: this.handleSwipe,
        // });

        if (this.container) {
            if (this.container === 'body') {
                // force mount the component to body
                document.body.insertBefore(this.$el, document.body.lastChild);
            } else {
                const container = document.querySelector(this.container);
                container.appendChild(this.$el);
            }
        }
    },
    destroyed() {
        this.clearFocusTrap();
        this.$el.remove();
    },

    methods: {
        // Events emitters
        previous(event) {
            // do not send the event if nothing is available
            if (this.hasPrevious) {
                // if data is set, then it's a user mouse event
                // and not the slideshow handler, therefore
                // we reset the timer
                if (event) {
                    this.resetSlideshow();
                }
                this.$emit('previous', event);
            }
        },
        next(event) {
            // do not send the event if nothing is available
            if (this.hasNext) {
                // if data is set, then it's a mouse event
                // and not the slideshow handler, therefore
                // we reset the timer
                if (event) {
                    this.resetSlideshow();
                }
                this.$emit('next', event);
            }
        },
        close(data) {
            // do not fire event if forbidden
            if (this.canClose) {
                // We set internalShow here, so the out transitions properly run before the component is destroyed
                this.internalShow = false;
                this.$emit('update:show', false);

                // delay closing for animation
                setTimeout(() => {
                    /**
                     * Emitted when the closing animation is finished
                     */
                    this.$emit('close', data);
                }, 300);
            }
        },

        /**
         * Handle click on modal wrapper
         * If `closeOnClickOutside` is set the modal will be closed
         *
         * @param {MouseEvent} event The click event
         */
        handleClickModalWrapper(event) {
            if (this.closeOnClickOutside) {
                this.close(event);
            }
        },

        /**
         * @param {KeyboardEvent} event - keyboard event
         */
        handleKeydown(event) {
            if (event.key === 'Escape') {
                const trapStack = getTrapStack();
                // Only close the most recent focus trap modal
                if (trapStack.length > 0 && trapStack[trapStack.length - 1] !== this.focusTrap) {
                    return;
                }
                return this.close(event);
            }

            const arrowHandlers = {
                ArrowLeft: this.previous,
                ArrowRight: this.next,
            };
            if (arrowHandlers[event.key]) {
                // Ignore arrow navigation, if there is a current focus outside the modal.
                // For example, when the focus is in Sidebar or NcActions's items,
                // arrow navigation should not be intercept by modal slider
                if (document.activeElement && !this.$el.contains(document.activeElement)) {
                    return;
                }
                return arrowHandlers[event.key](event);
            }
        },

        /**
         * handle the swipe event
         *
         * @param {TouchEvent} e The touch event
         * @param {import('@vueuse/core').SwipeDirection} direction Swipe direction
         */
        handleSwipe(e, direction) {
            if (this.enableSwipe) {
                if (direction === 'left') {
                    // swiping to left to go to the next item
                    this.next(e);
                } else if (direction === 'right') {
                    // swiping to right to go back to the previous item
                    this.previous(e);
                }
            }
        },

        /**
         * Toggle the slideshow state
         */
        togglePlayPause() {
            this.playing = !this.playing;
            if (this.playing) {
                this.handleSlideshow();
            } else {
                this.clearSlideshowTimeout();
            }
        },

        /**
         * Reset the slideshow timer and keep going if it was on
         */
        resetSlideshow() {
            this.playing = !this.playing;
            this.clearSlideshowTimeout();
            this.$nextTick(function () {
                this.togglePlayPause();
            });
        },

        /**
         * Handle the slideshow timer and next event
         */
        handleSlideshow() {
            this.playing = true;
            if (this.hasNext) {
                this.slideshowTimeout = new Timer(() => {
                    this.next();
                    this.handleSlideshow();
                }, this.slideshowDelay);
            } else {
                this.playing = false;
                this.clearSlideshowTimeout();
            }
        },

        /**
         * Clear slideshowTimeout if ongoing
         */
        clearSlideshowTimeout() {
            if (this.slideshowTimeout) {
                this.slideshowTimeout.clear();
            }
        },
        /**
         * Add focus trap for accessibility.
         */
        async useFocusTrap() {
            // Don't do anything if the modal is hidden,
            // or we have a focus trap already
            if (!this.showModal || this.focusTrap) {
                return;
            }

            const contentContainer = this.$refs.mask;
            // wait until all children are mounted and available in the DOM before focusTrap can be added
            await this.$nextTick();

            const options = {
                allowOutsideClick: true,
                fallbackFocus: contentContainer,
                trapStack: getTrapStack(),
                // Esc can be used without stop in content or additionalTrapElements where it should not deacxtivate modal's focus trap.
                // Focus trap is deactivated on modal close anyway.
                escapeDeactivates: false,
            };

            // Init focus trap
            this.focusTrap = createFocusTrap([contentContainer, ...this.additionalTrapElements], options);
            this.focusTrap.activate();
        },
        clearFocusTrap() {
            if (!this.focusTrap) {
                return;
            }
            this.focusTrap?.deactivate();
            this.focusTrap = null;
        },
    },
};
</script>

<style lang="css" scoped>
.modal-mask {
    position: fixed;
    z-index: 9998;
    top: 0;
    left: 0;
    display: block;
    width: 100%;
    height: 100%;
    background-color: rgba(0, 0, 0, 0.5);

    &--dark {
        background-color: rgba(0, 0, 0, 0.92);
    }
}

.modal-header {
    position: absolute;
    z-index: 10001;
    top: 0;
    right: 0;
    left: 0;
    /* prevent vue show to use display:none and reseting */
    /* the circle animation loop */
    display: flex !important;
    align-items: center;
    justify-content: center;
    width: 100%;
    height: var(--header-height);
    overflow: hidden;
    transition:
        opacity 250ms,
        visibility 250ms;

    .modal-name {
        overflow-x: hidden;
        box-sizing: border-box;
        width: 100%;
        /* maximum actions is 3 */
        padding: 0 var(--clickable-area * 3) 0 12px;
        transition: padding ease 100ms;
        white-space: nowrap;
        text-overflow: ellipsis;
        color: #fff;
        /* font-size: var(--icon-margin); */
        margin-bottom: 0;
    }

    /* On wider screens the name can be centered
    @media only screen and (min-width: $breakpoint-mobile) {
        .modal-name {
            padding-left: #{$clickable-area * 3}; // maximum actions is 3
            text-align: center;
        }
    } */

    .icons-menu {
        position: absolute;
        right: 0;
        display: flex;
        align-items: center;
        justify-content: flex-end;

        .header-close {
            display: flex;
            align-items: center;
            justify-content: center;
            box-sizing: border-box;
            /* margin: math.div($header-height - $clickable-area, 2); */
            padding: 0;
        }

        .play-pause-icons {
            position: relative;
            width: var(--header-height);
            height: var(header-height);
            margin: 0;
            padding: 0;
            cursor: pointer;
            border: none;
            background-color: transparent;

            &:hover,
            &:focus {

                .play-pause-icons__play,
                .play-pause-icons__pause {
                    opacity: var(--opacity_full);
                    /* border-radius: math.div($clickable-area, 2); */
                    background-color: var(--icon-focus-bg);
                }
            }

            &__play,
            &__pause {
                box-sizing: border-box;
                width: var(--default-clickable-area);
                height: var(--default-clickable-area);
                margin: var(--header-height) - var(--default-clickable-area);
                cursor: pointer;
                opacity: var(--opacity_normal);
            }
        }

        .header-actions {
            color: white;
        }

        &:deep() .action-item {
            /* margin: math.div($header-height - $clickable-area, 2); */

            &--single {
                box-sizing: border-box;
                /* width: $clickable-area;
                height: $clickable-area; */
                cursor: pointer;
                background-position: center;
                background-size: 22px;
            }
        }

        :deep(button) {
            /* force white instead of default main text */
            color: #fff;
        }

        /* Force the Actions menu icon to be the same size as other icons */
        &:deep(.action-item__menutoggle) {
            padding: 0;

            span,
            svg {
                width: var(--icon-size);
                height: var(--icon-size);
            }
        }
    }
}

.modal-wrapper {
    display: flex;
    align-items: center;
    justify-content: center;
    box-sizing: border-box;
    width: 100%;
    height: 100%;

    /* Navigation buttons */
    .prev,
    .next {
        z-index: 10000;
        height: 35vh;
        min-height: 300px;
        position: absolute;
        transition: opacity 250ms;
        /* // hover the mask */
        color: white;

        &:focus-visible {
            /* Override NcButton focus styles */
            box-shadow: 0 0 0 2px var(--color-primary-element-text);
            background-color: var(--color-box-shadow);
        }
    }

    .prev {
        left: 2px;
    }

    .next {
        right: 2px;
    }

    /* Content */
    .modal-container {
        position: relative;
        display: flex;
        padding: 0;
        transition: transform 300ms ease;
        border-radius: var(--border-radius-large);
        background-color: var(--color-main-background);
        color: var(--color-main-text);
        box-shadow: 0 0 40px rgba(0, 0, 0, 0.2);

        &__close {
            /* // Ensure the close button is always ontop of the content */
            z-index: 1;
            position: absolute;
            top: 4px;
            right: 4px;
        }

        &__content {
            width: 100%;
            /* At least the close button shall fit in */
            min-height: 52px;
            /* avoids unecessary hacks if the content should be bigger than the modal */
            overflow: auto;
        }
    }

    /* We allow 90% max-height, but we need to ensure the header does not overlap the modal
    as the modal is centered, we need the space on top and bottom */
    /* $max-modal-height: min(90%, calc(100% - 2 * $header-height)); */

    /* // Sizing */
    &--small {
        .modal-container {
            width: 400px;
            max-width: 90%;
            /* max-height: $max-modal-height; */
        }
    }

    &--normal {
        .modal-container {
            max-width: 90%;
            width: 600px;
            /* max-height: $max-modal-height; */
        }
    }

    &--large {
        .modal-container {
            max-width: 90%;
            width: 900px;
            /* max-height: $max-modal-height; */
        }
    }

    &--full {
        .modal-container {
            width: 100%;
            height: calc(100% - var(--header-height));
            position: absolute;
            /* top: $header-height; */
            border-radius: 0;
        }
    }

    /* // Make modal full screen on mobile */
    @media only screen and (max-width: 640px) {
        .modal-container {
            max-width: initial;
            width: 100%;
            max-height: initial;
            height: calc(100% - var(--header-height));
            position: absolute;
            border-radius: 0;
        }
    }
}

/* TRANSITIONS */
.fade-enter-active,
.fade-leave-active {
    transition: opacity 250ms;
}

.fade-enter,
.fade-leave-to {
    opacity: 0;
}

.fade-visibility-enter,
.fade-visibility-leave-to {
    visibility: hidden;
    opacity: 0;
}

.modal-in-enter-active,
.modal-in-leave-active,
.modal-out-enter-active,
.modal-out-leave-active {
    transition: opacity 250ms;
}

.modal-in-enter,
.modal-in-leave-to,
.modal-out-enter,
.modal-out-leave-to {
    opacity: 0;
}

.modal-in-enter .modal-container,
.modal-in-leave-to .modal-container {
    transform: scale(0.9);
}

.modal-out-enter .modal-container,
.modal-out-leave-to .modal-container {
    transform: scale(1.1);
}

/* // animated circle */
/* $radius: 15;
$pi: 3.14159265358979; */

.modal-mask .play-pause-icons {
    .progress-ring {
        position: absolute;
        top: 0;
        left: 0;
        transform: rotate(-90deg);

        .progress-ring__circle {
            transition: 100ms stroke-dashoffset;
            /* // axis compensation */
            transform-origin: 50% 50%;
            animation: progressring linear var(--slideshow-duration) infinite;

            stroke-linecap: round;
            /* radius * 2 * PI */
            stroke-dashoffset: 15 * 2 * 3.14159265358979;
            /* radius * 2 * PI */
            stroke-dasharray: 15 * 2 * 3.14159265358979;
        }
    }

    &--paused {
        .icon-pause {
            animation: breath 2s cubic-bezier(0.4, 0, 0.2, 1) infinite;
        }

        .progress-ring__circle {
            animation-play-state: paused !important;
        }
    }
}

/* keyframes get scoped too and break the animation name, we need them unscoped */
@keyframes progressring {
    from {
        /* radius * 2 * PI */
        stroke-dashoffset: 15 * 2 * 3.14159265358979;
    }

    to {
        stroke-dashoffset: 0;
    }
}

@keyframes breath {
    0% {
        opacity: 1;
    }

    50% {
        opacity: 0;
    }

    100% {
        opacity: 1;
    }
}
</style>
