/*
 * Copyright (C) 2013 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.base;

import static androidx.core.util.Preconditions.checkArgument;

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

import android.content.Context;
import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.DocumentsProvider;
import android.util.Log;

import com.android.documentsui.picker.LastAccessedProvider;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;

import javax.annotation.Nullable;

/**
 * Representation of a stack of {@link DocumentInfo}, usually the result of a
 * user-driven traversal.
 */
public class DocumentStack implements Durable, Parcelable {

    private static final String TAG = "DocumentStack";

    private static final int VERSION_INIT = 1;
    private static final int VERSION_ADD_ROOT = 2;

    private LinkedList<DocumentInfo> mList;
    private @Nullable RootInfo mRoot;

    private boolean mStackTouched;

    public DocumentStack() {
        mList = new LinkedList<>();
    }

    /**
     * Creates an instance, and pushes all docs to it in the same order as they're passed as
     * parameters, i.e. the last document will be at the top of the stack.
     */
    public DocumentStack(RootInfo root, DocumentInfo... docs) {
        mList = new LinkedList<>();
        for (int i = 0; i < docs.length; ++i) {
            mList.add(docs[i]);
        }

        mRoot = root;
    }

    /**
     * Same as {@link #DocumentStack(DocumentStack, DocumentInfo...)} except it takes a {@link List}
     * instead of an array.
     */
    public DocumentStack(RootInfo root, List<DocumentInfo> docs) {
        mList = new LinkedList<>(docs);
        mRoot = root;
    }

    /**
     * Makes a new copy, and pushes all docs to the new copy in the same order as they're
     * passed as parameters, i.e. the last document will be at the top of the stack.
     */
    public DocumentStack(DocumentStack src, DocumentInfo... docs) {
        mList = new LinkedList<>(src.mList);
        for (DocumentInfo doc : docs) {
            push(doc);
        }

        mStackTouched = false;
        mRoot = src.mRoot;
    }

    public boolean isInitialized() {
        return mRoot != null;
    }

    public @Nullable RootInfo getRoot() {
        return mRoot;
    }

    public boolean isEmpty() {
        return mList.isEmpty();
    }

    public int size() {
        return mList.size();
    }

    public DocumentInfo peek() {
        return mList.peekLast();
    }

    /**
     * Returns {@link DocumentInfo} at index counted from the bottom of this stack.
     */
    public DocumentInfo get(int index) {
        return mList.get(index);
    }

    public void push(DocumentInfo info) {
        checkArgument(!mList.contains(info));
        if (DEBUG) {
            Log.d(TAG, "Adding doc to stack: " + info);
        }
        mList.addLast(info);
        mStackTouched = true;
    }

    public DocumentInfo pop() {
        if (DEBUG) {
            Log.d(TAG, "Popping doc off stack.");
        }
        final DocumentInfo result = mList.removeLast();
        mStackTouched = true;

        return result;
    }

    public void popToRootDocument() {
        if (DEBUG) {
            Log.d(TAG, "Popping docs to root folder.");
        }
        while (mList.size() > 1) {
            mList.removeLast();
        }
        mStackTouched = true;
    }

    public void changeRoot(RootInfo root) {
        if (DEBUG) {
            Log.d(TAG, "Root changed to: " + root);
        }
        reset();
        mRoot = root;

        // Add this for keep stack size is 1 on recent root.
        if (root.isRecents()) {
            DocumentInfo rootRecent = new DocumentInfo();
            rootRecent.userId = root.userId;
            rootRecent.deriveFields();
            push(rootRecent);
        }
    }

    /** This will return true even when the initial location is set.
     * To get a read on if the user has changed something, use {@link #hasInitialLocationChanged()}.
     */
    public boolean hasLocationChanged() {
        return mStackTouched;
    }

    public String getTitle() {
        if (mList.size() == 1 && mRoot != null) {
            return mRoot.title;
        } else if (mList.size() > 1) {
            return peek().displayName;
        } else {
            return null;
        }
    }

    public boolean isRecents() {
        return mRoot != null && mRoot.isRecents() && size() == 1;
    }

    /**
     * Resets this stack to the given stack. It takes the reference of {@link #mList} and
     * {@link #mRoot} instead of making a copy.
     */
    public void reset(DocumentStack stack) {
        if (DEBUG) {
            Log.d(TAG, "Resetting the whole darn stack to: " + stack);
        }

        mList = stack.mList;
        mRoot = stack.mRoot;
        mStackTouched = true;
    }

    @Override
    public String toString() {
        return "DocumentStack{"
                + "root=" + mRoot
                + ", docStack=" + mList
                + ", stackTouched=" + mStackTouched
                + "}";
    }

    @Override
    public void reset() {
        mList.clear();
        mRoot = null;
    }

    private void updateRoot(Collection<RootInfo> matchingRoots) throws FileNotFoundException {
        for (RootInfo root : matchingRoots) {
            // RootInfo's equals() only checks authority and rootId, so this will update RootInfo if
            // its flag has changed.
            if (root.equals(this.mRoot)) {
                this.mRoot = root;
                return;
            }
        }
        throw new FileNotFoundException("Failed to find matching mRoot for " + mRoot);
    }

    /**
     * Update a possibly stale restored stack against a live
     * {@link DocumentsProvider}.
     */
    private void updateDocuments(Context context) throws FileNotFoundException {
        for (DocumentInfo info : mList) {
            info.updateSelf(info.userId.getContentResolver(context), info.userId);
        }
    }

    public static @Nullable DocumentStack fromLastAccessedCursor(
            Cursor cursor, Collection<RootInfo> matchingRoots, Context context)
            throws IOException {

        if (cursor.moveToFirst()) {
            DocumentStack stack = new DocumentStack();
            final byte[] rawStack = cursor.getBlob(
                    cursor.getColumnIndex(LastAccessedProvider.Columns.STACK));
            DurableUtils.readFromArray(rawStack, stack);

            stack.updateRoot(matchingRoots);
            stack.updateDocuments(context);

            return stack;
        }

        return null;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }

        if (!(o instanceof DocumentStack)) {
            return false;
        }

        DocumentStack other = (DocumentStack) o;
        return Objects.equals(mRoot, other.mRoot)
                && mList.equals(other.mList);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mRoot, mList);
    }

    @Override
    public void read(DataInputStream in) throws IOException {
        final int version = in.readInt();
        switch (version) {
            case VERSION_INIT:
                throw new ProtocolException("Ignored upgrade");
            case VERSION_ADD_ROOT:
                if (in.readBoolean()) {
                    mRoot = new RootInfo();
                    mRoot.read(in);
                }
                final int size = in.readInt();
                for (int i = 0; i < size; i++) {
                    final DocumentInfo doc = new DocumentInfo();
                    doc.read(in);
                    mList.add(doc);
                }
                mStackTouched = in.readInt() != 0;
                break;
            default:
                throw new ProtocolException("Unknown version " + version);
        }
    }

    @Override
    public void write(DataOutputStream out) throws IOException {
        out.writeInt(VERSION_ADD_ROOT);
        if (mRoot != null) {
            out.writeBoolean(true);
            mRoot.write(out);
        } else {
            out.writeBoolean(false);
        }
        final int size = mList.size();
        out.writeInt(size);
        for (int i = 0; i < size; i++) {
            final DocumentInfo doc = mList.get(i);
            doc.write(out);
        }
        out.writeInt(mStackTouched ? 1 : 0);
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        DurableUtils.writeToParcel(dest, this);
    }

    public static final Creator<DocumentStack> CREATOR = new Creator<DocumentStack>() {
        @Override
        public DocumentStack createFromParcel(Parcel in) {
            final DocumentStack stack = new DocumentStack();
            DurableUtils.readFromParcel(in, stack);
            return stack;
        }

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