/*
 * LpEncoder.java
 *
 * Copyright (C) 2006 - 2007 Martin Slota
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program 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 General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 51
 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

/*
 * History:
 * v1.0.0 (2007-05-04):
 * - no version information kept before :P
 *
 * PENDING create a junit test
 */

package lp.struct.util;

import java.io.StringWriter;
import java.io.Writer;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import lp.struct.LpAtom;
import lp.struct.LpCompoundTerm;
import lp.struct.LpConstant;
import lp.struct.LpFunction;
import lp.struct.LpLiteral;
import lp.struct.LpPredicate;
import lp.struct.LpRule;
import lp.struct.LpTerm;
import lp.struct.LpVariable;

/**
 * Transforms a logic program (or its components} represented by objects
 * implementing the {@link lp.struct.LpStructureUnit} interface into an encoded
 * textual form that only contains the characters [a-zA-Z_] and can be decoded
 * back to the original components.
 *
 * This is achieved by encoding all parenthesis, commas, dots, arrows separating
 * heads of rules from their bodies and the "not" representing default negation
 * in a special way. In particular, a left parenthesis is written as 'L', a
 * right parenthesis as 'R', a comma as 'C', a dot as 'D' and the arrow as 'A'.
 * Default negation is represented by the prefix "_N" instead of "not ". Letters
 * '_', 'L', 'R', 'A', 'C' and 'D' are escaped with an extra '_' and made
 * lowercase (just because I thought it looks better that way :o).
 *
 * The decoder associated to this encoder should replace each occurence of
 * <ul>
 * <li>'L' with '('</li>
 * <li>'R' with ')'</li>
 * <li>'C' with ','</li>
 * <li>'A' with "<-"</li>
 * <li>'D' with '.'</li>
 * <li>'_N' with "not "</li>
 * <li>"_l" with 'L',</li>
 * <li>"_r" with 'R',</li>
 * <li>"_c" with 'C',</li>
 * <li>"_a" with 'A' and</li>
 * <li>"_d" with 'D'.</li>
 * </ul>
 * After that the input should be parseable by {@link lp.parse.LpParser}.
 *
 * @author Martin Slota
 * @version 1.0.0
 * @see LpPrinter
 */
public class LpEncoder<W extends Writer> extends LpPrinter<W> {
	/**
	 * A convenience method that creates a new {@link LpBuffer} that uses a new
	 * {@link LpEncoder} instance to write {@link lp.struct.LpStructureUnit}s.
	 *
	 * @return as specified above
	 * @see LpBuffer
	 */
	public static LpBuffer getBuffer() {
		return new LpBuffer(new LpEncoder<StringWriter>(new StringWriter()));
	}
	
	/**
	 * Creates a new instance of {@code LpEncoder}.
	 *
	 * @param out the underlying writer into which everything will be written
	 */
	public LpEncoder(W out) {
		super(out);
	}
	
	/**
	 * Appends {@code seq} but escapes the characters '_', 'L', 'R', 'C', 'A',
	 * 'D' by writing "__", "_l", "_r", "_c", "_a", "_d" instead of them.
	 *
	 * @param seq the character sequence to be appended
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) in
	 * case an I/O exception occurs while writing to the underlying
	 * {@code Writer}
	 */
	protected void appendEncoded(CharSequence seq) {
		for (int i = 0; i < seq.length(); i++) {
			char c = seq.charAt(i);
			if (escapeChar(c)) {
				write('_');
				write(Character.toLowerCase(c));
			} else {
				write(c);
			}
		}
	}
	
	/**
	 * Returns {@code true} iff {@code c} is one of '_', 'L', 'R', 'C', 'A',
	 * 'D'.
	 *
	 * @param c examined character (should we escape it or not?)
	 * @return as specified above
	 */
	protected boolean escapeChar(char c) {
		return (c == '_'
				|| c == 'L'
				|| c == 'R'
				|| c == 'C'
				|| c == 'A'
				|| c == 'D');
	}
	
	/**
	 * Appends an encoded form of a {@link LpConstant} instance to this
	 * {@link LpEncoder}. Shouldn't be called directly,
	 * {@link #append(LpStructureUnit)} should be used instead.
	 *
	 * The encoded form of a constant is its name (see
	 * {@link LpConstant#getName()}) in which the characters '_', 'L', 'R', 'C',
	 * 'A' and 'D' are escaped (an extra '_' is prepended and they are converted
	 * to lowercase).
	 *
	 * @param con the {@code LpConstant} instance whose natural textual
	 * representation should be appended to the underlying {@code Writer}
	 * @throws NullPointerException if {@code con} is {@code null}
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) in
	 * case an I/O exception occurs while writing to the underlying
	 * {@code Writer}
	 * @see LpPrinter
	 * @see lp.struct.LpStructureUnitVisitor#visit(LpConstant)
	 */
	public void visit(LpConstant con) {
		appendEncoded(con.getName());
	}
	
	/**
	 * Appends an encoded form of a {@link LpVariable} instance to this
	 * {@link LpEncoder}. Shouldn't be called directly,
	 * {@link #append(LpStructureUnit)} should be used instead.
	 *
	 * The encoded form of a variable is its name (see
	 * {@link LpVariable#getName()}) in which the characters '_', 'L', 'R', 'C',
	 * 'A' and 'D' are escaped (an extra '_' is prepended and they are converted
	 * to lowercase).
	 *
	 * @param var the {@code LpVariable} instance whose natural textual
	 * representation should be appended to the underlying {@code Writer}
	 * @throws NullPointerException if {@code con} is {@code null}
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) in
	 * case an I/O exception occurs while writing to the underlying
	 * {@code Writer}
	 * @see LpPrinter
	 * @see lp.struct.LpStructureUnitVisitor#visit(LpVariable)
	 */
	public void visit(LpVariable var) {
		appendEncoded(var.getName());
	}
	
	/**
	 * Appends an encoded form of a {@link LpFunction} instance to this
	 * {@link LpEncoder}. Shouldn't be called directly,
	 * {@link #append(LpStructureUnit)} should be used instead.
	 *
	 * The encoded form of a function symbol is its name (see
	 * {@link LpFunction#getName()}) in which the characters '_', 'L', 'R', 'C',
	 * 'A' and 'D' are escaped (an extra '_' is prepended and they are converted
	 * to lowercase).
	 *
	 * @param fun the {@code LpFunction} instance whose natural textual
	 * representation should be appended to the underlying {@code Writer}
	 * @throws NullPointerException if {@code con} is {@code null}
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) in
	 * case an I/O exception occurs while writing to the underlying
	 * {@code Writer}
	 * @see LpPrinter
	 * @see lp.struct.LpStructureUnitVisitor#visit(LpFunction)
	 */
	public void visit(LpFunction fun) {
		appendEncoded(fun.getName());
	}
	
	/**
	 * This method is used in {@link #visit(LpCompoundTerm)} and
	 * {@link #visit(LpLiteral)}. Can also be useful for overriding classes.
	 *
	 * It appends an encoded form of a list of terms. It is composed of
	 * a 'L' by encoded representations of the terms in the list, separated from
	 * each other by a 'C'. It ends with an 'R'.
	 *
	 * @param args the argument list that will be appended to the
	 * underlying {@code Writer}
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) in
	 * case an I/O exception occurs while writing to the underlying
	 * {@code Writer}
	 */
	protected void visitArgumentList(List<LpTerm> args) {
		if (args != null && !args.isEmpty()) {
			write('L');
			Iterator<LpTerm> i = args.iterator();
			i.next().accept(this);
			while(i.hasNext()) {
				write('C');
				i.next().accept(this);
			}
			write('R');
		}
	}
	
	/**
	 * Appends a natural textual form of a {@link LpCompoundTerm} instance to
	 * the underlying {@code Writer}. Shouldn't be called directly,
	 * {@link #append(LpStructureUnit)} should be used instead.
	 *
	 * The encoded form of a compound term is its function's encoded form
	 * followed by an 'L' and the encoded textual representations of its
	 * arguments, separated from each other by a 'C'. It ends with an 'R'.
	 *
	 * @param term the {@code LpCompoundTerm} instance whose encoded
	 * representation should be appended to the underlying {@code Writer}
	 * @throws NullPointerException if {@code term} is {@code null}
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) in
	 * case an I/O exception occurs while writing to the underlying
	 * {@code Writer}
	 * @see LpPrinter
	 * @see lp.struct.LpStructureUnitVisitor#visit(LpCompoundTerm)
	 */
	public void visit(LpCompoundTerm term) {
		visit(term.getFunction());
		visitArgumentList(term.getArguments());
	}
	
	/**
	 * Appends an encoded form of a {@link LpPredicate} instance to this
	 * {@link LpEncoder}. Shouldn't be called directly,
	 * {@link #append(LpStructureUnit)} should be used instead.
	 *
	 * The encoded form of a predicate symbol is its name (see
	 * {@link LpPredicate#getName()}) in which the characters '_', 'L', 'R',
	 * 'C', 'A' and 'D' are escaped (an extra '_' is prepended and they are
	 * converted to lowercase).
	 *
	 * @param pred the {@code LpPredicate} instance whose natural textual
	 * representation should be appended to the underlying {@code Writer}
	 * @throws NullPointerException if {@code con} is {@code null}
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) in
	 * case an I/O exception occurs while writing to the underlying
	 * {@code Writer}
	 * @see LpPrinter
	 * @see lp.struct.LpStructureUnitVisitor#visit(LpPredicate)
	 */
	public void visit(LpPredicate pred) {
		appendEncoded(pred.getName());
	}
	
	/**
	 * Appends a natural textual form of a {@link LpAtom} instance to the
	 * underlying {@code Writer}. Shouldn't be called directly,
	 * {@link #append(LpStructureUnit)} should be used instead.
	 *
	 * The encoded form of an atom is its predicate's encoded form followed by
	 * an 'L' and the encoded textual representations of its arguments,
	 * separated from each other by a 'C'. It ends with an 'R'.
	 *
	 * @param atom the {@code LpAtom} instance whose encoded
	 * representation should be appended to the underlying {@code Writer}
	 * @throws NullPointerException if {@code term} is {@code null}
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) in
	 * case an I/O exception occurs while writing to the underlying
	 * {@code Writer}
	 * @see LpPrinter
	 * @see lp.struct.LpStructureUnitVisitor#visit(LpCompoundTerm)
	 */
	public void visit(LpAtom atom) {
		visit(atom.getPredicate());
		visitArgumentList(atom.getArguments());
	}
	
	/**
	 * Appends an encoded form of a {@link LpLiteral} instance to the underlying
	 * {@code Writer}. Shouldn't be called directly,
	 * {@link #append(LpStructureUnit)} should be used instead.
	 *
	 * The encoded form of a literal is the encoded form of its underlying atom
	 * with "_N" in front of it if {@link LpLiteral#isPositive()} is
	 * {@code false}.
	 *
	 * @param lit the {@code LpLiteral} instance whose encoded representation
	 * should be appended to the underlying {@code Writer}
	 * @throws NullPointerException if {@code lit} is {@code null}
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) in
	 * case an I/O exception occurs while writing to the underlying
	 * {@code Writer}
	 * @see LpPrinter
	 * @see lp.struct.LpStructureUnitVisitor#visit(LpLiteral)
	 */
	public void visit(LpLiteral lit) {
		if (!lit.isPositive())
			write("_N");
		visit(lit.getAtom());
	}
	
	/**
	 * Appends an encoded form of a {@link LpRule} instance to the underlying
	 * {@code Writer}. Shouldn't be called directly,
	 * {@link #append(LpStructureUnit)} should be used instead.
	 *
	 * The encoded form of a rule consists of the encoded form of its head
	 * followed by 'A' and natural forms of literals in its body, separated from
	 * each other by a 'C'. It ends with a 'D'.
	 *
	 * If {@link LpRule#getHead()} is {@code null}, the encoded form
	 * begins with 'A' (the head's encoded form is left out).
	 *
	 * @param rule the {@code LpRule} instance whose encoded representation
	 * should be appended to the underlying {@code Writer}
	 * @throws NullPointerException if {@code rule} is {@code null}
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) in
	 * case an I/O exception occurs while writing to the underlying
	 * {@code Writer}
	 * @see LpPrinter
	 * @see lp.struct.LpStructureUnitVisitor#visit(LpRule)
	 */
	public void visit(LpRule rule) {
		visit(rule.getHead());
		write('A');
		Set<LpLiteral> body = rule.getBody();
		if (!body.isEmpty()) {
			Iterator<LpLiteral> i = body.iterator();
			visit(i.next());
			while(i.hasNext()) {
				write('C');
				visit(i.next());
			}
		}
		write('D');
	}
}