package de.renew.util;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.StreamCorruptedException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;


/**
 * <p>
 * Helper class: supplies static methods to write/read instances
 * of <code>java.lang.reflect</code> classes to/from
 * ObjectInput/OutputStreams.
 * </p><p>
 * Also supplies static methods to write/read instances of
 * <code>java.lang.Class</code> to/from such Streams.
 * Unfortunately, <code>java.lang.Class</code> objects seem
 * to be serializable only if the class described by the
 * object is serializable. So this is a workaround for a bug(?)
 * in the package java.io of JDK 1.1.8. The workaround does
 * not seem to be necessary in JDK 1.2.
 * </p>
 *
 * ReflectionSerializer.java
 * <p>
 * Created: Tue Feb  1  2000
 *
 * @author Michael Duvigneau
 */
public class ReflectionSerializer {

    /** This class cannot be instantiated. */
    private ReflectionSerializer() {}

    // ---- FIELD ---------------------------------------------
    //
    // Uniquely identified by: Class    Declaring class
    //                         String   Field name

    /**
     * Write the specified field to the submitted Stream. The field will be uniquely identified by its declaring class
     * and its field name.
     * @param out the <code>ObjectOutputStream</code> to write to.
     * @param field the field to write to the stream. Might be a null object, in which case a null object is written to the stream.
     * @throws IOException from the submitted <code>ObjectOutputStream</code>
     */
    public static void writeField(ObjectOutputStream out, Field field) throws IOException {
        if (field == null) {
            out.writeObject(null);
        } else {
            writeClass(out, field.getDeclaringClass());
            out.writeObject(field.getName());
        }
    }

    /**
     * Read in some field from an ObjectInputStream. If a null object was written to the stream for the field, a null object will be returned.
     * @param in the ObjectInputStream from which to read.
     * @return a Field object from the ObjectInputStream. Might be a null object.
     * @throws IOException if the I/O operations on the stream fail
     * @throws ClassNotFoundException if the Class of the serialized object cannot be found
     */
    public static Field readField(ObjectInputStream in) throws IOException, ClassNotFoundException {
        try {
            Class<?> clazz = readClass(in);
            if (clazz == null) {
                return null;
            } else {
                String name = (String) in.readObject();
                return clazz.getField(name);
            }
        } catch (ClassCastException | NoSuchFieldException e) {
            throw new StreamCorruptedException(
                "(RRS) Could not read java.lang.reflect.Field: " + e);
        }
    }

    // ---- METHOD --------------------------------------------
    //
    // Uniquely identified by: Class    Declaring class
    //                         String   Method name
    //                         Class[]  Parameter types

    /**
     * Write out the submitted method object to the output stream. Methods are uniquely identified by their declaring
     * class, the string of their method names, and an array of all parameter types
     * @param out the output stream to use
     * @param method the method to write. may be a null object, in which case only a null object is written to the stream.
     * @throws IOException if the stream I/O fails
     */
    public static void writeMethod(ObjectOutputStream out, Method method) throws IOException {
        if (method == null) {
            out.writeObject(null);
        } else {
            writeClass(out, method.getDeclaringClass());
            out.writeObject(method.getName());
            writeClassArray(out, method.getParameterTypes());
        }
    }

    /**
     * Read in a method from the submitted input stream.
     * @param in the input stream to read from
     * @return null if the declaring class resolves to null. Otherwise, the method of the class.
     * @throws IOException if the I/O operations on the stream fail
     * @throws ClassNotFoundException if the Class of the serialized object cannot be found
     */
    public static Method readMethod(ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        try {
            Class<?> clazz = readClass(in);
            if (clazz == null) {
                return null;
            } else {
                String name = (String) in.readObject();
                Class<?>[] params = readClassArray(in);
                return clazz.getMethod(name, params);
            }
        } catch (ClassCastException e) {
            throw new StreamCorruptedException(
                "(RRS) Could not read java.lang.reflect.Method: " + e);
        } catch (NoSuchMethodException e) {
            throw new StreamCorruptedException(
                "(RRS) Could not read java.lang.reflectMethod: " + e);
        }
    }

    // ---- CONSTRUCTOR ---------------------------------------
    //
    // Uniquely identified by: Class    Declaring class
    //                         Class[]  Parameter types

    /**
     * Write out a constructor of some type to the submitted output stream. The constructor is uniquely identified by
     * its declaring Class object and the array of types of its parameters.
     * @param out the Output stream to use
     * @param constructor the constructor to write out. Might be a null object, in which case a single null object is written to the stream.
     * @throws IOException if the I/O operations on the stream fail
     */
    public static void writeConstructor(ObjectOutputStream out, Constructor<?> constructor)
        throws IOException
    {
        if (constructor == null) {
            out.writeObject(null);
        } else {
            writeClass(out, constructor.getDeclaringClass());
            writeClassArray(out, constructor.getParameterTypes());
        }
    }

    /**
     * Reads in a constructor from the submitted input stream.
     * @param in The input stream to read from
     * @return null if a null object was written out as a constructor. The constructor with its matching class type and parameters otherwise.
     * @throws IOException if the I/O operations on the stream fail
     * @throws ClassNotFoundException if the Class of the serialized object cannot be found
     */
    public static Constructor<?> readConstructor(ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        try {
            Class<?> clazz = readClass(in);
            if (clazz == null) {
                return null;
            } else {
                Class<?>[] params = readClassArray(in);
                return clazz.getConstructor(params);
            }
        } catch (ClassCastException | NoSuchMethodException e) {
            throw new StreamCorruptedException(
                "(RRS) Could not read java.lang.reflect.Constructor: " + e);
        }
    }

    // ---- CLASS ---------------------------------------------
    //
    // Uniquely identified by: String   fully qualified class name
    //
    // The primitive types (boolean, byte, char, double, float,
    // int, long, short, void) are represented by instances of
    // Class which refer to types not classes. They have to be
    // treated in a special way because ordinary class loading does not
    // work for them.

    /**
     *Write out the submitted class to the submitted output stream. The class is represented by its full class name. Primitive types
     * are treated as instances of class referring to types, which require special treatment on deserializing from the stream.
     * @param out the output stream to write to.
     * @param clazz the class object to write out by name. Might be a null object, in which case a null object is written to the stream.
     * @throws IOException if the I/O operations on the stream fail
     */
    public static void writeClass(ObjectOutputStream out, Class<?> clazz) throws IOException {
        if (clazz == null) {
            out.writeObject(null);
        } else {
            out.writeObject(clazz.getName());
        }
    }

    /**
     * Read in a class object from the submitted input stream, based on the name of the class. If the object read in
     * is of one of Java's primitive types, special handling applies to load the object. Otherwise, the currently configured
     * loader of {@link ClassSource} for class objects is invoked via {@link ClassSource#classForName(String)}.
     *
     * @param in the input stream to read from
     * @return the Class object
     * @throws IOException if the I/O operations on the stream fail
     * @throws ClassNotFoundException if the Class of the serialized object cannot be found
     */
    public static Class<?> readClass(ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        try {
            String name = (String) in.readObject();
            if (name == null) {
                return null;
            } else if (name.equals("boolean")) {
                return Boolean.TYPE;
            } else if (name.equals("byte")) {
                return Byte.TYPE;
            } else if (name.equals("char")) {
                return Character.TYPE;
            } else if (name.equals("double")) {
                return Double.TYPE;
            } else if (name.equals("float")) {
                return Float.TYPE;
            } else if (name.equals("int")) {
                return Integer.TYPE;
            } else if (name.equals("long")) {
                return Long.TYPE;
            } else if (name.equals("short")) {
                return Short.TYPE;
            } else if (name.equals("void")) {
                return Void.TYPE;
            } else {
                return ClassSource.classForName(name);
            }
        } catch (ClassCastException e) {
            throw new StreamCorruptedException("(RRS) Could not read java.lang.Class: " + e);
        }
    }

    // ---- CLASS[] -------------------------------------------
    //
    // The array itself is written as: Integer number of items
    //                                 Class   class 0 
    //                                 Class   class 1
    //                                 ...
    // The Class objects are written using writeClass/readClass.
    //
    // Compatibility with earlier streams:
    // If instead of an Integer object a Class[] object is found,
    // the object is returned immediately.

    /**
     * Writes out an array of class objects to the submitted output stream. A leading integer element identifies the size of the array to reading methods.
     * @param out  the output stream to write to
     * @param classes the array of Class objects to write out. Might be a null object, in which case, a single null object is written to the stream.
     * @throws IOException if the I/O operations on the stream fail
     */
    public static void writeClassArray(ObjectOutputStream out, Class<?>[] classes)
        throws IOException
    {
        if (classes == null) {
            out.writeObject(null);
        } else {
            out.writeObject(classes.length);
            for (Class<?> aClass : classes) {
                writeClass(out, aClass);
            }
        }
    }

    /**
     * Reads in an array of Class objects from the submitted input stream. Looks for a leading integer to identify how large the array of classes will be.
     * If no leading integer is found and instead an Object of type <code>Class[]</code> is found, assume legacy stream and return that object directly.
     * @param in the input stream to use
     * @return null object is a null object was written to the stream by writer method. Otherwise, the array of Class objects.
     * @throws IOException if the I/O operations on the stream fail
     * @throws ClassNotFoundException if the Class of the serialized object cannot be found
     */
    public static Class<?>[] readClassArray(ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        Object readObject = in.readObject();
        if (readObject == null) {
            return null;
        } else if (readObject instanceof Class<?>[]) {
            return (Class<?>[]) readObject;
        } else if (readObject instanceof Integer) {
            int count = (Integer) readObject;
            Class<?>[] classes = new Class<?>[count];
            for (int i = 0; i < count; i++) {
                classes[i] = readClass(in);
            }
            return classes;
        } else {
            throw new StreamCorruptedException(
                "(RRS) Could not read java.lang.Class[]: " + "Found "
                    + readObject.getClass().getName() + " instead.");
        }
    }
}