/**
 * @license
 * Copyright Tyler Technologies, Inc. 
 * License: Apache-2.0
 */
import { PointerState, StateLayerCoords, STATE_LAYER_CONSTANTS, TOUCH_DELAY_MS } from './state-layer-constants';
export class StateLayerFoundation {
    constructor(_adapter) {
        this._adapter = _adapter;
        // State
        this._target = null;
        this._disabled = false;
        this._attached = false;
        this._deferred = true;
        this._pointerState = PointerState.INACTIVE;
        this._checkBoundsAfterContextMenu = false;
        this._pointerEnterListener = (evt) => this._onPointerEnter(evt);
        this._pointerLeaveListener = (evt) => this._onPointerLeave(evt);
        this._pointerDownListener = (evt) => this._onPointerDown(evt);
        this._pointerUpListener = (evt) => this._onPointerUp(evt);
        this._pointerCancelListener = (evt) => this._onPointerCancel(evt);
        this._clickListener = () => this._onClick();
        this._contextmenuListener = () => this._onContextmenu();
    }
    initialize() {
        this._adapter.trySetTarget(this._target);
        // Clicks could be triggered programmatically so we need to listen for them regardless
        // of deferred initialization status
        this._adapter.addTargetListener('click', this._clickListener);
        // We defer initialization until the first pointerenter event is received.
        //
        // This is a performance optimization to avoid attaching many listeners to the target element
        // until the user is first interacting with it.
        this._deferInitialization();
    }
    disconnect() {
        this._pointerStartEvent = undefined;
        this._pointerState = PointerState.INACTIVE;
        this._adapter.setHovered(false);
        this._adapter.setPressed(false);
        this._removeListeners(); // Must be called before destroying adapter
        this._adapter.destroy();
    }
    playAnimation(coords) {
        this._adapter.startAnimation(coords);
        this._endAnimation();
    }
    _deferInitialization() {
        this._adapter.deferInitialization(this._onDeferredInitialize.bind(this));
    }
    _applyListeners() {
        if (this._disabled) {
            return;
        }
        this._adapter.addTargetListener('pointerenter', this._pointerEnterListener);
        this._adapter.addTargetListener('pointerleave', this._pointerLeaveListener);
        this._adapter.addTargetListener('pointerdown', this._pointerDownListener);
        this._adapter.addTargetListener('pointerup', this._pointerUpListener);
        this._adapter.addTargetListener('pointercancel', this._pointerCancelListener);
        this._adapter.addTargetListener('contextmenu', this._contextmenuListener);
        this._attached = true;
    }
    _removeListeners() {
        this._adapter.removeTargetListener('pointerenter', this._pointerEnterListener);
        this._adapter.removeTargetListener('pointerleave', this._pointerLeaveListener);
        this._adapter.removeTargetListener('pointerdown', this._pointerDownListener);
        this._adapter.removeTargetListener('pointerup', this._pointerUpListener);
        this._adapter.removeTargetListener('pointercancel', this._pointerCancelListener);
        this._adapter.removeTargetListener('click', this._clickListener);
        this._adapter.removeTargetListener('contextmenu', this._contextmenuListener);
        this._attached = false;
    }
    _onDeferredInitialize(evt) {
        if (!this._adapter.isConnected) {
            return;
        }
        this._applyListeners();
        if ((evt === null || evt === void 0 ? void 0 : evt.type) === 'pointerenter') {
            // Manually trigger the pointerenter listener since this is in response to a pointerenter event already
            this._pointerEnterListener(evt);
        }
        this._deferred = false;
    }
    _onPointerEnter(evt) {
        if (!this._canHandleEvent(evt)) {
            return;
        }
        this._adapter.setHovered(true);
    }
    _onPointerLeave(evt) {
        if (!this._canHandleEvent(evt)) {
            return;
        }
        this._adapter.setHovered(false);
        if (this._pointerState !== PointerState.INACTIVE) {
            this._adapter.endAnimation();
        }
    }
    async _onPointerDown(evt) {
        this._pointerStartEvent = evt;
        if (!this._canHandleEvent(evt)) {
            return;
        }
        if (!this._isTouch(evt.pointerType)) {
            this._pointerState = PointerState.WAITING_FOR_CLICK;
            this._startAnimation(evt);
            return;
        }
        // after a longpress contextmenu event, an extra `pointerdown` can be
        // dispatched to the pressed element. Check that the down is within
        // bounds of the element in this case.
        if (this._checkBoundsAfterContextMenu && !this._adapter.inBounds(evt.x, evt.y)) {
            return;
        }
        this._checkBoundsAfterContextMenu = false;
        this._pointerState = PointerState.TOUCH_DELAY;
        await new Promise(resolve => setTimeout(resolve, TOUCH_DELAY_MS));
        if (this._pointerState !== PointerState.TOUCH_DELAY) {
            return;
        }
        this._pointerState = PointerState.HOLDING;
        this._startAnimation(evt);
    }
    _onPointerUp(evt) {
        if (!this._canHandleEvent(evt)) {
            return;
        }
        if (this._pointerState === PointerState.HOLDING) {
            this._pointerState = PointerState.WAITING_FOR_CLICK;
            return;
        }
        if (this._pointerState === PointerState.TOUCH_DELAY) {
            this._pointerState = PointerState.WAITING_FOR_CLICK;
            this._startAnimation(this._pointerStartEvent);
            return;
        }
    }
    _onClick() {
        if (this._disabled) {
            return;
        }
        if (this._pointerState === PointerState.WAITING_FOR_CLICK) {
            this._endAnimation();
            return;
        }
        if (this._pointerState === PointerState.INACTIVE) {
            this._startAnimation(this._pointerStartEvent);
            this._endAnimation();
        }
    }
    _onPointerCancel(evt) {
        if (!this._canHandleEvent(evt)) {
            return;
        }
        this._endAnimation();
    }
    _onContextmenu() {
        if (this._disabled) {
            return;
        }
        this._checkBoundsAfterContextMenu = true;
        this._adapter.endAnimation();
    }
    _startAnimation(evt) {
        const coords = evt ? StateLayerCoords.fromPointerEvent(evt) : undefined;
        this._adapter.startAnimation(coords);
    }
    _endAnimation() {
        this._pointerState = PointerState.INACTIVE;
        this._adapter.endAnimation();
        this._pointerStartEvent = undefined;
    }
    _canHandleEvent({ type, isPrimary, buttons, pointerType, pointerId }) {
        if (this._disabled || !isPrimary) {
            return false;
        }
        if (this._pointerStartEvent && this._pointerStartEvent.pointerId !== pointerId) {
            return false;
        }
        if (type === 'pointerenter' || type === 'pointerleave') {
            return !this._isTouch(pointerType);
        }
        const isPrimaryButton = buttons === 1;
        return this._isTouch(pointerType) || isPrimaryButton;
    }
    _isTouch(pointerType) {
        return pointerType === 'touch';
    }
    get isAttached() {
        return this._attached;
    }
    get targetElement() {
        return this._adapter.getTargetElement();
    }
    set targetElement(el) {
        // Always remove the listeners from the previous target element
        this._removeListeners();
        // If unattached destroy the defer listener to recreate on the new target element
        if (!this._attached) {
            this._adapter.destroy();
            this._deferred = false;
        }
        this._adapter.setTargetElement(el);
        this._adapter.addTargetListener('click', this._clickListener);
        // If we are not already deferring attaching the listeners, then do that now
        if (!this._deferred) {
            this._deferInitialization();
        }
    }
    get target() {
        return this._adapter.getHostAttribute(STATE_LAYER_CONSTANTS.attributes.TARGET);
    }
    set target(value) {
        if (this._target !== value) {
            this._target = value;
            if (this._adapter.isConnected) {
                // Always remove the listeners from the previous target element
                this._removeListeners();
                if (!this._attached) {
                    this._adapter.destroy();
                    this._deferred = false;
                }
                this._adapter.trySetTarget(value);
                this._adapter.addTargetListener('click', this._clickListener);
                if (!this._deferred) {
                    this._deferInitialization();
                }
            }
            this._adapter.toggleHostAttribute(STATE_LAYER_CONSTANTS.attributes.TARGET, Boolean(this._target), this._target);
        }
    }
    get disabled() {
        return this._disabled;
    }
    set disabled(value) {
        value = Boolean(value);
        if (this._disabled !== value) {
            this._disabled = value;
            if (this._adapter.isConnected) {
                if (this._disabled) {
                    this._removeListeners();
                }
                else if (!this._deferred) {
                    this._deferInitialization();
                }
            }
            this._adapter.toggleHostAttribute(STATE_LAYER_CONSTANTS.attributes.DISABLED, this._disabled);
        }
    }
}
