<template>
    <teleport to="body" :disabled="!teleportToBody">
        <div
            v-show="ready"
            :class="[bem({ teleport: teleportToBody }), panelClass]"
            ref="panel"
            :style="cssPosition"
        >
            <div
                v-if="shouldShowHeader"
                :class="[bem('header'), headerClass]"
                :data-tool-panel-drag="allowDrag"
            >
                <slot name="header" :title="title">
                    <span>{{ title }}</span>
                </slot>
            </div>
            <div :class="bem('content')">
                <slot></slot>
            </div>

            <div
                v-show="canResizeNorth"
                :class="bem('resize-handle', { n: true })"
                data-resize-handle="n"
            ></div>
            <div
                v-show="canResizeSouth"
                :class="bem('resize-handle', { s: true })"
                data-resize-handle="s"
            ></div>
            <div
                v-show="canResizeEast"
                :class="bem('resize-handle', { e: true })"
                data-resize-handle="e"
            ></div>
            <div
                v-show="canResizeWest"
                :class="bem('resize-handle', { w: true })"
                data-resize-handle="w"
            ></div>
            <!-- Corner handles for diagonal resizing -->
            <div
                v-show="canResizeNorth && canResizeEast"
                :class="bem('resize-handle', { ne: true })"
                data-resize-handle="ne"
            ></div>
            <div
                v-show="canResizeNorth && canResizeWest"
                :class="bem('resize-handle', { nw: true })"
                data-resize-handle="nw"
            ></div>
            <div
                v-show="canResizeSouth && canResizeEast"
                :class="bem('resize-handle', { se: true })"
                data-resize-handle="se"
            ></div>
            <div
                v-show="canResizeSouth && canResizeWest"
                :class="bem('resize-handle', { sw: true })"
                data-resize-handle="sw"
            ></div>
        </div>
    </teleport>
</template>

<script>
import Drag from '../utils/drag.js';

export default {
    name: 'px-tool-panel',
    props: {
        /** Initial bounds object that defines the position and dimensions */
        bounds: { type: Object, default: null },

        /** Teleport component to body instead of positioning within parent element */
        teleportToBody: { type: Boolean, default: false },

        /** Show the header section of the panel. If null, header will only show if title or header slot content exists */
        showHeader: { type: Boolean, default: null },

        /** Title text to display in the panel header */
        title: { type: String, default: null },

        /** Additional CSS class to apply to the panel (use this instead of class because of the
         * teleport) */
        panelClass: { type: String, default: '' },

        /** Additional CSS class to apply to the header element */
        headerClass: { type: String, default: '' },

        /** Whether the panel can be dragged by its header (or other element with
         * data-tool-panel-drag="true") */
        allowDrag: { type: Boolean, default: true },

        /**
         * Boolean (true means all sides can be resized) or string containing N S E W characters to
         * indicate which sides can be resized
         */
        allowResize: { type: [String, Boolean], default: 'NSEW' },

        /** Width of resize handles in pixels */
        resizeHandleWidth: { type: Number, default: 8 },

        /** Minimum width of the panel in pixels */
        minWidth: { type: Number, default: null },

        /** Minimum height of the panel in pixels */
        minHeight: { type: Number, default: null },
    },
    data() {
        const initialBounds = this.bounds ? { ...this.bounds } : null;
        return {
            ready: false,
            localBounds: initialBounds,
            containerSize: {
                width: 0,
                height: 0,
            },
            resizeObserver: null,
            dragHandler: null,
        };
    },
    computed: {
        cssPosition() {
            if (this.validBounds) {
                const { x, y, width, height } = this.validBounds;
                return {
                    left: x != null ? this.cssPx(x) : 'auto',
                    top: y != null ? this.cssPx(y) : 'auto',
                    width: width != null ? this.cssPx(width) : 'auto',
                    height: height != null ? this.cssPx(height) : 'auto',
                    minWidth: this.minWidth ? this.cssPx(this.minWidth) : null,
                    minHeight: this.minHeight ? this.cssPx(this.minHeight) : null,
                };
            }

            return null;
        },

        canResizeNorth() {
            return (
                this.allowResize === true ||
                (typeof this.allowResize === 'string' && this.allowResize.includes('N'))
            );
        },
        canResizeSouth() {
            return (
                this.allowResize === true ||
                (typeof this.allowResize === 'string' && this.allowResize.includes('S'))
            );
        },
        canResizeEast() {
            return (
                this.allowResize === true ||
                (typeof this.allowResize === 'string' && this.allowResize.includes('E'))
            );
        },
        canResizeWest() {
            return (
                this.allowResize === true ||
                (typeof this.allowResize === 'string' && this.allowResize.includes('W'))
            );
        },

        shouldShowHeader() {
            if (this.showHeader === null) {
                return Boolean(this.title || this.$slots.header);
            }

            return !!this.showHeader;
        },
        validBounds() {
            // default validation constrains to parent/window bounds
            const validated = this.validateBounds(this.localBounds);
            return validated;
        },
    },
    watch: {
        bounds(nv) {
            if (nv && Object.keys(nv).length > 0) {
                this.localBounds = { ...nv };
            }
        },

        validBounds(nv, ov) {
            if (JSON.stringify(nv) !== JSON.stringify(ov)) {
                this.$emit('update:bounds', nv);
            }
        },
    },
    mounted() {
        // listen for window resize if teleported to body
        this.addPanelDragHandler();
        this.addPanelResizeHandlers();
        this.addContainerResizeHandlers();

        this.$nextTick(() => {
            this.updateContainerSize();
            this.localBounds = {
                x: 0,
                y: 0,
                width: null,
                height: null,
                ...this.bounds,
            };
            this.ready = true;
        });
    },
    beforeUnmount() {
        this.removeContainerResizeHandlers();
        if (this.dragHandler) {
            this.dragHandler.disabled = true;
            this.dragHandler = null;
        }
    },
    methods: {
        updateBounds(newBounds) {
            this.localBounds = {
                ...this.localBounds,
                ...newBounds,
            };
        },

        validateBounds(bounds) {
            if (!bounds) {
                return null;
            }

            const validated = { ...bounds };

            // Always ensure minimum dimensions
            if (validated.width != null) {
                validated.width = Math.max(validated.width, this.minWidth || 0);
            }
            if (validated.height != null) {
                validated.height = Math.max(validated.height, this.minHeight || 0);
            }

            // Constrain to container/window bounds
            if (validated.x != null) {
                validated.x = Math.max(
                    0,
                    Math.min(
                        validated.x,
                        this.containerSize.width - (validated.width || this.minWidth),
                    ),
                );
            }
            if (validated.y != null) {
                validated.y = Math.max(
                    0,
                    Math.min(
                        validated.y,
                        this.containerSize.height - (validated.height || this.minHeight),
                    ),
                );
            }

            return validated;
        },

        updateContainerSize() {
            // window size
            if (this.teleportToBody) {
                this.containerSize = {
                    width: document.documentElement.clientWidth,
                    height: document.documentElement.clientHeight,
                };
            }

            // parent size
            else if (this.$el && this.$el.parentElement) {
                const parentRect = this.$el.parentElement.getBoundingClientRect();
                this.containerSize = {
                    width: parentRect.width,
                    height: parentRect.height,
                };
            }

            // no parent or window
            else {
                this.containerSize = {
                    width: 0,
                    height: 0,
                };
            }
        },

        addContainerResizeHandlers() {
            if (this.teleportToBody) {
                window.addEventListener('resize', this.updateContainerSize);
            } else {
                if (typeof ResizeObserver === 'undefined' || !this.$el || !this.$el.parentElement) {
                    return;
                }

                this.resizeObserver = new ResizeObserver(entries => {
                    this.updateContainerSize();
                });

                // Start observing the parent element
                this.resizeObserver.observe(this.$el.parentElement);
            }
        },

        removeContainerResizeHandlers() {
            if (this.teleportToBody) {
                window.removeEventListener('resize', this.updateContainerSize);
            } else if (this.resizeObserver) {
                this.resizeObserver.disconnect();
                this.resizeObserver = null;
            }
        },

        addPanelDragHandler() {
            const panel = this.$refs.panel;
            if (!panel) return;

            const dragElement = panel.querySelector('[data-tool-panel-drag="true"]');
            if (!dragElement) return;

            let startBounds = null;

            this.dragHandler = new Drag(dragElement, {
                onStart: () => {
                    if (!this.allowDrag) return;
                    startBounds = { ...this.validBounds };
                    this.$emit('dragstart', startBounds);
                },
                onDrag: ev => {
                    if (!this.allowDrag) return;
                    if (!startBounds) return;

                    // Calculate new position
                    const newX = startBounds.x + ev.startDelta.x;
                    const newY = startBounds.y + ev.startDelta.y;

                    // get the panel's actual dimensions (explicity or content size)
                    const panelHeight = this.validBounds.height || panel.offsetHeight;
                    const panelWidth = this.validBounds.width || panel.offsetWidth;

                    // constrain to current window bounds
                    const constrainedX = Math.max(
                        0,
                        Math.min(newX, this.containerSize.width - panelWidth),
                    );
                    const constrainedY = Math.max(
                        0,
                        Math.min(newY, this.containerSize.height - panelHeight),
                    );

                    const newBounds = this.validateBounds({
                        ...this.validBounds,
                        x: constrainedX,
                        y: constrainedY,
                    });

                    this.localBounds = newBounds;
                    this.$emit('drag', newBounds);
                },
                onEnd: () => {
                    if (!this.allowDrag) return;

                    if (startBounds) {
                        this.$emit('dragend', this.validBounds);
                    }
                    startBounds = null;
                },
            });
        },

        addPanelResizeHandlers() {
            const panel = this.$refs.panel;
            if (!panel) return;

            const handles = panel.querySelectorAll('[data-resize-handle]');
            if (!handles.length) return;

            handles.forEach(handle => {
                const direction = handle.getAttribute('data-resize-handle');
                const isEast = direction.includes('e');
                const isWest = direction.includes('w');
                const isNorth = direction.includes('n');
                const isSouth = direction.includes('s');
                let startBounds = null;

                new Drag(handle, {
                    onStart: () => {
                        startBounds = { ...this.validBounds };
                        if (startBounds.height == null) {
                            startBounds.height = panel.offsetHeight;
                        }
                        if (startBounds.width == null) {
                            startBounds.width = panel.offsetWidth;
                        }

                        this.$emit('resizestart', startBounds);
                    },
                    onDrag: ev => {
                        if (!startBounds) return;

                        const newBounds = { ...startBounds };
                        const minX = 0;
                        const minY = 0;
                        const maxX = this.containerSize.width;
                        const maxY = this.containerSize.height;

                        // Handle horizontal resizing
                        if (isEast || isWest) {
                            if (isWest) {
                                // Resizing from left edge
                                const newX = Math.max(minX, startBounds.x + ev.startDelta.x);
                                const maxWidth = startBounds.x + startBounds.width - newX;
                                newBounds.x = newX;
                                newBounds.width = Math.min(
                                    startBounds.width - (newX - startBounds.x),
                                    maxWidth,
                                );
                            } else {
                                // Resizing from right edge
                                const maxWidth = maxX - startBounds.x;
                                newBounds.width = Math.min(
                                    startBounds.width + ev.startDelta.x,
                                    maxWidth,
                                );
                            }
                        }

                        // Handle vertical resizing
                        if (isNorth || isSouth) {
                            if (isNorth) {
                                // Resizing from top edge
                                const newY = Math.max(minY, startBounds.y + ev.startDelta.y);
                                const maxHeight = startBounds.y + startBounds.height - newY;
                                newBounds.y = newY;
                                newBounds.height = Math.min(
                                    startBounds.height - (newY - startBounds.y),
                                    maxHeight,
                                );
                            } else {
                                // Resizing from bottom edge
                                const maxHeight = maxY - startBounds.y;
                                newBounds.height = Math.min(
                                    startBounds.height + ev.startDelta.y,
                                    maxHeight,
                                );
                            }
                        }

                        // Only apply minimum dimensions to non-null values
                        if (newBounds.width != null) {
                            newBounds.width = Math.max(newBounds.width, this.minWidth || 0);
                        }
                        if (newBounds.height != null) {
                            newBounds.height = Math.max(newBounds.height, this.minHeight || 0);
                        }

                        this.updateBounds(newBounds);
                        this.$emit('resize', this.validBounds);
                    },
                    onEnd: () => {
                        if (startBounds) {
                            this.$emit('resizeend', this.validBounds);
                        }
                        startBounds = null;
                    },
                });
            });
        },
        getPanelElement() {
            return this.$refs.panel;
        },
    },
};
</script>

<style lang="scss">
@use '../styles/px.scss' as *;

.px-tool-panel {
    // panel
    --px-tool-panel-bg: #{get('controls.popup.background-color')};
    --px-tool-panel-padding: 0.75em;
    --px-tool-panel-radius: #{get('controls.popup.border-radius')};
    --px-tool-panel-shadow: #{get('controls.popup.box-shadow')};

    // header
    --px-tool-panel-header-bg: transparent;
    --px-tool-panel-header-padding: 0.5em 0.75em;
    --px-tool-panel-header-border-color: transparent;

    // resize
    --px-tool-panel-resize-width: 8px;
    --px-tool-panel-resize-handle-hover-bg: #{rgba(0, 0, 0, 0)};

    //--px-tool-panel-bg: var(--theme-page-bg, rgba(255, 255, 255, 0.9));

    position: absolute;
    border-radius: var(--px-tool-panel-radius);
    box-shadow: var(--px-tool-panel-shadow);
    background-color: var(--px-tool-panel-bg);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;

    &--teleport {
        position: fixed;
    }

    &__header {
        padding: var(--px-tool-panel-header-padding);
        background-color: var(--px-tool-panel-header-bg);
        border-bottom: 1px solid var(--px-tool-panel-header-border-color);
        user-select: none;
        display: flex;
        align-items: center;
    }

    [data-tool-panel-drag='true'] {
        cursor: move;
    }

    &__content {
        position: relative;
        flex: 1;
        min-height: 0;
        padding: var(--px-tool-panel-padding);
    }

    &__resize-handle {
        position: absolute;
        background-color: transparent;
        z-index: 1;

        &--n {
            top: calc(-1 * var(--px-tool-panel-resize-width) / 2);
            left: 0;
            right: 0;
            height: var(--px-tool-panel-resize-width);
            cursor: ns-resize;
        }

        &--s {
            bottom: calc(-1 * var(--px-tool-panel-resize-width) / 2);
            left: 0;
            right: 0;
            height: var(--px-tool-panel-resize-width);
            cursor: ns-resize;
        }

        &--e {
            top: 0;
            right: calc(-1 * var(--px-tool-panel-resize-width) / 2);
            bottom: 0;
            width: var(--px-tool-panel-resize-width);
            cursor: ew-resize;
        }

        &--w {
            top: 0;
            left: calc(-1 * var(--px-tool-panel-resize-width) / 2);
            bottom: 0;
            width: var(--px-tool-panel-resize-width);
            cursor: ew-resize;
        }

        /* Corner handles - small squares */
        &--ne {
            top: calc(-1 * var(--px-tool-panel-resize-width) / 2);
            right: calc(-1 * var(--px-tool-panel-resize-width) / 2);
            width: var(--px-tool-panel-resize-width);
            height: var(--px-tool-panel-resize-width);
            cursor: nesw-resize;
        }

        &--nw {
            top: calc(-1 * var(--px-tool-panel-resize-width) / 2);
            left: calc(-1 * var(--px-tool-panel-resize-width) / 2);
            width: var(--px-tool-panel-resize-width);
            height: var(--px-tool-panel-resize-width);
            cursor: nesw-resize;
        }

        &--se {
            bottom: calc(-1 * var(--px-tool-panel-resize-width) / 2);
            right: calc(-1 * var(--px-tool-panel-resize-width) / 2);
            width: var(--px-tool-panel-resize-width);
            height: var(--px-tool-panel-resize-width);
            cursor: nwse-resize;
        }

        &--sw {
            bottom: calc(-1 * var(--px-tool-panel-resize-width) / 2);
            left: calc(-1 * var(--px-tool-panel-resize-width) / 2);
            width: var(--px-tool-panel-resize-width);
            height: var(--px-tool-panel-resize-width);
            cursor: nesw-resize;
        }

        &:hover {
            background-color: var(--px-tool-panel-resize-handle-hover-bg);
        }
    }
}
</style>
