/*
 * Copyright (C) 2004-2006, C. Ramakrishnan / Illposed Software.
 * All rights reserved.
 *
 * This code is licensed under the BSD 3-Clause license.
 * See file LICENSE (or LICENSE.html) for more information.
 */

package com.illposed.osc.utility;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import com.illposed.osc.OSCBundle;
import com.illposed.osc.OSCMessage;
import com.illposed.osc.OSCPacket;

/**
 * Utility class to convert a byte array,
 * conforming to the OSC byte stream format,
 * into Java objects.
 *
 * @author Chandrasekhar Ramakrishnan
 */
public class OSCByteArrayToJavaConverter {

	private byte[] bytes;
	private int bytesLength;
	private int streamPosition;

	/**
	 * Creates a helper object for converting from a byte array
	 * to an {@link OSCPacket} object.
	 */
	public OSCByteArrayToJavaConverter() {
	}

	/**
	 * Converts a byte array into an {@link OSCPacket}
	 * (either an {@link OSCMessage} or {@link OSCBundle}).
	 */
	public OSCPacket convert(byte[] byteArray, int bytesLength) {
		this.bytes = byteArray;
		this.bytesLength = bytesLength;
		this.streamPosition = 0;
		if (isBundle()) {
			return convertBundle();
		} else {
			return convertMessage();
		}
	}

	/**
	 * Is my byte array a bundle?
	 * @return true if it the byte array is a bundle, false o.w.
	 */
	private boolean isBundle() {
		// only need the first 7 to check if it is a bundle
		String bytesAsString = new String(bytes, 0, 7);
		return bytesAsString.startsWith("#bundle");
	}

	/**
	 * Converts the byte array to a bundle.
	 * Assumes that the byte array is a bundle.
	 * @return a bundle containing the data specified in the byte stream
	 */
	private OSCBundle convertBundle() {
		// skip the "#bundle " stuff
		streamPosition = 8;
		Date timestamp = readTimeTag();
		OSCBundle bundle = new OSCBundle(timestamp);
		OSCByteArrayToJavaConverter myConverter
				= new OSCByteArrayToJavaConverter();
		while (streamPosition < bytesLength) {
			// recursively read through the stream and convert packets you find
			int packetLength = ((Integer) readInteger()).intValue();
			byte[] packetBytes = new byte[packetLength];
			for (int i = 0; i < packetLength; i++) {
				packetBytes[i] = bytes[streamPosition++];
			}
			OSCPacket packet = myConverter.convert(packetBytes, packetLength);
			bundle.addPacket(packet);
		}
		return bundle;
	}

	/**
	 * Converts the byte array to a simple message.
	 * Assumes that the byte array is a message.
	 * @return a message containing the data specified in the byte stream
	 */
	private OSCMessage convertMessage() {
		OSCMessage message = new OSCMessage();
		message.setAddress(readString());
		List<Character> types = readTypes();
		if (null == types) {
			// we are done
			return message;
		}
		moveToFourByteBoundry();
		for (int i = 0; i < types.size(); ++i) {
			if ('[' == types.get(i).charValue()) {
				// we're looking at an array -- read it in
				message.addArgument(readArray(types, ++i).toArray());
				// then increment i to the end of the array
				while (types.get(i).charValue() != ']') {
					i++;
				}
			} else {
				message.addArgument(readArgument(types.get(i)));
			}
		}
		return message;
	}

	/**
	 * Reads a string from the byte stream.
	 * @return the next string in the byte stream
	 */
	private String readString() {
		int strLen = lengthOfCurrentString();
		char[] stringChars = new char[strLen];
		for (int i = 0; i < strLen; i++) {
			stringChars[i] = (char) bytes[streamPosition++];
		}
		moveToFourByteBoundry();
		return new String(stringChars);
	}

	/**
	 * Reads the types of the arguments from the byte stream.
	 * @return a char array with the types of the arguments
	 */
	private List<Character> readTypes() {
		// the next byte should be a ','
		if (bytes[streamPosition] != 0x2C) {
			return null;
		}
		streamPosition++;
		// find out how long the list of types is
		int typesLen = lengthOfCurrentString();
		if (0 == typesLen) {
			return null;
		}

		// read in the types
		List<Character> typesChars = new ArrayList<Character>(typesLen);
		for (int i = 0; i < typesLen; i++) {
			typesChars.add((char) bytes[streamPosition++]);
		}
		return typesChars;
	}

	/**
	 * Reads an object of the type specified by the type char.
	 * @param type type of the argument to read
	 * @return a Java representation of the argument
	 */
	private Object readArgument(char type) {
		switch (type) {
			case 'i' :
				return readInteger();
			case 'h' :
				return readBigInteger();
			case 'f' :
				return readFloat();
			case 'd' :
				return readDouble();
			case 's' :
				return readString();
			case 'c' :
				return readChar();
			case 'T' :
				return Boolean.TRUE;
			case 'F' :
				return Boolean.FALSE;
			case 't' :
				return readTimeTag();
			default:
				return null;
		}
	}

	/**
	 * Reads a char from the byte stream.
	 * @return a {@link Character}
	 */
	private Object readChar() {
		return new Character((char) bytes[streamPosition++]);
	}

	/**
	 * Reads a double from the byte stream.
	 * This just reads a float.
	 * @return a {@link Double}
	 */
	private Object readDouble() {
		return readFloat();
	}

	/**
	 * Reads a float from the byte stream.
	 * @return a {@link Float}
	 */
	private Object readFloat() {
		byte[] floatBytes = new byte[4];
		floatBytes[0] = bytes[streamPosition++];
		floatBytes[1] = bytes[streamPosition++];
		floatBytes[2] = bytes[streamPosition++];
		floatBytes[3] = bytes[streamPosition++];
//		int floatBits =
//			(floatBytes[0] << 24)
//				| (floatBytes[1] << 16)
//				| (floatBytes[2] << 8)
//				| (floatBytes[3]);
		BigInteger floatBits = new BigInteger(floatBytes);
		return new Float(Float.intBitsToFloat(floatBits.intValue()));
	}

	/**
	 * Reads a Big Integer (64 bit integer) from the byte stream.
	 * @return a {@link BigInteger}
	 */
	private Object readBigInteger() {
		byte[] longintBytes = new byte[8];
		longintBytes[0] = bytes[streamPosition++];
		longintBytes[1] = bytes[streamPosition++];
		longintBytes[2] = bytes[streamPosition++];
		longintBytes[3] = bytes[streamPosition++];
		longintBytes[4] = bytes[streamPosition++];
		longintBytes[5] = bytes[streamPosition++];
		longintBytes[6] = bytes[streamPosition++];
		longintBytes[7] = bytes[streamPosition++];
		return new BigInteger(longintBytes);
	}

	/**
	 * Reads an Integer (32 bit integer) from the byte stream.
	 * @return an {@link Integer}
	 */
	private Object readInteger() {
		byte[] intBytes = new byte[4];
		intBytes[0] = bytes[streamPosition++];
		intBytes[1] = bytes[streamPosition++];
		intBytes[2] = bytes[streamPosition++];
		intBytes[3] = bytes[streamPosition++];
		BigInteger intBits = new BigInteger(intBytes);
		return Integer.valueOf(intBits.intValue());
	}

	/**
	 * Reads the time tag and convert it to a Java Date object.
	 * A timestamp is a 64 bit number representing the time in NTP format.
	 * The first 32 bits are seconds since 1900, the second 32 bits are
	 * fractions of a second.
	 * @return a {@link Date}
	 */
	private Date readTimeTag() {
		byte[] secondBytes = new byte[8];
		byte[] fractionBytes = new byte[8];
		for (int i = 0; i < 4; i++) {
			// clear the higher order 4 bytes
			secondBytes[i] = 0; fractionBytes[i] = 0;
		}
			// while reading in the seconds & fraction, check if
			// this timetag has immediate semantics
		boolean isImmediate = true;
		for (int i = 4; i < 8; i++) {
			secondBytes[i] = bytes[streamPosition++];
			if (secondBytes[i] > 0) {
				isImmediate = false;
			}
		}
		for (int i = 4; i < 8; i++) {
			fractionBytes[i] = bytes[streamPosition++];
			if (i < 7) {
				if (fractionBytes[i] > 0) {
					isImmediate = false;
				}
			} else {
				if (fractionBytes[i] > 1) {
					isImmediate = false;
				}
			}
		}

		if (isImmediate) {
			return OSCBundle.TIMESTAMP_IMMEDIATE;
		}

		BigInteger secsSince1900 = new BigInteger(secondBytes);
		long secsSince1970 =  secsSince1900.longValue()
				- OSCBundle.SECONDS_FROM_1900_TO_1970.longValue();

		// no point maintaining times in the distant past
		if (secsSince1970 < 0) {
			secsSince1970 = 0;
		}
		long fraction = (new BigInteger(fractionBytes).longValue());

		// this line was cribbed from jakarta commons-net's NTP TimeStamp code
		fraction = (fraction * 1000) / 0x100000000L;

		// I do not know where, but I'm losing 1ms somewhere...
		fraction = (fraction > 0) ? fraction + 1 : 0;
		long millisecs = (secsSince1970 * 1000) + fraction;
		return new Date(millisecs);
	}

	/**
	 * Reads an array from the byte stream.
	 * @param types
	 * @param pos at which position to start reading
	 * @return the array that was read
	 */
	private List<Object> readArray(List<Character> types, int pos) {
		int arrayLen = 0;
		while (types.get(pos + arrayLen).charValue() != ']') {
			arrayLen++;
		}
		List<Object> array = new ArrayList<Object>(arrayLen);
		for (int j = 0; j < arrayLen; j++) {
			array.add(readArgument(types.get(pos + j)));
		}
		return array;
	}

	/**
	 * Get the length of the string currently in the byte stream.
	 */
	private int lengthOfCurrentString() {
		int i = 0;
		while (bytes[streamPosition + i] != 0) {
			i++;
		}
		return i;
	}

	/**
	 * Move to the next byte with an index in the byte array
	 * which is dividable by four.
	 */
	private void moveToFourByteBoundry() {
		// If i am already at a 4 byte boundry, I need to move to the next one
		int mod = streamPosition % 4;
		streamPosition += (4 - mod);
	}
}
