/*
 * Copyright (c) 2016, 2020 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package jakarta.json.bind;

import jakarta.json.bind.adapter.JsonbAdapter;

import jakarta.json.bind.config.PropertyNamingStrategy;
import jakarta.json.bind.config.PropertyVisibilityStrategy;

import jakarta.json.bind.serializer.JsonbDeserializer;
import jakarta.json.bind.serializer.JsonbSerializer;

import java.lang.reflect.Array;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;

/**
 * <p>
 * <a id="supportedProps"></a>
 * <b>Supported Properties</b><br>
 * <blockquote>
 * <p>
 * All JSON Binding providers are required to support the following set of properties.
 * Some providers may support additional properties.
 * <dl>
 *   <dt><code>jsonb.to.json.formatted</code> - java.lang.Boolean
 *   <dd>Controls whether or not the {@link jakarta.json.bind.Jsonb Jsonb} {@code toJson()}
 *       methods will format the resulting JSON data with line breaks and indentation. A
 *       true value for this property indicates human readable indented
 *       data, while a false value indicates unformatted data.
 *       Default value is false (unformatted) if this property is not specified.
 * </dl>
 * <dl>
 *   <dt><code>jsonb.to.json.encoding</code> - java.lang.String
 *   <dd>The {@link jakarta.json.bind.Jsonb Jsonb} serialization {@code toJson()} methods
 *       will default to this property for encoding of output JSON data. Default
 *       value is 'UTF-8' if this property is not specified.
 * </dl>
 * <dl>
 *   <dt><code>jsonb.from.json.encoding</code> - java.lang.String
 *   <dd>The {@link jakarta.json.bind.Jsonb Jsonb} deserialization {@code fromJson()}
 *       methods will default to this property encoding of input JSON data if the
 *       encoding cannot be detected.
 * </dl>
 * </blockquote>
 *
 * This object is not thread safe. Implementations are expected to make a defensive copy
 * of the object before applying the configuration.
 *
 * @since JSON Binding 1.0
 */
public class JsonbConfig {

    private final Map<String, Object> configuration = new HashMap<>();

    /**
     * Property used to specify whether or not the serialized
     * JSON data is formatted with line feeds and indentation.
     */
    public static final String FORMATTING = "jsonb.formatting";

    /**
     * The Jsonb serialization {@code toJson()} methods will default to this property
     * for encoding of output JSON data. Default value is 'UTF-8'.
     *
     * The Jsonb deserialization {@code fromJson()} methods will default to this
     * property encoding of input JSON data if the encoding cannot be detected
     * automatically.
     */
    public static final String ENCODING = "jsonb.encoding";

    /**
     * Property used to specify custom naming strategy.
     */
    public static final String PROPERTY_NAMING_STRATEGY = "jsonb.property-naming-strategy";

    /**
     * Property used to specify custom order strategy.
     */
    public static final String PROPERTY_ORDER_STRATEGY = "jsonb.property-order-strategy";

    /**
     * Property used to specify null values serialization behavior.
     */
    public static final String NULL_VALUES = "jsonb.null-values";

    /**
     * Property used to specify strict I-JSON serialization compliance.
     */
    public static final String STRICT_IJSON = "jsonb.strict-ijson";

    /**
     * Property used to specify custom visibility strategy.
     */
    public static final String PROPERTY_VISIBILITY_STRATEGY = "jsonb.property-visibility-strategy";

    /**
     * Property used to specify custom mapping adapters for generic types.
     */
    public static final String ADAPTERS = "jsonb.adapters";

    /**
     * Property used to specify custom serializers.
     */
    public static final String SERIALIZERS = "jsonb.serializers";

    /**
     * Property used to specify custom deserializers.
     */
    public static final String DESERIALIZERS = "jsonb.derializers";

    /**
     * Property used to specify custom binary data strategy.
     */
    public static final String BINARY_DATA_STRATEGY = "jsonb.binary-data-strategy";

    /**
     * Property used to specify custom date format globally.
     */
    public static final String DATE_FORMAT = "jsonb.date-format";

    /**
     * Property used to specify locale globally.
     */
    public static final String LOCALE = "jsonb.locale";

    /**
     * Set the particular configuration property to a new value. The method can
     * only be used to set one of the standard JSON Binding properties defined in
     * this class or a provider specific property.
     *
     * @param name
     *      The name of the property to be set. This value can either
     *      be specified using one of the constant fields or a user supplied
     *      string.
     * @param value
     *      The value of the property to be set
     *
     * @return This JsonbConfig instance.
     *
     * @throws NullPointerException if the name parameter is null.
     */
    public final JsonbConfig setProperty(final String name, final Object value) {
        configuration.put(name, value);
        return this;
    }

    /**
     * Return value of particular configuration property. The method can
     * only be used to retrieve one of the standard JSON Binding properties defined
     * in this class or a provider specific property. Attempting to get an undefined
     * property will result in an empty Optional value.
     * See <a href="#supportedProps"> Supported Properties</a>.
     *
     * @param name
     *      The name of the property to retrieve
     *
     * @return The value of the requested property
     *
     * @throws NullPointerException if the name parameter is null.
     */
    public final Optional<Object> getProperty(final String name) {
        return Optional.ofNullable(configuration.get(name));
    }

    /**
     * Return all configuration properties as an unmodifiable map.
     *
     * @return All configuration properties as an unmodifiable map
     */
    public final Map<String, Object> getAsMap() {
        return Collections.unmodifiableMap(configuration);
    }

    /**
     * Property used to specify whether or not the serialized JSON data is formatted
     * with linefeeds and indentation.
     *
     * Configures value of {@link #FORMATTING} property.
     *
     * @param formatted
     *      True means serialized data is formatted, false (default)
     *      means no formatting.
     *
     * @return This JsonbConfig instance.
     */
    public final JsonbConfig withFormatting(final Boolean formatted) {
        return setProperty(FORMATTING, formatted);
    }

    /**
     * Property used to specify whether null values should be serialized to JSON document or skipped.
     *
     * Configures value of {@link #NULL_VALUES} property.
     *
     * @param serializeNullValues
     *      True means that null values will be serialized into JSON document,
     *      otherwise they will be effectively skipped.
     *
     * @return This JsonbConfig instance.
     */
    public final JsonbConfig withNullValues(final Boolean serializeNullValues) {
        return setProperty(NULL_VALUES, serializeNullValues);
    }

    /**
     * The binding operations will default to this property
     * for encoding of JSON data. For input data (fromJson), selected encoding is used if
     * the encoding cannot be detected automatically. Default value is 'UTF-8'.
     *
     * Configures value of {@link #ENCODING} property.
     *
     * @param encoding
     *      Valid character encoding as defined in the
     *      <a href="http://tools.ietf.org/html/rfc7159">RFC 7159</a> and supported by
     *      Java Platform.
     *
     * @return This JsonbConfig instance.
     */
    public final JsonbConfig withEncoding(final String encoding) {
        return setProperty(ENCODING, encoding);
    }

    /**
     * Property used to specify whether strict I-JSON serialization compliance should be enforced.
     *
     * Configures value of {@link #STRICT_IJSON} property.
     *
     * @param enabled
     *      True means data is serialized in strict compliance according to RFC 7493.
     *
     * @return This JsonbConfig instance.
     */
    public final JsonbConfig withStrictIJSON(final Boolean enabled) {
        return setProperty(STRICT_IJSON, enabled);
    }

    /**
     * Property used to specify custom naming strategy.
     *
     * Configures value of {@link #PROPERTY_NAMING_STRATEGY} property.
     *
     * @param propertyNamingStrategy
     *      Custom naming strategy which affects serialization and deserialization.
     *
     * @return This JsonbConfig instance.
     */
    public final JsonbConfig withPropertyNamingStrategy(final PropertyNamingStrategy propertyNamingStrategy) {
        return setProperty(PROPERTY_NAMING_STRATEGY, propertyNamingStrategy);
    }

    /**
     * Property used to specify custom naming strategy.
     *
     * Configures value of {@link #PROPERTY_NAMING_STRATEGY} property.
     *
     * @param propertyNamingStrategy
     *      Predefined naming strategy which affects serialization and deserialization.
     *
     * @return This JsonbConfig instance.
     */
    public final JsonbConfig withPropertyNamingStrategy(final String propertyNamingStrategy) {
        return setProperty(PROPERTY_NAMING_STRATEGY, propertyNamingStrategy);
    }

    /**
     * Property used to specify property order strategy.
     *
     * Configures values of {@link #PROPERTY_ORDER_STRATEGY} property.
     *
     * @param propertyOrderStrategy
     *      Predefined property order strategy which affects serialization.
     *
     * @return This JsonbConfig instance.
     */
    public final JsonbConfig withPropertyOrderStrategy(final String propertyOrderStrategy) {
        return setProperty(PROPERTY_ORDER_STRATEGY, propertyOrderStrategy);
    }

    /**
     * Property used to specify custom property visibility strategy.
     *
     * Configures value of {@link #PROPERTY_VISIBILITY_STRATEGY} property.
     *
     * @param propertyVisibilityStrategy
     *      Custom property visibility strategy which affects serialization and deserialization.
     *
     * @return This JsonbConfig instance.
     */
    public final JsonbConfig withPropertyVisibilityStrategy(final PropertyVisibilityStrategy
                                                                    propertyVisibilityStrategy) {
        return setProperty(PROPERTY_VISIBILITY_STRATEGY, propertyVisibilityStrategy);
    }

    /**
     * Property used to specify custom mapping adapters.
     *
     * Configures value of {@link #ADAPTERS} property.
     *
     * Calling withAdapters more than once will merge the adapters with previous value.
     *
     * @param adapters
     *      Custom mapping adapters which affects serialization and deserialization.
     *
     * @return This JsonbConfig instance.
     */
    public final JsonbConfig withAdapters(final JsonbAdapter... adapters) {
        mergeProperties(ADAPTERS, adapters, JsonbAdapter.class);
        return this;
    }

    /**
     * Property used to specify custom serializers.
     *
     * Configures value of {@link #SERIALIZERS} property.
     *
     * Calling withSerializers more than once will merge the serializers with previous value.
     *
     * @param serializers
     *      Custom serializers which affects serialization.
     *
     * @return This JsonbConfig instance.
     */
    public final JsonbConfig withSerializers(final JsonbSerializer... serializers) {
        mergeProperties(SERIALIZERS, serializers, JsonbSerializer.class);
        return this;
    }

    /**
     * Property used to specify custom deserializers.
     *
     * Configures value of {@link #DESERIALIZERS} property.
     *
     * Calling withDeserializers more than once will merge the deserializers with previous value.
     *
     * @param deserializers
     *      Custom deserializers which affects deserialization.
     *
     * @return This JsonbConfig instance.
     */
    public final JsonbConfig withDeserializers(final JsonbDeserializer... deserializers) {
        mergeProperties(DESERIALIZERS, deserializers, JsonbDeserializer.class);
        return this;
    }

    /**
     * Property used to specify custom binary data strategy.
     *
     * Configures value of {@link #BINARY_DATA_STRATEGY} property.
     *
     * @param binaryDataStrategy
     *      Custom binary data strategy which affects serialization and deserialization.
     *
     * @return This JsonbConfig instance.
     */
    public final JsonbConfig withBinaryDataStrategy(final String binaryDataStrategy) {
        return setProperty(BINARY_DATA_STRATEGY, binaryDataStrategy);
    }

    /**
     * Property used to specify custom date format. This format will be used by default for all date classes
     * serialization and deserialization.
     *
     * Configures values of {@link #DATE_FORMAT} and {@link #LOCALE} properties.
     *
     * @param dateFormat
     *      Custom date format as specified in {@link java.time.format.DateTimeFormatter}.
     * @param locale
     *      Locale, if null is specified {@link Locale#getDefault} will be used.
     * @return This JsonbConfig instance.
     */
    public final JsonbConfig withDateFormat(final String dateFormat, final Locale locale) {
        return setProperty(DATE_FORMAT, dateFormat)
                .setProperty(LOCALE, locale != null ? locale : Locale.getDefault());
    }

    /**
     * Property used to specify custom locale.
     *
     * Configures value of {@link #LOCALE} property.
     *
     * @param locale
     *      Locale, must not be null.
     * @return This JsonbConfig instance.
     */
    public final JsonbConfig withLocale(final Locale locale) {
        return setProperty(LOCALE, locale);
    }

    @SuppressWarnings("unchecked")
    private <T> void mergeProperties(final String propertyKey, final T[] values, final Class<T> tClass) {
        final Optional<Object> property = getProperty(propertyKey);
        if (!property.isPresent()) {
            setProperty(propertyKey, values);
            return;
        }
        T[] storedValues = (T[]) property.get();
        T[] newValues = (T[]) Array.newInstance(tClass, storedValues.length + values.length);
        System.arraycopy(storedValues, 0, newValues, 0, storedValues.length);
        System.arraycopy(values, 0, newValues, storedValues.length, values.length);
        setProperty(propertyKey, newValues);
    }
}
