<template>
    <Portal>
        <div
            ref="container"
            :class="cx('root')"
            :style="containerStyle"
            :data-p="dataP"
            :data-position="position"
            :data-expanded="hostDataExpanded"
            v-bind="ptmi('root')"
            @mouseenter="onContainerMouseEnter"
            @mouseleave="onContainerMouseLeave"
            @focusin="onContainerFocusIn"
            @focusout="onContainerFocusOut"
            @pointerdown="onContainerPointerDown"
            @pointerup="onContainerPointerUp"
        >
            <ToastMessage
                v-for="(msg, i) of messages"
                :key="msg.id"
                :index="i"
                :message="msg"
                :templates="$slots"
                :closeIcon="closeIcon"
                :infoIcon="infoIcon"
                :warnIcon="warnIcon"
                :errorIcon="errorIcon"
                :successIcon="successIcon"
                :secondaryIcon="secondaryIcon"
                :contrastIcon="contrastIcon"
                :closeButtonProps="closeButtonProps"
                :onMouseEnter="onMouseEnter"
                :onMouseLeave="onMouseLeave"
                :onClick="onClick"
                :unstyled="unstyled"
                @close="remove($event)"
                :pt="pt"
            />
        </div>
    </Portal>
</template>

<script>
import { cn } from '@primeuix/utils';
import { setAttribute } from '@primeuix/utils/dom';
import { isEmpty } from '@primeuix/utils/object';
import { ZIndex } from '@primeuix/utils/zindex';
import Portal from 'primevue/portal';
import ToastEventBus from 'primevue/toasteventbus';
import BaseToast from './BaseToast.vue';
import ToastMessage from './ToastMessage.vue';

let messageIdx = 0;

export default {
    name: 'Toast',
    extends: BaseToast,
    inheritAttrs: false,
    emits: ['close', 'life-end'],
    data() {
        return {
            messages: [],
            expanded: false,
            removingCount: 0,
            heights: [],
            isInteracting: false
        };
    },
    styleElement: null,
    zIndexClearTimeout: null,
    mounted() {
        ToastEventBus.on('add', this.onAdd);
        ToastEventBus.on('remove', this.onRemove);
        ToastEventBus.on('remove-group', this.onRemoveGroup);
        ToastEventBus.on('remove-all-groups', this.onRemoveAllGroups);

        if (this.breakpoints) {
            this.createStyle();
        }
    },
    beforeUnmount() {
        this.destroyStyle();

        if (this.zIndexClearTimeout) {
            clearTimeout(this.zIndexClearTimeout);
            this.zIndexClearTimeout = null;
        }

        if (this.$refs.container && this.autoZIndex) {
            ZIndex.clear(this.$refs.container);
        }

        ToastEventBus.off('add', this.onAdd);
        ToastEventBus.off('remove', this.onRemove);
        ToastEventBus.off('remove-group', this.onRemoveGroup);
        ToastEventBus.off('remove-all-groups', this.onRemoveAllGroups);
    },
    methods: {
        add(message) {
            if (message.id == null) {
                message.id = messageIdx++;
            }

            this.messages = [...this.messages, message];
        },
        remove(params) {
            const index = this.messages.findIndex((m) => m.id === params.message.id);

            if (index !== -1) {
                this.messages.splice(index, 1);

                this.heights = this.heights.filter((x) => x.index !== index).map((x) => (x.index > index ? { ...x, index: x.index - 1 } : x));

                if (this.messages.length <= 1) {
                    this.expanded = false;
                }

                this.$emit(params.type, { message: params.message });
            }
        },
        onAdd(message) {
            if (this.group == message.group) {
                this.add(message);
            }
        },
        onRemove(message) {
            this.remove({ message, type: 'close' });
        },
        onRemoveGroup(group) {
            if (this.group === group) {
                this.messages = [];
                this.heights = [];
                this.removingCount = 0;
                this.expanded = false;
                this.isInteracting = false;
            }
        },
        onRemoveAllGroups() {
            const closingMessages = this.messages;

            this.messages = [];
            this.heights = [];
            this.removingCount = 0;
            this.expanded = false;
            this.isInteracting = false;

            closingMessages.forEach((message) => this.$emit('close', { message }));
        },
        onEnter() {
            if (this.autoZIndex && this.$refs.container && this.$refs.container.style.zIndex === '') {
                ZIndex.set('modal', this.$refs.container, this.baseZIndex || this.$primevue.config.zIndex.modal);
            }
        },
        onLeave() {
            if (this.$refs.container && this.autoZIndex && isEmpty(this.messages)) {
                if (this.zIndexClearTimeout) {
                    clearTimeout(this.zIndexClearTimeout);
                }

                this.zIndexClearTimeout = setTimeout(() => {
                    ZIndex.clear(this.$refs.container);
                    this.zIndexClearTimeout = null;
                }, 200);
            }
        },
        onAfterLeave() {
            this.removingCount = Math.max(0, this.removingCount - 1);
        },
        onContainerMouseEnter() {
            this.expanded = true;
        },
        onContainerMouseLeave(event) {
            if (this.isInteracting) return;

            if (this.isPointerOrFocusInside(event.relatedTarget)) {
                return;
            }

            this.expanded = false;
        },
        onContainerFocusIn() {
            this.expanded = true;
        },
        onContainerFocusOut(event) {
            if (this.isInteracting) return;

            if (this.isPointerOrFocusInside(event.relatedTarget)) {
                return;
            }

            this.expanded = false;
        },
        onContainerPointerDown(event) {
            const target = event.target;

            if (target instanceof HTMLElement && target.closest('[data-dismissible="false"]')) return;

            this.isInteracting = true;
        },
        onContainerPointerUp() {
            this.isInteracting = false;
        },
        isPointerOrFocusInside(relatedTarget) {
            const container = this.$refs.container;

            return !!(relatedTarget && container && container.contains(relatedTarget));
        },
        onItemHeightChange(event) {
            if (event.removed) {
                this.heights = this.heights.filter((x) => x.index !== event.index);
                this.removingCount = this.removingCount + 1;

                return;
            }

            const existing = this.heights.findIndex((x) => x.index === event.index);

            if (existing >= 0) {
                const copy = [...this.heights];

                copy[existing] = { index: event.index, height: event.height };
                this.heights = copy;
            } else {
                this.heights = [...this.heights, { index: event.index, height: event.height }].sort((a, b) => a.index - b.index);
            }
        },
        getVisibleIndex(domIndex) {
            const visualIdx = this.visibleIndexMap.get(domIndex);

            return visualIdx != null ? visualIdx : this.messages.length - 1 - domIndex;
        },
        getOffset(domIndex) {
            const visualIdx = this.visibleIndexMap.get(domIndex) ?? 0;

            return this.offsets[visualIdx] ?? 0;
        },
        getIsVisible(domIndex) {
            return this.visibleDomIndices.has(domIndex);
        },
        createStyle() {
            if (!this.styleElement && !this.isUnstyled) {
                this.styleElement = document.createElement('style');
                this.styleElement.type = 'text/css';
                setAttribute(this.styleElement, 'nonce', this.$primevue?.config?.csp?.nonce);
                document.head.appendChild(this.styleElement);

                let innerHTML = '';

                for (let breakpoint in this.breakpoints) {
                    let breakpointStyle = '';

                    for (let styleProp in this.breakpoints[breakpoint]) {
                        breakpointStyle += styleProp + ':' + this.breakpoints[breakpoint][styleProp] + '!important;';
                    }

                    innerHTML += `
                        @media screen and (max-width: ${breakpoint}) {
                            .p-toast[${this.$attrSelector}] {
                                ${breakpointStyle}
                            }
                        }
                    `;
                }

                this.styleElement.innerHTML = innerHTML;
            }
        },
        destroyStyle() {
            if (this.styleElement) {
                document.head.removeChild(this.styleElement);
                this.styleElement = null;
            }
        }
    },
    computed: {
        isExpanded() {
            return this.mode === 'expanded' || this.expanded;
        },
        sortedHeights() {
            return [...this.heights].sort((a, b) => b.index - a.index);
        },
        frontToastHeight() {
            return this.sortedHeights[0]?.height ?? 0;
        },
        offsets() {
            const sorted = this.sortedHeights;
            const offsets = [0];

            for (let i = 1; i < sorted.length; i++) {
                offsets[i] = offsets[i - 1] + sorted[i - 1].height;
            }

            return offsets;
        },
        visibleIndexMap() {
            const map = new Map();

            this.sortedHeights.forEach((entry, idx) => map.set(entry.index, idx));

            return map;
        },
        visibleDomIndices() {
            return new Set(this.sortedHeights.slice(0, this.limit).map((x) => x.index));
        },
        raiseFactor() {
            return (this.position || '').startsWith('bottom') ? -1 : 1;
        },
        hostDataExpanded() {
            return this.isExpanded ? '' : null;
        },
        containerStyle() {
            const base = this.sx('root', true, { position: this.position });

            return [
                base,
                {
                    '--px-gap': `${this.gap}px`,
                    '--px-front-toast-height': `${this.frontToastHeight}px`,
                    '--px-raise-factor': this.raiseFactor
                }
            ];
        },
        dataP() {
            return cn({
                [this.position]: this.position
            });
        }
    },
    components: {
        ToastMessage: ToastMessage,
        Portal: Portal
    }
};
</script>
