/*
 * LpAtom.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-02-11): initial version
 * v0.2 (2007-02-11):
 * - implementation imported from LpLiteral
 * - docs added
 * - tests added
 * v0.2.1 (2007-02-14):
 * - keyBuilder added to static members
 * v0.2.2 (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 an atom in logic programming. Such an atom is composed
 * of a predicate symbol (represented by a {@link LpPredicate} instance) and its
 * arguments&#8212;terms represented by {@link LpTerm} instances.
 * The number of arguments must match the predicate's arity (returned by
 * {@link LpPredicate#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 LpPredicate
 * @see LpTerm
 * @see LpLiteral
 */
public class LpAtom extends LpAbstractStructureUnit {
	/**
	 * A pool of instances of this immutable class. The
	 * {@link #getInstance(LpPredicate, 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, LpAtom> pool =
			new HashMap<String, LpAtom>(100);
	
	/**
	 * Used to create construct keys in {@link #pool}.
	 */
	private static final LpBuffer keyBuilder = LpPrettyPrinter.getBuffer();
	
	/**
	 * Returns an instance of {@code LpAtom} with the given predicate symbol and
	 * argument list.
	 *
	 * @param predicate the requested atom's predicate symbol
	 * @param arguments the argument list of the requested {@code LpAtom}
	 * instance
	 * @throws IllegalArgumentException if {@code predicate} is {@code null}
	 * or the number of arguments doesn't match the predicate's arity, i.e. this
	 * condition doesn't hold:
	 * <pre>(arguments == null && predicate.getArity() == 0) || arguments.size() == predicate.getArity()</pre>
	 */
	public static synchronized LpAtom getInstance(LpPredicate predicate,
			List<LpTerm> arguments) {
		if (predicate == null) {
			throw new IllegalArgumentException("Predicate is null!");
		}
		
		List<LpTerm> args = toUnmodifiableList(arguments);
		assert args != null;
		
		checkArity(predicate, 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() == predicate.getArity());
		
		keyBuilder.reset();
		keyBuilder.append(predicate);
		keyBuilder.append('/').append(Integer.toString(predicate.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();
		
		LpAtom result = pool.get(key);
		if (result == null) {
			result = new LpAtom(predicate, args);
			pool.put(key, result);
		}
		return result;
	}
	
	/**
	 * Checks whether {@code predicate.getArity()} equals
	 * {@code arguments.size()}. If it doesn't, throws an
	 * {@code IllegalArgumentException}.
	 *
	 * @param predicate a predicate, its arity must match the length of the
	 * argument list
	 * @param arguments an argument list, its length must match the predicate's
	 * arity
	 * @throws IllegalArgumentException if {@code predicate.getArity()} is not
	 * the same as {@code arguments.size()}
	 */
	private static void checkArity(LpPredicate predicate,
			List<LpTerm> arguments) {
		if (predicate.getArity() != arguments.size())
			throw new IllegalArgumentException("Number of arguments (" +
					arguments.size() +
					") doesn't match the function's arity (" +
					predicate.getArity() + ")!");
	}
	
	/**
	 * The predicate symbol that is used in this atom.
	 */
	private final LpPredicate predicate;
	
	/**
	 * The predicate's arguments.
	 */
	private final List<LpTerm> arguments;
	
	/**
	 * A positive literal bound to this atom. Is returned lazily when requested.
	 */
	private LpLiteral posLit;
	
	/**
	 * A negative literal bound to this atom. Is returned lazily when requested.
	 */
	private LpLiteral negLit;
	
	/**
	 * Creates a new instance of {@code LpAtom} with the given predicate symbol
	 * and argument list.
	 *
	 * @param predicate the requested atom's predicate symbol
	 * @param arguments the argument list of the requested {@code LpAtom}
	 * instance
	 * @throws IllegalArgumentException if {@code predicate} is {@code null}
	 * or the number of arguments doesn't match the predicate's arity, i.e. this
	 * condition doesn't hold:
	 * <pre>(arguments == null && predicate.getArity() == 0) || arguments.size() == predicate.getArity()</pre>
	 */
	private LpAtom(LpPredicate predicate, List<LpTerm> arguments) {
		this.predicate = predicate;
		this.arguments = arguments;
		posLit = null;
		negLit = null;
	}
	
	/**
	 * Returns the predicate symbol of this atom, the same that was given to the
	 * constructor.
	 *
	 * @return the {@code LpPredicate} instance given in the constructor
	 */
	public LpPredicate getPredicate() {
		return predicate;
	}
	
	/**
	 * Returns the list of arguments of this atom. 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;
	}
	
	/**
	 * Returns a positive literal for this atom represented by a
	 * {@link LpLiteral} instance.
	 *
	 * @return as specified above
	 */
	public synchronized LpLiteral getPositiveLiteral() {
		if (posLit == null)
			posLit = new LpLiteral(true, this);
		return posLit;
	}
	
	/**
	 * Returns a negative literal for this atom represented by a
	 * {@link LpLiteral} instance.
	 *
	 * @return as specified above
	 */
	public synchronized  LpLiteral getNegativeLiteral() {
		if (negLit == null)
			negLit = new LpLiteral(false, this);
		return negLit;
	}
	
	/**
	 * Returns a literal for this atom represented by a {@link LpLiteral}
	 * instance. The parameter constrols if the literal will be positive or
	 * negative.
	 *
	 * @param positive if {@code true}, the resulting literal will be positive,
	 * otherwise it will be negative
	 * @return as specified above
	 */
	public LpLiteral getLiteral(boolean positive) {
		return positive ? getPositiveLiteral() : getNegativeLiteral();
	}
	
	/**
	 * 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 LpAtom} instance,</li>
	 * <li>its predicate is equal to this atom's predicate as defined by
	 * {@link LpPredicate#equals(Object)},</li>
	 * <li>its argument list are equal to this atom'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 LpPredicate#equals(Object)
	 * @see LpTerm#equals(Object)
	 * @see List#equals(Object)
	 */
	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof LpAtom))
			return false;
		
		LpAtom l = (LpAtom) obj;
		
		if (!getPredicate().equals(l.getPredicate()))
			return false;
		
		return getArguments().equals(l.getArguments());
	}
	
	/**
	 * Overriden in order to maintain the general contract of
	 * {@link Object#hashCode()}.
	 *
	 * @return the hash of this object
	 * @see LpPredicate#hashCode()
	 * @see List#hashCode()
	 */
	@Override
	public int hashCode() {
		int result = 0;
		result += getPredicate().hashCode();
		result += getArguments().hashCode();
		return result;
	}
}