/*
 * Copyright (c) 2012-2014, Paul Merlin. All Rights Reserved.
 *
 * 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 org.qi4j.api.value;

import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import org.qi4j.api.composite.AmbiguousTypeException;
import org.qi4j.functional.Function;

/**
 * Use a ValueSerializer to serialize values state.
 *
 * <p>
 *     Serialized object must be one of:
 * </p>
 * <ul>
 *     <li>a ValueComposite,</li>
 *     <li>an EntityComposite or EntityReference,</li>
 *     <li>an Iterable,</li>
 *     <li>a Map,</li>
 *     <li>a Plain Value.</li>
 * </ul>
 * <p>
 *     Nested plain values, EntityReferences, Iterables, Maps, ValueComposites and EntityComposites are supported.
 *     EntityComposites and EntityReferences are serialized as their identity string.
 * </p>
 * <p>
 *     Plain values can be one of:
 * </p>
 * <ul>
 *     <li>String,</li>
 *     <li>Character or char,</li>
 *     <li>Boolean or boolean,</li>
 *     <li>Integer or int,</li>
 *     <li>Long or long,</li>
 *     <li>Short or short,</li>
 *     <li>Byte or byte,</li>
 *     <li>Float or float,</li>
 *     <li>Double or double,</li>
 *     <li>BigInteger,</li>
 *     <li>BigDecimal,</li>
 *     <li>Date,</li>
 *     <li>DateTime (JodaTime),</li>
 *     <li>LocalDateTime (JodaTime),</li>
 *     <li>LocalDate (JodaTime).</li>
 * </ul>
 * <p>
 *     Values of unknown types and all arrays are considered as {@link java.io.Serializable} and by so are serialized to
 *     base64 encoded bytes using pure Java serialization. If it happens that the value is not Serializable, a
 *     ValueSerializationException is thrown.
 * </p>
 * <p>
 *     Having type information in the serialized payload allows to keep actual ValueComposite types and by so
 *     circumvent {@link AmbiguousTypeException} when deserializing.
 * </p>
 */
public interface ValueSerializer
{

    /**
     * Factory method for a serialize function.
     *
     * @param <T> the parametrized function input type
     * @return a serialization function.
     */
    <T> Function<T, String> serialize();

    /**
     * Factory method for a serialize function.
     *
     * @param <T> the parametrized function input type
     * @param options ValueSerializer Options
     * @return a serialization function.
     */
    <T> Function<T, String> serialize( Options options );

    /**
     * Factory method for a serialize function.
     *
     * @param <T> the parametrized function input type
     * @param includeTypeInfo if type information should be included in the output
     * @return a serialization function.
     */
    @Deprecated
    <T> Function<T, String> serialize( boolean includeTypeInfo );

    /**
     * Serialize the state of a value with type information.
     *
     * @param object an Object to serialize
     * @return the state
     * @throws ValueSerializationException if the Value serialization failed
     */
    String serialize( Object object )
        throws ValueSerializationException;

    /**
     * Serialize the state of a value.
     *
     * @param options ValueSerializer Options
     * @param object an Object to serialize
     * @return the state
     * @throws ValueSerializationException if the Value serialization failed
     */
    String serialize( Options options, Object object )
        throws ValueSerializationException;

    /**
     * Serialize the state of a value.
     *
     * @param object an Object to serialize
     * @param includeTypeInfo if type information should be included in the output
     * @return the state
     * @throws ValueSerializationException if the Value serialization failed
     */
    @Deprecated
    String serialize( Object object, boolean includeTypeInfo )
        throws ValueSerializationException;

    /**
     * Serialize the state of a value with type information.
     *
     * @param object an Object to serialize
     * @param output that will be used as output
     * @throws ValueSerializationException if the Value serialization failed
     */
    void serialize( Object object, OutputStream output )
        throws ValueSerializationException;

    /**
     * Serialize the state of a value.
     *
     * @param options ValueSerializer Options
     * @param object an Object to serialize
     * @param output that will be used as output
     * @throws ValueSerializationException if the Value serialization failed
     */
    void serialize( Options options, Object object, OutputStream output )
        throws ValueSerializationException;

    /**
     * Serialize the state of a value.
     *
     * @param object an Object to serialize
     * @param output that will be used as output
     * @param includeTypeInfo if type information should be included in the output
     * @throws ValueSerializationException if the Value serialization failed
     */
    @Deprecated
    void serialize( Object object, OutputStream output, boolean includeTypeInfo )
        throws ValueSerializationException;

    /**
     * Serialization options.
     */
    final class Options
    {
        /**
         * Boolean flag to include type information.
         * Default to TRUE.
         */
        public static final String INCLUDE_TYPE_INFO = "includeTypeInfo";
        public static final String MAP_ENTRIES_AS_OBJECTS = "mapentriesasobjects";
        private final Map<String, String> options = new HashMap<>();

        /**
         * Create new default ValueSerializer Options.
         */
        public Options()
        {
            this.options.put( INCLUDE_TYPE_INFO, "true" );
            this.options.put( MAP_ENTRIES_AS_OBJECTS, "false" );
        }

        /**
         * Set {@link #INCLUDE_TYPE_INFO} option to TRUE.
         * @return This
         */
        public Options withTypeInfo()
        {
            return put( INCLUDE_TYPE_INFO, true );
        }

        /**
         * Set {@link #INCLUDE_TYPE_INFO} option to FALSE.
         * @return This
         */
        public Options withoutTypeInfo()
        {
            return put( INCLUDE_TYPE_INFO, false );
        }

        public Options withMapEntriesAsObjects()
        {
            return put( MAP_ENTRIES_AS_OBJECTS, true );
        }

        public Options withMapEntriesAsKeyValuePairs()
        {
            return put( MAP_ENTRIES_AS_OBJECTS, false );
        }

        /**
         * Get Boolean option value.
         * @param option The option
         * @return The boolean value of the option, or null if absent
         */
        public Boolean getBoolean( String option )
        {
            if( !options.containsKey( option ) )
            {
                return null;
            }
            return Boolean.valueOf( options.get( option ) );
        }

        /**
         * Get Integer option value.
         * @param option The option
         * @return The integer value of the option, or null if absent
         */
        public Integer getInteger( String option )
        {
            if( !options.containsKey( option ) )
            {
                return null;
            }
            return Integer.valueOf( options.get( option ) );
        }

        /**
         * Get String option value.
         * @param option The option
         * @return The string value of the option, or null if absent
         */
        public String getString( String option )
        {
            return options.get( option );
        }

        /**
         * Put an option String value.
         * @param option The option
         * @param value The value
         * @return This Options instance
         */
        public Options put( String option, String value )
        {
            if( value == null )
            {
                return remove( option );
            }
            options.put( option, value );
            return this;
        }

        /**
         * Put an option boolean value.
         * @param option The option
         * @param value The value
         * @return This Options instance
         */
        public Options put( String option, Boolean value )
        {
            if( value == null )
            {
                return remove( option );
            }
            options.put( option, Boolean.toString( value ) );
            return this;
        }

        /**
         * Put an option Integer value.
         * @param option The option
         * @param value The value
         * @return This Options instance
         */
        public Options put( String option, Integer value )
        {
            if( value == null )
            {
                return remove( option );
            }
            options.put( option, value.toString() );
            return this;
        }

        /**
         * Remove an option value.
         * @param option The option
         * @return This Options instance
         */
        public Options remove( String option )
        {
            options.remove( option );
            return this;
        }

        /**
         * Get all defined options as a Map.
         * @return All defined options in a new Map
         */
        public Map<String, String> toMap()
        {
            return new HashMap<>( options );
        }
    }
}
