/**
 * ***************************************************************
 * JADE - Java Agent DEvelopment Framework is a framework to develop
 * multi-agent systems in compliance with the FIPA specifications.
 * Copyright (C) 2000 CSELT S.p.A.
 * 
 * GNU Lesser General Public License
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation,
 * version 2.1 of the License.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA  02111-1307, USA.
 * **************************************************************
 */
package jade.content.frame;

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

import jade.core.AID;
import jade.content.lang.sl.SL0Vocabulary;
import jade.util.leap.Iterator;

/**
   @author Giovanni Caire - TILAB
 */
public class LEAPFrameCodec implements jade.util.leap.Serializable {
	public static final String NAME = "LEAP";

	private transient ByteArrayOutputStream outBuffer = new ByteArrayOutputStream();
	private transient DataOutputStream      outStream = new DataOutputStream(outBuffer);
	private transient Vector             stringReferences = new Vector();
	//#MIDP_EXCLUDE_BEGIN
	private void readObject(java.io.ObjectInputStream oin) throws java.io.IOException, ClassNotFoundException {
		oin.defaultReadObject();
		outBuffer = new ByteArrayOutputStream();
		outStream = new DataOutputStream(outBuffer);
		stringReferences = new Vector();
	}
	//#MIDP_EXCLUDE_END

	// Primitive types
	private static final byte  STRING = 6;
	private static final byte  BOOLEAN = 7;
	private static final byte  INTEGER = 8;
	private static final byte  FLOAT = 9;
	private static final byte  DATE = 10;
	private static final byte  BYTE_SEQUENCE = 11;

	// Structured types
	private static final byte  AGGREGATE = 1;
	private static final byte  CONTENT_ELEMENT_LIST = 2;
	private static final byte  OBJECT = 3;

	// Markers for structured types
	private static final byte  ELEMENT = 4;
	private static final byte  END = 5;

	// Modifiers for string encoding
	private static final byte  MODIFIER = (byte) 0x10; // Only bit five set to 1
	private static final byte  UNMODIFIER = (byte) 0xEF; // Only bit five cleared to 1

	/**
     Transform a Frame into a sequence of bytes encoded according to the
     LEAP language
     @param content The Frame to be transformed
     @throws FrameException
	 */
	public synchronized byte[] encode(Frame content) throws FrameException {
		if (content == null) {
			return null;
		}
		try {
			write(outStream, content);
			return outBuffer.toByteArray();
		} 
		catch (FrameException fe) {
			throw fe;
		}
		catch (Throwable t) {
			throw new FrameException("Error encoding content", t);
		}
		finally {
			outBuffer.reset();
			stringReferences.removeAllElements();
		}
	} 

	/**
     Transform a sequence of bytes encoded according to the LEAP 
     language into a Frame 
     @param content The sequence of bytes to be transformed.
     @throws FrameException
	 */
	public synchronized Frame decode(byte[] content) throws FrameException {
		if (content == null || content.length == 0) {
			return null;
		}
		ByteArrayInputStream inpBuffer = null;
		DataInputStream      inpStream = null;
		try {
			inpBuffer = new ByteArrayInputStream(content);
			inpStream = new DataInputStream(inpBuffer);

			return (Frame) read(inpStream);
		} 
		catch (FrameException fe) {
			//fe.printStackTrace();
			throw fe;
		}
		catch (Throwable t) {
			//t.printStackTrace();
			throw new FrameException("Error decoding content", t);
		}
		finally {
			try {
				inpStream.close();
				inpBuffer.close();
			}
			catch (IOException ioe) {
				ioe.printStackTrace();
			}
			stringReferences.removeAllElements();
		}
	} 

	/**
	 */
	private void write(DataOutputStream stream, Object obj) throws Throwable {
		// PRIMITIVES
		if (obj instanceof String) {
			writeString(stream, STRING, (String) obj);
		}
		else if (obj instanceof Boolean) {
			stream.writeByte(BOOLEAN);
			stream.writeBoolean(((Boolean) obj).booleanValue());
		}
		else if (obj instanceof Long) {
			stream.writeByte(INTEGER);
			stream.writeLong(((Long) obj).longValue());
		}
		//#MIDP_EXCLUDE_BEGIN
		else if (obj instanceof Double) {
			stream.writeByte(FLOAT);
			stream.writeDouble(((Double) obj).doubleValue());
		} 
		//#MIDP_EXCLUDE_END
		else if (obj instanceof Date) {
			stream.writeByte(DATE);
			stream.writeLong(((Date) obj).getTime());
		}
		else if (obj instanceof byte[]) {
			stream.writeByte(BYTE_SEQUENCE);
			byte[] b = (byte[]) obj;
			stream.writeInt(b.length);
			stream.write(b, 0, b.length);
		}

		// ORDERED FRAME
		else if (obj instanceof OrderedFrame) {
			OrderedFrame f = (OrderedFrame) obj;
			int size = f.size();
			String typeName = f.getTypeName();
			if (typeName != null) {
				// AGGREGATE
				writeString(stream, AGGREGATE, typeName);
			}
			else {
				// CONTENT_ELEMENT_LIST
				stream.writeByte(CONTENT_ELEMENT_LIST);
			}
			for (int i = 0; i < size; i++) {
				stream.writeByte(ELEMENT);
				write(stream, f.elementAt(i));
			} 
			stream.writeByte(END);
		}

		// QUALIFIED_FRAME
		else if (obj instanceof QualifiedFrame) {
			QualifiedFrame f = (QualifiedFrame) obj;
			writeString(stream, OBJECT, f.getTypeName());

			Enumeration e = f.keys();
			while (e.hasMoreElements()) {
				String key = (String) e.nextElement();
				writeString(stream, ELEMENT, key);
				write(stream, f.get(key));
			} 
			stream.writeByte(END);
		}

		// AID
		else if (obj instanceof AID) {
			// Convert the AID into a qualified frame and call write() again
			write(stream, aidToFrame((AID) obj));
		}

		else {
			throw new FrameException("Object "+obj+" cannot be encoded");
		}
	} 

	/**
	 */
	private Object read(DataInputStream stream) throws Throwable {
		Object obj = null;  
		byte type = stream.readByte();

		// PRIMITIVES
		if ((type&UNMODIFIER) == STRING) {
			obj = readString(stream, type);
		}
		else if (type == BOOLEAN) {
			obj = new Boolean(stream.readBoolean());
		}
		else if (type == INTEGER) {
			obj = new Long(stream.readLong());
		}
		//#MIDP_EXCLUDE_BEGIN
		else if (type == FLOAT) {
			obj = new Double(stream.readDouble());
		} 
		//#MIDP_EXCLUDE_END
		else if (type == DATE) {
			obj = new Date(stream.readLong());
		}
		else if (type == BYTE_SEQUENCE) {
			int length = stream.readInt();
			obj = new byte[length];
			stream.read((byte[]) obj, 0, length);
		}

		// AGGREGATE
		else if ((type&UNMODIFIER) == AGGREGATE) {
			String typeName = readString(stream, type);
			OrderedFrame f = new OrderedFrame(typeName);
			fillOrderedFrame(stream, f);
			obj = f;
		} 

		// CONTENT_ELEMENT_LIST
		else if (type == CONTENT_ELEMENT_LIST) {
			OrderedFrame f = new OrderedFrame(null);
			fillOrderedFrame(stream, f);
			obj = f;
		}

		// QUALIFIED_FRAME
		else if ((type&UNMODIFIER) == OBJECT) {
			String typeName = readString(stream, type);
			QualifiedFrame f = new QualifiedFrame(typeName);

			byte marker = stream.readByte();
			do {
				if ((marker&UNMODIFIER) == ELEMENT) {
					String elementName = readString(stream, marker);
					Object elementVal = read(stream);
					f.put(elementName, elementVal);
					marker = stream.readByte();
				}
			} 
			while (marker != END);

			// If this QualifiedFrame represents an AID, convert it
			if (f.getTypeName().equals(SL0Vocabulary.AID)) {
				obj = frameToAid(f);
			}
			else {
				obj = f;
			}
		}
		else {
			throw new FrameException("Unexpected tag "+type);
		}

		return obj;
	} 

	private void fillOrderedFrame(DataInputStream stream, OrderedFrame f) throws Throwable {
		byte marker = stream.readByte();
		do {
			if (marker == ELEMENT) {
				Object elementVal = read(stream);
				f.addElement(elementVal);
				marker = stream.readByte();
			}
		} 
		while (marker != END);
	}

	private final void writeString(DataOutputStream stream, byte tag, String s) throws Throwable {
		int index = stringReferences.indexOf(s);
		if (index >= 0) {
			// Write the tag modified and just put the index
			stream.writeByte(tag|MODIFIER);
			stream.writeByte(index);

		}
		else {
			stream.writeByte(tag);
			stream.writeUTF(s);
			if ((s.length() > 1) && (stringReferences.size() < 256)) {
				stringReferences.addElement(s);
				//System.out.println("writeString: added:"+s);
			}
		}
	}

	private final String readString(DataInputStream stream, byte tag) throws Throwable {
		if ((tag&MODIFIER) != 0) {
			int index = stream.readUnsignedByte();
			String s = (String) stringReferences.elementAt(index);
			return s;
		}
		else {
			String s = stream.readUTF();
			if ((s.length() > 1) && (stringReferences.size() < 256)) {
				stringReferences.addElement(s);
			}
			return s;
		}
	}

	private final QualifiedFrame aidToFrame(AID id) {
		QualifiedFrame f = new QualifiedFrame(SL0Vocabulary.AID);
		// Name
		f.put(SL0Vocabulary.AID_NAME, id.getName());

		// Addresses
		Iterator i = id.getAllAddresses();
		if (i.hasNext()) {
			OrderedFrame addresses = new OrderedFrame(SL0Vocabulary.SEQUENCE);
			while (i.hasNext()) {
				addresses.addElement(i.next());
			}
			f.put(SL0Vocabulary.AID_ADDRESSES, addresses);
		}
		// Resolvers
		i = id.getAllResolvers();
		if (i.hasNext()) {
			OrderedFrame resolvers = new OrderedFrame(SL0Vocabulary.SEQUENCE);
			while (i.hasNext()) {
				AID res = (AID) i.next();
				resolvers.addElement(aidToFrame(res));
			}
			f.put(SL0Vocabulary.AID_RESOLVERS, resolvers);
		}
		return f;
	}

	private final AID frameToAid(QualifiedFrame f) {
		// Name
		AID id = new AID((String) f.get(SL0Vocabulary.AID_NAME), AID.ISGUID);

		// Addresses
		OrderedFrame addresses = (OrderedFrame) f.get(SL0Vocabulary.AID_ADDRESSES);
		if (addresses != null) {
			for (int i = 0; i < addresses.size(); ++i) {
				id.addAddresses((String) addresses.elementAt(i));
			}
		}
		// Resolvers
		OrderedFrame resolvers = (OrderedFrame) f.get(SL0Vocabulary.AID_RESOLVERS);
		if (resolvers != null) {
			for (int i = 0; i < resolvers.size(); ++i) {
				AID res = frameToAid((QualifiedFrame) resolvers.elementAt(i));
				id.addResolvers(res);
			}
		}
		return id;
	}
}

