// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import { hasEspeakIdentifier, hasNaturalIdentifier } from '../read_aloud/voice_language_conversions.js';
import { MetricsBrowserProxyImpl, ReadAnythingSpeechError, ReadAnythingVoiceType, UmaName } from './metrics_browser_proxy.js';
export var TimeFrom;
(function (TimeFrom) {
    TimeFrom["APP"] = "App";
    TimeFrom["TOOLBAR"] = "Toolbar";
})(TimeFrom || (TimeFrom = {}));
export var SpeechControls;
(function (SpeechControls) {
    SpeechControls["PLAY"] = "Play";
    SpeechControls["PAUSE"] = "Pause";
    SpeechControls["NEXT"] = "NextButton";
    SpeechControls["PREVIOUS"] = "PreviousButton";
    SpeechControls["PLAY_FROM_SELECTION"] = "PlayFromSelection";
    SpeechControls["PLAY_FROM_LINE_FOCUS"] = "PlayFromLineFocus";
})(SpeechControls || (SpeechControls = {}));
export var LinkStatus;
(function (LinkStatus) {
    LinkStatus["SUCCESS"] = "Success";
    LinkStatus["NO_HREF"] = "NoHref";
    LinkStatus["NO_MATCH"] = "NoMatch";
    LinkStatus["TOO_MANY_MATCHES"] = "TooManyMatches";
})(LinkStatus || (LinkStatus = {}));
export var PageType;
(function (PageType) {
    PageType["PDF"] = "PDF";
    PageType["WEB_PAGE"] = "WebPage";
})(PageType || (PageType = {}));
export var ViewMode;
(function (ViewMode) {
    ViewMode["FULL_PAGE"] = "FullPage";
    ViewMode["SIDE_PANEL"] = "SidePanel";
})(ViewMode || (ViewMode = {}));
// Handles the business logic for logging.
export class ReadAnythingLogger {
    metrics = MetricsBrowserProxyImpl.getInstance();
    // When this class is first instantiated, it will be because reading mode
    // is visible, so isHidden_ should be false be default.
    isHidden_ = false;
    setHidden(hidden) {
        this.isHidden_ = hidden;
    }
    logEmptyState() {
        // Don't log the empty state if the UI is hidden;
        if (this.isHidden_) {
            return;
        }
        this.metrics.recordEmptyState();
    }
    logSpeechStopSource(source) {
        this.metrics.recordSpeechStopSource(source);
    }
    logSpeechError(errorCode) {
        let error;
        switch (errorCode) {
            case 'text-too-long':
                error = ReadAnythingSpeechError.TEXT_TOO_LONG;
                break;
            case 'voice-unavailable':
                error = ReadAnythingSpeechError.VOICE_UNAVAILABE;
                break;
            case 'language-unavailable':
                error = ReadAnythingSpeechError.LANGUAGE_UNAVAILABLE;
                break;
            case 'invalid-argument':
                error = ReadAnythingSpeechError.INVALID_ARGUMENT;
                break;
            case 'synthesis-failed':
                error = ReadAnythingSpeechError.SYNTHESIS_FAILED;
                break;
            case 'synthesis-unavailable':
                error = ReadAnythingSpeechError.SYNTHESIS_UNVAILABLE;
                break;
            case 'audio-busy':
                error = ReadAnythingSpeechError.AUDIO_BUSY;
                break;
            case 'audio-hardware':
                error = ReadAnythingSpeechError.AUDIO_HARDWARE;
                break;
            case 'network':
                error = ReadAnythingSpeechError.NETWORK;
                break;
            case 'timeout-engine-stalled':
                error = ReadAnythingSpeechError.TIMEOUT_ENGINE_STALLED;
                break;
            case 'timeout-stalled-after-recovery':
                error = ReadAnythingSpeechError.TIMEOUT_STALLED_AFTER_ENGINE_RECOVERY;
                break;
            default:
                return;
        }
        // There are more error code possibilities, but right now, we only care
        // about tracking the above error codes.
        this.metrics.recordSpeechError(error);
    }
    logTimeFrom(from, startTime, endTime) {
        const umaName = `Accessibility.ReadAnything.TimeFrom${from}StartedToConstructor`;
        this.metrics.recordTime(umaName, endTime - startTime);
    }
    logNewPage(speechPlayed) {
        // Don't log the new page if the UI is hidden.
        if (this.isHidden_) {
            return;
        }
        speechPlayed ? this.metrics.recordNewPageWithSpeech() :
            this.metrics.recordNewPage();
    }
    logHighlightGranularity(highlight) {
        this.metrics.recordHighlightGranularity(highlight);
    }
    logVoiceLanguageChange(currentVoice, newVoice) {
        if (currentVoice && newVoice &&
            (currentVoice.lang.toLowerCase() !== newVoice.lang.toLowerCase())) {
            this.metrics.recordVoiceLanguageChange();
        }
    }
    logVoiceTypeUsedForReading_(voice) {
        if (!voice) {
            return;
        }
        let voiceType;
        if (hasNaturalIdentifier(voice)) {
            voiceType = ReadAnythingVoiceType.NATURAL;
        }
        else if (hasEspeakIdentifier(voice)) {
            voiceType = ReadAnythingVoiceType.ESPEAK;
        }
        else {
            // 
            // 
            voiceType = ReadAnythingVoiceType.SYSTEM;
            // When a system voice is used, log additional information to better
            // understand the TTS engine state when the system voice is used.
            // Extension state information cannot easily be passed to the renderer,
            // so this logging needs to be handled within the page handler.
            this.metrics.recordExtensionState();
            // 
        }
        this.metrics.recordVoiceType(voiceType);
    }
    logLanguageUsedForReading_(lang) {
        if (!lang) {
            return;
        }
        // See tools/metrics/histograms/enums.xml enum LocaleCodeBCP47. The enum
        // there doesn't always have locales where the base lang and the locale
        // are the same (e.g. they don't have id-id, but do have id). So if the
        // base lang and the locale are the same, just use the base lang.
        let langToLog = lang;
        const langSplit = lang.toLowerCase().split('-');
        if (langSplit.length === 2 && langSplit[0] === langSplit[1]) {
            langToLog = langSplit[0];
        }
        this.metrics.recordLanguage(langToLog);
    }
    logTextSettingsChange(settingsChange) {
        this.metrics.recordTextSettingsChange(settingsChange);
    }
    logSpeechSettingsChange(settingsChange) {
        this.metrics.recordSpeechSettingsChange(settingsChange);
    }
    logVoiceSpeed(index) {
        this.metrics.recordVoiceSpeed(index);
    }
    logSpeechPlaySession(startTime, voice) {
        this.logVoiceTypeUsedForReading_(voice);
        this.logLanguageUsedForReading_(voice?.lang);
        const playbackTime = Date.now() - startTime;
        this.metrics.recordSpeechPlaybackLengthLegacy(playbackTime);
        if (!chrome.readingMode.isImmersiveEnabled) {
            return;
        }
        const activePresentationState = chrome.readingMode.activePresentationState;
        const isImmersiveState = activePresentationState ===
            chrome.readingMode.inImmersiveOverlayPresentationState;
        if (!isImmersiveState &&
            activePresentationState !==
                chrome.readingMode.inSidePanelPresentationState) {
            return;
        }
        const pageType = chrome.readingMode.isPdf ? PageType.PDF : PageType.WEB_PAGE;
        const viewMode = isImmersiveState ? ViewMode.FULL_PAGE : ViewMode.SIDE_PANEL;
        const umaName = `${UmaName.SPEECH_PLAYBACK}.${pageType}In${viewMode}`;
        this.metrics.recordSpeechPlaybackLength(umaName, playbackTime);
    }
    logSpeechControlClick(control) {
        this.metrics.incrementMetricCount(`Accessibility.ReadAnything.ReadAloud${control}SessionCount`);
    }
    logLineFocusSession() {
        if (chrome.readingMode.isLineFocusEnabled) {
            this.metrics.recordLineFocusSession();
        }
    }
    logLineFocusToggled(enabled) {
        if (chrome.readingMode.isLineFocusEnabled) {
            this.metrics.recordLineFocusToggled(enabled);
        }
    }
    logLinkStatusCount(status, count) {
        const umaName = `Accessibility.ReadAnything.Readability.PageLinks${status}Count`;
        this.metrics.recordCount(umaName, count);
    }
    logDistilledPageStructure(wordCountContainer) {
        if (this.isHidden_) {
            return;
        }
        const paragraphs = wordCountContainer.querySelectorAll('p');
        const headerCounts = ReadAnythingLogger.getHeaderCounts(wordCountContainer);
        this.logOverallStructureMetrics_(headerCounts, paragraphs.length);
        this.logTopTwoHeaderMetrics_(headerCounts);
        if (chrome.readingMode.isPdf) {
            this.logPdfDistilledPageStructure_(headerCounts, paragraphs.length);
        }
    }
    logOverallStructureMetrics_(headerCounts, paragraphCount) {
        // The total number of distilled headers.
        const totalHeaderCount = headerCounts.reduce((sum, item) => sum + item.count, 0);
        this.metrics.recordCount(UmaName.TOTAL_HEADER_COUNT, totalHeaderCount);
        // The number of unique header tags present.
        const uniqueHeaderTags = headerCounts.filter(item => item.count > 0).length;
        this.metrics.recordCount(UmaName.UNIQUE_HEADER_TAGS, uniqueHeaderTags);
        // Log the number of paragraphs.
        this.metrics.recordCount(UmaName.NUMBER_PARAGRAPHS, paragraphCount);
        if (paragraphCount > 0) {
            // The ratio of headings to paragraphs, scaled by 100.
            const headingToParagraphRatio = Math.round((totalHeaderCount / paragraphCount) * 100);
            this.metrics.recordCount(UmaName.HEADING_TO_PARAGRAPH_RATIO, headingToParagraphRatio);
        }
    }
    // The "top two" headers in a hierarchy represent the first two non-zero
    // header tags present in the hierarchy. e.g. if there are 5 h1 tags,
    // 4 h2 tags, and 20 h3 tags, the top two header count would be h1 + h2 = 9.
    // There's an exception for h1 tags. Since the h1 tag is often used for
    // the title, it is excluded from being counted as one of the top two headers
    // if there is exactly one h1 tag present, as that means it's likely being
    // used as a title. headerCounts is assumed to be already sorted in order of hierarchy
    // based on how it was originally created from getHeaderCounts.
    logTopTwoHeaderMetrics_(headerCounts) {
        let hierarchy = [...headerCounts];
        const h1Count = headerCounts.find(item => item.tag === 'h1')?.count ?? 0;
        if (h1Count === 1) {
            hierarchy = hierarchy.filter(item => item.tag !== 'h1');
        }
        const presentHeaders = hierarchy.filter(item => item.count > 0);
        let topTwoHeadersCount = 0;
        if (presentHeaders.length > 0) {
            topTwoHeadersCount += presentHeaders[0].count;
        }
        if (presentHeaders.length > 1) {
            topTwoHeadersCount += presentHeaders[1].count;
        }
        this.metrics.recordCount(UmaName.TOP_TWO_HEADERS_COUNT, topTwoHeadersCount);
        let topTwoHeadersHaveMinimumTwoItems = false;
        if (presentHeaders.length >= 2) {
            topTwoHeadersHaveMinimumTwoItems =
                presentHeaders[0].count >= 2 && presentHeaders[1].count >= 2;
        }
        this.metrics.recordBoolean(UmaName.TOP_TWO_HEADERS_HAVE_MINIMUM_TWO_ITEMS, topTwoHeadersHaveMinimumTwoItems);
        if (presentHeaders.length >= 2 && presentHeaders[1].count > 0) {
            // The ratio of the top level to the second top level header, scaled by
            // 100.
            const topTwoHeadingRatio = Math.round((presentHeaders[0].count / presentHeaders[1].count) * 100);
            this.metrics.recordCount(UmaName.TOP_TWO_HEADING_RATIO, topTwoHeadingRatio);
        }
    }
    logPdfDistilledPageStructure_(headerCounts, paragraphCount) {
        for (const header of headerCounts) {
            const headingLevel = header.tag.toUpperCase();
            this.metrics.recordCount(`Accessibility.ReadAnything.Pdf.Headings.${headingLevel}`, header.count);
        }
        this.metrics.recordCount(UmaName.PDF_NUMBER_PARAGRAPHS, paragraphCount);
        if (paragraphCount > 0) {
            const totalHeaderCount = headerCounts.reduce((sum, item) => sum + item.count, 0);
            const headingToParagraphRatio = Math.round((totalHeaderCount / paragraphCount) * 100);
            this.metrics.recordCount(UmaName.PDF_HEADING_TO_PARAGRAPH_RATIO, headingToParagraphRatio);
        }
    }
    static getHeaderCounts(wordCountContainer) {
        return [
            { tag: 'h1', count: wordCountContainer.querySelectorAll('h1').length },
            { tag: 'h2', count: wordCountContainer.querySelectorAll('h2').length },
            { tag: 'h3', count: wordCountContainer.querySelectorAll('h3').length },
            { tag: 'h4', count: wordCountContainer.querySelectorAll('h4').length },
            { tag: 'h5', count: wordCountContainer.querySelectorAll('h5').length },
            { tag: 'h6', count: wordCountContainer.querySelectorAll('h6').length },
        ];
    }
    static getInstance() {
        return instance || (instance = new ReadAnythingLogger());
    }
    static setInstance(obj) {
        instance = obj;
    }
}
let instance = null;
