/*******************************************************************************
 * Copyright (C) 2011 - 2015 Yoav Artzi, All rights reserved.
 * <p>
 * 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 any later version.
 * <p>
 * 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.
 * <p>
 * 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.
 *******************************************************************************/
package edu.cornell.cs.nlp.spf.mr.lambda.printers;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import edu.cornell.cs.nlp.spf.mr.lambda.Lambda;
import edu.cornell.cs.nlp.spf.mr.lambda.Literal;
import edu.cornell.cs.nlp.spf.mr.lambda.LogicLanguageServices;
import edu.cornell.cs.nlp.spf.mr.lambda.LogicalConstant;
import edu.cornell.cs.nlp.spf.mr.lambda.LogicalExpression;
import edu.cornell.cs.nlp.spf.mr.lambda.SkolemId;
import edu.cornell.cs.nlp.spf.mr.lambda.Term;
import edu.cornell.cs.nlp.spf.mr.lambda.Variable;
import edu.cornell.cs.nlp.spf.mr.lambda.visitor.HasFreeVariables;
import edu.cornell.cs.nlp.spf.mr.lambda.visitor.ILogicalExpressionVisitor;
import edu.cornell.cs.nlp.utils.string.StringUtils;

/**
 * Creates a string representation for a {@link LogicalExpression}.
 *
 * @author Yoav Artzi
 */
public class LogicalExpressionToIndentedString implements
		ILogicalExpressionVisitor {
	private final static String			DEFAULT_IDENT		= "\t";
	private int							currentDepth		= 0;
	private final Set<Variable>			definedVariables	= new HashSet<Variable>();
	private final String				indentation;
	private int							skolemIdCounter		= 1;
	private final Map<SkolemId, String>	skolemIds			= new HashMap<SkolemId, String>();
	private final List<Variable>		variablesNamingList	= new LinkedList<Variable>();
	protected final StringBuilder		outputString		= new StringBuilder();

	protected LogicalExpressionToIndentedString(String indentation) {
		this.indentation = indentation;
	}

	public static String of(LogicalExpression expression) {
		return of(expression, DEFAULT_IDENT);
	}

	public static String of(LogicalExpression expression, String indentation) {
		final LogicalExpressionToIndentedString visitor = new LogicalExpressionToIndentedString(
				indentation);
		visitor.visit(expression);

		// Remove empty lines, which can happyen in cases like (lambda $0:e
		// (p:<e,<e,t>> $0 (a:<<e,t>,e (lambda $1:e (boo:<e,t> foo:e))))).
		return visitor.outputString.toString().replaceAll(
				"\n(" + indentation + ")+\n", "\n");
	}

	@Override
	final public void visit(Lambda lambda) {
		outputString.append("(lambda ");
		lambda.getArgument().accept(this);
		outputString.append(' ');
		lambda.getBody().accept(this);
		outputString.append(')');
	}

	@Override
	public void visit(Literal literal) {
		final int len = literal.numArgs();
		if (LogicLanguageServices.isCoordinationPredicate(literal
				.getPredicate())) {
			outputString.append("(");
			literal.getPredicate().accept(this);
			// Visit the arguments to print them. Print a space before each
			// argument.
			++currentDepth;
			for (int i = 0; i < len; ++i) {
				outputString.append("\n"
						+ StringUtils.multiply(indentation, currentDepth));
				literal.getArg(i).accept(this);
			}
			--currentDepth;
			outputString.append(')');
		} else if (!HasFreeVariables.of(literal, true)
				&& outputString.length() > 0) {
			++currentDepth;
			outputString.append("\n"
					+ StringUtils.multiply(indentation, currentDepth));
			outputString.append("(");
			literal.getPredicate().accept(this);
			// Visit the arguments to print them. Print a space before each
			// argument.
			// ++currentDepth;
			for (int i = 0; i < len; ++i) {
				// outputString.append("\n"
				// + StringUtils.multiply(indentation, currentDepth));
				outputString.append(' ');
				literal.getArg(i).accept(this);
			}
			// --currentDepth;
			--currentDepth;
			outputString.append(')');
		} else {
			outputString.append("(");
			literal.getPredicate().accept(this);
			// Visit the arguments to print them. Print a space before each
			// argument.
			for (int i = 0; i < len; ++i) {
				outputString.append(' ');
				literal.getArg(i).accept(this);
			}
			outputString.append(')');
		}
	}

	@Override
	public void visit(LogicalConstant logicalConstant) {
		outputString.append(logicalConstant.getName());
	}

	@Override
	public void visit(LogicalExpression logicalExpression) {
		logicalExpression.accept(this);
	}

	@Override
	public void visit(Variable variable) {
		if (variable instanceof SkolemId) {
			if (!skolemIds.containsKey(variable)) {
				skolemIds.put((SkolemId) variable,
						((SkolemId) variable).getName(skolemIdCounter++));
			}
			outputString.append(skolemIds.get(variable));
		} else {
			outputString.append(getVariableName(variable));
			if (!definedVariables.contains(variable)) {
				outputString.append(Term.TYPE_SEPARATOR);
				outputString.append(variable.getType().getName());
				definedVariables.add(variable);
			}
		}
	}

	private String getVariableName(Variable variable) {
		int num = 0;
		for (final Variable namedVariable : variablesNamingList) {
			if (namedVariable == variable) {
				return "$" + String.valueOf(num);
			}
			++num;
		}
		variablesNamingList.add(variable);
		return "$" + String.valueOf(num);
	}

	public static class Printer implements ILogicalExpressionPrinter {

		private final String	indentation;

		public Printer() {
			this(DEFAULT_IDENT);
		}

		public Printer(String indentation) {
			this.indentation = indentation;
		}

		@Override
		public String toString(LogicalExpression exp) {
			return LogicalExpressionToIndentedString.of(exp, indentation);
		}

	}

}