// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.microsoft.typespec.http.client.generator.core.extension.plugin;

import com.azure.json.JsonProviders;
import com.azure.json.JsonReader;
import com.azure.json.JsonToken;
import com.microsoft.typespec.http.client.generator.core.mapper.Mappers;
import com.microsoft.typespec.http.client.generator.core.mapper.azurevnext.AzureVNextMapperFactory;
import com.microsoft.typespec.http.client.generator.core.mapper.clientcore.ClientCoreMapperFactory;
import com.microsoft.typespec.http.client.generator.core.template.Templates;
import com.microsoft.typespec.http.client.generator.core.template.azurevnext.AzureVNextTemplateFactory;
import com.microsoft.typespec.http.client.generator.core.template.clientcore.ClientCoreTemplateFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.slf4j.Logger;

/**
 * Settings that are used by the Java AutoRest Generator.
 */
public class JavaSettings {
    private static final String VERSION = "4.0.0";
    private static JavaSettings instance;
    private static NewPlugin host;
    private static String header;
    private static final Map<String, Object> SIMPLE_JAVA_SETTINGS = new HashMap<>();
    private static Logger logger;
    private final boolean useKeyCredential;
    private final String flavor;
    private final boolean noCustomHeaders;
    private final boolean disableTypedHeadersMethods;
    private final boolean useRestProxy;

    static void setHeader(String value) {
        if ("MICROSOFT_MIT".equals(value)) {
            header = MICROSOFT_MIT_LICENSE_HEADER + "\n" + String.format(DEFAULT_CODE_GENERATION_HEADER, VERSION);
        } else if ("MICROSOFT_APACHE".equals(value)) {
            header = MICROSOFT_APACHE_LICENSE_HEADER + "\n" + String.format(DEFAULT_CODE_GENERATION_HEADER, VERSION);
        } else if ("MICROSOFT_MIT_NO_VERSION".equals(value)) {
            header = MICROSOFT_MIT_LICENSE_HEADER + "\n" + DEFAULT_CODE_GENERATION_HEADER_WITHOUT_VERSION;
        } else if ("MICROSOFT_MIT_SMALL_NO_VERSION".equals(value)) {
            header = MICROSOFT_MIT_SMALL_LICENSE_HEADER + "\n" + DEFAULT_CODE_GENERATION_HEADER_WITHOUT_VERSION;
        } else if ("MICROSOFT_APACHE_NO_VERSION".equals(value)) {
            header = MICROSOFT_APACHE_LICENSE_HEADER + "\n" + DEFAULT_CODE_GENERATION_HEADER_WITHOUT_VERSION;
        } else if ("MICROSOFT_MIT_NO_CODEGEN".equals(value)) {
            header = MICROSOFT_MIT_LICENSE_HEADER + "\n" + "Code generated by Microsoft (R) AutoRest Code Generator.";
        } else if ("NONE".equals(value)) {
            header = "";
        } else if ("MICROSOFT_MIT_SMALL".equals(value)) {
            header = MICROSOFT_MIT_SMALL_LICENSE_HEADER + "Code generated by Microsoft (R) AutoRest Code Generator.";
        } else if ("MICROSOFT_MIT_SMALL_TYPESPEC".equals(value)) {
            header = MICROSOFT_MIT_SMALL_LICENSE_HEADER + "Code generated by Microsoft (R) TypeSpec Code Generator.";
        } else if ("SMALL_TYPESPEC".equals(value)) {
            header = "Code generated by Microsoft (R) TypeSpec Code Generator.";
        } else if ("MICROSOFT_MIT_SMALL_NO_CODEGEN".equals(value)) {
            header = MICROSOFT_MIT_SMALL_LICENSE_HEADER;
        } else {
            header = value;
        }
    }

    static void setHost(NewPlugin host) {
        JavaSettings.host = host;
        logger = new PluginLogger(host, JavaSettings.class);
    }

    /**
     * Clear the JavaSettings instance.
     */
    public static void clear() {
        instance = null;
    }

    /**
     * Get the JavaSettings instance.
     *
     * @return The JavaSettings instance.
     */
    public static JavaSettings getInstance() {
        if (instance == null) {
            AutorestSettings autorestSettings = new AutorestSettings();
            loadStringSetting("title", autorestSettings::setTitle);
            loadStringOrArraySettingAsArray("security", autorestSettings::setSecurity);
            loadStringOrArraySettingAsArray("security-scopes", autorestSettings::setSecurityScopes);
            loadStringSetting("security-header-name", autorestSettings::setSecurityHeaderName);

            loadStringSetting("tag", autorestSettings::setTag);
            loadStringSetting("base-folder", autorestSettings::setBaseFolder);
            loadStringSetting("output-folder", autorestSettings::setOutputFolder);
            loadStringSetting("java-sdks-folder", autorestSettings::setJavaSdksFolder);
            // input-file
            List<String> inputFiles
                = host.getValueWithJsonReader("input-file", jsonReader -> jsonReader.readArray(JsonReader::getString));
            if (inputFiles != null) {
                autorestSettings.getInputFiles().addAll(inputFiles);
                logger.debug("List of input files : {}", autorestSettings.getInputFiles());
            }
            // require (readme.md etc.)
            List<String> require
                = host.getValueWithJsonReader("require", jsonReader -> jsonReader.readArray(JsonReader::getString));
            if (require != null) {
                autorestSettings.getRequire().addAll(require);
                logger.debug("List of require : {}", autorestSettings.getRequire());
            }

            setHeader(getStringValue(host, "license-header"));
            instance = new JavaSettings(autorestSettings);
        }
        return instance;
    }

    private static Map<Integer, String> parseStatusCodeMapping(JsonReader jsonReader) throws IOException {
        return jsonReader.readObject(reader -> {
            Map<Integer, String> mapping = new HashMap<>();
            while (reader.nextToken() != JsonToken.END_OBJECT) {
                int key = Integer.parseInt(reader.getFieldName());
                reader.nextToken();
                mapping.put(key, reader.getString());
            }

            return mapping;
        });
    }

    /**
     * Create a new JavaSettings object with the provided properties.
     *
     * @param autorestSettings The autorest settings.
     */
    private JavaSettings(AutorestSettings autorestSettings) {
        this.autorestSettings = autorestSettings;

        // The modeler settings.
        this.modelerSettings = new ModelerSettings(
            host.getValueWithJsonReader("modelerfour", jsonReader -> jsonReader.readMap(JsonReader::readUntyped)));

        // Whether to generate the SDK integration.
        this.sdkIntegration = getBooleanValue(host, "sdk-integration", false);

        // The fluent generation mode.
        String fluentString = getStringValue(host, "fluent");
        this.fluent = fluentString == null
            ? Fluent.NONE
            : (fluentString.isEmpty() || fluentString.equalsIgnoreCase("true")
                ? Fluent.PREMIUM
                : Fluent.valueOf(fluentString.toUpperCase(Locale.ROOT)));

        // Whether to regenerate the POM.
        this.regeneratePom = getBooleanValue(host, "regenerate-pom", false);

        // The file header text.
        this.fileHeaderText = header;

        // The service name.
        this.serviceName = getStringValue(host, "service-name");

        // The package keyword.
        this.packageName = getStringValue(host, "namespace", "com.azure.app").toLowerCase();

        // Whether to add client-side validations to the generated clients.
        this.clientSideValidations = getBooleanValue(host, "client-side-validations", false);

        // The prefix that will be added to each generated client type.
        this.clientTypePrefix = getStringValue(host, "client-type-prefix");

        // Whether interfaces will be generated for Service and Method Group clients.
        this.generateClientInterfaces = getBooleanValue(host, "generate-client-interfaces", false);

        // The sub-package that the Service and Method Group client implementation classes will be put into.
        this.implementationSubpackage = getStringValue(host, "implementation-subpackage", "implementation");

        // The brand name we use to generate SDK.
        this.flavor = getStringValue(host, "flavor", "azure");

        updateFlavorFactories();

        this.modelsSubpackage = getStringValue(host, "models-subpackage", isAzureV1() || isAzureV2() ? "models" : "");

        // The custom types that will be generated.
        String customTypes = getStringValue(host, "custom-types", "");
        this.customTypes = (customTypes == null || customTypes.isEmpty())
            ? new ArrayList<>()
            : Arrays.asList(customTypes.split(","));

        // The sub-package that custom types will be put into.
        this.customTypesSubpackage = getStringValue(host, "custom-types-subpackage", "");

        // The sub-package that Fluent interfaces will be put into.
        this.fluentSubpackage = getStringValue(host, "fluent-subpackage", "fluent");

        // Whether Service and Method Group client method overloads that omit optional parameters will be created.
        this.requiredParameterClientMethods = getBooleanValue(host, "required-parameter-client-methods", false);

        // Whether Service and Method Group clients will be generated with both synchronous and asynchronous methods.
        this.generateSyncAsyncClients = getBooleanValue(host, "generate-sync-async-clients", false);

        // Whether Service and Method Group clients will be generated as implementation
        this.generateClientAsImpl = getBooleanValue(host, "generate-client-as-impl", false)
            || this.generateSyncAsyncClients
            || this.generateClientInterfaces;

        // Whether a builder will be generated for each Service and Method Group client.
        this.generateBuilderPerClient = getBooleanValue(host, "generate-builder-per-client", false);

        // The sync methods generation mode.
        this.syncMethods = SyncMethodsGeneration.fromValue(getStringValue(host, "sync-methods", "essential"));

        // Whether to add a logger to the generated clients.
        this.clientLogger = getBooleanValue(host, "client-logger", false);

        // Whether required fields will be included in the constructor arguments for generated models.
        this.requiredFieldsAsConstructorArgs = getBooleanValue(host, "required-fields-as-ctor-args", false);

        // If set to true, proxy method service interface will be marked as public.
        this.serviceInterfaceAsPublic = getBooleanValue(host, "service-interface-as-public", true);

        // The artifactId for the generated project.
        this.artifactId = getStringValue(host, "artifact-id", "");

        // The types of credentials to generate.
        String credentialType = getStringValue(host, "credential-types", "none");
        if (credentialType != null) {
            String[] splits = credentialType.split(",");
            this.credentialTypes
                = Arrays.stream(splits).map(String::trim).map(CredentialType::fromValue).collect(Collectors.toSet());
        }

        // The scopes for the generated credential.
        String credentialScopes = getStringValue(host, "credential-scopes");
        if (credentialScopes != null) {
            String[] splits = credentialScopes.split(",");
            this.credentialScopes = Arrays.stream(splits).map(String::trim).map(split -> {
                if (!split.startsWith("\"")) {
                    split = "\"" + split + "\"";
                }
                return split;
            }).collect(Collectors.toSet());
        }

        // The path to the customization jar.
        this.customizationJarPath = getStringValue(host, "customization-jar-path");

        // The class to use for customization.
        this.customizationClass = getStringValue(host, "customization-class");

        // Whether to generate optional constants as enums.
        this.optionalConstantAsEnum = getBooleanValue(host, "optional-constant-as-enum", false);

        // Whether to generate a data plane client.
        this.dataPlaneClient = getBooleanValue(host, "data-plane", false);

        // Whether to use Iterable instead of List for collection types.
        this.useIterable = getBooleanValue(host, "use-iterable", false);

        // The versions of the service.
        this.serviceVersions
            = host.getValueWithJsonReader("service-versions", reader -> reader.readArray(JsonReader::getString));

        // The target for the <code>@JsonFlatten</code> annotation for x-ms-client-flatten.
        String clientFlattenAnnotationTarget = getStringValue(host, "client-flattened-annotation-target", "");
        this.clientFlattenAnnotationTarget
            = (clientFlattenAnnotationTarget == null || clientFlattenAnnotationTarget.isEmpty())
                ? ClientFlattenAnnotationTarget.TYPE
                : ClientFlattenAnnotationTarget.valueOf(clientFlattenAnnotationTarget.toUpperCase(Locale.ROOT));

        // The header name for the key credential.
        this.keyCredentialHeaderName = getStringValue(host, "key-credential-header-name", "");

        // Whether to disable the client builder.
        this.clientBuilderDisabled = getBooleanValue(host, "disable-client-builder", false);

        // The polling configuration.
        final Map<String, PollingSettings> operationPollingMapping
            = host.getValueWithJsonReader("polling", reader -> reader.readMap(PollingSettings::fromJson));
        if (operationPollingMapping != null && !operationPollingMapping.containsKey("default")) {
            operationPollingMapping.put("default", new PollingSettings());
        }
        this.operationPollingMapping = operationPollingMapping;

        // Whether to generate samples.
        this.generateSamples = getBooleanValue(host, "generate-samples", false);

        // Whether to generate tests.
        this.generateTests = getBooleanValue(host, "generate-tests", false);

        this.useRestProxy = getBooleanValue(host, "use-rest-proxy", false);

        // Whether to generate the send request method.
        this.generateSendRequestMethod = false;

        // If set to true, Jackson JsonGetter and JsonSetter will annotate getters and setters in generated models to
        // handle serialization and deserialization. For now, fields will continue being annotated to ensure that there
        // are no backwards compatibility breaks.
        this.annotateGettersAndSettersForSerialization
            = getBooleanValue(host, "annotate-getters-and-setters-for-serialization", false);

        // Error HTTP status code exception type handling.
        // The fully-qualified class that should be used as the default exception type. This class must extend from
        // HttpResponseException
        this.defaultHttpExceptionType = getStringValue(host, "default-http-exception-type");

        // Whether to use the default HTTP status code to exception type mapping.
        this.useDefaultHttpStatusCodeToExceptionTypeMapping
            = getBooleanValue(host, "use-default-http-status-code-to-exception-type-mapping", false);

        // A mapping of HTTP response status code to the exception type that should be thrown if that status code is
        // seen. All exception types must be fully-qualified and extend from HttpResponseException.
        this.httpStatusCodeToExceptionTypeMapping = host
            .getValueWithJsonReader("http-status-code-to-exception-type-mapping", JavaSettings::parseStatusCodeMapping);

        // Whether to handle partial updates.
        this.handlePartialUpdate = getBooleanValue(host, "partial-update", false);

        // If set to true, responses will only use Response, ResponseBase, PagedResponse, and PagedResponseBase types
        // with generics instead of creating a specific named type that extends one of those types.
        // If fluent default to false, this is because the automated test generation ends up with invalid code.
        // Once that is fixed, this can be switched over to true.
        this.genericResponseTypes = getBooleanValue(host, "generic-response-type", fluentString == null);

        // If set to true, models will handle serialization themselves using stream-style serialization instead of
        // relying on Jackson Databind.
        this.streamStyleSerialization = getBooleanValue(host, "stream-style-serialization", true);

        // If set to true, sync methods are generated using sync stack. i.e these methods do not use sync-over-async
        // stack.
        this.syncStackEnabled = getBooleanValue(host, "enable-sync-stack", false);

        // If set to true, the models that are determined as output only models will be made immutable without any
        // public constructors or setter methods.
        this.outputModelImmutable = getBooleanValue(host, "output-model-immutable", false);

        // Whether to use InputStream for binary responses.
        this.isInputStreamForBinary = getBooleanValue(host, "use-input-stream-for-binary", false);

        // If set to true, methods that have custom header types will also have an equivalent method that returns just
        // the response with untyped headers.
        this.noCustomHeaders = getBooleanValue(host, "no-custom-headers", true);

        // If set to true, read-only required properties will be included in the constructor if
        // {@code requiredFieldsAsConstructorArgs} is true. This is a backwards compatibility flag as previously
        // read-only required were included in constructors.
        this.includeReadOnlyInConstructorArgs = getBooleanValue(host, "include-read-only-in-constructor-args", false);

        // This generates all URLs as String type. This is enabled by default as required by the Java design guidelines.
        // For backward compatibility, this can be set to false.
        // setting the default as true as the Java design guideline recommends using String for URLs.
        this.urlAsString = getBooleanValue(host, "url-as-string", true);

        // Generate UUID as string.
        this.uuidAsString = getBooleanValue(host, "uuid-as-string", false);

        // If set to true, the required property annotation will be disabled.
        // setting this to false by default as a lot of existing libraries still use swagger and were generated with
        // required = true set in JsonProperty annotation
        this.disableRequiredJsonAnnotation = getBooleanValue(host, "disable-required-property-annotation", false);

        // If set to true, the generated client will have support for page size.
        this.pageSizeEnabled = getBooleanValue(host, "enable-page-size", false);

        // If set to true, the generated client will have support for key credential.
        this.useKeyCredential = getBooleanValue(host, "use-key-credential", false);

        // If set to true, {@code ArrayType.BYTE_ARRAY} will return an empty array instead of null when the default
        // value expression is null.
        this.nullByteArrayMapsToEmptyArray = getBooleanValue(host, "null-byte-array-maps-to-empty-array", false);

        // If set to true, the generated client will have support for GraalVM.
        this.generateGraalVmConfig = getBooleanValue(host, "graal-vm-config", false);

        // Prevents generating REST API methods that include typed headers. If set to true, {@code noCustomHeaders}
        // will be ignored as no REST APIs with typed headers will be generated.
        this.disableTypedHeadersMethods = getBooleanValue(host, "disable-typed-headers-methods", false);

        // Whether models implementing {@code JsonSerializable} can attempt to share code for toJson and fromJson.
        this.shareJsonSerializableCode = getBooleanValue(host, "share-jsonserializable-code", false);

        // Whether to use object for unknown.
        this.useObjectForUnknown = getBooleanValue(host, "use-object-for-unknown", false);
    }

    private void updateFlavorFactories() {
        if (isAzureV2()) {
            Mappers.setFactory(new AzureVNextMapperFactory());
            Templates.setFactory(new AzureVNextTemplateFactory());
        } else if (!isAzureV1()) {
            Mappers.setFactory(new ClientCoreMapperFactory());
            Templates.setFactory(new ClientCoreTemplateFactory());
        }
    }

    /**
     * Whether to generate with Azure V1.
     *
     * @return Whether to generate with Azure V1.
     */
    public boolean isAzureV1() {
        return "azure".equalsIgnoreCase(flavor);
    }

    /**
     * Whether to generate with Azure V2.
     *
     * @return Whether to generate with Azure V2.
     */
    public boolean isAzureV2() {
        return "azurev2".equalsIgnoreCase(this.flavor);
    }

    public boolean isUnbranded() {
        return !isAzureV1() && !isAzureV2();
    }

    public boolean useRestProxy() {
        return this.useRestProxy;
    }

    private final String keyCredentialHeaderName;

    /**
     * The header name for the key credential.
     *
     * @return The header name for the key credential.
     */
    public String getKeyCredentialHeaderName() {
        return this.keyCredentialHeaderName;
    }

    private Set<CredentialType> credentialTypes;

    /**
     * The types of credentials to generate.
     *
     * @return The types of credentials to generate.
     */
    public Set<CredentialType> getCredentialTypes() {
        return credentialTypes;
    }

    private Set<String> credentialScopes;

    /**
     * The scopes for the generated credential.
     *
     * @return The scopes for the generated credential.
     */
    public Set<String> getCredentialScopes() {
        return credentialScopes;
    }

    private final String artifactId;

    /**
     * The artifactId for the generated project.
     *
     * @return The artifactId for the generated project.
     */
    public String getArtifactId() {
        return artifactId;
    }

    /**
     * Whether to disable custom headers type generation.
     *
     * @return Whether to disable custom headers type generation.
     */
    public boolean isNoCustomHeaders() {
        return noCustomHeaders;
    }

    private final boolean urlAsString;

    /**
     * Whether to generate all URLs as String type. This is enabled by default as required by the Java design
     * guidelines. For backward compatibility, this can be set to false.
     *
     * @return Whether to generate all URLs as String type.
     */
    public boolean urlAsString() {
        return urlAsString;
    }

    private final boolean uuidAsString;

    /**
     * Whether to use string for uuid.
     *
     * @return Whether to use string for uuid.
     */
    public boolean uuidAsString() {
        return uuidAsString;
    }

    private final boolean disableRequiredJsonAnnotation;

    /**
     * Whether to disable the required property annotation.
     *
     * @return Whether to disable the required property annotation.
     */
    public boolean isDisableRequiredJsonAnnotation() {
        return disableRequiredJsonAnnotation;
    }

    /**
     * Represents Fluent generation mode.
     */
    public enum Fluent {
        /**
         * No Fluent generation.
         */
        NONE,
        /**
         * Fluent Lite generation.
         */
        LITE,
        /**
         * Fluent Premium generation.
         */
        PREMIUM;

        /**
         * Gets a {@link Fluent} value for the give {@code value} string.
         *
         * @param value The value to parse.
         * @return The {@link Fluent} value.
         */
        public static Fluent fromString(String value) {
            if (value == null || value.isEmpty()) {
                return null;
            }

            if ("none".equalsIgnoreCase(value)) {
                return NONE;
            } else if ("lite".equalsIgnoreCase(value)) {
                return LITE;
            } else if ("premium".equalsIgnoreCase(value)) {
                return PREMIUM;
            } else {
                return null;
            }
        }
    }

    private final Fluent fluent;

    /**
     * Whether to generate the Fluent.
     *
     * @return Whether to generate the Fluent.
     */
    public final boolean isFluent() {
        return fluent != Fluent.NONE;
    }

    /**
     * Whether to generate Fluent Lite.
     *
     * @return Whether to generate Fluent Lite.
     */
    public final boolean isFluentLite() {
        return fluent == Fluent.LITE;
    }

    /**
     * Whether to generate Fluent Premium.
     *
     * @return Whether to generate Fluent Premium.
     */
    public final boolean isFluentPremium() {
        return fluent == Fluent.PREMIUM;
    }

    // configure for model flatten in client

    /**
     * Represents the target for the <code>@JsonFlatten</code> annotation for x-ms-client-flatten.
     */
    public enum ClientFlattenAnnotationTarget {
        /**
         * JsonFlatten on class
         */
        TYPE,
        /**
         * JsonFlatten on class variable
         */
        FIELD,
        /**
         * Do not use @JsonFlatten. The model flatten is implemented as class variable getter/setter access the
         * flattened properties.
         */
        NONE,
        /**
         * Disable the model flatten
         */
        DISABLED
    }

    // target for @JsonFlatten annotation for x-ms-client-flatten
    private final ClientFlattenAnnotationTarget clientFlattenAnnotationTarget;

    /**
     * When flatten client mode, where to put the <code>@JsonFlatten</code> annotation. If NONE, flatten at
     * getter/setter methods via codegen.
     *
     * @return When flatten client mode, where to put the <code>@JsonFlatten</code> annotation. If NONE, flatten at
     * getter/setter methods via codegen.
     */
    public ClientFlattenAnnotationTarget getClientFlattenAnnotationTarget() {
        return this.clientFlattenAnnotationTarget;
    }

    /**
     * Represents the settings that are used by the modeler.
     */
    public static class ModelerSettings {
        private final Map<String, Object> settings;

        /**
         * Create a new ModelerSettings object with the provided settings.
         *
         * @param settings The settings that are used by the modeler.
         */
        public ModelerSettings(Map<String, Object> settings) {
            this.settings = settings == null ? Collections.emptyMap() : settings;
        }

        /**
         * Get the settings that are used by the modeler.
         *
         * @return The settings that are used by the modeler.
         */
        public Map<String, Object> getSettings() {
            return settings;
        }

        /**
         * If false, use client-flattened-annotation-target = TYPE for no flatten; client-flattened-annotation-target =
         * NONE for flatten at getter/setter methods via codegen.
         * <p>
         * If true, use client-flattened-annotation-target = TYPE for <code>@JsonFlatten</code> on type (i.e. on class);
         * client-flattened-annotation-target = FIELD for <code>@JsonFlatten</code> on field.
         * <p>
         * modelerfour.flatten-models = false and client-flattened-annotation-target = NONE would require
         * modelerfour.flatten-payloads = false.
         *
         * @return value of modelerfour.flatten-models
         */
        public boolean isFlattenModel() {
            return settings.containsKey("flatten-models") && (boolean) settings.get("flatten-models");
        }
    }

    private final ModelerSettings modelerSettings;

    /**
     * The settings that are used by the modeler.
     *
     * @return The settings that are used by the modeler.
     */
    public ModelerSettings getModelerSettings() {
        return modelerSettings;
    }

    private final AutorestSettings autorestSettings;

    /**
     * The settings that are used by the AutoRest generator.
     *
     * @return The settings that are used by the AutoRest generator.
     */
    public AutorestSettings getAutorestSettings() {
        return autorestSettings;
    }

    /**
     * The settings that are used by the AutoRest generator.
     *
     * @return The settings that are used by the AutoRest generator.
     */
    public Map<String, Object> getSimpleJavaSettings() {
        return SIMPLE_JAVA_SETTINGS;
    }

    private final boolean sdkIntegration;

    /**
     * Whether to generate the SDK integration.
     *
     * @return Whether to generate the SDK integration.
     */
    public boolean isSdkIntegration() {
        return sdkIntegration;
    }

    private final boolean regeneratePom;

    /**
     * Whether to regenerate the pom file.
     *
     * @return Whether to regenerate the pom file.
     */
    public final boolean isRegeneratePom() {
        return regeneratePom;
    }

    private final String fileHeaderText;

    /**
     * Get the file header text.
     *
     * @return The file header text.
     */
    public final String getFileHeaderText() {
        return fileHeaderText;
    }

    private final String serviceName;

    /**
     * Get the service name.
     *
     * @return The service name.
     */
    public final String getServiceName() {
        return serviceName;
    }

    private final String packageName;

    /**
     * Get the package name.
     *
     * @return The package name.
     */
    public final String getPackage() {
        return packageName;
    }

    /**
     * Get the package name with the provided package suffixes appended.
     *
     * @param packageSuffixes The package suffixes to append to the package name.
     * @return The package name with the provided package suffixes appended.
     */
    public final String getPackage(String... packageSuffixes) {
        return getPackageName(packageName, packageSuffixes);
    }

    /**
     * Get the package name with the provided package suffixes appended.
     *
     * @param packageName the package name.
     * @param packageSuffixes The package suffixes to append to the package name.
     * @return The package name with the provided package suffixes appended.
     */
    public final String getPackageName(String packageName, String... packageSuffixes) {
        StringBuilder packageBuilder = new StringBuilder(packageName);
        if (packageSuffixes != null) {
            for (String packageSuffix : packageSuffixes) {
                if (packageSuffix != null && !packageSuffix.isEmpty()) {
                    // Cleanse the package suffix to remove leading and trailing periods.
                    boolean startsWithPeriod = packageSuffix.startsWith(".");
                    boolean endsWithPeriod = packageSuffix.endsWith(".");

                    String cleansedPackageSuffix;
                    if (startsWithPeriod && endsWithPeriod) {
                        cleansedPackageSuffix = packageSuffix.substring(1, packageSuffix.length() - 1);
                    } else if (startsWithPeriod) {
                        cleansedPackageSuffix = packageSuffix.substring(1);
                    } else if (endsWithPeriod) {
                        cleansedPackageSuffix = packageSuffix.substring(0, packageSuffix.length() - 1);
                    } else {
                        cleansedPackageSuffix = packageSuffix;
                    }

                    packageBuilder.append(".").append(cleansedPackageSuffix);
                }
            }
        }
        return packageBuilder.toString();
    }

    private final boolean clientSideValidations;

    /**
     * Whether to add client side validations to the generated clients.
     *
     * @return Whether to add client side validations to the generated clients.
     */
    public final boolean isClientSideValidations() {
        return clientSideValidations;
    }

    /**
     * The prefix that will be added to each generated client type.
     */
    private final String clientTypePrefix;

    /**
     * The prefix that will be added to each generated client type.
     *
     * @return The prefix that will be added to each generated client type.
     */
    public final String getClientTypePrefix() {
        return clientTypePrefix;
    }

    /**
     * Whether interfaces will be generated for Service and Method Group clients.
     */
    private final boolean generateClientInterfaces;

    /**
     * Whether interfaces will be generated for Service and Method Group clients.
     *
     * @return Whether interfaces will be generated for Service and Method Group clients.
     */
    public final boolean isGenerateClientInterfaces() {
        return generateClientInterfaces;
    }

    /**
     * Whether interfaces will be generated for Service and Method Group clients.
     */
    private final boolean generateClientAsImpl;

    /**
     * Whether interfaces will be generated for Service and Method Group clients.
     *
     * @return Whether interfaces will be generated for Service and Method Group clients.
     */
    public final boolean isGenerateClientAsImpl() {
        return generateClientAsImpl;
    }

    /**
     * The sub-package that the Service and Method Group client implementation classes will be put into.
     */
    private final String implementationSubpackage;

    /**
     * The sub-package that the Service and Method Group client implementation classes will be put into.
     *
     * @return The sub-package that the Service and Method Group client implementation classes will be put into.
     */
    public final String getImplementationSubpackage() {
        return implementationSubpackage;
    }

    /**
     * The sub-package that Enums, Exceptions, and Model types will be put into.
     */
    private final String modelsSubpackage;

    /**
     * The sub-package that Enums, Exceptions, and Model types will be put into.
     *
     * @return The sub-package that Enums, Exceptions, and Model types will be put into.
     */
    public final String getModelsSubpackage() {
        return modelsSubpackage;
    }

    private final String fluentSubpackage;

    /**
     * The sub-package for Fluent SDK, that contains Client and Builder types, which is not recommended to be used
     * directly.
     *
     * @return The sub-package for Fluent SDK, that contains Client and Builder types, which is not recommended to be
     * used directly.
     */
    public final String getFluentSubpackage() {
        return fluentSubpackage;
    }

    /**
     * The sub-package for Fluent SDK, that contains Enums, Exceptions, and Model types, which is not recommended being
     * used directly.
     *
     * @return The sub-package for Fluent SDK, that contains Enums, Exceptions, and Model types, which is not
     * recommended being used directly.
     */
    public final String getFluentModelsSubpackage() {
        if (modelsSubpackage.contains(".")) {
            return fluentSubpackage + "." + modelsSubpackage.substring(modelsSubpackage.lastIndexOf(".") + 1);
        } else {
            return fluentSubpackage + "." + modelsSubpackage;
        }
    }

    /**
     * Whether Service and Method Group client method overloads that omit optional parameters will be created.
     */
    private final boolean requiredParameterClientMethods;

    /**
     * Whether Service and Method Group client method overloads that omit optional parameters will be created.
     *
     * @return Whether Service and Method Group client method overloads that omit optional parameters will be created.
     */
    public final boolean isRequiredParameterClientMethods() {
        return requiredParameterClientMethods;
    }

    private final boolean generateSyncAsyncClients;

    /**
     * Whether sync methods are generated using sync stack. i.e these methods do not use sync-over-async stack.
     *
     * @return Whether sync methods are generated using sync stack.
     */
    public final boolean isGenerateSyncAsyncClients() {
        return generateSyncAsyncClients;
    }

    /**
     * Whether sync methods are generated using sync stack. i.e these methods do not use sync-over-async stack.
     *
     * @return Whether sync methods are generated using sync stack.
     */
    public final boolean isSyncClientWrapAsyncClient() {
        return !syncStackEnabled;
    }

    private final SyncMethodsGeneration syncMethods;

    /**
     * Get the sync methods generation.
     *
     * @return The sync methods generation.
     */
    public final SyncMethodsGeneration getSyncMethods() {
        return syncMethods;
    }

    /**
     * Whether to generate async methods.
     *
     * @return Whether to generate async methods.
     */
    public final boolean isGenerateAsyncMethods() {
        SyncMethodsGeneration syncMethodsGeneration = getSyncMethods();
        return syncMethodsGeneration == SyncMethodsGeneration.ALL
            || syncMethodsGeneration == SyncMethodsGeneration.ESSENTIAL;
    }

    /**
     * Whether to generate sync methods.
     *
     * @return Whether to generate sync methods.
     */
    public final boolean isGenerateSyncMethods() {
        SyncMethodsGeneration syncMethodsGeneration = getSyncMethods();
        return syncMethodsGeneration == SyncMethodsGeneration.ALL
            || syncMethodsGeneration == SyncMethodsGeneration.SYNC_ONLY;
    }

    private final boolean requiredFieldsAsConstructorArgs;

    /**
     * Whether required fields will be included as constructor arguments.
     *
     * @return Whether required fields will be included as constructor arguments.
     */
    public boolean isRequiredFieldsAsConstructorArgs() {
        return requiredFieldsAsConstructorArgs;
    }

    private final boolean serviceInterfaceAsPublic;

    /**
     * Whether proxy method service interface will be marked as public.
     *
     * @return Whether proxy method service interface will be marked as public.
     */
    public boolean isServiceInterfaceAsPublic() {
        return serviceInterfaceAsPublic;
    }

    /**
     * Represents sync methods generation.
     */
    public enum SyncMethodsGeneration {
        /**
         * Generate all methods.
         */
        ALL,

        /**
         * Generate only essential methods.
         */
        ESSENTIAL,

        /**
         * Generate only sync methods.
         */
        SYNC_ONLY,  // SYNC_ONLY requires "enable-sync-stack"

        /**
         * Generate no methods.
         */
        NONE;

        /**
         * Convert the string value to the enum value.
         *
         * @param value The string value.
         * @return The enum value.
         */
        public static SyncMethodsGeneration fromValue(String value) {
            if (value == null) {
                return null;
            } else if (value.equals("all")) {
                return ALL;
            } else if (value.equals("essential")) {
                return ESSENTIAL;
            } else if (value.equals("none")) {
                return NONE;
            } else if (value.equals("sync-only")) {
                return SYNC_ONLY;
            }
            return null;
        }
    }

    private final List<String> customTypes;

    /**
     * The list of custom types.
     *
     * @return The list of custom types.
     */
    public List<String> getCustomTypes() {
        return customTypes;
    }

    /**
     * Whether the given type name is a custom type.
     *
     * @param typeName The type name.
     * @return Whether the given type name is a custom type.
     */
    public boolean isCustomType(String typeName) {
        return customTypes.contains(typeName);
    }

    private final String customTypesSubpackage;

    /**
     * The sub-package that custom types will be put into.
     *
     * @return The sub-package that custom types will be put into.
     */
    public final String getCustomTypesSubpackage() {
        return customTypesSubpackage;
    }

    /**
     * Represents the type of credential.
     */
    public enum CredentialType {
        /**
         * Token credential.
         */
        TOKEN_CREDENTIAL,

        /**
         * Azure key credential.
         */
        AZURE_KEY_CREDENTIAL,

        /**
         * No credential.
         */
        NONE;

        /**
         * Convert the string value to the enum value.
         *
         * @param value The string value.
         * @return The enum value.
         */
        public static CredentialType fromValue(String value) {
            if (value == null) {
                return null;
            } else if (value.equals("tokencredential")) {
                return TOKEN_CREDENTIAL;
            } else if (value.equals("azurekeycredential")) {
                return AZURE_KEY_CREDENTIAL;
            } else if (value.equals("none")) {
                return NONE;
            }
            return NONE;
        }
    }

    private final boolean clientLogger;

    /**
     * Whether to use client logger.
     *
     * @return Whether to use client logger.
     */
    public final boolean isUseClientLogger() {
        return clientLogger;
    }

    private final String customizationJarPath;

    /**
     * The path to the customization jar.
     *
     * @return The path to the customization jar.
     */
    public final String getCustomizationJarPath() {
        return customizationJarPath;
    }

    private final String customizationClass;

    /**
     * The fully qualified class name of the customization class.
     *
     * @return The fully qualified class name of the customization class.
     */
    public final String getCustomizationClass() {
        return customizationClass;
    }

    private final boolean optionalConstantAsEnum;

    /**
     * Whether to generate optional constants as enum.
     *
     * @return Whether to generate optional constants as enum.
     */
    public boolean isOptionalConstantAsEnum() {
        return optionalConstantAsEnum;
    }

    private final boolean dataPlaneClient;

    /**
     * Whether the client is a data plane client.
     *
     * @return Whether the client is a data plane client.
     */

    public boolean isDataPlaneClient() {
        return dataPlaneClient;
    }

    /**
     * Whether the client is vanilla client.
     *
     * @return Whether the client is a vanilla client.
     */
    public boolean isVanilla() {
        return isAzureV1() && !isDataPlaneClient() && !isFluent();
    }

    private final boolean useIterable;

    /**
     * Whether to use Iterable for list type properties.
     *
     * @return whether to use Iterable for list type properties.
     */
    public boolean isUseIterable() {
        return useIterable;
    }

    /**
     * Service version list. It maps to api-version parameter in swagger. Last one is the latest version, also default
     * version
     */
    private final List<String> serviceVersions;

    /**
     * Service version list. It maps to api-version parameter in swagger. Last one is the latest version, also
     * default version
     *
     * @return Service version list. It maps to api-version parameter in swagger. Last one is the latest version, also
     * default version
     */
    public List<String> getServiceVersions() {
        return serviceVersions;
    }

    private final boolean generateSamples;

    /**
     * Whether to generate samples.
     *
     * @return Whether to generate samples.
     */
    public boolean isGenerateSamples() {
        return generateSamples;
    }

    private final boolean generateTests;

    /**
     * Whether to generate tests.
     *
     * @return Whether to generate tests.
     */
    public boolean isGenerateTests() {
        return generateTests;
    }

    private final boolean generateSendRequestMethod;

    /**
     * Whether to generate the send request method.
     *
     * @return Whether to generate the send request method.
     */
    public boolean isGenerateSendRequestMethod() {
        return generateSendRequestMethod;
    }

    private final boolean syncStackEnabled;

    /**
     * Whether to enable sync stack.
     *
     * @return Whether to enable sync stack.
     */
    public boolean isSyncStackEnabled() {
        return syncStackEnabled;
    }

    private final boolean clientBuilderDisabled;

    /**
     * Whether to disable the client builder.
     *
     * @return Whether to disable the client builder.
     */
    public boolean clientBuilderDisabled() {
        return clientBuilderDisabled;
    }

    private final boolean outputModelImmutable;

    /**
     * Whether the models that are determined as output only models will be made immutable without any public
     * constructors or setter methods.
     *
     * @return Whether the models that are determined as output only models will be made immutable without any public
     * constructors or setter methods.
     */
    public boolean isOutputModelImmutable() {
        return outputModelImmutable;
    }

    private final boolean pageSizeEnabled;

    /**
     * Whether to enable page size.
     *
     * @return Whether to enable page size.
     */
    public boolean isPageSizeEnabled() {
        return pageSizeEnabled;
    }

    private final boolean generateGraalVmConfig;

    /**
     * Whether to generate a GraalVM configuration file.
     *
     * @return Whether to generate a GraalVM configuration file.
     */
    public boolean isGenerateGraalVmConfig() {
        return generateGraalVmConfig;
    }

    private final Map<String, PollingSettings> operationPollingMapping;

    /**
     * Gets the polling configuration for the specified operation.
     *
     * @param operationId The operation id.
     * @return The polling configuration for the specified operation, or the default polling configuration if no
     * configuration is specified for the operation.
     */
    public PollingSettings getPollingSettings(String operationId) {
        if (operationPollingMapping == null) {
            return null;
        }
        for (String key : operationPollingMapping.keySet()) {
            if (key.equalsIgnoreCase(operationId)) {
                return operationPollingMapping.get(key);
            }
        }
        return operationPollingMapping.get("default");
    }

    private final boolean annotateGettersAndSettersForSerialization;

    /**
     * Whether model getters and setters should be annotated with Jackson JsonGetter and JsonSetter to handle
     * serialization.
     * <p>
     * For now, Fields will continue to be annotated with JsonProperty to ensure there are no backwards compatibility
     * breaking changes.
     *
     * @return Whether model getters and setters should be annotated to handle serialization.
     */
    public boolean isGettersAndSettersAnnotatedForSerialization() {
        return annotateGettersAndSettersForSerialization;
    }

    private final String defaultHttpExceptionType;

    /**
     * Gets the fully-qualified exception type that is used for error HTTP status codes.
     *
     * @return The fully-qualified exception type.
     */
    public String getDefaultHttpExceptionType() {
        return defaultHttpExceptionType;
    }

    private final boolean useDefaultHttpStatusCodeToExceptionTypeMapping;

    /**
     * Whether to use the default error HTTP status code to exception type mapping.
     *
     * @return Whether to use the default error HTTP status code to exception type mapping.
     */
    public boolean isUseDefaultHttpStatusCodeToExceptionTypeMapping() {
        return useDefaultHttpStatusCodeToExceptionTypeMapping;
    }

    private final Map<Integer, String> httpStatusCodeToExceptionTypeMapping;

    /**
     * Gets a read-only view of the custom error HTTP status code to exception type mapping.
     *
     * @return A read-only view of the custom error HTTP status code to exception type mapping.
     */
    public Map<Integer, String> getHttpStatusCodeToExceptionTypeMapping() {
        return httpStatusCodeToExceptionTypeMapping == null
            ? null
            : Collections.unmodifiableMap(httpStatusCodeToExceptionTypeMapping);
    }

    private final boolean generateBuilderPerClient;

    /**
     * Whether a builder will be generated for each client.
     *
     * @return Whether a builder will be generated for each client.
     */
    public boolean isGenerateBuilderPerClient() {
        return generateBuilderPerClient;
    }

    private final boolean handlePartialUpdate;

    /**
     * Whether partial updates are handled.
     *
     * @return Whether partial updates are handled.
     */
    public boolean isHandlePartialUpdate() {
        return handlePartialUpdate;
    }

    private final boolean genericResponseTypes;

    /**
     * Whether Response, ResponseBase, PagedResponse, or PagedResponseBase will be used directly with generics instead
     * of creating a named type that extends one of those type.
     *
     * @return Whether generic response types are used instead of named types that extend the generic type.
     */
    public boolean isGenericResponseTypes() {
        return genericResponseTypes;
    }

    private final boolean streamStyleSerialization;

    /**
     * Whether models will handle serialization themselves using stream-style serialization instead of relying on
     * Jackson Databind.
     *
     * @return Whether models will handle serialization themselves.
     */
    public boolean isStreamStyleSerialization() {
        return streamStyleSerialization;
    }

    private final boolean isInputStreamForBinary;

    /**
     * Whether to use <code>InputStream</code> for binary in response body.
     *
     * @return If true, return <code>InputStream</code> for binary in response body. If false, return
     * <code>BinaryData</code>.
     */
    public boolean isInputStreamForBinary() {
        return isInputStreamForBinary;
    }

    private final boolean includeReadOnlyInConstructorArgs;

    /**
     * Whether required read-only properties will be included in constructor arguments.
     * <p>
     * In the past, required read-only properties were included in constructor arguments when
     * {@link #isRequiredFieldsAsConstructorArgs()} was true. This flag re-enables that capability when the property is
     * required and read-only and {@link #isRequiredFieldsAsConstructorArgs()} is true.
     * <p>
     * If this returns true but {@link #isRequiredFieldsAsConstructorArgs()} returns false this configuration does
     * nothing.
     *
     * @return Whether required read-only properties will be included in constructor arguments.
     */
    public boolean isIncludeReadOnlyInConstructorArgs() {
        return includeReadOnlyInConstructorArgs;
    }

    /**
     * Whether to use key credential for authentication.
     *
     * @return Whether to use key credential for authentication.
     */
    public boolean isUseKeyCredential() {
        return this.useKeyCredential;
    }

    private final boolean nullByteArrayMapsToEmptyArray;

    /**
     * Whether {@code ArrayType.BYTE_ARRAY} will return an empty array instead of null when the default value expression
     * is null.
     * <p>
     * Set this to true to ensure backwards compatibility with previous versions of the Java generator.
     *
     * @return Whether {@code ArrayType.BYTE_ARRAY} will return an empty array instead of null when the default value
     * expression is null.
     */
    public boolean isNullByteArrayMapsToEmptyArray() {
        return nullByteArrayMapsToEmptyArray;
    }

    /**
     * Determines whether REST API methods returning typed headers will be generated.
     * <p>
     * If set to true, {@link #isNoCustomHeaders()} and {@link #isGenericResponseTypes()} will be ignored as no REST
     * APIs returning typed headers will be generated. Meaning overloads without typed headers won't be generated and
     * since no methods with typed headers will exist there won't be usage of {@code ResponseBase}, or named subtypes of
     * it.
     * <p>
     * No matter the value, typed header classes will be generated. This can be useful if a majority of API calls don't
     * returned the typed headers at all but there are still cases where it would be useful to have them. Typed header
     * classes are simply created with {@code HttpHeaders} from {@code azure-core}.
     *
     * @return Whether REST APIs including typed headers should be generated.
     */
    public boolean isDisableTypedHeadersMethods() {
        return disableTypedHeadersMethods;
    }

    private final boolean shareJsonSerializableCode;

    /**
     * Whether code generation will attempt to share {@code toJson} and {@code fromJson} code generated for
     * {@code JsonSerializable} implementing models.
     * <p>
     * When this feature is enabled, the code generated for {@code toJson} and {@code fromJson} will attempt to be
     * shared across polymorphic types. This will only happen when all polymorphic models in the polymorphic hierarchy
     * are in the same package and meet a few criteria, such as no constructor arguments in the {@code fromJson} case.
     * The aim of this feature is to reduce the amount of code generated by {@code JsonSerializable}, which will aid in
     * testing all cases and should help the JVM determine more quickly when to JIT certain code.
     * <p>
     * This feature is still experimental, therefore is disabled by default. In the future this feature will become
     * enabled default, and then further out this feature will always be enabled without the ability to be configured.
     *
     * @return Whether code generation will attempt to share {@code toJson} and {@code fromJson} code generated for
     * {@code JsonSerializable} implementing models.
     */
    public boolean isShareJsonSerializableCode() {
        return shareJsonSerializableCode;
    }

    private final boolean useObjectForUnknown;

    public boolean isUseObjectForUnknown() {
        return useObjectForUnknown;
    }

    private static final String DEFAULT_CODE_GENERATION_HEADER
        = String.join("\n", "Code generated by Microsoft (R) AutoRest Code Generator %s",
            "Changes may cause incorrect behavior and will be lost if the code is regenerated.");

    private static final String DEFAULT_CODE_GENERATION_HEADER_WITHOUT_VERSION
        = String.join("\n", "Code generated by Microsoft (R) AutoRest Code Generator.",
            "Changes may cause incorrect behavior and will be lost if the code is regenerated.");

    private static final String MICROSOFT_APACHE_LICENSE_HEADER = String.join("\n",
        "Copyright (c) Microsoft and contributors.  All rights reserved.\n",
        "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",
        "  https://www.apache.org/licenses/LICENSE-2.0\n",
        "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.\n",
        "See the License for the specific language governing permissions and", "limitations under the License.", "");

    private static final String MICROSOFT_MIT_LICENSE_HEADER
        = String.join("\n", "Copyright (c) Microsoft Corporation. All rights reserved.",
            "Licensed under the MIT License. See License.txt in the project root for license information.", "");

    private static final String MICROSOFT_MIT_SMALL_LICENSE_HEADER = String.join("\n",
        "Copyright (c) Microsoft Corporation. All rights reserved.", "Licensed under the MIT License.", "");

    private static void loadStringSetting(String settingName, Consumer<String> action) {
        String settingValue = host.getStringValue(settingName);
        if (settingValue != null) {
            logger.debug("Option, string, {} : {}", settingName, settingValue);
            action.accept(settingValue);
        }
    }

    private static String getStringValue(NewPlugin host, String settingName) {
        return getStringValue(host, settingName, null);
    }

    private static String getStringValue(NewPlugin host, String settingName, String defaultValue) {
        String ret = host.getStringValue(settingName);
        if (ret == null) {
            return defaultValue;
        } else {
            logger.debug("Option, string, {} : {}", settingName, ret);
            SIMPLE_JAVA_SETTINGS.put(settingName, ret);
            return ret;
        }
    }

    private static boolean getBooleanValue(NewPlugin host, String settingName, boolean defaultValue) {
        Boolean ret = host.getBooleanValue(settingName);
        if (ret == null) {
            return defaultValue;
        } else {
            logger.debug("Option, boolean, {} : {}", settingName, ret);
            SIMPLE_JAVA_SETTINGS.put(settingName, ret);
            return ret;
        }
    }

    private static void loadStringOrArraySettingAsArray(String settingName, Consumer<List<String>> action) {
        host.getValue(settingName, jsonString -> {
            if (jsonString == null) {
                return null;
            } else if (jsonString.startsWith("[")) {
                // Array values will need to be parsed.
                try (JsonReader jsonReader = JsonProviders.createReader(jsonString)) {
                    List<String> settingValueList = jsonReader.readArray(JsonReader::getString);
                    logger.debug("Option, array, {} : {}", settingName, settingValueList);
                    action.accept(settingValueList);
                }
            } else {
                // Single values will be returned as the string representation.
                logger.debug("Option, string, {} : {}", settingName, jsonString);
                action.accept(Collections.singletonList(jsonString));
            }

            return null;
        });
    }
}
