/*
 * Copyright 2013 Yuichiro Moriguchi
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.morilib.sh.arith;

import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

import net.morilib.sh.ShEnvironment;

public final class ShExprParser {

	private static final BigInteger MONE = BigInteger.valueOf(-1);

	private ShExprParser() {}

	private static BigInteger findvar(ShEnvironment env,
			ShExprToken t) {
		String s;

		try {
			s = env.find(t.getString());
			if(s.equals("")) {
				return BigInteger.ZERO;
			} else {
				return new BigInteger(s);
			}
		} catch(NumberFormatException e1) {
			throw new ShExprSyntaxException();
		}
	}

	private static BigInteger bindvar(ShEnvironment env,
			ShExprToken t, BigInteger w) {
		if(t instanceof ShExprSymbol) {
			env.bind(t.getString(), w.toString());
			return w;
		} else {
			throw new ShExprSyntaxException();
		}
	}

	private static BigInteger incdecvar(ShEnvironment env,
			ShExprToken t, BigInteger w, boolean pre) {
		BigInteger v, x;
		String s;

		if(t instanceof ShExprSymbol) {
			s = t.getString();
			v = findvar(env, t);
			x = v.add(w);
			env.bind(s, x.toString());
			return pre ? x : v;
		} else {
			throw new ShExprSyntaxException();
		}
	}

	private static BigInteger incvar(ShEnvironment env,
			ShExprToken t, boolean pre) {
		return incdecvar(env, t, BigInteger.ONE, pre);
	}

	private static BigInteger decvar(ShEnvironment env,
			ShExprToken t, boolean pre) {
		return incdecvar(env, t, MONE, pre);
	}

	static BigInteger identifier(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		BigInteger e;
		ShExprToken t = rd.getToken();

		if(t instanceof ShExprIntegerToken) {
			rd.nextToken();
			return t.getInteger();
		} else if(t instanceof ShExprSymbol) {
			rd.nextToken();
			return findvar(env, t);
		} else if(t.equals(ShExprOp.LPAREN)) {
			rd.nextToken();
			e = parseExpression(env, rd);
			rd.eatToken(ShExprOp.RPAREN);
			return e;
		} else {
			throw new ShExprSyntaxException();
		}
	}

	static BigInteger incdec1(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		ShExprToken t = rd.getToken();

		if(t.equals(ShExprOp.INC)) {
			return incvar(env, rd.nextToken(), true);
		} else if(t.equals(ShExprOp.DEC)) {
			return decvar(env, rd.nextToken(), true);
		} else {
			return identifier(env, rd);
		}
	}

	static BigInteger incdec2(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		ShExprToken t = rd.getToken(), x;

		if(!(t instanceof ShExprSymbol)) {
			return incdec1(env, rd);
		} else if((x = rd.nextToken()).equals(ShExprOp.INC)) {
			return incvar(env, t, false);
		} else if(x.equals(ShExprOp.DEC)) {
			return decvar(env, t, false);
		} else {
			rd.backToken(t);
			return incdec1(env, rd);
		}
	}

	static BigInteger unaries1(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		ShExprToken t = rd.getToken();

		if(t.equals(ShExprOp.ADD)) {
			t = rd.nextToken();
			return incdec2(env, rd);
		} else if(t.equals(ShExprOp.SUB)) {
			t = rd.nextToken();
			return incdec2(env, rd).negate();
		} else {
			return incdec2(env, rd);
		}
	}

	static BigInteger unaries2(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		ShExprToken t = rd.getToken();

		if(t.equals(ShExprOp.L_NOT)) {
			t = rd.nextToken();
			return BigInteger.valueOf(
					unaries1(env, rd).signum() != 0 ? 0 : 1);
		} else if(t.equals(ShExprOp.B_NOT)) {
			t = rd.nextToken();
			return unaries1(env, rd).not();
		} else {
			return unaries1(env, rd);
		}
	}

	static BigInteger pow(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		BigInteger v;

		v = unaries2(env, rd);
		if(rd.getToken().equals(ShExprOp.POW)) {
			rd.nextToken();
			return v.pow(pow(env, rd).intValue());
		} else {
			return v;
		}
	}

	static BigInteger factor(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		List<BigInteger> l = new ArrayList<BigInteger>();
		List<ShExprToken> m = new ArrayList<ShExprToken>();
		BigInteger e;
		ShExprToken t;

		l.add(pow(env, rd));
		m.add(null);
		while((t = rd.getToken()).equals(ShExprOp.MUL) ||
				t.equals(ShExprOp.DIV) ||
				t.equals(ShExprOp.MOD)) {
			m.add(t);  rd.nextToken();
			l.add(pow(env, rd));
		}
		e = l.get(0);
		for(int i = 1; i < l.size(); i++) {
			if(m.get(i).equals(ShExprOp.MUL)) {
				e = e.multiply(l.get(i));
			} else if(m.get(i).equals(ShExprOp.DIV)) {
				e = e.divide(l.get(i));
			} else if(m.get(i).equals(ShExprOp.MOD)) {
				e = e.remainder(l.get(i));
			}
		}
		return e;
	}

	static BigInteger term(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		List<BigInteger> l = new ArrayList<BigInteger>();
		List<ShExprToken> m = new ArrayList<ShExprToken>();
		BigInteger e;
		ShExprToken t;

		l.add(factor(env, rd));
		m.add(null);
		while((t = rd.getToken()).equals(ShExprOp.ADD) ||
				t.equals(ShExprOp.SUB)) {
			m.add(t);  rd.nextToken();
			l.add(factor(env, rd));
		}
		e = l.get(0);
		for(int i = 1; i < l.size(); i++) {
			if(m.get(i).equals(ShExprOp.ADD)) {
				e = e.add(l.get(i));
			} else if(m.get(i).equals(ShExprOp.SUB)) {
				e = e.subtract(l.get(i));
			}
		}
		return e;
	}

	static BigInteger sft(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		List<BigInteger> l = new ArrayList<BigInteger>();
		List<ShExprToken> m = new ArrayList<ShExprToken>();
		BigInteger e;
		ShExprToken t;

		l.add(term(env, rd));
		m.add(null);
		while((t = rd.getToken()).equals(ShExprOp.SHIFTL) ||
				t.equals(ShExprOp.SHIFTR)) {
			m.add(t);  rd.nextToken();
			l.add(term(env, rd));
		}
		e = l.get(0);
		for(int i = 1; i < l.size(); i++) {
			if(m.get(i).equals(ShExprOp.SHIFTL)) {
				e = e.shiftLeft(l.get(i).intValue());
			} else if(m.get(i).equals(ShExprOp.SHIFTR)) {
				e = e.shiftRight(l.get(i).intValue());
			}
		}
		return e;
	}

	static BigInteger relop1(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		List<BigInteger> l = new ArrayList<BigInteger>();
		List<ShExprOpType> m = new ArrayList<ShExprOpType>();
		BigInteger e;
		ShExprToken t;
		boolean x;

		l.add(sft(env, rd));
		m.add(null);
		while((t = rd.getToken()) instanceof ShExprRelop1) {
			m.add(t.getRelation());  rd.nextToken();
			l.add(sft(env, rd));
		}
		e = l.get(0);
		for(int i = 1; i < l.size(); i++) {
			switch(m.get(i)) {
			case LT:  x = e.compareTo(l.get(i)) <  0;  break;
			case LE:  x = e.compareTo(l.get(i)) <= 0;  break;
			case GT:  x = e.compareTo(l.get(i)) >  0;  break;
			case GE:  x = e.compareTo(l.get(i)) >= 0;  break;
			default:  throw new RuntimeException();
			}
			e = BigInteger.valueOf(x ? 1 : 0);
		}
		return e;
	}

	static BigInteger relop2(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		List<BigInteger> l = new ArrayList<BigInteger>();
		List<ShExprOpType> m = new ArrayList<ShExprOpType>();
		BigInteger e;
		ShExprToken t;
		boolean x;

		l.add(relop1(env, rd));
		m.add(null);
		while((t = rd.getToken()) instanceof ShExprRelop2) {
			m.add(t.getRelation());  rd.nextToken();
			l.add(relop1(env, rd));
		}
		e = l.get(0);
		for(int i = 1; i < l.size(); i++) {
			switch(m.get(i)) {
			case EQ:  x = e.compareTo(l.get(i)) == 0;  break;
			case NE:  x = e.compareTo(l.get(i)) != 0;  break;
			default:  throw new RuntimeException();
			}
			e = BigInteger.valueOf(x ? 1 : 0);
		}
		return e;
	}

	static BigInteger band(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		List<BigInteger> l = new ArrayList<BigInteger>();
		BigInteger e;

		l.add(relop2(env, rd));
		while(rd.getToken().equals(ShExprOp.B_AND)) {
			rd.nextToken();  l.add(relop2(env, rd));
		}
		e = l.get(0);
		for(int i = 1; i < l.size(); i++) {
			e = e.and(l.get(i));
		}
		return e;
	}

	static BigInteger bxor(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		List<BigInteger> l = new ArrayList<BigInteger>();
		BigInteger e;

		l.add(band(env, rd));
		while(rd.getToken().equals(ShExprOp.B_XOR)) {
			rd.nextToken();  l.add(band(env, rd));
		}
		e = l.get(0);
		for(int i = 1; i < l.size(); i++) {
			e = e.xor(l.get(i));
		}
		return e;
	}

	static BigInteger bor(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		List<BigInteger> l = new ArrayList<BigInteger>();
		BigInteger e;

		l.add(bxor(env, rd));
		while(rd.getToken().equals(ShExprOp.B_OR)) {
			rd.nextToken();  l.add(bxor(env, rd));
		}
		e = l.get(0);
		for(int i = 1; i < l.size(); i++) {
			e = e.or(l.get(i));
		}
		return e;
	}

	static BigInteger logand(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		List<BigInteger> l = new ArrayList<BigInteger>();
		BigInteger e = BigInteger.ONE;

		l.add(bor(env, rd));
		while(rd.getToken().equals(ShExprOp.L_AND)) {
			rd.nextToken();  l.add(bor(env, rd));
		}
		for(int i = 0; i < l.size(); i++) {
			if((e = l.get(i)).signum() == 0)  return e;
		}
		return e;
	}

	static BigInteger logor(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		List<BigInteger> l = new ArrayList<BigInteger>();

		l.add(logand(env, rd));
		while(rd.getToken().equals(ShExprOp.L_OR)) {
			rd.nextToken();  l.add(logand(env, rd));
		}
		for(int i = 0; i < l.size(); i++) {
			if(l.get(i).signum() != 0)  return l.get(i);
		}
		return BigInteger.ZERO;
	}

	static BigInteger cond(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		BigInteger e, f, g;

		e = logor(env, rd);
		if(rd.getToken().equals(ShExprOp.TRI1)) {
			rd.nextToken();
			f = parseExpression(env, rd);
			rd.eatToken(ShExprOp.TRI2);
			g = parseExpression(env, rd);
			return e.signum() != 0 ? f : g;
		} else {
			return e;
		}
	}

	static BigInteger assign(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		ShExprToken t, s;
		BigInteger v;

		if(!((t = rd.getToken()) instanceof ShExprSymbol)) {
			return cond(env, rd);
		} else if((s = rd.nextToken()).equals(ShExprOp.ASSIGN)) {
			rd.nextToken();
			v = assign(env, rd);
			env.bind(t.getString(), v.toString());
			return v;
		} else if(s.equals(ShExprOp.A_ADD)) {
			rd.nextToken();
			return bindvar(env, t,
					findvar(env, t).add(assign(env, rd)));
		} else if(s.equals(ShExprOp.A_SUB)) {
			rd.nextToken();
			return bindvar(env, t,
					findvar(env, t).subtract(assign(env, rd)));
		} else if(s.equals(ShExprOp.A_MUL)) {
			rd.nextToken();
			return bindvar(env, t,
					findvar(env, t).multiply(assign(env, rd)));
		} else if(s.equals(ShExprOp.A_DIV)) {
			rd.nextToken();
			return bindvar(env, t,
					findvar(env, t).divide(assign(env, rd)));
		} else if(s.equals(ShExprOp.A_MOD)) {
			rd.nextToken();
			return bindvar(env, t,
					findvar(env, t).remainder(assign(env, rd)));
		} else if(s.equals(ShExprOp.A_SFL)) {
			rd.nextToken();
			return bindvar(env, t, findvar(env, t).shiftLeft(
					assign(env, rd).intValue()));
		} else if(s.equals(ShExprOp.A_SFR)) {
			rd.nextToken();
			return bindvar(env, t, findvar(env, t).shiftRight(
					assign(env, rd).intValue()));
		} else if(s.equals(ShExprOp.A_BAND)) {
			rd.nextToken();
			return bindvar(env, t,
					findvar(env, t).and(assign(env, rd)));
		} else if(s.equals(ShExprOp.A_BXOR)) {
			rd.nextToken();
			return bindvar(env, t,
					findvar(env, t).xor(assign(env, rd)));
		} else if(s.equals(ShExprOp.A_BOR)) {
			rd.nextToken();
			return bindvar(env, t,
					findvar(env, t).or(assign(env, rd)));
		} else {
			rd.backToken(t);
			return cond(env, rd);
		}
	}

	static BigInteger commaop(ShEnvironment env,
			ShExprLexer rd) throws IOException {
		BigInteger e;

		e = assign(env, rd);
		if(rd.getToken().equals(ShExprOp.COMMA)) {
			rd.nextToken();
			return commaop(env, rd);
		} else {
			return e;
		}
	}

	/**
	 * 
	 * @param rd
	 * @return
	 * @throws IOException
	 */
	public static BigInteger parseExpression(
			ShEnvironment env, ShExprLexer rd) throws IOException {
		return commaop(env, rd);
	}

}
