/*
 * 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.flags;

import android.annotation.NonNull;
import android.content.Context;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.ArraySet;
import android.util.Log;

import com.android.internal.annotations.VisibleForTesting;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A class for querying constants from the system - primarily booleans.
 *
 * Clients using this class can define their flags and their default values in one place,
 * can override those values on running devices for debugging and testing purposes, and can control
 * what flags are available to be used on release builds.
 *
 * TODO(b/279054964): A lot. This is skeleton code right now.
 * @hide
 */
public class FeatureFlags {
    private static final String TAG = "FeatureFlags";
    private static FeatureFlags sInstance;
    private static final Object sInstanceLock = new Object();

    private final Set<Flag<?>> mKnownFlags = new ArraySet<>();
    private final Set<Flag<?>> mDirtyFlags = new ArraySet<>();

    private IFeatureFlags mIFeatureFlags;
    private final Map<String, Map<String, Boolean>> mBooleanOverrides = new HashMap<>();
    private final Set<ChangeListener> mListeners = new HashSet<>();

    /**
     * Obtain a per-process instance of FeatureFlags.
     * @return A singleton instance of {@link FeatureFlags}.
     */
    @NonNull
    public static FeatureFlags getInstance() {
        synchronized (sInstanceLock) {
            if (sInstance == null) {
                sInstance = new FeatureFlags();
            }
        }

        return sInstance;
    }

    /** See {@link FeatureFlagsFake}. */
    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public static void setInstance(FeatureFlags instance) {
        synchronized (sInstanceLock) {
            sInstance = instance;
        }
    }

    private final IFeatureFlagsCallback mIFeatureFlagsCallback = new IFeatureFlagsCallback.Stub() {
        @Override
        public void onFlagChange(SyncableFlag flag) {
            for (Flag<?> f : mKnownFlags) {
                if (flagEqualsSyncableFlag(f, flag)) {
                    if (f instanceof DynamicFlag<?>) {
                        if (f instanceof DynamicBooleanFlag) {
                            String value = flag.getValue();
                            if (value == null) {  // Null means any existing overrides were erased.
                                value = ((DynamicBooleanFlag) f).getDefault().toString();
                            }
                            addBooleanOverride(flag.getNamespace(), flag.getName(), value);
                        }
                        FeatureFlags.this.onFlagChange((DynamicFlag<?>) f);
                    }
                    break;
                }
            }
        }
    };

    private FeatureFlags() {
        this(null);
    }

    @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
    public FeatureFlags(IFeatureFlags iFeatureFlags) {
        mIFeatureFlags = iFeatureFlags;

        if (mIFeatureFlags != null) {
            try {
                mIFeatureFlags.registerCallback(mIFeatureFlagsCallback);
            } catch (RemoteException e) {
                // Shouldn't happen with things passed into tests.
                Log.e(TAG, "Could not register callbacks!", e);
            }
        }
    }

    /**
     * Construct a new {@link BooleanFlag}.
     *
     * Use this instead of constructing a {@link BooleanFlag} directly, as it registers the flag
     * with the internals of the flagging system.
     */
    @NonNull
    public static BooleanFlag booleanFlag(
            @NonNull String namespace, @NonNull String name, boolean def) {
        return getInstance().addFlag(new BooleanFlag(namespace, name, def));
    }

    /**
     * Construct a new {@link FusedOffFlag}.
     *
     * Use this instead of constructing a {@link FusedOffFlag} directly, as it registers the
     * flag with the internals of the flagging system.
     */
    @NonNull
    public static FusedOffFlag fusedOffFlag(@NonNull String namespace, @NonNull String name) {
        return getInstance().addFlag(new FusedOffFlag(namespace, name));
    }

    /**
     * Construct a new {@link FusedOnFlag}.
     *
     * Use this instead of constructing a {@link FusedOnFlag} directly, as it registers the flag
     * with the internals of the flagging system.
     */
    @NonNull
    public static FusedOnFlag fusedOnFlag(@NonNull String namespace, @NonNull String name) {
        return getInstance().addFlag(new FusedOnFlag(namespace, name));
    }

    /**
     * Construct a new {@link DynamicBooleanFlag}.
     *
     * Use this instead of constructing a {@link DynamicBooleanFlag} directly, as it registers
     * the flag with the internals of the flagging system.
     */
    @NonNull
    public static DynamicBooleanFlag dynamicBooleanFlag(
            @NonNull String namespace, @NonNull String name, boolean def) {
        return getInstance().addFlag(new DynamicBooleanFlag(namespace, name, def));
    }

    /**
     * Add a listener to be alerted when a {@link DynamicFlag} changes.
     *
     * See also {@link #removeChangeListener(ChangeListener)}.
     *
     * @param listener The listener to add.
     */
    public void addChangeListener(@NonNull ChangeListener listener) {
        mListeners.add(listener);
    }

    /**
     * Remove a listener that was added earlier.
     *
     * See also {@link #addChangeListener(ChangeListener)}.
     *
     * @param listener The listener to remove.
     */
    public void removeChangeListener(@NonNull ChangeListener listener) {
        mListeners.remove(listener);
    }

    protected void onFlagChange(@NonNull DynamicFlag<?> flag) {
        for (ChangeListener l : mListeners) {
            l.onFlagChanged(flag);
        }
    }

    /**
     * Returns whether the supplied flag is true or not.
     *
     * {@link BooleanFlag} should only be used in debug builds. They do not get optimized out.
     *
     * The first time a flag is read, its value is cached for the lifetime of the process.
     */
    public boolean isEnabled(@NonNull BooleanFlag flag) {
        return getBooleanInternal(flag);
    }

    /**
     * Returns whether the supplied flag is true or not.
     *
     * Always returns false.
     */
    public boolean isEnabled(@NonNull FusedOffFlag flag) {
        return false;
    }

    /**
     * Returns whether the supplied flag is true or not.
     *
     * Always returns true;
     */
    public boolean isEnabled(@NonNull FusedOnFlag flag) {
        return true;
    }

    /**
     * Returns whether the supplied flag is true or not.
     *
     * Can return a different value for the flag each time it is called if an override comes in.
     */
    public boolean isCurrentlyEnabled(@NonNull DynamicBooleanFlag flag) {
        return getBooleanInternal(flag);
    }

    private boolean getBooleanInternal(Flag<Boolean> flag) {
        sync();
        Map<String, Boolean> ns = mBooleanOverrides.get(flag.getNamespace());
        Boolean value = null;
        if (ns != null) {
            value = ns.get(flag.getName());
        }
        if (value == null) {
            throw new IllegalStateException("Boolean flag being read but was not synced: " + flag);
        }

        return value;
    }

    private <T extends Flag<?>> T addFlag(T flag)  {
        synchronized (FeatureFlags.class) {
            mDirtyFlags.add(flag);
            mKnownFlags.add(flag);
        }
        return flag;
    }

    /**
     * Sync any known flags that have not yet been synced.
     *
     * This is called implicitly when any flag is read, and is not generally needed except in
     * exceptional circumstances.
     */
    public void sync() {
        synchronized (FeatureFlags.class) {
            if (mDirtyFlags.isEmpty()) {
                return;
            }
            syncInternal(mDirtyFlags);
            mDirtyFlags.clear();
        }
    }

    /**
     * Called when new flags have been declared. Gives the implementation a chance to act on them.
     *
     * Guaranteed to be called from a synchronized, thread-safe context.
     */
    protected void syncInternal(Set<Flag<?>> dirtyFlags) {
        IFeatureFlags iFeatureFlags = bind();
        List<SyncableFlag> syncableFlags = new ArrayList<>();
        for (Flag<?> f : dirtyFlags) {
            syncableFlags.add(flagToSyncableFlag(f));
        }

        List<SyncableFlag> serverFlags = List.of();  // Need to initialize the list with something.
        try {
            // New values come back from the service.
            serverFlags = iFeatureFlags.syncFlags(syncableFlags);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }

        for (Flag<?> f : dirtyFlags) {
            boolean found = false;
            for (SyncableFlag sf : serverFlags) {
                if (flagEqualsSyncableFlag(f, sf)) {
                    if (f instanceof BooleanFlag || f instanceof DynamicBooleanFlag) {
                        addBooleanOverride(sf.getNamespace(), sf.getName(), sf.getValue());
                    }
                    found = true;
                    break;
                }
            }
            if (!found) {
                if (f instanceof BooleanFlag) {
                    addBooleanOverride(
                            f.getNamespace(),
                            f.getName(),
                            ((BooleanFlag) f).getDefault() ? "true" : "false");
                }
            }
        }
    }

    private void addBooleanOverride(String namespace, String name, String override) {
        Map<String, Boolean> nsOverrides = mBooleanOverrides.get(namespace);
        if (nsOverrides == null) {
            nsOverrides = new HashMap<>();
            mBooleanOverrides.put(namespace, nsOverrides);
        }
        nsOverrides.put(name, parseBoolean(override));
    }

    private SyncableFlag flagToSyncableFlag(Flag<?> f) {
        return new SyncableFlag(
                f.getNamespace(),
                f.getName(),
                f.getDefault().toString(),
                f instanceof DynamicFlag<?>);
    }

    private IFeatureFlags bind() {
        if (mIFeatureFlags == null) {
            mIFeatureFlags = IFeatureFlags.Stub.asInterface(
                    ServiceManager.getService(Context.FEATURE_FLAGS_SERVICE));
            try {
                mIFeatureFlags.registerCallback(mIFeatureFlagsCallback);
            } catch (RemoteException e) {
                Log.e(TAG, "Failed to listen for flag changes!");
            }
        }

        return mIFeatureFlags;
    }

    static boolean parseBoolean(String value) {
        // Check for a truish string.
        boolean result = value.equalsIgnoreCase("true")
                || value.equals("1")
                || value.equalsIgnoreCase("t")
                || value.equalsIgnoreCase("on");
        if (!result) {  // Expect a falsish string, else log an error.
            if (!(value.equalsIgnoreCase("false")
                    || value.equals("0")
                    || value.equalsIgnoreCase("f")
                    || value.equalsIgnoreCase("off"))) {
                Log.e(TAG,
                        "Tried parsing " + value + " as boolean but it doesn't look like one. "
                                + "Value expected to be one of true|false, 1|0, t|f, on|off.");
            }
        }
        return result;
    }

    private static boolean flagEqualsSyncableFlag(Flag<?> f, SyncableFlag sf) {
        return f.getName().equals(sf.getName()) && f.getNamespace().equals(sf.getNamespace());
    }


    /**
     * A simpler listener that is alerted when a {@link DynamicFlag} changes.
     *
     * See {@link #addChangeListener(ChangeListener)}
     */
    public interface ChangeListener {
        /**
         * Called when a {@link DynamicFlag} changes.
         *
         * @param flag The flag that has changed.
         */
        void onFlagChanged(DynamicFlag<?> flag);
    }
}