// #region Imports
// #endregion
// #region Constants
/**
 * Additional events to listen for during resize.
 *
 * @internal
 */
const ADDITIONAL_EVENTS = [
    'pointermove',
    'lostpointercapture',
    'contextmenu'
];
// #endregion
// #region Helper Functions
/**
 * Creates a default resize ghost element.
 *
 * @param rect - The bounding rect of the original element.
 * @returns The ghost element.
 *
 * @internal
 */
function createDefaultDragResizeGhost(rect) {
    const element = document.createElement('div');
    Object.assign(element.style, {
        position: 'fixed',
        left: `${rect.x}px`,
        top: `${rect.y}px`,
        width: `${rect.width}px`,
        height: `${rect.height}px`,
        zIndex: '1000',
        background: 'rgba(0, 0, 0, 0.1)',
        border: '2px dashed rgba(0, 0, 0, 0.3)',
        borderRadius: 'inherit',
        pointerEvents: 'none',
        boxSizing: 'border-box'
    });
    return element;
}
/**
 * Clamps a value between min and max.
 *
 * @param value - The value to clamp.
 * @param min - The minimum value.
 * @param max - The maximum value.
 * @returns The clamped value.
 *
 * @internal
 */
function clamp(value, min, max) {
    return Math.max(min, Math.min(max, value));
}
/**
 * Rounds a value by device pixel ratio for crisp rendering.
 *
 * @param value - The value to round.
 * @returns The rounded value.
 *
 * @internal
 */
function roundByDPR(value) {
    const dpr = window.devicePixelRatio || 1;
    return Math.round(value * dpr) / dpr;
}
// #endregion
/**
 * A drag-to-resize controller implementing a ReactiveController pattern.
 *
 * This controller manages pointer events to enable resize functionality
 * on Lit elements via drag handles. It supports both immediate and deferred resize modes.
 *
 * @example
 * ```typescript
 * class MyElement extends LitElement {
 *     private readonly _dragResizeController = addDragResizeController(this, {
 *         matchHandle: (element) => {
 *             if (element.classList.contains('resize-side')) return 'side';
 *             if (element.classList.contains('resize-bottom')) return 'bottom';
 *             if (element.classList.contains('resize-corner')) return 'corner';
 *             return null;
 *         },
 *         ghost: () => this._createResizeGhost(),
 *         start: (params) => this._handleResizeStart(params),
 *         resize: (params) => this._handleResize(params),
 *         end: (params) => this._handleResizeEnd(params),
 *         cancel: (state) => this._handleResizeCancel(state),
 *     });
 * }
 * ```
 *
 * @public
 */
export class DragResizeController {
    // #region Fields
    _host;
    _options = {
        enabled: true,
        mode: 'deferred',
        minWidth: 50,
        minHeight: 50,
        maxWidth: Infinity,
        maxHeight: Infinity
    };
    _abortController = null;
    _state;
    _pointerId = -1;
    _hasPointerCapture = false;
    _ghost = null;
    _captureElement = null;
    // #endregion
    // #region Ctor
    /**
     * Constructs a new instance of the `DragResizeController` class.
     *
     * @param host - The host element.
     * @param options - The configuration options.
     *
     * @public
     */
    constructor(host, options) {
        this._host = host;
        this._host.addController(this);
        this.set(options);
    }
    // #endregion
    // #region Properties
    /**
     * Whether the resize controller is enabled.
     *
     * @public
     * @readonly
     */
    get enabled() {
        return Boolean(this._options.enabled);
    }
    /**
     * Whether a resize operation is currently in progress.
     *
     * @public
     * @readonly
     */
    get isResizing() {
        return this._hasPointerCapture;
    }
    /**
     * Whether the current resize mode is deferred.
     *
     * @private
     * @readonly
     */
    get _isDeferred() {
        return this._options.mode === 'deferred';
    }
    /**
     * The element being resized.
     * When in deferred mode this returns a reference to the resize ghost element,
     * otherwise it is the host element.
     *
     * @private
     * @readonly
     */
    get _resizeItem() {
        if (this._isDeferred && this._ghost) {
            return this._ghost;
        }
        return this._host;
    }
    /**
     * The current state parameters for callbacks.
     *
     * @private
     * @readonly
     */
    get _stateParameters() {
        return {
            ...this._state,
            ghost: this._ghost
        };
    }
    // #endregion
    // #region Methods
    /**
     * Updates the resize controller configuration.
     *
     * @param options - The new configuration options.
     *
     * @public
     */
    set(options) {
        Object.assign(this._options, options);
    }
    /**
     * Stops any resize operation and cleans up state, event listeners, and elements.
     *
     * @public
     */
    dispose() {
        this._removeGhost();
        this._setResizeState(false);
        this._captureElement = null;
    }
    /**
     * @internal
     */
    hostConnected() {
        this._abortController = new AbortController();
        const { signal } = this._abortController;
        this._host.addEventListener('pointerdown', this, { signal });
    }
    /**
     * @internal
     */
    hostDisconnected() {
        this._abortController?.abort();
        this._abortController = null;
        this._setResizeCancelListener(false);
        this._removeGhost();
    }
    /**
     * Handles events dispatched to the controller.
     *
     * @param event - The event.
     *
     * @internal
     */
    handleEvent(event) {
        if (!this.enabled) {
            return;
        }
        switch (event.type) {
            case 'contextmenu':
                event.preventDefault();
                break;
            case 'keydown':
                this._handleCancel(event);
                break;
            case 'pointerdown':
                this._handlePointerDown(event);
                break;
            case 'pointermove':
                this._handlePointerMove(event);
                break;
            case 'lostpointercapture':
                this._handlePointerEnd(event);
                break;
        }
    }
    /**
     * Handles pointer down events.
     *
     * @param event - The pointer event.
     *
     * @private
     */
    _handlePointerDown(event) {
        if (this._shouldSkip(event)) {
            return;
        }
        const handle = this._matchHandle(event);
        if (!handle) {
            return;
        }
        this._setInitialState(event, handle);
        this._createResizeGhost();
        const parameters = {
            event,
            state: this._stateParameters
        };
        if (this._options.start?.call(this._host, parameters) === false) {
            this.dispose();
            return;
        }
        // Capture pointer on the actual resize handle element
        this._captureElement = event.target;
        this._setResizeState();
    }
    /**
     * Handles pointer move events.
     *
     * @param event - The pointer event.
     *
     * @private
     */
    _handlePointerMove(event) {
        if (!this._hasPointerCapture) {
            return;
        }
        this._updatePosition(event);
        this._updateDimensions();
        const parameters = {
            event,
            state: this._stateParameters
        };
        this._options.resize?.call(this._host, parameters);
        this._applyDimensions(this._resizeItem);
    }
    /**
     * Handles pointer end events.
     *
     * @param event - The pointer event.
     *
     * @private
     */
    _handlePointerEnd(event) {
        this._options.end?.call(this._host, {
            event,
            state: this._stateParameters
        });
        this.dispose();
    }
    /**
     * Handles cancel events (Escape key).
     *
     * @param event - The keyboard event.
     *
     * @private
     */
    _handleCancel(event) {
        const key = event.key.toLowerCase();
        if (this._hasPointerCapture && key === 'escape') {
            this._options.cancel?.call(this._host, this._stateParameters);
            this.dispose();
        }
    }
    /**
     * Sets up the cancel key listener.
     *
     * @param enabled - Whether to enable the listener.
     *
     * @private
     */
    _setResizeCancelListener(enabled = true) {
        if (enabled) {
            globalThis.addEventListener('keydown', this);
        }
        else {
            globalThis.removeEventListener('keydown', this);
        }
    }
    /**
     * Sets the initial resize state.
     *
     * @param event - The pointer event.
     * @param handle - The resize handle.
     *
     * @private
     */
    _setInitialState(event, handle) {
        const rect = this._host.getBoundingClientRect();
        this._pointerId = event.pointerId;
        this._state = {
            initial: rect,
            current: structuredClone(rect),
            startPointer: {
                x: event.clientX,
                y: event.clientY
            },
            currentPointer: {
                x: event.clientX,
                y: event.clientY
            },
            delta: {
                x: 0,
                y: 0
            },
            dimensions: {
                width: rect.width,
                height: rect.height
            },
            handle
        };
    }
    /**
     * Sets the resize state (captures pointer, applies styles).
     *
     * @param enabled - Whether resize is active.
     *
     * @private
     */
    _setResizeState(enabled = true) {
        this._hasPointerCapture = enabled;
        const cssValue = enabled ? 'none' : '';
        Object.assign(this._host.style, {
            touchAction: cssValue,
            userSelect: cssValue
        });
        if (this._captureElement) {
            if (enabled) {
                this._captureElement.setPointerCapture(this._pointerId);
            }
            else {
                this._captureElement.releasePointerCapture(this._pointerId);
            }
        }
        this._setResizeCancelListener(enabled);
        for (const type of ADDITIONAL_EVENTS) {
            if (enabled) {
                this._host.addEventListener(type, this);
            }
            else {
                this._host.removeEventListener(type, this);
            }
        }
    }
    /**
     * Updates the position state based on the current pointer position.
     *
     * @param event - The pointer event.
     *
     * @private
     */
    _updatePosition(event) {
        this._state.currentPointer = {
            x: event.clientX,
            y: event.clientY
        };
        this._state.delta = {
            x: event.clientX - this._state.startPointer.x,
            y: event.clientY - this._state.startPointer.y
        };
    }
    /**
     * Updates the dimensions based on the current resize state.
     *
     * @private
     */
    _updateDimensions() {
        const { handle, delta, initial } = this._state;
        const isWidthResize = handle === 'side' || handle === 'corner';
        const isHeightResize = handle === 'bottom' || handle === 'corner';
        let newWidth = initial.width;
        let newHeight = initial.height;
        if (isWidthResize) {
            newWidth = initial.width + delta.x;
        }
        if (isHeightResize) {
            newHeight = initial.height + delta.y;
        }
        // Apply snap function if provided
        if (this._options.snap) {
            const snapped = this._options.snap.call(this._host, {
                width: newWidth,
                height: newHeight
            }, delta, handle);
            newWidth = snapped.width;
            newHeight = snapped.height;
        }
        // Apply constraints
        newWidth = clamp(newWidth, this._options.minWidth ?? 50, this._options.maxWidth ?? Infinity);
        newHeight = clamp(newHeight, this._options.minHeight ?? 50, this._options.maxHeight ?? Infinity);
        this._state.dimensions = {
            width: roundByDPR(newWidth),
            height: roundByDPR(newHeight)
        };
        // Update current rect
        this._state.current = new DOMRect(initial.x, initial.y, this._state.dimensions.width, this._state.dimensions.height);
    }
    /**
     * Applies the dimensions to the element.
     *
     * @param element - The element to resize.
     *
     * @private
     */
    _applyDimensions(element) {
        const { width, height } = this._state.dimensions;
        // Using setProperty to avoid lint warnings - this is necessary for dynamic ghost sizing
        element.style.setProperty('width', `${width}px`);
        element.style.setProperty('height', `${height}px`);
    }
    /**
     * Matches the resize handle from the event.
     *
     * @param event - The pointer event.
     * @returns The matched handle or null.
     *
     * @private
     */
    _matchHandle(event) {
        if (!this._options.matchHandle) {
            // Default: check for resize-handle class
            const target = event.target;
            if (target.classList.contains('resize-handle')) {
                if (target.classList.contains('resize-handle--side')) {
                    return 'side';
                }
                if (target.classList.contains('resize-handle--bottom')) {
                    return 'bottom';
                }
                if (target.classList.contains('resize-handle--corner')) {
                    return 'corner';
                }
                return 'corner';
            }
            return null;
        }
        const path = event.composedPath();
        for (const item of path) {
            if (item instanceof HTMLElement) {
                const handle = this._options.matchHandle.call(this._host, item);
                if (handle) {
                    return handle;
                }
            }
        }
        return null;
    }
    /**
     * Creates the resize ghost element.
     *
     * @private
     */
    _createResizeGhost() {
        if (!this._isDeferred) {
            return;
        }
        this._ghost =
            this._options.ghost?.call(this._host) ??
                createDefaultDragResizeGhost(this._host.getBoundingClientRect());
        document.body.append(this._ghost);
    }
    /**
     * Removes the resize ghost element.
     *
     * @private
     */
    _removeGhost() {
        this._ghost?.remove();
        this._ghost = null;
    }
    /**
     * Checks whether the resize should be skipped.
     *
     * @param event - The pointer event.
     * @returns True if the resize should be skipped.
     *
     * @private
     */
    _shouldSkip(event) {
        return (Boolean(event.button) ||
            Boolean(this._options.skip?.call(this._host, event)));
    }
}
/**
 * Adds a drag-to-resize controller to the given host.
 *
 * @param host - The host element.
 * @param options - The configuration options.
 * @returns The drag resize controller instance.
 *
 * @public
 */
export function addDragResizeController(host, options) {
    return new DragResizeController(host, options);
}
//# sourceMappingURL=DragResizeController.js.map