import { SearchQuery, getSearchState, setSearchState } from 'prosemirror-search';
import { Plugin, TextSelection } from "../../../pm/state.js";
import { findParentNodeClosestToPos } from "../../../pm/utils.js";
import { renderSearchPopup } from "../../../modules/search/index.js";
import { isElementInViewport } from "../../../utils/dom.js";
import { getReactRendererFromState } from "../ReactRenderer/index.js";
import { closeSearch, findNext, findPrev, replaceAll, replaceNext } from "./commands.js";
import { SearchClassName, pluginKey } from "./const.js";
import { searchKeyHandler } from "./key-handler.js";
import { startTracking } from "./utils/connect-tracker.js";
import { FocusManager } from "./utils/focus-manager.js";
import { getCounter } from "./utils/search-counter.js";
import "./search-plugin.css";
export const searchViewPlugin = (params) => {
    return new Plugin({
        key: pluginKey,
        props: {
            handleKeyDown: searchKeyHandler,
        },
        state: {
            init: () => ({ open: false }),
            apply(tr, value, _oldState, _newState) {
                const newValue = tr.getMeta(pluginKey);
                if (typeof newValue === 'object')
                    return newValue;
                return value;
            },
        },
        view(view) {
            return new SeachPluginView(view, params);
        },
    });
};
class SeachPluginView {
    _view;
    _renderer;
    _focusManager;
    _viewDomTrackerDispose;
    _counter;
    _isDomConnected;
    _viewState;
    _searchState;
    constructor(view, params) {
        this._view = view;
        this._viewState = pluginKey.getState(view.state);
        this._searchState = getSearchState(view.state)?.query;
        this._counter = getCounter(view.state);
        this._isDomConnected = view.dom.ownerDocument.contains(view.dom);
        this._renderer = this._createRenderer(params);
        this._focusManager = new FocusManager(view.dom.ownerDocument);
        // uses MutationObserver to detect when view.dom is disconnected from the DOM tree
        // TODO: replace with eventBus (subscribe to change-editor-mode event) to track when to hide the search bar
        // see https://github.com/gravity-ui/markdown-editor/issues/884
        this._viewDomTrackerDispose = startTracking(view.dom, {
            onConnect: this._onEditorViewDomConnected,
            onDisconnect: this._onEditorViewDomDisconnected,
        });
    }
    update() {
        const newCounter = getCounter(this._view.state);
        const newViewState = pluginKey.getState(this._view.state);
        const newSearchState = getSearchState(this._view.state)?.query;
        if (newViewState !== this._viewState ||
            newSearchState !== this._searchState ||
            newCounter.total !== this._counter.total ||
            newCounter.current !== this._counter.current) {
            this._counter = newCounter;
            this._viewState = newViewState;
            this._searchState = newSearchState;
            this._renderer.rerender();
        }
    }
    destroy() {
        this._renderer.remove();
        this._viewDomTrackerDispose();
    }
    _onEditorViewDomConnected = () => {
        this._isDomConnected = true;
        this._renderer.rerender();
    };
    _onEditorViewDomDisconnected = () => {
        this._isDomConnected = false;
        this._onClose();
    };
    _createRenderer(params) {
        return getReactRendererFromState(this._view.state).createItem('search-view', () => {
            const { _viewState: viewState, _searchState: searchState, _isDomConnected: domConnected, } = this;
            if (!domConnected || !viewState?.open || !searchState)
                return null;
            const { dom: viewDom } = this._view;
            const container = (params.editorSelector && viewDom.closest(params.editorSelector)) ||
                viewDom.ownerDocument;
            const anchor = container.querySelector(params.anchorSelector);
            return renderSearchPopup({
                anchor,
                open: viewState.open,
                counter: this._counter,
                state: searchState,
                onClose: this._onClose,
                onChange: this._onChange,
                onSearchPrev: this._onSearchPrev,
                onSearchNext: this._onSearchNext,
                onReplaceNext: this._onReplaceNext,
                onReplaceAll: this._onReplaceAll,
                intersectionTracking: {
                    container: this._view.dom,
                    selector: `.${SearchClassName.ActiveMatch}`,
                    observerOptions: {
                        childList: true,
                        subtree: true,
                        attributes: true,
                        attributeFilter: ['class'],
                    },
                },
            });
        });
    }
    _onChange = (config) => {
        const { state, dispatch } = this._view;
        const query = new SearchQuery({
            search: config.search,
            replace: config.replace,
            caseSensitive: config.caseSensitive,
            wholeWord: config.wholeWord,
        });
        const tr = setSearchState(state.tr, query);
        const { $from, $to } = tr.selection;
        const parent = findParentNodeClosestToPos($from, (node) => node.type.isTextblock);
        let result = null;
        // find match in [sel.$from, parent.$end]
        if (parent)
            result = query.findNext(state, $from.pos, parent.pos + parent.node.nodeSize);
        // find match in [parent.$start or sel.$from, doc.$end]
        if (!result)
            result = query.findNext(state, parent?.pos || $from.pos);
        // find match in [doc.$start, parent.$start or sel.$to]
        if (!result)
            result = query.findPrev(state, parent?.pos || $to.pos);
        // update text selection
        if (result)
            tr.setSelection(TextSelection.create(tr.doc, result.from, result.to));
        dispatch(tr);
    };
    _onClose = () => {
        closeSearch(this._view.state, this._view.dispatch);
        this._view.focus();
    };
    _onSearchPrev = () => {
        this._preserveFocus(findPrev);
        requestAnimationFrame(() => this._scrollToActiveIfNeeded());
    };
    _onSearchNext = () => {
        this._preserveFocus(findNext);
        requestAnimationFrame(() => this._scrollToActiveIfNeeded());
    };
    _onReplaceNext = () => {
        this._preserveFocus(replaceNext);
    };
    _onReplaceAll = () => {
        this._preserveFocus(replaceAll);
    };
    _preserveFocus(command) {
        this._focusManager.storeFocus();
        this._view.focus();
        command(this._view.state, this._view.dispatch, this._view);
        this._focusManager.restoreFocus({ preventScroll: true });
    }
    _scrollToActiveIfNeeded = () => {
        const activeElem = this._view.dom
            .getElementsByClassName(SearchClassName.ActiveMatch)
            .item(0);
        if (activeElem && !isElementInViewport(activeElem)) {
            activeElem.scrollIntoView({
                block: 'nearest',
                inline: 'nearest',
            });
        }
    };
}
//# sourceMappingURL=SearchViewPlugin.js.map
