import { addDisposableListener, EventType, getWindow } from '../../dom.js';
import { TimeoutTimer } from '../../../common/async.js';
import { CancellationTokenSource } from '../../../common/cancellation.js';
import { isMarkdownString } from '../../../common/htmlContent.js';
import { stripIcons } from '../../../common/iconLabels.js';
import { DisposableStore } from '../../../common/lifecycle.js';
import { isString, isFunction } from '../../../common/types.js';
import { localizeWithPath } from '../../../../nls.js';
const _moduleId = "vs/base/browser/ui/hover/updatableHoverWidget";
function setupNativeHover(htmlElement, tooltip) {
    if (isString(tooltip)) {
        htmlElement.title = stripIcons(tooltip);
    }
    else if (tooltip?.markdownNotSupportedFallback) {
        htmlElement.title = tooltip.markdownNotSupportedFallback;
    }
    else {
        htmlElement.removeAttribute('title');
    }
}
class UpdatableHoverWidget {
    constructor(hoverDelegate, target, fadeInAnimation) {
        this.hoverDelegate = hoverDelegate;
        this.target = target;
        this.fadeInAnimation = fadeInAnimation;
    }
    async update(content, focus, options) {
        if (this._cancellationTokenSource) {
            this._cancellationTokenSource.dispose(true);
            this._cancellationTokenSource = undefined;
        }
        if (this.isDisposed) {
            return;
        }
        let resolvedContent;
        if (content === undefined || isString(content) || content instanceof HTMLElement) {
            resolvedContent = content;
        }
        else if (!isFunction(content.markdown)) {
            resolvedContent = content.markdown ?? content.markdownNotSupportedFallback;
        }
        else {
            if (!this._hoverWidget) {
                this.show(( localizeWithPath(_moduleId, 0, "Loading...")), focus);
            }
            this._cancellationTokenSource = ( (new CancellationTokenSource()));
            const token = this._cancellationTokenSource.token;
            resolvedContent = await content.markdown(token);
            if (resolvedContent === undefined) {
                resolvedContent = content.markdownNotSupportedFallback;
            }
            if (this.isDisposed || token.isCancellationRequested) {
                return;
            }
        }
        this.show(resolvedContent, focus, options);
    }
    show(content, focus, options) {
        const oldHoverWidget = this._hoverWidget;
        if (this.hasContent(content)) {
            const hoverOptions = {
                content,
                target: this.target,
                appearance: {
                    showPointer: this.hoverDelegate.placement === 'element',
                    skipFadeInAnimation: !this.fadeInAnimation || !!oldHoverWidget,
                },
                position: {
                    hoverPosition: 2 ,
                },
                ...options
            };
            this._hoverWidget = this.hoverDelegate.showHover(hoverOptions, focus);
        }
        oldHoverWidget?.dispose();
    }
    hasContent(content) {
        if (!content) {
            return false;
        }
        if (isMarkdownString(content)) {
            return !!content.value;
        }
        return true;
    }
    get isDisposed() {
        return this._hoverWidget?.isDisposed;
    }
    dispose() {
        this._hoverWidget?.dispose();
        this._cancellationTokenSource?.dispose(true);
        this._cancellationTokenSource = undefined;
    }
}
function getHoverTargetElement(element, stopElement) {
    stopElement = stopElement ?? getWindow(element).document.body;
    while (!element.hasAttribute('custom-hover') && element !== stopElement) {
        element = element.parentElement;
    }
    return element;
}
function setupCustomHover(hoverDelegate, htmlElement, content, options) {
    htmlElement.setAttribute('custom-hover', 'true');
    if (htmlElement.title !== '') {
        console.warn('HTML element already has a title attribute, which will conflict with the custom hover. Please remove the title attribute.');
        console.trace('Stack trace:', htmlElement.title);
        htmlElement.title = '';
    }
    let hoverPreparation;
    let hoverWidget;
    const hideHover = (disposeWidget, disposePreparation) => {
        const hadHover = hoverWidget !== undefined;
        if (disposeWidget) {
            hoverWidget?.dispose();
            hoverWidget = undefined;
        }
        if (disposePreparation) {
            hoverPreparation?.dispose();
            hoverPreparation = undefined;
        }
        if (hadHover) {
            hoverDelegate.onDidHideHover?.();
            hoverWidget = undefined;
        }
    };
    const triggerShowHover = (delay, focus, target) => {
        return (
             (new TimeoutTimer(async () => {
                if (!hoverWidget || hoverWidget.isDisposed) {
                    hoverWidget = ( (new UpdatableHoverWidget(hoverDelegate, target || htmlElement, delay > 0)));
                    await hoverWidget.update(typeof content === 'function' ? content() : content, focus, options);
                }
            }, delay))
        );
    };
    let isMouseDown = false;
    const mouseDownEmitter = addDisposableListener(htmlElement, EventType.MOUSE_DOWN, () => {
        isMouseDown = true;
        hideHover(true, true);
    }, true);
    const mouseUpEmitter = addDisposableListener(htmlElement, EventType.MOUSE_UP, () => {
        isMouseDown = false;
    }, true);
    const mouseLeaveEmitter = addDisposableListener(htmlElement, EventType.MOUSE_LEAVE, (e) => {
        isMouseDown = false;
        hideHover(false, e.fromElement === htmlElement);
    }, true);
    const onMouseOver = (e) => {
        if (hoverPreparation) {
            return;
        }
        const toDispose = ( (new DisposableStore()));
        const target = {
            targetElements: [htmlElement],
            dispose: () => { }
        };
        if (hoverDelegate.placement === undefined || hoverDelegate.placement === 'mouse') {
            const onMouseMove = (e) => {
                target.x = e.x + 10;
                if ((e.target instanceof HTMLElement) && getHoverTargetElement(e.target, htmlElement) !== htmlElement) {
                    hideHover(true, true);
                }
            };
            toDispose.add(addDisposableListener(htmlElement, EventType.MOUSE_MOVE, onMouseMove, true));
        }
        hoverPreparation = toDispose;
        if ((e.target instanceof HTMLElement) && getHoverTargetElement(e.target, htmlElement) !== htmlElement) {
            return;
        }
        toDispose.add(triggerShowHover(hoverDelegate.delay, false, target));
    };
    const mouseOverDomEmitter = addDisposableListener(htmlElement, EventType.MOUSE_OVER, onMouseOver, true);
    const onFocus = () => {
        if (isMouseDown || hoverPreparation) {
            return;
        }
        const target = {
            targetElements: [htmlElement],
            dispose: () => { }
        };
        const toDispose = ( (new DisposableStore()));
        const onBlur = () => hideHover(true, true);
        toDispose.add(addDisposableListener(htmlElement, EventType.BLUR, onBlur, true));
        toDispose.add(triggerShowHover(hoverDelegate.delay, false, target));
        hoverPreparation = toDispose;
    };
    let focusDomEmitter;
    const tagName = htmlElement.tagName.toLowerCase();
    if (tagName !== 'input' && tagName !== 'textarea') {
        focusDomEmitter = addDisposableListener(htmlElement, EventType.FOCUS, onFocus, true);
    }
    const hover = {
        show: focus => {
            hideHover(false, true);
        },
        hide: () => {
            hideHover(true, true);
        },
        update: async (newContent, hoverOptions) => {
            content = newContent;
            await hoverWidget?.update(content, undefined, hoverOptions);
        },
        dispose: () => {
            mouseOverDomEmitter.dispose();
            mouseLeaveEmitter.dispose();
            mouseDownEmitter.dispose();
            mouseUpEmitter.dispose();
            focusDomEmitter?.dispose();
            hideHover(true, true);
        }
    };
    return hover;
}
export { setupCustomHover, setupNativeHover };
