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

import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
import android.service.credentials.CredentialEntry;

import com.android.internal.util.AnnotationValidations;
import com.android.internal.util.Preconditions;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * Represents the type and contained data fields of a {@link Credential}.
 */
public final class CredentialDescription implements Parcelable {

    private static final int MAX_ALLOWED_ENTRIES_PER_DESCRIPTION = 16;

    /**
     * The credential type.
     */
    @NonNull
    private final String mType;

    /**
     * Keys of elements to match with Credential requests.
     */
    @NonNull
    private final Set<String> mSupportedElementKeys;

    /**
     * The credential entries to be used in the UI.
     */
    @NonNull
    private final List<CredentialEntry> mCredentialEntries;

    /**
     * Constructs a {@link CredentialDescription}.
     *
     * @param type the type of the credential returned.
     * @param supportedElementKeys Keys of elements to match with Credential requests.
     * @param credentialEntries a list of {@link CredentialEntry}s that are to be shown on the
     *                          account selector if a credential matches with this description.
     *                          Each entry contains information to be displayed within an
     *                          entry on the UI, as well as a {@link android.app.PendingIntent}
     *                          that will be invoked if the user selects this entry.
     *
     * @throws IllegalArgumentException If type is empty.
     */
    public CredentialDescription(@NonNull String type,
            @NonNull Set<String> supportedElementKeys,
            @NonNull List<CredentialEntry> credentialEntries) {
        mType = Preconditions.checkStringNotEmpty(type, "type must not be empty");
        mSupportedElementKeys = Objects.requireNonNull(supportedElementKeys);
        mCredentialEntries = Objects.requireNonNull(credentialEntries);
        Preconditions.checkArgument(credentialEntries.size()
                        <= MAX_ALLOWED_ENTRIES_PER_DESCRIPTION,
                "The number of Credential Entries exceed 16.");
        Preconditions.checkArgument(compareEntryTypes(type, credentialEntries) == 0,
                "Credential Entry type(s) do not match the request type.");
    }

    private CredentialDescription(@NonNull Parcel in) {
        String type = in.readString8();
        List<String> descriptions = in.createStringArrayList();
        List<CredentialEntry> entries = new ArrayList<>();
        in.readTypedList(entries, CredentialEntry.CREATOR);

        mType = type;
        AnnotationValidations.validate(android.annotation.NonNull.class, null, mType);
        mSupportedElementKeys = new HashSet<>(descriptions);
        AnnotationValidations.validate(android.annotation.NonNull.class, null,
                mSupportedElementKeys);
        mCredentialEntries = entries;
        AnnotationValidations.validate(android.annotation.NonNull.class, null,
                mCredentialEntries);
    }

    private static int compareEntryTypes(@NonNull String type,
            @NonNull List<CredentialEntry> credentialEntries) {
        return credentialEntries.stream()
                .filter(credentialEntry ->
                        !credentialEntry.getType().equals(type)).toList().size();

    }

    public static final @NonNull Parcelable.Creator<CredentialDescription> CREATOR =
            new Parcelable.Creator<CredentialDescription>() {
                @Override
                public CredentialDescription createFromParcel(Parcel in) {
                    return new CredentialDescription(in);
                }

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

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

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        dest.writeString8(mType);
        dest.writeStringList(mSupportedElementKeys.stream().toList());
        dest.writeTypedList(mCredentialEntries, flags);
    }

    /**
     * Returns the type of the Credential described.
     */
    @NonNull
    public String getType() {
        return mType;
    }

    /**
     * Returns the flattened JSON string that will be matched with requests.
     */
    @NonNull
    public Set<String> getSupportedElementKeys() {
        return new HashSet<>(mSupportedElementKeys);
    }

    /**
     * Returns the credential entries to be used in the UI.
     */
    @NonNull
    public List<CredentialEntry> getCredentialEntries() {
        return mCredentialEntries;
    }

    /**
     * {@link CredentialDescription#mType} and
     * {@link CredentialDescription#mSupportedElementKeys} are enough for hashing. Constructor
     * enforces {@link CredentialEntry} to have the same type and
     * {@link android.app.slice.Slice} contained by the entry can not be hashed.
     */
    @Override
    public int hashCode() {
        return Objects.hash(mType, mSupportedElementKeys);
    }

    /**
     * {@link CredentialDescription#mType} and
     * {@link CredentialDescription#mSupportedElementKeys} are enough for equality check.
     */
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof CredentialDescription)) {
            return false;
        }
        CredentialDescription other = (CredentialDescription) obj;
        return mType.equals(other.mType)
                && mSupportedElementKeys.equals(other.mSupportedElementKeys);
    }
}
