/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.generic;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Iterator;

import lombok.experimental.UtilityClass;
import net.ranides.assira.functional.Functions;
import net.ranides.assira.reflection.IClass;

/**
 * Helper methods for object serialization
 *
 * @author ranides
 */
@UtilityClass
public class SerializationUtils {

    /**
     * Clones object by serializing and deserializing it.
     * It won't create real copy if object is correctly implemented singleton.
     *
     * @param value value
     * @param <T> T
     * @return T
     * @throws IOException on error
     */
    @SuppressWarnings("unchecked")
    public static <T> T copy(T value) throws IOException {
        return read( (Class<T>)value.getClass(), write(value));
    }

    /**
     * Returns serialized object as binary data.
     *
     * @param value value
     * @return binary
     * @throws IOException on error
     */
    public static byte[] write(Object value) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        try (ObjectOutputStream ostream = new ObjectOutputStream(bos)) {
            ostream.writeObject(value);
            ostream.flush();
            return bos.toByteArray();
        }
    }

    /**
     * Serializes provided object into file
     *
     * @param target target
     * @param value value
     * @throws IOException on error
     */
    public static void write(Path target, Object value) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(target.toFile()) ){
            try (ObjectOutputStream ostream = new ObjectOutputStream(fos)) {
                ostream.writeObject(value);
                ostream.flush();
            }
        }
    }

    /**
     * Deserializes object from provided binary data and casts it into "type"
     *
     * @param type type
     * @param data data
     * @param <T> T
     * @return object
     * @throws IOException on error
     */
    public static <T> T read(Class<T> type, byte[] data) throws IOException {
        return type.cast(read(data));
    }

    /**
     * Deserializes object from provided file and casts it into "type"
     *
     * @param type type
     * @param input input
     * @param <T> T
     * @return object
     * @throws IOException on error
     */
    public static <T> T read(Class<T> type, Path input) throws IOException {
        return type.cast(read(input));
    }

    /**
     * Deserializes object from provided binary data and casts it into "type"
     *
     * @param type type
     * @param data data
     * @param <T> T
     * @return object
     * @throws IOException on error
     */
    public static <T> T read(IClass<T> type, byte[] data) throws IOException {
        return type.cast(read(data));
    }

    /**
     * Deserializes object from provided file and casts it into "type"
     *
     * @param type type
     * @param input input
     * @param <T> T
     * @return object
     * @throws IOException on error
     */
    public static <T> T read(IClass<T> type, Path input) throws IOException {
        return type.cast(read(input));
    }

    /**
     * Deserializes object from provided binary data and returns as Object
     *
     * @param data data
     * @return object
     * @throws IOException on error
     */
    public static Object read(byte[] data) throws IOException {
        try (ByteArrayInputStream ibs = new ByteArrayInputStream(data)) {
            try(ObjectInputStream istream = new ObjectInputStream(ibs)) {
                return istream.readObject();
            } catch(ClassNotFoundException cause) {
                throw new IOException(cause);
            }
        }
    }

    /**
     * Deserializes object from provided file and returns as Object
     *
     * @param input input
     * @return object
     * @throws IOException on error
     */
    public static Object read(Path input) throws IOException {
        try (FileInputStream ifs = new FileInputStream(input.toFile())) {
            try(ObjectInputStream istream = new ObjectInputStream(ifs)) {
                return istream.readObject();
            } catch(ClassNotFoundException cause) {
                throw new IOException(cause);
            }
        }
    }

    /**
     * This class creates serializable object which will try to recreate object during
     * deserialization using ordinary constructor. Constructor will be called with provided
     * arguments. Provided argument is used to find matching constructor, its state is irrelevant.
     *
     * Please note, that provided object does not need to be Serializable at all, altough
     * most of the time this method will be used inside "writeReplace", so obviously you have to
     * implement Serializable.
     *
     * @param that that
     * @param params params
     * @return serializable object
     */
    public static Serializable proxy(Object that, Object... params) {
        return new Proxy(that, params);
    }

    /**
     * This class creates serializable object which saves into stream expression passed as argument.
     * Provided expression will be executed at deserialization stage to recreate deserialized object.
     *
     * Can be used inside method {@code protected Object writeReplace()}.
     *
     * Please note, that {@code supplier} can't use {@code this} variable even implicitly.
     * Every value used inside expression must be copied into final local variable
     * and then used inside lambda.
     *
     * @param supplier supplier
     * @return serializable object
     */
    public static Serializable proxy(Functions.Function0<Object> supplier) {
        return new ProxyFunction(supplier);
    }

    /**
     * Returns information about size of binary data used to serialize provided values.
     *
     * It assumes every element has the same size;
     * It tries to estimate average size of element and overhead introduced by writing data.
     *
     * For example, if all values share big object, it will be written into stream only one.
     *
     * It is especially true for non-static inner classes.
     *
     * @param values values
     * @return size information
     * @throws IOException on error
     */
    public static SizeInfo sizeof(Collection<?> values) throws IOException {
        ByteArrayOutputStream orstream = new ByteArrayOutputStream();
        int s1 = values.size() / 2;
        int s2 = values.size();
        try(ObjectOutputStream ostream =  new ObjectOutputStream(orstream)) {
            Iterator<?> iterator = values.iterator();
            for(int i=0; i<s1; i++) {
                ostream.writeObject(iterator.next());
            }
            ostream.flush();
            int b1 = orstream.size();
            for(int i=s1; i<s2; i++) {
                ostream.writeObject(iterator.next());
            }
            ostream.flush();
            int b2 = orstream.size();
            
            int element = (b2-b1) / (s2-s1);
            int overhead = b1 - s1*element;
            return new SizeInfo(b2, element, overhead);
        }
    }

    /**
     * Holds information about size of serialized values.
     * It can't be instantiated directly, it is returned by {@link #sizeof(Collection)}
     */
    public static class SizeInfo {
        
        private final int size;
        private final int element;
        private final int overhead;

        private SizeInfo(int sum, int element, int overhead) {
            this.size = sum;
            this.element = element;
            this.overhead = overhead;
        }

        /**
         * Returns overall size of all values
         *
         * @return int
         */
        public int size() {
            return size;
        }

        /**
         * Returns average size of each element
         *
         * @return int
         */
        public int element() {
            return element;
        }

        /**
         * Returns size of data shared between all serialized elements.
         *
         * @return int
         */
        public int overhead() {
            return overhead;
        }

        @Override
        public String toString() {
            return String.format("SizeOf{ size=%d, element=%d, overhead=%d }", size(), element(), overhead());
        }

    }
    
    @SuppressWarnings("PMD")
    private static class Proxy implements Serializable {

        private static final long serialVersionUID = 4L;

        private final Class<?> clazz;
        private final Object[] params;

        public Proxy(Object that, Object... params) {
            this.clazz = that.getClass();
            this.params = params;
        }

        private Object readResolve() {
            return IClass.typeinfo(clazz).construct(params);
        }

    }

    private static class ProxyFunction implements Serializable {

        private static final long serialVersionUID = 4L;

        private final Functions.Function0<Object> supplier;

        public ProxyFunction(Functions.Function0<Object> supplier) {
            this.supplier = supplier;
        }

        private Object readResolve() {
            return supplier.get();
        }

    }

}
