// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '//resources/cr_components/composebox/composebox_lens_search.js';
import '//resources/cr_components/composebox/contextual_entrypoint_button.js';
import '//resources/cr_components/searchbox/searchbox_dropdown.js';
import '//resources/cr_elements/icons.html.js';
import '/strings.m.js';
import { ColorChangeUpdater } from '//resources/cr_components/color_change_listener/colors_css_updater.js';
import { SearchboxBrowserProxy } from '//resources/cr_components/searchbox/searchbox_browser_proxy.js';
import { kDefaultSelection } from '//resources/cr_components/searchbox/searchbox_match.js';
import { getInstance as getA11yAnnouncer } from '//resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js';
import { I18nMixinLit } from '//resources/cr_elements/i18n_mixin_lit.js';
import { assertNotReached } from '//resources/js/assert.js';
import { EventTracker } from '//resources/js/event_tracker.js';
import { loadTimeData } from '//resources/js/load_time_data.js';
import { MetricsReporterImpl } from '//resources/js/metrics_reporter/metrics_reporter.js';
import { CrLitElement } from '//resources/lit/v3_0/lit.rollup.js';
import { SelectionDirection, SelectionLineState, SelectionStep } from '//resources/mojo/components/omnibox/browser/searchbox.mojom-webui.js';
import { getCss } from './app.css.js';
import { getHtml } from './app.html.js';
import { browserProxyFactory } from './omnibox_popup.mojom-webui.js';
// 675px ~= 449px (--cr-realbox-primary-side-min-width) * 1.5 + some margin.
const canShowSecondarySideMediaQueryList = window.matchMedia('(min-width: 675px)');
// Notifies an a11y announcer to read the aria-label for given element.
function announceElementAriaLabel(element) {
    const message = element.getAttribute('aria-label');
    if (message) {
        // Note: ariaNotify is more efficient and appears to work more reliably, but
        // support is not guaranteed in all browsers. Fall back on a11y announcer if
        // the ariaNotify function is unavailable.
        if (element.ariaNotify) {
            element.ariaNotify(message);
        }
        else {
            getA11yAnnouncer(element).announce(message);
        }
    }
}
// Not all selection states of the webui popup are supported on the native
// browser side.
function selectionIsNativelySupported(s) {
    return s.state !== SelectionLineState.kFocusedButtonContextEntrypoint;
}
function selectionsEqual(a, b) {
    return a.line === b.line && a.state === b.state &&
        a.actionIndex === b.actionIndex;
}
function selectionToString(s) {
    return `{${s.line},${s.state},${s.actionIndex}}`;
}
// Displays the autocomplete matches in the autocomplete result.
export class OmniboxPopupAppElement extends I18nMixinLit(CrLitElement) {
    static get is() {
        return 'omnibox-popup-app';
    }
    static get styles() {
        return getCss();
    }
    render() {
        return getHtml.bind(this)();
    }
    static get properties() {
        return {
            /**
             * Whether the secondary side can be shown based on the feature state and
             * the width available to the dropdown.
             */
            canShowSecondarySide: {
                type: Boolean,
                reflect: true,
            },
            /*
             * Whether the secondary side is currently available to be shown.
             */
            hasSecondarySide: {
                type: Boolean,
                reflect: true,
            },
            /**
             * Whether the app is in debug mode.
             */
            isDebug: {
                type: Boolean,
                reflect: true,
            },
            /**
             * Whether matches are visible, as some may be hidden by filtering rules
             * (e.g., Gemini suggestions).
             */
            hasVisibleMatches_: {
                type: Boolean,
                reflect: true,
            },
            isInKeywordMode_: { type: Boolean },
            result_: { type: Object },
            searchboxLayoutMode_: { reflect: true, type: String },
            showContextEntrypoint_: {
                type: Boolean,
                reflect: true,
            },
            showContextButtonSuggestionLabel_: { type: Boolean },
            isContentSharingEnabled_: { type: Boolean },
            isLensSearchEnabled_: { type: Boolean },
            isLensSearchEligible_: { type: Boolean },
            isAimPopupEligible_: { type: Boolean },
            isLensChipShown_: { type: Boolean },
            isAimButtonVisible_: { type: Boolean },
            webuiOmniboxPopupSelectionControlEnabled_: { type: Boolean },
            inputState_: { type: Object },
            usePecApi_: { type: Boolean },
            applyContextButtonBackground_: { type: Boolean },
            isOblongShape_: { type: Boolean },
        };
    }
    #canShowSecondarySide_accessor_storage = canShowSecondarySideMediaQueryList.matches;
    get canShowSecondarySide() { return this.#canShowSecondarySide_accessor_storage; }
    set canShowSecondarySide(value) { this.#canShowSecondarySide_accessor_storage = value; }
    #hasSecondarySide_accessor_storage = false;
    get hasSecondarySide() { return this.#hasSecondarySide_accessor_storage; }
    set hasSecondarySide(value) { this.#hasSecondarySide_accessor_storage = value; }
    #isDebug_accessor_storage = false;
    get isDebug() { return this.#isDebug_accessor_storage; }
    set isDebug(value) { this.#isDebug_accessor_storage = value; }
    #isInKeywordMode__accessor_storage = false;
    get isInKeywordMode_() { return this.#isInKeywordMode__accessor_storage; }
    set isInKeywordMode_(value) { this.#isInKeywordMode__accessor_storage = value; }
    #hasVisibleMatches__accessor_storage = false;
    get hasVisibleMatches_() { return this.#hasVisibleMatches__accessor_storage; }
    set hasVisibleMatches_(value) { this.#hasVisibleMatches__accessor_storage = value; }
    #result__accessor_storage = null;
    get result_() { return this.#result__accessor_storage; }
    set result_(value) { this.#result__accessor_storage = value; }
    #searchboxLayoutMode__accessor_storage = loadTimeData.getString('searchboxLayoutMode');
    get searchboxLayoutMode_() { return this.#searchboxLayoutMode__accessor_storage; }
    set searchboxLayoutMode_(value) { this.#searchboxLayoutMode__accessor_storage = value; }
    #showContextEntrypoint__accessor_storage = false;
    get showContextEntrypoint_() { return this.#showContextEntrypoint__accessor_storage; }
    set showContextEntrypoint_(value) { this.#showContextEntrypoint__accessor_storage = value; }
    #isContentSharingEnabled__accessor_storage = false;
    get isContentSharingEnabled_() { return this.#isContentSharingEnabled__accessor_storage; }
    set isContentSharingEnabled_(value) { this.#isContentSharingEnabled__accessor_storage = value; }
    #isLensSearchEnabled__accessor_storage = loadTimeData.getBoolean('composeboxShowLensSearchChip');
    get isLensSearchEnabled_() { return this.#isLensSearchEnabled__accessor_storage; }
    set isLensSearchEnabled_(value) { this.#isLensSearchEnabled__accessor_storage = value; }
    #webuiOmniboxPopupSelectionControlEnabled__accessor_storage = loadTimeData.getBoolean('webuiOmniboxPopupSelectionControlEnabled');
    get webuiOmniboxPopupSelectionControlEnabled_() { return this.#webuiOmniboxPopupSelectionControlEnabled__accessor_storage; }
    set webuiOmniboxPopupSelectionControlEnabled_(value) { this.#webuiOmniboxPopupSelectionControlEnabled__accessor_storage = value; }
    #isLensSearchEligible__accessor_storage = false;
    get isLensSearchEligible_() { return this.#isLensSearchEligible__accessor_storage; }
    set isLensSearchEligible_(value) { this.#isLensSearchEligible__accessor_storage = value; }
    #isLensChipShown__accessor_storage = false;
    get isLensChipShown_() { return this.#isLensChipShown__accessor_storage; }
    set isLensChipShown_(value) { this.#isLensChipShown__accessor_storage = value; }
    #isAimPopupEligible__accessor_storage = false;
    get isAimPopupEligible_() { return this.#isAimPopupEligible__accessor_storage; }
    set isAimPopupEligible_(value) { this.#isAimPopupEligible__accessor_storage = value; }
    #isAimButtonVisible__accessor_storage = false;
    get isAimButtonVisible_() { return this.#isAimButtonVisible__accessor_storage; }
    set isAimButtonVisible_(value) { this.#isAimButtonVisible__accessor_storage = value; }
    #inputState__accessor_storage = null;
    get inputState_() { return this.#inputState__accessor_storage; }
    set inputState_(value) { this.#inputState__accessor_storage = value; }
    #usePecApi__accessor_storage = loadTimeData.getBoolean('contextualMenuUsePecApi');
    get usePecApi_() { return this.#usePecApi__accessor_storage; }
    set usePecApi_(value) { this.#usePecApi__accessor_storage = value; }
    #applyContextButtonBackground__accessor_storage = false;
    get applyContextButtonBackground_() { return this.#applyContextButtonBackground__accessor_storage; }
    set applyContextButtonBackground_(value) { this.#applyContextButtonBackground__accessor_storage = value; }
    #isOblongShape__accessor_storage = loadTimeData.getBoolean('contextButtonShapeIsOblong');
    get isOblongShape_() { return this.#isOblongShape__accessor_storage; }
    set isOblongShape_(value) { this.#isOblongShape__accessor_storage = value; }
    searchboxBrowserProxy_;
    eventTracker_ = new EventTracker();
    hideContextButton_ = loadTimeData.getBoolean('hideClassicContextButton');
    contextButtonHasBackground_ = loadTimeData.getBoolean('contextButtonHasBackground');
    #showContextButtonSuggestionLabel__accessor_storage = loadTimeData.getBoolean('omniboxShowContextButtonSuggestionLabel');
    get showContextButtonSuggestionLabel_() { return this.#showContextButtonSuggestionLabel__accessor_storage; }
    set showContextButtonSuggestionLabel_(value) { this.#showContextButtonSuggestionLabel__accessor_storage = value; }
    listenerIds_ = [];
    browserProxy_;
    popupListenerIds_ = [];
    selection_ = kDefaultSelection;
    constructor() {
        super();
        this.searchboxBrowserProxy_ = SearchboxBrowserProxy.getInstance();
        this.browserProxy_ = browserProxyFactory.getInstance();
        this.isDebug = new URLSearchParams(window.location.search).has('debug');
        ColorChangeUpdater.forDocument().start();
    }
    async connectedCallback() {
        super.connectedCallback();
        // TODO(b/468113419): The handlers and their definitions are not ordered the
        // same as the mojom file.
        this.popupListenerIds_ = [
            this.browserProxy_.callbackRouter.onShow.addListener(this.onShow_.bind(this)),
            this.browserProxy_.callbackRouter.onContextMenuClosed.addListener(this.onContextMenuClosed_.bind(this)),
        ];
        this.listenerIds_ = [
            this.searchboxBrowserProxy_.callbackRouter.autocompleteResultChanged
                .addListener(this.onAutocompleteResultChanged_.bind(this)),
            this.searchboxBrowserProxy_.callbackRouter.updateSelection.addListener(this.onUpdateSelection_.bind(this)),
            this.searchboxBrowserProxy_.callbackRouter.setKeywordSelected.addListener((isKeywordSelected) => {
                this.isInKeywordMode_ = isKeywordSelected;
            }),
            this.searchboxBrowserProxy_.callbackRouter.updateLensSearchEligibility
                .addListener((eligible) => {
                this.isLensSearchEligible_ = this.isLensSearchEnabled_ && eligible;
            }),
            this.searchboxBrowserProxy_.callbackRouter.updateContentSharingPolicy
                .addListener((enabled) => {
                this.isContentSharingEnabled_ = enabled;
            }),
            this.searchboxBrowserProxy_.callbackRouter.onInputStateChanged
                .addListener((inputState) => {
                this.inputState_ = inputState;
            }),
        ];
        if (!this.hideContextButton_) {
            this.listenerIds_.push(this.searchboxBrowserProxy_.callbackRouter.updateAimPopupEligibility
                .addListener((eligible) => {
                this.isAimPopupEligible_ = eligible;
            }));
        }
        if (this.webuiOmniboxPopupSelectionControlEnabled_) {
            this.listenerIds_.push(this.searchboxBrowserProxy_.callbackRouter.stepSelection.addListener(this.stepSelection_.bind(this)), this.searchboxBrowserProxy_.callbackRouter.openCurrentSelection
                .addListener(this.openCurrentSelection_.bind(this)), this.searchboxBrowserProxy_.callbackRouter.setAimButtonVisible
                .addListener((visible) => {
                this.isAimButtonVisible_ = visible;
            }));
        }
        this.inputState_ =
            (await this.searchboxBrowserProxy_.handler.getInputState()).state;
        canShowSecondarySideMediaQueryList.addEventListener('change', this.onCanShowSecondarySideChanged_.bind(this));
        if (!this.isDebug) {
            this.eventTracker_.add(document.documentElement, 'contextmenu', (e) => {
                e.preventDefault();
            });
        }
    }
    disconnectedCallback() {
        super.disconnectedCallback();
        this.eventTracker_.removeAll();
        for (const listenerId of this.listenerIds_) {
            this.searchboxBrowserProxy_.callbackRouter.removeListener(listenerId);
        }
        this.listenerIds_ = [];
        for (const listenerId of this.popupListenerIds_) {
            this.browserProxy_.callbackRouter.removeListener(listenerId);
        }
        this.popupListenerIds_ = [];
        canShowSecondarySideMediaQueryList.removeEventListener('change', this.onCanShowSecondarySideChanged_.bind(this));
    }
    willUpdate(changedProperties) {
        super.willUpdate(changedProperties);
        const changedPrivateProperties = changedProperties;
        if (changedPrivateProperties.has('result_')) {
            this.hasVisibleMatches_ =
                this.result_?.matches.some(match => !match.isHidden) ?? false;
        }
        if (changedPrivateProperties.has('isAimPopupEligible_') ||
            changedPrivateProperties.has('searchboxLayoutMode_') ||
            changedPrivateProperties.has('isInKeywordMode_') ||
            changedPrivateProperties.has('result_') ||
            changedPrivateProperties.has('isLensSearchEligible_')) {
            this.showContextEntrypoint_ = this.computeShowContextEntrypoint_();
        }
        if (changedPrivateProperties.has('isContentSharingEnabled_') ||
            changedPrivateProperties.has('isLensSearchEligible_')) {
            this.isLensChipShown_ =
                this.isContentSharingEnabled_ && this.isLensSearchEligible_;
            this.applyContextButtonBackground_ =
                this.contextButtonHasBackground_ && !this.isLensChipShown_;
        }
    }
    getDropdown() {
        // Because there are 2 different cr-searchbox-dropdown instances that can be
        // exclusively shown, should always query the DOM to get the relevant one
        // and can't use this.$ to access it.
        return this.shadowRoot.querySelector('cr-searchbox-dropdown');
    }
    shouldHideEntrypointButton_() {
        return this.searchboxLayoutMode_ === 'Compact';
    }
    computeShowContextEntrypoint_() {
        if (this.hideContextButton_ || !this.isAimPopupEligible_ ||
            this.isInKeywordMode_) {
            return false;
        }
        if (this.searchboxLayoutMode_.startsWith('Tall')) {
            return true;
        }
        if (this.searchboxLayoutMode_ === 'Compact') {
            return this.isLensSearchEligible_;
        }
        return false;
    }
    onCanShowSecondarySideChanged_(e) {
        this.canShowSecondarySide = e.matches;
    }
    onAutocompleteResultChanged_(result) {
        // Skip empty results. Otherwise, blurring/closing the omnibox would clear
        // the results in the debug UI.
        if (this.isDebug && !result.matches.length) {
            return;
        }
        this.result_ = result;
        if (this.webuiOmniboxPopupSelectionControlEnabled_) {
            const available = this.getResultSelections_(this.result_);
            const sameLineSelection = {
                ...this.selection_,
                state: SelectionLineState.kNormal,
            };
            if (result.matches[0]?.allowedToBeDefaultMatch) {
                this.setSelection_(available[0] || kDefaultSelection);
            }
            else if (available.some(s => selectionsEqual(s, sameLineSelection))) {
                this.setSelection_(sameLineSelection);
            }
            else {
                this.setSelection_(kDefaultSelection);
            }
            return;
        }
        if (result.matches[0]?.allowedToBeDefaultMatch) {
            this.getDropdown().selectFirst();
        }
        else if (this.getDropdown().selectedMatchIndex >= result.matches.length) {
            this.getDropdown().unselect();
        }
    }
    getContextualEntrypointButton_() {
        if (this.showContextEntrypoint_ && !this.shouldHideEntrypointButton_()) {
            return this.shadowRoot.querySelector('#context');
        }
        return null;
    }
    onShow_() {
        // When the popup is shown, blur the contextual entrypoint. This prevents a
        // focus ring from appearing on the entrypoint, e.g. when the user clicks
        // away and then re-focuses the Omnibox.
        this.getContextualEntrypointButton_()?.blur();
    }
    onDropdownDomChange_() {
        const metricsReporter = MetricsReporterImpl.getInstance();
        metricsReporter.measure('ResultChanged')
            .then(duration => metricsReporter.umaReportTime(loadTimeData.getString('resultChangedToPaintMetricName'), duration))
            .then(() => metricsReporter.clearMark('ResultChanged'))
            // Ignore silently if mark 'ResultChanged' is missing.
            .catch(() => { });
    }
    onUpdateSelection_(oldSelection, selection) {
        if (this.webuiOmniboxPopupSelectionControlEnabled_) {
            this.setSelection_(selection, false);
        }
        else {
            this.getDropdown().updateSelection(oldSelection, selection);
        }
    }
    setSelection_(selection, notify = true) {
        const oldSelection = this.selection_;
        this.selection_ = selection;
        this.getDropdown().updateSelection(oldSelection, this.selection_);
        if (notify) {
            this.searchboxBrowserProxy_.handler.setPopupSelection(selectionIsNativelySupported(this.selection_) ? this.selection_ :
                kDefaultSelection);
        }
        const entrypoint = this.getContextualEntrypointButton_();
        if (entrypoint) {
            entrypoint.hasPopupFocus = this.selection_.state ===
                SelectionLineState.kFocusedButtonContextEntrypoint;
            if (entrypoint.hasPopupFocus) {
                announceElementAriaLabel(entrypoint.shadowRoot.querySelector('#entrypoint'));
            }
        }
    }
    // Changes the current popup selection to the next selection in the order of
    // all available selections. That is, it translates a user intent (the given
    // `direction` and `step`) into a change of popup-focus (distinct from actual
    // browser/input focus, popup-focus shows what item from the popup will be
    // opened when user presses Enter).
    stepSelection_(direction, step) {
        this.setSelection_(this.getNextSelection_(this.selection_, direction, step));
    }
    // Given a current `from` selection, finds the next selection in the order
    // of all available selections. Traverses forward or backward by larger
    // or smaller steps as indicated by `direction` and `step` parameters,
    // which are in turn determined by user input signals such as arrow or
    // tab keystrokes.
    getNextSelection_(from, direction, step) {
        if (!this.result_) {
            return from;
        }
        const available = this.getResultSelections_(this.result_);
        if (available.length === 0) {
            return from;
        }
        const isNormal = (selection) => selection.state === SelectionLineState.kNormal;
        let fromIndex = available.findIndex(s => selectionsEqual(from, s));
        if (fromIndex < 0 && from.state === SelectionLineState.kKeywordMode) {
            // Second chance for keyword mode selections, to accommodate instant
            // keyword mode lines activating keyword mode from native side.
            fromIndex = available.findIndex(s => selectionsEqual({ ...from, state: SelectionLineState.kNormal }, s));
        }
        if (fromIndex < 0) {
            available.splice(0, 0, from);
            fromIndex = 0;
        }
        if (step === SelectionStep.kAllLines) {
            const normalIndex = direction === SelectionDirection.kBackward ?
                available.findIndex(isNormal) :
                available.findLastIndex(isNormal);
            return normalIndex < 0 ? from : available[normalIndex];
        }
        for (let offset = 1; offset < available.length; offset++) {
            const index = (fromIndex +
                (direction === SelectionDirection.kForward ? offset : -offset) +
                available.length) %
                available.length;
            const selection = available[index];
            if (step === SelectionStep.kStateOrLine || isNormal(selection)) {
                return selection;
            }
        }
        return from;
    }
    // Returns the full set of selections available based on the given
    // AutocompleteResult. Note, this currently also depends on the
    // current popup state, e.g. the AI Mode button visibility, but
    // should eventually be driven entirely by a single data structure.
    getResultSelections_(result) {
        const available = [];
        for (let matchIndex = 0; matchIndex < result.matches.length; matchIndex++) {
            const match = result.matches[matchIndex];
            // Preserve selection of the default match, even if hidden, to stay
            // compatible with OmniboxEditModel's keyword mode handling.
            if (match.isHidden && !match.allowedToBeDefaultMatch) {
                continue;
            }
            available.push({
                line: matchIndex,
                state: SelectionLineState.kNormal,
                actionIndex: 0,
            });
            if (match.keywordChipHint.length > 0) {
                available.push({
                    line: matchIndex,
                    state: SelectionLineState.kKeywordMode,
                    actionIndex: 0,
                });
            }
            for (let actionIndex = 0; actionIndex < match.actions.length; actionIndex++) {
                available.push({
                    line: matchIndex,
                    state: SelectionLineState.kFocusedButtonAction,
                    actionIndex: actionIndex,
                });
            }
            if (match.supportsDeletion) {
                available.push({
                    line: matchIndex,
                    state: SelectionLineState.kFocusedButtonRemoveSuggestion,
                    actionIndex: 0,
                });
            }
        }
        // TODO(crbug.com/462775253): Ideally everything available for selection
        // comes from the AutocompleteResult.
        if (this.showContextEntrypoint_ && !this.shouldHideEntrypointButton_()) {
            available.push({
                line: -1,
                state: SelectionLineState.kFocusedButtonContextEntrypoint,
                actionIndex: 0,
            });
        }
        if (this.isAimButtonVisible_) {
            const insertionIndex = available.length > 0 && result.matches[0]?.allowedToBeDefaultMatch ?
                1 :
                0;
            available.splice(insertionIndex, 0, {
                // Use first default match if available (if not, the -1 means kNoMatch).
                line: result.matches.findIndex(m => m.allowedToBeDefaultMatch),
                state: SelectionLineState.kFocusedButtonAim,
                actionIndex: 0,
            });
            if (available.length === 1) {
                // If AIM button is the only selection available, provide a way to
                // deselect it.
                available.splice(0, 0, kDefaultSelection);
            }
        }
        return available;
    }
    // Opens the current popup selection (the one visually indicated by the
    // element with popup-focus).
    openCurrentSelection_(disposition) {
        if (this.selection_.state ===
            SelectionLineState.kFocusedButtonContextEntrypoint) {
            this.browserProxy_.handler.showContextMenu({ x: 0, y: 0 });
        }
        else if (selectionIsNativelySupported(this.selection_)) {
            this.searchboxBrowserProxy_.handler.openPopupSelection(this.result_?.sequenceId || 0, this.selection_, disposition);
        }
        else {
            assertNotReached(`openCurrentSelection_ called for unsupported selection: ${selectionToString(this.selection_)}`);
        }
    }
    onHasSecondarySideChanged_(e) {
        this.hasSecondarySide = e.detail.value;
    }
    onContextMenuEntrypointClick_(e) {
        e.preventDefault();
        const point = {
            x: e.detail.x,
            y: e.detail.y,
        };
        // Force the button to keep its hover background visually while
        // the menu is open, even if the mouse doesn't move out of the button
        // area after clicking.
        const contextButton = this.getContextualEntrypointButton_();
        if (contextButton) {
            contextButton.classList.add('menu-open');
        }
        this.browserProxy_.handler.showContextMenu(point);
    }
    onContextMenuClosed_() {
        const contextButton = this.getContextualEntrypointButton_();
        if (contextButton) {
            contextButton.classList.remove('menu-open');
        }
    }
    onLensSearchClick_() {
        this.searchboxBrowserProxy_.handler.openLensSearch();
    }
}
customElements.define(OmniboxPopupAppElement.is, OmniboxPopupAppElement);
