<template>
    <div
        ref="messageEl"
        :class="[cx('message'), message.styleClass]"
        role="alert"
        aria-live="assertive"
        aria-atomic="true"
        :aria-hidden="isAriaHidden"
        :data-p="dataP"
        :data-id="message?.id"
        :data-index="index"
        data-stack=""
        :data-mounted="isMounted ? '' : undefined"
        :data-removed="removed ? '' : undefined"
        :data-front="visibleIndex === 0 ? '' : undefined"
        :data-expanded="isExpanded ? '' : undefined"
        :data-visible="isVisible ? '' : undefined"
        :data-swiping="swiping ? '' : undefined"
        :data-swiped="isSwiped ? '' : undefined"
        :data-swipe-out="swipeOut ? '' : undefined"
        :data-swipe-direction="swipeOutDirection ? swipeOutDirection : undefined"
        :data-dismissible="String(isDismissible())"
        :style="stackStyles"
        v-bind="ptm('message')"
        @click="onMessageClick"
        @mouseenter="onMessageMouseEnter"
        @mouseleave="onMessageMouseLeave"
        @pointerdown="onPointerDown"
        @pointermove="onPointerMove"
        @pointerup="onPointerUp"
        @dragend="onDragEnd"
    >
        <component v-if="templates.container" :is="templates.container" :message="message" :closeCallback="onCloseClick" />
        <div v-else :class="[cx('messageContent'), message.contentStyleClass]" v-bind="ptm('messageContent')">
            <template v-if="!templates.message">
                <component v-if="templates.messageicon" :is="templates.messageicon" :message="message" :class="cx('messageIcon')" v-bind="ptm('messageIcon')" />
                <component v-else-if="isComponentIcon(message.icon)" :is="resolveIcon(message.icon)" :class="cx('messageIcon')" v-bind="ptm('messageIcon')" />
                <span v-else-if="message.icon" :class="[cx('messageIcon'), message.icon]" v-bind="ptm('messageIcon')" />
                <component v-else-if="iconComponent" :is="iconComponent" :class="cx('messageIcon')" v-bind="ptm('messageIcon')" />
                <div :class="cx('messageText')" :data-p="dataP" v-bind="ptm('messageText')">
                    <span :class="cx('summary')" :data-p="dataP" v-bind="ptm('summary')">{{ message.summary }}</span>
                    <div v-if="message.detail" :class="cx('detail')" :data-p="dataP" v-bind="ptm('detail')">{{ message.detail }}</div>
                </div>
            </template>
            <component v-else :is="templates.message" :message="message"></component>
            <div v-if="message.closable !== false" v-bind="ptm('buttonContainer')">
                <button v-ripple :class="cx('closeButton')" type="button" :aria-label="closeAriaLabel" :tabindex="isTabbable ? null : -1" @click="onCloseClick" :data-p="dataP" v-bind="{ ...closeButtonProps, ...ptm('closeButton') }">
                    <component :is="templates.closeicon || 'Times'" :class="[cx('closeIcon'), closeIcon]" v-bind="ptm('closeIcon')" />
                </button>
            </div>
        </div>
    </div>
</template>

<script>
import Check from '@primeicons/vue/check';
import ExclamationTriangle from '@primeicons/vue/exclamation-triangle';
import InfoCircle from '@primeicons/vue/info-circle';
import Times from '@primeicons/vue/times';
import TimesCircle from '@primeicons/vue/times-circle';
import { cn } from '@primeuix/utils';
import { isString } from '@primeuix/utils/object';
import BaseComponent from '@primevue/core/basecomponent';
import Ripple from 'primevue/ripple';
import { toRaw } from 'vue';

const SWIPE_THRESHOLD = 50;
const VELOCITY_THRESHOLD = 0.11;
const SWIPE_OUT_DURATION = 500;

export default {
    name: 'ToastMessage',
    hostName: 'Toast',
    extends: BaseComponent,
    inject: ['$pcToast'],
    emits: ['close'],
    closeTimeout: null,
    closeRaf: null,
    remainingTime: 0,
    timerStartTime: 0,
    pointerStartPosition: null,
    swipeStartTime: 0,
    props: {
        message: {
            type: null,
            default: null
        },
        templates: {
            type: Object,
            default: null
        },
        closeIcon: {
            type: String,
            default: null
        },
        infoIcon: {
            type: String,
            default: null
        },
        warnIcon: {
            type: String,
            default: null
        },
        errorIcon: {
            type: String,
            default: null
        },
        successIcon: {
            type: String,
            default: null
        },
        secondaryIcon: {
            type: String,
            default: null
        },
        contrastIcon: {
            type: String,
            default: null
        },
        closeButtonProps: {
            type: null,
            default: null
        },
        onMouseEnter: {
            type: Function,
            default: undefined
        },
        onMouseLeave: {
            type: Function,
            default: undefined
        },
        onClick: {
            type: Function,
            default: undefined
        },
        index: {
            type: Number,
            default: 0
        }
    },
    data() {
        return {
            isMounted: false,
            measuredHeight: 0,
            removed: false,
            offsetBeforeRemove: 0,
            swiping: false,
            isSwiped: false,
            swipeOut: false,
            swipeDirection: null,
            swipeOutDirection: null,
            swipeAmountX: 0,
            swipeAmountY: 0
        };
    },
    watch: {
        shouldPauseTimer(v) {
            if (this.removed) return;

            if (v) {
                this.pauseTimer();
            } else {
                this.startTimer();
            }
        }
    },
    mounted() {
        this.measureHeight();
        this.isMounted = true;
        this.$pcToast?.onEnter?.();

        if (!this.shouldPauseTimer) {
            this.startTimer();
        }
    },
    beforeUnmount() {
        this.clearCloseTimeout();
        this.$pcToast?.onLeave?.();
    },
    unmounted() {
        if (this.removed) {
            this.$pcToast?.onAfterLeave?.();
        }
    },
    methods: {
        measureHeight() {
            const el = this.$refs.messageEl;

            if (!el) return;

            const originalHeight = el.style.height;
            el.style.height = 'auto';
            const height = el.getBoundingClientRect().height;
            el.style.height = originalHeight;

            this.measuredHeight = height;
            this.$pcToast?.onItemHeightChange?.({ index: this.index, height });
        },
        startTimer() {
            this.clearCloseTimeout();

            if (this.message.sticky) return;

            if (!this.remainingTime || this.remainingTime <= 0) {
                if (!this.message.life) return;
                this.remainingTime = this.message.life;
            }

            this.timerStartTime = Date.now();
            this.closeTimeout = setTimeout(() => {
                this.onMessageRemoveFocus();
                this.closeStack();
            }, this.remainingTime);
        },
        pauseTimer() {
            if (this.timerStartTime > 0 && this.closeTimeout) {
                const elapsed = Date.now() - this.timerStartTime;

                this.remainingTime = Math.max(0, this.remainingTime - elapsed);
            }

            this.clearCloseTimeout();
        },
        markRemoved() {
            this.offsetBeforeRemove = this.offset;
            this.removed = true;
            this.$pcToast?.onItemHeightChange?.({ index: this.index, height: 0, removed: true });
        },
        isDismissible() {
            return this.message?.closable !== false;
        },
        onPointerDown(event) {
            if (event.button !== 0) return;
            if (!this.isDismissible()) return;

            this.swipeStartTime = Date.now();
            this.offsetBeforeRemove = this.offset;

            try {
                event.target.setPointerCapture(event.pointerId);
            } catch (_) {
                // ignore — some elements don't support pointer capture
            }

            this.swiping = true;
            this.pointerStartPosition = { x: event.clientX, y: event.clientY };
        },
        onPointerMove(event) {
            if (!this.pointerStartPosition || !this.isDismissible()) return;
            if ((window.getSelection()?.toString().length ?? 0) > 0) return;

            const yDelta = event.clientY - this.pointerStartPosition.y;
            const xDelta = event.clientX - this.pointerStartPosition.x;
            const isRealSwipe = Math.abs(xDelta) > 1 || Math.abs(yDelta) > 1;

            const positionParts = (this.$pcToast?.position ?? 'top-right').split('-');
            const side = positionParts[0];
            const align = positionParts[1];

            if (!this.swipeDirection && isRealSwipe) {
                this.swipeDirection = Math.abs(xDelta) > Math.abs(yDelta) ? 'x' : 'y';
            }

            let nextX = 0;
            let nextY = 0;

            if (this.swipeDirection === 'x') {
                const naturalDirection = (align === 'left' && xDelta < 0) || (align === 'right' && xDelta > 0);

                nextX = naturalDirection ? xDelta : this.applyDampening(xDelta);
            } else if (this.swipeDirection === 'y') {
                const naturalDirection = (side === 'top' && yDelta < 0) || (side === 'bottom' && yDelta > 0);

                nextY = naturalDirection ? yDelta : this.applyDampening(yDelta);
            }

            if (Math.abs(nextX) > 0 || Math.abs(nextY) > 0) {
                this.isSwiped = true;
            }

            this.swipeAmountX = nextX;
            this.swipeAmountY = nextY;
        },
        onPointerUp() {
            if (this.swipeOut || !this.isDismissible()) return;

            this.swiping = false;
            this.pointerStartPosition = null;

            const swipeAmount = this.swipeDirection === 'x' ? this.swipeAmountX : this.swipeAmountY;
            const elapsed = Date.now() - (this.swipeStartTime || Date.now());
            const velocity = elapsed > 0 ? Math.abs(swipeAmount) / elapsed : 0;

            if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > VELOCITY_THRESHOLD) {
                this.offsetBeforeRemove = this.offset;

                if (this.swipeDirection === 'x') {
                    this.swipeOutDirection = this.swipeAmountX > 0 ? 'right' : 'left';
                } else {
                    this.swipeOutDirection = this.swipeAmountY > 0 ? 'down' : 'up';
                }

                this.swipeOut = true;
                this.markRemoved();
                this.scheduleSwipeOutClose();

                return;
            }

            this.swipeAmountX = 0;
            this.swipeAmountY = 0;
            this.isSwiped = false;
            this.swipeDirection = null;
        },
        onDragEnd() {
            this.swiping = false;
            this.swipeDirection = null;
            this.pointerStartPosition = null;
        },
        applyDampening(delta) {
            const factor = Math.abs(delta) / 20;
            const dampenedDelta = delta * (1 / (1.5 + factor));

            return Math.abs(dampenedDelta) < Math.abs(delta) ? dampenedDelta : delta;
        },
        scheduleSwipeOutClose() {
            this.clearCloseTimeout();
            this.closeTimeout = setTimeout(() => {
                this.close({ message: this.message, type: 'close' });
            }, SWIPE_OUT_DURATION);
        },
        scheduleClose(type) {
            this.clearCloseTimeout();

            this.closeRaf = requestAnimationFrame(() => {
                this.closeRaf = null;

                const el = this.$refs.messageEl;
                const duration = el ? (parseFloat(getComputedStyle(el).transitionDuration) || 0) * 1000 : 0;

                this.closeTimeout = setTimeout(() => {
                    this.close({ message: this.message, type });
                }, duration || SWIPE_OUT_DURATION);
            });
        },
        closeStack() {
            this.markRemoved();
            this.scheduleClose('life-end');
        },
        close(params) {
            this.$emit('close', params);
        },
        onCloseClick() {
            this.clearCloseTimeout();
            this.onMessageRemoveFocus();
            this.markRemoved();
            this.scheduleClose('close');
        },
        onMessageRemoveFocus() {
            const el = this.$refs.messageEl;

            if (!el) return;

            const activeEl = document.activeElement;

            if (!el.contains(activeEl)) return;

            const selector = '[data-pc-section="closebutton"]:not([tabindex="-1"])';
            const next = el.nextElementSibling?.querySelector(selector);
            const prev = el.previousElementSibling?.querySelector(selector);

            requestAnimationFrame(() => {
                if (next) next.focus({ preventScroll: true });
                else if (prev) prev.focus({ preventScroll: true });
            });
        },
        clearCloseTimeout() {
            if (this.closeTimeout) {
                clearTimeout(this.closeTimeout);
                this.closeTimeout = null;
            }

            if (this.closeRaf) {
                cancelAnimationFrame(this.closeRaf);
                this.closeRaf = null;
            }
        },
        onMessageClick(event) {
            this.onClick?.({ originalEvent: event, message: this.message });
        },
        onMessageMouseEnter(event) {
            this.onMouseEnter?.({ originalEvent: event, message: this.message });
        },
        onMessageMouseLeave(event) {
            this.onMouseLeave?.({ originalEvent: event, message: this.message });
        },

        resolveIcon(icon) {
            return isString(icon) ? icon : toRaw(icon);
        },
        isComponentIcon(icon) {
            return !!icon && !isString(icon);
        }
    },
    computed: {
        isExpanded() {
            return this.$pcToast?.isExpanded ?? false;
        },
        toastCount() {
            return this.$pcToast?.messages?.length ?? 0;
        },
        isVisible() {
            return this.$pcToast?.getIsVisible?.(this.index) ?? false;
        },
        stackExpanded() {
            return this.$pcToast?.expanded ?? false;
        },
        visibleIndex() {
            return this.$pcToast?.getVisibleIndex?.(this.index) ?? 0;
        },
        offset() {
            return this.$pcToast?.getOffset?.(this.index) ?? 0;
        },
        isInteracting() {
            return this.$pcToast?.isInteracting ?? false;
        },
        shouldPauseTimer() {
            return this.stackExpanded || this.isInteracting || this.swiping;
        },
        isAriaHidden() {
            return !this.isVisible && !this.removed ? 'true' : null;
        },
        isTabbable() {
            if (this.removed) return false;

            return this.isVisible;
        },
        stackStyles() {
            return {
                '--px-toast-index': this.removed ? this.index : this.visibleIndex,
                '--px-toast-z-index': this.toastCount - this.visibleIndex,
                '--px-initial-height': this.measuredHeight ? `${this.measuredHeight}px` : undefined,
                '--px-toast-offset': `${this.removed ? this.offsetBeforeRemove : this.offset}px`,
                '--px-swipe-amount-x': `${this.swipeAmountX}px`,
                '--px-swipe-amount-y': `${this.swipeAmountY}px`,
                'z-index': this.toastCount - this.visibleIndex
            };
        },
        iconComponent() {
            return {
                info: this.infoIcon ? 'span' : InfoCircle,
                success: this.successIcon ? 'span' : Check,
                warn: this.warnIcon ? 'span' : ExclamationTriangle,
                error: this.errorIcon ? 'span' : TimesCircle,
                secondary: this.secondaryIcon ? 'span' : InfoCircle,
                contrast: this.contrastIcon ? 'span' : InfoCircle
            }[this.message.severity];
        },
        closeAriaLabel() {
            return this.$primevue.config.locale.aria ? this.$primevue.config.locale.aria.close : undefined;
        },
        dataP() {
            return cn({
                [this.message.severity]: this.message.severity
            });
        }
    },
    components: {
        Times: Times,
        InfoCircle: InfoCircle,
        Check: Check,
        ExclamationTriangle: ExclamationTriangle,
        TimesCircle: TimesCircle
    },
    directives: {
        ripple: Ripple
    }
};
</script>
