/*
 * Copyright (c) 2009-2012 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.network.serializing;

import com.jme3.math.Vector3f;
import com.jme3.network.message.ChannelInfoMessage;
import com.jme3.network.message.ClientRegistrationMessage;
import com.jme3.network.message.DisconnectMessage;
import com.jme3.network.message.GZIPCompressedMessage;
import com.jme3.network.message.ZIPCompressedMessage;
import com.jme3.network.serializing.serializers.*;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.*;
import java.util.jar.Attributes;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;

/**
 * The main serializer class, which will serialize objects such that
 *  they can be sent across the network. Serializing classes should extend
 *  this to provide their own serialization.
 *
 * @author Lars Wesselius
 */
public abstract class Serializer {
    protected static final Logger log = Logger.getLogger(Serializer.class.getName());

    private static final SerializerRegistration NULL_CLASS = new SerializerRegistration( null, Void.class, (short)-1 );

    private static final Map<Short, SerializerRegistration> idRegistrations         = new HashMap<Short, SerializerRegistration>();
    private static final Map<Class, SerializerRegistration> classRegistrations      = new HashMap<Class, SerializerRegistration>();
    private static final List<SerializerRegistration> registrations                 = new ArrayList<SerializerRegistration>();

    private static final Serializer                         fieldSerializer         = new FieldSerializer();
    private static final Serializer                         serializableSerializer  = new SerializableSerializer();
    private static final Serializer                         arraySerializer         = new ArraySerializer();

    private static short nextAvailableId = -2; // historically the first ID was always -2

    private static boolean strictRegistration = true;

    private static volatile boolean locked = false;
    

    // Registers the classes we already have serializers for.
    static {

        // Preregister some fixed serializers so that they don't move
        // if the list below is modified.  Automatic ID generation will
        // skip these IDs.    
        registerClassForId( DisconnectMessage.SERIALIZER_ID, DisconnectMessage.class, 
                            new DisconnectMessage.DisconnectSerializer() );
        registerClassForId( ClientRegistrationMessage.SERIALIZER_ID, ClientRegistrationMessage.class, 
                            new ClientRegistrationMessage.ClientRegistrationSerializer() );
         
    
    
        registerClass(boolean.class,   new BooleanSerializer());
        registerClass(byte.class,      new ByteSerializer());
        registerClass(char.class,      new CharSerializer());
        registerClass(short.class,     new ShortSerializer());
        registerClass(int.class,       new IntSerializer());
        registerClass(long.class,      new LongSerializer());
        registerClass(float.class,     new FloatSerializer());
        registerClass(double.class,    new DoubleSerializer());

        registerClass(Boolean.class,   new BooleanSerializer());
        registerClass(Byte.class,      new ByteSerializer());
        registerClass(Character.class, new CharSerializer());
        registerClass(Short.class,     new ShortSerializer());
        registerClass(Integer.class,   new IntSerializer());
        registerClass(Long.class,      new LongSerializer());
        registerClass(Float.class,     new FloatSerializer());
        registerClass(Double.class,    new DoubleSerializer());
        registerClass(String.class,    new StringSerializer());

        registerClass(Vector3f.class,  new Vector3Serializer());

        registerClass(Date.class,      new DateSerializer());
        
        // all the Collection classes go here
        registerClass(AbstractCollection.class,         new CollectionSerializer());
        registerClass(AbstractList.class,               new CollectionSerializer());
        registerClass(AbstractSet.class,                new CollectionSerializer());
        registerClass(ArrayList.class,                  new CollectionSerializer());
        registerClass(HashSet.class,                    new CollectionSerializer());
        registerClass(LinkedHashSet.class,              new CollectionSerializer());
        registerClass(LinkedList.class,                 new CollectionSerializer());
        registerClass(TreeSet.class,                    new CollectionSerializer());
        registerClass(Vector.class,                     new CollectionSerializer());
        
        // All the Map classes go here
        registerClass(AbstractMap.class,                new MapSerializer());
        registerClass(Attributes.class,                 new MapSerializer());
        registerClass(HashMap.class,                    new MapSerializer());
        registerClass(Hashtable.class,                  new MapSerializer());
        registerClass(IdentityHashMap.class,            new MapSerializer());
        registerClass(TreeMap.class,                    new MapSerializer());
        registerClass(WeakHashMap.class,                new MapSerializer());
 
        registerClass(Enum.class,      new EnumSerializer());
        registerClass(GZIPCompressedMessage.class, new GZIPSerializer());
        registerClass(ZIPCompressedMessage.class, new ZIPSerializer());

        registerClass(ChannelInfoMessage.class);
    }
    
    /**
     *  When set to true, classes that do not have intrinsic IDs in their
     *  @Serializable will not be auto-registered during write.  Defaults
     *  to true since this is almost never desired behavior with the way
     *  this code works.  Set to false to get the old permissive behavior.
     */
    public static void setStrictRegistration( boolean b ) {
        strictRegistration = b;
    }

    public static SerializerRegistration registerClass(Class cls) {
        return registerClass(cls, true);
    }
    
    public static void registerClasses(Class... classes) {
        for( Class c : classes ) {
            registerClass(c);
        }
    }
    
    private static short nextId() {
    
        // If the ID we are about to return is already in use
        // then skip it.
        while (idRegistrations.containsKey(nextAvailableId) ) {
            nextAvailableId--;   
        }
        
        // Return the available ID and post-decrement to get
        // ready for next time.    
        return nextAvailableId--;
    }
 
    /**
     *  Can put the registry in a read-only state such that additional attempts
     *  to register classes will fail.  This can be used by servers to lock the
     *  registry to avoid accidentally registering classes after a full registry
     *  set has been compiled.
     */
    public static void setReadOnly( boolean b ) {
        locked = b;
    }

    public static boolean isReadOnly() {
        return locked;
    }
 
    /**
     *  Directly registers a class for a specific ID.  Generally, use the regular
     *  registerClass() method.  This method is intended for framework code that might
     *  be maintaining specific ID maps across client and server.
     */
    public static SerializerRegistration registerClassForId( short id, Class cls, Serializer serializer ) {
 
        if( locked ) {
            throw new RuntimeException("Serializer registry locked trying to register class:" + cls);
        }
 
        SerializerRegistration reg = new SerializerRegistration(serializer, cls, id);        

        idRegistrations.put(id, reg);
        classRegistrations.put(cls, reg);
        
        log.log( Level.FINE, "Registered class[" + id + "]:{0} to:" + serializer, cls );

        serializer.initialize(cls);

        // Add the class after so that dependency order is preserved if the
        // serializer registers its own classes.
        registrations.add(reg);

        return reg;    
    }
    
    /**
     *  Registers the specified class. The failOnMiss flag controls whether or
     *  not this method returns null for failed registration or throws an exception.
     */
    @SuppressWarnings("unchecked")
    public static SerializerRegistration registerClass(Class cls, boolean failOnMiss) {
        if (cls.isAnnotationPresent(Serializable.class)) {
            Serializable serializable = (Serializable)cls.getAnnotation(Serializable.class);

            Class serializerClass = serializable.serializer();
            short classId = serializable.id();
            if (classId == 0) classId = nextId();

            Serializer serializer = getSerializer(serializerClass, false);

            if (serializer == null) serializer = fieldSerializer;

            SerializerRegistration existingReg = getExactSerializerRegistration(cls);

            if (existingReg != null) classId = existingReg.getId();
            
            SerializerRegistration reg = new SerializerRegistration(serializer, cls, classId);

            return registerClassForId( classId, cls, serializer );
        }
        if (failOnMiss) {
            throw new IllegalArgumentException( "Class is not marked @Serializable:" + cls );            
        }
        return null;
    }

    /**
     *  @deprecated This cannot be implemented in a reasonable way that works in
     *              all deployment methods.
     */
    @Deprecated
    public static SerializerRegistration[] registerPackage(String pkgName) {
        try {
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            String path = pkgName.replace('.', '/');
            Enumeration<URL> resources = classLoader.getResources(path);
            List<File> dirs = new ArrayList<File>();
            while (resources.hasMoreElements()) {
                URL resource = resources.nextElement();
                dirs.add(new File(resource.getFile()));
            }
            ArrayList<Class> classes = new ArrayList<Class>();
            for (File directory : dirs) {
                classes.addAll(findClasses(directory, pkgName));
            }

            SerializerRegistration[] registeredClasses = new SerializerRegistration[classes.size()];
            for (int i = 0; i != classes.size(); ++i) {
                Class clz = classes.get(i);
                registeredClasses[i] = registerClass(clz, false);
            }
            return registeredClasses;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new SerializerRegistration[0];
    }

    private static List<Class> findClasses(File dir, String pkgName) throws ClassNotFoundException {
        List<Class> classes = new ArrayList<Class>();
        if (!dir.exists()) {
            return classes;
        }
        File[] files = dir.listFiles();
        for (File file : files) {
            if (file.isDirectory()) {
                assert !file.getName().contains(".");
                classes.addAll(findClasses(file, pkgName + "." + file.getName()));
            } else if (file.getName().endsWith(".class")) {
                classes.add(Class.forName(pkgName + '.' + file.getName().substring(0, file.getName().length() - 6)));
            }
        }
        return classes;
    }

    public static SerializerRegistration registerClass(Class cls, Serializer serializer) {
        SerializerRegistration existingReg = getExactSerializerRegistration(cls);

        short id;
        if (existingReg != null) { 
            id = existingReg.getId();
        } else {
            id = nextId();
        }            
        return registerClassForId( id, cls, serializer );
    }

    public static Serializer getExactSerializer(Class cls) {
        return classRegistrations.get(cls).getSerializer();
    }

    public static Serializer getSerializer(Class cls) {
        return getSerializer(cls, true);
    }
    
    public static Serializer getSerializer(Class cls, boolean failOnMiss) {
        return getSerializerRegistration(cls, failOnMiss).getSerializer();
    }

    public static Collection<SerializerRegistration> getSerializerRegistrations() {
        return registrations;
    }

    public static SerializerRegistration getExactSerializerRegistration(Class cls) {
        return classRegistrations.get(cls);
    }
    
    public static SerializerRegistration getSerializerRegistration(Class cls) {
        return getSerializerRegistration(cls, strictRegistration); 
    }
    
    @SuppressWarnings("unchecked")
    public static SerializerRegistration getSerializerRegistration(Class cls, boolean failOnMiss) {
        SerializerRegistration reg = classRegistrations.get(cls);
        
        if (reg != null) return reg;

        for (Map.Entry<Class, SerializerRegistration> entry : classRegistrations.entrySet()) {
            if (entry.getKey().isAssignableFrom(Serializable.class)) continue;
            if (entry.getKey().isAssignableFrom(cls)) return entry.getValue();
        }

        if (cls.isArray()) return registerClass(cls, arraySerializer);

        if (Serializable.class.isAssignableFrom(cls)) { 
            return getExactSerializerRegistration(java.io.Serializable.class);
        }

        // See if the class could be safely auto-registered
        if (cls.isAnnotationPresent(Serializable.class)) {
            Serializable serializable = (Serializable)cls.getAnnotation(Serializable.class);
            short classId = serializable.id();
            if( classId != 0 ) {
                // No reason to fail because the ID is fixed
                failOnMiss = false;
            }
        }            
        
        if( failOnMiss ) {
            throw new IllegalArgumentException( "Class has not been registered:" + cls );
        }
        return registerClass(cls, fieldSerializer);
    }


    ///////////////////////////////////////////////////////////////////////////////////


    /**
     * Read the class from given buffer and return its SerializerRegistration.
     *
     * @param buffer The buffer to read from.
     * @return The SerializerRegistration, or null if non-existent.
     */
    public static SerializerRegistration readClass(ByteBuffer buffer) {
        short classID = buffer.getShort();
        if (classID == -1) return NULL_CLASS;
        return idRegistrations.get(classID);
    }

    /**
     * Read the class and the object.
     *
     * @param buffer Buffer to read from.
     * @return The Object that was read.
     * @throws IOException If serialization failed.
     */
    @SuppressWarnings("unchecked")
    public static Object readClassAndObject(ByteBuffer buffer) throws IOException {
        SerializerRegistration reg = readClass(buffer);
        if (reg == NULL_CLASS) return null;
        if (reg == null) throw new SerializerException( "Class not found for buffer data." );
        return reg.getSerializer().readObject(buffer, reg.getType());
    }

    /**
     * Write a class and return its SerializerRegistration.
     *
     * @param buffer The buffer to write the given class to.
     * @param type The class to write.
     * @return The SerializerRegistration that's registered to the class.
     */
    public static SerializerRegistration writeClass(ByteBuffer buffer, Class type) throws IOException {
        SerializerRegistration reg = getSerializerRegistration(type);
        if (reg == null) {
            throw new SerializerException( "Class not registered:" + type );
        }
        
        if( log.isLoggable(Level.FINER) ) {
            log.log(Level.FINER, "writing class:{0} with ID:{1}", new Object[]{type, reg.getId()});
        }
        buffer.putShort(reg.getId());
        return reg;
    }

    /**
     * Write the class and object.
     *
     * @param buffer The buffer to write to.
     * @param object The object to write.
     * @throws IOException If serializing fails.
     */
    public static void writeClassAndObject(ByteBuffer buffer, Object object) throws IOException {
        if (object == null) {
            buffer.putShort((short)-1);
            return;
        }
        SerializerRegistration reg = writeClass(buffer, object.getClass());
        
        // If the caller (or us) has registered a generic base class (like Enum)
        // that is meant to steer automatic resolution for things like FieldSerializer
        // that have final classes in fields... then there are cases where the exact 
        // type isn't known by the outer class.  (Think of a message object
        // that has an Object field but tries to send an Enum subclass in it.)
        // In that case, the SerializerRegistration object we get back isn't
        // really going to be capable of recreating the object on the other
        // end because it won't know what class to use.  This only comes up
        // in writeclassAndObejct() because we just wrote an ID to a more generic
        // class than will be readable on the other end.  The check is simple, though.
        if( reg.getType() != object.getClass() ) {
            throw new IllegalArgumentException("Class has not been registered:" 
                    + object.getClass() + " but resolved to generic serializer for:" + reg.getType());
        } 

        reg.getSerializer().writeObject(buffer, object);
    }

    /**
     * Read an object from the buffer, effectively deserializing it.
     *
     * @param data The buffer to read from.
     * @param c The class of the object.
     * @return The object read.
     * @throws IOException If deserializing fails.
     */
    public abstract <T> T readObject(ByteBuffer data, Class<T> c) throws IOException;

    /**
     * Write an object to the buffer, effectively serializing it.
     *
     * @param buffer The buffer to write to.
     * @param object The object to serialize.
     * @throws IOException If serializing fails.
     */
    public abstract void writeObject(ByteBuffer buffer, Object object) throws IOException;

    /**
     * Registration for when a serializer may need to cache something.
     *
     * Override to use.
     *
     * @param clazz The class that has been registered to the serializer.
     */
    public void initialize(Class clazz) { }
}
