/* 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.components.metrics

import android.content.Intent
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.core.view.doOnPreDraw
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner
import mozilla.components.support.utils.SafeIntent
import org.mozilla.fenix.components.metrics.Event.AppAllStartup
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.APP_ICON
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.CUSTOM_TAB
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.LINK
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.UNKNOWN
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.ERROR
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.HOT
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.COLD
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.WARM
import org.mozilla.fenix.perf.runBlockingIncrement
import java.lang.reflect.Modifier.PRIVATE

/**
 * Tracks application startup source, type, launch time, and whether or not activity has
 * savedInstance to restore the activity from.
 * Sample = [source = COLD, type = APP_ICON, hasSavedInstanceState = false,launchTimeNanoSeconds = 1824000000]
 * The basic idea is to collect these metrics from different phases of startup through
 * [AppAllStartup] and finally report them on Activity's onResume() function.
 */
@Suppress("TooManyFunctions")
class AppStartupTelemetry(
    private val metrics: MetricController,
    @VisibleForTesting(otherwise = PRIVATE)
    var appLaunchTimeMeasurement: AppLaunchTimeMeasurement = AppLaunchTimeMeasurement()
) : LifecycleObserver {

    init {
        ProcessLifecycleOwner.get().lifecycle.addObserver(this)
    }

    private var isMetricRecordedSinceAppWasForegrounded = false
    private var wasAppCreateCalledBeforeActivityCreate = false

    private var onCreateData: AppAllStartup? = null
    private var onRestartData: Pair<Type, Boolean?>? = null
    private var onNewIntentData: Source? = null

    fun onFenixApplicationOnCreate() {
        wasAppCreateCalledBeforeActivityCreate = true
    }

    fun onHomeActivityOnCreate(
        safeIntent: SafeIntent,
        hasSavedInstanceState: Boolean,
        homeActivityInitTimeStampNanoSeconds: Long,
        rootContainer: View
    ) {
        setOnCreateData(safeIntent, hasSavedInstanceState, homeActivityInitTimeStampNanoSeconds, false)
        rootContainer.doOnPreDraw {
            onPreDraw()
        }
    }

    fun onExternalAppBrowserOnCreate(
        safeIntent: SafeIntent,
        hasSavedInstanceState: Boolean,
        homeActivityInitTimeStampNanoSeconds: Long,
        rootContainer: View
    ) {
        setOnCreateData(safeIntent, hasSavedInstanceState, homeActivityInitTimeStampNanoSeconds, true)
        rootContainer.doOnPreDraw {
            onPreDraw()
        }
    }

    fun onHomeActivityOnRestart(rootContainer: View) {
        // DO NOT MOVE ANYTHING ABOVE THIS..
        // we are measuring startup time for hot startup type
        appLaunchTimeMeasurement.onHomeActivityOnRestart()

        // we are not setting [Source] in this method since source is derived from an intent.
        // therefore source gets set in onNewIntent().
        onRestartData = Pair(HOT, null)

        rootContainer.doOnPreDraw {
            onPreDraw()
        }
    }

    fun onHomeActivityOnNewIntent(safeIntent: SafeIntent) {
        // we are only setting [Source] in this method since source is derived from an intent].
        // other metric fields are set in onRestart()
        onNewIntentData = getStartupSourceFromIntent(safeIntent, false)
    }

    private fun setOnCreateData(
        safeIntent: SafeIntent,
        hasSavedInstanceState: Boolean,
        homeActivityInitTimeStampNanoSeconds: Long,
        isExternalAppBrowserActivity: Boolean
    ) {
        onCreateData = AppAllStartup(
            getStartupSourceFromIntent(safeIntent, isExternalAppBrowserActivity),
            getAppStartupType(),
            hasSavedInstanceState
        )
        appLaunchTimeMeasurement.onHomeActivityOnCreate(homeActivityInitTimeStampNanoSeconds)
        wasAppCreateCalledBeforeActivityCreate = false
    }

    private fun getAppStartupType(): Type {
        return if (wasAppCreateCalledBeforeActivityCreate) COLD else WARM
    }

    private fun getStartupSourceFromIntent(
        intent: SafeIntent,
        isExternalAppBrowserActivity: Boolean
    ): Source {
        return when {
            // since the intent action is same (ACTION_VIEW) for both CUSTOM_TAB and LINK.
            // we have to make sure that we are checking for CUSTOM_TAB condition first as this
            // check does not rely on intent action
            isExternalAppBrowserActivity -> CUSTOM_TAB
            intent.isLauncherIntent -> APP_ICON
            intent.action == Intent.ACTION_VIEW -> LINK
            // one of the unknown case is app switcher, where we go to the recent tasks to launch
            // Fenix.
            else -> UNKNOWN
        }
    }

    private suspend fun recordMetric() {
        if (!isMetricRecordedSinceAppWasForegrounded) {
            val appAllStartup: AppAllStartup = if (onCreateData != null) {
                onCreateData!!
            } else {
                mergeOnRestartAndOnNewIntentIntoStartup()
            }
            appAllStartup.launchTime = appLaunchTimeMeasurement.getApplicationLaunchTime(appAllStartup.type)
            metrics.track(appAllStartup)
            isMetricRecordedSinceAppWasForegrounded = true
        }
        // we don't want any weird previous states to persist on our next metric record.
        onCreateData = null
        onNewIntentData = null
        onRestartData = null
        appLaunchTimeMeasurement = AppLaunchTimeMeasurement()
    }

    private fun mergeOnRestartAndOnNewIntentIntoStartup(): AppAllStartup {
        return AppAllStartup(
            onNewIntentData ?: UNKNOWN,
            onRestartData?.first ?: ERROR,
            onRestartData?.second
        )
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    fun onApplicationOnStop() {
        // application was backgrounded, we need to record the new metric type if
        // application was to come to foreground again.
        // Therefore we set the isMetricRecorded flag to false.
        isMetricRecordedSinceAppWasForegrounded = false
    }

    /**
     *record the timestamp for the first frame drawn
     */
    @VisibleForTesting(otherwise = PRIVATE)
    fun onPreDraw() {
        // DO NOT MOVE ANYTHING ABOVE THIS..
        // we are measuring startup time here.
        appLaunchTimeMeasurement.onFirstFramePreDraw()
    }

    /**
     * record the metrics, blocking the main thread to make sure we get our metrics recorded before
     * the application potentially closes.
     */
    fun onStop() {
        runBlockingIncrement {
            recordMetric()
        }
    }
}
