/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.perf

import android.os.Process
import android.os.SystemClock
import java.io.FileNotFoundException
import kotlin.math.roundToLong
import org.mozilla.fenix.GleanMetrics.StartupTimeline as Telemetry

/**
 * A class to measure the time the Android framework executes before letting us execute our own code
 * for the first time in Application's initializer: this value is captured in the
 * [Telemetry.frameworkStart] probe.
 *
 * Since we cannot execute code at process start, this measurement does not fit within the Glean
 * Timespan metric start/stop programming model so it lives in its own class.
 */
internal class StartupFrameworkStartMeasurement(
    private val stat: Stat = Stat(),
    private val telemetry: Telemetry = Telemetry,
    private val getElapsedRealtimeNanos: () -> Long = SystemClock::elapsedRealtimeNanos
) {

    private var isMetricSet = false

    private var applicationInitNanos = -1L
    private var isApplicationInitCalled = false

    fun onApplicationInit() {
        // This gets called from multiple processes: don't do anything expensive. See call site for details.
        //
        // In the main process, there are multiple Application impl so we ensure it's only set by
        // the first one.
        if (!isApplicationInitCalled) {
            isApplicationInitCalled = true
            applicationInitNanos = getElapsedRealtimeNanos()
        }
    }

    /**
     * Sets the values for metrics to record in glean.
     *
     * We defer these metrics, rather than setting them as soon as the values are available,
     * because they are slow to fetch and we don't want to impact startup.
     */
    fun setExpensiveMetric() {
        // The application is only init once per process lifetime so we only set this value once.
        if (isMetricSet) return
        isMetricSet = true

        if (applicationInitNanos < 0) {
            telemetry.frameworkStartError.set(true)
        } else {
            try {
                telemetry.frameworkStart.setRawNanos(getFrameworkStartNanos())
            } catch (e: FileNotFoundException) {
                // Privacy managers can add hooks that block access to reading system /proc files.
                // We want to catch these exception and report an error on accessing the file
                // rather than an implementation error.
                telemetry.frameworkStartReadError.set(true)
            }

            // frameworkStart is derived from the number of clock ticks per second. To ensure this
            // value does not throw off our result, we capture it too.
            telemetry.clockTicksPerSecond.add(stat.clockTicksPerSecond.toInt())
        }
    }
    /**
     * @throws [java.io.FileNotFoundException]
     */
    private fun getFrameworkStartNanos(): Long {
        // Get our timestamps in ticks: we expect ticks to be less granular than nanoseconds so,
        // to ensure our measurement uses the correct number of significant figures, we convert
        // everything to ticks before getting the result.
        //
        // Similarly, we round app init to a whole integer tick value because process start only
        // comes in integer ticks values.
        val processStartTicks = stat.getProcessStartTimeTicks(Process.myPid())
        val applicationInitTicks = applicationInitNanos.nanosToTicks().roundToLong()

        val frameworkStartTicks = applicationInitTicks - processStartTicks

        // Glean only takes whole unit nanoseconds so we round to that. I'm not sure but it may be
        // possible that capturing nanos in a double will produce a rounding error that chops off
        // significant values. However, since we expect to be using a much smaller portion of the
        // nano field - if ticks are actually less granular than nanoseconds - I don't expect for
        // this to be a problem.
        return frameworkStartTicks.ticksToNanos().roundToLong()
    }

    private fun Long.nanosToTicks(): Double = stat.convertNanosToTicks(this)
    private fun Long.ticksToNanos(): Double = stat.convertTicksToNanos(this)
}
