/*
 * Copyright (C) 2015 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.widget.picker;

import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_WIDGETSTRAY_APP_EXPANDED;
import static com.android.launcher3.recyclerview.ViewHolderBinder.POSITION_DEFAULT;
import static com.android.launcher3.recyclerview.ViewHolderBinder.POSITION_FIRST;
import static com.android.launcher3.recyclerview.ViewHolderBinder.POSITION_LAST;

import android.content.Context;
import android.graphics.Rect;
import android.os.Process;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.Adapter;
import androidx.recyclerview.widget.RecyclerView.LayoutParams;
import androidx.recyclerview.widget.RecyclerView.ViewHolder;

import com.android.launcher3.R;
import com.android.launcher3.icons.IconCache;
import com.android.launcher3.model.data.PackageItemInfo;
import com.android.launcher3.recyclerview.ViewHolderBinder;
import com.android.launcher3.util.LabelComparator;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.widget.model.WidgetListSpaceEntry;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
import com.android.launcher3.widget.model.WidgetsListContentEntry;
import com.android.launcher3.widget.model.WidgetsListHeaderEntry;
import com.android.launcher3.widget.model.WidgetsListSearchHeaderEntry;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.OptionalInt;
import java.util.function.IntSupplier;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * Recycler view adapter for the widget tray.
 *
 * <p>This adapter supports view binding of subclasses of {@link WidgetsListBaseEntry}. There are 2
 * subclasses: {@link WidgetsListHeader} & {@link WidgetsListContentEntry}.
 * {@link WidgetsListHeader} entries are always visible in the recycler view. At most one
 * {@link WidgetsListContentEntry} is shown in the recycler view at any time. Clicking a
 * {@link WidgetsListHeader} will result in expanding / collapsing a corresponding
 * {@link WidgetsListContentEntry} of the same app.
 */
public class WidgetsListAdapter extends Adapter<ViewHolder> implements OnHeaderClickListener {

    private static final String TAG = "WidgetsListAdapter";
    private static final boolean DEBUG = false;

    /** Uniquely identifies widgets list view type within the app. */
    private static final int VIEW_TYPE_WIDGETS_SPACE = R.id.view_type_widgets_space;
    private static final int VIEW_TYPE_WIDGETS_LIST = R.id.view_type_widgets_list;
    private static final int VIEW_TYPE_WIDGETS_HEADER = R.id.view_type_widgets_header;
    private static final int VIEW_TYPE_WIDGETS_SEARCH_HEADER = R.id.view_type_widgets_search_header;

    private final Context mContext;
    private final WidgetsDiffReporter mDiffReporter;
    private final SparseArray<ViewHolderBinder> mViewHolderBinders = new SparseArray<>();
    private final WidgetListBaseRowEntryComparator mRowComparator =
            new WidgetListBaseRowEntryComparator();

    private final List<WidgetsListBaseEntry> mAllEntries = new ArrayList<>();
    private ArrayList<WidgetsListBaseEntry> mVisibleEntries = new ArrayList<>();
    @Nullable private PackageUserKey mWidgetsContentVisiblePackageUserKey = null;

    private Predicate<WidgetsListBaseEntry> mHeaderAndSelectedContentFilter = entry ->
            entry instanceof WidgetsListHeaderEntry
                    || entry instanceof WidgetsListSearchHeaderEntry
                    || PackageUserKey.fromPackageItemInfo(entry.mPkgItem)
                            .equals(mWidgetsContentVisiblePackageUserKey);
    @Nullable private Predicate<WidgetsListBaseEntry> mFilter = null;
    @Nullable private RecyclerView mRecyclerView;
    @Nullable private PackageUserKey mPendingClickHeader;
    private final int mSpacingBetweenEntries;
    private int mMaxSpanSize = 4;

    public WidgetsListAdapter(Context context, LayoutInflater layoutInflater,
            IconCache iconCache, IntSupplier emptySpaceHeightProvider,
            OnClickListener iconClickListener, OnLongClickListener iconLongClickListener) {
        mContext = context;
        mDiffReporter = new WidgetsDiffReporter(iconCache, this);
        WidgetsListDrawableFactory listDrawableFactory = new WidgetsListDrawableFactory(context);

        mViewHolderBinders.put(
                VIEW_TYPE_WIDGETS_LIST,
                new WidgetsListTableViewHolderBinder(
                        layoutInflater, iconClickListener, iconLongClickListener,
                        listDrawableFactory));
        mViewHolderBinders.put(
                VIEW_TYPE_WIDGETS_HEADER,
                new WidgetsListHeaderViewHolderBinder(
                        layoutInflater,
                        /* onHeaderClickListener= */ this,
                        listDrawableFactory));
        mViewHolderBinders.put(
                VIEW_TYPE_WIDGETS_SEARCH_HEADER,
                new WidgetsListSearchHeaderViewHolderBinder(
                        layoutInflater,
                        /* onHeaderClickListener= */ this,
                        listDrawableFactory));
        mViewHolderBinders.put(
                VIEW_TYPE_WIDGETS_SPACE,
                new WidgetsSpaceViewHolderBinder(emptySpaceHeightProvider));
        mSpacingBetweenEntries =
                context.getResources().getDimensionPixelSize(R.dimen.widget_list_entry_spacing);
    }

    @Override
    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
        mRecyclerView = recyclerView;

        mRecyclerView.addItemDecoration(new RecyclerView.ItemDecoration() {
            @Override
            public void getItemOffsets(
                    @NonNull Rect outRect,
                    @NonNull View view,
                    @NonNull RecyclerView parent,
                    @NonNull RecyclerView.State state) {
                super.getItemOffsets(outRect, view, parent, state);
                int position = ((LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
                boolean isHeader =
                        view.getTag(R.id.tag_widget_entry) instanceof WidgetsListBaseEntry.Header;
                outRect.top += position > 0 && isHeader ? mSpacingBetweenEntries : 0;
            }
        });
    }

    @Override
    public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
        mRecyclerView = null;
    }

    public void setFilter(Predicate<WidgetsListBaseEntry> filter) {
        mFilter = filter;
    }

    @Override
    public int getItemCount() {
        return mVisibleEntries.size();
    }

    /**
     * Returns true if the adapter has entries which will be visible to the user
     */
    public boolean hasVisibleEntries() {
        // Account for the 1st space entry
        return getItemCount() > 1;
    }

    /** Returns all items that will be drawn in a recycler view. */
    public List<WidgetsListBaseEntry> getItems() {
        return mVisibleEntries;
    }

    /** Gets the section name for {@link com.android.launcher3.views.RecyclerViewFastScroller}. */
    public String getSectionName(int pos) {
        return mVisibleEntries.get(pos).mTitleSectionName;
    }

    /** Updates the widget list based on {@code tempEntries}. */
    public void setWidgets(List<WidgetsListBaseEntry> tempEntries) {
        mAllEntries.clear();
        mAllEntries.add(new WidgetListSpaceEntry());
        tempEntries.stream().sorted(mRowComparator).forEach(mAllEntries::add);
        if (shouldClearVisibleEntries()) {
            mVisibleEntries.clear();
        }
        updateVisibleEntries();
    }

    /** Updates the widget list based on {@code searchResults}. */
    public void setWidgetsOnSearch(List<WidgetsListBaseEntry> searchResults) {
        // Forget the expanded package every time widget list is refreshed in search mode.
        mWidgetsContentVisiblePackageUserKey = null;
        setWidgets(searchResults);
    }

    private void updateVisibleEntries() {
        // Get the current top of the header with the matching key before adjusting the visible
        // entries.
        OptionalInt previousPositionForPackageUserKey =
                getPositionForPackageUserKey(mPendingClickHeader);
        OptionalInt topForPackageUserKey =
                getOffsetForPosition(previousPositionForPackageUserKey);

        List<WidgetsListBaseEntry> newVisibleEntries = mAllEntries.stream()
                .filter(entry -> ((mFilter == null || mFilter.test(entry))
                        && mHeaderAndSelectedContentFilter.test(entry))
                        || entry instanceof WidgetListSpaceEntry)
                .map(entry -> {
                    if (entry instanceof WidgetsListBaseEntry.Header<?>
                            && matchesKey(entry, mWidgetsContentVisiblePackageUserKey)) {
                        // Adjust the original entries to expand headers for the selected content.
                        return ((WidgetsListBaseEntry.Header<?>) entry).withWidgetListShown();
                    } else if (entry instanceof WidgetsListContentEntry) {
                        // Adjust the original content entries to accommodate for the current
                        // maxSpanSize.
                        return ((WidgetsListContentEntry) entry).withMaxSpanSize(mMaxSpanSize);
                    }
                    return entry;
                })
                .collect(Collectors.toList());

        mDiffReporter.process(mVisibleEntries, newVisibleEntries, mRowComparator);

        if (mPendingClickHeader != null) {
            // Get the position for the clicked header after adjusting the visible entries. The
            // position may have changed if another header had previously been expanded.
            OptionalInt positionForPackageUserKey =
                    getPositionForPackageUserKey(mPendingClickHeader);
            scrollToPositionAndMaintainOffset(positionForPackageUserKey, topForPackageUserKey);
            mPendingClickHeader = null;
        }
    }


    /** Returns whether {@code entry} matches {@code key}. */
    private static boolean isHeaderForPackageUserKey(
            @NonNull WidgetsListBaseEntry entry, @Nullable PackageUserKey key) {
        return entry instanceof WidgetsListBaseEntry.Header && matchesKey(entry, key);
    }

    private static boolean matchesKey(@NonNull WidgetsListBaseEntry entry,
            @Nullable PackageUserKey key) {
        if (key == null) return false;
        return entry.mPkgItem.packageName.equals(key.mPackageName)
                && entry.mPkgItem.widgetCategory == key.mWidgetCategory
                && entry.mPkgItem.user.equals(key.mUser);
    }

    /**
     * Resets any expanded widget header.
     */
    public void resetExpandedHeader() {
        if (mWidgetsContentVisiblePackageUserKey != null) {
            mWidgetsContentVisiblePackageUserKey = null;
            updateVisibleEntries();
        }
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        onBindViewHolder(holder, position, Collections.EMPTY_LIST);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int pos, List<Object> payloads) {
        ViewHolderBinder viewHolderBinder = mViewHolderBinders.get(getItemViewType(pos));
        WidgetsListBaseEntry entry = mVisibleEntries.get(pos);

        // The first entry has an empty space, count from second entries.
        int listPos = (pos > 1) ? POSITION_DEFAULT : POSITION_FIRST;
        if (pos == (getItemCount() - 1)) {
            listPos |= POSITION_LAST;
        }
        viewHolderBinder.bindViewHolder(holder, mVisibleEntries.get(pos), listPos, payloads);
        holder.itemView.setTag(R.id.tag_widget_entry, entry);
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (DEBUG) {
            Log.v(TAG, "\nonCreateViewHolder");
        }

        return mViewHolderBinders.get(viewType).newViewHolder(parent);
    }

    @Override
    public void onViewRecycled(ViewHolder holder) {
        mViewHolderBinders.get(holder.getItemViewType()).unbindViewHolder(holder);
    }

    @Override
    public boolean onFailedToRecycleView(ViewHolder holder) {
        // If child views are animating, then the RecyclerView may choose not to recycle the view,
        // causing extraneous onCreateViewHolder() calls.  It is safe in this case to continue
        // recycling this view, and take care in onViewRecycled() to cancel any existing
        // animations.
        return true;
    }

    @Override
    public long getItemId(int pos) {
        return Arrays.hashCode(new Object[]{
                mVisibleEntries.get(pos).mPkgItem.hashCode(),
                getItemViewType(pos)});
    }

    @Override
    public int getItemViewType(int pos) {
        WidgetsListBaseEntry entry = mVisibleEntries.get(pos);
        if (entry instanceof WidgetsListContentEntry) {
            return VIEW_TYPE_WIDGETS_LIST;
        } else if (entry instanceof WidgetsListHeaderEntry) {
            return VIEW_TYPE_WIDGETS_HEADER;
        } else if (entry instanceof WidgetsListSearchHeaderEntry) {
            return VIEW_TYPE_WIDGETS_SEARCH_HEADER;
        } else if (entry instanceof WidgetListSpaceEntry) {
            return VIEW_TYPE_WIDGETS_SPACE;
        }
        throw new UnsupportedOperationException("ViewHolderBinder not found for " + entry);
    }

    @Override
    public void onHeaderClicked(boolean showWidgets, PackageUserKey packageUserKey) {
        // Ignore invalid clicks, such as collapsing a package that isn't currently expanded.
        if (!showWidgets && !packageUserKey.equals(mWidgetsContentVisiblePackageUserKey)) return;

        if (showWidgets) {
            mWidgetsContentVisiblePackageUserKey = packageUserKey;
            ActivityContext.lookupContext(mContext)
                    .getStatsLogManager().logger().log(LAUNCHER_WIDGETSTRAY_APP_EXPANDED);
        } else {
            mWidgetsContentVisiblePackageUserKey = null;
        }

        // Store the header that was clicked so that its position will be maintained the next time
        // we update the entries.
        mPendingClickHeader = packageUserKey;

        updateVisibleEntries();
    }

    /**
     * Returns the position of {@code key} in {@link #mVisibleEntries}, or  empty if it's not
     * present.
     */
    @NonNull
    private OptionalInt getPositionForPackageUserKey(@Nullable PackageUserKey key) {
        return IntStream.range(0, mVisibleEntries.size())
                .filter(index -> isHeaderForPackageUserKey(mVisibleEntries.get(index), key))
                .findFirst();
    }

    /**
     * Returns the top of {@code positionOptional} in the recycler view, or empty if its view
     * can't be found for any reason, including the position not being currently visible. The
     * returned value does not include the top padding of the recycler view.
     */
    private OptionalInt getOffsetForPosition(OptionalInt positionOptional) {
        if (!positionOptional.isPresent() || mRecyclerView == null) return OptionalInt.empty();

        RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
        if (layoutManager == null) return OptionalInt.empty();

        View view = layoutManager.findViewByPosition(positionOptional.getAsInt());
        if (view == null) return OptionalInt.empty();

        return OptionalInt.of(layoutManager.getDecoratedTop(view));
    }

    /**
     * Scrolls to the selected header position with the provided offset. LinearLayoutManager
     * scrolls the minimum distance necessary, so this will keep the selected header in place during
     * clicks, without interrupting the animation.
     *
     * @param positionOptional The position too scroll to. No scrolling will be done if empty.
     * @param offsetOptional The offset from the top to maintain. If empty, then the list will
     *                       scroll to the top of the position.
     */
    private void scrollToPositionAndMaintainOffset(
            OptionalInt positionOptional,
            OptionalInt offsetOptional) {
        if (!positionOptional.isPresent() || mRecyclerView == null) return;
        int position = positionOptional.getAsInt();

        LinearLayoutManager layoutManager = (LinearLayoutManager) mRecyclerView.getLayoutManager();
        if (layoutManager == null) return;

        if (position == mVisibleEntries.size() - 2
                && mVisibleEntries.get(mVisibleEntries.size() - 1)
                instanceof WidgetsListContentEntry) {
            // If the selected header is in the last position and its content is showing, then
            // scroll to the final position so the last list of widgets will show.
            layoutManager.scrollToPosition(mVisibleEntries.size() - 1);
            return;
        }

        // Scroll to the header view's current offset, accounting for the recycler view's padding.
        // If the header view couldn't be found, then it will appear at the top of the list.
        layoutManager.scrollToPositionWithOffset(
                position,
                offsetOptional.orElse(0) - mRecyclerView.getPaddingTop());
    }

    /**
     * Sets the max horizontal span in cells that is allowed for grouping more than one widget in a
     * table row.
     */
    public void setMaxHorizontalSpansPerRow(int maxHorizontalSpans) {
        mMaxSpanSize = maxHorizontalSpans;
        updateVisibleEntries();
    }

    /**
     * Returns {@code true} if there is a change in {@link #mAllEntries} that results in an
     * invalidation of {@link #mVisibleEntries}. e.g. there is change in the device language.
     */
    private boolean shouldClearVisibleEntries() {
        Map<PackageUserKey, PackageItemInfo> packagesInfo =
                mAllEntries.stream()
                        .filter(entry -> entry instanceof WidgetsListHeaderEntry)
                        .map(entry -> entry.mPkgItem)
                        .collect(Collectors.toMap(
                                entry -> PackageUserKey.fromPackageItemInfo(entry),
                                entry -> entry));
        for (WidgetsListBaseEntry visibleEntry: mVisibleEntries) {
            PackageUserKey key = PackageUserKey.fromPackageItemInfo(visibleEntry.mPkgItem);
            PackageItemInfo packageItemInfo = packagesInfo.get(key);
            if (packageItemInfo != null
                    && !visibleEntry.mPkgItem.title.equals(packageItemInfo.title)) {
                return true;
            }
        }
        return false;
    }

    /** Comparator for sorting WidgetListRowEntry based on package title. */
    public static class WidgetListBaseRowEntryComparator implements
            Comparator<WidgetsListBaseEntry> {

        private final LabelComparator mComparator = new LabelComparator();

        @Override
        public int compare(WidgetsListBaseEntry a, WidgetsListBaseEntry b) {
            int i = mComparator.compare(a.mPkgItem.title.toString(), b.mPkgItem.title.toString());
            if (i != 0) {
                return i;
            }
            // Prioritize entries from current user over other users if the entries are same.
            if (a.mPkgItem.user.equals(b.mPkgItem.user)) return 0;
            if (a.mPkgItem.user.equals(Process.myUserHandle())) return -1;
            return 1;
        }
    }
}
