/*
 * 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.c.pre.parser;

import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import net.morilib.c.pre.CpreSyntaxException;

public final class CpreLexer {

	private static class EndException extends RuntimeException {}

	private static enum St1 {
		INIT, LT, GT, EQ, EX, AND, OR,
		PLUS, MINUS, ASTERISK, SLASH, CARET, PERCENT,
		ZERO, NUMBER, NUMBER_OCT, NUMBER_HEX,
		FLOAT1, FLOAT2, FLOAT_E1, FLOAT_E2, FLOAT_E3,
		SYMBOL, CH1, CH2, CH3
	}

	private static final Map<Integer, CpreToken> OP1;

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

	static {
		Map<Integer, CpreToken> o = new HashMap<Integer, CpreToken>();

		o.put((int)'?', CpreOperator.TRI1);
		o.put((int)':', CpreOperator.TRI2);
		o.put((int)'~', CpreOperator.B_NOT);
		o.put((int)'(', CpreOperator.LPAREN);
		o.put((int)')', CpreOperator.RPAREN);
		o.put((int)',', CpreOperator.COMMA);
		OP1 = Collections.unmodifiableMap(o);
	}

	//
	private CpreToken token;
	private PushbackReader reader;
	private int headchr = 0, lineno;

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

	/**
	 * 
	 * @return
	 */
	public CpreToken getToken() {
		return token;
	}

	/**
	 * 
	 * @return
	 * @throws IOException
	 */
	public CpreToken nextToken() throws IOException {
		if(!token.equals(CpreToken.ENDMARKER)) {
			headchr = reader.read();
			if(headchr >= 0)  reader.unread(headchr);
			token = getToken(lineno, reader);
		}
		return token;
	}

	/**
	 * 
	 * @param t
	 * @return
	 * @throws IOException
	 */
	public CpreToken eatToken(CpreToken t) throws IOException {
		if(!token.equals(t)) {
			throw new CpreSyntaxException(lineno, t, token);
		}
		return nextToken();
	}

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

	/**
	 * 
	 * @return
	 */
	public int getLineNumber() {
		return lineno;
	}

	// -------------------------------------------
	private static int rde(int ln,
			PushbackReader rd) throws IOException {
		int c;

		if((c = rd.read()) < 0) {
			throw new CpreSyntaxException(ln, "unexpected EOF");
		} else {
			return c;
		}
	}

	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;
	}

	private static int getoct1(int ln,
			PushbackReader rd) throws IOException {
		int c;

		if((c = rde(ln, rd)) < '0' || c > '7') {
			throw new CpreSyntaxException(ln, "octal number required");
		}
		return c - '0';
	}

	private static int getoct(int ln,
			PushbackReader rd) throws IOException {
		return (getoct1(ln, rd) * 64 +
				getoct1(ln, rd) * 8 +
				getoct1(ln, rd));
	}

	private static int gethex1(int ln,
			PushbackReader rd) throws IOException {
		int c;

		if(((c = rde(ln, rd)) >= '0' && c <= '9')) {
			return c - '0';
		} else if(c >= 'a' && c <= 'f') {
			return c - 'a' + 10;
		} else if(c >= 'A' && c <= 'F') {
			return c - 'A' + 10;
		} else {
			throw new CpreSyntaxException(
					ln, "hexadecimal number required");
		}
	}

	private static int gethex(int ln,
			PushbackReader rd) throws IOException {
		return (gethex1(ln, rd) * 0x1000 +
				gethex1(ln, rd) * 0x100  +
				gethex1(ln, rd) * 0x10   +
				gethex1(ln, rd));
	}

	private static int gethexc(int ln,
			PushbackReader rd) throws IOException {
		return (gethex1(ln, rd) * 0x10   + gethex1(ln, rd));
	}

	static CpreToken _getToken(int ln,
			PushbackReader rd) throws IOException {
		StringBuffer b1 = new StringBuffer();
		CpreToken c0 = null;
		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(c == '\'') {
					stat = St1.CH1;
				} 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 CpreRelop1.LT;
				} else if(c == '=') {
					return CpreRelop1.LE;
				} else if(c == '<') {
					return CpreOperator.SHIFTL;
				} else {
					rd.unread(c);
					return CpreRelop1.LT;
				}
			case GT:
				if((c = rd.read()) < 0) {
					return CpreRelop1.GT;
				} else if(c == '=') {
					return CpreRelop1.GE;
				} else if(c == '>') {
					return CpreOperator.SHIFTR;
				} else {
					rd.unread(c);
					return CpreRelop1.GT;
				}
			case EQ:
				if((c = rd.read()) < 0) {
					throw new CpreSyntaxException(
							ln, "unexpected EOF");
				} else if(c == '=') {
					return CpreRelop2.EQ;
				} else {
					throw new CpreSyntaxException(
							ln, "assignment operator is not valid");
				}
			case EX:
				if((c = rd.read()) < 0) {
					return CpreOperator.L_NOT;
				} else if(c == '=') {
					return CpreRelop2.NE;
				} else {
					rd.unread(c);
					return CpreOperator.L_NOT;
				}
			case AND:
				if((c = rd.read()) < 0) {
					return CpreOperator.B_AND;
				} else if(c == '&') {
					return CpreOperator.L_AND;
				} else {
					rd.unread(c);
					return CpreOperator.B_AND;
				}
			case OR:
				if((c = rd.read()) < 0) {
					return CpreOperator.B_OR;
				} else if(c == '|') {
					return CpreOperator.L_OR;
				} else {
					rd.unread(c);
					return CpreOperator.B_OR;
				}
			case PLUS:
				if((c = rd.read()) < 0) {
					return CpreOperator.ADD;
				} else if(c == '+') {
					return CpreOperator.INC;
				} else {
					rd.unread(c);
					return CpreOperator.ADD;
				}
			case MINUS:
				if((c = rd.read()) < 0) {
					return CpreOperator.SUB;
				} else if(c == '-') {
					return CpreOperator.DEC;
				} else {
					rd.unread(c);
					return CpreOperator.SUB;
				}
			case ASTERISK:
				if((c = rd.read()) < 0) {
					return CpreOperator.MUL;
				} else {
					rd.unread(c);
					return CpreOperator.MUL;
				}
			case SLASH:
				if((c = rd.read()) < 0) {
					return CpreOperator.DIV;
				} else {
					rd.unread(c);
					return CpreOperator.DIV;
				}
			case PERCENT:
				if((c = rd.read()) < 0) {
					return CpreOperator.MOD;
				} else {
					rd.unread(c);
					return CpreOperator.MOD;
				}
			case CARET:
				if((c = rd.read()) < 0) {
					return CpreOperator.B_XOR;
				} else {
					rd.unread(c);
					return CpreOperator.B_XOR;
				}
			case ZERO:
				if((c = rd.read()) < 0) {
					return new CpreIntegerToken("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 CpreIntegerToken("0", 10);
				}
				break;
			case NUMBER:
				if((c = rd.read()) < 0) {
					return new CpreIntegerToken(b1.toString(), 10);
				} else if(c >= '0' && c <= '9') {
					b1.append((char)c);
				} else if(c == '.') {
					b1.append((char)c);
					stat = St1.FLOAT1;
				} else if(c == 'e' || c == 'E') {
					b1.append((char)c);
					stat = St1.FLOAT_E1;
				} else {
					rd.unread(c);
					return new CpreIntegerToken(b1.toString(), 10);
				}
				break;
			case NUMBER_OCT:
				if((c = rd.read()) < 0) {
					return new CpreIntegerToken(b1.toString(), 8);
				} else if(c >= '0' && c <= '9') {
					b1.append((char)c);
				} else {
					rd.unread(c);
					return new CpreIntegerToken(b1.toString(), 8);
				}
				break;
			case NUMBER_HEX:
				if((c = rd.read()) < 0) {
					return new CpreIntegerToken(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 CpreIntegerToken(b1.toString(), 16);
				}
				break;
			case FLOAT1:
				if((c = rde(ln, rd)) >= '0' && c <= '9') {
					b1.append((char)c);
					stat = St1.FLOAT2;
				} else {
					throw new CpreSyntaxException(
							ln, "number required");
				}
				break;
			case FLOAT2:
				if((c = rd.read()) < 0) {
					return new CpreFloatToken(b1.toString());
				} else if(c >= '0' && c <= '9') {
					b1.append((char)c);
				} else if(c == 'e' || c == 'E') {
					b1.append((char)c);
					stat = St1.FLOAT_E1;
				} else {
					rd.unread(c);
					return new CpreFloatToken(b1.toString());
				}
				break;
			case FLOAT_E1:
				if((c = rde(ln, rd)) >= '0' && c <= '9') {
					b1.append((char)c);
					stat = St1.FLOAT_E3;
				} else if(c == '+' || c == '-') {
					b1.append((char)c);
					stat = St1.FLOAT_E2;
				} else {
					throw new CpreSyntaxException(
							ln, "number required");
				}
				break;
			case FLOAT_E2:
				if((c = rde(ln, rd)) >= '0' && c <= '9') {
					b1.append((char)c);
					stat = St1.FLOAT_E3;
				} else {
					throw new CpreSyntaxException(
							ln, "number required");
				}
				break;
			case FLOAT_E3:
				if((c = rd.read()) < 0) {
					return new CpreFloatToken(b1.toString());
				} else if(c >= '0' && c <= '9') {
					b1.append((char)c);
				} else {
					rd.unread(c);
					return new CpreFloatToken(b1.toString());
				}
				break;
			case SYMBOL:
				if((c = rd.read()) >= 'a' && c <= 'z' ||
						c >= 'A' && c <= 'z' ||
						c >= '0' && c <= '9' ||
						c == '_') {
					b1.appendCodePoint(c);
				} else if(b1.toString().equals("defined")) {
					rd.unread(c);
					return CpreOperator.DEFINED;
				} else {
					rd.unread(c);
					return CpreSymbol.getInstance(b1.toString());
				}
				break;
			case CH1:
				if((c = rd.read()) == '\\') {
					stat = St1.CH2;
				} else {
					stat = St1.CH3;
					c0 = new CpreIntegerToken(c);
				}
				break;
			case CH2:
				switch((c = rd.read())) {
				case 'n':  c0 = new CpreIntegerToken('\n');  break;
				case 'b':  c0 = new CpreIntegerToken('\b');  break;
				case 'r':  c0 = new CpreIntegerToken('\r');  break;
				case '0':  case '1':  case '2':  case '3':
					rd.unread((char)c);
					c0 = new CpreIntegerToken(getoct(ln, rd));  break;
				case 'x':
					c0 = new CpreIntegerToken(gethexc(ln, rd));  break;
				case 'u':
					c0 = new CpreIntegerToken(gethex(ln, rd));  break;
				default:   c0 = new CpreIntegerToken(c);  break;
				}
				stat = St1.CH3;
				break;
			case CH3:
				if((c = rd.read()) != '\'') {
					throw new CpreSyntaxException(ln, ERR001);
				}
				return c0;
			}
		}
	}

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

}
