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

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.appwidget.AppWidgetHostView;
import android.util.SparseIntArray;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RemoteViews.ColorResources;
import android.widget.RemoteViews.InteractionHandler;
import android.widget.RemoteViews.RemoteCollectionItems;

import java.util.stream.IntStream;

/**
 * List {@link Adapter} backed by a {@link RemoteCollectionItems}.
 *
 * @hide
 */
class RemoteCollectionItemsAdapter extends BaseAdapter {

    private final int mViewTypeCount;

    private RemoteCollectionItems mItems;
    private InteractionHandler mInteractionHandler;
    private ColorResources mColorResources;
    private boolean mOnLightBackground;
    private float mOriginalDensity;

    private SparseIntArray mLayoutIdToViewType;

    RemoteCollectionItemsAdapter(
            @NonNull RemoteCollectionItems items,
            @NonNull InteractionHandler interactionHandler,
            @NonNull ColorResources colorResources,
            boolean onLightBackground, float originalDensity) {
        // View type count can never increase after an adapter has been set on a ListView.
        // Additionally, decreasing it could inhibit view recycling if the count were to back and
        // forth between 3-2-3-2 for example. Therefore, the view type count, should be fixed for
        // the lifetime of the adapter.
        mViewTypeCount = items.getViewTypeCount();

        mItems = items;
        mInteractionHandler = interactionHandler;
        mColorResources = colorResources;
        mOnLightBackground = onLightBackground;
        mOriginalDensity = originalDensity;

        initLayoutIdToViewType();
    }

    /**
     * Updates the data for the adapter, allowing recycling of views. Note that if the view type
     * count has increased, a new adapter should be created and set on the AdapterView instead of
     * calling this method.
     */
    void setData(
            @NonNull RemoteCollectionItems items,
            @NonNull InteractionHandler interactionHandler,
            @NonNull ColorResources colorResources,
            boolean onLightBackground, float originalDensity) {
        if (mViewTypeCount < items.getViewTypeCount()) {
            throw new IllegalArgumentException(
                    "RemoteCollectionItemsAdapter cannot increase view type count after creation");
        }

        mItems = items;
        mInteractionHandler = interactionHandler;
        mColorResources = colorResources;
        mOnLightBackground = onLightBackground;
        mOriginalDensity = originalDensity;

        initLayoutIdToViewType();

        notifyDataSetChanged();
    }

    private void initLayoutIdToViewType() {
        SparseIntArray previousLayoutIdToViewType = mLayoutIdToViewType;
        mLayoutIdToViewType = new SparseIntArray(mViewTypeCount);

        int[] layoutIds = IntStream.range(0, mItems.getItemCount())
                .map(position -> mItems.getItemView(position).getLayoutId())
                .distinct()
                .toArray();
        if (layoutIds.length > mViewTypeCount) {
            throw new IllegalArgumentException(
                    "Collection items uses " + layoutIds.length + " distinct layouts, which is "
                            + "more than view type count of " + mViewTypeCount);
        }

        // Tracks whether a layout id (by index, not value) has been assigned a view type.
        boolean[] processedLayoutIdIndices = new boolean[layoutIds.length];
        // Tracks whether a view type has been assigned to a layout id already.
        boolean[] assignedViewTypes = new boolean[mViewTypeCount];

        if (previousLayoutIdToViewType != null) {
            for (int i = 0; i < layoutIds.length; i++) {
                int layoutId = layoutIds[i];
                // Copy over any previously used view types for layout ids in the collection to keep
                // view types stable across data updates.
                int previousViewType = previousLayoutIdToViewType.get(layoutId, -1);
                // Skip this layout id if it wasn't assigned to a view type previously.
                if (previousViewType < 0) continue;

                mLayoutIdToViewType.put(layoutId, previousViewType);
                processedLayoutIdIndices[i] = true;
                assignedViewTypes[previousViewType] = true;
            }
        }

        int lastViewType = -1;
        for (int i = 0; i < layoutIds.length; i++) {
            // If a view type has already been assigned to the layout id, skip it.
            if (processedLayoutIdIndices[i]) continue;

            int layoutId = layoutIds[i];
            // If no view type is assigned for the layout id, choose the next possible value that
            // isn't already assigned to a layout id. There is guaranteed to be some value available
            // due to the prior validation logic that count(distinct layout ids) <= viewTypeCount.
            int viewType = IntStream.range(lastViewType + 1, layoutIds.length)
                    .filter(type -> !assignedViewTypes[type])
                    .findFirst()
                    .orElseThrow(
                            () -> new IllegalStateException(
                                    "RemoteCollectionItems has more distinct layout ids than its "
                                            + "view type count"));
            mLayoutIdToViewType.put(layoutId, viewType);
            processedLayoutIdIndices[i] = true;
            assignedViewTypes[viewType] = true;
            lastViewType = viewType;
        }
    }

    @Override
    public int getCount() {
        return mItems.getItemCount();
    }

    @Override
    public RemoteViews getItem(int position) {
        return mItems.getItemView(position);
    }

    @Override
    public long getItemId(int position) {
        return mItems.getItemId(position);
    }

    @Override
    public int getItemViewType(int position) {
        return mLayoutIdToViewType.get(mItems.getItemView(position).getLayoutId());
    }

    @Override
    public int getViewTypeCount() {
        return mViewTypeCount;
    }

    @Override
    public boolean hasStableIds() {
        return mItems.hasStableIds();
    }

    @Nullable
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        if (position >= getCount()) return null;

        RemoteViews item = mItems.getItemView(position);
        item.addFlags(RemoteViews.FLAG_WIDGET_IS_COLLECTION_CHILD);
        item.setOriginalDensity(mOriginalDensity);

        AppWidgetHostView newView = convertView instanceof AppWidgetHostView.AdapterChildHostView
                widgetChildView
                ? widgetChildView
                : new AppWidgetHostView.AdapterChildHostView(parent.getContext());
        newView.setInteractionHandler(mInteractionHandler);
        newView.setColorResourcesNoReapply(mColorResources);
        newView.setOnLightBackground(mOnLightBackground);
        newView.updateAppWidget(item);
        return newView;
    }
}
