/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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 groovy.json;

import groovy.lang.Closure;
import groovy.transform.stc.ClosureParams;
import groovy.transform.stc.FromString;

import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;

/**
 * Generates JSON from objects.
 *
 * The {@link Options} builder can be used to configure an instance of a JsonGenerator.
 *
 * @see Options#build()
 * @since 2.5.0
 */
public interface JsonGenerator {

    /**
     * Converts an object to its JSON representation.
     *
     * @param object to convert to JSON
     * @return JSON
     */
    String toJson(Object object);

    /**
     * Indicates whether this JsonGenerator is configured to exclude fields by
     * the given name.
     *
     * @param name of the field
     * @return true if that field is being excluded, else false
     */
    boolean isExcludingFieldsNamed(String name);

    /**
     * Indicates whether this JsonGenerator is configured to exclude values
     * of the given object (may be {@code null}).
     *
     * @param value an instance of an object
     * @return true if values like this are being excluded, else false
     */
    boolean isExcludingValues(Object value);

    /**
     * Handles converting a given type.
     *
     * @since 2.5.0
     */
    interface Converter {

        /**
         * Returns {@code true} if this converter can handle conversions
         * of the given type.
         *
         * @param type the type of the object to convert
         * @return {@code true} if this converter can successfully convert values of
         *      the given type, else {@code false}
         */
        boolean handles(Class<?> type);

        /**
         * Converts a given object.
         *
         * @param value the object to convert
         * @param key the key name for the value, may be {@code null}
         * @return the converted object
         */
        Object convert(Object value, String key);

    }

    /**
     * A builder used to construct a {@link JsonGenerator} instance that allows
     * control over the serialized JSON output.  If you do not need to customize the
     * output it is recommended to use the static {@code JsonOutput.toJson} methods.
     *
     * <p>
     * Example:
     * <pre><code class="groovyTestCase">
     *     def generator = new groovy.json.JsonGenerator.Options()
     *                         .excludeNulls()
     *                         .dateFormat('yyyy')
     *                         .excludeFieldsByName('bar', 'baz')
     *                         .excludeFieldsByType(java.sql.Date)
     *                         .build()
     *
     *     def input = [foo: null, lastUpdated: Date.parse('yyyy-MM-dd', '2014-10-24'),
     *                   bar: 'foo', baz: 'foo', systemDate: new java.sql.Date(new Date().getTime())]
     *
     *     assert generator.toJson(input) == '{"lastUpdated":"2014"}'
     * </code></pre>
     *
     * @since 2.5.0
     */
    class Options {

        protected static final String JSON_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ";
        protected static final Locale JSON_DATE_FORMAT_LOCALE = Locale.US;
        protected static final String DEFAULT_TIMEZONE = "GMT";

        protected boolean excludeNulls;
        protected boolean disableUnicodeEscaping;
        protected String dateFormat = JSON_DATE_FORMAT;
        protected Locale dateLocale = JSON_DATE_FORMAT_LOCALE;
        protected TimeZone timezone = TimeZone.getTimeZone(DEFAULT_TIMEZONE);
        protected final Set<Converter> converters = new LinkedHashSet<Converter>();
        protected final Set<String> excludedFieldNames = new HashSet<String>();
        protected final Set<Class<?>> excludedFieldTypes = new HashSet<Class<?>>();

        public Options() {}

        /**
         * Do not serialize {@code null} values.
         *
         * @return a reference to this {@code Options} instance
         */
        public Options excludeNulls() {
            excludeNulls = true;
            return this;
        }

        /**
         * Disables the escaping of Unicode characters in JSON String values.
         *
         * @return a reference to this {@code Options} instance
         */
        public Options disableUnicodeEscaping() {
            disableUnicodeEscaping = true;
            return this;
        }

        /**
         * Sets the date format that will be used to serialize {@code Date} objects.
         * This must be a valid pattern for {@link java.text.SimpleDateFormat} and the
         * date formatter will be constructed with the default locale of {@link Locale#US}.
         *
         * @param format date format pattern used to serialize dates
         * @return a reference to this {@code Options} instance
         * @exception NullPointerException if the given pattern is null
         * @exception IllegalArgumentException if the given pattern is invalid
         */
        public Options dateFormat(String format) {
            return dateFormat(format, JSON_DATE_FORMAT_LOCALE);
        }

        /**
         * Sets the date format that will be used to serialize {@code Date} objects.
         * This must be a valid pattern for {@link java.text.SimpleDateFormat}.
         *
         * @param format date format pattern used to serialize dates
         * @param locale the locale whose date format symbols will be used
         * @return a reference to this {@code Options} instance
         * @exception IllegalArgumentException if the given pattern is invalid
         */
        public Options dateFormat(String format, Locale locale) {
            // validate date format pattern
            new SimpleDateFormat(format, locale);
            dateFormat = format;
            dateLocale = locale;
            return this;
        }

        /**
         * Sets the time zone that will be used to serialize dates.
         *
         * @param timezone used to serialize dates
         * @return a reference to this {@code Options} instance
         * @exception NullPointerException if the given timezone is null
         */
        public Options timezone(String timezone) {
            this.timezone = TimeZone.getTimeZone(timezone);
            return this;
        }

        /**
         * Registers a converter that will be called when a type it handles is encountered.
         *
         * @param converter to register
         * @return a reference to this {@code Options} instance
         */
        public Options addConverter(Converter converter) {
            if (converter != null) {
                converters.add(converter);
            }
            return this;
        }

        /**
         * Registers a closure that will be called when the specified type or subtype
         * is serialized.
         *
         * <p>The closure must accept either 1 or 2 parameters.  The first parameter
         * is required and will be instance of the {@code type} for which the closure
         * is registered.  The second optional parameter should be of type {@code String}
         * and, if available, will be passed the name of the key associated with this
         * value if serializing a JSON Object.  This parameter will be {@code null} when
         * serializing a JSON Array or when there is no way to determine the name of the key.
         *
         * <p>
         * Example:
         * <pre><code class="groovyTestCase">
         *     def generator = new groovy.json.JsonGenerator.Options()
         *                         .addConverter(URL) { URL u ->
         *                             u.getHost()
         *                         }
         *                         .build()
         *
         *     def input = [domain: new URL('http://groovy-lang.org/json.html#_parser_variants')]
         *
         *     assert generator.toJson(input) == '{"domain":"groovy-lang.org"}'
         * </code></pre>
         *
         * <p>If two or more closures are registered for the exact same type the last
         * closure based on the order they were specified will be used.  When serializing an
         * object its type is compared to the list of registered types in the order the were
         * given and the closure for the first suitable type will be called.  Therefore, it is
         * important to register more specific types first.
         *
         * @param type the type to convert
         * @param closure called when the registered type or any type assignable to the given
         *                type is encountered
         * @param <T> the type this converter is registered to handle
         * @return a reference to this {@code Options} instance
         * @exception NullPointerException if the given type or closure is null
         * @exception IllegalArgumentException if the given closure does not accept
         *                  a parameter of the given type
         */
        public <T> Options addConverter(Class<T> type,
                                        @ClosureParams(value=FromString.class, options={"T","T,String"})
                                        Closure<?> closure)
        {
            Converter converter = new DefaultJsonGenerator.ClosureConverter(type, closure);
            converters.remove(converter);
            return addConverter(converter);
        }

        /**
         * Excludes from the output any fields that match the specified names.
         *
         * @param fieldNames name of the field to exclude from the output
         * @return a reference to this {@code Options} instance
         */
        public Options excludeFieldsByName(CharSequence... fieldNames) {
            return excludeFieldsByName(Arrays.asList(fieldNames));
        }

        /**
         * Excludes from the output any fields that match the specified names.
         *
         * @param fieldNames collection of names to exclude from the output
         * @return a reference to this {@code Options} instance
         */
        public Options excludeFieldsByName(Iterable<? extends CharSequence> fieldNames) {
            for (CharSequence cs : fieldNames) {
                if (cs != null) {
                    excludedFieldNames.add(cs.toString());
                }
            }
            return this;
        }

        /**
         * Excludes from the output any fields whose type is the same or is
         * assignable to any of the given types.
         *
         * @param types excluded from the output
         * @return a reference to this {@code Options} instance
         */
        public Options excludeFieldsByType(Class<?>... types) {
            return excludeFieldsByType(Arrays.asList(types));
        }

        /**
         * Excludes from the output any fields whose type is the same or is
         * assignable to any of the given types.
         *
         * @param types collection of types to exclude from the output
         * @return a reference to this {@code Options} instance
         */
        public Options excludeFieldsByType(Iterable<Class<?>> types) {
            for (Class<?> c : types) {
                if (c != null) {
                    excludedFieldTypes.add(c);
                }
            }
            return this;
        }

        /**
         * Creates a {@link JsonGenerator} that is based on the current options.
         *
         * @return a fully configured {@link JsonGenerator}
         */
        public JsonGenerator build() {
            return new DefaultJsonGenerator(this);
        }
    }

}
