<script lang="ts">
// import NcButton from '../NcButton/index.js'
import BaseButton from '@/Components/BaseButton.vue';
// import NcPopover from '../NcPopover/index.js'
import GenRandomId from '../utils/GenRandomId';
// import { t } from '../../l10n.js'

import { computed } from 'vue';
import DotsHorizontal from '@/Components/Icons/DotsHorizontal.vue';

const focusableSelector = '.focusable';

/**
 * The Actions component can be used to display one ore more actions.
 * If only a single action is provided, it will be rendered as an inline icon.
 * For more, a menu indicator will be shown and a popovermenu containing the
 * actions will be opened on click.
 *
 * @since 0.10.0
 */
export default {
    name: 'NcActions',

    components: {
        // NcButton,
        BaseButton,
        DotsHorizontal,
        // NcPopover,
    },

    provide() {
        return {
            /**
             * NcActions can be used as:
             * - Application menu (has menu role)
             * - Navigation (has no specific role, should be used an element with navigation role)
             * - Popover with plain text or text inputs (has no specific role)
             * Depending on the usage (used items), the menu and its items should have different roles for a11y.
             * Provide the role for NcAction* components in the NcActions content.
             * @type {import('vue').ComputedRef<boolean>}
             */
            'NcActions:isSemanticMenu': computed(() => this.isSemanticMenu),
        };
    },

    props: {
        /**
         * Specify the open state of the popover menu
         */
        open: {
            type: Boolean,
            default: false,
        },

        /**
         * This disables the internal open management,
         * so the actions menu only respects the `open` prop.
         * This is e.g. necessary for the NcAvatar component
         * to only open the actions menu after loading it's entries has finished.
         */
        manualOpen: {
            type: Boolean,
            default: false,
        },

        /**
         * Force the actions to display in a three dot menu
         */
        forceMenu: {
            type: Boolean,
            default: false,
        },

        /**
         * Force the name to show for single actions
         */
        forceName: {
            type: Boolean,
            default: false,
        },

        /**
         * Specify the menu name
         */
        menuName: {
            type: String,
            default: null,
        },

        /**
         * Apply primary styling for this menu
         */
        primary: {
            type: Boolean,
            default: false,
        },

        /**
         * Specifies the button type used for trigger and single actions buttons
         * Accepted values: primary, secondary, tertiary, tertiary-no-background, tertiary-on-primary, error, warning, success. If left empty,
         * the default button style will be applied.
         */
        type: {
            type: String,
            validator(value: string) {
                return (
                    [
                        'primary',
                        'secondary',
                        'tertiary',
                        'tertiary-no-background',
                        'tertiary-on-primary',
                        'error',
                        'warning',
                        'success',
                    ].indexOf(value) !== -1
                );
            },
            default: null,
        },

        /**
         * Icon to show for the toggle menu button
         * when more than one action is inside the actions component.
         * Only replace the default three-dot icon if really necessary.
         */
        defaultIcon: {
            type: String,
            default: '',
        },

        /**
         * Aria label for the actions menu.
         *
         * If `menuName` is defined this will not be used to prevent
         * any accessible name conflicts. This ensures that the
         * element can be activated via voice input.
         */
        ariaLabel: {
            type: String,
            default: 'Actions',
        },

        /**
         * @deprecated To be removed in @nextcloud/vue 9. Migration guide: remove ariaHidden prop from NcAction* components.
         * @todo Add a check in @nextcloud/vue 9 that this prop is not provided,
         * otherwise root element will inherit incorrect aria-hidden.
         */
        ariaHidden: {
            type: Boolean,
            default: null,
        },

        /**
         * Wanted direction of the menu
         */
        placement: {
            type: String,
            default: 'bottom',
        },

        /**
         * DOM element for the actions' popover boundaries
         */
        boundariesElement: {
            type: Element,
            default: () => document.querySelector('body'),
        },

        /**
         * Selector for the actions' popover container
         */
        container: {
            type: [String, Object, Element, Boolean],
            default: 'body',
        },

        /**
         * Disabled state of the main button (single action or menu toggle)
         */
        disabled: {
            type: Boolean,
            default: false,
        },

        /**
         * Display x items inline out of the dropdown menu
         * Will be ignored if `forceMenu` is set
         */
        inline: {
            type: Number,
            default: 0,
        },
    },

    emits: ['open', 'update:open', 'close', 'focus', 'blur'],

    data() {
        return {
            opened: this.open,
            focusIndex: 0,
            randomId: `menu-${GenRandomId()}`,
            isSemanticMenu: false,
            isSemanticNavigation: false,
            isSemanticPopoverLike: false,
        };
    },

    computed: {
        triggerBtnType() {
            // If requested, we use a primary button
            return (
                this.type ||
                (this.primary
                    ? 'primary'
                    : // If it has a name, we use a secondary button
                    this.menuName
                        ? 'secondary'
                        : 'tertiary')
            );
        },
    },

    watch: {
        // Watch parent prop
        open(state) {
            if (state === this.opened) {
                return;
            }

            this.opened = state;
        },
    },

    methods: {
        /**
         * Do we have exactly one Action and
         * is it allowed as a standalone element?
         *
         * @param {Array} action The action to check
         * @return {boolean}
         */
        isValidSingleAction(action) {
            const componentName = action?.componentOptions?.Ctor?.extendOptions?.name ?? action?.componentOptions?.tag;
            return ['NcActionButton', 'NcActionLink', 'NcActionRouter'].includes(componentName);
        },

        // MENU STATE MANAGEMENT
        openMenu() {
            if (this.opened) {
                return;
            }
            this.opened = true;

            /**
             * Event emitted when the popover menu open state is changed
             *
             * @type {boolean}
             */
            this.$emit('update:open', true);

            /**
             * Event emitted when the popover menu is opened
             */
            this.$emit('open');
        },
        closeMenu(returnFocus = true) {
            if (!this.opened) {
                return;
            }

            this.opened = false;

            this.$refs.popover.clearFocusTrap({ returnFocus });

            /**
             * Event emitted when the popover menu open state is changed
             *
             * @type {boolean}
             */
            this.$emit('update:open', false);

            /**
             * Event emitted when the popover menu is closed
             */
            this.$emit('close');

            // close everything
            this.focusIndex = 0;

            // focus back the menu button
            this.$refs.menuButton.$el.focus();
        },

        onOpen(event) {
            this.$nextTick(() => {
                this.focusFirstAction(event);
            });
        },

        // MENU KEYS & FOCUS MANAGEMENT
        // focus nearest focusable item on mouse move
        // DO NOT change the focus if the target is already focused
        // this will prevent issues with input being unfocused
        // on mouse move
        onMouseFocusAction(event) {
            if (document.activeElement === event.target) {
                return;
            }

            const menuItem = event.target.closest('li');
            if (menuItem && this.$refs.menu.contains(menuItem)) {
                const focusableItem = menuItem.querySelector(focusableSelector);
                if (focusableItem) {
                    const focusList = this.$refs.menu.querySelectorAll(focusableSelector);
                    const focusIndex = [...focusList].indexOf(focusableItem);
                    if (focusIndex > -1) {
                        this.focusIndex = focusIndex;
                        this.focusAction();
                    }
                }
            }
        },
        /**
         * Dispatches the keydown listener to different handlers
         *
         * @param {object} event The keydown event
         */
        onKeydown(event) {
            if (event.key === 'Tab' && !this.isSemanticPopoverLike) {
                this.closeMenu(false);
            }

            if (event.key === 'ArrowUp') {
                this.focusPreviousAction(event);
            }

            if (event.key === 'ArrowDown') {
                this.focusNextAction(event);
            }

            if (event.key === 'PageUp') {
                this.focusFirstAction(event);
            }

            if (event.key === 'PageDown') {
                this.focusLastAction(event);
            }

            if (event.key === 'Escape') {
                this.closeMenu();
                event.preventDefault();
            }
        },
        removeCurrentActive() {
            const currentActiveElement = this.$refs.menu.querySelector('li.active');
            if (currentActiveElement) {
                currentActiveElement.classList.remove('active');
            }
        },
        focusAction() {
            // TODO: have a global disabled state for non input elements
            const focusElement = this.$refs.menu.querySelectorAll(focusableSelector)[this.focusIndex];
            if (focusElement) {
                this.removeCurrentActive();
                const liMenuParent = focusElement.closest('li.action');
                focusElement.focus();
                if (liMenuParent) {
                    liMenuParent.classList.add('active');
                }
            }
        },
        focusPreviousAction(event) {
            if (this.opened) {
                if (this.focusIndex === 0) {
                    this.focusLastAction(event);
                } else {
                    this.preventIfEvent(event);
                    this.focusIndex = this.focusIndex - 1;
                }
                this.focusAction();
            }
        },
        focusNextAction(event) {
            if (this.opened) {
                const indexLength = this.$refs.menu.querySelectorAll(focusableSelector).length - 1;
                if (this.focusIndex === indexLength) {
                    this.focusFirstAction(event);
                } else {
                    this.preventIfEvent(event);
                    this.focusIndex = this.focusIndex + 1;
                }
                this.focusAction();
            }
        },
        focusFirstAction(event) {
            if (this.opened) {
                this.preventIfEvent(event);
                // In case a button is considered aria-selected we will use this one as a initial focus
                const firstSelectedIndex = [...this.$refs.menu.querySelectorAll(focusableSelector)].findIndex((button) => {
                    return button.parentElement.getAttribute('aria-selected');
                });
                this.focusIndex = firstSelectedIndex > -1 ? firstSelectedIndex : 0;
                this.focusAction();
            }
        },
        focusLastAction(event) {
            if (this.opened) {
                this.preventIfEvent(event);
                this.focusIndex = this.$refs.menu.querySelectorAll(focusableSelector).length - 1;
                this.focusAction();
            }
        },
        preventIfEvent(event) {
            if (event) {
                event.preventDefault();
                event.stopPropagation();
            }
        },
        onFocus(event) {
            this.$emit('focus', event);
        },
        onBlur(event) {
            this.$emit('blur', event);
        },
    },

    /**
     * The render function to display the component
     *
     * @param {Function} h The function to create VNodes
     * @return {object|undefined} The created VNode
     */
    render(h) {
        /**
         * Filter the Actions, so that we only get allowed components.
         * This also ensure that we don't get 'text' elements, which would
         * become problematic later on.
         */        
        const actions = ([this.$slots.default] || []).filter(
            (action) => action?.componentOptions?.tag || action?.componentOptions?.Ctor?.extendOptions?.name,
        );

        const getActionName = (action) => action?.componentOptions?.Ctor?.extendOptions?.name ?? action?.componentOptions?.tag;

        const menuItemsActions = ['NcActionButton', 'NcActionButtonGroup', 'NcActionCheckbox', 'NcActionRadio'];
        const textInputActions = ['NcActionInput', 'NcActionTextEditable'];
        const linkActions = ['NcActionLink', 'NcActionRouter'];

        const hasTextInputAction = actions.some((action) => textInputActions.includes(getActionName(action)));
        const hasMenuItemAction = actions.some((action) => menuItemsActions.includes(getActionName(action)));
        const hasLinkAction = actions.some((action) => linkActions.includes(getActionName(action)));

        // We consider the NcActions to have role="menu" if it consists some button-like action and not text inputs
        this.isSemanticMenu = hasMenuItemAction && !hasTextInputAction;
        // We consider the NcActions to be navigation if it consists some link-like action
        this.isSemanticNavigation = hasLinkAction && !hasMenuItemAction && !hasTextInputAction;
        // If it is no a manu and not a navigation, it is a popover with items: a form or just a text
        this.isSemanticPopoverLike = !this.isSemanticMenu && !this.isSemanticNavigation;

        /**
         * Filter and list actions that are allowed to be displayed inline
         */
        let inlineActions = actions.filter(this.isValidSingleAction);
        if (this.forceMenu && inlineActions.length > 0 && this.inline > 0) {
            // Vue.util.warn('Specifying forceMenu will ignore any inline actions rendering.');
            inlineActions = [];
        }

        // Check that we have at least one action
        if (actions.length === 0) {
            return;
        }

        /**
         * Render the provided action
         *
         * @param {import('vue').VNode} action the action to render
         * @return {Function} the vue render function
         */
        const renderInlineAction = (action) => {
            const icon =
                action?.data?.scopedSlots?.icon()?.[0] || h('span', { class: ['icon', action?.componentOptions?.propsData?.icon] });
            const attrs = action?.data?.attrs || {};
            const clickListener = action?.componentOptions?.listeners?.click;

            const text = action?.componentOptions?.children?.[0]?.text?.trim?.();
            const ariaLabel = action?.componentOptions?.propsData?.ariaLabel || text;
            const buttonText = this.forceName ? text : '';

            let title = action?.componentOptions?.propsData?.title;
            // Show a default title for single actions if none is present
            if (!(this.forceName || title)) {
                title = text;
            }

            return h(
                'BaseButton',
                {
                    class: ['action-item action-item--single', action?.data?.staticClass, action?.data?.class],
                    attrs: {
                        ...attrs,
                        'aria-label': ariaLabel,
                        title,
                    },
                    ref: action?.data?.ref,
                    props: {
                        // If it has a menuName, we use a secondary button
                        type: this.type || (buttonText ? 'secondary' : 'tertiary'),
                        disabled: this.disabled || action?.componentOptions?.propsData?.disabled,
                        ...action?.componentOptions?.propsData,
                    },
                    on: {
                        focus: this.onFocus,
                        blur: this.onBlur,
                        // If we have a click listener,
                        // we bind it to execute on click and forward the click event
                        ...(!!clickListener && {
                            click: (event) => {
                                if (clickListener) {
                                    clickListener(event);
                                }
                            },
                        }),
                    },
                },
                [h('template', { slot: 'icon' }, [icon]), buttonText],
            );
        };

        /**
         * Render the actions popover
         *
         * @param {Array} actions the actions to render within
         * @return {Function} the vue render function
         */
        const renderActionsPopover = (actions) => {
            const triggerIcon =
                this.$slots.icon?.[0] ||
                (this.defaultIcon
                    ? h('span', { class: ['icon', this.defaultIcon] })
                    : h('DotsHorizontal', {
                        props: {
                            size: 20,
                        },
                    }));
            return h(
                'NcPopover',
                {
                    ref: 'popover',
                    props: {
                        delay: 0,
                        handleResize: true,
                        shown: this.opened,
                        placement: this.placement,
                        boundary: this.boundariesElement,
                        container: this.container,
                        popoverBaseClass: 'action-item__popper',
                        // Menu and navigation should not have focus trap
                        // Tab should close the menu and move focus to the next UI element
                        setReturnFocus: !this.isSemanticPopoverLike ? null : this.$refs.menuButton?.$el,
                        focusTrap: this.isSemanticPopoverLike,
                    },
                    // For some reason the popover component
                    // does not react to props given under the 'props' key,
                    // so we use both 'attrs' and 'props'
                    attrs: {
                        delay: 0,
                        handleResize: true,
                        shown: this.opened,
                        placement: this.placement,
                        boundary: this.boundariesElement,
                        container: this.container,
                        ...(this.manualOpen && { triggers: [] }),
                    },
                    on: {
                        'show': this.openMenu,
                        'after-show': this.onOpen,
                        'hide': this.closeMenu,
                    },
                },
                [
                    h(
                        'BaseButton',
                        {
                            class: 'action-item__menutoggle',
                            props: {
                                type: this.triggerBtnType,
                                disabled: this.disabled,
                            },
                            slot: 'trigger',
                            ref: 'menuButton',
                            attrs: {
                                'aria-haspopup': this.isSemanticMenu ? 'menu' : null,
                                'aria-label': this.menuName ? null : this.ariaLabel,
                                'aria-controls': this.opened ? this.randomId : null,
                                'aria-expanded': this.opened ? 'true' : 'false',
                            },
                            on: {
                                focus: this.onFocus,
                                blur: this.onBlur,
                            },
                        },
                        [h('template', { slot: 'icon' }, [triggerIcon]), this.menuName],
                    ),
                    h(
                        'div',
                        {
                            class: {
                                open: this.opened,
                            },
                            attrs: {
                                tabindex: '-1',
                            },
                            on: {
                                keydown: this.onKeydown,
                                mousemove: this.onMouseFocusAction,
                            },
                            ref: 'menu',
                        },
                        [
                            h(
                                'ul',
                                {
                                    attrs: {
                                        id: this.randomId,
                                        tabindex: '-1',
                                        role: this.isSemanticMenu ? 'menu' : undefined,
                                    },
                                },
                                [actions],
                            ),
                        ],
                    ),
                ],
            );
        };

        /**
         * If we have a single action only and didn't force a menu,
         * we render the action as a standalone button
         */
        if (actions.length === 1 && inlineActions.length === 1 && !this.forceMenu) {
            return renderInlineAction(inlineActions[0]);
        }

        // If we completely re-render the children
        // we need to focus the first action again
        // Mostly used when clicking a menu item
        this.$nextTick(() => {
            if (this.opened && this.$refs.menu) {
                const isAnyActive = this.$refs.menu.querySelector('li.active') || [];
                if (isAnyActive.length === 0) {
                    this.focusFirstAction();
                }
            }
        });

        /**
         * If we some inline actions to render, render them, then the menu
         */
        if (inlineActions.length > 0 && this.inline > 0) {
            const renderedInlineActions = inlineActions.slice(0, this.inline);
            // Filter already rendered actions
            const menuActions = actions.filter((action) => !renderedInlineActions.includes(action));
            return h(
                'div',
                {
                    class: ['action-items', `action-item--${this.triggerBtnType}`],
                },
                [
                    // Render inline actions
                    ...renderedInlineActions.map(renderInlineAction),
                    // render the rest within the popover menu
                    menuActions.length > 0
                        ? h(
                            'div',
                            {
                                class: [
                                    'action-item',
                                    {
                                        'action-item--open': this.opened,
                                    },
                                ],
                            },
                            [renderActionsPopover(menuActions)],
                        )
                        : null,
                ],
            );
        }

        /**
         * Otherwise, we render the actions in a popover
         */
        return h(
            'div',
            {
                class: [
                    'action-item action-item--default-popover',
                    `action-item--${this.triggerBtnType}`,
                    {
                        'action-item--open': this.opened,
                    },
                ],
            },
            [renderActionsPopover(actions)],
        );
    },
};
</script>

<style lang="css" scoped>
/* Inline buttons */
.action-items {
    display: flex;
    align-items: center;

    /* Spacing between buttons */
    /* &>button {
        margin-right: math.div(28, 2);
    } */
}

.action-item {
    /* --open-background-color: var(--color-background-hover, $action-background-hover); */
    position: relative;
    display: inline-block;

    &.action-item--primary {
        --open-background-color: var(--color-primary-element-hover);
    }

    &.action-item--secondary {
        --open-background-color: var(--color-primary-element-light-hover);
    }

    &.action-item--error {
        --open-background-color: var(--color-error-hover);
    }

    &.action-item--warning {
        --open-background-color: var(--color-warning-hover);
    }

    &.action-item--success {
        --open-background-color: var(--color-success-hover);
    }

    &.action-item--tertiary-no-background {
        --open-background-color: transparent;
    }

    &.action-item--open .action-item__menutoggle {
        background-color: var(--open-background-color);
    }
}
</style>

<style lang="css">
/* // We overwrote the popover base class, so we can style
// the popover__inner for actions only. */
.v-popper--theme-dropdown.v-popper__popper.action-item__popper .v-popper__wrapper {
    border-radius: var(--border-radius-large);
    overflow: hidden;

    .v-popper__inner {
        border-radius: var(--border-radius-large);
        padding: 4px;
        max-height: calc(50vh - 16px);
        overflow: auto;
    }
}
</style>
