/*
 * 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.documentsui;

import static android.content.ContentResolver.wrap;
import static android.provider.DocumentsContract.buildChildDocumentsUri;
import static android.provider.DocumentsContract.buildDocumentUri;
import static android.provider.DocumentsContract.buildRootsUri;

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

import static com.android.documentsui.base.DocumentInfo.getCursorString;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.fail;

import android.content.ContentProviderClient;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.FileUtils;
import android.os.ParcelFileDescriptor;
import android.os.ParcelFileDescriptor.AutoCloseInputStream;
import android.os.ParcelFileDescriptor.AutoCloseOutputStream;
import android.os.RemoteException;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.test.MoreAsserts;
import android.text.TextUtils;

import androidx.annotation.Nullable;

import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.RootInfo;
import com.android.documentsui.base.UserId;
import com.android.documentsui.roots.RootCursorWrapper;

import com.google.common.collect.Lists;

import libcore.io.Streams;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * Provides support for creation of documents in a test settings.
 */
public class DocumentsProviderHelper {

    private final UserId mUserId;
    private final String mAuthority;
    private final ContentProviderClient mClient;

    public DocumentsProviderHelper(UserId userId, String authority, Context context, String name) {
        checkArgument(!TextUtils.isEmpty(authority));
        mUserId = userId;
        mAuthority = authority;
        mClient = userId.getContentResolver(context).acquireContentProviderClient(name);
    }

    public RootInfo getRoot(String documentId) throws RemoteException {
        final Uri rootsUri = buildRootsUri(mAuthority);
        Cursor cursor = null;
        try {
            cursor = mClient.query(rootsUri, null, null, null, null);
            while (cursor.moveToNext()) {
                if (documentId.equals(getCursorString(cursor, Root.COLUMN_ROOT_ID))) {
                    return RootInfo.fromRootsCursor(mUserId, mAuthority, cursor);
                }
            }
            throw new IllegalArgumentException("Can't find matching root for id=" + documentId);
        } catch (Exception e) {
            throw new RuntimeException("Can't load root for id=" + documentId , e);
        } finally {
            FileUtils.closeQuietly(cursor);
        }
    }

    public Uri createDocument(Uri parentUri, String mimeType, String name) {
        if (name.contains("/")) {
            throw new IllegalArgumentException("Name and mimetype probably interposed.");
        }
        try {
            Uri uri = DocumentsContract.createDocument(wrap(mClient), parentUri, mimeType, name);
            return uri;
        } catch (FileNotFoundException e) {
            throw new RuntimeException("Couldn't create document: " + name + " with mimetype "
                    + mimeType, e);
        }
    }

    public Uri createDocument(String parentId, String mimeType, String name) {
        Uri parentUri = buildDocumentUri(mAuthority, parentId);
        return createDocument(parentUri, mimeType, name);
    }

    public Uri createDocument(RootInfo root, String mimeType, String name) {
        return createDocument(root.documentId, mimeType, name);
    }

    public Uri createDocumentWithFlags(String documentId, String mimeType, String name, int flags,
            String... streamTypes)
            throws RemoteException {
        Bundle in = new Bundle();
        in.putInt(StubProvider.EXTRA_FLAGS, flags);
        in.putString(StubProvider.EXTRA_PARENT_ID, documentId);
        in.putString(Document.COLUMN_MIME_TYPE, mimeType);
        in.putString(Document.COLUMN_DISPLAY_NAME, name);
        in.putStringArrayList(StubProvider.EXTRA_STREAM_TYPES, Lists.newArrayList(streamTypes));

        Bundle out = mClient.call("createDocumentWithFlags", null, in);
        Uri uri = out.getParcelable(DocumentsContract.EXTRA_URI);
        return uri;
    }

    public Uri createFolder(Uri parentUri, String name) {
        return createDocument(parentUri, Document.MIME_TYPE_DIR, name);
    }

    public Uri createFolder(String parentId, String name) {
        Uri parentUri = buildDocumentUri(mAuthority, parentId);
        return createDocument(parentUri, Document.MIME_TYPE_DIR, name);
    }

    public Uri createFolder(RootInfo root, String name) {
        return createDocument(root, Document.MIME_TYPE_DIR, name);
    }

    public void writeDocument(Uri documentUri, byte[] contents)
            throws RemoteException, IOException {
        ParcelFileDescriptor file = mClient.openFile(documentUri, "w", null);
        try (AutoCloseOutputStream out = new AutoCloseOutputStream(file)) {
            out.write(contents, 0, contents.length);
        }
        waitForWrite();
    }

    public void writeAppendDocument(Uri documentUri, byte[] contents, int length)
            throws RemoteException, IOException {
        ParcelFileDescriptor file = mClient.openFile(documentUri, "wa", null);
        try (AutoCloseOutputStream out = new AutoCloseOutputStream(file)) {
            out.write(contents, 0, length);
        }
        waitForWrite();
    }

    public void waitForWrite() throws RemoteException {
        mClient.call("waitForWrite", null, null);
    }

    public byte[] readDocument(Uri documentUri) throws RemoteException, IOException {
        ParcelFileDescriptor file = mClient.openFile(documentUri, "r", null);
        byte[] buf = null;
        try (AutoCloseInputStream in = new AutoCloseInputStream(file)) {
            buf = Streams.readFully(in);
        }
        return buf;
    }

    public void assertChildCount(Uri parentUri, int expected) throws Exception {
        List<DocumentInfo> children = listChildren(parentUri);
        assertEquals("Incorrect file count after copy", expected, children.size());
    }

    public void assertChildCount(String parentId, int expected) throws Exception {
        List<DocumentInfo> children = listChildren(parentId, -1);
        assertEquals("Incorrect file count after copy", expected, children.size());
    }

    public void assertChildCount(RootInfo root, int expected) throws Exception {
        assertChildCount(root.documentId, expected);
    }

    public void assertHasFile(Uri parentUri, String name) throws Exception {
        List<DocumentInfo> children = listChildren(parentUri);
        for (DocumentInfo child : children) {
            if (name.equals(child.displayName) && !child.isDirectory()) {
                return;
            }
        }
        fail("Could not find file named=" + name + " in children " + children);
    }

    public void assertHasFile(String parentId, String name) throws Exception {
        Uri parentUri = buildDocumentUri(mAuthority, parentId);
        assertHasFile(parentUri, name);
    }

    public void assertHasFile(RootInfo root, String name) throws Exception {
        assertHasFile(root.documentId, name);
    }

    public void assertHasDirectory(Uri parentUri, String name) throws Exception {
        List<DocumentInfo> children = listChildren(parentUri);
        for (DocumentInfo child : children) {
            if (name.equals(child.displayName) && child.isDirectory()) {
                return;
            }
        }
        fail("Could not find name=" + name + " in children " + children);
    }

    public void assertHasDirectory(String parentId, String name) throws Exception {
        Uri parentUri = buildDocumentUri(mAuthority, parentId);
        assertHasDirectory(parentUri, name);
    }

    public void assertHasDirectory(RootInfo root, String name) throws Exception {
        assertHasDirectory(root.documentId, name);
    }

    public void assertDoesNotExist(Uri parentUri, String name) throws Exception {
        List<DocumentInfo> children = listChildren(parentUri);
        for (DocumentInfo child : children) {
            if (name.equals(child.displayName)) {
                fail("Found name=" + name + " in children " + children);
            }
        }
    }

    public void assertDoesNotExist(String parentId, String name) throws Exception {
        Uri parentUri = buildDocumentUri(mAuthority, parentId);
        assertDoesNotExist(parentUri, name);
    }

    public void assertDoesNotExist(RootInfo root, String name) throws Exception {
        assertDoesNotExist(root.getUri(), name);
    }

    public @Nullable DocumentInfo findFile(String parentId, String name)
            throws Exception {
        List<DocumentInfo> children = listChildren(parentId);
        for (DocumentInfo child : children) {
            if (name.equals(child.displayName)) {
                return child;
            }
        }
        return null;
    }

    public DocumentInfo findDocument(String parentId, String name) throws Exception {
        List<DocumentInfo> children = listChildren(parentId);
        for (DocumentInfo child : children) {
            if (name.equals(child.displayName)) {
                return child;
            }
        }
        return null;
    }

    public DocumentInfo findDocument(Uri parentUri, String name) throws Exception {
        List<DocumentInfo> children = listChildren(parentUri);
        for (DocumentInfo child : children) {
            if (name.equals(child.displayName)) {
                return child;
            }
        }
        return null;
    }

    public List<DocumentInfo> listChildren(Uri parentUri) throws Exception {
        String id = DocumentsContract.getDocumentId(parentUri);
        return listChildren(id);
    }

    public List<DocumentInfo> listChildren(String documentId) throws Exception {
        return listChildren(documentId, 100);
    }

    public List<DocumentInfo> listChildren(Uri parentUri, int maxCount) throws Exception {
        String id = DocumentsContract.getDocumentId(parentUri);
        return listChildren(id, maxCount);
    }

    public List<DocumentInfo> listChildren(String documentId, int maxCount) throws Exception {
        Uri uri = buildChildDocumentsUri(mAuthority, documentId);
        List<DocumentInfo> children = new ArrayList<>();
        try (Cursor cursor = mClient.query(uri, null, null, null, null, null)) {
            Cursor wrapper = new RootCursorWrapper(mUserId, mAuthority, "totally-fake", cursor,
                    maxCount);
            while (wrapper.moveToNext()) {
                children.add(DocumentInfo.fromDirectoryCursor(wrapper));
            }
        }
        return children;
    }

    public void assertFileContents(Uri documentUri, byte[] expected) throws Exception {
        MoreAsserts.assertEquals(
                "Copied file contents differ",
                expected, readDocument(documentUri));
    }

    public void assertFileContents(String parentId, String fileName, byte[] expected)
            throws Exception {
        DocumentInfo file = findFile(parentId, fileName);
        assertNotNull(file);
        assertFileContents(file.derivedUri, expected);
    }

    /**
     * A helper method for StubProvider only. Won't work with other providers.
     * @throws RemoteException
     */
    public Uri createVirtualFile(
            RootInfo root, String path, String mimeType, byte[] content, String... streamTypes)
                    throws RemoteException {

        Bundle args = new Bundle();
        args.putString(StubProvider.EXTRA_ROOT, root.rootId);
        args.putString(StubProvider.EXTRA_PATH, path);
        args.putString(Document.COLUMN_MIME_TYPE, mimeType);
        args.putStringArrayList(StubProvider.EXTRA_STREAM_TYPES, Lists.newArrayList(streamTypes));
        args.putByteArray(StubProvider.EXTRA_CONTENT, content);

        Bundle result = mClient.call("createVirtualFile", null, args);
        String documentId = result.getString(Document.COLUMN_DOCUMENT_ID);

        return DocumentsContract.buildDocumentUri(mAuthority, documentId);
    }

    public void setLoadingDuration(long duration) throws RemoteException {
        final Bundle extra = new Bundle();
        extra.putLong(DocumentsContract.EXTRA_LOADING, duration);
        mClient.call("setLoadingDuration", null, extra);
    }

    public void configure(String args, Bundle configuration) throws RemoteException {
        mClient.call("configure", args, configuration);
    }

    public void simulateReadErrorsForFile(String args, Bundle configuration)
            throws RemoteException {
        mClient.call("simulateReadErrorsForFile", args, configuration);
    }

    public void clear(String args, Bundle configuration) throws RemoteException {
        mClient.call("clear", args, configuration);
    }

    public List<RootInfo> getRootList() throws RemoteException {
        List<RootInfo> list = new ArrayList<>();
        final Uri rootsUri = DocumentsContract.buildRootsUri(mAuthority);
        Cursor cursor = null;
        try {
            cursor = mClient.query(rootsUri, null, null, null, null);
            while (cursor.moveToNext()) {
                RootInfo rootInfo = RootInfo.fromRootsCursor(mUserId, mAuthority, cursor);
                if (rootInfo != null) {
                    list.add(rootInfo);
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("Can't load rootInfo list", e);
        } finally {
            FileUtils.closeQuietly(cursor);
        }
        return list;
    }

    public void cleanUp() {
        mClient.close();
    }
}
