<template>
    <div :class="cx('root')" :data-orientation="layout" :data-resizing="resizing ? '' : undefined" :data-disabled="disabled ? '' : undefined" v-bind="ptmi('root', getPTOptions)" :data-p="dataP">
        <template v-for="(panel, i) of panels" :key="i">
            <component :is="panel" tabindex="-1"></component>
            <div
                v-if="i !== panels.length - 1"
                ref="gutter"
                :class="cx('gutter')"
                :style="{ touchAction: 'none', userSelect: 'none' }"
                role="separator"
                :tabindex="isGutterDisabled(i) ? -1 : 0"
                :aria-orientation="layout"
                :aria-valuenow="Math.round(panelSizes[i] ?? 0)"
                :aria-valuemin="Math.round(getPanelMinSize(i))"
                :aria-valuemax="Math.round(100 - getPanelMinSize(i + 1))"
                :aria-controls="ariaControlsFor(i)"
                :aria-disabled="isGutterDisabled(i) || undefined"
                @pointerdown="onGutterPointerDown($event, i)"
                @keydown="onGutterKeyDown($event, i)"
                @keyup="onGutterKeyUp"
                :data-orientation="layout"
                :data-resizing="resizing && prevPanelIndex === i ? '' : undefined"
                :data-disabled="isGutterDisabled(i) ? '' : undefined"
                :data-p="dataP"
                v-bind="ptm('gutter')"
            >
                <div :class="cx('gutterHandle')" :style="[gutterStyle]" :data-orientation="layout" :data-p="dataP" v-bind="ptm('gutterHandle')"></div>
            </div>
        </template>
    </div>
</template>

<script>
import { cn } from '@primeuix/utils';
import { isRTL } from '@primeuix/utils/dom';
import { isArray, isNotEmpty } from '@primeuix/utils/object';
import { getVNodeProp } from '@primevue/core/utils';
import BaseSplitter from './BaseSplitter.vue';

export default {
    name: 'Splitter',
    extends: BaseSplitter,
    inheritAttrs: false,
    emits: ['resizestart', 'resizeend', 'resize', 'collapse', 'update:sizes'],
    pointerMoveListener: null,
    pointerUpListener: null,
    size: null,
    gutterMidpoint: null,
    initialSizes: [],
    initialCollapsed: [],
    reportedCollapsed: [],
    initialTotal: 100,
    timer: null,
    data() {
        return {
            panelSizes: [],
            panelRegistry: [],
            resizing: false,
            prevPanelIndex: null
        };
    },
    mounted() {
        this.initializePanels();
    },
    watch: {
        panelSizeConfig() {
            this.initializePanels();
        },
        sizes: {
            deep: true,
            handler(newSizes, oldSizes) {
                if (this.resizing) return;
                if (!newSizes || !this.panels.length) return;
                if (oldSizes && oldSizes.length === newSizes.length && oldSizes.every((v, i) => v === newSizes[i])) return;
                if (this.panelSizes.length === newSizes.length && this.panelSizes.every((v, i) => v === newSizes[i])) return;

                for (let i = 0; i < this.panelRegistry.length; i++) {
                    const info = this.panelRegistry[i];

                    if (info?.collapsible) {
                        info.collapsed = newSizes[i] <= (info.collapsedSize ?? 0);
                    }
                }

                this.panelSizes = [...newSizes];
                this.applyPanelStyles();
            }
        }
    },
    beforeUnmount() {
        this.clearTimer();
        this.unbindPointerListeners();
    },
    methods: {
        isSplitterPanel(child) {
            return child.type.name === 'SplitterPanel';
        },
        getPanelSize(panel) {
            let size = panel.props && isNotEmpty(panel.props.size) ? panel.props.size : null;

            return size ?? 100 / this.panels.length;
        },
        buildPanelRegistry() {
            const prev = this.panelRegistry ?? [];

            this.panelRegistry = this.panels.map((panel, i) => {
                const existing = prev[i];
                const minSize = getVNodeProp(panel, 'minSize');
                const maxSize = getVNodeProp(panel, 'maxSize');
                const collapsible = getVNodeProp(panel, 'collapsible');
                const collapsedSize = getVNodeProp(panel, 'collapsedSize');

                return {
                    index: i,
                    minSize: minSize ?? 0,
                    maxSize: maxSize ?? 100,
                    collapsible: collapsible ?? false,
                    collapsedSize: collapsedSize ?? 0,
                    collapsed: existing?.collapsed ?? false
                };
            });
        },
        initializePanels() {
            if (!this.panels || !this.panels.length) return;

            this.buildPanelRegistry();

            let initialized = false;

            if (this.isStateful()) {
                initialized = this.restoreState();
            }

            if (!initialized) {
                const sourceSizes = this.sizes ?? null;
                const _panelSizes = [];

                this.panels.forEach((panel, i) => {
                    _panelSizes[i] = sourceSizes?.[i] ?? this.getPanelSize(panel);
                });

                for (let i = 0; i < this.panelRegistry.length; i++) {
                    const info = this.panelRegistry[i];

                    if (info?.collapsible && _panelSizes[i] <= (info.collapsedSize ?? 0)) {
                        info.collapsed = true;
                    }
                }

                this.panelSizes = _panelSizes;
            }

            this.$nextTick(() => this.applyPanelStyles());
        },
        getPanelChildren() {
            return [...this.$el.children].filter((child) => child.getAttribute('data-pc-name') === 'splitterpanel');
        },
        applyPanelStyles() {
            const children = this.getPanelChildren();

            this.panels.forEach((_, i) => {
                const el = children[i];

                if (!el) return;

                const flexGrow = this.panelSizes[i] ?? 100 / this.panels.length;

                el.style.flex = `${flexGrow} 1 0px`;
                el.style.overflow = 'hidden';

                if (this.horizontal) {
                    el.style.minWidth = '0';
                    el.style.minHeight = '';
                } else {
                    el.style.minHeight = '0';
                    el.style.minWidth = '';
                }

                if (!el.id) el.id = this.panelIds[i];

                el.setAttribute('data-index', i);
                el.setAttribute('data-orientation', this.layout);

                const info = this.panelRegistry[i];

                if (info?.collapsed) el.setAttribute('data-collapsed', '');
                else el.removeAttribute('data-collapsed');

                if (this.resizing) el.setAttribute('data-resizing', '');
                else el.removeAttribute('data-resizing');
            });
        },
        getPanelMinSize(index) {
            return this.panelRegistry?.[index]?.minSize ?? 0;
        },
        ariaControlsFor(index) {
            const ids = [this.panelIds[index], this.panelIds[index + 1]].filter(Boolean);

            return ids.length ? ids.join(' ') : undefined;
        },
        getPanelMaxSize(index) {
            return this.panelRegistry?.[index]?.maxSize ?? 100;
        },
        isGutterDisabled() {
            return this.disabled;
        },
        validatePanelSize(rawSize, info, min, isKeyDown) {
            if (!info?.collapsible) {
                return { size: Math.max(rawSize, min), collapsed: false };
            }

            const collapsedSize = info.collapsedSize ?? 0;

            if (rawSize >= min) {
                return { size: rawSize, collapsed: false };
            }

            if (isKeyDown) {
                return { size: collapsedSize, collapsed: true };
            }

            const halfwayPoint = (collapsedSize + min) / 2;

            if (rawSize <= halfwayPoint) {
                return { size: collapsedSize, collapsed: true };
            }

            return { size: min, collapsed: false };
        },
        getTotalPanelPx() {
            const children = this.getPanelChildren();
            let total = 0;

            for (const el of children) {
                const rect = el.getBoundingClientRect();

                total += this.horizontal ? rect.width : rect.height;
            }

            return total;
        },
        onResizeStart(event, index, isKeyDown) {
            this.size = this.getTotalPanelPx();

            if (!isKeyDown) {
                this.gutterMidpoint = this.horizontal ? event.clientX : event.clientY;
            }

            this.resizing = true;
            this.prevPanelIndex = index;

            this.initialSizes = [...this.panelSizes];
            this.initialCollapsed = this.panelRegistry.map((info) => info.collapsed);
            this.reportedCollapsed = this.panelRegistry.map((info) => info.collapsed);
            this.initialTotal = this.initialSizes.reduce((a, b) => a + b, 0) || 100;

            this.applyPanelStyles();

            if (!isKeyDown) {
                this.$emit('resizestart', { originalEvent: event, sizes: [...this.initialSizes] });
            }
        },
        onResize(event, step, isKeyDown) {
            if (this.size === null || this.prevPanelIndex === null || this.initialSizes.length === 0) {
                return;
            }

            const gutterIdx = this.prevPanelIndex;
            const total = this.initialTotal;
            let delta;

            if (isKeyDown) {
                delta = (total * step) / this.size;
            } else {
                if (this.gutterMidpoint === null) return;

                if (this.horizontal) {
                    if (isRTL(this.$el)) {
                        delta = ((this.gutterMidpoint - event.clientX) * total) / this.size;
                    } else {
                        delta = ((event.clientX - this.gutterMidpoint) * total) / this.size;
                    }
                } else {
                    delta = ((event.clientY - this.gutterMidpoint) * total) / this.size;
                }
            }

            const newSizes = [...this.initialSizes];

            for (let i = 0; i < this.panelRegistry.length; i++) {
                if (this.initialCollapsed[i] !== undefined) {
                    this.panelRegistry[i].collapsed = this.initialCollapsed[i];
                }
            }

            if (delta > 0) {
                this.distribute(newSizes, gutterIdx, delta, isKeyDown, 'forward');
            } else if (delta < 0) {
                this.distribute(newSizes, gutterIdx + 1, -delta, isKeyDown, 'backward');
            }

            for (let i = 0; i < this.panelRegistry.length; i++) {
                const info = this.panelRegistry[i];

                if (info.collapsed !== this.reportedCollapsed[i]) {
                    this.reportedCollapsed[i] = info.collapsed;
                    this.$emit('collapse', { index: i, collapsed: info.collapsed, sizes: [...newSizes] });
                }
            }

            this.panelSizes = [...newSizes];
            this.applyPanelStyles();
            this.$emit('update:sizes', [...newSizes]);
            this.$emit('resize', { originalEvent: event, sizes: [...newSizes] });
        },
        distribute(newSizes, growIdx, requested, isKeyDown, direction) {
            const growInfo = this.panelRegistry[growIdx];
            const max = this.getPanelMaxSize(growIdx);
            let toGrow = Math.min(requested, Math.max(0, max - newSizes[growIdx]));

            if (growInfo?.collapsible && growInfo.collapsed) {
                const gapSize = this.getPanelMinSize(growIdx) - growInfo.collapsedSize;
                const halfwayDelta = gapSize / 2;

                if (isKeyDown) {
                    toGrow = Math.max(toGrow, gapSize);
                    toGrow = Math.min(toGrow, Math.max(0, max - newSizes[growIdx]));
                } else if (toGrow < halfwayDelta) {
                    toGrow = 0;
                } else {
                    toGrow = Math.max(toGrow, gapSize);
                    toGrow = Math.min(toGrow, Math.max(0, max - newSizes[growIdx]));
                }
            }

            let shrunk = 0;
            const range = direction === 'forward' ? this.range(growIdx + 1, newSizes.length, 1) : this.range(growIdx - 1, -1, -1);

            for (const i of range) {
                if (shrunk >= toGrow) break;

                const info = this.panelRegistry[i];
                const min = this.getPanelMinSize(i);

                if (info?.collapsed) continue;

                const effectiveMin = info?.collapsible ? (info.collapsedSize ?? 0) : min;
                const available = Math.max(0, newSizes[i] - effectiveMin);
                const amount = Math.min(toGrow - shrunk, available);
                const rawSize = newSizes[i] - amount;

                const validated = this.validatePanelSize(rawSize, info, min, isKeyDown);
                const actualShrunk = newSizes[i] - validated.size;

                newSizes[i] = validated.size;
                shrunk += actualShrunk;

                if (info) info.collapsed = validated.collapsed;
            }

            newSizes[growIdx] += shrunk;

            if (growInfo?.collapsible && growInfo.collapsed && newSizes[growIdx] >= this.getPanelMinSize(growIdx)) {
                growInfo.collapsed = false;
            }
        },
        range(start, end, step) {
            const out = [];

            for (let i = start; step > 0 ? i < end : i > end; i += step) out.push(i);

            return out;
        },
        onResizeEnd(event) {
            if (this.isStateful()) {
                this.saveState();
            }

            this.resizing = false;
            this.$emit('resizeend', { originalEvent: event, sizes: [...this.panelSizes] });
            this.applyPanelStyles();
            this.clear();
        },
        repeat(event, index, step) {
            this.onResizeStart(event, index, true);
            this.onResize(event, step, true);
        },
        setTimer(event, index, step) {
            if (!this.timer) {
                this.$emit('resizestart', { originalEvent: event, sizes: [...this.panelSizes] });
                this.repeat(event, index, step);
                this.timer = setInterval(() => {
                    this.repeat(event, index, step);
                }, 40);
            }
        },
        clearTimer() {
            if (this.timer) {
                clearInterval(this.timer);
                this.timer = null;
            }
        },
        onGutterKeyUp() {
            if (!this.timer) return;
            this.clearTimer();
            this.onResizeEnd();
        },
        onGutterKeyDown(event, index) {
            if (this.disabled) return;

            switch (event.code) {
                case 'ArrowLeft':
                    if (this.horizontal) this.setTimer(event, index, this.step * -1);
                    event.preventDefault();
                    break;

                case 'ArrowRight':
                    if (this.horizontal) this.setTimer(event, index, this.step);
                    event.preventDefault();
                    break;

                case 'ArrowDown':
                    if (!this.horizontal) this.setTimer(event, index, this.step);
                    event.preventDefault();
                    break;

                case 'ArrowUp':
                    if (!this.horizontal) this.setTimer(event, index, this.step * -1);
                    event.preventDefault();
                    break;

                default:
                    break;
            }
        },
        onGutterPointerDown(event, index) {
            if (this.disabled) return;

            this.onResizeStart(event, index);
            this.bindPointerListeners();
        },
        bindPointerListeners() {
            if (!this.pointerMoveListener) {
                this.pointerMoveListener = (event) => this.onResize(event);
                document.addEventListener('pointermove', this.pointerMoveListener);
            }

            if (!this.pointerUpListener) {
                this.pointerUpListener = (event) => {
                    this.onResizeEnd(event);
                    this.unbindPointerListeners();
                };
                document.addEventListener('pointerup', this.pointerUpListener);
            }
        },
        unbindPointerListeners() {
            if (this.pointerMoveListener) {
                document.removeEventListener('pointermove', this.pointerMoveListener);
                this.pointerMoveListener = null;
            }

            if (this.pointerUpListener) {
                document.removeEventListener('pointerup', this.pointerUpListener);
                this.pointerUpListener = null;
            }
        },
        clear() {
            this.size = null;
            this.gutterMidpoint = null;
            this.initialSizes = [];
            this.initialCollapsed = [];
            this.reportedCollapsed = [];
            this.initialTotal = 100;
            this.prevPanelIndex = null;
        },
        isStateful() {
            return this.stateKey != null;
        },
        getStorage() {
            switch (this.stateStorage) {
                case 'local':
                    return window.localStorage;

                case 'session':
                    return window.sessionStorage;

                default:
                    throw new Error(this.stateStorage + ' is not a valid value for the state storage, supported values are "local" and "session".');
            }
        },
        saveState() {
            if (isArray(this.panelSizes)) {
                this.getStorage().setItem(this.stateKey, JSON.stringify(this.panelSizes));
            }
        },
        restoreState() {
            const storage = this.getStorage();
            const stateString = storage.getItem(this.stateKey);

            if (stateString) {
                this.panelSizes = JSON.parse(stateString);

                return true;
            }

            return false;
        },
        resetState() {
            this.initializePanels();
        }
    },
    computed: {
        panels() {
            const panels = [];

            this.$slots.default?.().forEach((child) => {
                if (this.isSplitterPanel(child)) {
                    panels.push(child);
                } else if (child.children instanceof Array) {
                    child.children.forEach((nestedChild) => {
                        if (this.isSplitterPanel(nestedChild)) {
                            panels.push(nestedChild);
                        }
                    });
                }
            });

            return panels;
        },
        panelSizeConfig() {
            return this.panels.map((panel) => this.getPanelSize(panel)).join(',');
        },
        panelIds() {
            return this.panels.map((_, i) => `${this.$id}_panel_${i}`);
        },
        gutterStyle() {
            if (this.horizontal) return { width: this.gutterSize + 'px' };
            else return { height: this.gutterSize + 'px' };
        },
        horizontal() {
            return this.layout === 'horizontal';
        },
        getPTOptions() {
            return {
                context: {
                    nested: this.$parentInstance?.nestedState
                }
            };
        },
        dataP() {
            return cn({
                [this.layout]: this.layout,
                nested: this.$parentInstance?.nestedState != null,
                resizing: this.resizing,
                disabled: this.disabled
            });
        }
    }
};
</script>
