/*******************************************************************************
 *  Copyright (c) 2000, 2015 IBM Corporation and others.
 *
 *  This program and the accompanying materials
 *  are made available under the terms of the Eclipse Public License 2.0
 *  which accompanies this distribution, and is available at
 *  https://www.eclipse.org/legal/epl-2.0/
 *
 *  SPDX-License-Identifier: EPL-2.0
 *
 *  Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.pde.internal.ui.wizards.templates;

import java.util.Hashtable;
import java.util.Stack;

import org.eclipse.pde.ui.templates.IVariableProvider;

public class PreprocessorParser {
	private static final int T_VAR = 1;
	private static final int T_LBR = 2;
	private static final int T_RBR = 3;
	private static final int T_NOT = 4;
	private static final int T_AND = 5;
	private static final int T_OR = 6;
	private static final int T_EQ = 7;
	private static final int T_NEQ = 8;
	private static final int T_STRING = 9;
	private static final int T_TRUE = 22;
	private static final int T_FALSE = 23;
	private static final int T_ERROR = 99;
	private static final int T_EOF = 10;

	//private static final int OP_LEAF = -1;
	private static final int OP_AND = 1;
	private static final int OP_OR = 2;
	private static final int OP_EQ = 3;
	private static final int OP_NEQ = 4;
	private static final int OP_NOT = 5;
	//private static final int OP_DEFER = 55;

	private IVariableProvider provider;
	private String line;
	private final Stack<RootEntry> exprStack;
	private int loc;
	private String tvalue;

	abstract static class Node {
		abstract Object getValue();
	}

	static class LeafNode extends Node {
		Object value;

		LeafNode(Object value) {
			this.value = value;
		}

		@Override
		public Object getValue() {
			return value;
		}

		@Override
		public String toString() {
			if (value != null)
				return "leaf[" + value.toString() + "]"; //$NON-NLS-1$ //$NON-NLS-2$
			return "leaf[null]"; //$NON-NLS-1$
		}
	}

	static class ExpressionNode extends Node {
		int opcode;
		Node left;
		Node right;

		public ExpressionNode(Node left, Node right, int opcode) {
			this.opcode = opcode;
			this.left = left;
			this.right = right;
		}

		@Override
		public Object getValue() {
			boolean result = false;
			Object leftValue = left != null ? left.getValue() : Boolean.FALSE;
			Object rightValue = right != null ? right.getValue() : Boolean.FALSE;

			if (opcode == OP_NOT && rightValue instanceof Boolean) {
				result = rightValue.equals(Boolean.TRUE) ? false : true;
			} else {

				if (leftValue instanceof Boolean && rightValue instanceof Boolean) {
					boolean bleft = ((Boolean) leftValue).booleanValue();
					boolean bright = ((Boolean) rightValue).booleanValue();

					result = switch (opcode) {
						case OP_AND -> bleft && bright;
						case OP_OR -> bleft || bright;
						case OP_EQ -> bleft == bright;
						case OP_NEQ -> bleft != bright;
						default -> result;
					};
				}
				if (leftValue instanceof String && rightValue instanceof String) {
					result = switch (opcode) {
						case OP_EQ -> leftValue.equals(rightValue);
						case OP_NEQ -> leftValue.equals(rightValue);
						default -> result;
					};
				}
			}
			return result ? Boolean.TRUE : Boolean.FALSE;
		}

		@Override
		public String toString() {
			String lstring = left != null ? left.toString() : "*"; //$NON-NLS-1$
			String rstring = right != null ? right.toString() : "*"; //$NON-NLS-1$
			return "(" + lstring + "<" + opcode + ">" + rstring + ")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
		}
	}

	static class RootEntry {
		Node root;
	}

	public PreprocessorParser() {
		this(null);
	}

	public PreprocessorParser(IVariableProvider provider) {
		this.provider = provider;
		exprStack = new Stack<>();
	}

	public void setVariableProvider(IVariableProvider provider) {
		this.provider = provider;
	}

	public static void main(String[] args) {
		final Hashtable<String, Comparable<? extends Object>> vars = new Hashtable<>();
		vars.put("a", Boolean.FALSE); //$NON-NLS-1$
		vars.put("b", "3"); //$NON-NLS-1$ //$NON-NLS-2$
		vars.put("c", Boolean.TRUE); //$NON-NLS-1$
		PreprocessorParser parser = new PreprocessorParser(variable -> vars.get(variable));
		try {
			boolean value = parser.parseAndEvaluate("!a || (b==\"2\" && c)"); //$NON-NLS-1$
			System.out.println("Result: " + value); //$NON-NLS-1$
		} catch (Exception e) {
			System.out.println(e);
		}
	}

	public boolean parseAndEvaluate(String line) throws Exception {
		reset();
		this.line = line;
		//System.out.println("Line: " + line);
		parse();
		//printExpression();
		return evaluate();
	}

	private boolean evaluate() {
		boolean result = false;
		if (exprStack.isEmpty() == false) {
			RootEntry entry = exprStack.peek();
			if (entry.root != null) {
				Object value = entry.root.getValue();
				if (value != null && value instanceof Boolean) {
					if (((Boolean) value).equals(Boolean.TRUE))
						result = true;
				}
			}
		}
		return result;
	}

	private void reset() {
		loc = 0;
		tvalue = null;
		exprStack.clear();
	}

	private void parse() throws Exception {
		for (;;) {
			int token = getNextToken();
			//System.out.println("Token: " + token + ", val=\"" + tvalue+"\"");
			if (token == T_EOF)
				break;

			if (token == T_VAR) {
				Node node = new LeafNode(provider.getValue(tvalue.toString()));
				pushNode(node);
				continue;
			}
			if (token == T_TRUE || token == T_FALSE) {
				Object value = token == T_TRUE ? Boolean.TRUE : Boolean.FALSE;
				Node node = new LeafNode(value);
				pushNode(node);
				continue;
			}
			if (token == T_STRING) {
				Node node = new LeafNode(tvalue);
				pushNode(node);
				continue;
			}

			if (token == T_NOT) {
				pushNode(OP_NOT);
				continue;
			}

			int opcode = switch (token) {
				case T_AND -> OP_AND;
				case T_OR -> OP_OR;
				case T_EQ -> OP_EQ;
				case T_NEQ -> OP_NEQ;
				default -> 0;
			};
			if (opcode != 0) {
				pushNode(opcode);
				continue;
			}
			if (token == T_LBR) {
				pushRoot();
				continue;
			}
			if (token == T_RBR) {
				if (exprStack.isEmpty())
					throwUnexpectedToken("not )", token); //$NON-NLS-1$
				popRoot();
				continue;
			}
		}
	}

	private RootEntry getCurrentRoot() {
		if (exprStack.isEmpty()) {
			RootEntry entry = new RootEntry();
			exprStack.push(entry);
		}
		return exprStack.peek();
	}

	private void replaceRoot(ExpressionNode newRoot) {
		RootEntry entry = getCurrentRoot();
		if (entry.root != null)
			newRoot.left = entry.root;
		entry.root = newRoot;
	}

	private void pushNode(Node node) {
		RootEntry entry = getCurrentRoot();
		if (entry.root == null)
			entry.root = node;
		else {
			ExpressionNode enode = (ExpressionNode) entry.root;
			if (enode.opcode == OP_NOT)
				enode.right = node;
			else {
				if (enode.left == null)
					enode.left = node;
				else
					enode.right = node;
			}
		}
	}

	private void pushNode(int opcode) {
		ExpressionNode node = new ExpressionNode(null, null, opcode);
		replaceRoot(node);
	}

	private void pushRoot() {
		exprStack.push(new RootEntry());
	}

	private void popRoot() {
		RootEntry entry = getCurrentRoot();
		exprStack.pop();
		pushNode(entry.root);
	}

	private void throwUnexpectedToken(String expected, int token) throws Exception {
		String message = "Expected " + expected + ", found " + token; //$NON-NLS-1$ //$NON-NLS-2$
		throw new Exception(message);
	}

	private int getNextToken() {
		boolean string = false;
		boolean variable = false;
		int vloc = loc;
		tvalue = null;
		for (;;) {
			if (loc == line.length()) {
				// check if we have panding identifier
				if (variable) {
					tvalue = line.substring(vloc, loc);
					variable = false;
					if (tvalue.equalsIgnoreCase("false")) //$NON-NLS-1$
						return T_FALSE;
					if (tvalue.equalsIgnoreCase("true")) //$NON-NLS-1$
						return T_TRUE;
					return T_VAR;
				}
				if (string) {
					// EOF in string
					string = false;
					return T_ERROR;
				}
				// regular end of line
				tvalue = "EOF"; //$NON-NLS-1$
				return T_EOF;
			}
			char c = line.charAt(loc++);

			if (c == '\"') {
				if (string) {
					tvalue = line.substring(vloc, loc - 1);
					string = false;
					return T_STRING;
				}
				vloc = loc;
				string = true;
				continue;
			} else if (string)
				continue;

			if (!variable && Character.isJavaIdentifierStart(c)) {
				variable = true;
				vloc = loc - 1;
				continue;
			}
			if (variable) {
				if (!Character.isJavaIdentifierPart(c)) {
					loc--;
					tvalue = line.substring(vloc, loc);
					variable = false;
					if (tvalue.equalsIgnoreCase("false")) //$NON-NLS-1$
						return T_FALSE;
					if (tvalue.equalsIgnoreCase("true")) //$NON-NLS-1$
						return T_TRUE;
					return T_VAR;
				}
				continue;
			}

			if (testDoubleToken(c, "!=")) //$NON-NLS-1$
				return T_NEQ;
			if (testDoubleToken(c, "==")) //$NON-NLS-1$
				return T_EQ;
			if (testDoubleToken(c, "&&")) //$NON-NLS-1$
				return T_AND;
			if (testDoubleToken(c, "||")) //$NON-NLS-1$
				return T_OR;
			if (testSingleToken(c, '!'))
				return T_NOT;
			if (testSingleToken(c, '('))
				return T_LBR;
			if (testSingleToken(c, ')'))
				return T_RBR;
			if (c == ' ' || c == '\t' || c == '\n')
				continue;
			tvalue = "" + c; //$NON-NLS-1$
			return T_ERROR;
		}
	}

	private boolean testSingleToken(char c, char expected) {
		if (c == expected) {
			tvalue = "" + expected; //$NON-NLS-1$
			return true;
		}
		return false;
	}

	private boolean testDoubleToken(char c1, String pattern) {
		if (c1 != pattern.charAt(0))
			return false;
		char c2 = line.charAt(loc);
		if (c2 == pattern.charAt(1)) {
			loc++;
			tvalue = pattern;
			return true;
		}
		return false;
	}
}
