/*
 * 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.car.carlauncher.recyclerview;

import static com.android.car.carlauncher.AppGridConstants.AppItemBoundDirection;

import android.content.Context;
import android.graphics.Rect;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.RecyclerView;

import com.android.car.carlauncher.AppGridFragment.Mode;
import com.android.car.carlauncher.AppGridPageSnapper;
import com.android.car.carlauncher.AppGridRecyclerView;
import com.android.car.carlauncher.AppItem;
import com.android.car.carlauncher.LauncherItem;
import com.android.car.carlauncher.LauncherItemDiffCallback;
import com.android.car.carlauncher.R;
import com.android.car.carlauncher.RecentAppsRowViewHolder;
import com.android.car.carlauncher.pagination.PageIndexingHelper;

import java.util.ArrayList;
import java.util.List;

/**
 * The adapter that populates the grid view with apps.
 */
public class AppGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    public static final int RECENT_APPS_TYPE = 1;
    public static final int APP_ITEM_TYPE = 2;

    private static final String TAG = "AppGridAdapter";
    private final Context mContext;
    private final LayoutInflater mInflater;
    private PageIndexingHelper mIndexingHelper;
    private final AppItemViewHolder.AppItemDragCallback mDragCallback;
    private final AppGridPageSnapper.AppGridPageSnapCallback mSnapCallback;
    private int mNumOfCols;
    private int mNumOfRows;
    private int mAppItemWidth;
    private int mAppItemHeight;
    // grid order of the mLauncherItems used by DiffUtils in dispatchUpdates to animate UI updates
    private final List<LauncherItem> mGridOrderedLauncherItems;

    private List<LauncherItem> mLauncherItems;
    private boolean mIsDistractionOptimizationRequired;
    private int mPageScrollDestination;
    // the global bounding rect of the app grid including margins (excluding page indicator bar)
    private Rect mPageBound;
    private Mode mAppGridMode;

    private AppGridAdapterListener mAppGridAdapterListener;

    public AppGridAdapter(Context context,
            AppItemViewHolder.AppItemDragCallback dragCallback,
            AppGridPageSnapper.AppGridPageSnapCallback snapCallback,
            AppGridAdapterListener appGridAdapterListener,
            Mode mode) {
        mContext = context;
        mInflater = LayoutInflater.from(context);
        mDragCallback = dragCallback;
        mSnapCallback = snapCallback;
        mGridOrderedLauncherItems = new ArrayList<>();
        mAppGridMode = mode;
        mAppGridAdapterListener = appGridAdapterListener;
    }

    @Override
    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
        if (!(recyclerView instanceof AppGridRecyclerView)) {
            throw new IllegalStateException(
                    "AppGridPageSnapper can only be used with AppGridRecyclerView.");
        }
        super.onAttachedToRecyclerView(recyclerView);
        mNumOfRows = ((AppGridRecyclerView) recyclerView).getNumOfRows();
        mNumOfCols = ((AppGridRecyclerView) recyclerView).getNumOfCols();
        mIndexingHelper = ((AppGridRecyclerView) recyclerView).getPageIndexingHelper();
        if (mIndexingHelper == null) {
            throw new IllegalStateException(
                    "AppGridRecyclerView's PageIndexingHelper is not initialized. "
                            + "Please ensure the adapter is attached to AppGridRecyclerView only "
                            + "after the bounds are ready.");
        }
    }

    /**
     * Updates the dimension measurements of the app items and app grid bounds.
     *
     * To dispatch the UI changes, the recyclerview needs to call {@link RecyclerView#setAdapter}
     * after calling this method to recreate the view holders.
     */
    public void updateViewHolderDimensions(Rect pageBound, int appItemWidth, int appItemHeight) {
        mPageBound = pageBound;
        mAppItemWidth = appItemWidth;
        mAppItemHeight = appItemHeight;
    }

    /**
     * Updates the current driving restriction to {@code isDistractionOptimizationRequired}, then
     * rebind the view holders.
     */
    public void setIsDistractionOptimizationRequired(boolean isDistractionOptimizationRequired) {
        mIsDistractionOptimizationRequired = isDistractionOptimizationRequired;
        // notifyDataSetChanged will rebind distraction optimization to all app items
        notifyDataSetChanged();
    }

    /**
     * Updates the current app grid mode to {@code mode}, then
     * rebind the view holders.
     */
    public void setMode(Mode mode) {
        mAppGridMode = mode;
        notifyDataSetChanged();
    }

    /**
     * Sets a new list of launcher items to be displayed in the app grid.
     * This should only be called by onChanged() in the observer as a response to data change in the
     * adapter's LauncherViewModel.
     */
    public void setLauncherItems(List<? extends LauncherItem> launcherItems) {
        mLauncherItems = (List<LauncherItem>) launcherItems;
        int newSnapPosition = mSnapCallback.getSnapPosition();
        if (newSnapPosition != 0 && newSnapPosition >= getItemCount()) {
            // in case user deletes the only app item on the last page, the page should snap to the
            // last icon on the second last page.
            mSnapCallback.notifySnapToPosition(getItemCount() - 1);
        }
        dispatchUpdates();
    }

    @Override
    public int getItemViewType(int position) {
        if (position == 0 && hasRecentlyUsedApps()) {
            return RECENT_APPS_TYPE;
        }
        return APP_ITEM_TYPE;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == RECENT_APPS_TYPE) {
            View view =
                    mInflater.inflate(R.layout.recent_apps_row, parent, /* attachToRoot= */ false);
            return new RecentAppsRowViewHolder(view, mContext);
        } else {
            View view = mInflater.inflate(R.layout.app_item, parent, /* attachToRoot= */ false);
            LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
                    mAppItemWidth, mAppItemHeight);
            view.setLayoutParams(layoutParams);
            return new AppItemViewHolder(view, mContext, mDragCallback, mSnapCallback);
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        AppItemViewHolder viewHolder = (AppItemViewHolder) holder;
        LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
                mAppItemWidth, mAppItemHeight);
        holder.itemView.setLayoutParams(layoutParams);

        AppItemViewHolder.BindInfo bindInfo = new AppItemViewHolder.BindInfo(
                mIsDistractionOptimizationRequired, mPageBound, mAppGridMode);
        int adapterIndex = mIndexingHelper.gridPositionToAdaptorIndex(position);
        if (adapterIndex >= mLauncherItems.size()) {
            // the current view holder is an empty item used to pad the last page.
            viewHolder.bind(null, bindInfo);
            return;
        }
        AppItem item = (AppItem) mLauncherItems.get(adapterIndex);
        viewHolder.bind(item.getAppMetaData(), bindInfo);
    }

    /**
     * Sets the layout direction of the indexing helper.
     */
    public void setLayoutDirection(int layoutDirection) {
        mIndexingHelper.setLayoutDirection(layoutDirection);
    }

    @Override
    public int getItemCount() {
        return getItemCountInternal(getLauncherItemsCount());
    }

    /** Returns the item count including padded spaces on the last page */
    private int getItemCountInternal(int unpaddedItemCount) {
        // item count should always be a multiple of block size to ensure pagination
        // is done properly. Extra spaces will have empty ViewHolders binded.
        float pageFraction = (float) unpaddedItemCount / (mNumOfCols * mNumOfRows);
        int pageCount = (int) Math.ceil(pageFraction);
        return pageCount * mNumOfCols * mNumOfRows;
    }

    public int getLauncherItemsCount() {
        return mLauncherItems == null ? 0 : mLauncherItems.size();
    }

    /**
     * Calculates the number of pages required to fit the all app items in the recycler view, with
     * minimum of 1 page when no items have been added to data model.
     */
    public int getPageCount() {
        return getPageCount(/* unpaddedItemCount */ getItemCount());
    }

    /**
     * Calculates the number of pages required to fit {@code unpaddedItemCount} number of app items.
     */
    public int getPageCount(int unpaddedItemCount) {
        int pageCount = getItemCountInternal(unpaddedItemCount) / (mNumOfRows * mNumOfCols);
        return Math.max(pageCount, 1);
    }

    /**
     * Return the offset bound direction of the given gridPosition.
     */
    @AppItemBoundDirection
    public int getOffsetBoundDirection(int gridPosition) {
        return mIndexingHelper.getOffsetBoundDirection(gridPosition);
    }


    private boolean hasRecentlyUsedApps() {
        // TODO (b/266988404): deprecate ui logic associated with recently used apps
        return false;
    }

    /**
     * Sets the cached drag start position to {@code gridPosition}.
     */
    public void setDragStartPoint(int gridPosition) {
        mPageScrollDestination = mIndexingHelper.roundToFirstIndexOnPage(gridPosition);
        mSnapCallback.notifySnapToPosition(mPageScrollDestination);
    }

    /**
     * The magical function that writes the new order to proto datastore.
     *
     * There should not be any calls to update RecyclerView, such as via notifyDatasetChanged in
     * this method since UI changes relating to data model should be handled by data observer.
     */
    public void moveAppItem(int gridPositionFrom, int gridPositionTo) {
        int adaptorIndexFrom = mIndexingHelper.gridPositionToAdaptorIndex(gridPositionFrom);
        int adaptorIndexTo = mIndexingHelper.gridPositionToAdaptorIndex(gridPositionTo);
        mPageScrollDestination = mIndexingHelper.roundToFirstIndexOnPage(gridPositionTo);
        mSnapCallback.notifySnapToPosition(mPageScrollDestination);

        // we need to move package to target index even if the from and to index are the same to
        // ensure dispatchLayout gets called to re-anchor the recyclerview to current page.
        AppItem selectedApp = (AppItem) mLauncherItems.get(adaptorIndexFrom);
        mAppGridAdapterListener.onAppPositionChanged(adaptorIndexTo, selectedApp);
    }


    /**
     * Updates page scroll destination after user has held the app item at the end of page for
     * longer than the scroll dispatch threshold.
     */
    public void updatePageScrollDestination(boolean scrollToNextPage) {
        int newDestination;
        int blockSize = mNumOfCols * mNumOfRows;
        if (scrollToNextPage) {
            newDestination = mPageScrollDestination + blockSize;
            mPageScrollDestination = (newDestination >= getItemCount()) ? mPageScrollDestination :
                    mIndexingHelper.roundToLastIndexOnPage(newDestination);
        } else {
            newDestination = mPageScrollDestination - blockSize;
            mPageScrollDestination = (newDestination < 0) ? mPageScrollDestination :
                    mIndexingHelper.roundToFirstIndexOnPage(newDestination);
        }
        mSnapCallback.notifySnapToPosition(mPageScrollDestination);
    }

    /**
     * Returns the last cached page scroll destination.
     */
    public int getPageScrollDestination() {
        return mPageScrollDestination;
    }

    /**
     * Dispatches the paged reordering animation using async list differ, based on
     * the current adapter order when the method is called.
     */
    private void dispatchUpdates() {
        List<LauncherItem> newAppsList = new ArrayList<>();
        // we first need to pad the empty items on the last page
        for (int i = 0; i < getItemCount(); i++) {
            newAppsList.add(getEmptyLauncherItem());
        }

        for (int i = 0; i < mLauncherItems.size(); i++) {
            newAppsList.set(mIndexingHelper.adaptorIndexToGridPosition(i), mLauncherItems.get(i));
        }
        LauncherItemDiffCallback callback = new LauncherItemDiffCallback(
                /* oldList */ mGridOrderedLauncherItems, /* newList */ newAppsList);
        DiffUtil.DiffResult result = DiffUtil.calculateDiff(callback);

        mGridOrderedLauncherItems.clear();
        mGridOrderedLauncherItems.addAll(newAppsList);
        result.dispatchUpdatesTo(this);
    }

    private LauncherItem getEmptyLauncherItem() {
        return new AppItem(/* packageName*/ "", /* className */ "", /* displayName */ "",
                /* appMetaData */ null);
    }

    /**
     * Returns the grid position of the next intended rotary focus view. This should follow the
     * same logical order as the adapter indexes.
     */
    public int getNextRotaryFocus(int focusedGridPosition, int direction) {
        int targetAdapterIndex = mIndexingHelper.gridPositionToAdaptorIndex(focusedGridPosition)
                + (direction == View.FOCUS_FORWARD ? 1 : -1);
        if (targetAdapterIndex < 0 || targetAdapterIndex >= getLauncherItemsCount()) {
            return focusedGridPosition;
        }
        return mIndexingHelper.adaptorIndexToGridPosition(targetAdapterIndex);
    }

    public interface AppGridAdapterListener {
        void onAppPositionChanged(int newPosition, AppItem appItem);
    }
}
