/*
 * 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.launcher3.taskbar

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.text.SpannableString
import android.text.method.LinkMovementMethod
import android.text.style.URLSpan
import android.view.Gravity
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.MarginLayoutParams
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.TextView
import androidx.annotation.IntDef
import androidx.annotation.LayoutRes
import androidx.annotation.VisibleForTesting
import androidx.core.text.HtmlCompat
import androidx.core.view.updateLayoutParams
import com.airbnb.lottie.LottieAnimationView
import com.android.launcher3.LauncherPrefs
import com.android.launcher3.R
import com.android.launcher3.RemoveAnimationSettingsTracker
import com.android.launcher3.Utilities
import com.android.launcher3.config.FeatureFlags.enableTaskbarPinning
import com.android.launcher3.taskbar.TaskbarAutohideSuspendController.FLAG_AUTOHIDE_SUSPEND_EDU_OPEN
import com.android.launcher3.taskbar.TaskbarControllers.LoggableTaskbarController
import com.android.launcher3.util.OnboardingPrefs.TASKBAR_EDU_TOOLTIP_STEP
import com.android.launcher3.util.OnboardingPrefs.TASKBAR_SEARCH_EDU_SEEN
import com.android.launcher3.util.ResourceBasedOverride
import com.android.launcher3.views.ActivityContext
import com.android.launcher3.views.BaseDragLayer
import com.android.quickstep.util.ContextualSearchInvoker
import com.android.quickstep.util.LottieAnimationColorUtils
import com.android.wm.shell.shared.TypefaceUtils
import com.android.wm.shell.shared.TypefaceUtils.FontFamily
import java.io.PrintWriter

/** First EDU step for swiping up to show transient Taskbar. */
const val TOOLTIP_STEP_SWIPE = 0
/** Second EDU step for explaining Taskbar functionality when unstashed. */
const val TOOLTIP_STEP_FEATURES = 1
/** Third EDU step for explaining Taskbar pinning. */
const val TOOLTIP_STEP_PINNING = 2

/**
 * EDU is completed.
 *
 * This value should match the maximum count for [TASKBAR_EDU_TOOLTIP_STEP].
 */
const val TOOLTIP_STEP_NONE = 3
/** The base URL for the Privacy Policy that will later be localized. */
private const val PRIVACY_POLICY_BASE_URL = "https://policies.google.com/privacy/embedded?hl="
/** The base URL for the Terms of Service that will later be localized. */
private const val TOS_BASE_URL = "https://policies.google.com/terms?hl="

/** Current step in the tooltip EDU flow. */
@Retention(AnnotationRetention.SOURCE)
@IntDef(TOOLTIP_STEP_SWIPE, TOOLTIP_STEP_FEATURES, TOOLTIP_STEP_PINNING, TOOLTIP_STEP_NONE)
annotation class TaskbarEduTooltipStep

/** Controls stepping through the Taskbar tooltip EDU. */
open class TaskbarEduTooltipController(context: Context) :
    ResourceBasedOverride, LoggableTaskbarController {

    protected val activityContext: TaskbarActivityContext = ActivityContext.lookupContext(context)
    open val shouldShowSearchEdu: Boolean
        get() =
            ContextualSearchInvoker(activityContext)
                .runContextualSearchInvocationChecksAndLogFailures()

    private val isTooltipEnabled: Boolean
        get() {
            return !Utilities.isRunningInTestHarness() &&
                !activityContext.isPhoneMode &&
                !activityContext.isTinyTaskbar
        }

    val isTooltipOpen: Boolean
        get() = tooltip?.isOpen ?: false

    val isBeforeTooltipFeaturesStep: Boolean
        get() = isTooltipEnabled && tooltipStep <= TOOLTIP_STEP_FEATURES

    private lateinit var controllers: TaskbarControllers

    // Keep track of whether the user has seen the Search Edu
    @VisibleForTesting
    var userHasSeenSearchEdu: Boolean
        get() {
            return TASKBAR_SEARCH_EDU_SEEN.get(activityContext)
        }
        private set(seen) {
            LauncherPrefs.get(activityContext).put(TASKBAR_SEARCH_EDU_SEEN, seen)
        }

    @TaskbarEduTooltipStep
    var tooltipStep: Int
        get() {
            return TASKBAR_EDU_TOOLTIP_STEP.get(activityContext)
        }
        private set(step) {
            TASKBAR_EDU_TOOLTIP_STEP.set(step, activityContext)
        }

    private var tooltip: TaskbarEduTooltip? = null

    fun init(controllers: TaskbarControllers) {
        this.controllers = controllers
        // We want to show the Search Edu right after pinning the taskbar, so we post it here
        activityContext.dragLayer.post { maybeShowSearchEdu() }
    }

    /**
     * Turns off auto play of lottie animations if user has opted to remove animation else attaches
     * click listener to allow user to play or pause animations.
     */
    fun handleEduAnimations(animationViews: List<LottieAnimationView>) {
        for (animationView in animationViews) {
            if (
                RemoveAnimationSettingsTracker.INSTANCE.get(animationView.context)
                    .isRemoveAnimationEnabled()
            ) {
                animationView.pauseAnimation()
            } else {
                animationView.setOnClickListener {
                    if (animationView.isAnimating) animationView.pauseAnimation()
                    else animationView.playAnimation()
                }
            }
        }
    }

    /** Shows swipe EDU tooltip if it is the current [tooltipStep]. */
    fun maybeShowSwipeEdu() {
        if (
            !isTooltipEnabled ||
                !activityContext.isTransientTaskbar ||
                tooltipStep > TOOLTIP_STEP_SWIPE
        ) {
            return
        }

        tooltipStep = TOOLTIP_STEP_FEATURES
        inflateTooltip(R.layout.taskbar_edu_swipe)
        tooltip?.run {
            TypefaceUtils.setTypeface(
                requireViewById(R.id.taskbar_edu_title),
                FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED,
            )
            val swipeAnimation = requireViewById<LottieAnimationView>(R.id.swipe_animation)
            swipeAnimation.supportLightTheme()
            handleEduAnimations(listOf(swipeAnimation))
            show()
        }
    }

    /**
     * Shows feature EDU tooltip if this step has not been seen.
     *
     * If [TOOLTIP_STEP_SWIPE] has not been seen at this point, the first step is skipped because a
     * swipe up is necessary to show this step.
     */
    fun maybeShowFeaturesEdu() {
        if (!isTooltipEnabled || tooltipStep > TOOLTIP_STEP_FEATURES) {
            maybeShowPinningEdu()
            maybeShowSearchEdu()
            return
        }

        tooltipStep = TOOLTIP_STEP_NONE
        inflateTooltip(R.layout.taskbar_edu_features)
        tooltip?.run {
            allowTouchDismissal = false
            val splitscreenAnim = requireViewById<LottieAnimationView>(R.id.splitscreen_animation)
            val suggestionsAnim = requireViewById<LottieAnimationView>(R.id.suggestions_animation)
            val pinningAnim = requireViewById<LottieAnimationView>(R.id.pinning_animation)
            val pinningEdu = requireViewById<View>(R.id.pinning_edu)
            splitscreenAnim.supportLightTheme()
            suggestionsAnim.supportLightTheme()
            pinningAnim.supportLightTheme()
            handleEduAnimations(listOf(splitscreenAnim, suggestionsAnim, pinningAnim))
            if (activityContext.isTransientTaskbar) {
                splitscreenAnim.setAnimation(R.raw.taskbar_edu_splitscreen_transient)
                suggestionsAnim.setAnimation(R.raw.taskbar_edu_suggestions_transient)
                pinningEdu.visibility = if (enableTaskbarPinning()) VISIBLE else GONE
            } else {
                splitscreenAnim.setAnimation(R.raw.taskbar_edu_splitscreen_persistent)
                suggestionsAnim.setAnimation(R.raw.taskbar_edu_suggestions_persistent)
                pinningEdu.visibility = GONE
            }

            TypefaceUtils.setTypeface(
                requireViewById(R.id.taskbar_edu_title),
                FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED,
            )
            TypefaceUtils.setTypeface(
                requireViewById(R.id.splitscreen_text),
                FontFamily.GSF_BODY_MEDIUM,
            )
            TypefaceUtils.setTypeface(
                requireViewById(R.id.suggestions_text),
                FontFamily.GSF_BODY_MEDIUM,
            )
            TypefaceUtils.setTypeface(
                requireViewById(R.id.pinning_text),
                FontFamily.GSF_BODY_MEDIUM,
            )

            // Set up layout parameters.
            content.updateLayoutParams { width = MATCH_PARENT }
            updateLayoutParams<MarginLayoutParams> {
                if (activityContext.isTransientTaskbar) {
                    width =
                        resources.getDimensionPixelSize(
                            if (enableTaskbarPinning())
                                R.dimen.taskbar_edu_features_tooltip_width_with_three_features
                            else R.dimen.taskbar_edu_features_tooltip_width_with_two_features
                        )

                    bottomMargin += activityContext.deviceProfile.taskbarHeight
                } else {
                    width =
                        resources.getDimensionPixelSize(
                            R.dimen.taskbar_edu_features_tooltip_width_with_two_features
                        )
                }
            }

            findViewById<View>(R.id.done_button)?.setOnClickListener { hide() }
            show()
        }
    }

    /**
     * Shows standalone Pinning EDU tooltip if this EDU has not been seen.
     *
     * We show this standalone edu if users have seen the previous version of taskbar education,
     * which did not include the pinning feature.
     */
    private fun maybeShowPinningEdu() {
        // use old value of tooltipStep that was set to the previous value of TOOLTIP_STEP_NONE (2
        // for the original 2 edu steps) as a proxy to needing to show the separate pinning edu
        if (
            !enableTaskbarPinning() ||
                !activityContext.isTransientTaskbar ||
                !isTooltipEnabled ||
                tooltipStep > TOOLTIP_STEP_PINNING ||
                tooltipStep < TOOLTIP_STEP_FEATURES
        ) {
            return
        }
        tooltipStep = TOOLTIP_STEP_NONE
        inflateTooltip(R.layout.taskbar_edu_pinning)

        tooltip?.run {
            allowTouchDismissal = true
            TypefaceUtils.setTypeface(
                requireViewById(R.id.taskbar_edu_title),
                FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED,
            )
            TypefaceUtils.setTypeface(
                requireViewById(R.id.pinning_text),
                FontFamily.GSF_BODY_MEDIUM,
            )

            val pinningAnim =
                requireViewById<LottieAnimationView>(R.id.standalone_pinning_animation)
            pinningAnim.supportLightTheme()
            handleEduAnimations(listOf(pinningAnim))
            updateLayoutParams<BaseDragLayer.LayoutParams> {
                if (activityContext.isTransientTaskbar) {
                    bottomMargin += activityContext.deviceProfile.taskbarHeight
                }
                // Unlike other tooltips, we want to align with taskbar divider rather than center.
                gravity = Gravity.BOTTOM
                marginStart = 0
                width =
                    resources.getDimensionPixelSize(
                        R.dimen.taskbar_edu_features_tooltip_width_with_one_feature
                    )
            }

            // Calculate the amount the tooltip must be shifted by to align with the taskbar divider
            val taskbarDividerView = controllers.taskbarViewController.taskbarDividerView
            val dividerLocation = taskbarDividerView.x + taskbarDividerView.width / 2
            x = dividerLocation - layoutParams.width / 2

            show()
        }
    }

    /**
     * Shows standalone Search EDU tooltip if this EDU has not been seen.
     *
     * We show this standalone edu for users to learn to how to trigger Search from the pinned
     * taskbar
     */
    fun maybeShowSearchEdu() {
        if (
            !enableTaskbarPinning() ||
                !activityContext.isPinnedTaskbar ||
                !isTooltipEnabled ||
                !shouldShowSearchEdu ||
                userHasSeenSearchEdu ||
                !controllers.taskbarStashController.isTaskbarVisibleAndNotStashing
        ) {
            return
        }
        userHasSeenSearchEdu = true
        inflateTooltip(R.layout.taskbar_edu_search)
        tooltip?.run {
            allowTouchDismissal = true
            val searchEdu = requireViewById<LottieAnimationView>(R.id.search_edu_animation)
            searchEdu.supportLightTheme()
            handleEduAnimations(listOf(searchEdu))
            val eduSubtitle: TextView = requireViewById(R.id.search_edu_text)

            TypefaceUtils.setTypeface(
                requireViewById(R.id.taskbar_edu_title),
                FontFamily.GSF_HEADLINE_SMALL_EMPHASIZED,
            )
            TypefaceUtils.setTypeface(eduSubtitle, FontFamily.GSF_BODY_SMALL)

            showDisclosureText(eduSubtitle)
            updateLayoutParams<BaseDragLayer.LayoutParams> {
                if (activityContext.isTransientTaskbar) {
                    bottomMargin += activityContext.deviceProfile.taskbarHeight
                }
                // Unlike other tooltips, we want to align with the all apps button rather than
                // center.
                gravity = Gravity.BOTTOM
                marginStart = 0
                width =
                    resources.getDimensionPixelSize(
                        R.dimen.taskbar_edu_features_tooltip_width_with_one_feature
                    )
            }

            // Calculate the amount the tooltip must be shifted by to align with the action key
            val allAppsButtonView = controllers.taskbarViewController.allAppsButtonView
            if (allAppsButtonView != null) {
                val allAppsIconLocation = allAppsButtonView.x + allAppsButtonView.width / 2
                x = allAppsIconLocation - layoutParams.width / 2
            }

            show()
        }
    }

    /**
     * Set up the provided TextView to display legal disclosures. The method takes locale into
     * account to show the appropriate links to regional disclosures.
     */
    private fun TaskbarEduTooltip.showDisclosureText(
        textView: TextView,
        stringId: Int = R.string.taskbar_edu_search_disclosure,
    ) {
        val locale = resources.configuration.locales[0]
        val text =
            SpannableString(
                HtmlCompat.fromHtml(
                    resources.getString(
                        stringId,
                        PRIVACY_POLICY_BASE_URL + locale.language,
                        TOS_BASE_URL + locale.language,
                    ),
                    HtmlCompat.FROM_HTML_MODE_COMPACT,
                )
            )
        // Directly process URLSpan clicks
        text.getSpans(0, text.length, URLSpan::class.java).forEach { urlSpan ->
            val url: URLSpan =
                object : URLSpan(urlSpan.url) {
                    override fun onClick(widget: View) {
                        val uri = Uri.parse(urlSpan.url)
                        val context = widget.context
                        val intent =
                            Intent(Intent.ACTION_VIEW, uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                        context.startActivity(intent)
                    }
                }

            val spanStart = text.getSpanStart(urlSpan)
            val spanEnd = text.getSpanEnd(urlSpan)
            val spanFlags = text.getSpanFlags(urlSpan)
            text.removeSpan(urlSpan)
            text.setSpan(url, spanStart, spanEnd, spanFlags)
        }
        textView.text = text
        textView.movementMethod = LinkMovementMethod.getInstance()
    }

    /** Closes the current [tooltip]. */
    fun hide() {
        tooltip?.close(true)
    }

    /** Initializes [tooltip] with content from [contentResId]. */
    private fun inflateTooltip(@LayoutRes contentResId: Int) {
        val overlayContext = controllers.taskbarOverlayController.requestWindow()
        val tooltip =
            overlayContext.layoutInflater.inflate(
                R.layout.taskbar_edu_tooltip,
                overlayContext.dragLayer,
                false,
            ) as TaskbarEduTooltip

        controllers.taskbarAutohideSuspendController.updateFlag(
            FLAG_AUTOHIDE_SUSPEND_EDU_OPEN,
            true,
        )

        tooltip.onCloseCallback = {
            this.tooltip = null
            controllers.taskbarAutohideSuspendController.updateFlag(
                FLAG_AUTOHIDE_SUSPEND_EDU_OPEN,
                false,
            )
            controllers.taskbarStashController.updateAndAnimateTransientTaskbar(true)
        }
        tooltip.accessibilityDelegate = createAccessibilityDelegate()

        overlayContext.layoutInflater.inflate(contentResId, tooltip.content, true)
        this.tooltip = tooltip
    }

    private fun createAccessibilityDelegate() =
        object : View.AccessibilityDelegate() {
            override fun performAccessibilityAction(
                host: View,
                action: Int,
                args: Bundle?,
            ): Boolean {
                if (action == R.id.close) {
                    hide()
                    return true
                }
                return super.performAccessibilityAction(host, action, args)
            }

            override fun onPopulateAccessibilityEvent(host: View, event: AccessibilityEvent) {
                super.onPopulateAccessibilityEvent(host, event)
                if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
                    event.text.add(host.context?.getText(R.string.taskbar_edu_a11y_title))
                }
            }

            override fun onInitializeAccessibilityNodeInfo(
                host: View,
                info: AccessibilityNodeInfo,
            ) {
                super.onInitializeAccessibilityNodeInfo(host, info)
                info.addAction(
                    AccessibilityNodeInfo.AccessibilityAction(
                        R.id.close,
                        host.context?.getText(R.string.taskbar_edu_close),
                    )
                )
            }
        }

    override fun dumpLogs(prefix: String?, pw: PrintWriter?) {
        pw?.println(prefix + "TaskbarEduTooltipController:")
        pw?.println("$prefix\tisTooltipEnabled=$isTooltipEnabled")
        pw?.println("$prefix\tisOpen=$isTooltipOpen")
        pw?.println("$prefix\ttooltipStep=$tooltipStep")
    }

    companion object {
        @JvmStatic
        fun newInstance(context: Context): TaskbarEduTooltipController {
            return ResourceBasedOverride.Overrides.getObject(
                TaskbarEduTooltipController::class.java,
                context,
                R.string.taskbar_edu_tooltip_controller_class,
            )
        }
    }
}

/**
 * Maps colors in the dark-themed Lottie assets to their light-themed equivalents.
 *
 * For instance, `".blue100" to R.color.lottie_blue400` means objects that are material blue100 in
 * dark theme should be changed to material blue400 in light theme.
 */
private val DARK_TO_LIGHT_COLORS =
    mapOf(
        ".blue100" to R.color.lottie_blue400,
        ".blue400" to R.color.lottie_blue600,
        ".green100" to R.color.lottie_green400,
        ".green400" to R.color.lottie_green600,
        ".grey300" to R.color.lottie_grey600,
        ".grey400" to R.color.lottie_grey700,
        ".grey800" to R.color.lottie_grey200,
        ".red400" to R.color.lottie_red600,
        ".yellow100" to R.color.lottie_yellow400,
        ".yellow400" to R.color.lottie_yellow600,
    )

private fun LottieAnimationView.supportLightTheme() {
    if (Utilities.isDarkTheme(context)) {
        return
    }

    LottieAnimationColorUtils.updateToColorResources(this, DARK_TO_LIGHT_COLORS, context.theme)
}
