/**
 * ***************************************************************
 * 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.lang.sl;

import jade.content.onto.Ontology;
import jade.content.onto.OntologyException;
import jade.content.abs.*;
import jade.content.schema.ObjectSchema;
import jade.content.lang.StringCodec;
//#MIDP_EXCLUDE_BEGIN
import jade.lang.acl.ISO8601;
import jade.util.leap.Iterator;
import jade.domain.FIPANames;
import jade.core.CaseInsensitiveString;

import java.util.Date;
import java.io.StringReader;

import java.io.BufferedReader; // only for debugging purposes in the main
import java.io.InputStreamReader; // only for debugging purposes in the main
import java.io.UnsupportedEncodingException;

import org.apache.commons.codec.binary.Base64;
//#MIDP_EXCLUDE_END


/**  
 * The codec class for the <b><i>FIPA-SL</i>n</b> languages. This class
 * implements the <code>Codec</code> interface and allows converting
 * back and forth between strings and frames, according to the SL
 * grammar.
 * By default the class implements full SL grammar, otherwise the proper
 * value must be used in the constructor.
 * @author Fabio Bellifemine - TILAB 
 * @author Nicolas Lhuillier - Motorola (added support for byte[] primitive)
 * @version $Date: 2010-07-05 17:38:19 +0200(lun, 05 lug 2010) $ $Revision: 6354 $
 */
/*#MIDP_INCLUDE_BEGIN
public class SLCodec extends SimpleSLCodec {
#MIDP_INCLUDE_END*/
//#MIDP_EXCLUDE_BEGIN
public class SLCodec extends StringCodec {

	public static final String PRESERVE_JAVA_TYPES = "SL-preserve-java-types";
	
	private transient SLParser parser;
	private transient ExtendedSLParser extendedParser;
	private SL0Ontology slOnto; // ontology of the content language
	private Ontology domainOnto = null; // application ontology
	/** This is the StringBuffer used by the encode method **/
	private transient StringBuffer buffer = null; 
	/** This variable is true, when meta symbols are allowed (metas are a semantics-specific extension to the SL Grammar) **/
	private boolean metaAllowed = true; //FIXME set/unset this variable to do
	
	private boolean preserveJavaTypes = false;

	/**
	 * Construct a Codec object for the full SL-language (FIPA-SL).
	 */
	public SLCodec() {
		this(3, readPreserveJavaTypesProperty());
	}

	/**
	 * Create an SLCodec for the full SL-language (FIPA-SL) specifying whether or not java primitive types 
	 * (long, int, float, double) must be preserved.
	 * This is achieved by encoding long values as <numeric-value>L and float values as <numeric-valueF.
	 * It should be noticed that this encoding is NOT FIPA SL standard
	 * @param preserveJavaTypes Indicates whether or not java primitive types must be preserved
	 */
	public SLCodec(boolean preserveJavaTypes) {
		this(3, preserveJavaTypes);
	}

	/**
	 * Construct a Codec object for the given profile of SL-language.
	 * @param slType specify 0 for FIPA-SL0, 1 for FIPA-SL1, 2 for FIPA-SL2, any other value can be used for full FIPA-SL
	 */
	public SLCodec(int slType) {
		this(slType, readPreserveJavaTypesProperty());
	}
	
	/**
	 * Create an SLCodec for the given profile of SL-language specifying whether or not java primitive types 
	 * (long, int, float, double) must be preserved.
	 * @param slType specify 0 for FIPA-SL0, 1 for FIPA-SL1, 2 for FIPA-SL2, any other value can be used for full FIPA-SL
	 * @param preserveJavaTypes Indicates whether or not java primitive types must be preserved
	 */
	public SLCodec(int slType, boolean preserveJavaTypes) {
		super((slType==0 ? FIPANames.ContentLanguage.FIPA_SL0 :
			(slType==1 ? FIPANames.ContentLanguage.FIPA_SL1 :
				(slType==2 ? FIPANames.ContentLanguage.FIPA_SL2 :
					FIPANames.ContentLanguage.FIPA_SL ))));
		if ((slType < 0) || (slType > 2)) // if outside range, set to full SL
			slType = 3;
		slOnto = (SL0Ontology) (slType == 0 ? SL0Ontology.getInstance() : 
			(slType == 1 ? SL1Ontology.getInstance() :
				(slType == 2 ? SL2Ontology.getInstance() : SLOntology.getInstance())));
		this.preserveJavaTypes = preserveJavaTypes;
		initParser();
	}

	private static boolean readPreserveJavaTypesProperty() {
		String strPreserveJavaTypes = System.getProperty(PRESERVE_JAVA_TYPES);
		return "true".equals(strPreserveJavaTypes);
	}
	
	private void initParser() {
		int	slType	= jade.domain.FIPANames.ContentLanguage.FIPA_SL0.equals(getName()) ? 0
					: jade.domain.FIPANames.ContentLanguage.FIPA_SL2.equals(getName()) ? 1 
					: jade.domain.FIPANames.ContentLanguage.FIPA_SL2.equals(getName()) ? 2 : 3;
		
		if (preserveJavaTypes) {
			extendedParser = new ExtendedSLParser(new StringReader(""));
			extendedParser.setSLType(slType);
		}
		else {
			parser = new SLParser(new StringReader(""));
			parser.setSLType(slType); 
		}
	}
	
	public boolean getPreserveJavaTypes() {
		return preserveJavaTypes;
	}
	
	/**
	 * Encodes a content into a String.
	 * @param content the content as an abstract descriptor.
	 * @return the content as a String.
	 * @throws CodecException
	 */
	public String encode(AbsContentElement content) throws CodecException {
		return encode(null, content);
	}

	/**
	 * Encodes a content into a String.
	 * @param ontology the ontology 
	 * @param content the content as an abstract descriptor.
	 * @return the content as a String.
	 * @throws CodecException
	 */
	public synchronized String encode(Ontology ontology, AbsContentElement content) throws CodecException {
		try {
			domainOnto = ontology;
			buffer = new StringBuffer("(");
			if (content instanceof AbsContentElementList) {
				for (Iterator i=((AbsContentElementList)content).iterator(); i.hasNext(); ) {
					AbsObject o = (AbsObject)i.next();
					encodeAndAppend(o);
					buffer.append(' ');
				}
			} else encodeAndAppend(content);
			buffer.append(')');
			return buffer.toString();
		} finally {
			buffer = null; //frees the memory
		}
	}



	/**
	 * Encode a string, taking care of quoting separated words and
	 * escaping strings, if necessary.
	 * And append it to the buffer.
	 **/
	private void encodeAndAppend(String val) {
		// if the slotName is a String of words then quote it. If it is a meta (i.e. startsWith "??") do not quote it.
		String out = ( (SimpleSLTokenizer.isAWord(val) || (metaAllowed && val.startsWith("??")) ) ? val : SimpleSLTokenizer.quoteString(val));
		buffer.append(out);
	}


	/** Encode the passed Abstract Predicate and append its encoding to buffer **/
	private void encodeAndAppend(AbsPredicate val) throws CodecException {
		String propositionSymbol = val.getTypeName();
		if (val.getCount() > 0) { // predicate with arguments
			String[] slotNames = getSlotNames(val);
			buffer.append('(');
			if (slOnto.isUnaryLogicalOp(propositionSymbol)) {
				// Unary logical operator of the SL language (NOT)
				buffer.append(propositionSymbol);
				buffer.append(' ');
				try {
					encodeAndAppend((AbsPredicate)val.getAbsObject(slotNames[0]));
				} catch (RuntimeException e) {
					throw new CodecException("A UnaryLogicalOp requires a formula argument",e);
				}
			} else if (slOnto.isBinaryLogicalOp(propositionSymbol)) {
				// Bynary logical operator of the SL language (AND, OR)
				buffer.append(propositionSymbol);
				buffer.append(' ');
				try {
					encodeAndAppend((AbsPredicate)val.getAbsObject(slotNames[0]));
					buffer.append(' ');
					encodeAndAppend((AbsPredicate)val.getAbsObject(slotNames[1]));
				} catch (RuntimeException e) {
					throw new CodecException("A BinaryLogicalOp requires 2 formula arguments",e);
				}
			} else if (slOnto.isQuantifier(propositionSymbol)) {
				// Quantifier operator of the SL language (EXISTS, FORALL)
				buffer.append(propositionSymbol);
				buffer.append(' ');
				try {
					encodeAndAppend((AbsVariable)val.getAbsObject(slotNames[0])); //FIXME. The hypothesis is that the first slot is the variable
					buffer.append(' ');
					encodeAndAppend((AbsPredicate)val.getAbsObject(slotNames[1]));
				} catch (RuntimeException e) {
					throw new CodecException("A Quantifier requires a variable and a formula arguments",e);
				}
			} else if (slOnto.isModalOp(propositionSymbol)) {
				// Modal operator of the SL language (B, I, U, PG)
				buffer.append(propositionSymbol);
				buffer.append(' ');
				try {
					encodeAndAppend((AbsTerm)val.getAbsObject(slotNames[0]));
					buffer.append(' ');
					encodeAndAppend((AbsPredicate)val.getAbsObject(slotNames[1]));
				} catch (RuntimeException e) {
					throw new CodecException("A ModalOp requires a term and a formula arguments",e);
				}
			} else if (slOnto.isActionOp(propositionSymbol)) {
				// Action operator of the SL language (DONE, FEASIBLE)
				buffer.append(propositionSymbol);
				buffer.append(' ');
				try {
					encodeAndAppend((AbsTerm)val.getAbsObject(slotNames[0])); //FIXME check it is an action expression
					AbsPredicate ap = (AbsPredicate)val.getAbsObject(slotNames[1]);
					if (ap != null) { // Second argument is optional
						buffer.append(' ');
						encodeAndAppend(ap);
					}
				} catch (RuntimeException e) {
					throw new CodecException("An ActionOp requires an actionexpression and (optionally) a formula arguments",e);
				}
			} else if (slOnto.isBinaryTermOp(propositionSymbol)) {
				// Binary term operator of the SL language (RESULT, =)
				buffer.append(propositionSymbol);
				buffer.append(' ');
				try {
					encodeAndAppend((AbsTerm)val.getAbsObject(slotNames[0]));
					buffer.append(' ');
					encodeAndAppend((AbsTerm)val.getAbsObject(slotNames[1]));
				} catch (RuntimeException e) {
					throw new CodecException("A BinaryTermOp requires 2 term arguments",e);
				}
			} else {
				encodeAndAppend(propositionSymbol);
				// Predicate in the ontology
				try {
					encodeSlotsByOrder(val, slotNames);
				} catch (RuntimeException e) {
					throw new CodecException("SL allows predicates with term arguments only",e);
				}
			}
			buffer.append(')');
		} else
			// Proposition
			encodeAndAppend(propositionSymbol);  
	}

	private void encodeAndAppend(AbsIRE val) throws CodecException {
		buffer.append('(');
		encodeAndAppend(val.getTypeName());
		buffer.append(' ');
		encodeAndAppend(val.getTerm());
		buffer.append(' ');
		encodeAndAppend(val.getProposition());
		buffer.append(')'); 
	}

	private void encodeAndAppend(AbsVariable val) throws CodecException {
		String var = val.getName();
		if (var.charAt(0) == '?') {
			encodeAndAppend(var);
		} else {
			buffer.append('?');
			encodeAndAppend(var);
		}
	}

	private void encodeAndAppend(AbsConcept val) throws CodecException {
		String functionSymbol = val.getTypeName();
		buffer.append('(');
		String[] slotNames = getSlotNames(val);
		if (slOnto.isSLFunctionWithoutSlotNames(functionSymbol)) { 
			// A Functional operator of the SL language (ACTION, + ...)
			// The form is: functionSymbol Term*
			buffer.append(functionSymbol);
			try {
				encodeSlotsByOrder(val, slotNames);
			} catch (RuntimeException e) {
				throw new CodecException("A FunctionalOperator requires 1 or 2 Term arguments",e);
			}
		} else { 
			// A generic term in the ontology. The form can be both 
			// functionSymbol Parameter* or functionSymbol Term*. Get the 
			// preferred way from the ontology.
			encodeAndAppend(functionSymbol);
			try {
				// FIXME: To improve performances the two operations that imply
				// retrieving a schema from the ontology (getting slot names and
				// getting the preferred encoding type) should be carried out at 
				// the same time.
				if (getEncodingByOrder(val)) {
					encodeSlotsByOrder(val, slotNames);
				}
				else {
					encodeSlotsByName(val, slotNames);
				}
			} catch (RuntimeException e) {
				throw new CodecException("A FunctionalTerm requires Terms arguments",e);
			}
		}

		buffer.append(')');
	}


	private void encodeAndAppend(AbsAggregate val) throws CodecException {
		buffer.append('(');
		encodeAndAppend(val.getTypeName());
		for (Iterator i=val.iterator(); i.hasNext(); ) {
			buffer.append(' ');
			encodeAndAppend((AbsObject)i.next());
		}
		buffer.append(')');
	}


	private void encodeAndAppend(AbsPrimitive val) throws CodecException {
		Object v = val.getObject();
		if (v instanceof Date)
			buffer.append(ISO8601.toString((Date)v));
		else if (v instanceof Number) {
			buffer.append(v.toString());
			if (preserveJavaTypes) {
				if (v instanceof Long) {
					buffer.append('L');
				}
				else if (v instanceof Float) {
					buffer.append('F');
				}
			}
		}
		else if (v instanceof byte[]) {
			// Note: Use US-ASCII charSet and Base64 encoding 
			byte[] b = (byte[]) v;
			b = Base64.encodeBase64(b);
			
			buffer.append('#');
			buffer.append(b.length);
			buffer.append('"');
			try {
				buffer.append(new String(b, "US-ASCII"));
			} catch (UnsupportedEncodingException uee) {
				throw new CodecException("Error encoding byte-array to Base64 US-ASCII", uee);
			}
		}
		else if (v instanceof Boolean) 
			buffer.append(v.toString());
		else {
			String vs = v.toString();
			if ( (CaseInsensitiveString.equalsIgnoreCase("true",vs)) ||
					(CaseInsensitiveString.equalsIgnoreCase("false",vs)) ) {
				// quote true and false to avoid confusion with booleans
				buffer.append('"');
				buffer.append(vs);
				buffer.append('"');
			} else
				encodeAndAppend(vs);
		}
	}

	private void encodeAndAppend(AbsObject val) throws CodecException { 
		if (val instanceof AbsPrimitive)      encodeAndAppend( (AbsPrimitive)val);
		else if (val instanceof AbsPredicate) encodeAndAppend( (AbsPredicate)val);
		else if (val instanceof AbsIRE)       encodeAndAppend( (AbsIRE)val);
		else if (val instanceof AbsVariable)  encodeAndAppend( (AbsVariable)val);
		//	if (val instanceof AbsAgentAction) return toString( (AbsAgentAction)val);
		else if (val instanceof AbsAggregate) encodeAndAppend( (AbsAggregate)val);
		else if (val instanceof AbsConcept)   encodeAndAppend( (AbsConcept)val);
		else throw new CodecException("SLCodec cannot encode this object "+val);
	}


	/**
	 * Decodes the content to an abstract description.
	 * @param content the content as a String.
	 * @return the content as an abstract description.
	 * @throws CodecException
	 */
	public AbsContentElement decode(String content) throws CodecException {
		return decode(null, content); 
	}

	/**
	 * Decodes the content to an abstract description.
	 * @param ontology the ontology.
	 * @param content the content as a String.
	 * @return the content as an abstract description.
	 * @throws CodecException
	 */
	public synchronized AbsContentElement decode(Ontology ontology, String content) throws CodecException {
		try {
			AbsContentElementList tuple = null;
			if (preserveJavaTypes) {
				extendedParser.reinit(ontology, content);
				tuple = extendedParser.Content();
			}
			else {
				parser.reinit(ontology, content);
				tuple = parser.Content();
			}
			if (tuple.size() > 1)
				return tuple;
			else  // if there is a single ContentExpression than return just it, not the tuple
				return tuple.get(0);
		}  catch(Throwable e) { // both ParseException and TokenMgrError
			throw new CodecException("Parse exception", e);
		}
	}



	/**
	 * Decodes the content to an abstract description, where the content is known to be a Term.
	 * @param ontology the ontology.
	 * @param cterm the term as a String.
	 * @return the content as an abstract description.
	 * @throws CodecException
	 * @since JADE 3.4
	 */
	public synchronized AbsTerm decodeTerm(Ontology ontology, String term) throws CodecException {
		try {
			if (preserveJavaTypes) {
				extendedParser.reinit(ontology, term);
				return extendedParser.Term();
			}
			else {
				parser.reinit(ontology, term);
				return parser.Term();
			}
		}  catch(Throwable e) { // both ParseException and TokenMgrError
			throw new CodecException("Parse exception", e);
		}
	}


	/**
	 * Encodes the content into a String, where the content is known to be a Term.
	 * @param ontology the ontology.
	 * @param term the termt as an abstract descriptor
	 * @return the content as a String 
	 * @throws CodecException
	 * @since JADE 3.4
	 */
	public synchronized String encodeTerm(Ontology ontology, AbsTerm term) throws CodecException {
		try {
			domainOnto = ontology;
			buffer = new StringBuffer();
			encodeAndAppend(term);
			return buffer.toString();
		} finally {
			buffer = null; //frees the memory
		}
	}


	/**
	 * Decodes the content to an abstract description, where the content is known to be a Well-formed Formula
	 * @param ontology the ontology.
	 * @param formula the content as a String.
	 * @return the content as an abstract description.
	 * @throws CodecException
	 * @since JADE 3.4
	 */
	public synchronized AbsPredicate decodeFormula(Ontology ontology, String formula) throws CodecException {
		try {
			if (preserveJavaTypes) {
				extendedParser.reinit(ontology, formula);
				return extendedParser.Wff();
			}
			else {
				parser.reinit(ontology, formula);
				return parser.Wff();
			}
		}  catch(Throwable e) { // both ParseException and TokenMgrError
			throw new CodecException("Parse exception", e);
		}
	}


	/**
	 * Encodes the content into a String, where the content is known to be a Well-formed Formula
	 * @param ontology the ontology.
	 * @param formula the formula as an abstract descriptor
	 * @return the content as a String 
	 * @throws CodecException
	 * @since JADE 3.4
	 */
	public synchronized String encodeFormula(Ontology ontology, AbsPredicate formula) throws CodecException {
		try {
			domainOnto = ontology;
			buffer = new StringBuffer();
			encodeAndAppend(formula);
			return buffer.toString();
		} finally {
			buffer = null; //frees the memory
		}
	}


	public static void main(String[] args) {
		SLCodec codec = null;
		char contentType = 'C';
		try {
			codec = new SLCodec(Integer.parseInt(args[0]));
			contentType = (args.length > 1 ? args[1].charAt(0) : 'C');
		} catch (Exception e) {
			System.out.println("usage: SLCodec SLLevel [ContentType]\n where SLLevel can be 0 for SL0, 1 for SL1, 2 for SL2, 3 or more for full SL \n and where ContentType is a char representing the type of content to be parsed: C for a contentexpression (default), T for a term, F for a formula");
			System.exit(0);
		}

		while (true) {
			try {
				System.out.println("insert an SL " + (contentType == 'F' ? "Well-Formed Formula" : (contentType == 'T' ? "Term" : "Content Expression")) + " to parse (all the expression on a single line!): ");
				BufferedReader buff = new BufferedReader(new InputStreamReader(System.in));
				String str = buff.readLine();
				System.out.println("\n\n");
				if (contentType == 'F') {
					AbsPredicate result = codec.decodeFormula(null, str);
					System.out.println("DUMP OF THE DECODE OUTPUT (just for debugging):");
					System.out.println(result);
					System.out.println("\n\n");
					System.out.println("AFTER ENCODE:");
					System.out.println(codec.encodeFormula(null, result));
					System.out.println("\n\n");
				} else if (contentType == 'T') {
					AbsTerm result = codec.decodeTerm(null, str);
					System.out.println("DUMP OF THE DECODE OUTPUT (just for debugging):");
					System.out.println(result);
					System.out.println("\n\n");
					System.out.println("AFTER ENCODE:");
					System.out.println(codec.encodeTerm(null, result));
					System.out.println("\n\n");
				} else {
					AbsContentElement result = codec.decode(str);
					System.out.println("DUMP OF THE DECODE OUTPUT (just for debugging):");
					System.out.println(result);
					System.out.println("\n\n");
					System.out.println("AFTER ENCODE:");
					System.out.println(codec.encode(result));
					System.out.println("\n\n");
				}
			} catch(Exception pe) {
				pe.printStackTrace();
				//System.exit(0);
			}
		}
	}

	/**
	 * @return the ontology containing the schemas of the operator
	 * defined in this language
	 */
	public Ontology getInnerOntology() {
		return slOnto;
	}

	private String[] getSlotNames(AbsObject abs) throws CodecException {
		String[] slotNames = null;
		String type = abs.getTypeName();
		if (domainOnto != null) {
			// If an ontology is specified, get the slot names from it 
			// (and not directly from the abstract descriptor val) to preserve 
			// the order
			try {
				ObjectSchema s = domainOnto.getSchema(type);
				if (s == null) {
					throw new CodecException("No schema found for symbol "+type);
				}
				slotNames = s.getNames();
			}
			catch (OntologyException oe) {
				throw new CodecException("Error getting schema for symbol "+type, oe);
			}
		}
		else {
			slotNames = abs.getNames();
		}
		return slotNames;
	}

	private boolean getEncodingByOrder(AbsObject abs) throws CodecException {
		if (domainOnto != null) {
			String type = abs.getTypeName();
			try {
				ObjectSchema s = domainOnto.getSchema(type);
				return s.getEncodingByOrder();
			}
			catch (Exception e) {
				// Just ignore it
			}
		}
		return false;
	}

	/**
	 * Encode the slots of an abstract descriptor by order, i.e. 
	 * without writing the slot names. Also take into account that, in
	 * order to ensure a correct parsing, empty slots can only occur at 
	 * the end.
	 * Append this encoded string to buffer.
	 */
	private void encodeSlotsByOrder(AbsObject val, String[] slotNames) throws CodecException {
		boolean lastSlotEmpty = false;
		for (int i=0; i<slotNames.length; i++) {
			AbsTerm t = (AbsTerm)val.getAbsObject(slotNames[i]);
			if (t != null) {
				if (lastSlotEmpty) {
					throw new CodecException("Non-empty slot "+slotNames[i]+" follows empty slot "+slotNames[i-1]);
				}
				buffer.append(' ');
				encodeAndAppend(t);
			}
			else {
				lastSlotEmpty = true;
			}
		}
	}

	/**
	 * Encode the slots of an abstract descriptor by name, i.e. 
	 * writing for each non-empty slot the slot name followed by the
	 * slot value.
	 * Append this encoded string to buffer.
	 */
	private void encodeSlotsByName(AbsObject val, String[] slotNames) throws CodecException {
		for (int i=0; i<slotNames.length; i++) {
			AbsTerm t = (AbsTerm)val.getAbsObject(slotNames[i]);
			if (t != null) {
				// if this isn't un unnamed slot, then encode it otherwise just encode its value
				if (!slotNames[i].startsWith(this.UNNAMEDPREFIX)) {
					buffer.append(" :");
					encodeAndAppend(slotNames[i]);
				}
				buffer.append(' ');
				encodeAndAppend(t);
			}
		}
	}

	/**
	 * Restore parser after deserialization. <br>
	 * The readResolve method is called when ObjectInputStream has read an object from the stream 
	 * and is preparing to return it to the caller. <br>
	 * The readResolve method is not invoked on the object until the object is fully constructed. 
	 */
	protected Object readResolve() throws java.io.ObjectStreamException {
		initParser();
		return this; 
	}

//#MIDP_EXCLUDE_END
}

