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

import static android.annotation.SystemApi.Client.MODULE_LIBRARIES;

import android.annotation.SystemApi;
import android.compat.annotation.ChangeId;

import libcore.api.IntraCoreApi;

import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import libcore.util.NonNull;

/**
 * Internal APIs for logging and gating compatibility changes.
 *
 * @see ChangeId
 *
 * @hide
 */
@SystemApi(client = MODULE_LIBRARIES)
@IntraCoreApi
public final class Compatibility {

    private Compatibility() {}

    /**
     * Reports that a compatibility change is affecting the current process now.
     *
     * <p>Calls to this method from a non-app process are ignored. This allows code implementing
     * APIs that are used by apps and by other code (e.g. the system server) to report changes
     * regardless of the process it's running in. When called in a non-app process, this method is
     * a no-op.
     *
     * <p>Note: for changes that are gated using {@link #isChangeEnabled(long)}, you do not need to
     * call this API directly. The change will be reported for you in the case that
     * {@link #isChangeEnabled(long)} returns {@code true}.
     *
     * @param changeId The ID of the compatibility change taking effect.
     *
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    @IntraCoreApi
    public static void reportUnconditionalChange(@ChangeId long changeId) {
        sCallbacks.onChangeReported(changeId);
    }

    /**
     * Query if a given compatibility change is enabled for the current process. This method should
     * only be called by code running inside a process of the affected app.
     *
     * <p>If this method returns {@code true}, the calling code should implement the compatibility
     * change, resulting in differing behaviour compared to earlier releases. If this method returns
     * {@code false}, the calling code should behave as it did in earlier releases.
     *
     * <p>When this method returns {@code true}, it will also report the change as
     * {@link #reportUnconditionalChange(long)} would, so there is no need to call that method
     * directly.
     *
     * @param changeId The ID of the compatibility change in question.
     * @return {@code true} if the change is enabled for the current app.
     *
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    @IntraCoreApi
    public static boolean isChangeEnabled(@ChangeId long changeId) {
        return sCallbacks.isChangeEnabled(changeId);
    }

    private static final BehaviorChangeDelegate DEFAULT_CALLBACKS = new BehaviorChangeDelegate(){};

    private volatile static BehaviorChangeDelegate sCallbacks = DEFAULT_CALLBACKS;

    /**
     * Sets the behavior change delegate.
     *
     * All changes reported via the {@link Compatibility} class will be forwarded to this class.
     *
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public static void setBehaviorChangeDelegate(BehaviorChangeDelegate callbacks) {
        sCallbacks = Objects.requireNonNull(callbacks);
    }

    /**
     * Removes a behavior change delegate previously set via {@link #setBehaviorChangeDelegate}.
     *
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public static void clearBehaviorChangeDelegate() {
        sCallbacks = DEFAULT_CALLBACKS;
    }

    /**
     * Return the behavior change delegate
     *
     * @hide
     */
    // VisibleForTesting
    @NonNull
    public static BehaviorChangeDelegate getBehaviorChangeDelegate() {
        return sCallbacks;
    }

    /**
     * For use by tests only. Causes values from {@code overrides} to be returned instead of the
     * real value.
     *
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public static void setOverrides(ChangeConfig overrides) {
        // Setting overrides twice in a row does not need to be supported because
        // this method is only for enabling/disabling changes for the duration of
        // a single test.
        // In production, the app is restarted when changes get enabled or disabled,
        // and the ChangeConfig is then set exactly once on that app process.
        if (sCallbacks instanceof OverrideCallbacks) {
            throw new IllegalStateException("setOverrides has already been called!");
        }
        sCallbacks = new OverrideCallbacks(sCallbacks, overrides);
    }

    /**
     * For use by tests only. Removes overrides set by {@link #setOverrides}.
     *
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public static void clearOverrides() {
        if (!(sCallbacks instanceof OverrideCallbacks)) {
            throw new IllegalStateException("No overrides set");
        }
        sCallbacks = ((OverrideCallbacks) sCallbacks).delegate;
    }

    /**
     * Base class for compatibility API implementations. The default implementation logs a warning
     * to logcat.
     *
     * This is provided as a class rather than an interface to allow new methods to be added without
     * breaking @SystemApi binary compatibility.
     *
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    public interface BehaviorChangeDelegate {
        /**
         * Called when a change is reported via {@link Compatibility#reportUnconditionalChange}
         *
         * @hide
         */
        @SystemApi(client = MODULE_LIBRARIES)
        default void onChangeReported(long changeId) {
            // Do not use String.format here (b/160912695)
            System.logW("No Compatibility callbacks set! Reporting change " + changeId);
        }

        /**
         * Called when a change is queried via {@link Compatibility#isChangeEnabled}
         *
         * @hide
         */
        @SystemApi(client = MODULE_LIBRARIES)
        default boolean isChangeEnabled(long changeId) {
            // Do not use String.format here (b/160912695)
            // TODO(b/289900411): Rate limit this log if it's necessary in the release build.
            // System.logW("No Compatibility callbacks set! Querying change " + changeId);
            return true;
        }
    }

    /**
     * @hide
     */
    @SystemApi(client = MODULE_LIBRARIES)
    @IntraCoreApi
    public static final class ChangeConfig {
        private final Set<Long> enabled;
        private final Set<Long> disabled;

        /**
         * @hide
         */
        @SystemApi(client = MODULE_LIBRARIES)
        @IntraCoreApi
        public ChangeConfig(@NonNull Set<@NonNull Long> enabled, @NonNull Set<@NonNull Long> disabled) {
            this.enabled = Objects.requireNonNull(enabled);
            this.disabled = Objects.requireNonNull(disabled);
            if (enabled.contains(null)) {
                throw new NullPointerException();
            }
            if (disabled.contains(null)) {
                throw new NullPointerException();
            }
            Set<Long> intersection = new HashSet<>(enabled);
            intersection.retainAll(disabled);
            if (!intersection.isEmpty()) {
                throw new IllegalArgumentException("Cannot have changes " + intersection
                        + " enabled and disabled!");
            }
        }

        /**
         * @hide
         */
        @SystemApi(client = MODULE_LIBRARIES)
        @IntraCoreApi
        public boolean isEmpty() {
            return enabled.isEmpty() && disabled.isEmpty();
        }

        private static long[] toLongArray(Set<Long> values) {
            long[] result = new long[values.size()];
            int idx = 0;
            for (Long value: values) {
                result[idx++] = value;
            }
            return result;
        }

        /**
         * @hide
         */
        @SystemApi(client = MODULE_LIBRARIES)
        @IntraCoreApi
        public @NonNull long[] getEnabledChangesArray() {
            return toLongArray(enabled);
        }


        /**
         * @hide
         */
        @SystemApi(client = MODULE_LIBRARIES)
        @IntraCoreApi
        public @NonNull long[] getDisabledChangesArray() {
            return toLongArray(disabled);
        }


        /**
         * @hide
         */
        @SystemApi(client = MODULE_LIBRARIES)
        @IntraCoreApi
        public @NonNull Set<@NonNull Long> getEnabledSet() {
            return Collections.unmodifiableSet(enabled);
        }


        /**
         * @hide
         */
        @SystemApi(client = MODULE_LIBRARIES)
        @IntraCoreApi
        public @NonNull Set<@NonNull Long> getDisabledSet() {
            return Collections.unmodifiableSet(disabled);
        }


        /**
         * @hide
         */
        @SystemApi(client = MODULE_LIBRARIES)
        @IntraCoreApi
        public boolean isForceEnabled(long changeId) {
            return enabled.contains(changeId);
        }


        /**
         * @hide
         */
        @SystemApi(client = MODULE_LIBRARIES)
        @IntraCoreApi
        public boolean isForceDisabled(long changeId) {
            return disabled.contains(changeId);
        }


        /**
         * @hide
         */
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof ChangeConfig)) {
                return false;
            }
            ChangeConfig that = (ChangeConfig) o;
            return enabled.equals(that.enabled) &&
                    disabled.equals(that.disabled);
        }

        /**
         * @hide
         */
        @Override
        public int hashCode() {
            return Objects.hash(enabled, disabled);
        }


        /**
         * @hide
         */
        @Override
        public String toString() {
            return "ChangeConfig{enabled=" + enabled + ", disabled=" + disabled + '}';
        }
    }

    private static class OverrideCallbacks implements BehaviorChangeDelegate {
        private final BehaviorChangeDelegate delegate;
        private final ChangeConfig changeConfig;

        private OverrideCallbacks(BehaviorChangeDelegate delegate, ChangeConfig changeConfig) {
            this.delegate = Objects.requireNonNull(delegate);
            this.changeConfig = Objects.requireNonNull(changeConfig);
        }
        @Override
        public boolean isChangeEnabled(long changeId) {
           if (changeConfig.isForceEnabled(changeId)) {
               return true;
           }
           if (changeConfig.isForceDisabled(changeId)) {
               return false;
           }
           return delegate.isChangeEnabled(changeId);
        }
    }
}
