/*
 * Copyright (C) 2020 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.service.timezone;

import android.annotation.ElapsedRealtimeLong;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
 * A suggestion from a {@link TimeZoneProviderService} containing zero or more time zones.
 *
 * @hide
 */
@SystemApi
public final class TimeZoneProviderSuggestion implements Parcelable {

    @NonNull
    private final List<String> mTimeZoneIds;

    @ElapsedRealtimeLong
    private final long mElapsedRealtimeMillis;

    private TimeZoneProviderSuggestion(@NonNull List<String> timeZoneIds,
            @ElapsedRealtimeLong long elapsedRealtimeMillis) {
        mTimeZoneIds = immutableList(timeZoneIds);
        mElapsedRealtimeMillis = elapsedRealtimeMillis;
    }

    /**
     * Returns the time of the suggestion in elapsed real-time since system boot. Where possible,
     * the time should be based on the time of the data used when determining time zone. For
     * example, if it was based on a {@link android.location.Location} then it should be the time
     * associated with that location.
     *
     * <p>This value is compared to {@link
     * android.os.SystemClock#elapsedRealtime()}, to calculate the age of a fix and to compare
     * {@link TimeZoneProviderSuggestion} instances.
     *
     * @return elapsed real-time of fix, in milliseconds
     */
    @ElapsedRealtimeLong
    public long getElapsedRealtimeMillis() {
        return mElapsedRealtimeMillis;
    }

    /**
     * Returns the zero or more time zone IDs for this suggestion.
     *
     * <p>Time zone IDs are TZDB IDs like "America/Los_Angeles" that would be accepted by {@link
     * java.util.TimeZone#getTimeZone(String)}.
     *
     * <p>Most often a suggestion will contain a single time zone ID but other possibilities are
     * valid. A suggestion with zero time zone IDs means the provider is certain there are no time
     * zones for the current location, e.g. for oceans, boundaries or disputed areas. A suggestion
     * with multiple IDs can occur on boundaries or disputed areas. The ordering should be in order
     * of likelihood if possible, but the time zone detection service may choose from any of the
     * zones suggested if it has other supporting information available.
     */
    @NonNull
    public List<String> getTimeZoneIds() {
        return mTimeZoneIds;
    }

    @Override
    public String toString() {
        return "TimeZoneProviderSuggestion{"
                + "mTimeZoneIds=" + mTimeZoneIds
                + ", mElapsedRealtimeMillis=" + mElapsedRealtimeMillis
                + "(" + Duration.ofMillis(mElapsedRealtimeMillis) + ")"
                + '}';
    }

    public static final @NonNull Creator<TimeZoneProviderSuggestion> CREATOR =
            new Creator<TimeZoneProviderSuggestion>() {
                @Override
                public TimeZoneProviderSuggestion createFromParcel(Parcel in) {
                    @SuppressWarnings("unchecked")
                    ArrayList<String> timeZoneIds =
                            (ArrayList<String>) in.readArrayList(null /* classLoader */, java.lang.String.class);
                    long elapsedRealtimeMillis = in.readLong();
                    return new TimeZoneProviderSuggestion(timeZoneIds, elapsedRealtimeMillis);
                }

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

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

    @Override
    public void writeToParcel(@NonNull Parcel parcel, int flags) {
        parcel.writeList(mTimeZoneIds);
        parcel.writeLong(mElapsedRealtimeMillis);
    }

    /**
     * Similar to {@link #equals} except this methods checks for equivalence, not equality.
     * i.e. two suggestions are equivalent if they suggest the same time zones.
     *
     * @hide
     */
    @SuppressWarnings("ReferenceEquality")
    public boolean isEquivalentTo(@Nullable TimeZoneProviderSuggestion other) {
        if (this == other) {
            return true;
        }
        if (other == null) {
            return false;
        }
        // Only check the time zone IDs. The times can be different, but we don't mind.
        return mTimeZoneIds.equals(other.mTimeZoneIds);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        TimeZoneProviderSuggestion that = (TimeZoneProviderSuggestion) o;
        return mElapsedRealtimeMillis == that.mElapsedRealtimeMillis
                && mTimeZoneIds.equals(that.mTimeZoneIds);
    }

    @Override
    public int hashCode() {
        return Objects.hash(mTimeZoneIds, mElapsedRealtimeMillis);
    }

    /** A builder for {@link TimeZoneProviderSuggestion}. */
    public static final class Builder {

        private @NonNull List<String> mTimeZoneIds = Collections.emptyList();
        @ElapsedRealtimeLong
        private long mElapsedRealtimeMillis = SystemClock.elapsedRealtime();

        /**
         * Sets the time zone IDs of this suggestion.
         */
        @NonNull
        public Builder setTimeZoneIds(@NonNull List<String> timeZoneIds) {
            mTimeZoneIds = Objects.requireNonNull(timeZoneIds);
            return this;
        }

        /**
         * Sets the time of this suggestion, in elapsed real-time since system boot.
         */
        @NonNull
        public Builder setElapsedRealtimeMillis(@ElapsedRealtimeLong long time) {
            mElapsedRealtimeMillis = time;
            return this;
        }

        /**
         * Builds a {@link TimeZoneProviderSuggestion} instance.
         */
        @NonNull
        public TimeZoneProviderSuggestion build() {
            return new TimeZoneProviderSuggestion(mTimeZoneIds, mElapsedRealtimeMillis);
        }
    }

    @NonNull
    private static List<String> immutableList(@NonNull List<String> list) {
        Objects.requireNonNull(list);
        if (list.isEmpty()) {
            return Collections.emptyList();
        } else {
            return Collections.unmodifiableList(new ArrayList<>(list));
        }
    }
}
