/*
 * Copyright 2013 Nicolas Morel
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.nmorel.gwtjackson.client;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.fasterxml.jackson.annotation.ObjectIdGenerator;
import com.github.nmorel.gwtjackson.client.exception.JsonSerializationException;
import com.github.nmorel.gwtjackson.client.ser.bean.AbstractBeanJsonSerializer;
import com.github.nmorel.gwtjackson.client.ser.bean.ObjectIdSerializer;
import com.github.nmorel.gwtjackson.client.stream.JsonWriter;
import com.github.nmorel.gwtjackson.client.stream.impl.FastJsonWriter;
import com.google.gwt.core.client.GWT;

/**
 * Context for the serialization process.
 *
 * @author Nicolas Morel
 * @version $Id: $
 */
public class JsonSerializationContext extends JsonMappingContext {

    /**
     * Builder for {@link JsonSerializationContext}. To override default settings globally, you can extend this class, modify the
     * default settings inside the constructor and tell the compiler to use your builder instead in your gwt.xml file :
     * <pre>
     * {@code
     *
     * <replace-with class="your.package.YourBuilder">
     *   <when-type-assignable class="com.github.nmorel.gwtjackson.client.JsonSerializationContext.Builder" />
     * </replace-with>
     *
     * }
     * </pre>
     */
    public static class Builder {

        protected boolean useEqualityForObjectId = false;

        protected boolean serializeNulls = true;

        protected boolean writeDatesAsTimestamps = true;

        protected boolean writeDateKeysAsTimestamps = false;

        protected boolean indent = false;

        protected boolean wrapRootValue = false;

        protected boolean writeCharArraysAsJsonArrays = false;

        protected boolean writeNullMapValues = true;

        protected boolean writeEmptyJsonArrays = true;

        protected boolean orderMapEntriesByKeys = false;

        protected boolean writeSingleElemArraysUnwrapped = false;

        protected boolean wrapExceptions = true;

        /**
         * @deprecated Use {@link JsonSerializationContext#builder()} instead. This constructor will be made protected in v1.0.
         */
        @Deprecated
        public Builder() { }

        /**
         * Determines whether Object Identity is compared using
         * true JVM-level identity of Object (false); or, <code>equals()</code> method.
         * Latter is sometimes useful when dealing with Database-bound objects with
         * ORM libraries (like Hibernate).
         * <p>
         * Option is disabled by default; meaning that strict identity is used, not
         * <code>equals()</code>
         * </p>
         *
         * @param useEqualityForObjectId true if should useEqualityForObjectId
         *
         * @return the builder
         */
        public Builder useEqualityForObjectId( boolean useEqualityForObjectId ) {
            this.useEqualityForObjectId = useEqualityForObjectId;
            return this;
        }

        /**
         * Sets whether object members are serialized when their value is null.
         * This has no impact on array elements. The default is true.
         *
         * @param serializeNulls true if should serializeNulls
         *
         * @return the builder
         */
        public Builder serializeNulls( boolean serializeNulls ) {
            this.serializeNulls = serializeNulls;
            return this;
        }

        /**
         * Determines whether {@link java.util.Date} and {@link java.sql.Timestamp} values are to be serialized as numeric timestamps
         * (true; the default), or as textual representation.
         * <p>If textual representation is used, the actual format is
         * {@link com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat#ISO_8601}</p>
         * Option is enabled by default.
         *
         * @param writeDatesAsTimestamps true if should writeDatesAsTimestamps
         *
         * @return the builder
         */
        public Builder writeDatesAsTimestamps( boolean writeDatesAsTimestamps ) {
            this.writeDatesAsTimestamps = writeDatesAsTimestamps;
            return this;
        }

        /**
         * Feature that determines whether {@link java.util.Date}s and {@link java.sql.Timestamp}s used as {@link java.util.Map} keys are
         * serialized as timestamps or as textual values.
         * <p>If textual representation is used, the actual format is
         * {@link com.google.gwt.i18n.client.DateTimeFormat.PredefinedFormat#ISO_8601}</p>
         * Option is disabled by default.
         *
         * @param writeDateKeysAsTimestamps true if should writeDateKeysAsTimestamps
         *
         * @return the builder
         */
        public Builder writeDateKeysAsTimestamps( boolean writeDateKeysAsTimestamps ) {
            this.writeDateKeysAsTimestamps = writeDateKeysAsTimestamps;
            return this;
        }

        /**
         * Feature that allows enabling (or disabling) indentation
         * for the underlying writer.
         * <p>Feature is disabled by default.</p>
         *
         * @param indent true if should indent
         *
         * @return the builder
         */
        public Builder indent( boolean indent ) {
            this.indent = indent;
            return this;
        }

        /**
         * Feature that can be enabled to make root value (usually JSON
         * Object but can be any type) wrapped within a single property
         * JSON object, where key as the "root name", as determined by
         * annotation introspector or fallback (non-qualified
         * class name).
         * <p>Feature is disabled by default.</p>
         *
         * @param wrapRootValue true if should wrapRootValue
         *
         * @return the builder
         */
        public Builder wrapRootValue( boolean wrapRootValue ) {
            this.wrapRootValue = wrapRootValue;
            return this;
        }

        /**
         * Feature that determines how type <code>char[]</code> is serialized:
         * when enabled, will be serialized as an explict JSON array (with
         * single-character Strings as values); when disabled, defaults to
         * serializing them as Strings (which is more compact).
         * <p>
         * Feature is disabled by default.
         * </p>
         *
         * @param writeCharArraysAsJsonArrays true if should writeCharArraysAsJsonArrays
         *
         * @return the builder
         */
        public Builder writeCharArraysAsJsonArrays( boolean writeCharArraysAsJsonArrays ) {
            this.writeCharArraysAsJsonArrays = writeCharArraysAsJsonArrays;
            return this;
        }

        /**
         * Feature that determines whether Map entries with null values are
         * to be serialized (true) or not (false).
         * <p>
         * Feature is enabled by default.
         * </p>
         *
         * @param writeNullMapValues true if should writeNullMapValues
         *
         * @return the builder
         */
        public Builder writeNullMapValues( boolean writeNullMapValues ) {
            this.writeNullMapValues = writeNullMapValues;
            return this;
        }

        /**
         * Feature that determines whether Container properties (POJO properties
         * with declared value of Collection or array; i.e. things that produce JSON
         * arrays) that are empty (have no elements)
         * will be serialized as empty JSON arrays (true), or suppressed from output (false).
         * <p>
         * Note that this does not change behavior of {@link java.util.Map}s, or
         * "Collection-like" types.
         * </p>
         * <p>
         * Feature is enabled by default.
         * </p>
         *
         * @param writeEmptyJsonArrays true if should writeEmptyJsonArrays
         *
         * @return the builder
         */
        public Builder writeEmptyJsonArrays( boolean writeEmptyJsonArrays ) {
            this.writeEmptyJsonArrays = writeEmptyJsonArrays;
            return this;
        }

        /**
         * Feature that determines whether {@link java.util.Map} entries are first
         * sorted by key before serialization or not: if enabled, additional sorting
         * step is performed if necessary (not necessary for {@link java.util.SortedMap}s),
         * if disabled, no additional sorting is needed.
         * <p>
         * Feature is disabled by default.
         * </p>
         *
         * @param orderMapEntriesByKeys true if should orderMapEntriesByKeys
         *
         * @return the builder
         */
        public Builder orderMapEntriesByKeys( boolean orderMapEntriesByKeys ) {
            this.orderMapEntriesByKeys = orderMapEntriesByKeys;
            return this;
        }

        /**
         * Feature added for interoperability, to work with oddities of
         * so-called "BadgerFish" convention.
         * Feature determines handling of single element {@link java.util.Collection}s
         * and arrays: if enabled, {@link java.util.Collection}s and arrays that contain exactly
         * one element will be serialized as if that element itself was serialized.
         * <br>
         * <br>
         * When enabled, a POJO with array that normally looks like this:
         * <pre>
         *  { "arrayProperty" : [ 1 ] }
         * </pre>
         * will instead be serialized as
         * <pre>
         *  { "arrayProperty" : 1 }
         * </pre>
         * <p>
         * Note that this feature is counterpart to {@link JsonDeserializationContext.Builder#acceptSingleValueAsArray(boolean)}
         * (that is, usually both are enabled, or neither is).
         * </p>
         * Feature is disabled by default, so that no special handling is done.
         *
         * @param writeSingleElemArraysUnwrapped true if should writeSingleElemArraysUnwrapped
         *
         * @return the builder
         */
        public Builder writeSingleElemArraysUnwrapped( boolean writeSingleElemArraysUnwrapped ) {
            this.writeSingleElemArraysUnwrapped = writeSingleElemArraysUnwrapped;
            return this;
        }

        /**
         * Feature that determines whether gwt-jackson code should catch
         * and wrap {@link RuntimeException}s (but never {@link Error}s!)
         * to add additional information about
         * location (within input) of problem or not. If enabled,
         * exceptions will be caught and re-thrown; this can be
         * convenient both in that all exceptions will be checked and
         * declared, and so there is more contextual information.
         * However, sometimes calling application may just want "raw"
         * unchecked exceptions passed as is.
         * <br>
         * <br>
         * Feature is enabled by default.
         *
         * @param wrapExceptions true if should wrapExceptions
         *
         * @return the builder
         */
        public Builder wrapExceptions( boolean wrapExceptions ) {
            this.wrapExceptions = wrapExceptions;
            return this;
        }

        public final JsonSerializationContext build() {
            return new JsonSerializationContext( useEqualityForObjectId, serializeNulls, writeDatesAsTimestamps,
                    writeDateKeysAsTimestamps, indent, wrapRootValue, writeCharArraysAsJsonArrays, writeNullMapValues,
                    writeEmptyJsonArrays, orderMapEntriesByKeys, writeSingleElemArraysUnwrapped, wrapExceptions );
        }
    }

    public static class DefaultBuilder extends Builder {

        private DefaultBuilder() { }

    }

    /**
     * <p>builder</p>
     *
     * @return a {@link com.github.nmorel.gwtjackson.client.JsonSerializationContext.Builder} object.
     */
    public static Builder builder() {
        return GWT.create( Builder.class );
    }

    private static final Logger logger = Logger.getLogger( "JsonSerialization" );

    private Map<Object, ObjectIdSerializer<?>> mapObjectId;

    private List<ObjectIdGenerator<?>> generators;

    /*
     * Serialization options
     */
    private final boolean useEqualityForObjectId;

    private final boolean serializeNulls;

    private final boolean writeDatesAsTimestamps;

    private final boolean writeDateKeysAsTimestamps;

    private final boolean indent;

    private final boolean wrapRootValue;

    private final boolean writeCharArraysAsJsonArrays;

    private final boolean writeNullMapValues;

    private final boolean writeEmptyJsonArrays;

    private final boolean orderMapEntriesByKeys;

    private final boolean writeSingleElemArraysUnwrapped;

    private final boolean wrapExceptions;

    private JsonSerializationContext( boolean useEqualityForObjectId, boolean serializeNulls, boolean writeDatesAsTimestamps, boolean
            writeDateKeysAsTimestamps, boolean indent, boolean wrapRootValue, boolean writeCharArraysAsJsonArrays, boolean
                                              writeNullMapValues, boolean writeEmptyJsonArrays, boolean orderMapEntriesByKeys, boolean
            writeSingleElemArraysUnwrapped,
                                      boolean wrapExceptions ) {
        this.useEqualityForObjectId = useEqualityForObjectId;
        this.serializeNulls = serializeNulls;
        this.writeDatesAsTimestamps = writeDatesAsTimestamps;
        this.writeDateKeysAsTimestamps = writeDateKeysAsTimestamps;
        this.indent = indent;
        this.wrapRootValue = wrapRootValue;
        this.writeCharArraysAsJsonArrays = writeCharArraysAsJsonArrays;
        this.writeNullMapValues = writeNullMapValues;
        this.writeEmptyJsonArrays = writeEmptyJsonArrays;
        this.orderMapEntriesByKeys = orderMapEntriesByKeys;
        this.writeSingleElemArraysUnwrapped = writeSingleElemArraysUnwrapped;
        this.wrapExceptions = wrapExceptions;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Logger getLogger() {
        return logger;
    }

    /**
     * <p>isSerializeNulls</p>
     *
     * @return a boolean.
     * @see Builder#serializeNulls(boolean)
     */
    public boolean isSerializeNulls() {
        return serializeNulls;
    }

    /**
     * <p>isWriteDatesAsTimestamps</p>
     *
     * @return a boolean.
     * @see Builder#writeDatesAsTimestamps(boolean)
     */
    public boolean isWriteDatesAsTimestamps() {
        return writeDatesAsTimestamps;
    }

    /**
     * <p>isWriteDateKeysAsTimestamps</p>
     *
     * @return a boolean.
     * @see Builder#writeDateKeysAsTimestamps(boolean)
     */
    public boolean isWriteDateKeysAsTimestamps() {
        return writeDateKeysAsTimestamps;
    }

    /**
     * <p>isWrapRootValue</p>
     *
     * @return a boolean.
     * @see Builder#wrapRootValue(boolean)
     */
    public boolean isWrapRootValue() {
        return wrapRootValue;
    }

    /**
     * <p>isWriteCharArraysAsJsonArrays</p>
     *
     * @return a boolean.
     * @see Builder#writeCharArraysAsJsonArrays(boolean)
     */
    public boolean isWriteCharArraysAsJsonArrays() {
        return writeCharArraysAsJsonArrays;
    }

    /**
     * <p>isWriteNullMapValues</p>
     *
     * @return a boolean.
     * @see Builder#writeNullMapValues(boolean)
     */
    public boolean isWriteNullMapValues() {
        return writeNullMapValues;
    }

    /**
     * <p>isWriteEmptyJsonArrays</p>
     *
     * @return a boolean.
     * @see Builder#writeEmptyJsonArrays(boolean)
     */
    public boolean isWriteEmptyJsonArrays() {
        return writeEmptyJsonArrays;
    }

    /**
     * <p>isOrderMapEntriesByKeys</p>
     *
     * @return a boolean.
     * @see Builder#orderMapEntriesByKeys(boolean)
     */
    public boolean isOrderMapEntriesByKeys() {
        return orderMapEntriesByKeys;
    }

    /**
     * <p>isWriteSingleElemArraysUnwrapped</p>
     *
     * @return a boolean.
     * @see Builder#writeSingleElemArraysUnwrapped(boolean)
     */
    public boolean isWriteSingleElemArraysUnwrapped() {
        return writeSingleElemArraysUnwrapped;
    }

    /**
     * <p>newJsonWriter</p>
     *
     * @return a {@link com.github.nmorel.gwtjackson.client.stream.JsonWriter} object.
     */
    public JsonWriter newJsonWriter() {
        JsonWriter writer = new FastJsonWriter( new StringBuilder() );
        writer.setLenient( true );
        if ( indent ) {
            writer.setIndent( "  " );
        }
        return writer;
    }

    /**
     * Trace an error and returns a corresponding exception.
     *
     * @param value current value
     * @param message error message
     *
     * @return a {@link JsonSerializationException} with the given message
     */
    public JsonSerializationException traceError( Object value, String message ) {
        getLogger().log( Level.SEVERE, message );
        return new JsonSerializationException( message );
    }

    /**
     * Trace an error with current writer state and returns a corresponding exception.
     *
     * @param value current value
     * @param message error message
     * @param writer current writer
     *
     * @return a {@link JsonSerializationException} with the given message
     */
    public JsonSerializationException traceError( Object value, String message, JsonWriter writer ) {
        JsonSerializationException exception = traceError( value, message );
        traceWriterInfo( value, writer );
        return exception;
    }

    /**
     * Trace an error and returns a corresponding exception.
     *
     * @param value current value
     * @param cause cause of the error
     *
     * @return a {@link JsonSerializationException} if we wrap the exceptions, the cause otherwise
     */
    public RuntimeException traceError( Object value, RuntimeException cause ) {
        getLogger().log( Level.SEVERE, "Error during serialization", cause );
        if ( wrapExceptions ) {
            return new JsonSerializationException( cause );
        } else {
            return cause;
        }
    }

    /**
     * Trace an error with current writer state and returns a corresponding exception.
     *
     * @param value current value
     * @param cause cause of the error
     * @param writer current writer
     *
     * @return a {@link JsonSerializationException} if we wrap the exceptions, the cause otherwise
     */
    public RuntimeException traceError( Object value, RuntimeException cause, JsonWriter writer ) {
        RuntimeException exception = traceError( value, cause );
        traceWriterInfo( value, writer );
        return exception;
    }

    /**
     * Trace the current writer state
     *
     * @param value current value
     */
    private void traceWriterInfo( Object value, JsonWriter writer ) {
        if ( getLogger().isLoggable( Level.INFO ) ) {
            getLogger().log( Level.INFO, "Error on value <" + value + ">. Current output : <" + writer.getOutput() + ">" );
        }
    }

    /**
     * <p>addObjectId</p>
     *
     * @param object a {@link java.lang.Object} object.
     * @param id a {@link com.github.nmorel.gwtjackson.client.ser.bean.ObjectIdSerializer} object.
     */
    public void addObjectId( Object object, ObjectIdSerializer<?> id ) {
        if ( null == mapObjectId ) {
            if ( useEqualityForObjectId ) {
                mapObjectId = new HashMap<Object, ObjectIdSerializer<?>>();
            } else {
                mapObjectId = new IdentityHashMap<Object, ObjectIdSerializer<?>>();
            }
        }
        mapObjectId.put( object, id );
    }

    /**
     * <p>getObjectId</p>
     *
     * @param object a {@link java.lang.Object} object.
     *
     * @return a {@link com.github.nmorel.gwtjackson.client.ser.bean.ObjectIdSerializer} object.
     */
    public ObjectIdSerializer<?> getObjectId( Object object ) {
        if ( null != mapObjectId ) {
            return mapObjectId.get( object );
        }
        return null;
    }

    /**
     * Used by generated {@link AbstractBeanJsonSerializer}
     *
     * @param generator instance of generator to add
     */
    @SuppressWarnings( "UnusedDeclaration" )
    public void addGenerator( ObjectIdGenerator<?> generator ) {
        if ( null == generators ) {
            generators = new ArrayList<ObjectIdGenerator<?>>();
        }
        generators.add( generator );
    }

    /**
     * Used by generated {@link AbstractBeanJsonSerializer}
     *
     * @param gen generator used to find equivalent generator
     * @param <T> a T object.
     *
     * @return a {@link com.fasterxml.jackson.annotation.ObjectIdGenerator} object.
     */
    @SuppressWarnings( {"UnusedDeclaration", "unchecked"} )
    public <T> ObjectIdGenerator<T> findObjectIdGenerator( ObjectIdGenerator<T> gen ) {
        if ( null != generators ) {
            for ( ObjectIdGenerator<?> generator : generators ) {
                if ( generator.canUseFor( gen ) ) {
                    return (ObjectIdGenerator<T>) generator;
                }
            }
        }
        return null;
    }
}