/*
 *  Copyright (C) 2023 The Android Open Source Project
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package com.android.quickstep.util

import android.annotation.IntDef
import android.app.ActivityManager.RunningTaskInfo
import android.app.ActivityTaskManager.INVALID_TASK_ID
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ShortcutInfo
import android.os.UserHandle
import android.util.Log
import com.android.internal.annotations.VisibleForTesting
import com.android.launcher3.logging.StatsLogManager.EventEnum
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.shortcuts.ShortcutKey
import com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_UNDEFINED
import com.android.launcher3.util.SplitConfigurationOptions.StagePosition
import com.android.launcher3.util.SplitConfigurationOptions.getOppositeStagePosition
import com.android.quickstep.util.SplitSelectDataHolder.Companion.SplitLaunchType
import java.io.PrintWriter

/**
 * Holds/transforms/signs/seals/delivers information for the transient state of the user selecting a
 * first app to start split with and then choosing a second app. This class DOES NOT associate
 * itself with drag-and-drop split screen starts because they come from the bad part of town.
 *
 * After setting the correct fields for initial/second.* variables, this converts them into the
 * correct [PendingIntent] and [ShortcutInfo] objects where applicable and sends the necessary data
 * back via [getSplitLaunchData]. Note: there should be only one "initial" field and one "second"
 * field set, with the rest remaining null. (Exception: [Intent] and [UserHandle] are always passed
 * in together as a set, and are converted to a single [PendingIntent] or
 * [ShortcutInfo]+[PendingIntent] before launch.)
 *
 * [SplitLaunchType] indicates the type of tasks/apps/intents being launched given the provided
 * state
 */
class SplitSelectDataHolder(var context: Context?) {
    val TAG = SplitSelectDataHolder::class.simpleName

    /**
     * Order of the constant indicates the order of which task/app was selected. Ex.
     * SPLIT_TASK_SHORTCUT means primary split app identified by task, secondary is shortcut
     * SPLIT_SHORTCUT_TASK means primary split app is determined by shortcut, secondary is task
     */
    companion object {
        @IntDef(
            SPLIT_TASK_TASK,
            SPLIT_TASK_PENDINGINTENT,
            SPLIT_TASK_SHORTCUT,
            SPLIT_PENDINGINTENT_TASK,
            SPLIT_PENDINGINTENT_PENDINGINTENT,
            SPLIT_SHORTCUT_TASK,
            SPLIT_SINGLE_TASK_FULLSCREEN,
            SPLIT_SINGLE_INTENT_FULLSCREEN,
            SPLIT_SINGLE_SHORTCUT_FULLSCREEN
        )
        @Retention(AnnotationRetention.SOURCE)
        annotation class SplitLaunchType

        const val SPLIT_TASK_TASK = 0
        const val SPLIT_TASK_PENDINGINTENT = 1
        const val SPLIT_TASK_SHORTCUT = 2
        const val SPLIT_PENDINGINTENT_TASK = 3
        const val SPLIT_SHORTCUT_TASK = 4
        const val SPLIT_PENDINGINTENT_PENDINGINTENT = 5

        // Non-split edge case of launching the initial selected task as a fullscreen task
        const val SPLIT_SINGLE_TASK_FULLSCREEN = 6
        const val SPLIT_SINGLE_INTENT_FULLSCREEN = 7
        const val SPLIT_SINGLE_SHORTCUT_FULLSCREEN = 8
    }

    @StagePosition private var initialStagePosition: Int = STAGE_POSITION_UNDEFINED
    private var itemInfo: ItemInfo? = null
    private var secondItemInfo: ItemInfo? = null
    private var splitEvent: EventEnum? = null

    private var initialTaskId: Int = INVALID_TASK_ID
    private var secondTaskId: Int = INVALID_TASK_ID
    private var initialIntent: Intent? = null
    private var secondIntent: Intent? = null
    private var widgetSecondIntent: Intent? = null
    private var initialUser: UserHandle? = null
    private var secondUser: UserHandle? = null
    private var initialPendingIntent: PendingIntent? = null
    private var secondPendingIntent: PendingIntent? = null
    private var initialShortcut: ShortcutInfo? = null
    private var secondShortcut: ShortcutInfo? = null

    fun onDestroy() {
        context = null
    }

    /**
     * @param alreadyRunningTask if set to [android.app.ActivityTaskManager.INVALID_TASK_ID]
     *   then @param intent will be used to launch the initial task
     * @param intent will be ignored if @param alreadyRunningTask is set
     */
    fun setInitialTaskSelect(
        intent: Intent?,
        @StagePosition stagePosition: Int,
        itemInfo: ItemInfo?,
        splitEvent: EventEnum?,
        alreadyRunningTask: Int
    ) {
        if (alreadyRunningTask != INVALID_TASK_ID) {
            initialTaskId = alreadyRunningTask
        } else {
            initialIntent = intent!!
            initialUser = itemInfo!!.user
        }
        setInitialData(stagePosition, splitEvent, itemInfo)
    }

    /**
     * To be called after first task selected from using a split shortcut from the fullscreen
     * running app.
     */
    fun setInitialTaskSelect(
        info: RunningTaskInfo,
        @StagePosition stagePosition: Int,
        itemInfo: ItemInfo?,
        splitEvent: EventEnum?
    ) {
        initialTaskId = info.taskId
        setInitialData(stagePosition, splitEvent, itemInfo)
    }

    private fun setInitialData(
        @StagePosition stagePosition: Int,
        event: EventEnum?,
        item: ItemInfo?
    ) {
        itemInfo = item
        initialStagePosition = stagePosition
        splitEvent = event
    }

    /**
     * To be called as soon as user selects the second task (even if animations aren't complete)
     *
     * @param taskId The second task that will be launched.
     */
    fun setSecondTask(taskId: Int, itemInfo: ItemInfo) {
        secondTaskId = taskId
        secondItemInfo = itemInfo
    }

    /**
     * To be called as soon as user selects the second app (even if animations aren't complete)
     *
     * @param intent The second intent that will be launched.
     * @param user The user of that intent.
     */
    fun setSecondTask(intent: Intent, user: UserHandle, itemInfo: ItemInfo) {
        secondIntent = intent
        secondUser = user
        secondItemInfo = itemInfo
    }

    /**
     * To be called as soon as user selects the second app (even if animations aren't complete) Sets
     * [secondUser] from that of the pendingIntent
     *
     * @param pendingIntent The second PendingIntent that will be launched.
     */
    fun setSecondTask(pendingIntent: PendingIntent, itemInfo: ItemInfo) {
        secondPendingIntent = pendingIntent
        secondUser = pendingIntent.creatorUserHandle
        secondItemInfo = itemInfo
    }

    /**
     * Similar to [setSecondTask] except this is to be called for widgets which can pass through an
     * extra intent from their RemoteResponse. See
     * [android.widget.RemoteViews.RemoteResponse.getLaunchOptions].first
     */
    fun setSecondWidget(pendingIntent: PendingIntent, widgetIntent: Intent?, itemInfo: ItemInfo) {
        setSecondTask(pendingIntent, itemInfo)
        widgetSecondIntent = widgetIntent
    }

    private fun getShortcutInfo(intent: Intent?, user: UserHandle?): ShortcutInfo? {
        val intentPackage = intent?.getPackage() ?: return null
        val shortcutId = intent.getStringExtra(ShortcutKey.EXTRA_SHORTCUT_ID) ?: return null
        try {
            val context: Context =
                if (user != null) {
                    context!!.createPackageContextAsUser(intentPackage, 0 /* flags */, user)
                } else {
                    context!!.createPackageContext(intentPackage, 0 /* *flags */)
                }
            return ShortcutInfo.Builder(context, shortcutId).build()
        } catch (e: PackageManager.NameNotFoundException) {
            Log.w(TAG, "Failed to create a ShortcutInfo for " + intent.getPackage())
        }
        return null
    }

    /** Converts intents to pendingIntents, associating the [user] with the intent if provided */
    private fun getPendingIntent(intent: Intent?, user: UserHandle?): PendingIntent? {
        if (intent != initialIntent && intent != secondIntent) {
            throw IllegalStateException("Invalid intent to convert to PendingIntent")
        }

        return if (intent == null) {
            null
        } else if (user != null) {
            PendingIntent.getActivityAsUser(
                context,
                0,
                intent,
                PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT,
                null /* options */,
                user
            )
        } else {
            PendingIntent.getActivity(
                context,
                0,
                intent,
                PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT
            )
        }
    }

    /**
     * @return [SplitLaunchData] with the necessary fields populated as determined by
     *   [SplitLaunchData.splitLaunchType]. This is to be used for launching splitscreen
     */
    fun getSplitLaunchData(): SplitLaunchData {
        // Convert all intents to shortcut infos to see if determine if we launch shortcut or intent
        convertIntentsToFinalTypes()
        val splitLaunchType = getSplitLaunchType()
        if (splitLaunchType == SPLIT_TASK_PENDINGINTENT || splitLaunchType == SPLIT_TASK_SHORTCUT) {
            // need to get opposite stage position
            initialStagePosition = getOppositeStagePosition(initialStagePosition)
        }

        return generateSplitLaunchData(splitLaunchType)
    }

    /**
     * @return [SplitLaunchData] with the necessary fields populated as determined by
     *   [SplitLaunchData.splitLaunchType]. This is to be used for launching an initially selected
     *   split task in fullscreen
     */
    fun getFullscreenLaunchData(): SplitLaunchData {
        // Convert all intents to shortcut infos to determine if we launch shortcut or intent
        convertIntentsToFinalTypes()
        val splitLaunchType = getFullscreenLaunchType()

        return generateSplitLaunchData(splitLaunchType)
    }

    private fun generateSplitLaunchData(@SplitLaunchType splitLaunchType: Int): SplitLaunchData {
        return SplitLaunchData(
            splitLaunchType,
            initialTaskId,
            secondTaskId,
            initialPendingIntent,
            secondPendingIntent,
            widgetSecondIntent,
            initialUser?.identifier ?: -1,
            secondUser?.identifier ?: -1,
            initialShortcut,
            secondShortcut,
            itemInfo,
            splitEvent,
            initialStagePosition
        )
    }

    /**
     * Converts our [initialIntent] and [secondIntent] into shortcuts and pendingIntents, if
     * possible.
     *
     * Note that both [initialIntent] and [secondIntent] will be nullified on method return
     *
     * One caveat is that if [secondPendingIntent] is set, we will use that and *not* attempt to
     * convert [secondIntent]. This also leaves [widgetSecondIntent] untouched.
     */
    private fun convertIntentsToFinalTypes() {
        initialShortcut = getShortcutInfo(initialIntent, initialUser)
        initialPendingIntent = getPendingIntent(initialIntent, initialUser)
        initialIntent = null

        // Only one of the two is currently allowed (secondPendingIntent directly set for widgets)
        if (secondIntent != null && secondPendingIntent != null) {
            throw IllegalStateException("Both secondIntent and secondPendingIntent non-null")
        }
        // If secondPendingIntent already set, no need to convert. Prioritize using that
        if (secondPendingIntent != null) {
            secondIntent = null
            return
        }

        secondShortcut = getShortcutInfo(secondIntent, secondUser)
        secondPendingIntent = getPendingIntent(secondIntent, secondUser)
        secondIntent = null
    }

    /**
     * Only valid data fields at this point should be tasks, shortcuts, or pendingIntents Intents
     * need to be converted in [convertIntentsToFinalTypes] prior to calling this method
     */
    @VisibleForTesting
    @SplitLaunchType
    fun getSplitLaunchType(): Int {
        if (initialIntent != null || secondIntent != null) {
            throw IllegalStateException("Intents need to be converted")
        }

        // Prioritize task launches first
        if (initialTaskId != INVALID_TASK_ID) {
            if (secondTaskId != INVALID_TASK_ID) {
                return SPLIT_TASK_TASK
            }
            if (secondShortcut != null) {
                return SPLIT_TASK_SHORTCUT
            }
            if (secondPendingIntent != null) {
                return SPLIT_TASK_PENDINGINTENT
            }
        }

        if (secondTaskId != INVALID_TASK_ID) {
            if (initialShortcut != null) {
                return SPLIT_SHORTCUT_TASK
            }
            if (initialPendingIntent != null) {
                return SPLIT_PENDINGINTENT_TASK
            }
        }

        // All task+shortcut combinations are handled above, only launch left is with multiple
        // intents (and respective shortcut infos, if necessary)
        if (initialPendingIntent != null && secondPendingIntent != null) {
            return SPLIT_PENDINGINTENT_PENDINGINTENT
        }
        throw IllegalStateException("Unidentified split launch type")
    }

    @SplitLaunchType
    private fun getFullscreenLaunchType(): Int {
        if (initialTaskId != INVALID_TASK_ID) {
            return SPLIT_SINGLE_TASK_FULLSCREEN
        }

        if (initialShortcut != null) {
            return SPLIT_SINGLE_SHORTCUT_FULLSCREEN
        }

        if (initialPendingIntent != null) {
            return SPLIT_SINGLE_INTENT_FULLSCREEN
        }
        throw IllegalStateException("Unidentified fullscreen launch type")
    }

    data class SplitLaunchData(
        @SplitLaunchType val splitLaunchType: Int,
        var initialTaskId: Int = INVALID_TASK_ID,
        var secondTaskId: Int = INVALID_TASK_ID,
        var initialPendingIntent: PendingIntent? = null,
        var secondPendingIntent: PendingIntent? = null,
        var widgetSecondIntent: Intent? = null,
        var initialUserId: Int = -1,
        var secondUserId: Int = -1,
        var initialShortcut: ShortcutInfo? = null,
        var secondShortcut: ShortcutInfo? = null,
        var itemInfo: ItemInfo? = null,
        var splitEvent: EventEnum? = null,
        val initialStagePosition: Int = STAGE_POSITION_UNDEFINED
    )

    /**
     * @return `true` if first task has been selected and waiting for the second task to be chosen
     */
    fun isSplitSelectActive(): Boolean {
        return isInitialTaskIntentSet() && !isSecondTaskIntentSet()
    }

    /**
     * @return `true` if the first and second task have been chosen and split is waiting to be
     *   launched
     */
    fun isBothSplitAppsConfirmed(): Boolean {
        return isInitialTaskIntentSet() && isSecondTaskIntentSet()
    }

    private fun isInitialTaskIntentSet(): Boolean {
        return initialTaskId != INVALID_TASK_ID ||
            initialIntent != null ||
            initialPendingIntent != null
    }

    fun getInitialTaskId(): Int {
        return initialTaskId
    }

    fun getSecondTaskId(): Int {
        return secondTaskId
    }

    fun getSplitEvent(): EventEnum? {
        return splitEvent
    }

    fun getInitialStagePosition(): Int {
        return initialStagePosition
    }

    fun getItemInfo(): ItemInfo? {
        return itemInfo
    }

    fun getSecondItemInfo(): ItemInfo? {
        return secondItemInfo
    }

    private fun isSecondTaskIntentSet(): Boolean {
        return secondTaskId != INVALID_TASK_ID ||
            secondIntent != null ||
            secondPendingIntent != null
    }

    fun resetState() {
        initialStagePosition = STAGE_POSITION_UNDEFINED
        initialTaskId = INVALID_TASK_ID
        secondTaskId = INVALID_TASK_ID
        initialUser = null
        secondUser = null
        initialIntent = null
        secondIntent = null
        initialPendingIntent = null
        secondPendingIntent = null
        itemInfo = null
        splitEvent = null
        initialShortcut = null
        secondShortcut = null
    }

    fun dump(prefix: String, writer: PrintWriter) {
        writer.println("$prefix SplitSelectDataHolder")
        writer.println("$prefix\tinitialStagePosition= $initialStagePosition")
        writer.println("$prefix\tinitialTaskId= $initialTaskId")
        writer.println("$prefix\tsecondTaskId= $secondTaskId")
        writer.println("$prefix\tinitialUser= $initialUser")
        writer.println("$prefix\tsecondUser= $secondUser")
        writer.println("$prefix\tinitialIntent= $initialIntent")
        writer.println("$prefix\tsecondIntent= $secondIntent")
        writer.println("$prefix\tsecondPendingIntent= $secondPendingIntent")
        writer.println("$prefix\titemInfo= $itemInfo")
        writer.println("$prefix\tsplitEvent= $splitEvent")
        writer.println("$prefix\tinitialShortcut= $initialShortcut")
        writer.println("$prefix\tsecondShortcut= $secondShortcut")
    }
}
