/*
 * 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.bc;

import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;
import java.io.StringReader;

/**
 * 
 *
 *
 * @author MORIGUCHI, Yuichiro 2013/07/14
 */
public final class BcLexer {

	static enum S1 {
		INIT, NUM1, NUM2, NUM3,
		LESS, MORE, EQUL, EXCL, PLUS, MINS, ASTR, SLAS, PERC, CART,
		COM1, COM2, SYM1, STR1, STR2, AMPS, VERT
	}

	/**
	 * 
	 */
	public static final Object EOF = new Object();

	private PushbackReader rd;
	private Object lookahead;
	private int level;
	private boolean nl = false;

	/**
	 * 
	 * @param rd
	 * @throws IOException
	 */
	public BcLexer(Reader rd) throws IOException {
		this.rd = new PushbackReader(rd);
		lookahead = _nextToken();
		level = -1;
	}

	/**
	 * 
	 * @param s
	 */
	public BcLexer(String s) throws IOException {
		this(new StringReader(s));
	}

	/**
	 * 
	 * @param rd
	 * @throws IOException
	 */
	public BcLexer(Reader rd, int level) throws IOException {
		this.rd = new PushbackReader(rd);
		this.level = level;
		lookahead = _nextToken();
	}

	/**
	 * 
	 * @return
	 */
	public Object getLookahead() {
		return lookahead;
	}

	/**
	 * 
	 * @return
	 * @throws IOException
	 */
	public Object next() throws IOException {
		if(lookahead != EOF) {
			lookahead = _nextToken();
		}
		return lookahead;
	}

	/**
	 * 
	 * @return
	 * @throws IOException
	 */
	public Object restart() throws IOException {
		return lookahead = _nextToken();
	}

	/**
	 * 
	 * @param c
	 * @return
	 */
	public boolean eqchar(char c) throws IOException {
		if(checkchar(c)) {
			next();
			return true;
		} else {
			return false;
		}
	}

	/**
	 * 
	 * @param c
	 * @return
	 */
	public boolean checkchar(char c) throws IOException {
		return (getLookahead() instanceof Character &&
				((Character)getLookahead()).charValue() == c);
	}

	/**
	 * 
	 * @param c
	 * @return
	 */
	public void eatchar(char c) throws IOException {
		if(!eqchar(c)) {
			throw new BcSyntaxException();
		}
	}

	/**
	 * 
	 * @param o
	 * @return
	 */
	public boolean equalTo(Object o) throws IOException {
		if(getLookahead().equals(o)) {
			next();
			return true;
		} else {
			return false;
		}
	}

	/**
	 * 
	 * @return
	 */
	public boolean isSymbol(String s) throws IOException {
		Object o;

		if(!((o = getLookahead()) instanceof BcSymbol)) {
			return false;
		} else if(o.toString().equals(s)) {
			next();
			return true;
		} else {
			return false;
		}
	}

	/**
	 * 
	 * @return
	 */
	public BcSymbol getSymbol() throws IOException {
		Object o;

		if((o = getLookahead()) instanceof BcSymbol) {
			next();
			return (BcSymbol)o;
		} else {
			return null;
		}
	}

	/**
	 * 
	 * @return
	 */
	public String eatSymbol() throws IOException {
		Object o;

		if((o = getLookahead()) instanceof BcSymbol) {
			next();
			return o.toString();
		} else {
			throw new BcSyntaxException();
		}
	}

	/**
	 * 
	 * @return
	 */
	public BcStringLiteral getString() throws IOException {
		Object o;

		if((o = getLookahead()) instanceof BcStringLiteral) {
			next();
			return (BcStringLiteral)o;
		} else {
			return null;
		}
	}

	/**
	 * 
	 * @return
	 */
	public boolean isEof() {
		return getLookahead() == EOF;
	}

	/**
	 * 
	 * @return
	 */
	public BcNumber getNumber() throws IOException {
		Object o;

		if((o = getLookahead()) instanceof BcNumber) {
			next();
			return (BcNumber)o;
		} else {
			return null;
		}
	}

	/**
	 * 
	 * @return
	 */
	public BcRelop getRelop() throws IOException {
		Object o;

		if((o = getLookahead()) instanceof BcRelop) {
			next();
			return (BcRelop)o;
		} else {
			return null;
		}
	}

	/**
	 * 
	 * @return
	 */
	public BcAssignOp getAssignop() throws IOException {
		Object o;

		if((o = getLookahead()) instanceof BcAssignOp) {
			next();
			return (BcAssignOp)o;
		} else {
			return null;
		}
	}

	/**
	 * 
	 * @param c
	 * @return
	 */
	public boolean checkeos() throws IOException {
		Object o;

		return ((o = getLookahead()).equals(Character.valueOf(';')) ||
				o.equals(Character.valueOf('\n')));
	}

	/**
	 * 
	 * @throws IOException
	 */
	public void eateos() throws IOException {
		if(checkeos()) {
			next();
		} else {
			throw new BcSyntaxException();
		}
	}

	/**
	 * 
	 * @throws IOException
	 */
	public void skipnl() throws IOException {
		while(!isEof() &&
				getLookahead().equals(Character.valueOf('\n'))) {
			next();
		}
		nl = false;
	}

	/**
	 * 
	 */
	public void incLevel() {
		if(level >= 0)  level++;
	}

	/**
	 * 
	 */
	public void decLevel() {
		if(level > 0)  level--;
	}

	/**
	 * 
	 */
	public void setnl() {
		nl = true;
	}

	//
	Object _nextToken() throws IOException {
		StringBuffer b = null;
		S1 stat = S1.INIT;
		int c;

		for(;;) {
			c = rd.read();
			switch(stat) {
			case INIT:
				if(c < 0) {
					return EOF;
				} else if((c >= '0' && c <= '9') ||
						(c >= 'A' && c <= 'F')) {
					b = new StringBuffer();
					b.append((char)c);
					stat = S1.NUM1;
				} else if(c == '.') {
					b = new StringBuffer();
					b.append('0').append((char)c);
					stat = S1.NUM1;
				} else if(c == '<') {
					stat = S1.LESS;
				} else if(c == '>') {
					stat = S1.MORE;
				} else if(c == '=') {
					stat = S1.EQUL;
				} else if(c == '!') {
					stat = S1.EXCL;
				} else if(c == '+') {
					stat = S1.PLUS;
				} else if(c == '-') {
					stat = S1.MINS;
				} else if(c == '*') {
					stat = S1.ASTR;
				} else if(c == '/') {
					stat = S1.SLAS;
				} else if(c == '%') {
					stat = S1.PERC;
				} else if(c == '^') {
					stat = S1.CART;
				} else if(c == '&') {
					stat = S1.AMPS;
				} else if(c == '|') {
					stat = S1.VERT;
				} else if(c == '\n') {
					if(nl) {
						nl = false;
						return Character.valueOf('\n');
					} else if(level == 0) {
						return EOF;
					} else {
						return Character.valueOf('\n');
					}
				} else if(c != '\n' && Character.isWhitespace(c)) {
					// do nothing
				} else if(Character.isJavaIdentifierStart(c)) {
					b = new StringBuffer();
					b.append((char)c);
					stat = S1.SYM1;
				} else if(c == '"') {
					b = new StringBuffer();
					stat = S1.STR1;
				} else {
					if(c == '{') {
						if(level >= 0)  level++;
					} else if(c == '}') {
						if(level > 0)  level--;
					}
					nl = nl && c != '}';
					return Character.valueOf((char)c);
				}
				break;
			case NUM1:
				if((c >= '0' && c <= '9') ||
						(c >= 'A' && c <= 'F')) {
					b.append((char)c);
				} else if(c == '.') {
					b.append((char)c);
					stat = S1.NUM2;
				} else {
					if(c >= 0)  rd.unread(c);
					return new BcNumber(b.toString());
				}
				break;
			case NUM2:
				if((c >= '0' && c <= '9') ||
						(c >= 'A' && c <= 'F')) {
					b.append((char)c);
					stat = S1.NUM3;
				} else {
					throw new BcSyntaxException();
				}
				break;
			case NUM3:
				if((c >= '0' && c <= '9') ||
						(c >= 'A' && c <= 'F')) {
					b.append((char)c);
				} else {
					if(c >= 0)  rd.unread(c);
					return new BcNumber(b.toString());
				}
				break;
			case LESS:
				if(c == '=') {
					return BcRelop.LE;
				} else {
					if(c >= 0)  rd.unread(c);
					return BcRelop.LT;
				}
			case MORE:
				if(c == '=') {
					return BcRelop.GE;
				} else {
					if(c >= 0)  rd.unread(c);
					return BcRelop.GT;
				}
			case EQUL:
				if(c == '=') {
					return BcRelop.EQ;
				} else {
					if(c >= 0)  rd.unread(c);
					return BcAssignOp.SIMPLE;
				}
			case EXCL:
				if(c == '=') {
					return BcRelop.NE;
				} else {
					if(c >= 0)  rd.unread(c);
					return Character.valueOf('!');
				}
			case PLUS:
				if(c == '=') {
					return BcAssignOp.ADD;
				} else if(c == '+') {
					return BcIncDec.INC;
				} else {
					if(c >= 0)  rd.unread(c);
					return Character.valueOf('+');
				}
			case MINS:
				if(c == '=') {
					return BcAssignOp.SUB;
				} else if(c == '-') {
					return BcIncDec.DEC;
				} else {
					if(c >= 0)  rd.unread(c);
					return Character.valueOf('-');
				}
			case ASTR:
				if(c == '=') {
					return BcAssignOp.MUL;
				} else {
					if(c >= 0)  rd.unread(c);
					return Character.valueOf('*');
				}
			case SLAS:
				if(c == '=') {
					return BcAssignOp.DIV;
				} else if(c == '*') {
					stat = S1.COM1;
				} else {
					if(c >= 0)  rd.unread(c);
					return Character.valueOf('/');
				}
				break;
			case PERC:
				if(c == '=') {
					return BcAssignOp.MOD;
				} else {
					if(c >= 0)  rd.unread(c);
					return Character.valueOf('%');
				}
			case CART:
				if(c == '=') {
					return BcAssignOp.POW;
				} else {
					if(c >= 0)  rd.unread(c);
					return Character.valueOf('^');
				}
			case COM1:
				if(c < 0)  throw new BcSyntaxException();
				if(c == '*')  stat = S1.COM2;
				break;
			case COM2:
				if(c < 0)  throw new BcSyntaxException();
				stat = c == '/' ? S1.INIT :
					c == '*' ? S1.COM2 : S1.COM1;
				break;
			case SYM1:
				if(Character.isJavaIdentifierPart(c)) {
					b.append((char)c);
				} else {
					if(c >= 0)  rd.unread(c);
					return new BcSymbol(b.toString());
				}
				break;
			case STR1:
				if(c < 0)  throw new BcSyntaxException();
				if(c == '"') {
					return new BcStringLiteral(b.toString());
				} else if(c == '\\') {
					stat = S1.STR2;
				} else {
					b.append((char)c);
				}
				break;
			case STR2:
				if(c < 0)  throw new BcSyntaxException();
				switch(c) {
				case 'n':  b.append('\n');  break;
				case 'r':  b.append('\r');  break;
				case 't':  b.append('\t');  break;
				default:   b.append((char)c);  break;
				}
				stat = S1.STR1;
				break;
			case AMPS:
				if(c == '&') {
					return BcLogical.AND;
				} else {
					rd.unread(c);
					return Character.valueOf('&');
				}
			case VERT:
				if(c == '|') {
					return BcLogical.OR;
				} else {
					rd.unread(c);
					return Character.valueOf('|');
				}
			}
		}
	}

}
