// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.cronet_sample_apk;

import androidx.annotation.OptIn;

import org.chromium.net.ConnectionMigrationOptions;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Adding an option here will make it show up in the list of available options.
 * Each {@link Option} has the following attributes:
 * <ul>
 *   <li>A short name which appears in bold on the options list.</li>
 *  <li>A description which provides a thorough explanation of what this option does.</li>
 *  <li>An {@link Action} which is applied on CronetEngine's Builder each time the user hits "Reset
 * Engine". </li> <li>A default value, every option must have a default value.</li>
 * </ul>
 * <b>NOTE</b>: Each option must map to one {@link OptionsIdentifier OptionsIdentifier}. This is
 * necessary to provide custom implementation for options that does not configure the builders. See
 * {@link OptionsIdentifier#SLOW_DOWNLOAD} as an example.
 *
 * <p> To add a new option, do the following:
 * <ol>
 *  <li> Add a new optionIdentifier {@link OptionsIdentifier} </li>
 *  <li> Inject a new Option instance into your optionIdentifier enum value. </li>
 *  <li> Implement the logic for the new option within a new {@link Action}. </li>
 *  <li> If the {@link Action} interface is not enough to satisfy the use-case. Feel free to add
 * custom logic, See {@link OptionsIdentifier#SLOW_DOWNLOAD} as an example.</li>
 *  <li> Restart the APK and verify that your option is working as intended. </li>
 * </ol>
 */
public class Options {
    public enum OptionsIdentifier {
        MIGRATE_SESSIONS_ON_NETWORK_CHANGE_V2(
                new BooleanOption("migrate_sessions_on_network_change_v2",
                        "Enable QUIC connection migration. This only occurs when a network has "
                                + "completed"
                                + " disconnected and no longer reachable. QUIC will try to migrate "
                                + "the current session(maybe idle? depending on another option) to "
                                + "another network.",
                        new Action<>() {
                            @Override
                            public void configureBuilder(ActionData data, Boolean value) {
                                data.getMigrationBuilder().enableDefaultNetworkMigration(value);
                            }
                        },
                        false)),
        MIGRATION_SESSION_EARLY_V2(new BooleanOption("migrate_sessions_early_v2",
                "Enable QUIC early session migration. This will make quic send probing"
                        + " packets when the network is degrading, QUIC will migrate the "
                        + "sessions to a different network even before the original network "
                        + "has disconnected.",
                new Action<Boolean>() {
                    @Override
                    @OptIn(markerClass = ConnectionMigrationOptions.Experimental.class)
                    public void configureBuilder(ActionData data, Boolean value) {
                        data.getMigrationBuilder().enablePathDegradationMigration(value);
                        data.getMigrationBuilder().allowNonDefaultNetworkUsage(value);
                    }
                },
                false)),
        SLOW_DOWNLOAD(new BooleanOption("Slow Download (10s)",
                "Hang the onReadCompleted for 10s before proceeding. This should simulate slow connection.",
                new Action<>() {}, false));

        private final Option<?> mOption;
        OptionsIdentifier(Option<?> option) {
            this.mOption = option;
        }

        public Option<?> getOption() {
            return mOption;
        }
    }

    private static final Map<OptionsIdentifier, Option> OPTIONS =
            Collections.unmodifiableMap(createOptionsMap());
    private static final List<Option> OPTION_LIST = List.copyOf(OPTIONS.values());

    private static Map<OptionsIdentifier, Option> createOptionsMap() {
        Map<OptionsIdentifier, Option> optionsMap = new LinkedHashMap<>();
        for (OptionsIdentifier optionIdentifier : OptionsIdentifier.values()) {
            optionsMap.put(optionIdentifier, optionIdentifier.getOption());
        }
        return optionsMap;
    }

    public abstract static class Option<T> {
        private final String mOptionName;
        private final String mOptionDescription;
        private final Action<T> mAction;
        private T mOptionValue;
        private Option(
                String optionName, String optionDescription, Action<T> action, T defaultValue) {
            this.mOptionName = optionName;
            this.mOptionDescription = optionDescription;
            this.mAction = action;
            this.mOptionValue = defaultValue;
        }

        public String getShortName() {
            return mOptionName;
        }
        public String getDescription() {
            return mOptionDescription;
        }

        void configure(ActionData data) {
            mAction.configureBuilder(data, getValue());
        }

        abstract Class<T> getInputType();

        T getValue() {
            return mOptionValue;
        }

        void setValue(T newValue) {
            mOptionValue = newValue;
        }
    }

    public static class BooleanOption extends Option<Boolean> {
        private BooleanOption(String optionName, String optionDescription, Action<Boolean> action,
                Boolean defaultValue) {
            super(optionName, optionDescription, action, defaultValue);
        }

        @Override
        Class<Boolean> getInputType() {
            return Boolean.class;
        }
    }

    private Options() {}

    public static List<Option> getOptions() {
        return OPTION_LIST;
    }

    public static boolean isBooleanOptionOn(OptionsIdentifier identifier) {
        if (!OPTIONS.containsKey(identifier)) {
            throw new IllegalArgumentException(
                    "The provided identifier does not map to any option");
        }
        if (!OPTIONS.get(identifier).getInputType().equals(Boolean.class)) {
            throw new IllegalStateException("The provided identifier maps to a non-boolean value");
        }
        return (boolean) OPTIONS.get(identifier).getValue();
    }
}
