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

import android.annotation.CurrentTimeMillisLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.appsearch.AppSearchSchema;
import android.app.appsearch.AppSearchSession;
import android.app.appsearch.EmbeddingVector;
import android.app.appsearch.GenericDocument;
import android.app.appsearch.annotation.CanIgnoreReturnValue;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArrayMap;

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

/**
 * Holds data for a {@link GenericDocument}.
 *
 * @hide
 */
@SafeParcelable.Class(creator = "GenericDocumentParcelCreator")
// This won't be used to send data over binder, and we have to use Parcelable for code sync purpose.
@SuppressLint("BanParcelableUsage")
public final class GenericDocumentParcel extends AbstractSafeParcelable implements Parcelable {
    @NonNull
    public static final Parcelable.Creator<GenericDocumentParcel> CREATOR =
            new GenericDocumentParcelCreator();

    /** The default score of document. */
    private static final int DEFAULT_SCORE = 0;

    /** The default time-to-live in millisecond of a document, which is infinity. */
    private static final long DEFAULT_TTL_MILLIS = 0L;

    /** Default but invalid value for {@code mCreationTimestampMillis}. */
    private static final long INVALID_CREATION_TIMESTAMP_MILLIS = -1L;

    @Field(id = 1, getter = "getNamespace")
    @NonNull
    private final String mNamespace;

    @Field(id = 2, getter = "getId")
    @NonNull
    private final String mId;

    @Field(id = 3, getter = "getSchemaType")
    @NonNull
    private final String mSchemaType;

    @Field(id = 4, getter = "getCreationTimestampMillis")
    private final long mCreationTimestampMillis;

    @Field(id = 5, getter = "getTtlMillis")
    private final long mTtlMillis;

    @Field(id = 6, getter = "getScore")
    private final int mScore;

    /**
     * Contains all properties in {@link GenericDocument} in a list.
     *
     * <p>Unfortunately SafeParcelable doesn't support map type so we have to use a list here.
     */
    @Field(id = 7, getter = "getProperties")
    @NonNull
    private final List<PropertyParcel> mProperties;

    /** Contains all parent properties for this {@link GenericDocument} in a list. */
    @Field(id = 8, getter = "getParentTypes")
    @Nullable
    private final List<String> mParentTypes;

    /**
     * Contains all properties in {@link GenericDocument} to support getting properties via name
     *
     * <p>This map is created for quick looking up property by name.
     */
    @NonNull private final Map<String, PropertyParcel> mPropertyMap;

    @Nullable private Integer mHashCode;

    /**
     * The constructor taking the property list, and create map internally from this list.
     *
     * <p>This will be used in createFromParcel, so creating the property map can not be avoided in
     * this constructor.
     */
    @Constructor
    GenericDocumentParcel(
            @Param(id = 1) @NonNull String namespace,
            @Param(id = 2) @NonNull String id,
            @Param(id = 3) @NonNull String schemaType,
            @Param(id = 4) long creationTimestampMillis,
            @Param(id = 5) long ttlMillis,
            @Param(id = 6) int score,
            @Param(id = 7) @NonNull List<PropertyParcel> properties,
            @Param(id = 8) @Nullable List<String> parentTypes) {
        this(
                namespace,
                id,
                schemaType,
                creationTimestampMillis,
                ttlMillis,
                score,
                properties,
                createPropertyMapFromPropertyArray(properties),
                parentTypes);
    }

    /**
     * A constructor taking both property list and property map.
     *
     * <p>Caller needs to make sure property list and property map matches(map is generated from
     * list, or list generated from map).
     */
    GenericDocumentParcel(
            @NonNull String namespace,
            @NonNull String id,
            @NonNull String schemaType,
            long creationTimestampMillis,
            long ttlMillis,
            int score,
            @NonNull List<PropertyParcel> properties,
            @NonNull Map<String, PropertyParcel> propertyMap,
            @Nullable List<String> parentTypes) {
        mNamespace = Objects.requireNonNull(namespace);
        mId = Objects.requireNonNull(id);
        mSchemaType = Objects.requireNonNull(schemaType);
        mCreationTimestampMillis = creationTimestampMillis;
        mTtlMillis = ttlMillis;
        mScore = score;
        mProperties = Objects.requireNonNull(properties);
        mPropertyMap = Objects.requireNonNull(propertyMap);
        mParentTypes = parentTypes;
    }

    /** Returns the {@link GenericDocumentParcel} object from the given {@link GenericDocument}. */
    @NonNull
    public static GenericDocumentParcel fromGenericDocument(
            @NonNull GenericDocument genericDocument) {
        Objects.requireNonNull(genericDocument);
        return genericDocument.getDocumentParcel();
    }

    private static Map<String, PropertyParcel> createPropertyMapFromPropertyArray(
            @NonNull List<PropertyParcel> properties) {
        Objects.requireNonNull(properties);
        Map<String, PropertyParcel> propertyMap = new ArrayMap<>(properties.size());
        for (int i = 0; i < properties.size(); ++i) {
            PropertyParcel property = properties.get(i);
            propertyMap.put(property.getPropertyName(), property);
        }
        return propertyMap;
    }

    /** Returns the unique identifier of the {@link GenericDocument}. */
    @NonNull
    public String getId() {
        return mId;
    }

    /** Returns the namespace of the {@link GenericDocument}. */
    @NonNull
    public String getNamespace() {
        return mNamespace;
    }

    /** Returns the {@link AppSearchSchema} type of the {@link GenericDocument}. */
    @NonNull
    public String getSchemaType() {
        return mSchemaType;
    }

    /** Returns the creation timestamp of the {@link GenericDocument}, in milliseconds. */
    @CurrentTimeMillisLong
    public long getCreationTimestampMillis() {
        return mCreationTimestampMillis;
    }

    /** Returns the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds. */
    public long getTtlMillis() {
        return mTtlMillis;
    }

    /** Returns the score of the {@link GenericDocument}. */
    public int getScore() {
        return mScore;
    }

    /** Returns the names of all properties defined in this document. */
    @NonNull
    public Set<String> getPropertyNames() {
        return mPropertyMap.keySet();
    }

    /** Returns all the properties the document has. */
    @NonNull
    public List<PropertyParcel> getProperties() {
        return mProperties;
    }

    /** Returns the property map the document has. */
    @NonNull
    public Map<String, PropertyParcel> getPropertyMap() {
        return mPropertyMap;
    }

    /** Returns the list of parent types for the {@link GenericDocument}. */
    @Nullable
    public List<String> getParentTypes() {
        return mParentTypes;
    }

    @Override
    public boolean equals(@Nullable Object other) {
        if (this == other) {
            return true;
        }
        if (!(other instanceof GenericDocumentParcel)) {
            return false;
        }
        GenericDocumentParcel otherDocument = (GenericDocumentParcel) other;
        return mNamespace.equals(otherDocument.mNamespace)
                && mId.equals(otherDocument.mId)
                && mSchemaType.equals(otherDocument.mSchemaType)
                && mTtlMillis == otherDocument.mTtlMillis
                && mCreationTimestampMillis == otherDocument.mCreationTimestampMillis
                && mScore == otherDocument.mScore
                && Objects.equals(mProperties, otherDocument.mProperties)
                && Objects.equals(mPropertyMap, otherDocument.mPropertyMap)
                && Objects.equals(mParentTypes, otherDocument.mParentTypes);
    }

    @Override
    public int hashCode() {
        if (mHashCode == null) {
            mHashCode =
                    Objects.hash(
                            mNamespace,
                            mId,
                            mSchemaType,
                            mTtlMillis,
                            mScore,
                            mCreationTimestampMillis,
                            Objects.hashCode(mProperties),
                            Objects.hashCode(mPropertyMap),
                            Objects.hashCode(mParentTypes));
        }
        return mHashCode;
    }

    @Override
    public void writeToParcel(@NonNull Parcel dest, int flags) {
        GenericDocumentParcelCreator.writeToParcel(this, dest, flags);
    }

    /** The builder class for {@link GenericDocumentParcel}. */
    public static final class Builder {
        private String mNamespace;
        private String mId;
        private String mSchemaType;
        private long mCreationTimestampMillis;
        private long mTtlMillis;
        private int mScore;
        private Map<String, PropertyParcel> mPropertyMap;
        @Nullable private List<String> mParentTypes;

        /**
         * Creates a new {@link GenericDocumentParcel.Builder}.
         *
         * <p>Document IDs are unique within a namespace.
         *
         * <p>The number of namespaces per app should be kept small for efficiency reasons.
         */
        public Builder(@NonNull String namespace, @NonNull String id, @NonNull String schemaType) {
            mNamespace = Objects.requireNonNull(namespace);
            mId = Objects.requireNonNull(id);
            mSchemaType = Objects.requireNonNull(schemaType);
            mCreationTimestampMillis = INVALID_CREATION_TIMESTAMP_MILLIS;
            mTtlMillis = DEFAULT_TTL_MILLIS;
            mScore = DEFAULT_SCORE;
            mPropertyMap = new ArrayMap<>();
        }

        /**
         * Creates a new {@link GenericDocumentParcel.Builder} from the given {@link
         * GenericDocumentParcel}.
         */
        public Builder(@NonNull GenericDocumentParcel documentSafeParcel) {
            Objects.requireNonNull(documentSafeParcel);

            mNamespace = documentSafeParcel.mNamespace;
            mId = documentSafeParcel.mId;
            mSchemaType = documentSafeParcel.mSchemaType;
            mCreationTimestampMillis = documentSafeParcel.mCreationTimestampMillis;
            mTtlMillis = documentSafeParcel.mTtlMillis;
            mScore = documentSafeParcel.mScore;

            // Create a shallow copy of the map so we won't change the original one.
            Map<String, PropertyParcel> propertyMap = documentSafeParcel.mPropertyMap;
            mPropertyMap = new ArrayMap<>(propertyMap.size());
            for (PropertyParcel value : propertyMap.values()) {
                mPropertyMap.put(value.getPropertyName(), value);
            }

            // We don't need to create a shallow copy here, as in the setter for ParentTypes we
            // will create a new list anyway.
            mParentTypes = documentSafeParcel.mParentTypes;
        }

        /**
         * Sets the app-defined namespace this document resides in, changing the value provided in
         * the constructor. No special values are reserved or understood by the infrastructure.
         *
         * <p>Document IDs are unique within a namespace.
         *
         * <p>The number of namespaces per app should be kept small for efficiency reasons.
         */
        @CanIgnoreReturnValue
        @NonNull
        public Builder setNamespace(@NonNull String namespace) {
            Objects.requireNonNull(namespace);
            mNamespace = namespace;
            return this;
        }

        /**
         * Sets the ID of this document, changing the value provided in the constructor. No special
         * values are reserved or understood by the infrastructure.
         *
         * <p>Document IDs are unique within a namespace.
         */
        @CanIgnoreReturnValue
        @NonNull
        public Builder setId(@NonNull String id) {
            Objects.requireNonNull(id);
            mId = id;
            return this;
        }

        /**
         * Sets the schema type of this document, changing the value provided in the constructor.
         *
         * <p>To successfully index a document, the schema type must match the name of an {@link
         * AppSearchSchema} object previously provided to {@link AppSearchSession#setSchema}.
         */
        @CanIgnoreReturnValue
        @NonNull
        public Builder setSchemaType(@NonNull String schemaType) {
            Objects.requireNonNull(schemaType);
            mSchemaType = schemaType;
            return this;
        }

        /** Sets the score of the parent {@link GenericDocument}. */
        @CanIgnoreReturnValue
        @NonNull
        public Builder setScore(int score) {
            mScore = score;
            return this;
        }

        /**
         * Sets the creation timestamp of the {@link GenericDocument}, in milliseconds.
         *
         * <p>This should be set using a value obtained from the {@link System#currentTimeMillis}
         * time base.
         *
         * <p>If this method is not called, this will be set to the time the object is built.
         *
         * @param creationTimestampMillis a creation timestamp in milliseconds.
         */
        @CanIgnoreReturnValue
        @NonNull
        public Builder setCreationTimestampMillis(
                @CurrentTimeMillisLong long creationTimestampMillis) {
            mCreationTimestampMillis = creationTimestampMillis;
            return this;
        }

        /**
         * Sets the TTL (time-to-live) of the {@link GenericDocument}, in milliseconds.
         *
         * <p>The TTL is measured against {@link #getCreationTimestampMillis}. At the timestamp of
         * {@code creationTimestampMillis + ttlMillis}, measured in the {@link
         * System#currentTimeMillis} time base, the document will be auto-deleted.
         *
         * <p>The default value is 0, which means the document is permanent and won't be
         * auto-deleted until the app is uninstalled or {@link AppSearchSession#remove} is called.
         *
         * @param ttlMillis a non-negative duration in milliseconds.
         * @throws IllegalArgumentException if ttlMillis is negative.
         */
        @CanIgnoreReturnValue
        @NonNull
        public Builder setTtlMillis(long ttlMillis) {
            if (ttlMillis < 0) {
                throw new IllegalArgumentException("Document ttlMillis cannot be negative.");
            }
            mTtlMillis = ttlMillis;
            return this;
        }

        /**
         * Sets the list of parent types of the {@link GenericDocument}'s type.
         *
         * <p>Child types must appear before parent types in the list.
         */
        @CanIgnoreReturnValue
        @NonNull
        public Builder setParentTypes(@NonNull List<String> parentTypes) {
            Objects.requireNonNull(parentTypes);
            mParentTypes = new ArrayList<>(parentTypes);
            return this;
        }

        /**
         * Clears the value for the property with the given name.
         *
         * <p>Note that this method does not support property paths.
         *
         * @param name The name of the property to clear.
         */
        @CanIgnoreReturnValue
        @NonNull
        public Builder clearProperty(@NonNull String name) {
            Objects.requireNonNull(name);
            mPropertyMap.remove(name);
            return this;
        }

        /** puts an array of {@link String} in property map. */
        @CanIgnoreReturnValue
        @NonNull
        public Builder putInPropertyMap(@NonNull String name, @NonNull String[] values)
                throws IllegalArgumentException {
            putInPropertyMap(
                    name, new PropertyParcel.Builder(name).setStringValues(values).build());
            return this;
        }

        /** puts an array of boolean in property map. */
        @CanIgnoreReturnValue
        @NonNull
        public Builder putInPropertyMap(@NonNull String name, @NonNull boolean[] values) {
            putInPropertyMap(
                    name, new PropertyParcel.Builder(name).setBooleanValues(values).build());
            return this;
        }

        /** puts an array of double in property map. */
        @CanIgnoreReturnValue
        @NonNull
        public Builder putInPropertyMap(@NonNull String name, @NonNull double[] values) {
            putInPropertyMap(
                    name, new PropertyParcel.Builder(name).setDoubleValues(values).build());
            return this;
        }

        /** puts an array of long in property map. */
        @CanIgnoreReturnValue
        @NonNull
        public Builder putInPropertyMap(@NonNull String name, @NonNull long[] values) {
            putInPropertyMap(name, new PropertyParcel.Builder(name).setLongValues(values).build());
            return this;
        }

        /** Converts and saves a byte[][] into {@link #mProperties}. */
        @CanIgnoreReturnValue
        @NonNull
        public Builder putInPropertyMap(@NonNull String name, @NonNull byte[][] values) {
            putInPropertyMap(name, new PropertyParcel.Builder(name).setBytesValues(values).build());
            return this;
        }

        /** puts an array of {@link GenericDocumentParcel} in property map. */
        @CanIgnoreReturnValue
        @NonNull
        public Builder putInPropertyMap(
                @NonNull String name, @NonNull GenericDocumentParcel[] values) {
            putInPropertyMap(
                    name, new PropertyParcel.Builder(name).setDocumentValues(values).build());
            return this;
        }

        /** puts an array of {@link EmbeddingVector} in property map. */
        @CanIgnoreReturnValue
        @NonNull
        public Builder putInPropertyMap(@NonNull String name, @NonNull EmbeddingVector[] values) {
            putInPropertyMap(
                    name, new PropertyParcel.Builder(name).setEmbeddingValues(values).build());
            return this;
        }

        /** Directly puts a {@link PropertyParcel} in property map. */
        @CanIgnoreReturnValue
        @NonNull
        public Builder putInPropertyMap(@NonNull String name, @NonNull PropertyParcel value) {
            Objects.requireNonNull(value);
            mPropertyMap.put(name, value);
            return this;
        }

        /** Builds the {@link GenericDocument} object. */
        @NonNull
        public GenericDocumentParcel build() {
            // Set current timestamp for creation timestamp by default.
            if (mCreationTimestampMillis == INVALID_CREATION_TIMESTAMP_MILLIS) {
                mCreationTimestampMillis = System.currentTimeMillis();
            }
            return new GenericDocumentParcel(
                    mNamespace,
                    mId,
                    mSchemaType,
                    mCreationTimestampMillis,
                    mTtlMillis,
                    mScore,
                    new ArrayList<>(mPropertyMap.values()),
                    mParentTypes);
        }
    }
}
