package shared;

import java.util.Vector;
import java.io.*;

public class Serializer {
	
	// read characters off of the specified input stream until a newline or cr character is encountered
	public static String readLine(InputStream in) throws IOException {
		if(in == null) { return null; }
		
		char c = '\0';
		String s = new String("");
		
		// accumulate the string until a newline character is encountered
		while(true) {
			c = readSerializedCharacter(in);
			if(c == '\n' || c == '\r') { break; }
			
			s += c;
		}
		return s;
	}
	
	// read characters off of the specified input stream until a newline, cr, space or tab character is encountered
	public static String readToken(InputStream in) throws IOException {
		if(in == null) { return null; }
		
		char c = '\0';
		String s = new String("");
		
		// accumulate the string until a newline or spacing character is encountered
		while(true) {
			c = readSerializedCharacter(in);
			if(c == '\n' || c == '\r' || c == '\t' || c == ' ') { break; }
			
			s += c;
		}
		return s;
	}
	
	// write a string to the specified output stream
	public static boolean writeString(String s, OutputStream out) throws IOException {
		if(s == null || out == null) { return false; }
		
		// serialize and write each character in the string
		for(int i=0;i<s.length();i++) {
			if(!writeSerializedCharacter(s.charAt(i), out)) {
				return false;
			}
		}
		
		return true;
	}
	
	// serialize the specified string
	public static byte[] serializeString(String s) {
		if(s == null) { return null; }
		if(s.length() == 0) { return null; }
		
		byte[] data = new byte[s.length() * 2];
		
		// serialize and store the bytes for each character in the string
		int j = 0;
		for(int i=0;i<s.length();i++) {
			data[j++] = (byte) (s.charAt(i) >> 8);
			data[j++] = (byte) (s.charAt(i));
		}
		
		return data;
	}
	
	// de-serialize the specified string
	public static String deserializeString(byte[] data) {
		if(data == null) { return null; }
		if(data.length == 0 || data.length % 2 != 0) { return null; }
		
		String s = "";
		
		for(int i=0;i<data.length;i+=2) {
			s += (char) (data[i] << 8
					  | (data[i+1] & 0xff));
		}
		
		return s;
	}
	
	// serializes the specified array of objects
	public static byte[] serializeArray(Object[] objects) throws IOException {
		if(objects == null || objects.length == 0) { return null; }
		
		Vector<byte[]> serializedObjects = new Vector<byte[]>(objects.length);
		
		int length = 4;
		
		// serialize and store the bytes for each object
		byte[] serializedObject = null;
		for(int i=0;i<objects.length;i++) {
			serializedObject = serializeObject(objects[i]);
			if(serializedObject == null) {
				throw new IOException("error serializing object");
			}
			length += 4 + serializedObject.length;
			serializedObjects.add(serializedObject);
		}
		
		// initialize the byte stream
		ByteArrayOutputStream byteStream = new ByteArrayOutputStream(length);
		
		// write the serialized bytes for the number of objects
		byteStream.write(serializeInteger(serializedObjects.size()));
		
		// write the serialized bytes for the size of the serialized object, followed by the serialized bytes of each object
		for(int i=0;i<serializedObjects.size();i++) {
			byteStream.write(serializeInteger(serializedObjects.elementAt(i).length));
			byteStream.write(serializedObjects.elementAt(i));
		}
		
		return byteStream.toByteArray();
	}
	
	// de-serializes the specified array of objects
	public static Object[] deserializeArray(byte[] data) throws IOException {
		if(data == null || data.length == 0) { return null; }
		
		int length, numberOfObjects = -1;
		byte[] temp;
		Object[] objects;
		ByteArrayInputStream byteStream = new ByteArrayInputStream(data);
		
		// read and de-serialize the bytes for the number of objects
		temp = new byte[4];
		byteStream.read(temp);
		numberOfObjects = deserializeInteger(temp);
		if(numberOfObjects < 1) { return null; }
		objects = new Object[numberOfObjects];
		
		// read all of the objects
		for(int i=0;i<numberOfObjects;i++) {
			// read and de-serialize the bytes for the size of the object
			temp = new byte[4];
			byteStream.read(temp);
			length = deserializeInteger(temp);
			if(length < 1) { return null; }
			
			// read and de-serialize the bytes for each object
			temp = new byte[length];
			byteStream.read(temp);
			objects[i] = deserializeObject(temp);
			if(objects[i] == null) { return null; }
		}
		
		return objects;
	}
	
	// return a byte array representing the serialized version of an object
	public static byte[] serializeObject(Object o) throws IOException {
		if(o == null) { return null; }
		ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
		ObjectOutputStream objectStream = null;
		
	    objectStream = new ObjectOutputStream(byteStream);
	    objectStream.writeObject(o);
	    objectStream.close();
		
	    return byteStream.toByteArray();
	}
	
	// return an object which has been deserialized from a byte array
	public static Object deserializeObject(byte[] data) throws IOException {
		if(data == null || data.length < 1) { return null; }
		
		Object object;
		ObjectInputStream objectStream = null;
		try {
			objectStream = new ObjectInputStream(new ByteArrayInputStream(data));
			object = objectStream.readObject();
			objectStream.close();
		}
		catch(ClassNotFoundException e) {
			e.printStackTrace();
			return null;
		}
		
		return object;
	}
	
	// read a serialized object from a specified input stream
	public static Object readSerializedObject(InputStream in) throws IOException {
		if(in == null) { return null; }
		
		int length = readSerializedInteger(in);
		
		if(length < 1) { return null; }
		
		byte[] data = new byte[length];
		
		in.read(data);
		
		return deserializeObject(data);
	}
	
	// serialize and write an object to a specified output stream 
	public static boolean writeSerializedObject(Object o, OutputStream out) throws IOException {
		if(o == null || out == null) { return false; }
		
		byte[] data = serializeObject(o);
		if(data == null) { return false; }
		
		writeSerializedInteger(data.length, out);
		out.write(data, 0, data.length);
		
		return true;
	}
	
	// read a serialized character off of a specified input stream
	public static char readSerializedCharacter(InputStream in) throws IOException {
		if(in == null) { return '\0'; }
		
		byte[] data = new byte[2];
		
		in.read(data);
		
		return deserializeCharacter(data);
	}
	
	// serialize and write a character to a specified output stream
	public static boolean writeSerializedCharacter(char c, OutputStream out) throws IOException {
		if(out == null) { return false; }
		
		byte[] data = serializeCharacter(c);
		
		out.write(data, 0, data.length);
		
		return true;
	}
	
	// read a serialized integer off of a specified input stream
	public static int readSerializedInteger(InputStream in) throws IOException {
		if(in == null) { return -1; }
		
		byte[] data = new byte[4];
		
		in.read(data);
		
		return deserializeInteger(data);
	}
	
	// serialize and write an integer to a specified output stream
	public static boolean writeSerializedInteger(int i, OutputStream out) throws IOException {
		if(out == null) { return false; }
		
		byte[] data = serializeInteger(i);
		
		out.write(data, 0, data.length);
		
		return true;
	}
	
	// serialize a character to a byte array
	public static byte[] serializeCharacter(char c) {
		byte[] data = new byte[2];
		data[0] = (byte) (c >> 8);
		data[1] = (byte) (c);
		return data;
	}
	
	// deserialize a byte array back into a character
	public static char deserializeCharacter(byte[] data) {
		if(data == null || data.length != 2) { return '\0'; }
		return (char) (data[0] << 8
					| (data[1] & 0xff));
	}
	
	// serialize an integer to a byte array
	public static byte[] serializeInteger(int i) {
		byte[] data = new byte[4];
		data[0] = (byte) (i >> 24);
		data[1] = (byte) (i >> 16);
		data[2] = (byte) (i >> 8);
		data[3] = (byte) (i);
		return data;
	}
	
	// deserialize a byte array back into an integer
	public static int deserializeInteger(byte[] data) {
		if(data == null || data.length != 4) { return -1; }
		return (int) (data[0] << 24
				   | (data[1] & 0xff) << 16
				   | (data[2] & 0xff) << 8
				   | (data[3] & 0xff));
	}
	
}
