// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import './icons.html.js';
import '../read_aloud/voice_selection_menu.js';
import '../menus/simple_action_menu.js';
import '../menus/color_menu.js';
import '../menus/font_menu.js';
import '../menus/line_focus_menu.js';
import '../menus/line_spacing_menu.js';
import '../menus/letter_spacing_menu.js';
import '../menus/highlight_menu.js';
import '../menus/rate_menu.js';
import '../menus/presentation_menu.js';
import '../menus/settings_menu.js';
import '//resources/cr_elements/cr_action_menu/cr_action_menu.js';
import '//resources/cr_elements/cr_button/cr_button.js';
import '//resources/cr_elements/cr_icon_button/cr_icon_button.js';
import '//resources/cr_elements/cr_lazy_render/cr_lazy_render_lit.js';
import '//resources/cr_elements/icons.html.js';
import { HelpBubbleMixinLit } from '//resources/cr_components/help_bubble/help_bubble_mixin_lit.js';
import { AnchorAlignment } from '//resources/cr_elements/cr_action_menu/cr_action_menu.js';
import { I18nMixinLit } from '//resources/cr_elements/i18n_mixin_lit.js';
import { WebUiListenerMixinLit } from '//resources/cr_elements/web_ui_listener_mixin_lit.js';
import { assert } from '//resources/js/assert.js';
import { loadTimeData } from '//resources/js/load_time_data.js';
import { CrLitElement } from '//resources/lit/v3_0/lit.rollup.js';
import { DEFAULT_SETTINGS, SettingsOption, ToolbarEvent } from '../content/read_anything_types.js';
import { getCurrentSpeechRate } from '../read_aloud/speech_presentation_rules.js';
import { minOverflowLengthToScroll, openMenu, spinnerDebounceTimeout } from '../shared/common.js';
import { getNewIndex, isArrow, isForwardArrow, isHorizontalArrow } from '../shared/keyboard_util.js';
import { ReadAnythingSettingsChange } from '../shared/metrics_browser_proxy.js';
import { ReadAnythingLogger, SpeechControls, TimeFrom } from '../shared/read_anything_logger.js';
import { getCss } from './read_anything_toolbar.css.js';
import { getHtml } from './read_anything_toolbar.html.js';
export const moreOptionsClass = '.more-options-icon';
// Link toggle button constants.
export const LINKS_ENABLED_ICON = 'read-anything:links-enabled';
export const LINKS_DISABLED_ICON = 'read-anything:links-disabled';
export const LINK_TOGGLE_BUTTON_ID = 'link-toggle-button';
// Images toggle button constants.
export const IMAGES_ENABLED_ICON = 'read-anything:images-enabled';
export const IMAGES_DISABLED_ICON = 'read-anything:images-disabled';
export const IMAGES_TOGGLE_BUTTON_ID = 'images-toggle-button';
// Max number of paragraph elements inside an aria-live region for
// announcing setting changes. Not clearing the element may make
// the announce block too big and waste memory. Trade-off is that every
// MAX_PARAGRAOHS_IN_ANNOUNCE_BLOCK font sizes, there is a chance the
// announcement won't happen the sixth time, if the change is too fast.
// It is unlikely someone will change the font size more than 5 times so
// this covers most use cases.
const MAX_PARAGRAPHS_IN_ANNOUNCE_BLOCK = 5;
// Constants for styling the toolbar when page zoom changes.
const flexWrapTypical = 'nowrap';
const flexWrapOverflow = 'wrap';
const ReadAnythingToolbarElementBase = HelpBubbleMixinLit(WebUiListenerMixinLit(I18nMixinLit(CrLitElement)));
export class ReadAnythingToolbarElement extends ReadAnythingToolbarElementBase {
    static get is() {
        return 'read-anything-toolbar';
    }
    static get styles() {
        return getCss();
    }
    render() {
        return getHtml.bind(this)();
    }
    static get properties() {
        return {
            isSpeechActive: { type: Boolean },
            isAudioCurrentlyPlaying: { type: Boolean },
            isReadAloudPlayable: { type: Boolean },
            selectedVoice: { type: Object },
            availableVoices: { type: Array },
            enabledLangs: { type: Array },
            localeToDisplayName: { type: Object },
            previewVoicePlaying: { type: Object },
            settingsPrefs: { type: Object },
            areFontsLoaded_: { type: Boolean },
            textStyleOptions_: { type: Array },
            textStyleToggles_: { type: Array },
            hideSpinner_: { type: Boolean },
            speechRate_: { type: Number },
            moreOptionsButtons_: { type: Array },
            pageLanguage: { type: String },
            presentationState: { type: Number },
            isImmersiveMode: { type: Boolean },
            isReadAnythingPinned: { type: Boolean },
            isImmersiveEnabled_: { type: Boolean },
            lineFocusStyle: { type: Object },
            lineFocusMovement: { type: Number },
        };
    }
    #presentationState_accessor_storage = 0;
    // Reactive properties below
    get presentationState() { return this.#presentationState_accessor_storage; }
    set presentationState(value) { this.#presentationState_accessor_storage = value; }
    #availableVoices_accessor_storage = [];
    get availableVoices() { return this.#availableVoices_accessor_storage; }
    set availableVoices(value) { this.#availableVoices_accessor_storage = value; }
    #enabledLangs_accessor_storage = [];
    get enabledLangs() { return this.#enabledLangs_accessor_storage; }
    set enabledLangs(value) { this.#enabledLangs_accessor_storage = value; }
    #isSpeechActive_accessor_storage = false;
    // If Read Aloud is playing speech.
    get isSpeechActive() { return this.#isSpeechActive_accessor_storage; }
    set isSpeechActive(value) { this.#isSpeechActive_accessor_storage = value; }
    #isAudioCurrentlyPlaying_accessor_storage = false;
    // If speech is actually playing. Due to latency with the TTS engine, there
    // can be a delay between when the user presses play and speech actually
    // plays.
    get isAudioCurrentlyPlaying() { return this.#isAudioCurrentlyPlaying_accessor_storage; }
    set isAudioCurrentlyPlaying(value) { this.#isAudioCurrentlyPlaying_accessor_storage = value; }
    #isReadAloudPlayable_accessor_storage = false;
    // If Read Aloud is playable. Certain states, such as when Read Anything does
    // not have content or when the speech engine is loading should disable
    // certain toolbar buttons like the play / pause button should be disabled.
    // This is set from the parent element via one way data binding.
    get isReadAloudPlayable() { return this.#isReadAloudPlayable_accessor_storage; }
    set isReadAloudPlayable(value) { this.#isReadAloudPlayable_accessor_storage = value; }
    #isReadAnythingPinned_accessor_storage = false;
    get isReadAnythingPinned() { return this.#isReadAnythingPinned_accessor_storage; }
    set isReadAnythingPinned(value) { this.#isReadAnythingPinned_accessor_storage = value; }
    #localeToDisplayName_accessor_storage = {};
    get localeToDisplayName() { return this.#localeToDisplayName_accessor_storage; }
    set localeToDisplayName(value) { this.#localeToDisplayName_accessor_storage = value; }
    #previewVoicePlaying_accessor_storage = null;
    get previewVoicePlaying() { return this.#previewVoicePlaying_accessor_storage; }
    set previewVoicePlaying(value) { this.#previewVoicePlaying_accessor_storage = value; }
    #settingsPrefs_accessor_storage = DEFAULT_SETTINGS;
    get settingsPrefs() { return this.#settingsPrefs_accessor_storage; }
    set settingsPrefs(value) { this.#settingsPrefs_accessor_storage = value; }
    #selectedVoice_accessor_storage = null;
    get selectedVoice() { return this.#selectedVoice_accessor_storage; }
    set selectedVoice(value) { this.#selectedVoice_accessor_storage = value; }
    #pageLanguage_accessor_storage = '';
    get pageLanguage() { return this.#pageLanguage_accessor_storage; }
    set pageLanguage(value) { this.#pageLanguage_accessor_storage = value; }
    #isImmersiveMode_accessor_storage = false;
    get isImmersiveMode() { return this.#isImmersiveMode_accessor_storage; }
    set isImmersiveMode(value) { this.#isImmersiveMode_accessor_storage = value; }
    #lineFocusStyle_accessor_storage = null;
    get lineFocusStyle() { return this.#lineFocusStyle_accessor_storage; }
    set lineFocusStyle(value) { this.#lineFocusStyle_accessor_storage = value; }
    #lineFocusMovement_accessor_storage = null;
    get lineFocusMovement() { return this.#lineFocusMovement_accessor_storage; }
    set lineFocusMovement(value) { this.#lineFocusMovement_accessor_storage = value; }
    #hideSpinner__accessor_storage = true;
    get hideSpinner_() { return this.#hideSpinner__accessor_storage; }
    set hideSpinner_(value) { this.#hideSpinner__accessor_storage = value; }
    #isImmersiveEnabled__accessor_storage = false;
    get isImmersiveEnabled_() { return this.#isImmersiveEnabled__accessor_storage; }
    set isImmersiveEnabled_(value) { this.#isImmersiveEnabled__accessor_storage = value; }
    #moreOptionsButtons__accessor_storage = [];
    // Overflow buttons on the toolbar that open a menu of options.
    get moreOptionsButtons_() { return this.#moreOptionsButtons__accessor_storage; }
    set moreOptionsButtons_(value) { this.#moreOptionsButtons__accessor_storage = value; }
    #speechRate__accessor_storage = 1;
    get speechRate_() { return this.#speechRate__accessor_storage; }
    set speechRate_(value) { this.#speechRate__accessor_storage = value; }
    #textStyleOptions__accessor_storage = [];
    // Buttons on the toolbar that open a menu of options.
    get textStyleOptions_() { return this.#textStyleOptions__accessor_storage; }
    set textStyleOptions_(value) { this.#textStyleOptions__accessor_storage = value; }
    #textStyleToggles__accessor_storage = [
        {
            id: LINK_TOGGLE_BUTTON_ID,
            icon: chrome.readingMode.linksEnabled ?
                LINKS_ENABLED_ICON : LINKS_DISABLED_ICON,
            title: chrome.readingMode.linksEnabled ?
                loadTimeData.getString('disableLinksLabel') :
                loadTimeData.getString('enableLinksLabel'),
        },
    ];
    get textStyleToggles_() { return this.#textStyleToggles__accessor_storage; }
    set textStyleToggles_(value) { this.#textStyleToggles__accessor_storage = value; }
    #areFontsLoaded__accessor_storage = false;
    get areFontsLoaded_() { return this.#areFontsLoaded__accessor_storage; }
    set areFontsLoaded_(value) { this.#areFontsLoaded__accessor_storage = value; }
    // Member variables below
    startTime_ = Date.now();
    constructorTime_ = 0;
    currentFocusId_ = '';
    windowResizeCallback_ = () => { };
    toolbarContainerBlurCallback_ = () => { };
    // The previous speech active status so we can track when it changes.
    wasSpeechActive_ = false;
    spinnerDebouncerCallbackHandle_;
    logger_ = ReadAnythingLogger.getInstance();
    // Corresponds to UI setup being complete on the toolbar when
    // connectedCallback has finished executing.
    isSetupComplete_ = false;
    isFontSizeDefault_() {
        return chrome.readingMode.fontSize === 2.0;
    }
    isReadingModeInactive() {
        return this.presentationState ===
            chrome.readingMode.inHiddenPresentationState;
    }
    isReadingModeInSidePanel() {
        return this.presentationState ===
            chrome.readingMode.inSidePanelPresentationState;
    }
    constructor() {
        super();
        this.constructorTime_ = Date.now();
        this.logger_.logTimeFrom(TimeFrom.TOOLBAR, this.startTime_, this.constructorTime_);
        this.isImmersiveEnabled_ = chrome.readingMode.isImmersiveEnabled;
        // Only add the button to the toolbar if the feature is enabled.
        if (chrome.readingMode.imagesFeatureEnabled) {
            this.textStyleToggles_.push({
                id: IMAGES_TOGGLE_BUTTON_ID,
                icon: chrome.readingMode.imagesEnabled ? IMAGES_ENABLED_ICON :
                    IMAGES_DISABLED_ICON,
                title: chrome.readingMode.imagesEnabled ?
                    loadTimeData.getString('disableImagesLabel') :
                    loadTimeData.getString('enableImagesLabel'),
            });
        }
    }
    connectedCallback() {
        super.connectedCallback();
        this.windowResizeCallback_ = this.maybeUpdateMoreOptions_.bind(this);
        window.addEventListener('resize', this.windowResizeCallback_);
        if (this.isImmersiveEnabled_) {
            this.toolbarContainerBlurCallback_ =
                this.onToolbarContainerBlur_.bind(this);
            this.$.toolbarContainer.addEventListener('blur', this.toolbarContainerBlurCallback_);
        }
        this.loadFontsStylesheet();
        this.initializeMenuButtons_();
        this.isSetupComplete_ = true;
    }
    disconnectedCallback() {
        if (this.windowResizeCallback_) {
            window.removeEventListener('resize', this.windowResizeCallback_);
        }
        if (this.isImmersiveEnabled_) {
            this.$.toolbarContainer.removeEventListener('blur', this.toolbarContainerBlurCallback_);
        }
        if (this.spinnerDebouncerCallbackHandle_ !== undefined) {
            clearTimeout(this.spinnerDebouncerCallbackHandle_);
        }
        super.disconnectedCallback();
    }
    firstUpdated(_changedProperties) {
        super.firstUpdated(_changedProperties);
        this.registerHelpBubble('kReadAnythingViewModeElementId', '#toolbarContainer');
        this.registerHelpBubble('kReadAnythingSettingsButtonElementId', '#more');
    }
    updated(changedProperties) {
        super.updated(changedProperties);
        if (changedProperties.has('isSpeechActive') ||
            changedProperties.has('isAudioCurrentlyPlaying')) {
            this.onSpeechPlayingStateChanged_();
        }
        if (changedProperties.has('presentationState') &&
            (this.isReadingModeInSidePanel() || this.isReadingModeInactive())) {
            this.$.toolbarContainer.tabIndex = 0;
            const currentFocusedElement = this.$.toolbarContainer.querySelector('[tabindex="0"]');
            if (!currentFocusedElement) {
                const tabIndexElementId = this.isReadAloudPlayable ? '#play-pause' : '#rate';
                const element = this.$.toolbarContainer.querySelector(tabIndexElementId);
                if (element) {
                    element.tabIndex = 0;
                }
            }
        }
    }
    maybeUpdateMoreOptions_() {
        if (this.isImmersiveEnabled_) {
            return;
        }
        // Hide the more options button first to calculate if we need it
        const toolbar = this.$.toolbarContainer;
        const moreOptionsButton = toolbar.querySelector('#more');
        assert(moreOptionsButton, 'more options button doesn\'t exist');
        this.hideElement_(moreOptionsButton, false);
        // Show all the buttons to see if they fit.
        const buttons = Array.from(toolbar.querySelectorAll('.text-style-button'));
        assert(buttons, 'no toolbar buttons');
        buttons.forEach(btn => this.showElement_(btn));
        toolbar.dispatchEvent(new CustomEvent('reset-toolbar', {
            bubbles: true,
            composed: true,
        }));
        if (!toolbar.offsetParent) {
            return;
        }
        // When the toolbar's width exceeds the parent width, then the content has
        // overflowed.
        const parentWidth = toolbar.offsetParent.scrollWidth;
        if (toolbar.scrollWidth > parentWidth) {
            // Hide at least 3 buttons and more if needed.
            let numOverflowButtons = 3;
            let nextOverflowButton = buttons[buttons.length - numOverflowButtons];
            assert(nextOverflowButton);
            // No need to hide a button if it only exceeds the width by a little (i.e.
            // only the padding overflows).
            const maxDiff = 5;
            let overflowLength = nextOverflowButton.offsetLeft +
                nextOverflowButton.offsetWidth - parentWidth;
            while (overflowLength > maxDiff) {
                numOverflowButtons++;
                nextOverflowButton = buttons[buttons.length - numOverflowButtons];
                if (!nextOverflowButton) {
                    break;
                }
                overflowLength = nextOverflowButton.offsetLeft +
                    nextOverflowButton.offsetWidth - parentWidth;
            }
            // Notify the app and toolbar of the overflow.
            toolbar.dispatchEvent(new CustomEvent('toolbar-overflow', {
                bubbles: true,
                composed: true,
                detail: { numOverflowButtons, overflowLength },
            }));
            // If we have too much overflow, we won't use the more options button.
            if (numOverflowButtons > buttons.length) {
                return;
            }
            // Hide the overflowed buttons and show the more options button in front
            // of them.
            this.showElement_(moreOptionsButton);
            const overflowedButtons = buttons.slice(buttons.length - numOverflowButtons);
            overflowedButtons.forEach(btn => this.hideElement_(btn, true));
            toolbar.insertBefore(moreOptionsButton, overflowedButtons[0]);
        }
    }
    hideElement_(element, keepSpace) {
        if (keepSpace) {
            element.classList.add('visibility-hidden');
        }
        else {
            element.classList.add('hidden');
        }
    }
    showElement_(element) {
        element.classList.remove('hidden', 'visibility-hidden');
    }
    initializeMenuButtons_() {
        const fontSizeElement = {
            id: 'font-size',
            icon: 'read-anything:font-size',
            ariaLabel: loadTimeData.getString('fontSizeTitle'),
            openMenu: (target) => openMenu(this.$.fontSizeMenu.get(), target),
            announceId: 'size-announce',
        };
        if (this.isImmersiveEnabled_) {
            this.textStyleOptions_ = [fontSizeElement];
            return;
        }
        this.textStyleOptions_.push(fontSizeElement, {
            id: 'font',
            icon: 'read-anything:font',
            ariaLabel: loadTimeData.getString('fontNameTitle'),
            openMenu: (target) => this.$.fontMenu.open(target),
        }, {
            id: 'color',
            icon: 'read-anything:color',
            ariaLabel: loadTimeData.getString('themeTitle'),
            openMenu: (target) => this.$.colorMenu.open(target),
        }, {
            id: 'line-spacing',
            icon: 'read-anything:line-spacing',
            ariaLabel: loadTimeData.getString('lineSpacingTitle'),
            openMenu: (target) => this.$.lineSpacingMenu.open(target),
        }, {
            id: 'letter-spacing',
            icon: 'read-anything:letter-spacing',
            ariaLabel: loadTimeData.getString('letterSpacingTitle'),
            openMenu: (target) => this.$.letterSpacingMenu.open(target),
        });
        if (chrome.readingMode.isLineFocusEnabled) {
            this.textStyleOptions_.push({
                id: 'line-focus',
                icon: 'read-anything:line-focus',
                ariaLabel: loadTimeData.getString('lineFocusLabel'),
                openMenu: (target) => this.$.lineFocusMenu.open(target),
            });
        }
        this.requestUpdate();
    }
    getHighlightButtonLabel_() {
        return loadTimeData.getString('voiceHighlightLabel');
    }
    getFormattedSpeechRate_() {
        const includeSuffix = this.speechRate_ % 1 === 0;
        return includeSuffix ?
            loadTimeData.getStringF('voiceSpeedOptionTitle', this.speechRate_.toLocaleString()) :
            this.speechRate_.toLocaleString();
    }
    onCloseClick_() {
        chrome.readingMode.close();
    }
    // Loading the fonts stylesheet can take a while, especially with slow
    // Internet connections. Since we don't want this to block the rest of
    // Reading Mode from loading, we load this stylesheet asynchronously
    // in TypeScript instead of in read_anything.html
    loadFontsStylesheet() {
        const link = document.createElement('link');
        link.rel = 'preload';
        link.as = 'style';
        link.href = 'https://fonts.googleapis.com/css?family=';
        link.href += chrome.readingMode.allFonts.join('|');
        link.href = link.href.replace(' ', '+');
        link.addEventListener('load', () => {
            link.media = 'all';
            link.rel = 'stylesheet';
            this.setFontsLoaded();
        }, { once: true });
        document.head.appendChild(link);
    }
    setFontsLoaded() {
        this.areFontsLoaded_ = true;
    }
    onResetToolbar_() {
        this.$.moreOptionsMenu.getIfExists()?.close();
        this.moreOptionsButtons_ = [];
        this.style.setProperty('--toolbar-flex-wrap', flexWrapTypical);
    }
    onToolbarOverflow_(event) {
        const firstHiddenButton = this.textStyleOptions_.length - event.detail.numOverflowButtons;
        // Wrap the buttons if we overflow significantly but aren't yet scrolling
        // the whole app.
        if (firstHiddenButton < 0 &&
            event.detail.overflowLength < minOverflowLengthToScroll) {
            this.style.setProperty('--toolbar-flex-wrap', flexWrapOverflow);
            return;
        }
        // If we only overflow by a little, use the more options button.
        this.moreOptionsButtons_ = this.textStyleOptions_.slice(firstHiddenButton);
    }
    restoreSettingsFromPrefs() {
        this.updateLinkToggleButton();
        this.updateImagesToggleButton();
        this.setFont_(chrome.readingMode.fontName);
        this.speechRate_ = getCurrentSpeechRate();
    }
    playPauseButtonAriaLabel_() {
        return loadTimeData.getString('playAriaLabel');
    }
    playPauseButtonTitle_() {
        return loadTimeData.getString(this.isSpeechActive ? 'pauseTooltip' : 'playTooltip');
    }
    playPauseButtonIronIcon_() {
        return this.isSpeechActive ? 'read-anything-20:pause' :
            'read-anything-20:play';
    }
    onNextGranularityClick_() {
        this.logger_.logSpeechControlClick(SpeechControls.NEXT);
        this.fire(ToolbarEvent.NEXT_GRANULARITY);
    }
    onPreviousGranularityClick_() {
        this.logger_.logSpeechControlClick(SpeechControls.PREVIOUS);
        this.fire(ToolbarEvent.PREVIOUS_GRANULARITY);
    }
    onTextStyleMenuButtonFromOverflowClick_(e) {
        const currentTarget = e.currentTarget;
        const index = Number.parseInt(currentTarget.dataset['index']);
        const menu = this.moreOptionsButtons_[index];
        assert(menu);
        menu.openMenu(currentTarget);
    }
    onTextStyleMenuButtonClick_(e) {
        const currentTarget = e.currentTarget;
        const index = Number.parseInt(currentTarget.dataset['index']);
        const menu = this.textStyleOptions_[index];
        assert(menu);
        menu.openMenu(currentTarget);
    }
    onShowRateMenuClick_(event) {
        this.$.rateMenu.open(event.target);
    }
    onVoiceSelectionMenuClick_(event) {
        const voiceMenu = this.$.toolbarContainer.querySelector('#voiceSelectionMenu');
        assert(voiceMenu, 'no voiceMenu element');
        voiceMenu
            .onVoiceSelectionMenuClick(event.target);
    }
    onMoreOptionsClick_(event) {
        const target = event.target;
        if (this.isImmersiveEnabled_) {
            this.$.settingsMenu.open(target);
            return;
        }
        const menu = this.$.moreOptionsMenu.get();
        openMenu(menu, target);
    }
    onHighlightChange_(event) {
        // Event handler for highlight-change (from highlight-menu).
        const changedHighlight = event.detail.data;
        if (!this.isImmersiveEnabled_) {
            this.setHighlightButtonIcon_(changedHighlight !== chrome.readingMode.noHighlighting);
        }
    }
    onHighlightClick_(event) {
        // Click handler for the highlight button. Used both for the
        // highlight menu mode and the toggle button mode.
        this.$.highlightMenu.open(event.target);
    }
    setHighlightButtonIcon_(turnOn) {
        // Sets the icon of the highlight button. This happens regardless of
        // whether the button toggles highlight on/off (the behavior when the phrase
        // highlighting flag is off), or the button shows the highlight menu (when
        // the flag is on).
        const button = this.$.toolbarContainer.querySelector('#highlight');
        assert(button, 'no highlight button');
        if (turnOn) {
            button.setAttribute('iron-icon', 'read-anything:highlight-on');
        }
        else {
            button.setAttribute('iron-icon', 'read-anything:highlight-off');
        }
    }
    setFont_(font) {
        this.style.fontFamily = chrome.readingMode.getValidatedFontName(font);
    }
    onFontChange_(event) {
        this.setFont_(event.detail.data);
    }
    onRateChange_(event) {
        this.speechRate_ = event.detail.data;
    }
    onFontSizeIncreaseClick_() {
        this.updateFontSize_(true);
    }
    onFontSizeDecreaseClick_() {
        this.updateFontSize_(false);
    }
    onToggleButtonClick_(e) {
        const toggleMenuId = e.currentTarget.id;
        if (toggleMenuId === LINK_TOGGLE_BUTTON_ID) {
            this.onToggleLinksClick_();
        }
        else if (toggleMenuId === IMAGES_TOGGLE_BUTTON_ID) {
            this.onToggleImagesClick_();
        }
    }
    onToggleLinksClick_() {
        this.logger_.logTextSettingsChange(ReadAnythingSettingsChange.LINKS_ENABLED_CHANGE);
        chrome.readingMode.onLinksEnabledToggled();
        this.fire(ToolbarEvent.LINKS);
        this.updateLinkToggleButton();
    }
    onToggleImagesClick_() {
        this.logger_.logTextSettingsChange(ReadAnythingSettingsChange.IMAGES_ENABLED_CHANGE);
        chrome.readingMode.onImagesEnabledToggled();
        this.fire(ToolbarEvent.IMAGES);
        this.updateImagesToggleButton();
    }
    updateLinkToggleButton() {
        const button = this.shadowRoot.getElementById(LINK_TOGGLE_BUTTON_ID);
        if (button) {
            button.ironIcon = chrome.readingMode.linksEnabled ? LINKS_ENABLED_ICON :
                LINKS_DISABLED_ICON;
            const linkStatusLabel = chrome.readingMode.linksEnabled ?
                loadTimeData.getString('disableLinksLabel') :
                loadTimeData.getString('enableLinksLabel');
            button.title = linkStatusLabel;
            button.ariaLabel = linkStatusLabel;
        }
    }
    updateImagesToggleButton() {
        const button = this.shadowRoot?.getElementById(IMAGES_TOGGLE_BUTTON_ID);
        if (button) {
            button.ironIcon = chrome.readingMode.imagesEnabled ? IMAGES_ENABLED_ICON :
                IMAGES_DISABLED_ICON;
            const imageStatusLabel = chrome.readingMode.imagesEnabled ?
                loadTimeData.getString('disableImagesLabel') :
                loadTimeData.getString('enableImagesLabel');
            button.title = imageStatusLabel;
            button.ariaLabel = imageStatusLabel;
        }
    }
    announceSizeChage(increase) {
        const sizeChangeAnnounce = this.shadowRoot?.getElementById('size-announce');
        if (sizeChangeAnnounce) {
            // We must add a new HTML element otherwise aria-live won't catch it.
            const paragraph = document.createElement('p');
            if (increase) {
                paragraph.textContent = this.i18n('increaseFontSizeAnnouncement');
            }
            else {
                paragraph.textContent = this.i18n('decreaseFontSizeAnnouncement');
            }
            sizeChangeAnnounce.appendChild(paragraph);
            // To avoid adding indefinite number of HTML elements. If the list of
            // paragraphs in size_change_announce has become too large reset it.
            if (sizeChangeAnnounce.getElementsByTagName('p').length >
                MAX_PARAGRAPHS_IN_ANNOUNCE_BLOCK) {
                this.restoreAnnounceState('size-announce');
            }
        }
    }
    // Helper function to clear html in an aria announce element.
    restoreAnnounceState(id) {
        const srNotice = this.shadowRoot?.getElementById(id);
        if (srNotice) {
            const paragraphs = srNotice.querySelectorAll('p');
            paragraphs.forEach(paragraph => {
                paragraph.remove();
            });
        }
    }
    updateFontSize_(increase) {
        this.logger_.logTextSettingsChange(ReadAnythingSettingsChange.FONT_SIZE_CHANGE);
        const startingSize = chrome.readingMode.fontSize;
        chrome.readingMode.onFontSizeChanged(increase);
        this.fire(ToolbarEvent.FONT_SIZE);
        if (startingSize !== chrome.readingMode.fontSize) {
            this.announceSizeChage(increase);
        }
        this.requestUpdate();
        // Don't close the menu
    }
    onFontResetClick_() {
        this.logger_.logTextSettingsChange(ReadAnythingSettingsChange.FONT_SIZE_CHANGE);
        chrome.readingMode.onFontSizeReset();
        this.fire(ToolbarEvent.FONT_SIZE);
        this.requestUpdate();
    }
    onPlayPauseClick_() {
        this.logger_.logSpeechControlClick(this.isSpeechActive ? SpeechControls.PAUSE : SpeechControls.PLAY);
        if (this.isSpeechActive) {
            this.logger_.logSpeechStopSource(chrome.readingMode.pauseButtonStopSource);
        }
        this.fire(ToolbarEvent.PLAY_PAUSE);
    }
    onToolbarKeydown_(e) {
        const toolbar = this.$.toolbarContainer;
        const buttons = Array.from(
        // TODO(crbug.com/342411653): Update the loading spinner to be inside a
        // button and update the querySelectorAll to use just '.toolbar-button'.
        toolbar.querySelectorAll('cr-icon-button.toolbar-button, cr-button.toolbar-button'));
        assert(buttons, 'no toolbar buttons');
        // Only allow focus on the currently visible and actionable elements.
        const focusableElements = buttons.filter(el => {
            return (el.clientHeight > 0) && (el.clientWidth > 0) &&
                (el.getBoundingClientRect().right < toolbar.clientWidth) &&
                (el.style.visibility !== 'hidden') && (el.style.display !== 'none') &&
                (!el.disabled) && (el.className !== 'separator');
        });
        // Allow focusing the more options menu if it's visible.
        const moreOptionsButton = this.$.more;
        assert(moreOptionsButton, 'no more options button');
        if (moreOptionsButton.style.display &&
            (moreOptionsButton.style.display !== 'none')) {
            focusableElements.push(moreOptionsButton);
            Array.from(toolbar.querySelectorAll(moreOptionsClass))
                .forEach(element => {
                focusableElements.push(element);
            });
        }
        this.onKeyDown_(e, focusableElements);
    }
    onFontSizeMenuKeydown_(e) {
        // The font size selection menu is laid out horizontally, so users should be
        // able to navigate it using either up and down arrows, or left and right
        // arrows.
        if (!isArrow(e.key)) {
            return;
        }
        e.preventDefault();
        const focusableElements = Array.from(this.$.fontSizeMenu.get().children);
        assert(e.target instanceof HTMLElement);
        const elementToFocus = focusableElements[getNewIndex(e.key, e.target, focusableElements)];
        assert(elementToFocus, 'no element to focus');
        elementToFocus.focus();
    }
    onCloseAllMenus_(event) {
        this.closeAllMenus_(event.detail?.previousId);
    }
    onCloseSubmenuRequested_(event) {
        this.closeSubmenu_(event.detail.previousId);
    }
    onOpenSettingsSubmenu_(event) {
        if (!this.isImmersiveEnabled_) {
            return;
        }
        const { id, previousId, target } = event.detail;
        if (previousId) {
            const previousMenu = this.settingsMenu_[previousId];
            previousMenu?.close();
        }
        const showAtConfig = {
            minY: 0,
            anchorAlignmentY: AnchorAlignment.AFTER_START,
        };
        const currentMenu = this.settingsMenu_[id];
        currentMenu?.open(target, showAtConfig);
    }
    closeAllMenus_(previousId = null) {
        if (!this.isImmersiveEnabled_) {
            return;
        }
        if (previousId) {
            this.closeSubmenu_(previousId);
        }
        this.$.settingsMenu.close();
    }
    closeSubmenu_(submenuId) {
        const previousMenu = this.settingsMenu_[submenuId];
        assert(previousMenu, `settings ${submenuId} submenu not found`);
        previousMenu.close();
    }
    get settingsMenu_() {
        return {
            [SettingsOption.COLOR]: this.$.colorMenu,
            [SettingsOption.VOICE_HIGHLIGHT]: this.$.highlightMenu,
            [SettingsOption.FONT]: this.$.fontMenu,
            [SettingsOption.LETTER_SPACING]: this.$.letterSpacingMenu,
            [SettingsOption.LINE_FOCUS]: this.$.lineFocusMenu,
            [SettingsOption.LINE_SPACING]: this.$.lineSpacingMenu,
            [SettingsOption.VOICE_SELECTION]: this.$.voiceSelectionMenu,
            [SettingsOption.PRESENTATION]: this.$.presentationMenu,
        };
    }
    getMoreOptionsButtons_() {
        return Array.from(this.$.toolbarContainer.querySelectorAll(moreOptionsClass));
    }
    onKeyDown_(e, focusableElements) {
        if (!isHorizontalArrow(e.key)) {
            return;
        }
        e.preventDefault();
        //  Move to the next focusable item in the toolbar, wrapping around
        //  if we've reached the end or beginning.
        assert(e.target instanceof HTMLElement);
        let newIndex = getNewIndex(e.key, e.target, focusableElements);
        const direction = isForwardArrow(e.key) ? 1 : -1;
        // If the next item has overflowed, skip focusing the more options button
        // itself and go directly to the children. We still need this button in the
        // list of focusable elements because it can become focused by tabbing while
        // the menu is open and we want the arrow key behavior to continue smoothly.
        // This is skipped in immersive mode because the more options button opens
        // the main settings menu instead of the overflow menu.
        const elementToFocus = focusableElements[newIndex];
        assert(elementToFocus);
        if ((elementToFocus.id === 'more' && !this.isImmersiveEnabled_) ||
            elementToFocus.classList.contains(moreOptionsClass.slice(1))) {
            const moreOptionsRendered = this.$.moreOptionsMenu.getIfExists();
            // If the more options menu has not been rendered yet, render it and wait
            // for it to be drawn so we can get the number of elements in the menu.
            if (!moreOptionsRendered || !moreOptionsRendered.open) {
                openMenu(this.$.moreOptionsMenu.get(), this.$.more);
                requestAnimationFrame(() => {
                    const moreOptions = this.getMoreOptionsButtons_();
                    focusableElements = focusableElements.concat(moreOptions);
                    newIndex = (direction === 1) ? (newIndex + 1) :
                        (focusableElements.length - 1);
                    this.updateFocus_(focusableElements, newIndex);
                });
                return;
            }
        }
        this.updateFocus_(focusableElements, newIndex);
    }
    resetHideSpinnerDebouncer_() {
        // Use a debouncer to reduce glitches. Even when audio is fast to respond to
        // the play button, there are still milliseconds of delay. To prevent the
        // spinner from quickly appearing and disappearing, we use a debouncer. If
        // either the values of `isSpeechActive` or `isAudioCurrentlyPlaying`
        // change, the previously scheduled callback is canceled and a new callback
        // is scheduled.
        // TODO: crbug.com/339860819 - improve debouncer logic so that the spinner
        // disappears immediately when speech starts playing, or when the pause
        // button is hit.
        if (this.spinnerDebouncerCallbackHandle_ !== undefined) {
            clearTimeout(this.spinnerDebouncerCallbackHandle_);
        }
        this.spinnerDebouncerCallbackHandle_ = setTimeout(() => {
            this.hideSpinner_ = !this.isSpeechActive || this.isAudioCurrentlyPlaying;
            this.spinnerDebouncerCallbackHandle_ = undefined;
        }, spinnerDebounceTimeout);
    }
    onSpeechPlayingStateChanged_() {
        this.resetHideSpinnerDebouncer_();
        // If the previously focused item becomes disabled or disappears from the
        // toolbar because of speech starting or stopping, put the focus on the
        // play/pause button so keyboard navigation continues working.
        // If we're still loading the reading mode panel on
        // a first open, we shouldn't attempt to refocus the play button or the
        // rate menu.
        if (this.isSetupComplete_ && (this.shadowRoot !== null) &&
            (this.shadowRoot.activeElement === null ||
                this.shadowRoot.activeElement.clientHeight === 0)) {
            // If the play / pause button is enabled, we should focus it. Otherwise,
            // we should focus the rate menu.
            const tagToFocus = this.isReadAloudPlayable ? '#play-pause' : '#rate';
            this.$.toolbarContainer.querySelector(tagToFocus)?.focus();
        }
        if (this.isSpeechActive !== this.wasSpeechActive_) {
            this.maybeUpdateMoreOptions_();
            this.wasSpeechActive_ = this.isSpeechActive;
        }
    }
    updateFocus_(focusableElements, newIndex) {
        const elementToFocus = focusableElements[newIndex];
        assert(elementToFocus, 'no element to focus');
        // When the user tabs away from the toolbar and then tabs back, we want to
        // focus the last focused item in the toolbar
        focusableElements.forEach(el => {
            el.tabIndex = -1;
        });
        this.currentFocusId_ = elementToFocus.id;
        // If a more options button is focused and we tab away, we need to tab
        // back to the more options button instead of the item inside the menu since
        // the menu closes when we tab away.
        if (elementToFocus.classList.contains(moreOptionsClass.slice(1))) {
            this.$.more.tabIndex = 0;
        }
        else {
            elementToFocus.tabIndex = 0;
            // Close the overflow menu if the next button is not in the menu.
            if (!this.isImmersiveEnabled_) {
                this.$.moreOptionsMenu.getIfExists()?.close();
            }
        }
        // Wait for the next animation frame for the overflow menu to show or hide.
        requestAnimationFrame(() => {
            elementToFocus.focus();
        });
    }
    getRateTabIndex_() {
        return (!this.isReadAloudPlayable || this.currentFocusId_ === 'rate') ? 0 :
            -1;
    }
    getVoiceSpeedLabel_() {
        return loadTimeData.getStringF('voiceSpeedWithRateLabel', this.speechRate_);
    }
    getAudioState_() {
        if (this.isImmersiveEnabled_) {
            return 'immersive-enabled';
        }
        if (this.isSpeechActive) {
            return 'active';
        }
        return 'inactive';
    }
    getToolbarContainerClass_() {
        return this.isImmersiveEnabled_ ? 'immersive-toolbar-container' :
            'toolbar-container';
    }
    getGranularityContainerClass_() {
        return this.isSpeechActive || this.isImmersiveEnabled_ ?
            'granularity-container-when-active-true' :
            'granularity-container-when-active-false';
    }
    shouldDisableGranularityNavButtons_() {
        return !this.isReadAloudPlayable ||
            (this.isImmersiveEnabled_ && !this.isSpeechActive);
    }
    onToolbarContainerBlur_() {
        if (this.isImmersiveEnabled_) {
            this.$.toolbarContainer.tabIndex = -1;
        }
    }
}
customElements.define(ReadAnythingToolbarElement.is, ReadAnythingToolbarElement);
