/*
 * Copyright (C) 2018 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.touch;

import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_BIND_PENDING_APPWIDGET;
import static com.android.launcher3.LauncherConstants.ActivityCodes.REQUEST_RECONFIGURE_APPWIDGET;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.IGNORE;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_FOLDER_OPEN;
import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_INSTALL_APP_BUTTON_TAP;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_BY_PUBLISHER;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_LOCKED_USER;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_QUIET_USER;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SAFEMODE;
import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_DISABLED_SUSPENDED;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;

import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.content.pm.LauncherApps;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.PackageInstaller.SessionInfo;
import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Toast;

import com.android.launcher3.BubbleTextView;
import com.android.launcher3.Flags;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.allapps.AlphabeticalAppsList;
import com.android.launcher3.allapps.PrivateProfileManager;
import com.android.launcher3.allapps.search.AppsSearchContainerLayout;
import com.android.launcher3.apppairs.AppPairIcon;
import com.android.launcher3.folder.Folder;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.logging.InstanceId;
import com.android.launcher3.logging.InstanceIdSequence;
import com.android.launcher3.logging.StatsLogManager;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.AppPairInfo;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.ItemInfoWithIcon;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pm.InstallSessionHelper;
import com.android.launcher3.shortcuts.ShortcutKey;
import com.android.launcher3.testing.TestLogging;
import com.android.launcher3.testing.shared.TestProtocol;
import com.android.launcher3.util.ApiWrapper;
import com.android.launcher3.util.ItemInfoMatcher;
import com.android.launcher3.views.FloatingIconView;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.PendingAppWidgetHostView;
import com.android.launcher3.widget.WidgetAddFlowHandler;
import com.android.launcher3.widget.WidgetManagerHelper;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;

import app.murinelauncher.util.Constants;

/**
 * Class for handling clicks on workspace and all-apps items
 */
public class ItemClickHandler {

    private static final String TAG = "ItemClickHandler";
    private static final boolean DEBUG = true;

    /**
     * Instance used for click handling on items
     */
    public static final OnClickListener INSTANCE = ItemClickHandler::onClick;

    private static void onClick(View v) {
        // Make sure that rogue clicks don't get through while allapps is launching, or after the
        // view has detached (it's possible for this to happen if the view is removed mid touch).
        if (v.getWindowToken() == null) return;

        Launcher launcher = Launcher.getLauncher(v.getContext());
        if (!launcher.getWorkspace().isFinishedSwitchingState()) return;

        Object tag = v.getTag();
        if (tag instanceof WorkspaceItemInfo) {
            onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher);
        } else if (tag instanceof FolderInfo) {
            onClickFolderIcon(v);
        } else if (tag instanceof AppPairInfo) {
            onClickAppPairIcon(v);
        } else if (tag instanceof AppInfo) {
            startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher);
        } else if (tag instanceof LauncherAppWidgetInfo) {
            if (v instanceof PendingAppWidgetHostView) {
                if (DEBUG) {
                    String targetPackage = ((LauncherAppWidgetInfo) tag).getTargetPackage();
                    Log.d(TAG, "onClick: PendingAppWidgetHostView clicked for"
                            + " package=" + targetPackage);
                }
                onClickPendingWidget((PendingAppWidgetHostView) v, launcher);
            } else {
                if (DEBUG) {
                    String targetPackage = ((LauncherAppWidgetInfo) tag).getTargetPackage();
                    Log.d(TAG, "onClick: LauncherAppWidgetInfo clicked,"
                            + " but not instance of PendingAppWidgetHostView. Returning."
                            + " package=" + targetPackage);
                }
            }
        } else if (tag instanceof ItemClickProxy) {
            ((ItemClickProxy) tag).onItemClicked(v);
        }
    }

    /**
     * Event handler for a folder icon click.
     *
     * @param v The view that was clicked. Must be an instance of {@link FolderIcon}.
     */
    private static void onClickFolderIcon(View v) {
        Folder folder = ((FolderIcon) v).getFolder();
        if (!folder.isOpen() && !folder.isDestroyed()) {
            // Open the requested folder
            folder.animateOpen();
            StatsLogManager.newInstance(v.getContext()).logger().withItemInfo(folder.mInfo)
                    .log(LAUNCHER_FOLDER_OPEN);
        }
    }

    /**
     * Event handler for an app pair icon click.
     *
     * @param v The view that was clicked. Must be an instance of {@link AppPairIcon}.
     */
    private static void onClickAppPairIcon(View v) {
        Launcher launcher = Launcher.getLauncher(v.getContext());
        AppPairIcon icon = (AppPairIcon) v;
        AppPairInfo info = icon.getInfo();
        boolean isApp1Launchable = info.isLaunchable(launcher).getFirst(),
                isApp2Launchable = info.isLaunchable(launcher).getSecond();
        if (!isApp1Launchable || !isApp2Launchable) {
            // App pair is unlaunchable due to screen size.
            boolean isFoldable = InvariantDeviceProfile.INSTANCE.get(launcher)
                    .supportedProfiles.stream().anyMatch(dp -> dp.isTwoPanels);
            Toast.makeText(launcher, isFoldable
                            ? R.string.app_pair_needs_unfold
                            : R.string.app_pair_unlaunchable_at_screen_size,
                    Toast.LENGTH_SHORT).show();
            return;
        } else if (info.isDisabled()) {
            // App pair is disabled for another reason.
            WorkspaceItemInfo app1 = info.getFirstApp();
            WorkspaceItemInfo app2 = info.getSecondApp();
            // Show the user why the app pair is disabled.
            if (app1.isDisabled() && app2.isDisabled()) {
                // Both apps are disabled, show generic "app pair is not available" toast.
                Toast.makeText(launcher, R.string.app_pair_not_available, Toast.LENGTH_SHORT)
                        .show();
                return;
            } else if ((app1.isDisabled() && handleDisabledItemClicked(app1, launcher))
                    || (app2.isDisabled() && handleDisabledItemClicked(app2, launcher))) {
                // Only one is disabled, and handleDisabledItemClicked() showed a specific toast
                // explaining why, so we are done.
                return;
            }
        }

        // Either the app pair is not disabled, or it is a disabled state that can be handled by
        // framework directly (e.g. one app is paused), so go ahead and launch.
        launcher.launchAppPair(icon);
    }

    /**
     * Event handler for the app widget view which has not fully restored.
     */
    private static void onClickPendingWidget(PendingAppWidgetHostView v, Launcher launcher) {
        if (launcher.getPackageManager().isSafeMode()) {
            Toast.makeText(launcher, R.string.safemode_widget_error, Toast.LENGTH_SHORT).show();
            return;
        }

        final LauncherAppWidgetInfo info = (LauncherAppWidgetInfo) v.getTag();
        if (v.isReadyForClickSetup()) {
            LauncherAppWidgetProviderInfo appWidgetInfo = new WidgetManagerHelper(launcher)
                    .findProvider(info.providerName, info.user);
            if (appWidgetInfo == null) {
                Log.e(TAG, "onClickPendingWidget: Pending widget ready for click setup,"
                        + " but LauncherAppWidgetProviderInfo was null. Returning."
                        + " component=" + info.getTargetComponent());
                return;
            }
            WidgetAddFlowHandler addFlowHandler = new WidgetAddFlowHandler(appWidgetInfo);

            if (info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID)) {
                if (!info.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_ALLOCATED)) {
                    // This should not happen, as we make sure that an Id is allocated during bind.
                    Log.e(TAG, "onClickPendingWidget: Pending widget ready for click setup,"
                            + " and LauncherAppWidgetProviderInfo was found. However,"
                            + " no appWidgetId was allocated. Returning."
                            + " component=" + info.getTargetComponent());
                    return;
                }
                addFlowHandler.startBindFlow(launcher, info.appWidgetId, info,
                        REQUEST_BIND_PENDING_APPWIDGET);
            } else {
                addFlowHandler.startConfigActivity(launcher, info, REQUEST_RECONFIGURE_APPWIDGET);
            }
        } else {
            final String packageName = info.providerName.getPackageName();
            onClickPendingAppItem(v, launcher, packageName, info.installProgress >= 0);
        }
    }

    private static void onClickPendingAppItem(View v, Launcher launcher, String packageName,
            boolean downloadStarted) {
        ItemInfo item = (ItemInfo) v.getTag();
        CompletableFuture<SessionInfo> siFuture = CompletableFuture.supplyAsync(() ->
                InstallSessionHelper.INSTANCE.get(launcher)
                        .getActiveSessionInfo(item.user, packageName),
                UI_HELPER_EXECUTOR);
        Consumer<SessionInfo> marketLaunchAction = sessionInfo -> {
            if (sessionInfo != null) {
                LauncherApps launcherApps = launcher.getSystemService(LauncherApps.class);
                try {
                    launcherApps.startPackageInstallerSessionDetailsActivity(sessionInfo, null,
                            launcher.getActivityLaunchOptions(v, item).toBundle());
                    return;
                } catch (Exception e) {
                    Log.e(TAG, "Unable to launch market intent for package=" + packageName, e);
                }
            }
            // Fallback to using custom market intent.
            Intent intent = ApiWrapper.INSTANCE.get(launcher).getMarketSearchIntent(
                    packageName, item.user);
            launcher.startActivitySafely(v, intent, item);
        };

        if (downloadStarted) {
            // If the download has started, simply direct to the market app.
            siFuture.thenAcceptAsync(marketLaunchAction, MAIN_EXECUTOR);
            return;
        }
        new AlertDialog.Builder(launcher)
                .setTitle(R.string.abandoned_promises_title)
                .setMessage(R.string.abandoned_promise_explanation)
                .setPositiveButton(R.string.abandoned_search,
                        (d, i) -> siFuture.thenAcceptAsync(marketLaunchAction, MAIN_EXECUTOR))
                .setNeutralButton(R.string.abandoned_clean_this,
                        (d, i) -> launcher.getWorkspace()
                                .persistRemoveItemsByMatcher(ItemInfoMatcher.ofPackages(
                                        Collections.singleton(packageName), item.user),
                                        "user explicitly removes the promise app icon"))
                .create().show();
    }

    /**
     * Handles clicking on a disabled shortcut
     *
     * @return true iff the disabled item click has been handled.
     */
    public static boolean handleDisabledItemClicked(WorkspaceItemInfo shortcut, Context context) {
        final int disabledFlags = shortcut.runtimeStatusFlags
                & WorkspaceItemInfo.FLAG_DISABLED_MASK;
        // Handle the case where the disabled reason is DISABLED_REASON_VERSION_LOWER.
        // Show an AlertDialog for the user to choose either updating the app or cancel the launch.
        if (maybeCreateAlertDialogForShortcut(shortcut, context)) {
            return true;
        }

        if ((disabledFlags
                & ~FLAG_DISABLED_SUSPENDED
                & ~FLAG_DISABLED_QUIET_USER) == 0) {
            // If the app is only disabled because of the above flags, launch activity anyway.
            // Framework will tell the user why the app is suspended.
            return false;
        } else {
            if (!TextUtils.isEmpty(shortcut.disabledMessage)) {
                // Use a message specific to this shortcut, if it has one.
                Toast.makeText(context, shortcut.disabledMessage, Toast.LENGTH_SHORT).show();
                return true;
            }
            // Otherwise just use a generic error message.
            int error = R.string.activity_not_available;
            if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_SAFEMODE) != 0) {
                error = R.string.safemode_shortcut_error;
            } else if ((shortcut.runtimeStatusFlags & FLAG_DISABLED_BY_PUBLISHER) != 0
                    || (shortcut.runtimeStatusFlags & FLAG_DISABLED_LOCKED_USER) != 0) {
                error = R.string.shortcut_not_available;
            }
            Toast.makeText(context, error, Toast.LENGTH_SHORT).show();
            return true;
        }
    }

    private static boolean maybeCreateAlertDialogForShortcut(final WorkspaceItemInfo shortcut,
            Context context) {
        try {
            final Launcher launcher = Launcher.getLauncher(context);
            if (shortcut.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT
                    && shortcut.isDisabledVersionLower()) {
                final Intent marketIntent = shortcut.getMarketIntent(context);
                // No market intent means no target package for the shortcut, which should be an
                // issue. Falling back to showing toast messages.
                if (marketIntent == null) {
                    return false;
                }

                new AlertDialog.Builder(context)
                        .setTitle(R.string.dialog_update_title)
                        .setMessage(R.string.dialog_update_message)
                        .setPositiveButton(R.string.dialog_update, (d, i) -> {
                            // Direct the user to the play store to update the app
                            context.startActivity(marketIntent);
                        })
                        .setNeutralButton(R.string.dialog_remove, (d, i) -> {
                            // Remove the icon if launcher is successfully initialized
                            launcher.getWorkspace().persistRemoveItemsByMatcher(ItemInfoMatcher
                                    .ofShortcutKeys(Collections.singleton(ShortcutKey
                                            .fromItemInfo(shortcut))),
                                    "user explicitly removes disabled shortcut");
                        })
                        .create()
                        .show();
                return true;
            }
        } catch (Exception e) {
            Log.e(TAG, "Error creating alert dialog", e);
        }

        return false;
    }

    /**
     * Event handler for an app shortcut click.
     *
     * @param v The view that was clicked. Must be a tagged with a {@link WorkspaceItemInfo}.
     */
    public static void onClickAppShortcut(View v, WorkspaceItemInfo shortcut, Launcher launcher) {
        if (shortcut.isDisabled() && handleDisabledItemClicked(shortcut, launcher)) {
            return;
        }

        // Check for abandoned promise
        if ((v instanceof BubbleTextView) && shortcut.hasPromiseIconUi()
                && (!Flags.enableSupportForArchiving() || !shortcut.isArchived())) {
            String packageName = shortcut.getTargetPackage();
            if (!TextUtils.isEmpty(packageName)) {
                onClickPendingAppItem(
                        v,
                        launcher,
                        packageName,
                        (shortcut.runtimeStatusFlags
                                & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0);
                return;
            }
        }

        // Start activities
        startAppShortcutOrInfoActivity(v, shortcut, launcher);
    }

    private static void startAppShortcutOrInfoActivity(View v, ItemInfo item, Launcher launcher) {
        TestLogging.recordEvent(
                TestProtocol.SEQUENCE_MAIN, "start: startAppShortcutOrInfoActivity");
        Intent intent = item.getIntent();
        if (item instanceof ItemInfoWithIcon itemInfoWithIcon) {
            if ((itemInfoWithIcon.runtimeStatusFlags
                    & ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE) != 0) {
                intent = ApiWrapper.INSTANCE.get(launcher).getAppMarketActivityIntent(
                        itemInfoWithIcon.getTargetComponent().getPackageName(),
                        Process.myUserHandle());
            } else if (itemInfoWithIcon.itemType
                    == LauncherSettings.Favorites.ITEM_TYPE_PRIVATE_SPACE_INSTALL_APP_BUTTON) {
                UserHandle privateUser = launcher.getAppsView().getPrivateProfileManager().getProfileUser();
                if (privateUser != null) try {
                    if (listPrivateSpaceMarketApps(launcher, privateUser, v, item)) return;
                } catch (Exception e) {
                    Log.w(TAG, "Failed to launch market in private user", e);
                }
                android.widget.Toast.makeText(launcher, R.string.activity_not_found, android.widget.Toast.LENGTH_SHORT).show();
                return;
            }
        }

        boolean enableMovingContentIntoPrivateSpace = false;
        if (Utilities.ATLEAST_BAKLAVA) {
            try {
                /* LC-Note: Some devices (Android 16 QPR) doesn't have or expose this flag to user.
                 * Let's assume no, because (the flags) enableMovingContentIntoPrivateSpace seems
                 * to be False for R8 by default.
                 * */
                enableMovingContentIntoPrivateSpace = android.multiuser.Flags.enableMovingContentIntoPrivateSpace();
            } catch (NoClassDefFoundError | NoSuchMethodError e) {
                enableMovingContentIntoPrivateSpace = false;
            }
        }
        if (enableMovingContentIntoPrivateSpace &&
                Objects.equals(item.getTargetPackage(), AlphabeticalAppsList.PRIVATE_SPACE_PACKAGE)
                && item.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
            // Only show the popup menu when clicking on the icon itself.
            if (v instanceof BubbleTextView btv) {
                btv.startLongPressAction();
                return;
            }
        }
        if (intent != null && PrivateProfileManager.ACTION_PRIVATE_SPACE_UNLOCK.equals(intent.getAction())) {
            PrivateProfileManager ppm = launcher.getAppsView().getPrivateProfileManager();
            if (ppm != null) ppm.setQuietMode(false);
            return;
        }
        if (intent == null) {
            throw new IllegalArgumentException("Input must have a valid intent");
        }
        if (item instanceof WorkspaceItemInfo) {
            WorkspaceItemInfo si = (WorkspaceItemInfo) item;
            if (si.hasStatusFlag(WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI)
                    && Intent.ACTION_VIEW.equals(intent.getAction())) {
                // make a copy of the intent that has the package set to null
                // we do this because the platform sometimes disables instant
                // apps temporarily (triggered by the user) and fallbacks to the
                // web ui. This only works though if the package isn't set
                intent = new Intent(intent);
                intent.setPackage(null);
            }
            if ((si.options & WorkspaceItemInfo.FLAG_START_FOR_RESULT) != 0) {
                launcher.startActivityForResult(item.getIntent(), 0);
                InstanceId instanceId = new InstanceIdSequence().newInstanceId();
                launcher.logAppLaunch(launcher.getStatsLogManager(), item, instanceId);
                return;
            }
        }
        if (v != null && launcher.supportsAdaptiveIconAnimation(v)
                && !item.shouldUseBackgroundAnimation()) {
            // Preload the icon to reduce latency b/w swapping the floating view with the original.
            FloatingIconView.fetchIcon(launcher, v, item, true /* isOpening */);
        }
        launcher.startActivitySafely(v, intent, item);
    }

    /**
     * Show a selection popup view with all markets installed for private space
     */
    private static boolean listPrivateSpaceMarketApps(Launcher launcher, UserHandle privateUser, View v, ItemInfo item) {
        LauncherApps launcherApps = launcher.getSystemService(LauncherApps.class);
        Intent marketIntent = new Intent(Intent.ACTION_VIEW).setData(Uri.parse("market://search?c=apps"));
        java.util.LinkedHashMap<String, LauncherActivityInfo> marketApps = new java.util.LinkedHashMap<>();
        // Find market handlers on main user that also exist in private profile
        for (var ri : launcher.getPackageManager().queryIntentActivities(marketIntent, 0)) {
            marketApps.computeIfAbsent(ri.activityInfo.packageName, pkg -> {
                List<LauncherActivityInfo> a = launcherApps.getActivityList(pkg, privateUser);
                return a.isEmpty() ? null : a.get(0);
            });
        }
        // Fallback: check known store packages directly in private profile
        for (String pkg : Constants.KNOWN_STORES) marketApps.computeIfAbsent(pkg, p -> {
            List<LauncherActivityInfo> a = launcherApps.getActivityList(p, privateUser);
            return a.isEmpty() ? null : a.get(0);
        });
        if (marketApps.isEmpty()) return false;
        /* else if (marketApps.size() == 1) {
            // Only one market; open directly (COMMENTED: prefer always asking)
            var only = marketApps.values().stream().findFirst().get();
            launcherApps.startMainActivity(only.getComponentName(), privateUser, null, launcher.getActivityLaunchOptions(v, item).toBundle());
            launcher.getStatsLogManager().logger().log(LAUNCHER_PRIVATE_SPACE_INSTALL_APP_BUTTON_TAP);
            return true;
        } */
        else {
            // Show native launcher-style options popup with store icons
            android.graphics.Rect pos = new android.graphics.Rect();
            launcher.getDragLayer().getDescendantRectRelativeToSelf(v, pos);
            pos.top += v.getPaddingTop();
            pos.left += v.getPaddingLeft();
            pos.right -= v.getPaddingRight();
            pos.bottom -= v.getPaddingBottom();
            android.graphics.RectF target = new android.graphics.RectF(pos);
            List<com.android.launcher3.views.OptionsPopupView.OptionItem> options = new java.util.ArrayList<>();
            for (LauncherActivityInfo lai : marketApps.values()) {
                options.add(new com.android.launcher3.views.OptionsPopupView.OptionItem(
                        lai.getLabel(),
                        lai.getBadgedIcon(0),
                        IGNORE,
                        view -> {
                            launcherApps.startMainActivity(lai.getComponentName(), privateUser, null, launcher.getActivityLaunchOptions(v, item).toBundle());
                            launcher.getStatsLogManager().logger().log(LAUNCHER_PRIVATE_SPACE_INSTALL_APP_BUTTON_TAP);
                            return true;
                        },
                        true /* useRawIcon */)
                );
            }
            com.android.launcher3.views.OptionsPopupView.show(launcher, target, options, true);
            return true;
        }
    }

    /**
     * Interface to indicate that an item will handle the click itself.
     */
    public interface ItemClickProxy {

        /**
         * Called when the item is clicked
         */
        void onItemClicked(View view);
    }
}
