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

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

import android.app.Activity;
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledAfter;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Looper;
import android.os.Process;
import android.provider.DocumentsContract;
import android.provider.Settings;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;

import androidx.annotation.NonNull;
import androidx.annotation.PluralsRes;
import androidx.appcompat.app.AlertDialog;

import com.android.documentsui.R;
import com.android.documentsui.ui.MessageBuilder;
import com.android.documentsui.util.VersionUtils;

import java.text.Collator;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;

import javax.annotation.Nullable;

/** @hide */
public final class Shared {

    /** Intent action name to pick a copy destination. */
    public static final String ACTION_PICK_COPY_DESTINATION =
            "com.android.documentsui.PICK_COPY_DESTINATION";

    // These values track values declared in MediaDocumentsProvider.
    public static final String METADATA_KEY_AUDIO = "android.media.metadata.audio";
    public static final String METADATA_KEY_VIDEO = "android.media.metadata.video";
    public static final String METADATA_VIDEO_LATITUDE = "android.media.metadata.video:latitude";
    public static final String METADATA_VIDEO_LONGITUTE = "android.media.metadata.video:longitude";

    /**
     * Extra flag used to store the current stack so user opens in right spot.
     */
    public static final String EXTRA_STACK = "com.android.documentsui.STACK";

    /**
     * Extra flag used to store query of type String in the bundle.
     */
    public static final String EXTRA_QUERY = "query";

    /**
     * Extra flag used to store chip's title of type String array in the bundle.
     */
    public static final String EXTRA_QUERY_CHIPS = "query_chips";

    /**
     * Extra flag used to store state of type State in the bundle.
     */
    public static final String EXTRA_STATE = "state";

    /**
     * Extra flag used to store root of type RootInfo in the bundle.
     */
    public static final String EXTRA_ROOT = "root";

    /**
     * Extra flag used to store document of DocumentInfo type in the bundle.
     */
    public static final String EXTRA_DOC = "document";

    /**
     * Extra flag used to store DirectoryFragment's selection of Selection type in the bundle.
     */
    public static final String EXTRA_SELECTION = "selection";

    /**
     * Extra flag used to store DirectoryFragment's ignore state of boolean type in the bundle.
     */
    public static final String EXTRA_IGNORE_STATE = "ignoreState";

    /**
     * Extra flag used to store pick result state of PickResult type in the bundle.
     */
    public static final String EXTRA_PICK_RESULT = "pickResult";

    /**
     * Extra for an Intent for enabling performance benchmark. Used only by tests.
     */
    public static final String EXTRA_BENCHMARK = "com.android.documentsui.benchmark";

    /**
     * Extra flag used to signify to inspector that debug section can be shown.
     */
    public static final String EXTRA_SHOW_DEBUG = "com.android.documentsui.SHOW_DEBUG";

    /**
     * Maximum number of items in a Binder transaction packet.
     */
    public static final int MAX_DOCS_IN_INTENT = 500;

    /**
     * Animation duration of checkbox in directory list/grid in millis.
     */
    public static final int CHECK_ANIMATION_DURATION = 100;

    /**
     * Class name of launcher icon avtivity.
     */
    public static final String LAUNCHER_TARGET_CLASS = "com.android.documentsui.LauncherActivity";

    private static final Collator sCollator;

    /**
     * We support restrict Storage Access Framework from {@link android.os.Build.VERSION_CODES#R}.
     * App Compatibility flag that indicates whether the app should be restricted or not.
     * This flag is turned on by default for all apps targeting >
     * {@link android.os.Build.VERSION_CODES#Q}.
     */
    @ChangeId
    @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.Q)
    private static final long RESTRICT_STORAGE_ACCESS_FRAMEWORK = 141600225L;

    static {
        sCollator = Collator.getInstance();
        sCollator.setStrength(Collator.SECONDARY);
    }

    /**
     * @deprecated use {@link MessageBuilder#getQuantityString}
     */
    @Deprecated
    public static String getQuantityString(Context context, @PluralsRes int resourceId,
            int quantity) {
        return context.getResources().getQuantityString(resourceId, quantity, quantity);
    }

    /**
     * Whether the calling app should be restricted in Storage Access Framework or not.
     */
    public static boolean shouldRestrictStorageAccessFramework(Activity activity) {
        if (VersionUtils.isAtLeastS()) {
            return true;
        }

        if (!VersionUtils.isAtLeastR()) {
            return false;
        }

        final String packageName = getCallingPackageName(activity);
        final boolean ret = CompatChanges.isChangeEnabled(RESTRICT_STORAGE_ACCESS_FRAMEWORK,
                packageName, Process.myUserHandle());

        Log.d(TAG,
                "shouldRestrictStorageAccessFramework = " + ret + ", packageName = " + packageName);

        return ret;
    }

    public static String formatTime(Context context, long when) {
        // TODO: DateUtils should make this easier
        ZoneId zoneId = ZoneId.systemDefault();
        LocalDateTime then = LocalDateTime.ofInstant(Instant.ofEpochMilli(when), zoneId);
        LocalDateTime now = LocalDateTime.ofInstant(Instant.now(), zoneId);

        int flags = DateUtils.FORMAT_NO_NOON | DateUtils.FORMAT_NO_MIDNIGHT
                | DateUtils.FORMAT_ABBREV_ALL;

        if (then.getYear() != now.getYear()) {
            flags |= DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_SHOW_DATE;
        } else if (then.getDayOfYear() != now.getDayOfYear()) {
            flags |= DateUtils.FORMAT_SHOW_DATE;
        } else {
            flags |= DateUtils.FORMAT_SHOW_TIME;
        }

        return DateUtils.formatDateTime(context, when, flags);
    }

    /**
     * A convenient way to transform any list into a (parcelable) ArrayList.
     * Uses cast if possible, else creates a new list with entries from {@code list}.
     */
    public static <T> ArrayList<T> asArrayList(List<T> list) {
        return list instanceof ArrayList
            ? (ArrayList<T>) list
            : new ArrayList<>(list);
    }

    /**
     * Compare two strings against each other using system default collator in a
     * case-insensitive mode. Clusters strings prefixed with {@link DIR_PREFIX}
     * before other items.
     */
    public static int compareToIgnoreCaseNullable(String lhs, String rhs) {
        final boolean leftEmpty = TextUtils.isEmpty(lhs);
        final boolean rightEmpty = TextUtils.isEmpty(rhs);

        if (leftEmpty && rightEmpty) return 0;
        if (leftEmpty) return -1;
        if (rightEmpty) return 1;

        return sCollator.compare(lhs, rhs);
    }

    private static boolean isSystemApp(ApplicationInfo ai) {
        return (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
    }

    private static boolean isUpdatedSystemApp(ApplicationInfo ai) {
        return (ai.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
    }

    /**
     * Returns the calling package, possibly overridden by EXTRA_PACKAGE_NAME.
     * @param activity
     * @return
     */
    public static String getCallingPackageName(Activity activity) {
        String callingPackage = activity.getCallingPackage();
        // System apps can set the calling package name using an extra.
        try {
            ApplicationInfo info =
                    activity.getPackageManager().getApplicationInfo(callingPackage, 0);
            if (isSystemApp(info) || isUpdatedSystemApp(info)) {
                final String extra = activity.getIntent().getStringExtra(
                        Intent.EXTRA_PACKAGE_NAME);
                if (extra != null && !TextUtils.isEmpty(extra)) {
                    callingPackage = extra;
                }
            }
        } catch (NameNotFoundException e) {
            // Couldn't lookup calling package info. This isn't really
            // gonna happen, given that we're getting the name of the
            // calling package from trusty old Activity.getCallingPackage.
            // For that reason, we ignore this exception.
        }
        return callingPackage;
    }

    /**
     * Returns the calling app name.
     * @param activity
     * @return the calling app name or general anonymous name if not found
     */
    @NonNull
    public static String getCallingAppName(Activity activity) {
        final String anonymous = activity.getString(R.string.anonymous_application);
        final String packageName = getCallingPackageName(activity);
        if (TextUtils.isEmpty(packageName)) {
            return anonymous;
        }

        final PackageManager pm = activity.getPackageManager();
        ApplicationInfo ai;
        try {
            ai = pm.getApplicationInfo(packageName, 0);
        } catch (final PackageManager.NameNotFoundException e) {
            return anonymous;
        }

        CharSequence result = pm.getApplicationLabel(ai);
        return TextUtils.isEmpty(result) ? anonymous : result.toString();
    }

    /**
     * Returns the default directory to be presented after starting the activity.
     * Method can be overridden if the change of the behavior of the the child activity is needed.
     */
    public static Uri getDefaultRootUri(Activity activity) {
        Uri defaultUri = Uri.parse(activity.getResources().getString(R.string.default_root_uri));

        if (!DocumentsContract.isRootUri(activity, defaultUri)) {
            Log.e(TAG, "Default Root URI is not a valid root URI, falling back to Downloads.");
            defaultUri = DocumentsContract.buildRootUri(Providers.AUTHORITY_DOWNLOADS,
                    Providers.ROOT_ID_DOWNLOADS);
        }

        return defaultUri;
    }

    public static boolean isHardwareKeyboardAvailable(Context context) {
        return context.getResources().getConfiguration().keyboard != Configuration.KEYBOARD_NOKEYS;
    }

    public static void ensureKeyboardPresent(Context context, AlertDialog dialog) {
        if (!isHardwareKeyboardAvailable(context)) {
            dialog.getWindow().setSoftInputMode(
                    WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE
                            | WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
        }
    }

    /**
     * Check config whether DocumentsUI is launcher enabled or not.
     * @return true if launcher icon is shown.
     */
    public static boolean isLauncherEnabled(Context context) {
        PackageManager pm = context.getPackageManager();
        if (pm != null) {
            final ComponentName component = new ComponentName(
                    context.getPackageName(), LAUNCHER_TARGET_CLASS);
            final int value = pm.getComponentEnabledSetting(component);
            return value == PackageManager.COMPONENT_ENABLED_STATE_ENABLED;
        }

        return false;
    }

    public static String getDeviceName(ContentResolver resolver) {
        // We match the value supplied by ExternalStorageProvider for
        // the internal storage root.
        return Settings.Global.getString(resolver, Settings.Global.DEVICE_NAME);
    }

    public static void checkMainLoop() {
        if (Looper.getMainLooper() != Looper.myLooper()) {
            Log.e(TAG, "Calling from non-UI thread!");
        }
    }

    /**
     * This method exists solely to smooth over the fact that two different types of
     * views cannot be bound to the same id in different layouts. "What's this crazy-pants
     * stuff?", you say? Here's an example:
     *
     * The main DocumentsUI view (aka "Files app") when running on a phone has a drop-down
     * "breadcrumb" (file path representation) in both landscape and portrait orientation.
     * Larger format devices, like a tablet, use a horizontal "Dir1 > Dir2 > Dir3" format
     * breadcrumb in landscape layouts, but the regular drop-down breadcrumb in portrait
     * mode.
     *
     * Our initial inclination was to give each of those views the same ID (as they both
     * implement the same "Breadcrumb" interface). But at runtime, when rotating a device
     * from one orientation to the other, deeeeeeep within the UI toolkit a exception
     * would happen, because one view instance (drop-down) was being inflated in place of
     * another (horizontal). I'm writing this code comment significantly after the face,
     * so I don't recall all of the details, but it had to do with View type-checking the
     * Parcelable state in onRestore, or something like that. Either way, this isn't
     * allowed (my patch to fix this was rejected).
     *
     * To work around this we have this cute little method that accepts multiple
     * resource IDs, and along w/ type inference finds our view, no matter which
     * id it is wearing, and returns it.
     */
    @SuppressWarnings("TypeParameterUnusedInFormals")
    public static @Nullable <T> T findView(Activity activity, int... resources) {
        for (int id : resources) {
            @SuppressWarnings("unchecked")
            View view = activity.findViewById(id);
            if (view != null) {
                return (T) view;
            }
        }
        return null;
    }

    private Shared() {
        throw new UnsupportedOperationException("provides static fields only");
    }
}
