package info.bliki.wiki.template.expr;

import info.bliki.wiki.template.expr.ast.ASTNode;
import info.bliki.wiki.template.expr.ast.IParserFactory;
import info.bliki.wiki.template.expr.ast.NumberNode;
import info.bliki.wiki.template.expr.operator.ASTNodeFactory;
import info.bliki.wiki.template.expr.operator.InfixOperator;
import info.bliki.wiki.template.expr.operator.Operator;
import info.bliki.wiki.template.expr.operator.PostfixOperator;
import info.bliki.wiki.template.expr.operator.PrefixOperator;

/**
 * Create an expression of the <code>ASTNode</code> class-hierarchy from a
 * math formulas string representation
 * 
 * See <a
 * href="http://en.wikipedia.org/wiki/Operator-precedence_parser">Operator-precedence
 * parser</a> for the idea, how to parse the operators depending on their
 * precedence.
 */
public class Parser extends Scanner {
	/**
	 * Use '('...')' as brackets for arguments
	 */
	boolean fRelaxedSyntax;

	public Parser() {
		this(ASTNodeFactory.MMA_STYLE_FACTORY, false);
	}

	public Parser(final boolean relaxedSyntax) throws SyntaxError {
		this(ASTNodeFactory.MMA_STYLE_FACTORY, relaxedSyntax);
	}

	public Parser(IParserFactory factory, final boolean relaxedSyntax) throws SyntaxError {
		super();
		fRelaxedSyntax = relaxedSyntax;
		fFactory = factory;
	}

	public void setFactory(final IParserFactory factory) {
		this.fFactory = factory;
	}

	public IParserFactory getFactory() {
		return fFactory;
	}

	/**
	 * Determine the current PrefixOperator
	 * 
	 * @return <code>null</code> if no prefix operator could be determined
	 */
	private PrefixOperator determinePrefixOperator() {
		Operator oper = null;
		for (int i = 0; i < fOperList.size(); i++) {
			oper = (Operator) fOperList.get(i);
			if (oper instanceof PrefixOperator) {
				return (PrefixOperator) oper;
			}
		}
		return null;
	}

	/**
	 * Determine the current PostfixOperator
	 * 
	 * @return <code>null</code> if no postfix operator could be determined
	 */
	private PostfixOperator determinePostfixOperator() {
		Operator oper = null;
		for (int i = 0; i < fOperList.size(); i++) {
			oper = (Operator) fOperList.get(i);
			if (oper instanceof PostfixOperator) {
				return (PostfixOperator) oper;
			}
		}
		return null;
	}

	/**
	 * Determine the current BinaryOperator
	 * 
	 * @return <code>null</code> if no binary operator could be determined
	 */
	private InfixOperator determineBinaryOperator() {
		Operator oper = null;
		for (int i = 0; i < fOperList.size(); i++) {
			oper = (Operator) fOperList.get(i);
			if (oper instanceof InfixOperator) {
				return (InfixOperator) oper;
			}
		}
		return null;
	}

	private ASTNode parsePrimary() {
		if (fToken == TT_OPERATOR) {
			final Operator oper = determinePrefixOperator();

			if (oper instanceof PrefixOperator) {
				getNextToken();
				final ASTNode temp = parseLookaheadOperator(oper.getPrecedence());
				if (oper.getFunctionName().equals("PreMinus")) {
					// special cases for negative numbers
					if (temp instanceof NumberNode) {
						((NumberNode) temp).toggleSign();
						return temp;
					}
				}
				return ((PrefixOperator) oper).createFunction(fFactory, temp);
			}
			throwSyntaxError("Operator: " + fOperatorString + " is no prefix operator.");

		}
		return getFactor();
	}

	private ASTNode parseLookaheadOperator(final int min_precedence) {
		ASTNode rhs = parsePrimary();
		Operator operLookahead;
		InfixOperator binOper;
		while (true) {
			final int lookahead = fToken;
			if (lookahead != TT_OPERATOR) {
				break;
			}
			operLookahead = determineBinaryOperator();
			if (operLookahead instanceof InfixOperator) {
				binOper = (InfixOperator) operLookahead;
				if (binOper.getPrecedence() > min_precedence) {
					rhs = parseOperators(rhs, operLookahead.getPrecedence());
					continue;
				} else if ((binOper.getPrecedence() == min_precedence) && (binOper.getGrouping() == InfixOperator.RIGHT_ASSOCIATIVE)) {
					rhs = parseOperators(rhs, operLookahead.getPrecedence());
					continue;
				}
			} else {
				operLookahead = determinePostfixOperator();

				if (operLookahead instanceof PostfixOperator) {
					if (operLookahead.getPrecedence() > min_precedence) {
						getNextToken();
						rhs = ((PostfixOperator) operLookahead).createFunction(fFactory, rhs);
						continue;
					}
				}
			}
			break;
		}
		return rhs;
	}

	/**
	 * See <a
	 * href="http://en.wikipedia.org/wiki/Operator-precedence_parser">Operator-precedence
	 * parser</a> for the idea, how to parse the operators depending on their
	 * precedence.
	 * 
	 * @param lhs
	 *          the already parsed left-hand-side of the operator
	 * @param min_precedence
	 * @return
	 */
	private ASTNode parseOperators(ASTNode lhs, final int min_precedence) {
		ASTNode rhs = null;
		Operator oper;
		while (true) {
			if (fToken != TT_OPERATOR) {
				break;
			}
			oper = determineBinaryOperator();

			if (oper instanceof InfixOperator) {
				if (oper.getPrecedence() >= min_precedence) {
					getNextToken();
					rhs = parseLookaheadOperator(oper.getPrecedence());
					lhs = ((InfixOperator) oper).createFunction(fFactory, lhs, rhs);
					// lhs = parseArguments(lhs);
					continue;
				}
			} else {
				oper = determinePostfixOperator();

				if (oper instanceof PostfixOperator) {
					getNextToken();
					lhs = ((PostfixOperator) oper).createFunction(fFactory, lhs);
					// lhs = parseArguments(lhs);
					continue;
				}
				throwSyntaxError("Operator: " + fOperatorString + " is no infix or postfix operator.");
			}
			// }
			break;
		}
		return lhs;
	}

	/**
	 * Parse the given <code>expression</code> String into an ASTNode.
	 * 
	 * @param expression
	 *          a formula string which should be parsed.
	 * @return the parsed ASTNode representation of the given formula string
	 * @throws SyntaxError
	 */
	public ASTNode parse(final String expression) throws SyntaxError {
		initialize(expression);
		final ASTNode temp = parseOperators(parsePrimary(), 0);
		if (fToken != TT_EOF) {
			if (fToken == TT_PRECEDENCE_CLOSE) {
				throwSyntaxError("Too many closing ')'; End-of-file not reached.");
			}

			if (fOperatorString=="E") {
				fCurrentPosition--;
			}
			throwSyntaxError("End-of-file not reached.");
		}

		return temp;
	}

	/**
	 * Method Declaration.
	 * 
	 * @return
	 * @see
	 */
	private ASTNode getNumber(final boolean negative) throws SyntaxError {
		ASTNode temp = null;
		final Object[] result = getNumberString();
		String number = (String) result[0];
		final int numFormat = ((Integer) result[1]).intValue();
		try {
			if (negative) {
				number = '-' + number;
			}
			if (numFormat < 0) {
				temp = fFactory.createDouble(number);
			} else {
				temp = fFactory.createInteger(number, numFormat);
			}
		} catch (final Throwable e) {
			throwSyntaxError("Number format error: " + number, number.length());
		}
		getNextToken();
		return temp;
	}

	private int getIntegerNumber() throws SyntaxError {
		final Object[] result = getNumberString();
		final String number = (String) result[0];
		final int numFormat = ((Integer) result[1]).intValue();
		int intValue = 0;
		try {
			intValue = Integer.parseInt(number, numFormat);
		} catch (final NumberFormatException e) {
			throwSyntaxError("Number format error (not an int type): " + number, number.length());
		}
		getNextToken();
		return intValue;
	}

	// private ASTNode getString() throws SyntaxError {
	// final StringBuffer ident = getStringBuffer();
	//
	// getNextToken();
	//
	// return fFactory.createString(ident);
	// }

	private ASTNode getFactor() throws SyntaxError {
		ASTNode temp;

		if (fToken == TT_CONSTANT) {
			temp = fFactory.createSymbol(fOperatorString);
			getNextToken();
			return temp;
		}
		if (fToken == TT_DIGIT) {
			return getNumber(false);
		}
		if (fToken == TT_PRECEDENCE_OPEN) {
			getNextToken();

			temp = parseOperators(parsePrimary(), 0);

			if (fToken != TT_PRECEDENCE_CLOSE) {
				throwSyntaxError("\')\' expected.");
			}

			getNextToken();

			return temp;
		}

		// if (fToken == TT_STRING) {
		// return getString();
		// }

		switch (fToken) {

		case TT_PRECEDENCE_CLOSE:
			throwSyntaxError("Too much open ) in factor.");
			break;
		}

		throwSyntaxError("Error in factor at character: '" + fCurrentChar + "' (" + fToken + ")");
		return null;
	}

}