/*
 * Copyright (C) 2016 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.documentsui.sorting;

import static com.android.documentsui.base.SharedMinimal.DEBUG;

import android.content.ContentResolver;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.DocumentsContract.Document;
import android.util.Log;
import android.util.SparseArray;
import android.view.View;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import com.android.documentsui.R;
import com.android.documentsui.base.Lookup;
import com.android.documentsui.sorting.SortDimension.SortDirection;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Consumer;

/**
 * Sort model that contains all columns and their sorting state.
 */
public class SortModel implements Parcelable {
    @IntDef({
            SORT_DIMENSION_ID_UNKNOWN,
            SORT_DIMENSION_ID_TITLE,
            SORT_DIMENSION_ID_SUMMARY,
            SORT_DIMENSION_ID_SIZE,
            SORT_DIMENSION_ID_FILE_TYPE,
            SORT_DIMENSION_ID_DATE
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface SortDimensionId {}
    public static final int SORT_DIMENSION_ID_UNKNOWN = 0;
    public static final int SORT_DIMENSION_ID_TITLE = android.R.id.title;
    public static final int SORT_DIMENSION_ID_SUMMARY = android.R.id.summary;
    public static final int SORT_DIMENSION_ID_SIZE = R.id.size;
    public static final int SORT_DIMENSION_ID_FILE_TYPE = R.id.file_type;
    public static final int SORT_DIMENSION_ID_DATE = R.id.date;

    @IntDef(flag = true, value = {
            UPDATE_TYPE_NONE,
            UPDATE_TYPE_UNSPECIFIED,
            UPDATE_TYPE_VISIBILITY,
            UPDATE_TYPE_SORTING
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface UpdateType {}
    /**
     * Default value for update type. Nothing is updated.
     */
    public static final int UPDATE_TYPE_NONE = 0;
    /**
     * Indicates the visibility of at least one dimension has changed.
     */
    public static final int UPDATE_TYPE_VISIBILITY = 1;
    /**
     * Indicates the sorting order has changed, either because the sorted dimension has changed or
     * the sort direction has changed.
     */
    public static final int UPDATE_TYPE_SORTING = 1 << 1;
    /**
     * Anything can be changed if the type is unspecified.
     */
    public static final int UPDATE_TYPE_UNSPECIFIED = -1;

    private static final String TAG = "SortModel";

    private final SparseArray<SortDimension> mDimensions;

    private transient final List<UpdateListener> mListeners;
    private transient Consumer<SortDimension> mMetricRecorder;

    private int mDefaultDimensionId = SORT_DIMENSION_ID_UNKNOWN;
    private boolean mIsUserSpecified = false;
    private @Nullable SortDimension mSortedDimension;

    @VisibleForTesting
    SortModel(Collection<SortDimension> columns) {
        mDimensions = new SparseArray<>(columns.size());

        for (SortDimension column : columns) {
            if (column.getId() == SORT_DIMENSION_ID_UNKNOWN) {
                throw new IllegalArgumentException(
                        "SortDimension id can't be " + SORT_DIMENSION_ID_UNKNOWN + ".");
            }
            if (mDimensions.get(column.getId()) != null) {
                throw new IllegalStateException(
                        "SortDimension id must be unique. Duplicate id: " + column.getId());
            }
            mDimensions.put(column.getId(), column);
        }

        mListeners = new ArrayList<>();
    }

    public int getSize() {
        return mDimensions.size();
    }

    public SortDimension getDimensionAt(int index) {
        return mDimensions.valueAt(index);
    }

    public @Nullable SortDimension getDimensionById(int id) {
        return mDimensions.get(id);
    }

    /**
     * Gets the sorted dimension id.
     * @return the sorted dimension id or {@link #SORT_DIMENSION_ID_UNKNOWN} if there is no sorted
     * dimension.
     */
    public int getSortedDimensionId() {
        return mSortedDimension != null ? mSortedDimension.getId() : SORT_DIMENSION_ID_UNKNOWN;
    }

    public @SortDirection int getCurrentSortDirection() {
        return mSortedDimension != null
                ? mSortedDimension.getSortDirection()
                : SortDimension.SORT_DIRECTION_NONE;
    }

    /**
     * Sort by the default direction of the given dimension if user has never specified any sort
     * direction before.
     * @param dimensionId the id of the dimension
     */
    public void setDefaultDimension(int dimensionId) {
        final boolean mayNeedSorting = (mDefaultDimensionId != dimensionId);

        mDefaultDimensionId = dimensionId;

        if (mayNeedSorting) {
            sortOnDefault();
        }
    }

    void setMetricRecorder(Consumer<SortDimension> metricRecorder) {
        mMetricRecorder = metricRecorder;
    }

    /**
     * Sort by given dimension and direction. Should only be used when user explicitly asks to sort
     * docs.
     * @param dimensionId the id of the dimension
     * @param direction the direction to sort docs in
     */
    public void sortByUser(int dimensionId, @SortDirection int direction) {
        SortDimension dimension = mDimensions.get(dimensionId);
        if (dimension == null) {
            throw new IllegalArgumentException("Unknown column id: " + dimensionId);
        }

        sortByDimension(dimension, direction);

        if (mMetricRecorder != null) {
            mMetricRecorder.accept(dimension);
        }

        mIsUserSpecified = true;
    }

    private void sortByDimension(
            SortDimension newSortedDimension, @SortDirection int direction) {
        if (newSortedDimension == mSortedDimension
                && mSortedDimension.mSortDirection == direction) {
            // Sort direction not changed, no need to proceed.
            return;
        }

        if ((newSortedDimension.getSortCapability() & direction) == 0) {
            throw new IllegalStateException(
                    "Dimension with id: " + newSortedDimension.getId()
                    + " can't be sorted in direction:" + direction);
        }

        switch (direction) {
            case SortDimension.SORT_DIRECTION_ASCENDING:
            case SortDimension.SORT_DIRECTION_DESCENDING:
                newSortedDimension.mSortDirection = direction;
                break;
            default:
                throw new IllegalArgumentException("Unknown sort direction: " + direction);
        }

        if (mSortedDimension != null && mSortedDimension != newSortedDimension) {
            mSortedDimension.mSortDirection = SortDimension.SORT_DIRECTION_NONE;
        }

        mSortedDimension = newSortedDimension;

        notifyListeners(UPDATE_TYPE_SORTING);
    }

    public void setDimensionVisibility(int columnId, int visibility) {
        assert(mDimensions.get(columnId) != null);

        mDimensions.get(columnId).mVisibility = visibility;

        notifyListeners(UPDATE_TYPE_VISIBILITY);
    }

    public Cursor sortCursor(Cursor cursor, Lookup<String, String> fileTypesMap) {
        if (mSortedDimension != null) {
            return new SortingCursorWrapper(cursor, mSortedDimension, fileTypesMap);
        } else {
            return cursor;
        }
    }

    public void addQuerySortArgs(Bundle queryArgs) {
        // should only be called when R.bool.feature_content_paging is true

        final int id = getSortedDimensionId();
        switch (id) {
            case SORT_DIMENSION_ID_UNKNOWN:
                return;
            case SortModel.SORT_DIMENSION_ID_TITLE:
                queryArgs.putStringArray(
                        ContentResolver.QUERY_ARG_SORT_COLUMNS,
                        new String[]{ Document.COLUMN_DISPLAY_NAME });
                break;
            case SortModel.SORT_DIMENSION_ID_DATE:
                queryArgs.putStringArray(
                        ContentResolver.QUERY_ARG_SORT_COLUMNS,
                        new String[]{ Document.COLUMN_LAST_MODIFIED });
                break;
            case SortModel.SORT_DIMENSION_ID_SIZE:
                queryArgs.putStringArray(
                        ContentResolver.QUERY_ARG_SORT_COLUMNS,
                        new String[]{ Document.COLUMN_SIZE });
                break;
            case SortModel.SORT_DIMENSION_ID_FILE_TYPE:
                // Unfortunately sorting by mime type is pretty much guaranteed different from
                // sorting by user-friendly type, so there is no point to guide the provider to sort
                // in a particular order.
                return;
            default:
                throw new IllegalStateException(
                        "Unexpected sort dimension id: " + id);
        }

        final SortDimension dimension = getDimensionById(id);
        switch (dimension.getSortDirection()) {
            case SortDimension.SORT_DIRECTION_ASCENDING:
                queryArgs.putInt(
                        ContentResolver.QUERY_ARG_SORT_DIRECTION,
                        ContentResolver.QUERY_SORT_DIRECTION_ASCENDING);
                break;
            case SortDimension.SORT_DIRECTION_DESCENDING:
                queryArgs.putInt(
                        ContentResolver.QUERY_ARG_SORT_DIRECTION,
                        ContentResolver.QUERY_SORT_DIRECTION_DESCENDING);
                break;
            default:
                throw new IllegalStateException(
                        "Unexpected sort direction: " + dimension.getSortDirection());
        }
    }

    public @Nullable String getDocumentSortQuery() {
        // This method should only be called when R.bool.feature_content_paging exists.
        // Once that feature is enabled by default (and reference removed), this method
        // should also be removed.
        // The following log message exists simply to make reference to
        // R.bool.feature_content_paging so that compiler will fail when value
        // is remove from config.xml.
        int readTheCommentAbove = R.bool.feature_content_paging;

        final int id = getSortedDimensionId();
        final String columnName;
        switch (id) {
            case SORT_DIMENSION_ID_UNKNOWN:
                return null;
            case SortModel.SORT_DIMENSION_ID_TITLE:
                columnName = Document.COLUMN_DISPLAY_NAME;
                break;
            case SortModel.SORT_DIMENSION_ID_DATE:
                columnName = Document.COLUMN_LAST_MODIFIED;
                break;
            case SortModel.SORT_DIMENSION_ID_SIZE:
                columnName = Document.COLUMN_SIZE;
                break;
            case SortModel.SORT_DIMENSION_ID_FILE_TYPE:
                // Unfortunately sorting by mime type is pretty much guaranteed different from
                // sorting by user-friendly type, so there is no point to guide the provider to sort
                // in a particular order.
                return null;
            default:
                throw new IllegalStateException(
                        "Unexpected sort dimension id: " + id);
        }

        final SortDimension dimension = getDimensionById(id);
        final String direction;
        switch (dimension.getSortDirection()) {
            case SortDimension.SORT_DIRECTION_ASCENDING:
                direction = " ASC";
                break;
            case SortDimension.SORT_DIRECTION_DESCENDING:
                direction = " DESC";
                break;
            default:
                throw new IllegalStateException(
                        "Unexpected sort direction: " + dimension.getSortDirection());
        }

        return columnName + direction;
    }

    private void notifyListeners(@UpdateType int updateType) {
        for (int i = mListeners.size() - 1; i >= 0; --i) {
            mListeners.get(i).onModelUpdate(this, updateType);
        }
    }

    public void addListener(UpdateListener listener) {
        mListeners.add(listener);
    }

    public void removeListener(UpdateListener listener) {
        mListeners.remove(listener);
    }

    /**
     * Sort by default dimension and direction if there is no history of user specifying a sort
     * order.
     */
    private void sortOnDefault() {
        if (!mIsUserSpecified) {
            SortDimension dimension = mDimensions.get(mDefaultDimensionId);
            if (dimension == null) {
                if (DEBUG) {
                    Log.d(TAG, "No default sort dimension.");
                }
                return;
            }

            sortByDimension(dimension, dimension.getDefaultSortDirection());
        }
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || !(o instanceof SortModel)) {
            return false;
        }

        if (this == o) {
            return true;
        }

        SortModel other = (SortModel) o;
        if (mDimensions.size() != other.mDimensions.size()) {
            return false;
        }
        for (int i = 0; i < mDimensions.size(); ++i) {
            final SortDimension dimension = mDimensions.valueAt(i);
            final int id = dimension.getId();
            if (!dimension.equals(other.getDimensionById(id))) {
                return false;
            }
        }

        return mDefaultDimensionId == other.mDefaultDimensionId
                && (mSortedDimension == other.mSortedDimension
                    || mSortedDimension.equals(other.mSortedDimension));
    }

    @Override
    public String toString() {
        return new StringBuilder()
                .append("SortModel{")
                .append("dimensions=").append(mDimensions)
                .append(", defaultDimensionId=").append(mDefaultDimensionId)
                .append(", sortedDimension=").append(mSortedDimension)
                .append("}")
                .toString();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel out, int flag) {
        out.writeInt(mDimensions.size());
        for (int i = 0; i < mDimensions.size(); ++i) {
            out.writeParcelable(mDimensions.valueAt(i), flag);
        }

        out.writeInt(mDefaultDimensionId);
        out.writeInt(getSortedDimensionId());
    }

    public static final Parcelable.Creator<SortModel> CREATOR =
            new Parcelable.Creator<SortModel>() {

        @Override
        public SortModel createFromParcel(Parcel in) {
            final int size = in.readInt();
            Collection<SortDimension> columns = new ArrayList<>(size);
            for (int i = 0; i < size; ++i) {
                columns.add(in.readParcelable(getClass().getClassLoader()));
            }
            SortModel model = new SortModel(columns);

            model.mDefaultDimensionId = in.readInt();
            model.mSortedDimension = model.getDimensionById(in.readInt());

            return model;
        }

        @Override
        public SortModel[] newArray(int size) {
            return new SortModel[size];
        }
    };

    /**
     * Creates a model for all other roots.
     *
     * TODO: move definition of columns into xml, and inflate model from it.
     */
    public static SortModel createModel() {
        List<SortDimension> dimensions = new ArrayList<>(4);
        SortDimension.Builder builder = new SortDimension.Builder();

        // Name column
        dimensions.add(builder
                .withId(SORT_DIMENSION_ID_TITLE)
                .withLabelId(R.string.sort_dimension_name)
                .withDataType(SortDimension.DATA_TYPE_STRING)
                .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
                .withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
                .withVisibility(View.VISIBLE)
                .build()
        );

        // Summary column
        // Summary is only visible in Downloads and Recents root.
        dimensions.add(builder
                .withId(SORT_DIMENSION_ID_SUMMARY)
                .withLabelId(R.string.sort_dimension_summary)
                .withDataType(SortDimension.DATA_TYPE_STRING)
                .withSortCapability(SortDimension.SORT_CAPABILITY_NONE)
                .withVisibility(View.INVISIBLE)
                .build()
        );

        // Size column
        dimensions.add(builder
                .withId(SORT_DIMENSION_ID_SIZE)
                .withLabelId(R.string.sort_dimension_size)
                .withDataType(SortDimension.DATA_TYPE_NUMBER)
                .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
                .withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
                .withVisibility(View.VISIBLE)
                .build()
        );

        // Type column
        dimensions.add(builder
            .withId(SORT_DIMENSION_ID_FILE_TYPE)
            .withLabelId(R.string.sort_dimension_file_type)
            .withDataType(SortDimension.DATA_TYPE_STRING)
            .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
            .withDefaultSortDirection(SortDimension.SORT_DIRECTION_ASCENDING)
            .withVisibility(View.VISIBLE)
            .build());

        // Date column
        dimensions.add(builder
                .withId(SORT_DIMENSION_ID_DATE)
                .withLabelId(R.string.sort_dimension_date)
                .withDataType(SortDimension.DATA_TYPE_NUMBER)
                .withSortCapability(SortDimension.SORT_CAPABILITY_BOTH_DIRECTION)
                .withDefaultSortDirection(SortDimension.SORT_DIRECTION_DESCENDING)
                .withVisibility(View.VISIBLE)
                .build()
        );

        return new SortModel(dimensions);
    }

    public interface UpdateListener {
        void onModelUpdate(SortModel newModel, @UpdateType int updateType);
    }
}
