# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import asyncio
import os
import tempfile
from contextlib import ExitStack

from .. import trace

__COLD_START_HISTOGRAM_NAME = "Startup.Android.Cold.TimeToFirstVisibleContent4"


async def measure_cold_start(app, url, trace_file=None, trace_config=None):
    """Measures Chrome's cold start time by checking the value recorded into
    `Startup.Android.Cold.TimeToFirstVisibleContent4`

    See `tools/metrics/histograms/metadata/startup/histograms.xml` for more
    details about when the metric is recorded.

    This function ensures the app is stopped, performs a cold start, while
    recording a trace and then queries the trace for the histogram value.

    Args:
        app: The chrome.App object with which to start and stop chrome
        url: The URL to launch the Chrome with.
        trace_file: An optional `trace.TraceFile` object to store the trace.
                    If not provided, a temporary file will be created.
        trace_config: An optional path to a Perfetto trace config file.
                      If not provided, a default config is used.

    Returns:
        The cold startup time in milliseconds.
    """
    with ExitStack() as stack:
        if trace_config is None:
            trace_config = stack.enter_context(
                trace.histograms_trace_config(__COLD_START_HISTOGRAM_NAME)
            )

        if not os.path.exists(trace_config):
            raise FileNotFoundError(f"Trace config not found at {trace_config}")

        # If no trace file is provided, create a temporary one that will be
        # cleaned up upon exiting the context.
        if trace_file is None:
            temporary_recorded_trace = stack.enter_context(
                tempfile.NamedTemporaryFile(mode='w')
            )
            trace_file = trace.TraceFile(temporary_recorded_trace.name)

        return await _measure_cold_start(app, url, trace_file, trace_config)


async def _measure_cold_start(app, url, trace_file, trace_config):
    return await _measure_startup(
        app,
        url,
        trace_file,
        trace_config,
        trace.histogram_values_query(__COLD_START_HISTOGRAM_NAME),
        "Could not find histogram sample for "
        + __COLD_START_HISTOGRAM_NAME
        + " in the trace.",
    )


async def _measure_first_frame(app, url, trace_file, trace_config):
    return await _measure_startup(
        app,
        url,
        trace_file,
        trace_config,
        _FIRST_FRAME_TIME_QUERY,
        "Could not find logcat message for "
        "'Displayed com.google.android.apps.chrome/"
        "org.chromium.chrome.browser.ChromeTabbedActivity' in the trace.",
    )


async def _measure_startup(
    app, url, trace_file, trace_config, query, error_message
):
    # Stop the app before recording the trace so that the trace cleanly shows
    # the cold start
    await app.stop()

    async with trace_file.record(trace_config):
        # Allow some time for Perfetto to start recording the trace before the
        # app starts.
        await asyncio.sleep(2)

        await app.start(url=url)
        await asyncio.sleep(5)  # Startup should not take more than 5 seconds
        await app.stop()

    df = await trace_file.query(query)
    try:
        return int(df.iloc[0, 0])
    except IndexError:
        raise LookupError(error_message)


async def measure_first_frame(app, url, trace_file=None, trace_config=None):
    """Measures Chrome's time to first frame.

    This function ensures the app is stopped, performs a cold start, while
    recording a trace and then queries the trace for the first frame time.
    The display time is reported by android in logcat.

    Args:
        app: The chrome.App object with which to start and stop chrome
        url: The URL to launch the Chrome with.
        trace_file: An optional `trace.TraceFile` object to store the trace.
                    If not provided, a temporary file will be created.
        trace_config: An optional path to a Perfetto trace config file.
                      If not provided, a default config is used.

    Returns:
        The time to first frame in milliseconds.
    """
    if trace_config is None:
        # This assumes the current working directory is the chromium src root.
        trace_config = os.path.join(
            os.getcwd(),
            "tools",
            "android",
            "colabutils",
            "res",
            "cold_start_trace_with_logcat_timing_cfg.pbtxt",
        )

    if not os.path.exists(trace_config):
        raise FileNotFoundError(f"Trace config not found at {trace_config}")

    with ExitStack() as stack:
        # If no trace file is provided, create a temporary one that will be
        # cleaned up upon exiting the context.
        if trace_file is None:
            temporary_recorded_trace = stack.enter_context(
                tempfile.NamedTemporaryFile(mode='w')
            )
            trace_file = trace.TraceFile(temporary_recorded_trace.name)

        return await _measure_first_frame(app, url, trace_file, trace_config)


_FIRST_FRAME_TIME_QUERY = r"""
SELECT
  CAST(
    REGEXP_EXTRACT(
      msg,
      'Displayed com.google.android.apps.chrome/org.chromium.chrome.browser.ChromeTabbedActivity[^+]+\+(.+)ms'
    ) AS INT
  ) AS launch_time
FROM
  android_logs
WHERE
  msg LIKE 'Displayed %'
"""
