package org.apereo.cas.util.serialization;

import com.google.common.base.Throwables;
import org.apereo.cas.CipherExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;

/**
 * This is {@link SerializationUtils}
 * that encapsulates common serialization operations
 * in one spot.
 *
 * @author Timur Duehr timur.duehr@nccgroup.trust
 * @since 5.0.0
 */
public final class SerializationUtils {
    private static final Logger LOGGER = LoggerFactory.getLogger(SerializationUtils.class);
    
    private SerializationUtils() {
    }

    /**
     * Serialize an object.
     *
     * @param object The object to be serialized
     * @return the +byte[]+ containing the object
     * @since 5.0.0
     */
    public static byte[] serialize(final Serializable object) {
        final ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
        serialize(object, outBytes);
        return outBytes.toByteArray();
    }

    /**
     * Serialize an object.
     *
     * @param object       The object to be serialized
     * @param outputStream The stream to receive the object
     * @since 5.0.0
     */
    public static void serialize(final Serializable object, final OutputStream outputStream) {
        try (ObjectOutputStream out = new ObjectOutputStream(outputStream)) {
            out.writeObject(object);
        } catch (final IOException e) {
            throw Throwables.propagate(e);
        }
    }

    /**
     * Deserialize an object.
     *
     * @param <T>     the type parameter
     * @param inBytes The bytes to be deserialized
     * @return the object
     * @since 5.0.0
     */
    public static <T> T deserialize(final byte[] inBytes) {
        final ByteArrayInputStream inputStream = new ByteArrayInputStream(inBytes);
        return deserialize(inputStream);
    }

    /**
     * Deserialize an object.
     *
     * @param <T>         the type parameter
     * @param inputStream The stream to be deserialized
     * @return the object
     * @since 5.0.0
     */
    public static <T> T deserialize(final InputStream inputStream) {
        ObjectInputStream in = null;
        try {
            in = new ObjectInputStream(inputStream);
            final T obj = (T) in.readObject();
            return obj;
        } catch (final ClassNotFoundException | IOException e) {
            throw Throwables.propagate(e);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (final IOException e) {
                   LOGGER.error(e.getMessage(), e);
                }
            }
        }
    }

    /**
     * Serialize and encode object.
     *
     * @param cipher the cipher
     * @param object the object
     * @return the byte []
     * @since 4.2
     */
    public static byte[] serializeAndEncodeObject(final CipherExecutor cipher,
                                                  final Serializable object) {
        final byte[] outBytes = serialize(object);
        return (byte[]) cipher.encode(outBytes);
    }

    /**
     * Decode and serialize object.
     *
     * @param <T>    the type parameter
     * @param object the object
     * @param cipher the cipher
     * @param type   the type
     * @return the t
     * @since 4.2
     */
    public static <T> T decodeAndDeserializeObject(final byte[] object,
                                                   final CipherExecutor cipher,
                                                   final Class<? extends Serializable> type) {
        try {
            final byte[] decoded = (byte[]) cipher.decode(object);
            return deserializeAndCheckObject(decoded, type);
        } catch (final Exception e) {
            throw Throwables.propagate(e);
        }
    }

    /**
     * Decode and serialize object.
     *
     * @param <T>    the type parameter
     * @param object the object
     * @param type   the type
     * @return the t
     * @since 4.2
     */
    public static <T> T deserializeAndCheckObject(final byte[] object, final Class<? extends Serializable> type) {
        final Object result = deserialize(object);
        if (!type.isAssignableFrom(result.getClass())) {
            throw new ClassCastException("Decoded object is of type " + result.getClass()
                    + " when we were expecting " + type);
        }
        return (T) result;
    }
}
