// 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 '/strings.m.js';
import 'chrome://resources/cr_elements/cr_tab_box/cr_tab_box.js';
import './field_trials.js';
import './private_metrics.js';
import { assert } from 'chrome://resources/js/assert.js';
import { addWebUiListener } from 'chrome://resources/js/cr.js';
import { CustomElement } from 'chrome://resources/js/custom_element.js';
import { loadTimeData } from 'chrome://resources/js/load_time_data.js';
import { getTemplate } from './app.html.js';
import { MetricsInternalsBrowserProxyImpl } from './browser_proxy.js';
import { getEventsPeekString, logEventToString, sizeToString, timestampToString, umaLogTypeToString } from './log_utils.js';
/**
 * An empty log. It is appended to a logs table when there are no logs (for
 * purely aesthetic reasons).
 */
const EMPTY_LOG = {
    type: 'N/A',
    hash: 'N/A',
    timestamp: '',
    size: -1,
    events: [],
};
/**
 * The maximum length of a value in the seed info table. Values longer than
 * this will be truncated with an ellipsis.
 */
const VALUE_LENGTH_LIMIT = 64;
export class MetricsInternalsAppElement extends CustomElement {
    static get is() {
        return 'metrics-internals-app';
    }
    static get template() {
        return getTemplate();
    }
    /**
     * Resolves once the component has finished loading.
     */
    initPromise;
    browserProxy_ = MetricsInternalsBrowserProxyImpl.getInstance();
    /**
     * Previous summary tables data. Used to prevent re-renderings of the tables
     * when the data has not changed.
     */
    previousVariationsSummaryData_ = '';
    previousUmaSummaryData_ = '';
    constructor() {
        super();
        this.initPromise = this.init_();
    }
    /**
     * Returns UMA logs data (with their proto) as a JSON string. Used when
     * exporting UMA logs data. Returns a promise.
     */
    getUmaLogsExportContent() {
        return this.browserProxy_.getUmaLogData(/*includeLogProtoData*/ true);
    }
    async init_() {
        this.syncTabsWithUrlHash_();
        // Fetch variations summary data and set up a recurring timer.
        await this.updateVariationsSummary_();
        setInterval(() => this.updateVariationsSummary_(), 3000);
        // Fetch stored latest and safe seeds info and set up a listener for the
        // buttons for refreshing the data.
        await this.updateStoredSeedInfo_('Latest');
        const fetchStoredLatestSeedInfoButton = this.getRequiredElement('#fetch-stored-latest-seed-info');
        assert(fetchStoredLatestSeedInfoButton);
        fetchStoredLatestSeedInfoButton.addEventListener('click', () => this.updateStoredSeedInfo_('Latest'));
        await this.updateStoredSeedInfo_('Safe');
        const fetchStoredSafeSeedInfoButton = this.getRequiredElement('#fetch-stored-safe-seed-info');
        assert(fetchStoredSafeSeedInfoButton);
        fetchStoredSafeSeedInfoButton.addEventListener('click', () => this.updateStoredSeedInfo_('Safe'));
        // Fetch UMA summary data and set up a recurring timer.
        await this.updateUmaSummary_();
        setInterval(() => this.updateUmaSummary_(), 3000);
        // Set up the UMA table caption.
        const umaTableCaption = this.getRequiredElement('#uma-table-caption');
        const isUsingMetricsServiceObserver = await this.browserProxy_.isUsingMetricsServiceObserver();
        let firstPartOfCaption = isUsingMetricsServiceObserver ?
            'List of all UMA logs closed since browser startup.' :
            'List of UMA logs closed since opening this page. Starting the browser \
        with the --export-uma-logs-to-file command line flag will instead show \
        all logs closed since browser startup.';
        firstPartOfCaption += ' See ';
        const linkInCaptionNode = document.createElement('a');
        linkInCaptionNode.appendChild(document.createTextNode('documentation'));
        linkInCaptionNode.href =
            'https://chromium.googlesource.com/chromium/src/components/metrics/+/HEAD/debug/README.md';
        // Don't clobber the current page.  The current page (in release builds)
        // shows only the logs since the page was opened.  We don't want to allow
        // the current page to be navigated away from lest useful logs be lost.
        linkInCaptionNode.target = '_blank';
        const secondPartOfCaption = ' for more information about this debug page and tools for working \
         with the exported logs.';
        umaTableCaption.appendChild(document.createTextNode(firstPartOfCaption));
        umaTableCaption.appendChild(linkInCaptionNode);
        umaTableCaption.appendChild(document.createTextNode(secondPartOfCaption));
        // Set up a listener for UMA logs. Also update UMA log data immediately in
        // case there are logs that we already have data on.
        addWebUiListener('uma-log-created-or-event', () => this.updateUmaLogsData_());
        await this.updateUmaLogsData_();
        // Set up the UMA "Export logs" button.
        const exportUmaLogsButton = this.getRequiredElement('#export-uma-logs');
        exportUmaLogsButton.addEventListener('click', () => this.exportUmaLogs_());
        if (!loadTimeData.getBoolean('enablePrivateMetricsTab')) {
            this.getRequiredElement('#private-metrics-tab').style.display = 'none';
            this.getRequiredElement('#private-metrics-panel').style.display = 'none';
        }
    }
    /**
     * Synchronize the selected tab and the URL hash. Allows, for example,
     * chrome://metrics-internals#variations to directly open the variations tab.
     */
    syncTabsWithUrlHash_() {
        const tabUrlHashes = [
            '#uma',
            '#variations',
            '#field-trials',
            '#private-metrics',
        ];
        const tabBox = this.shadowRoot.querySelector('cr-tab-box');
        tabBox.addEventListener('selected-index-change', (e) => {
            window.location.hash = tabUrlHashes[e.detail] || '';
        });
        if (window.location.hash.startsWith('#')) {
            const entryIndex = tabUrlHashes.indexOf(window.location.hash);
            if (entryIndex >= 0) {
                tabBox.setAttribute('selected-index', String(entryIndex));
            }
        }
    }
    /**
     * Callback function to expand/collapse an element on click.
     * @param e The click event.
     */
    toggleEventsExpand_(e) {
        let umaLogEventsDiv = e.target;
        // It is possible we have clicked a descendant. Keep checking the parent
        // until we are the the root div of the events.
        while (!umaLogEventsDiv.classList.contains('uma-log-events')) {
            umaLogEventsDiv = umaLogEventsDiv.parentElement;
        }
        umaLogEventsDiv.classList.toggle('uma-log-events-expanded');
    }
    /**
     * Fills the passed table element with the given summary.
     */
    updateSummaryTable_(tableBody, summary) {
        // Clear the table first.
        tableBody.replaceChildren();
        const template = this.getRequiredElement('#summary-row-template');
        for (const info of summary) {
            const row = template.content.cloneNode(true);
            const [key, value] = row.querySelectorAll('td');
            assert(key);
            key.textContent = info.key;
            assert(value);
            value.textContent = info.value;
            tableBody.appendChild(row);
        }
    }
    /**
     * Fills the passed table element with the given seed info.
     */
    updateSeedInfoTable_(tableBody, summary) {
        // Clear the table first.
        tableBody.replaceChildren();
        const template = this.getRequiredElement('#seed-info-row-template');
        for (const info of summary) {
            const row = template.content.cloneNode(true);
            const [key, value, copy] = row.querySelectorAll('td');
            assert(key);
            key.textContent = info.key;
            assert(value);
            if (info.value.length > VALUE_LENGTH_LIMIT) {
                value.textContent = info.value.substring(0, VALUE_LENGTH_LIMIT) + '...';
            }
            else {
                value.textContent = info.value;
            }
            assert(copy);
            const copyButton = copy.querySelector('button');
            assert(copyButton);
            copyButton.addEventListener('click', () => {
                navigator.clipboard.writeText(info.value);
            });
            tableBody.appendChild(row);
        }
    }
    /**
     * Fetches variations summary data and updates the view.
     */
    async updateVariationsSummary_() {
        const summary = await this.browserProxy_.fetchVariationsSummary();
        // Don't re-render the table if the data has not changed.
        const newDataString = summary.toString();
        if (newDataString === this.previousVariationsSummaryData_) {
            return;
        }
        this.previousVariationsSummaryData_ = newDataString;
        const variationsSummaryTableBody = this.getRequiredElement('#variations-summary-body');
        this.updateSummaryTable_(variationsSummaryTableBody, summary);
    }
    /**
     * Fetches stored seed info and updates the view for the given seed type.
     */
    async updateStoredSeedInfo_(seedType) {
        const seedTypeKey = seedType.toLowerCase();
        const refreshButton = this.getRequiredElement(`#fetch-stored-${seedTypeKey}-seed-info`);
        assert(refreshButton);
        refreshButton.setAttribute('disabled', '');
        const storedSeedInfo = await this.browserProxy_.fetchStoredSeedInfo(seedType);
        const storedSeedInfoTableBody = this.getRequiredElement(`#stored-${seedTypeKey}-seed-info-body`);
        this.updateSeedInfoTable_(storedSeedInfoTableBody, storedSeedInfo);
        refreshButton.removeAttribute('disabled');
    }
    /**
     * Fetches UMA summary data and updates the view.
     */
    async updateUmaSummary_() {
        const summary = await this.browserProxy_.fetchUmaSummary();
        const umaSummaryTableBody = this.$('#uma-summary-body');
        // Don't re-render the table if the data has not changed.
        const newDataString = summary.toString();
        if (newDataString === this.previousUmaSummaryData_) {
            return;
        }
        this.previousUmaSummaryData_ = newDataString;
        this.updateSummaryTable_(umaSummaryTableBody, summary);
    }
    /**
     * Fills the passed table element with the given logs.
     */
    updateLogsTable_(tableBody, logs) {
        // Clear the table first.
        tableBody.replaceChildren();
        const template = this.getRequiredElement('#uma-log-row-template');
        // Iterate through the logs in reverse order so that the most recent log
        // shows up first.
        for (const log of logs.slice(0).reverse()) {
            const row = template.content.cloneNode(true);
            const [type, hash, timestamp, size, events] = row.querySelectorAll('td');
            assert(type);
            type.textContent = umaLogTypeToString(log.type);
            assert(hash);
            hash.textContent = log.hash;
            assert(timestamp);
            timestamp.textContent = timestampToString(log.timestamp);
            assert(size);
            size.textContent = sizeToString(log.size);
            assert(events);
            const eventsPeekDiv = events.querySelector('.uma-log-events-peek');
            assert(eventsPeekDiv);
            eventsPeekDiv.addEventListener('click', this.toggleEventsExpand_, false);
            const eventsPeekText = events.querySelector('.uma-log-events-peek-text');
            assert(eventsPeekText);
            eventsPeekText.textContent = getEventsPeekString(log.events);
            const eventsText = events.querySelector('.uma-log-events-text');
            assert(eventsText);
            // Iterate through the events in reverse order so that the most recent
            // event shows up first.
            for (const event of log.events.slice(0).reverse()) {
                const div = document.createElement('div');
                div.textContent = logEventToString(event);
                eventsText.appendChild(div);
            }
            tableBody.appendChild(row);
        }
    }
    /**
     * Fetches the latest UMA logs and renders them. This is called when the page
     * is loaded and whenever there is a log that created or changed.
     */
    async updateUmaLogsData_() {
        const logsData = await this.browserProxy_.getUmaLogData(/*includeLogProtoData=*/ false);
        const logs = JSON.parse(logsData);
        // If there are no logs, append an empty log. This is purely for aesthetic
        // reasons. Otherwise, the table may look confusing.
        if (!logs.logs.length) {
            logs.logs = [EMPTY_LOG];
        }
        // We don't compare the new data with the old data to prevent re-renderings
        // because this should only be called when there is an actual change.
        const umaLogsTableBody = this.getRequiredElement('#uma-logs-body');
        this.updateLogsTable_(umaLogsTableBody, logs.logs);
    }
    /**
     * Exports the accumulated UMA logs, including their proto data, as a JSON
     * file. This will initiate a download.
     */
    async exportUmaLogs_() {
        const logsData = await this.getUmaLogsExportContent();
        const file = new Blob([logsData], { type: 'text/plain' });
        const a = document.createElement('a');
        a.href = URL.createObjectURL(file);
        a.download = `uma_logs_${new Date().getTime()}.json`;
        a.click();
    }
}
customElements.define(MetricsInternalsAppElement.is, MetricsInternalsAppElement);
