/*
 * LpCompoundTerm.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:
 * v0.1 (2007-01-02): initial version
 * v0.2 (2007-01-02):
 * - members added, documented
 * - constructor added, tested, documented
 * v0.2.1 (2007-01-04):
 * - equals and hashCode added, tested, documented
 * v0.2.2 (2007-01-05):
 * - added "extends LpAbstractStructureUnit" declaration
 * - appendNormalForm(Appendable) added, tested, documented
 * v0.2.3 (2007-01-12):
 * - appendNormalForm modified to throw IllegalArgumentException
 * v0.2.4 (2007-01-18):
 * - appendNormalForm() deleted, the same functionality now provided by
 *   LpPrettyPrinter
 * - getter methods getFunction() and getArguments() added
 * - accept(LpStructureUnit) added
 * - documentation updated
 * v0.2.5 (2007-01-28):
 * - documentation updated
 * v0.2.6 (2007-02-11):
 * - some refactoring
 * - some minor changes in documentation
 * v0.2.7 (2007-02-14):
 * - a pool like in LpAtom added
 * v0.2.8 (2007-03-06):
 * - constructor checks moved to getInstance()
 * v1.0.0 (2007-05-04):
 * - minor changes to reflect the new LpPrettyPrinter structure
 */

package lp.struct;

import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import lp.struct.util.LpBuffer;

import lp.struct.util.LpPrettyPrinter;

/**
 * This class represents a compound term in logic programming. Such a
 * term is composed of a function symbol (represented by a {@link LpFunction}
 * instance) and its arguments&#8212;terms represented by {@link LpTerm}
 * instances. The number of arguments must match the function's arity
 * (returned by {@link LpFunction#getArity()}). This class is immutable as long
 * as the argument list used to create it is not changed later.
 *
 * @author Martin Slota
 * @version 1.0.0
 * @see LpFunction
 * @see LpTerm
 */
public class LpCompoundTerm extends LpAbstractStructureUnit implements LpTerm {
	/**
	 * A pool of instances of this immutable class. The
	 * {@link #getInstance(LpFunction, List)} method first looks here for the
	 * requested instances and if it is forced to create a new one, it is added
	 * to this pool.
	 */
	private static final Map<String, LpCompoundTerm> pool =
			new HashMap<String, LpCompoundTerm>(100);
	
	/**
	 * Used to create construct keys in {@link #pool}.
	 */
	private static final LpBuffer keyBuilder = LpPrettyPrinter.getBuffer();
	
	/**
	 * Returns an instance of {@code LpCompoundTerm} with the given function
	 * symbol and argument list.
	 *
	 * @param function the requested term's function symbol
	 * @param arguments the argument list of the requested
	 * {@code LpCompoundTerm} instance
	 * @throws IllegalArgumentException if {@code function} is {@code null}
	 * or the number of args doesn't match the function's arity, i.e. this
	 * condition doesn't hold:
	 * <pre>(args == null && function.getArity() == 0) || args.size() == function.getArity()</pre>
	 */
	public static synchronized LpCompoundTerm getInstance(LpFunction function,
			List<LpTerm> arguments) {
		if (function == null) {
			throw new IllegalArgumentException("Function is null!");
		}
		
		List<LpTerm> args = toUnmodifiableList(arguments);
		assert args != null;
		
		checkArity(function, args);
		// check the condition from the method's description via an assert
		// should always hold at this place because of the previous checks
		assert (args.size() == function.getArity());
		
		keyBuilder.reset();
		keyBuilder.append(function);
		keyBuilder.append('/').append(Integer.toString(function.getArity()));
		if (args != null && !args.isEmpty()) {
			keyBuilder.append('(');
			Iterator<LpTerm> argIter = args.iterator();
			keyBuilder.append(argIter.next());
			while(argIter.hasNext()) {
				keyBuilder.append(',');
				keyBuilder.append(argIter.next());
			}
			keyBuilder.append(')');
		}
		String key = keyBuilder.toString();
		
		LpCompoundTerm result = pool.get(key);
		if (result == null) {
			result = new LpCompoundTerm(function, args);
			pool.put(key, result);
		}
		return result;
	}
	
	/**
	 * Checks whether {@code function.getArity()} equals
	 * {@code arguments.size()}. If it doesn't, throws an
	 * {@code IllegalArgumentException}.
	 *
	 * @param function a function, its arity must match the length of the
	 * argument list
	 * @param arguments an argument list, its length must match the function's
	 * arity
	 * @throws IllegalArgumentException if {@code function.getArity()} is not
	 * the same as {@code arguments.size()}
	 */
	private static void checkArity(LpFunction function, List<LpTerm> arguments) {
		if (function.getArity() != arguments.size())
			throw new IllegalArgumentException("Number of arguments (" +
					arguments.size() +
					") doesn't match the function's arity (" +
					function.getArity() + ")!");
	}
	
	/**
	 * The top-most function symbol that is used in this compound term.
	 */
	private final LpFunction function;
	
	/**
	 * The function's arguments.
	 */
	private final List<LpTerm> arguments;
	
	/**
	 * Creates a new instance with the given values as members. Arguments of the
	 * constructor are not copied (especially the {@code arguments}
	 * {@link List}).
	 *
	 * @param function the top-most function symbol used in this compound term
	 * @param arguments terms that will become arguments of the function
	 * @throws IllegalArgumentException if {@code function} is {@code null} or
	 * the number of arguments doesn't match the function's arity, i.e. if this
	 * condition doesn't hold:
	 * <pre>(arguments == null && function.getArity() == 0) || arguments.size() == function.getArity()</pre>
	 */
	private LpCompoundTerm(LpFunction function, List<LpTerm> arguments) {
		this.function = function;
		this.arguments = arguments;
	}
	
	/**
	 * Returns the function symbol of this compound term, the same that was
	 * given to the constructor.
	 *
	 * @return the {@code LpFunction} instance given in the constructor
	 */
	public LpFunction getFunction() {
		return function;
	}
	
	/**
	 * Returns the list of arguments of this compound term. Doesn't return
	 * {@code null}, even if it was given to the constructor of this instance
	 * (in that case an empty list is returned).
	 *
	 * @return an unmodifiable version of the argument list from the constructor
	 */
	public List<LpTerm> getArguments() {
		return arguments;
	}
	
	/**
	 * Accepts {@code LpStructureUnitVisitor} instance, i.e. calls its
	 * {@code visitor.visit(this)}.
	 *
	 * @param visitor the visitor to accept
	 */
	public void accept(LpStructureUnitVisitor visitor) {
		visitor.visit(this);
	}
	
	/**
	 * Returns {@code true} if and only if
	 * <ol>
	 * <li>{@code obj} is a {@link LpCompoundTerm} instance,</li>
	 * <li>its function symbol is equal to this term's function symbol as
	 * defined by {@link LpFunction#equals(Object)},</li>
	 * <li>its argument list is equal to this term's argument list</li>
	 * </ol>
	 *
	 * @param obj the object to compare with
	 * @return {@code true} if this object is equal to {@code obj} according to
	 * the description above, and {@code false} otherwise
	 * @see LpFunction#equals(Object)
	 * @see List#equals(Object)
	 * @see LpTerm#equals(Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof LpCompoundTerm))
			return false;
		
		LpCompoundTerm t = (LpCompoundTerm) obj;
		
		if (!getFunction().equals(t.getFunction()))
			return false;
		
		return getArguments().equals(t.getArguments());
	}
	
	/**
	 * Overriden in order to maintain the general contract of
	 * {@link Object#hashCode()}.
	 *
	 * @return the hash of this object
	 * @see LpFunction#hashCode()
	 * @see List#hashCode()
	 * @see LpTerm#equals(Object)
	 */
	@Override
	public int hashCode() {
		super.hashCode();
		int result = 0;
		result += getFunction().hashCode();
		result += getArguments().hashCode();
		return result;
	}
}