/*
 * 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.graphics;

import static android.app.WallpaperManager.FLAG_SYSTEM;
import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;
import static android.view.View.VISIBLE;

import static com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
import static com.android.launcher3.model.ModelUtils.getMissingHotseatRanks;
import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially;

import android.annotation.TargetApi;
import android.app.Fragment;
import android.app.WallpaperColors;
import android.app.WallpaperManager;
import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.util.AttributeSet;
import android.util.SparseIntArray;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.TextClock;

import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Hotseat;
import com.android.launcher3.InsettableFrameLayout;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.Workspace;
import com.android.launcher3.WorkspaceLayoutManager;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.icons.BaseIconFactory;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.WidgetsModel;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pm.InstallSessionHelper;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.uioverrides.PredictedAppIconInflater;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.MainThreadInitializedObject.SandboxContext;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
import com.android.launcher3.widget.BaseLauncherAppWidgetHostView;
import com.android.launcher3.widget.LauncherAppWidgetHost;
import com.android.launcher3.widget.LauncherAppWidgetProviderInfo;
import com.android.launcher3.widget.LocalColorExtractor;
import com.android.launcher3.widget.NavigableAppWidgetHostView;
import com.android.launcher3.widget.custom.CustomWidgetManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * Utility class for generating the preview of Launcher for a given InvariantDeviceProfile.
 * Steps:
 *   1) Create a dummy icon info with just white icon
 *   2) Inflate a strip down layout definition for Launcher
 *   3) Place appropriate elements like icons and first-page qsb
 *   4) Measure and draw the view on a canvas
 */
@TargetApi(Build.VERSION_CODES.O)
public class LauncherPreviewRenderer extends ContextWrapper
        implements ActivityContext, WorkspaceLayoutManager, LayoutInflater.Factory2 {

    /**
     * Context used just for preview. It also provides a few objects (e.g. UserCache) just for
     * preview purposes.
     */
    public static class PreviewContext extends SandboxContext {

        private final InvariantDeviceProfile mIdp;
        private final ConcurrentLinkedQueue<LauncherIconsForPreview> mIconPool =
                new ConcurrentLinkedQueue<>();

        public PreviewContext(Context base, InvariantDeviceProfile idp) {
            super(base, UserCache.INSTANCE, InstallSessionHelper.INSTANCE,
                    LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE,
                    CustomWidgetManager.INSTANCE, PluginManagerWrapper.INSTANCE);
            mIdp = idp;
            mObjectMap.put(InvariantDeviceProfile.INSTANCE, idp);
            mObjectMap.put(LauncherAppState.INSTANCE,
                    new LauncherAppState(this, null /* iconCacheFileName */));

        }

        public LauncherIcons newLauncherIcons(Context context, boolean shapeDetection) {
            LauncherIconsForPreview launcherIconsForPreview = mIconPool.poll();
            if (launcherIconsForPreview != null) {
                return launcherIconsForPreview;
            }
            return new LauncherIconsForPreview(context, mIdp.fillResIconDpi, mIdp.iconBitmapSize,
                    -1 /* poolId */, shapeDetection);
        }

        private final class LauncherIconsForPreview extends LauncherIcons {

            private LauncherIconsForPreview(Context context, int fillResIconDpi, int iconBitmapSize,
                    int poolId, boolean shapeDetection) {
                super(context, fillResIconDpi, iconBitmapSize, poolId, shapeDetection);
            }

            @Override
            public void recycle() {
                // Clear any temporary state variables
                clear();
                mIconPool.offer(this);
            }
        }
    }

    private final Handler mUiHandler;
    private final Context mContext;
    private final InvariantDeviceProfile mIdp;
    private final DeviceProfile mDp;
    private final Rect mInsets;
    private final WorkspaceItemInfo mWorkspaceItemInfo;
    private final LayoutInflater mHomeElementInflater;
    private final InsettableFrameLayout mRootView;
    private final Hotseat mHotseat;
    private final Map<Integer, CellLayout> mWorkspaceScreens = new HashMap<>();
    private final AppWidgetHost mAppWidgetHost;
    private final SparseIntArray mWallpaperColorResources;

    public LauncherPreviewRenderer(Context context,
            InvariantDeviceProfile idp,
            WallpaperColors wallpaperColorsOverride) {

        super(context);
        mUiHandler = new Handler(Looper.getMainLooper());
        mContext = context;
        mIdp = idp;
        mDp = idp.getDeviceProfile(context).copy(context);

        if (Utilities.ATLEAST_R) {
            WindowInsets currentWindowInsets = context.getSystemService(WindowManager.class)
                    .getCurrentWindowMetrics().getWindowInsets();
            mInsets = new Rect(
                    currentWindowInsets.getSystemWindowInsetLeft(),
                    currentWindowInsets.getSystemWindowInsetTop(),
                    currentWindowInsets.getSystemWindowInsetRight(),
                    currentWindowInsets.getSystemWindowInsetBottom());
        } else {
            mInsets = new Rect();
            mInsets.left = mInsets.right = (mDp.widthPx - mDp.availableWidthPx) / 2;
            mInsets.top = mInsets.bottom = (mDp.heightPx - mDp.availableHeightPx) / 2;
        }
        mDp.updateInsets(mInsets);

        BaseIconFactory iconFactory =
                new BaseIconFactory(context, mIdp.fillResIconDpi, mIdp.iconBitmapSize) { };
        BitmapInfo iconInfo = iconFactory.createBadgedIconBitmap(new AdaptiveIconDrawable(
                        new ColorDrawable(Color.WHITE), new ColorDrawable(Color.WHITE)),
                Process.myUserHandle(),
                Build.VERSION.SDK_INT);

        mWorkspaceItemInfo = new WorkspaceItemInfo();
        mWorkspaceItemInfo.bitmap = iconInfo;
        mWorkspaceItemInfo.intent = new Intent();
        mWorkspaceItemInfo.contentDescription = mWorkspaceItemInfo.title =
                context.getString(R.string.label_application);

        mHomeElementInflater = LayoutInflater.from(
                new ContextThemeWrapper(this, R.style.HomeScreenElementTheme));
        mHomeElementInflater.setFactory2(this);

        int layoutRes = mDp.isTwoPanels ? R.layout.launcher_preview_two_panel_layout
                : R.layout.launcher_preview_layout;
        mRootView = (InsettableFrameLayout) mHomeElementInflater.inflate(
                layoutRes, null, false);
        mRootView.setInsets(mInsets);
        measureView(mRootView, mDp.widthPx, mDp.heightPx);

        mHotseat = mRootView.findViewById(R.id.hotseat);
        mHotseat.resetLayout(false);

        CellLayout firstScreen = mRootView.findViewById(R.id.workspace);
        firstScreen.setPadding(mDp.workspacePadding.left + mDp.cellLayoutPaddingLeftRightPx,
                mDp.workspacePadding.top,
                (mDp.isTwoPanels ? mDp.cellLayoutBorderSpacePx.x / 2
                        : mDp.workspacePadding.right) + mDp.cellLayoutPaddingLeftRightPx,
                mDp.workspacePadding.bottom
        );
        mWorkspaceScreens.put(FIRST_SCREEN_ID, firstScreen);

        if (mDp.isTwoPanels) {
            CellLayout rightPanel = mRootView.findViewById(R.id.workspace_right);
            rightPanel.setPadding(
                    mDp.cellLayoutBorderSpacePx.x / 2 + mDp.cellLayoutPaddingLeftRightPx,
                    mDp.workspacePadding.top,
                    mDp.workspacePadding.right + mDp.cellLayoutPaddingLeftRightPx,
                    mDp.workspacePadding.bottom
            );
            mWorkspaceScreens.put(Workspace.SECOND_SCREEN_ID, rightPanel);
        }

        if (Utilities.ATLEAST_S) {
            WallpaperColors wallpaperColors = wallpaperColorsOverride != null
                    ? wallpaperColorsOverride
                    : WallpaperManager.getInstance(context).getWallpaperColors(FLAG_SYSTEM);
            mWallpaperColorResources = wallpaperColors != null ? LocalColorExtractor.newInstance(
                    context).generateColorsOverride(wallpaperColors) : null;
        } else {
            mWallpaperColorResources = null;
        }
        mAppWidgetHost = FeatureFlags.WIDGETS_IN_LAUNCHER_PREVIEW.get()
                ? new LauncherPreviewAppWidgetHost(context)
                : null;
    }

    /** Populate preview and render it. */
    public View getRenderedView(BgDataModel dataModel,
            Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
        populate(dataModel, widgetProviderInfoMap);
        return mRootView;
    }

    @Override
    public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        if ("TextClock".equals(name)) {
            // Workaround for TextClock accessing handler for unregistering ticker.
            return new TextClock(context, attrs) {

                @Override
                public Handler getHandler() {
                    return mUiHandler;
                }
            };
        } else if (!"fragment".equals(name)) {
            return null;
        }

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PreviewFragment);
        FragmentWithPreview f = (FragmentWithPreview) Fragment.instantiate(
                context, ta.getString(R.styleable.PreviewFragment_android_name));
        f.enterPreviewMode(context);
        f.onInit(null);

        View view = f.onCreateView(LayoutInflater.from(context), (ViewGroup) parent, null);
        view.setId(ta.getInt(R.styleable.PreviewFragment_android_id, View.NO_ID));
        return view;
    }

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
        return onCreateView(null, name, context, attrs);
    }

    @Override
    public BaseDragLayer getDragLayer() {
        throw new UnsupportedOperationException();
    }

    @Override
    public DeviceProfile getDeviceProfile() {
        return mDp;
    }

    @Override
    public Hotseat getHotseat() {
        return mHotseat;
    }

    @Override
    public CellLayout getScreenWithId(int screenId) {
        return mWorkspaceScreens.get(screenId);
    }

    private void inflateAndAddIcon(WorkspaceItemInfo info) {
        CellLayout screen = mWorkspaceScreens.get(info.screenId);
        BubbleTextView icon = (BubbleTextView) mHomeElementInflater.inflate(
                R.layout.app_icon, screen, false);
        icon.applyFromWorkspaceItem(info);
        addInScreenFromBind(icon, info);
    }

    private void inflateAndAddFolder(FolderInfo info) {
        CellLayout screen = info.container == Favorites.CONTAINER_DESKTOP
                ? mWorkspaceScreens.get(info.screenId)
                : mHotseat;
        FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, screen,
                info);
        addInScreenFromBind(folderIcon, info);
    }

    private void inflateAndAddWidgets(
            LauncherAppWidgetInfo info,
            Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
        if (widgetProviderInfoMap == null) {
            return;
        }
        AppWidgetProviderInfo providerInfo = widgetProviderInfoMap.get(
                new ComponentKey(info.providerName, info.user));
        if (providerInfo == null) {
            return;
        }
        inflateAndAddWidgets(info, LauncherAppWidgetProviderInfo.fromProviderInfo(
                getApplicationContext(), providerInfo));
    }

    private void inflateAndAddWidgets(LauncherAppWidgetInfo info, WidgetsModel widgetsModel) {
        WidgetItem widgetItem = widgetsModel.getWidgetProviderInfoByProviderName(
                info.providerName, info.user);
        if (widgetItem == null) {
            return;
        }
        inflateAndAddWidgets(info, widgetItem.widgetInfo);
    }

    private void inflateAndAddWidgets(
            LauncherAppWidgetInfo info, LauncherAppWidgetProviderInfo providerInfo) {
        AppWidgetHostView view;
        if (FeatureFlags.WIDGETS_IN_LAUNCHER_PREVIEW.get()) {
            view = mAppWidgetHost.createView(mContext, info.appWidgetId, providerInfo);
        } else {
            view = new NavigableAppWidgetHostView(this) {
                @Override
                protected boolean shouldAllowDirectClick() {
                    return false;
                }
            };
            view.setAppWidget(-1, providerInfo);
            view.updateAppWidget(null);
        }

        if (mWallpaperColorResources != null) {
            view.setColorResources(mWallpaperColorResources);
        }

        view.setTag(info);
        addInScreenFromBind(view, info);
    }

    private void inflateAndAddPredictedIcon(WorkspaceItemInfo info) {
        CellLayout screen = mWorkspaceScreens.get(info.screenId);
        View view = PredictedAppIconInflater.inflate(mHomeElementInflater, screen, info);
        if (view != null) {
            addInScreenFromBind(view, info);
        }
    }

    private void dispatchVisibilityAggregated(View view, boolean isVisible) {
        // Similar to View.dispatchVisibilityAggregated implementation.
        final boolean thisVisible = view.getVisibility() == VISIBLE;
        if (thisVisible || !isVisible) {
            view.onVisibilityAggregated(isVisible);
        }

        if (view instanceof ViewGroup) {
            isVisible = thisVisible && isVisible;
            ViewGroup vg = (ViewGroup) view;
            int count = vg.getChildCount();

            for (int i = 0; i < count; i++) {
                dispatchVisibilityAggregated(vg.getChildAt(i), isVisible);
            }
        }
    }

    private void populate(BgDataModel dataModel,
            Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
        // Separate the items that are on the current screen, and the other remaining items.
        ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
        ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
        ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
        ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();

        IntSet currentScreenIds = IntSet.wrap(mWorkspaceScreens.keySet());
        filterCurrentWorkspaceItems(currentScreenIds, dataModel.workspaceItems,
                currentWorkspaceItems, otherWorkspaceItems);
        filterCurrentWorkspaceItems(currentScreenIds, dataModel.appWidgets, currentAppWidgets,
                otherAppWidgets);

        sortWorkspaceItemsSpatially(mIdp, currentWorkspaceItems);
        for (ItemInfo itemInfo : currentWorkspaceItems) {
            switch (itemInfo.itemType) {
                case Favorites.ITEM_TYPE_APPLICATION:
                case Favorites.ITEM_TYPE_SHORTCUT:
                case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
                    inflateAndAddIcon((WorkspaceItemInfo) itemInfo);
                    break;
                case Favorites.ITEM_TYPE_FOLDER:
                    inflateAndAddFolder((FolderInfo) itemInfo);
                    break;
                default:
                    break;
            }
        }
        for (ItemInfo itemInfo : currentAppWidgets) {
            switch (itemInfo.itemType) {
                case Favorites.ITEM_TYPE_APPWIDGET:
                case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
                    if (widgetProviderInfoMap != null) {
                        inflateAndAddWidgets(
                                (LauncherAppWidgetInfo) itemInfo, widgetProviderInfoMap);
                    } else {
                        inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo,
                                dataModel.widgetsModel);
                    }
                    break;
                default:
                    break;
            }
        }
        IntArray ranks = getMissingHotseatRanks(currentWorkspaceItems,
                mDp.numShownHotseatIcons);
        FixedContainerItems hotseatpredictions =
                dataModel.extraItems.get(CONTAINER_HOTSEAT_PREDICTION);
        List<ItemInfo> predictions = hotseatpredictions == null
                ? Collections.emptyList() : hotseatpredictions.items;
        int count = Math.min(ranks.size(), predictions.size());
        for (int i = 0; i < count; i++) {
            int rank = ranks.get(i);
            WorkspaceItemInfo itemInfo =
                    new WorkspaceItemInfo((WorkspaceItemInfo) predictions.get(i));
            itemInfo.container = CONTAINER_HOTSEAT_PREDICTION;
            itemInfo.rank = rank;
            itemInfo.cellX = mHotseat.getCellXFromOrder(rank);
            itemInfo.cellY = mHotseat.getCellYFromOrder(rank);
            itemInfo.screenId = rank;
            inflateAndAddPredictedIcon(itemInfo);
        }

        // Add first page QSB
        if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
            CellLayout firstScreen = mWorkspaceScreens.get(FIRST_SCREEN_ID);
            View qsb = mHomeElementInflater.inflate(R.layout.qsb_preview, firstScreen,
                    false);
            CellLayout.LayoutParams lp =
                    new CellLayout.LayoutParams(0, 0, firstScreen.getCountX(), 1);
            lp.canReorder = false;
            firstScreen.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true);
        }

        measureView(mRootView, mDp.widthPx, mDp.heightPx);
        dispatchVisibilityAggregated(mRootView, true);
        measureView(mRootView, mDp.widthPx, mDp.heightPx);
        // Additional measure for views which use auto text size API
        measureView(mRootView, mDp.widthPx, mDp.heightPx);
    }

    private static void measureView(View view, int width, int height) {
        view.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
        view.layout(0, 0, width, height);
    }

    private class LauncherPreviewAppWidgetHost extends AppWidgetHost {

        private LauncherPreviewAppWidgetHost(Context context) {
            super(context, LauncherAppWidgetHost.APPWIDGET_HOST_ID);
        }

        @Override
        protected AppWidgetHostView onCreateView(
                Context context,
                int appWidgetId,
                AppWidgetProviderInfo appWidget) {
            return new LauncherPreviewAppWidgetHostView(LauncherPreviewRenderer.this);
        }
    }

    private static class LauncherPreviewAppWidgetHostView extends BaseLauncherAppWidgetHostView {
        private LauncherPreviewAppWidgetHostView(Context context) {
            super(context);
        }

        @Override
        protected boolean shouldAllowDirectClick() {
            return false;
        }
    }

    /** Root layout for launcher preview that intercepts all touch events. */
    public static class LauncherPreviewLayout extends InsettableFrameLayout {
        public LauncherPreviewLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            return true;
        }
    }
}
