package com.j256.ormlite.field.types;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.Arrays;

import com.j256.ormlite.field.FieldType;
import com.j256.ormlite.field.SqlType;
import com.j256.ormlite.misc.IOUtils;
import com.j256.ormlite.misc.SqlExceptionUtil;
import com.j256.ormlite.support.DatabaseResults;

/**
 * Persists an unknown Java Object that is {@link Serializable}.
 * 
 * @author graywatson
 */
public class SerializableType extends BaseDataType {

	private static final SerializableType singleTon = new SerializableType();

	public static SerializableType getSingleton() {
		return singleTon;
	}

	private SerializableType() {
		/*
		 * NOTE: Serializable class should _not_ be in the list because _everything_ is serializable and we want to
		 * force folks to use DataType.SERIALIZABLE -- especially for forwards compatibility.
		 */
		super(SqlType.SERIALIZABLE);
	}

	/**
	 * Here for others to subclass.
	 */
	protected SerializableType(SqlType sqlType, Class<?>[] classes) {
		super(sqlType, classes);
	}

	@Override
	public Object parseDefaultString(FieldType fieldType, String defaultStr) throws SQLException {
		throw new SQLException("Default values for serializable types are not supported");
	}

	@Override
	public Object resultToSqlArg(FieldType fieldType, DatabaseResults results, int columnPos) throws SQLException {
		return results.getBytes(columnPos);
	}

	@Override
	public Object sqlArgToJava(FieldType fieldType, Object sqlArg, int columnPos) throws SQLException {
		byte[] bytes = (byte[]) sqlArg;
		ObjectInputStream objInStream = null;
		try {
			objInStream = new ObjectInputStream(new ByteArrayInputStream(bytes));
			return objInStream.readObject();
		} catch (Exception e) {
			throw SqlExceptionUtil.create("Could not read serialized object from byte array: " + Arrays.toString(bytes)
					+ "(len " + bytes.length + ")", e);
		} finally {
			// we do this to give GC a hand with ObjectInputStream reference maps
			IOUtils.closeQuietly(objInStream);
		}
	}

	@Override
	public Object javaToSqlArg(FieldType fieldType, Object obj) throws SQLException {
		ObjectOutputStream objOutStream = null;
		try {
			ByteArrayOutputStream outStream = new ByteArrayOutputStream();
			objOutStream = new ObjectOutputStream(outStream);
			objOutStream.writeObject(obj);
			objOutStream.close();
			objOutStream = null;
			return outStream.toByteArray();
		} catch (Exception e) {
			throw SqlExceptionUtil.create("Could not write serialized object to byte array: " + obj, e);
		} finally {
			// we do this to give GC a hand with ObjectOutputStream reference maps
			IOUtils.closeQuietly(objOutStream);
		}
	}

	@Override
	public boolean isValidForField(Field field) {
		return Serializable.class.isAssignableFrom(field.getType());
	}

	@Override
	public boolean isStreamType() {
		// can't do a getObject call beforehand so we have to check for nulls
		return true;
	}

	@Override
	public boolean isComparable() {
		return false;
	}

	@Override
	public boolean isAppropriateId() {
		return false;
	}

	@Override
	public boolean isArgumentHolderRequired() {
		return true;
	}

	@Override
	public Object resultStringToJava(FieldType fieldType, String stringValue, int columnPos) throws SQLException {
		throw new SQLException("Serializable type cannot be converted from string to Java");
	}

	@Override
	public Class<?> getPrimaryClass() {
		return Serializable.class;
	}
}