/*
 * 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.io.PushbackReader;
import java.io.Reader;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public final class ShExprLexer {

	private static class EndException extends RuntimeException {}

	private static enum St1 {
		INIT, LT, GT, EQ, EX, AND, OR,
		PLUS, MINUS, ASTERISK, SLASH, CARET, PERCENT,
		SHIFTL, SHIFTR, ZERO, NUMBER, NUMBER_OCT, NUMBER_HEX,
		SYMBOL,
	}

	private static final Map<Integer, ShExprToken> OP1;

	static final String ERR001 =
			"more than two characters is not allowed";

	static {
		Map<Integer, ShExprToken> o;

		o = new HashMap<Integer, ShExprToken>();
		o.put((int)'?', ShExprOp.TRI1);
		o.put((int)':', ShExprOp.TRI2);
		o.put((int)'~', ShExprOp.B_NOT);
		o.put((int)'(', ShExprOp.LPAREN);
		o.put((int)')', ShExprOp.RPAREN);
		o.put((int)',', ShExprOp.COMMA);
		OP1 = Collections.unmodifiableMap(o);
	}

	//
	private ShExprToken token, backed = null;
	private PushbackReader reader;
	private int headchr = 0;

	/**
	 * 
	 * @param rd
	 */
	public ShExprLexer(Reader rd) throws IOException {
		reader  = new PushbackReader(rd);
		headchr = reader.read();
		if(headchr >= 0)  reader.unread(headchr);
		token   = getToken(reader);
	}

	/**
	 * 
	 * @return
	 */
	public ShExprToken getToken() {
		return backed != null ? backed : token;
	}

	/**
	 * 
	 * @return
	 * @throws IOException
	 */
	public ShExprToken nextToken() throws IOException {
		if(backed != null) {
			backed = null;
		} else if(!token.equals(ShExprToken.ENDMARKER)) {
			headchr = reader.read();
			if(headchr >= 0)  reader.unread(headchr);
			token = getToken(reader);
		}
		return token;
	}

	public void backToken(ShExprToken t) {
		backed = t;
	}

	/**
	 * 
	 * @param t
	 * @return
	 * @throws IOException
	 */
	public ShExprToken eatToken(ShExprToken t) throws IOException {
		if(!token.equals(t)) {
			throw new ShExprSyntaxException();
		}
		return nextToken();
	}

	/**
	 * 
	 * @param t
	 * @return
	 * @throws IOException
	 */
	public ShExprToken eatTokenOpt(ShExprToken t) throws IOException {
		if(token.equals(t))  return nextToken();
		return token;
	}

	// -------------------------------------------
	private static int skipws(
			PushbackReader rd) throws IOException {
		boolean cm = false;
		int c;

		while((c = rd.read()) >= 0) {
			if(cm) {
				cm = c != '\n';
			} else if(c == '#') {
				cm = true;
			} else if(!Character.isWhitespace(c)) {
				return c;
			}
		}
		if(c < 0)  throw new EndException();
		return c;
	}

	static ShExprToken _getToken(
			PushbackReader rd) throws IOException {
		StringBuffer b1 = new StringBuffer();
		St1 stat = St1.INIT;
		int c;

		while(true) {
			switch(stat) {
			case INIT:
				if((c = skipws(rd)) == '<') {
					stat = St1.LT;
				} else if(c == '>') {
					stat = St1.GT;
				} else if(c == '=') {
					stat = St1.EQ;
				} else if(c == '!') {
					stat = St1.EX;
				} else if(c == '&') {
					stat = St1.AND;
				} else if(c == '|') {
					stat = St1.OR;
				} else if(c == '+') {
					stat = St1.PLUS;
				} else if(c == '-') {
					stat = St1.MINUS;
				} else if(c == '*') {
					stat = St1.ASTERISK;
				} else if(c == '/') {
					stat = St1.SLASH;
				} else if(c == '%') {
					stat = St1.PERCENT;
				} else if(c == '^') {
					stat = St1.CARET;
				} else if(OP1.containsKey(c)) {
					return OP1.get(c);
				} else if(c == '0') {
					stat = St1.ZERO;
				} else if(c >= '1' && c <= '9') {
					b1 = new StringBuffer().append((char)c);
					stat = St1.NUMBER;
				} else if(c >= 'a' && c <= 'z' ||
						c >= 'A' && c <= 'z' ||
						c == '_') {
					b1 = new StringBuffer().appendCodePoint(c);
					stat = St1.SYMBOL;
				}
				break;
			case LT:
				if((c = rd.read()) < 0) {
					return ShExprRelop1.LT;
				} else if(c == '=') {
					return ShExprRelop1.LE;
				} else if(c == '<') {
					stat = St1.SHIFTL;
				} else {
					rd.unread(c);
					return ShExprRelop1.LT;
				}
				break;
			case SHIFTL:
				if((c = rd.read()) < 0) {
					return ShExprRelop1.SHIFTL;
				} else if(c == '=') {
					return ShExprRelop1.A_SFL;
				} else {
					rd.unread(c);
					return ShExprRelop1.SHIFTL;
				}
			case GT:
				if((c = rd.read()) < 0) {
					return ShExprRelop1.GT;
				} else if(c == '=') {
					return ShExprRelop1.GE;
				} else if(c == '>') {
					stat = St1.SHIFTR;
				} else {
					rd.unread(c);
					return ShExprRelop1.GT;
				}
				break;
			case SHIFTR:
				if((c = rd.read()) < 0) {
					return ShExprRelop1.SHIFTR;
				} else if(c == '=') {
					return ShExprRelop1.A_SFR;
				} else {
					rd.unread(c);
					return ShExprRelop1.SHIFTR;
				}
			case EQ:
				if((c = rd.read()) < 0) {
					return ShExprOp.ASSIGN;
				} else if(c == '=') {
					return ShExprRelop2.EQ;
				} else {
					return ShExprOp.ASSIGN;
				}
			case EX:
				if((c = rd.read()) < 0) {
					return ShExprOp.L_NOT;
				} else if(c == '=') {
					return ShExprRelop2.NE;
				} else {
					rd.unread(c);
					return ShExprOp.L_NOT;
				}
			case AND:
				if((c = rd.read()) < 0) {
					return ShExprOp.B_AND;
				} else if(c == '&') {
					return ShExprOp.L_AND;
				} else if(c == '=') {
					return ShExprOp.A_BAND;
				} else {
					rd.unread(c);
					return ShExprOp.B_AND;
				}
			case OR:
				if((c = rd.read()) < 0) {
					return ShExprOp.B_OR;
				} else if(c == '|') {
					return ShExprOp.L_OR;
				} else if(c == '=') {
					return ShExprOp.A_BOR;
				} else {
					rd.unread(c);
					return ShExprOp.B_OR;
				}
			case PLUS:
				if((c = rd.read()) < 0) {
					return ShExprOp.ADD;
				} else if(c == '+') {
					return ShExprOp.INC;
				} else if(c == '=') {
					return ShExprOp.A_ADD;
				} else {
					rd.unread(c);
					return ShExprOp.ADD;
				}
			case MINUS:
				if((c = rd.read()) < 0) {
					return ShExprOp.SUB;
				} else if(c == '-') {
					return ShExprOp.DEC;
				} else if(c == '=') {
					return ShExprOp.A_SUB;
				} else {
					rd.unread(c);
					return ShExprOp.SUB;
				}
			case ASTERISK:
				if((c = rd.read()) < 0) {
					return ShExprOp.MUL;
				} else if(c == '*') {
					return ShExprOp.POW;
				} else if(c == '=') {
					return ShExprOp.A_MUL;
				} else {
					rd.unread(c);
					return ShExprOp.MUL;
				}
			case SLASH:
				if((c = rd.read()) < 0) {
					return ShExprOp.DIV;
				} else if(c == '=') {
					return ShExprOp.A_DIV;
				} else {
					rd.unread(c);
					return ShExprOp.DIV;
				}
			case PERCENT:
				if((c = rd.read()) < 0) {
					return ShExprOp.MOD;
				} else if(c == '=') {
					return ShExprOp.A_MOD;
				} else {
					rd.unread(c);
					return ShExprOp.MOD;
				}
			case CARET:
				if((c = rd.read()) < 0) {
					return ShExprOp.B_XOR;
				} else if(c == '=') {
					return ShExprOp.A_BXOR;
				} else {
					rd.unread(c);
					return ShExprOp.B_XOR;
				}
			case ZERO:
				if((c = rd.read()) < 0) {
					return new ShExprIntegerToken("0", 10);
				} else if(c == 'x') {
					stat = St1.NUMBER_HEX;
				} else if(c >= '0' && c <= '9') {
					b1.append((char)c);
					stat = St1.NUMBER_OCT;
				} else {
					rd.unread(c);
					return new ShExprIntegerToken("0", 10);
				}
				break;
			case NUMBER:
				if((c = rd.read()) < 0) {
					return new ShExprIntegerToken(b1.toString(), 10);
				} else if(c >= '0' && c <= '9') {
					b1.append((char)c);
				} else {
					rd.unread(c);
					return new ShExprIntegerToken(b1.toString(), 10);
				}
				break;
			case NUMBER_OCT:
				if((c = rd.read()) < 0) {
					return new ShExprIntegerToken(b1.toString(), 8);
				} else if(c >= '0' && c <= '9') {
					b1.append((char)c);
				} else {
					rd.unread(c);
					return new ShExprIntegerToken(b1.toString(), 8);
				}
				break;
			case NUMBER_HEX:
				if((c = rd.read()) < 0) {
					return new ShExprIntegerToken(b1.toString(), 16);
				} else if((c >= '0' && c <= '9') ||
						(c >= 'a' && c <= 'f') ||
						(c >= 'A' && c <= 'F')) {
					b1.append((char)c);
				} else {
					rd.unread(c);
					return new ShExprIntegerToken(b1.toString(), 16);
				}
				break;
			case SYMBOL:
				if((c = rd.read()) >= 'a' && c <= 'z' ||
						c >= 'A' && c <= 'z' ||
						c >= '0' && c <= '9' ||
						c == '_') {
					b1.appendCodePoint(c);
				} else {
					rd.unread(c);
					return ShExprSymbol.getInstance(b1.toString());
				}
				break;
			}
		}
	}

	/**
	 * 
	 * @param rd
	 * @return
	 * @throws IOException
	 */
	public static ShExprToken getToken(
			PushbackReader rd) throws IOException {
		try {
			return _getToken(rd);
		} catch(EndException e) {
			return ShExprToken.ENDMARKER;
		}
	}

}
