/*
 * Copyright 2021 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 android.app.appsearch;

import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.appsearch.annotation.CanIgnoreReturnValue;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.ArraySet;

import com.android.internal.util.Preconditions;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

/** The response class of {@link AppSearchSession#getSchema} */
public final class GetSchemaResponse {
    private static final String VERSION_FIELD = "version";
    private static final String SCHEMAS_FIELD = "schemas";
    private static final String SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD =
            "schemasNotDisplayedBySystem";
    private static final String SCHEMAS_VISIBLE_TO_PACKAGES_FIELD = "schemasVisibleToPackages";
    private static final String SCHEMAS_VISIBLE_TO_PERMISSION_FIELD = "schemasVisibleToPermissions";
    private static final String ALL_REQUIRED_PERMISSION_FIELD = "allRequiredPermission";
    /**
     * This Set contains all schemas that are not displayed by the system. All values in the set are
     * prefixed with the package-database prefix. We do lazy fetch, the object will be created when
     * the user first time fetch it.
     */
    @Nullable private Set<String> mSchemasNotDisplayedBySystem;
    /**
     * This map contains all schemas and {@link PackageIdentifier} that has access to the schema.
     * All keys in the map are prefixed with the package-database prefix. We do lazy fetch, the
     * object will be created when the user first time fetch it.
     */
    @Nullable private Map<String, Set<PackageIdentifier>> mSchemasVisibleToPackages;

    /**
     * This map contains all schemas and Android Permissions combinations that are required to
     * access the schema. All keys in the map are prefixed with the package-database prefix. We do
     * lazy fetch, the object will be created when the user first time fetch it. The Map is
     * constructed in ANY-ALL cases. The querier could read the {@link GenericDocument} objects
     * under the {@code schemaType} if they holds ALL required permissions of ANY combinations. The
     * value set represents {@link
     * android.app.appsearch.SetSchemaRequest.AppSearchSupportedPermission}.
     */
    @Nullable private Map<String, Set<Set<Integer>>> mSchemasVisibleToPermissions;

    private final Bundle mBundle;

    GetSchemaResponse(@NonNull Bundle bundle) {
        mBundle = Objects.requireNonNull(bundle);
    }

    /**
     * Returns the {@link Bundle} populated by this builder.
     *
     * @hide
     */
    @NonNull
    public Bundle getBundle() {
        return mBundle;
    }

    /**
     * Returns the overall database schema version.
     *
     * <p>If the database is empty, 0 will be returned.
     */
    @IntRange(from = 0)
    public int getVersion() {
        return mBundle.getInt(VERSION_FIELD);
    }

    /**
     * Return the schemas most recently successfully provided to {@link AppSearchSession#setSchema}.
     *
     * <p>It is inefficient to call this method repeatedly.
     */
    @NonNull
    @SuppressWarnings("deprecation")
    public Set<AppSearchSchema> getSchemas() {
        ArrayList<Bundle> schemaBundles =
                Objects.requireNonNull(mBundle.getParcelableArrayList(SCHEMAS_FIELD));
        Set<AppSearchSchema> schemas = new ArraySet<>(schemaBundles.size());
        for (int i = 0; i < schemaBundles.size(); i++) {
            schemas.add(new AppSearchSchema(schemaBundles.get(i)));
        }
        return schemas;
    }

    /**
     * Returns all the schema types that are opted out of being displayed and visible on any system
     * UI surface.
     */
    @NonNull
    public Set<String> getSchemaTypesNotDisplayedBySystem() {
        checkGetVisibilitySettingSupported();
        if (mSchemasNotDisplayedBySystem == null) {
            List<String> schemasNotDisplayedBySystemList =
                    mBundle.getStringArrayList(SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD);
            mSchemasNotDisplayedBySystem =
                    Collections.unmodifiableSet(new ArraySet<>(schemasNotDisplayedBySystemList));
        }
        return mSchemasNotDisplayedBySystem;
    }

    /**
     * Returns a mapping of schema types to the set of packages that have access to that schema
     * type.
     */
    @NonNull
    @SuppressWarnings("deprecation")
    public Map<String, Set<PackageIdentifier>> getSchemaTypesVisibleToPackages() {
        checkGetVisibilitySettingSupported();
        if (mSchemasVisibleToPackages == null) {
            Bundle schemaVisibleToPackagesBundle =
                    Objects.requireNonNull(mBundle.getBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD));
            Map<String, Set<PackageIdentifier>> copy = new ArrayMap<>();
            for (String key : schemaVisibleToPackagesBundle.keySet()) {
                List<Bundle> PackageIdentifierBundles =
                        Objects.requireNonNull(
                                schemaVisibleToPackagesBundle.getParcelableArrayList(key));
                Set<PackageIdentifier> packageIdentifiers =
                        new ArraySet<>(PackageIdentifierBundles.size());
                for (int i = 0; i < PackageIdentifierBundles.size(); i++) {
                    packageIdentifiers.add(new PackageIdentifier(PackageIdentifierBundles.get(i)));
                }
                copy.put(key, packageIdentifiers);
            }
            mSchemasVisibleToPackages = Collections.unmodifiableMap(copy);
        }
        return mSchemasVisibleToPackages;
    }

    /**
     * Returns a mapping of schema types to the Map of {@link android.Manifest.permission}
     * combinations that querier must hold to access that schema type.
     *
     * <p>The querier could read the {@link GenericDocument} objects under the {@code schemaType} if
     * they holds ALL required permissions of ANY of the individual value sets.
     *
     * <p>For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB}, {
     * PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}.
     *
     * <ul>
     *   <li>A querier holds both PermissionA and PermissionB has access.
     *   <li>A querier holds both PermissionC and PermissionD has access.
     *   <li>A querier holds only PermissionE has access.
     *   <li>A querier holds both PermissionA and PermissionE has access.
     *   <li>A querier holds only PermissionA doesn't have access.
     *   <li>A querier holds both PermissionA and PermissionC doesn't have access.
     * </ul>
     *
     * @return The map contains schema type and all combinations of required permission for querier
     *     to access it. The supported Permission are {@link SetSchemaRequest#READ_SMS}, {@link
     *     SetSchemaRequest#READ_CALENDAR}, {@link SetSchemaRequest#READ_CONTACTS}, {@link
     *     SetSchemaRequest#READ_EXTERNAL_STORAGE}, {@link
     *     SetSchemaRequest#READ_HOME_APP_SEARCH_DATA} and {@link
     *     SetSchemaRequest#READ_ASSISTANT_APP_SEARCH_DATA}.
     */
    @NonNull
    @SuppressWarnings("deprecation")
    public Map<String, Set<Set<Integer>>> getRequiredPermissionsForSchemaTypeVisibility() {
        checkGetVisibilitySettingSupported();
        if (mSchemasVisibleToPermissions == null) {
            Map<String, Set<Set<Integer>>> copy = new ArrayMap<>();
            Bundle schemaVisibleToPermissionBundle =
                    Objects.requireNonNull(mBundle.getBundle(SCHEMAS_VISIBLE_TO_PERMISSION_FIELD));
            for (String key : schemaVisibleToPermissionBundle.keySet()) {
                ArrayList<Bundle> allRequiredPermissionsBundle =
                        schemaVisibleToPermissionBundle.getParcelableArrayList(key);
                Set<Set<Integer>> visibleToPermissions = new ArraySet<>();
                if (allRequiredPermissionsBundle != null) {
                    // This should never be null
                    for (int i = 0; i < allRequiredPermissionsBundle.size(); i++) {
                        visibleToPermissions.add(
                                new ArraySet<>(
                                        allRequiredPermissionsBundle
                                                .get(i)
                                                .getIntegerArrayList(
                                                        ALL_REQUIRED_PERMISSION_FIELD)));
                    }
                }
                copy.put(key, visibleToPermissions);
            }
            mSchemasVisibleToPermissions = Collections.unmodifiableMap(copy);
        }
        return mSchemasVisibleToPermissions;
    }

    private void checkGetVisibilitySettingSupported() {
        if (!mBundle.containsKey(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD)) {
            throw new UnsupportedOperationException(
                    "Get visibility setting is not supported with"
                            + " this backend/Android API level combination.");
        }
    }

    /** Builder for {@link GetSchemaResponse} objects. */
    public static final class Builder {
        private int mVersion = 0;
        private ArrayList<Bundle> mSchemaBundles = new ArrayList<>();
        /**
         * Creates the object when we actually set them. If we never set visibility settings, we
         * should throw {@link UnsupportedOperationException} in the visibility getters.
         */
        @Nullable private ArrayList<String> mSchemasNotDisplayedBySystem;

        private Bundle mSchemasVisibleToPackages;
        private Bundle mSchemasVisibleToPermissions;
        private boolean mBuilt = false;

        /** Create a {@link Builder} object} */
        public Builder() {
            this(/*getVisibilitySettingSupported=*/ true);
        }

        /**
         * Create a {@link Builder} object}.
         *
         * <p>This constructor should only be used in Android API below than T.
         *
         * @param getVisibilitySettingSupported whether supported {@link
         *     Features#ADD_PERMISSIONS_AND_GET_VISIBILITY} by this backend/Android API level.
         * @hide
         */
        public Builder(boolean getVisibilitySettingSupported) {
            if (getVisibilitySettingSupported) {
                mSchemasNotDisplayedBySystem = new ArrayList<>();
                mSchemasVisibleToPackages = new Bundle();
                mSchemasVisibleToPermissions = new Bundle();
            }
        }

        /**
         * Sets the database overall schema version.
         *
         * <p>Default version is 0
         */
        @CanIgnoreReturnValue
        @NonNull
        public Builder setVersion(@IntRange(from = 0) int version) {
            resetIfBuilt();
            mVersion = version;
            return this;
        }

        /** Adds one {@link AppSearchSchema} to the schema list. */
        @CanIgnoreReturnValue
        @NonNull
        public Builder addSchema(@NonNull AppSearchSchema schema) {
            Objects.requireNonNull(schema);
            resetIfBuilt();
            mSchemaBundles.add(schema.getBundle());
            return this;
        }

        /**
         * Sets whether or not documents from the provided {@code schemaType} will be displayed and
         * visible on any system UI surface.
         *
         * @param schemaType The name of an {@link AppSearchSchema} within the same {@link
         *     GetSchemaResponse}, which won't be displayed by system.
         */
        // Getter getSchemaTypesNotDisplayedBySystem returns plural objects.
        @CanIgnoreReturnValue
        @SuppressLint("MissingGetterMatchingBuilder")
        @NonNull
        public Builder addSchemaTypeNotDisplayedBySystem(@NonNull String schemaType) {
            Objects.requireNonNull(schemaType);
            resetIfBuilt();
            if (mSchemasNotDisplayedBySystem == null) {
                mSchemasNotDisplayedBySystem = new ArrayList<>();
            }
            mSchemasNotDisplayedBySystem.add(schemaType);
            return this;
        }

        /**
         * Sets whether or not documents from the provided {@code schemaType} can be read by the
         * specified package.
         *
         * <p>Each package is represented by a {@link PackageIdentifier}, containing a package name
         * and a byte array of type {@link android.content.pm.PackageManager#CERT_INPUT_SHA256}.
         *
         * <p>To opt into one-way data sharing with another application, the developer will need to
         * explicitly grant the other application’s package name and certificate Read access to its
         * data.
         *
         * <p>For two-way data sharing, both applications need to explicitly grant Read access to
         * one another.
         *
         * @param schemaType The schema type to set visibility on.
         * @param packageIdentifiers Represents the package that has access to the given schema
         *     type.
         */
        // Getter getSchemaTypesVisibleToPackages returns a map contains all schema types.
        @CanIgnoreReturnValue
        @SuppressLint("MissingGetterMatchingBuilder")
        @NonNull
        public Builder setSchemaTypeVisibleToPackages(
                @NonNull String schemaType, @NonNull Set<PackageIdentifier> packageIdentifiers) {
            Objects.requireNonNull(schemaType);
            Objects.requireNonNull(packageIdentifiers);
            resetIfBuilt();
            ArrayList<Bundle> bundles = new ArrayList<>(packageIdentifiers.size());
            for (PackageIdentifier packageIdentifier : packageIdentifiers) {
                bundles.add(packageIdentifier.getBundle());
            }
            mSchemasVisibleToPackages.putParcelableArrayList(schemaType, bundles);
            return this;
        }

        /**
         * Sets a set of required {@link android.Manifest.permission} combinations to the given
         * schema type.
         *
         * <p>The querier could read the {@link GenericDocument} objects under the {@code
         * schemaType} if they holds ALL required permissions of ANY of the individual value sets.
         *
         * <p>For example, if the Map contains {@code {% verbatim %}{{permissionA, PermissionB},
         * {PermissionC, PermissionD}, {PermissionE}}{% endverbatim %}}.
         *
         * <ul>
         *   <li>A querier holds both PermissionA and PermissionB has access.
         *   <li>A querier holds both PermissionC and PermissionD has access.
         *   <li>A querier holds only PermissionE has access.
         *   <li>A querier holds both PermissionA and PermissionE has access.
         *   <li>A querier holds only PermissionA doesn't have access.
         *   <li>A querier holds both PermissionA and PermissionC doesn't have access.
         * </ul>
         *
         * @see android.Manifest.permission#READ_SMS
         * @see android.Manifest.permission#READ_CALENDAR
         * @see android.Manifest.permission#READ_CONTACTS
         * @see android.Manifest.permission#READ_EXTERNAL_STORAGE
         * @see android.Manifest.permission#READ_HOME_APP_SEARCH_DATA
         * @see android.Manifest.permission#READ_ASSISTANT_APP_SEARCH_DATA
         * @param schemaType The schema type to set visibility on.
         * @param visibleToPermissions The Android permissions that will be required to access the
         *     given schema.
         */
        // Getter getRequiredPermissionsForSchemaTypeVisibility returns a map for all schemaTypes.
        @CanIgnoreReturnValue
        @SuppressLint("MissingGetterMatchingBuilder")
        @NonNull
        public Builder setRequiredPermissionsForSchemaTypeVisibility(
                @NonNull String schemaType,
                @SetSchemaRequest.AppSearchSupportedPermission @NonNull
                        Set<Set<Integer>> visibleToPermissions) {
            Objects.requireNonNull(schemaType);
            Objects.requireNonNull(visibleToPermissions);
            resetIfBuilt();
            ArrayList<Bundle> visibleToPermissionsBundle = new ArrayList<>();
            for (Set<Integer> allRequiredPermissions : visibleToPermissions) {
                for (int permission : allRequiredPermissions) {
                    Preconditions.checkArgumentInRange(
                            permission,
                            SetSchemaRequest.READ_SMS,
                            SetSchemaRequest.READ_ASSISTANT_APP_SEARCH_DATA,
                            "permission");
                }
                Bundle allRequiredPermissionsBundle = new Bundle();
                allRequiredPermissionsBundle.putIntegerArrayList(
                        ALL_REQUIRED_PERMISSION_FIELD, new ArrayList<>(allRequiredPermissions));
                visibleToPermissionsBundle.add(allRequiredPermissionsBundle);
            }
            mSchemasVisibleToPermissions.putParcelableArrayList(
                    schemaType, visibleToPermissionsBundle);
            return this;
        }

        /** Builds a {@link GetSchemaResponse} object. */
        @NonNull
        public GetSchemaResponse build() {
            Bundle bundle = new Bundle();
            bundle.putInt(VERSION_FIELD, mVersion);
            bundle.putParcelableArrayList(SCHEMAS_FIELD, mSchemaBundles);
            if (mSchemasNotDisplayedBySystem != null) {
                // Only save the visibility fields if it was actually set.
                bundle.putStringArrayList(
                        SCHEMAS_NOT_DISPLAYED_BY_SYSTEM_FIELD, mSchemasNotDisplayedBySystem);
                bundle.putBundle(SCHEMAS_VISIBLE_TO_PACKAGES_FIELD, mSchemasVisibleToPackages);
                bundle.putBundle(SCHEMAS_VISIBLE_TO_PERMISSION_FIELD, mSchemasVisibleToPermissions);
            }
            mBuilt = true;
            return new GetSchemaResponse(bundle);
        }

        private void resetIfBuilt() {
            if (mBuilt) {
                mSchemaBundles = new ArrayList<>(mSchemaBundles);
                if (mSchemasNotDisplayedBySystem != null) {
                    // Only reset the visibility fields if it was actually set.
                    mSchemasNotDisplayedBySystem = new ArrayList<>(mSchemasNotDisplayedBySystem);
                    Bundle copyVisibleToPackages = new Bundle();
                    copyVisibleToPackages.putAll(mSchemasVisibleToPackages);
                    mSchemasVisibleToPackages = copyVisibleToPackages;
                    Bundle copyVisibleToPermissions = new Bundle();
                    copyVisibleToPermissions.putAll(mSchemasVisibleToPermissions);
                    mSchemasVisibleToPermissions = copyVisibleToPermissions;
                }
                mBuilt = false;
            }
        }
    }
}
