//==============================================================================
//	
//	Copyright (c) 2002-
//	Authors:
//	* Dave Parker <david.parker@comlab.ox.ac.uk> (University of Oxford, formerly University of Birmingham)
//	
//------------------------------------------------------------------------------
//	
//	This file is part of PRISM.
//	
//	PRISM 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.
//	
//	PRISM 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 PRISM; if not, write to the Free Software Foundation,
//	Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//	
//==============================================================================

package parser.ast;

import java.util.ArrayList;

import common.SafeCast;
import param.BigRational;
import parser.*;
import parser.visitor.*;
import prism.PrismLangException;
import prism.PrismUtils;
import parser.type.*;

public class ExpressionFunc extends Expression
{
	// Built-in function name constants
	public static final int MIN = 0;
	public static final int MAX = 1;
	public static final int FLOOR = 2;
	public static final int CEIL = 3;
	public static final int ROUND = 4;
	public static final int POW = 5;
	public static final int MOD = 6;
	public static final int LOG = 7;
	public static final int MULTI = 8;
	// Built-in function names
	public static final String names[] = { "min", "max", "floor", "ceil", "round", "pow", "mod", "log", "multi"};
	// Min/max function arities
	public static final int minArities[] = { 2, 2, 1, 1, 1, 2, 2, 2, 1 };
	public static final int maxArities[] = { -1, -1, 1, 1, 1, 2, 2, 2, -1 };

	// Function name
	private String name = "";
	private int code = -1;
	// Operands
	private ArrayList<Expression> operands;
	// Was function written in old style notation (using "func" keyword)?
	private boolean oldStyle = false;

	// Constructors

	public ExpressionFunc()
	{
		operands = new ArrayList<Expression>();
	}

	public ExpressionFunc(String name)
	{
		setName(name);
		operands = new ArrayList<Expression>();
	}

	// Set methods

	public void setName(String s)
	{
		int i, n;
		// Set string
		name = s;
		// Determine and set code
		n = names.length;
		code = -1;
		for (i = 0; i < n; i++) {
			if (s.equals(names[i])) {
				code = i;
				break;
			}
		}
	}

	public void addOperand(Expression e)
	{
		operands.add(e);
	}

	public void setOperand(int i, Expression e)
	{
		operands.set(i, e);
	}

	public void setOldStyle(boolean b)
	{
		oldStyle = b;
	}

	// Get methods

	public String getName()
	{
		return name;
	}

	public int getNameCode()
	{
		return code;
	}

	public int getNumOperands()
	{
		return operands.size();
	}

	public Expression getOperand(int i)
	{
		return operands.get(i);
	}

	public boolean getOldStyle()
	{
		return oldStyle;
	}

	public int getMinArity()
	{
		return code == -1 ? Integer.MAX_VALUE : minArities[code];
	}

	public int getMaxArity()
	{
		return code == -1 ? -1 : maxArities[code];
	}

	// Methods required for Expression:

	@Override
	public boolean isConstant()
	{
		int i, n;
		n = getNumOperands();
		for (i = 0; i < n; i++) {
			if (!getOperand(i).isConstant())
				return false;
		}
		return true;
	}

	@Override
	public boolean isProposition()
	{
		int i, n;
		n = getNumOperands();
		for (i = 0; i < n; i++) {
			if (!getOperand(i).isProposition())
				return false;
		}
		return true;
	}
	
	@Override
	public Object evaluate(EvaluateContext ec) throws PrismLangException
	{
		switch (code) {
		case MIN:
			return evaluateMin(ec);
		case MAX:
			return evaluateMax(ec);
		case FLOOR:
			return evaluateFloor(ec);
		case CEIL:
			return evaluateCeil(ec);
		case ROUND:
			return evaluateRound(ec);
		case POW:
			return evaluatePow(ec);
		case MOD:
			return evaluateMod(ec);
		case LOG:
			return evaluateLog(ec);
		}
		throw new PrismLangException("Unknown function \"" + name + "\"", this);
	}

	@Override
	public BigRational evaluateExact(EvaluateContext ec) throws PrismLangException
	{
		switch (code) {
		case MIN:
			return evaluateMinExact(ec);
		case MAX:
			return evaluateMaxExact(ec);
		case FLOOR:
			return evaluateFloorExact(ec);
		case CEIL:
			return evaluateCeilExact(ec);
		case ROUND:
			return evaluateRoundExact(ec);
		case POW:
			return evaluatePowExact(ec);
		case MOD:
			return evaluateModExact(ec);
		case LOG:
			return evaluateLogExact(ec);
		}
		throw new PrismLangException("Unknown function \"" + name + "\"", this);
	}

	
	private Object evaluateMin(EvaluateContext ec) throws PrismLangException
	{
		int i, j, n, iMin;
		double d, dMin;

		if (type instanceof TypeInt) {
			iMin = getOperand(0).evaluateInt(ec);
			n = getNumOperands();
			for (i = 1; i < n; i++) {
				j = getOperand(i).evaluateInt(ec);
				iMin = (j < iMin) ? j : iMin;
			}
			return iMin;
		} else {
			dMin = getOperand(0).evaluateDouble(ec);
			n = getNumOperands();
			for (i = 1; i < n; i++) {
				d = getOperand(i).evaluateDouble(ec);
				dMin = (d < dMin) ? d : dMin;
			}
			return dMin;
		}
	}

	private BigRational evaluateMinExact(EvaluateContext ec) throws PrismLangException
	{
		BigRational min;
	
		min = getOperand(0).evaluateExact(ec);
		for (int i = 1, n = getNumOperands(); i < n; i++) {
			min = min.min(getOperand(i).evaluateExact(ec));
		}
		return min;
	}

	private Object evaluateMax(EvaluateContext ec) throws PrismLangException
	{
		int i, j, n, iMax;
		double d, dMax;

		if (type instanceof TypeInt) {
			iMax = getOperand(0).evaluateInt(ec);
			n = getNumOperands();
			for (i = 1; i < n; i++) {
				j = getOperand(i).evaluateInt(ec);
				iMax = (j > iMax) ? j : iMax;
			}
			return iMax;
		} else {
			dMax = getOperand(0).evaluateDouble(ec);
			n = getNumOperands();
			for (i = 1; i < n; i++) {
				d = getOperand(i).evaluateDouble(ec);
				dMax = (d > dMax) ? d : dMax;
			}
			return dMax;
		}
	}

	private BigRational evaluateMaxExact(EvaluateContext ec) throws PrismLangException
	{
		BigRational max;

		max = getOperand(0).evaluateExact(ec);
		for (int i = 1, n = getNumOperands(); i < n; i++) {
			max = max.max(getOperand(i).evaluateExact(ec));
		}
		return max;
	}

	public Integer evaluateFloor(EvaluateContext ec) throws PrismLangException
	{
		try {
			return evaluateFloor(getOperand(0).evaluateDouble(ec));
		} catch (PrismLangException e) {
			e.setASTElement(this);
			throw e;
		}
	}

	public static int evaluateFloor(double arg) throws PrismLangException
	{
		try {
			return SafeCast.toIntExact(Math.floor(arg));
		} catch (ArithmeticException e) {
			throw new PrismLangException("Cannot take floor() of " + arg + ": " + e.getMessage());
		}
	}

	public Integer evaluateCeil(EvaluateContext ec) throws PrismLangException
	{
		try {
			return evaluateCeil(getOperand(0).evaluateDouble(ec));
		} catch (PrismLangException e) {
			e.setASTElement(this);
			throw e;
		}
	}

	public static int evaluateCeil(double arg) throws PrismLangException
	{
		try {
			return SafeCast.toIntExact(Math.ceil(arg));
		} catch (ArithmeticException e) {
			throw new PrismLangException("Cannot take ceil() of " + arg + ": " + e.getMessage());
		}
	}

	public Integer evaluateRound(EvaluateContext ec) throws PrismLangException
	{
		try {
			return evaluateRound(getOperand(0).evaluateDouble(ec));
		} catch (PrismLangException e) {
			e.setASTElement(this);
			throw e;
		}
	}

	public static int evaluateRound(double arg) throws PrismLangException
	{
		try {
			return SafeCast.toIntExact(Math.round(arg));
		} catch (ArithmeticException e) {
			throw new PrismLangException("Cannot take round() of " + arg + ": " + e.getMessage());
		}
	}

	public BigRational evaluateFloorExact(EvaluateContext ec) throws PrismLangException
	{
		return getOperand(0).evaluateExact(ec).floor();
	}

	public BigRational evaluateCeilExact(EvaluateContext ec) throws PrismLangException
	{
		return getOperand(0).evaluateExact(ec).ceil();
	}

	public BigRational evaluateRoundExact(EvaluateContext ec) throws PrismLangException
	{
		return getOperand(0).evaluateExact(ec).round();
	}

	public Object evaluatePow(EvaluateContext ec) throws PrismLangException
	{
		try {
			if (type instanceof TypeInt) {
				return evaluatePowInt(getOperand(0).evaluateInt(ec), getOperand(1).evaluateInt(ec));
			} else {
				return evaluatePowDouble(getOperand(0).evaluateDouble(ec), getOperand(1).evaluateDouble(ec));
			}
		} catch (PrismLangException e) {
			e.setASTElement(this);
			throw e;
		}
	}

	public static int evaluatePowInt(int base, int exp) throws PrismLangException
	{
		// Not allowed to do e.g. pow(2,-2) because of typing (should be pow(2.0,-2) instead)
		if (exp < 0)
			throw new PrismLangException("Negative exponent not allowed for integer power");
		try {
			return SafeCast.toIntExact(Math.pow(base, exp));
		} catch (ArithmeticException e) {
			throw new PrismLangException("Overflow evaluating integer power: " + e.getMessage());
		}
	}

	public static double evaluatePowDouble(double base, double exp) throws PrismLangException
	{
		return Math.pow(base, exp);
	}

	public BigRational evaluatePowExact(EvaluateContext ec) throws PrismLangException
	{
		BigRational base = getOperand(0).evaluateExact(ec);
		BigRational exp = getOperand(1).evaluateExact(ec);

		try {
			int expInt = exp.toInt();
			return base.pow(expInt);
		} catch (PrismLangException e) {
			throw new PrismLangException("Can not compute pow exactly, as there is a problem with the exponent: " + e.getMessage(), this);
		}
	}

	public Object evaluateMod(EvaluateContext ec) throws PrismLangException
	{
		try {
			return evaluateMod(getOperand(0).evaluateInt(ec), getOperand(1).evaluateInt(ec));
		} catch (PrismLangException e) {
			e.setASTElement(this);
			throw e;
		}
	}

	public static int evaluateMod(int i, int j) throws PrismLangException
	{
		// Non-positive divisor not allowed 
		if (j <= 0)
			throw new PrismLangException("Attempt to compute modulo with non-positive divisor");
		// Take care of negative case (% is remainder, not modulo)
		int rem = i % j;
		return (rem < 0) ? rem + j : rem;
	}

	public BigRational evaluateModExact(EvaluateContext ec) throws PrismLangException
	{
		BigRational a = getOperand(0).evaluateExact(ec);
		BigRational b = getOperand(1).evaluateExact(ec);

		if (!a.isInteger() && !b.isInteger()) {
			throw new PrismLangException("Can not compute mod for non-integer arguments", this);
		}
		return new BigRational(a.getNum().mod(b.getNum()));
	}

	public Object evaluateLog(EvaluateContext ec) throws PrismLangException
	{
		try {
			return evaluateLog(getOperand(0).evaluateDouble(ec), getOperand(1).evaluateDouble(ec));
		} catch (PrismLangException e) {
			e.setASTElement(this);
			throw e;
		}
	}

	public static double evaluateLog(double x, double b) throws PrismLangException
	{
		return PrismUtils.log(x, b);
	}

	public BigRational evaluateLogExact(EvaluateContext ec) throws PrismLangException
	{
		throw new PrismLangException("Currently, can not compute log exactly", this);
	}

	@Override
	public boolean returnsSingleValue()
	{
		int i, n;
		// Otherwise, true iff all operands true
		n = getNumOperands();
		for (i = 0; i < n; i++) {
			if (!getOperand(i).returnsSingleValue())
				return false;
		}
		return true;
	}

	// Methods required for ASTElement:

	@Override
	public Object accept(ASTVisitor v) throws PrismLangException
	{
		return v.visit(this);
	}

	@Override
	public Expression deepCopy()
	{
		int i, n;
		ExpressionFunc e;

		e = new ExpressionFunc(name);
		e.setOldStyle(oldStyle);
		n = getNumOperands();
		for (i = 0; i < n; i++) {
			e.addOperand((Expression) getOperand(i).deepCopy());
		}
		e.setType(type);
		e.setPosition(this);

		return e;
	}
	
	// Standard methods
	
	@Override
	public String toString()
	{
		int i, n;
		String s = "";
		boolean first = true;

		if (!oldStyle)
			s += name + "(";
		else
			s += "func(" + name + ", ";
		n = operands.size();
		for (i = 0; i < n; i++) {
			if (!first)
				s += ", ";
			else
				first = false;
			s = s + getOperand(i);
		}
		s += ")";

		return s;
	}

	@Override
	public int hashCode()
	{
		final int prime = 31;
		int result = 1;
		result = prime * result + code;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		result = prime * result + (oldStyle ? 1231 : 1237);
		result = prime * result + ((operands == null) ? 0 : operands.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj)
	{
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		ExpressionFunc other = (ExpressionFunc) obj;
		if (code != other.code)
			return false;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		if (oldStyle != other.oldStyle)
			return false;
		if (operands == null) {
			if (other.operands != null)
				return false;
		} else if (!operands.equals(other.operands))
			return false;
		return true;
	}
}

// ------------------------------------------------------------------------------
