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

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

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;
import android.view.View;
import android.view.View.OnClickListener;

import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.VisibleForTesting;

import com.android.documentsui.DocumentsApplication;
import com.android.documentsui.ProviderExecutor;
import com.android.documentsui.R;
import com.android.documentsui.base.DocumentInfo;
import com.android.documentsui.base.Shared;
import com.android.documentsui.base.UserId;
import com.android.documentsui.inspector.actions.Action;
import com.android.documentsui.inspector.actions.ClearDefaultAppAction;
import com.android.documentsui.inspector.actions.ShowInProviderAction;
import com.android.documentsui.roots.ProvidersAccess;
import com.android.documentsui.ui.Snackbars;

import java.util.function.Consumer;
/**
 * A controller that coordinates retrieving document information and sending it to the view.
 */
public final class InspectorController {

    private final DataSupplier mLoader;
    private final HeaderDisplay mHeader;
    private final DetailsDisplay mDetails;
    private final MediaDisplay mMedia;
    private final ActionDisplay mShowProvider;
    private final ActionDisplay mAppDefaults;
    private final DebugDisplay mDebugView;
    private final Context mContext;
    private final PackageManager mPackageManager;
    private final ProvidersAccess mProviders;
    private final Runnable mErrorSnackbar;
    private final String mTitle;
    private final boolean mShowDebug;

    /**
     * InspectorControllerTest relies on this controller.
     */
    @VisibleForTesting
    public InspectorController(
            Context context,
            DataSupplier loader,
            PackageManager pm,
            ProvidersAccess providers,
            HeaderDisplay header,
            DetailsDisplay details,
            MediaDisplay media,
            ActionDisplay showProvider,
            ActionDisplay appDefaults,
            DebugDisplay debugView,
            String title,
            boolean showDebug,
            Runnable errorRunnable) {

        checkArgument(context != null);
        checkArgument(loader != null);
        checkArgument(pm != null);
        checkArgument(providers != null);
        checkArgument(header != null);
        checkArgument(details != null);
        checkArgument(media != null);
        checkArgument(showProvider != null);
        checkArgument(appDefaults != null);
        checkArgument(debugView != null);
        checkArgument(errorRunnable != null);

        mContext = context;
        mLoader = loader;
        mPackageManager = pm;
        mProviders = providers;
        mHeader = header;
        mDetails = details;
        mMedia = media;
        mShowProvider = showProvider;
        mAppDefaults = appDefaults;
        mTitle = title;
        mShowDebug = showDebug;
        mDebugView = debugView;

        mErrorSnackbar = errorRunnable;
    }

    /**
     * @param activity
     * @param loader
     * @param layout
     * @param args Bundle of arguments passed to our host {@link InspectorActivity}. These
     *     can include extras that enable debug mode ({@link Shared#EXTRA_SHOW_DEBUG}
     *     and override the file title (@link {@link Intent#EXTRA_TITLE}).
     */
    public InspectorController(Activity activity, DataSupplier loader, View layout,
            String title, boolean showDebug) {
        this(activity,
            loader,
            activity.getPackageManager(),
            DocumentsApplication.getProvidersCache (activity),
            (HeaderView) layout.findViewById(R.id.inspector_header_view),
            (DetailsView) layout.findViewById(R.id.inspector_details_view),
            (MediaView) layout.findViewById(R.id.inspector_media_view),
            (ActionDisplay) layout.findViewById(R.id.inspector_show_in_provider_view),
            (ActionDisplay) layout.findViewById(R.id.inspector_app_defaults_view),
            (DebugView) layout.findViewById(R.id.inspector_debug_view),
            title,
            showDebug,
            () -> {
                // using a runnable to support unit testing this feature.
                Snackbars.showInspectorError(activity);
            }
        );

        if (showDebug) {
            DebugView view = (DebugView) layout.findViewById(R.id.inspector_debug_view);
            view.init(ProviderExecutor::forAuthority);
        }
    }

    public void reset() {
        mLoader.reset();
    }

    public void loadInfo(Uri uri, UserId userId) {
        mLoader.loadDocInfo(uri, userId, this::updateView);
    }

    /**
     * Updates the view with documentInfo.
     */
    private void updateView(@Nullable DocumentInfo docInfo) {
        if (docInfo == null) {
            mErrorSnackbar.run();
        } else {
            mHeader.accept(docInfo);
            mDetails.accept(docInfo, mTitle != null ? mTitle : docInfo.displayName);

            if (docInfo.isDirectory()) {
                mLoader.loadDirCount(docInfo, this::displayChildCount);
            } else {

                mShowProvider.setVisible(docInfo.isSettingsSupported());
                if (docInfo.isSettingsSupported()) {
                    Action showProviderAction =
                        new ShowInProviderAction(mContext, mPackageManager, docInfo, mProviders);
                    mShowProvider.init(
                        showProviderAction,
                        (view) -> {
                            showInProvider(docInfo.derivedUri, UserId.DEFAULT_USER);
                        });
                }

                Action defaultAction =
                    new ClearDefaultAppAction(mContext, mPackageManager, docInfo);

                mAppDefaults.setVisible(defaultAction.canPerformAction());
            }

            if (docInfo.isMetadataSupported()) {
                mLoader.getDocumentMetadata(
                        docInfo.derivedUri,
                        docInfo.userId,
                        (Bundle bundle) -> {
                            onDocumentMetadataLoaded(docInfo, bundle);
                        });
            }
            mMedia.setVisible(!mMedia.isEmpty());

            if (mShowDebug) {
                mDebugView.accept(docInfo);
            }
            mDebugView.setVisible(mShowDebug && !mDebugView.isEmpty());
        }
    }

    private void onDocumentMetadataLoaded(DocumentInfo doc, @Nullable Bundle metadata) {
        if (metadata == null) {
            return;
        }

        Runnable geoClickListener = null;
        if (MetadataUtils.hasGeoCoordinates(metadata)) {
            float[] coords = MetadataUtils.getGeoCoordinates(metadata);
            final Intent intent = createGeoIntent(coords[0], coords[1], doc.displayName);
            if (hasHandler(intent)) {
                geoClickListener = () -> {
                    startActivity(intent);
                };
            }
        }

        mMedia.accept(doc, metadata, geoClickListener);

        if (mShowDebug) {
            mDebugView.accept(metadata);
        }
    }

    /**
     * Displays a directory's information to the view.
     *
     * @param count - number of items in the directory.
     */
    private void displayChildCount(Integer count) {
        mDetails.setChildrenCount(count);
    }

    private void startActivity(Intent intent) {
        assert hasHandler(intent);
        mContext.startActivity(intent);
    }

    /**
     * checks that we can handle a geo-intent.
     */
    private boolean hasHandler(Intent intent) {
        return mPackageManager.resolveActivity(intent, 0) != null;
    }

    /**
     * Creates a geo-intent for opening a location in maps.
     *
     * @see https://developer.android.com/guide/components/intents-common.html#Maps
     */
    private static Intent createGeoIntent(
            float latitude, float longitude, @Nullable String label) {
        label = Uri.encode(label == null ? "" : label);
        String data = "geo:0,0?q=" + latitude + " " + longitude + "(" + label + ")";
        Uri uri = Uri.parse(data);
        return new Intent(Intent.ACTION_VIEW, uri);
    }

    /**
     * Shows the selected document in it's content provider.
     *
     * @param DocumentInfo whose flag FLAG_SUPPORTS_SETTINGS is set.
     */
    public void showInProvider(Uri uri, UserId userId) {

        Intent intent = new Intent(DocumentsContract.ACTION_DOCUMENT_SETTINGS);
        intent.setPackage(mProviders.getPackageName(userId, uri.getAuthority()));
        intent.addCategory(Intent.CATEGORY_DEFAULT);
        intent.setData(uri);
        userId.startActivityAsUser(mContext, intent);
    }

    /**
     * Interface for loading all the various forms of document data. This primarily
     * allows us to easily supply test data in tests.
     */
    public interface DataSupplier {

        /**
         * Starts the Asynchronous process of loading file data.
         *
         * @param uri - A content uri to query metadata from.
         * @param userId - A user to load the uri from.
         * @param callback - Function to be called when the loader has finished loading metadata. A
         * DocumentInfo will be sent to this method. DocumentInfo may be null.
         */
        void loadDocInfo(Uri uri, UserId userId, Consumer<DocumentInfo> callback);

        /**
         * Loads a folders item count.
         * @param directory - a documentInfo thats a directory.
         * @param callback - Function to be called when the loader has finished loading the number
         * of children.
         */
        void loadDirCount(DocumentInfo directory, Consumer<Integer> callback);

        /**
         * Deletes all loader id's when android lifecycle ends.
         */
        void reset();

        /**
         * @param uri
         * @param callback
         */
        void getDocumentMetadata(Uri uri, UserId userId, Consumer<Bundle> callback);
    }

    /**
     * This interface is for unit testing.
     */
    public interface Display {
        /**
         * Makes the action visible.
         */
        void setVisible(boolean visible);
    }

    /**
     * This interface is for unit testing.
     */
    public interface ActionDisplay extends Display {

        /**
         * Initializes the view based on the action.
         * @param action - ClearDefaultAppAction or ShowInProviderAction
         * @param listener - listener for when the action is pressed.
         */
        void init(Action action, OnClickListener listener);

        void setActionHeader(String header);

        void setAppIcon(Drawable icon);

        void setAppName(String name);

        void showAction(boolean visible);
    }

    /**
     * Provides details about a file.
     */
    public interface HeaderDisplay {
        void accept(DocumentInfo info);
    }

    /**
     * Provides basic details about a file.
     */
    public interface DetailsDisplay {

        void accept(DocumentInfo info, String displayName);

        void setChildrenCount(int count);
    }

    /**
     * Provides details about a media file.
     */
    public interface MediaDisplay extends Display {
        void accept(DocumentInfo info, Bundle metadata, @Nullable Runnable geoClickListener);

        /**
         * Returns true if there are now rows in the display. Does not consider the title.
         */
        boolean isEmpty();
    }

    /**
     * Provides details about a media file.
     */
    public interface DebugDisplay extends Display {
        void accept(DocumentInfo info);
        void accept(Bundle metadata);

        /**
         * Returns true if there are now rows in the display. Does not consider the title.
         */
        boolean isEmpty();
    }

    /**
     * Displays a table of image metadata.
     */
    public interface TableDisplay extends Display {

        /**
         * Adds a row in the table.
         */
        void put(@StringRes int keyId, CharSequence value);

        /**
         * Adds a row in the table and makes it clickable.
         */
        void put(@StringRes int keyId, CharSequence value, OnClickListener callback);

        /**
         * Returns true if there are now rows in the display. Does not consider the title.
         */
        boolean isEmpty();
    }
}