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

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

import net.morilib.awk.AwkLexerException;
import net.morilib.awk.AwkSyntaxException;
import net.morilib.awk.misc.PromptReader;
import net.morilib.awk.misc.PushbackLineNumberReadable;
import net.morilib.awk.misc.PushbackLineNumberReader;

public final class AwkLexer {

	private static class EndException extends RuntimeException {}

	private static enum St1 {
		INIT, LT, GT, EQ, EX, AND, OR, KEYWORD,
		STRING, STRING_ESC,
		PLUS, MINUS, ASTERISK, SLASH, CARET, PERCENT, DOT, BACKSLASH,
		ZERO, NUMBER, NUMBER_OCT, NUMBER_HEX,
		FLOAT1, FLOAT2, FLOAT_E1, FLOAT_E2, FLOAT_E3,
	}

	private static final Map<Integer, AwkToken> OP1;
	private static final Map<String, AwkToken> RES1;

	static {
		Map<Integer, AwkToken> o = new HashMap<Integer, AwkToken>();
		Map<String, AwkToken>  r = new HashMap<String, AwkToken>();

		o.put((int)'?', AwkOperator.TRI1);
		o.put((int)':', AwkOperator.TRI2);
		o.put((int)'~', AwkOperator.MATCH);
		o.put((int)'(', AwkOperator.LPAREN);
		o.put((int)')', AwkOperator.RPAREN);
		o.put((int)'{', AwkReserved.BLOCK_B);
		o.put((int)'}', AwkReserved.BLOCK_E);
		o.put((int)',', AwkOperator.COMMA);
		o.put((int)'$', AwkOperator.FIELD);
		o.put((int)';', AwkReserved.SEMICL);
		o.put((int)'\n', AwkReserved.EOL);
		o.put((int)'[', AwkOperator.LBRAKET);
		o.put((int)']', AwkOperator.RBRAKET);
		o.put((int)'@', AwkOperator.REFFN);
		OP1 = Collections.unmodifiableMap(o);

		r.put("if", AwkReserved.IF);
		r.put("else", AwkReserved.ELSE);
		r.put("for", AwkReserved.FOR);
		r.put("in", AwkOperator.IN);
		r.put("while", AwkReserved.WHILE);
		r.put("do", AwkReserved.DO);
		r.put("break", AwkReserved.BREAK);
		r.put("continue", AwkReserved.CONT);
		r.put("next", AwkReserved.NEXT);
		r.put("exit", AwkReserved.EXIT);
		r.put("delete", AwkReserved.DELETE);
		r.put("function", AwkReserved.FUNC);

		r.put("print", AwkReserved.PRINT);
		r.put("printf", AwkReserved.PRINTF);
		r.put("getline", AwkReserved.GETLINE);
		r.put("close", AwkReserved.CLOSE);
		RES1 = Collections.unmodifiableMap(r);
	}

	//
	private AwkToken token;
	private PushbackLineNumberReadable reader;
	private int headchr = 0;
	private boolean reset = false;
	private int blocks = 0;

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

	/**
	 * 
	 * @param rd
	 */
	public AwkLexer(String prompt1, String prompt2,
			Reader rd) throws IOException {
		reader  = new PromptReader(prompt1, prompt2, rd);
		headchr = reader.read();
		if(headchr >= 0)  reader.unread(headchr);
		token   = getToken(reader);
	}

	/**
	 * 
	 * @return
	 */
	public AwkToken getToken() throws IOException {
		if(reset) {
			reset = false;
			headchr = reader.read();
			if(headchr >= 0)  reader.unread(headchr);
			token = getToken(reader);
		}
		return token;
	}

	/**
	 * 
	 * @return
	 * @throws IOException
	 */
	public AwkToken nextToken() throws IOException {
		if(!token.equals(AwkReserved.ENDMARKER)) {
			if(blocks == 0 && reader.isNewline()) {
				token = AwkReserved.ENDMARKER;
			} else {
				headchr = reader.read();
				if(headchr >= 0)  reader.unread(headchr);
				token = getToken(reader);
			}
		}
		return token;
	}

	/**
	 * 
	 * @return
	 * @throws IOException
	 */
	public AwkToken nextTokenBegin() throws IOException {
		blocks++;
		if(!token.equals(AwkReserved.ENDMARKER)) {
			headchr = reader.read();
			if(headchr >= 0)  reader.unread(headchr);
			token = getToken(reader);
		}
		return token;
	}

	/**
	 * 
	 * @return
	 * @throws IOException
	 */
	public boolean isEos() throws IOException {
		return (token.equals(AwkReserved.SEMICL) ||
				token.equals(AwkReserved.BLOCK_B) ||
				token.equals(AwkReserved.BLOCK_E) ||
				token.equals(AwkOperator.IN) ||
				token.equals(AwkReserved.APNDOUT) ||
				token.equals(AwkReserved.PIPE) ||
				token.equals(AwkReserved.ENDMARKER) ||
				headchr == '\n');
	}

	/**
	 * 
	 * @return
	 * @throws IOException
	 */
	public boolean eatEos() throws IOException {
		if(token.equals(AwkReserved.SEMICL)) {
			nextToken();
			return true;
		} else if(token.equals(AwkReserved.ENDMARKER)) {
			return true;
		} else {
			return (token.equals(AwkReserved.BLOCK_E) ||
					token.equals(AwkReserved.ENDMARKER) ||
					headchr == '\n');
		}
	}

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

	/**
	 * 
	 * @param t
	 * @return
	 * @throws IOException
	 */
	public AwkToken eatTokenEnd(AwkToken t) throws IOException {
		blocks--;
		if(!token.equals(t)) {
			throw new AwkSyntaxException(getLineNumber(), t, token);
		} else if(reader instanceof PromptReader &&
				reader.isNewline() && blocks == 0) {
			return AwkReserved.ENDMARKER;
		} else {
			return nextToken();
		}
	}

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

	/**
	 * 
	 * @return
	 * @throws IOException
	 */
	public String getPattern() throws IOException {
		return getPattern(reader);
	}

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

	/**
	 * 
	 */
	public void resetPrompt() throws IOException {
		reader.resetPrompt();
		reset = true;
	}

	/**
	 * 
	 * @return
	 */
	public boolean isNewline() throws IOException {
		return reader.isNewline();
	}

	// -------------------------------------------
	private static int rde(
			PushbackLineNumberReadable rd) throws IOException {
		int c;

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

	private static int skipws(
			PushbackLineNumberReadable 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 AwkToken getr(String s) {
		return RES1.containsKey(s) ?
				RES1.get(s) : AwkSymbol.getInstance(s);
	}

	private static boolean isnum(int c) {
		return c >= '0' && c <= '9';
	}

	private static boolean isAwkIdentifierStart(int c) {
		return (Character.isLetter(c) ||
				(Character.isDigit(c) && !isnum(c)) ||
				Character.getType(c) == Character.LETTER_NUMBER ||
				c == '_');
	}

	private static boolean isAwkIdentifierPart(int c) {
		return (Character.isLetter(c) ||
				Character.isDigit(c)  ||
				Character.getType(c) == Character.LETTER_NUMBER ||
				c == '_');
	}

	private static int getoct1(
			PushbackLineNumberReadable rd) throws IOException {
		int c;

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

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

	private static int gethex1(
			PushbackLineNumberReadable rd) throws IOException {
		int c;

		if(((c = rde(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 AwkLexerException(rd.getLineNumber(),
					"hexadecimal number required");
		}
	}

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

	static AwkToken _getToken(
			PushbackLineNumberReadable 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.STRING;
				} 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 == '\\') {
					
				} 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 == '.') {
					b1 = new StringBuffer().append((char)c);
					stat = St1.DOT;
				} else if(isAwkIdentifierStart(c)) {
					b1 = new StringBuffer().appendCodePoint(c);
					stat = St1.KEYWORD;
				}
				break;
			case LT:
				if((c = rd.read()) < 0) {
					return AwkRelop.LT;
				} else if(c == '=') {
					return AwkRelop.LE;
				} else {
					rd.unread(c);
					return AwkRelop.LT;
				}
			case GT:
				if((c = rd.read()) < 0) {
					return AwkRelop.GT;
				} else if(c == '=') {
					return AwkRelop.GE;
				} else if(c == '>') {
					return AwkReserved.APNDOUT;
				} else {
					rd.unread(c);
					return AwkRelop.GT;
				}
			case EQ:
				if((c = rd.read()) < 0) {
					return AwkAssignop.ASSIGN;
				} else if(c == '=') {
					return AwkRelop.EQ;
				} else {
					rd.unread(c);
					return AwkAssignop.ASSIGN;
				}
			case EX:
				if((c = rd.read()) < 0) {
					return AwkOperator.L_NOT;
				} else if(c == '=') {
					return AwkRelop.NE;
				} else if(c == '~') {
					return AwkOperator.NMATCH;
				} else {
					rd.unread(c);
					return AwkOperator.L_NOT;
				}
			case AND:
				if((c = rd.read()) < 0) {
					throw new AwkLexerException(rd.getLineNumber(),
							"'&' is not a valid token");
				} else if(c == '&') {
					return AwkOperator.L_AND;
				} else {
					throw new AwkLexerException(rd.getLineNumber(),
							"'&' is not a valid token");
				}
			case OR:
				if((c = rd.read()) < 0) {
					return AwkReserved.PIPE;
				} else if(c == '|') {
					return AwkOperator.L_OR;
				} else {
					rd.unread(c);
					return AwkReserved.PIPE;
				}
			case PLUS:
				if((c = rd.read()) < 0) {
					return AwkOperator.ADD;
				} else if(c == '+') {
					return AwkOperator.INC;
				} else if(c == '=') {
					return AwkAssignop.A_ADD;
				} else {
					rd.unread(c);
					return AwkOperator.ADD;
				}
			case MINUS:
				if((c = rd.read()) < 0) {
					return AwkOperator.SUB;
				} else if(c == '-') {
					return AwkOperator.DEC;
				} else if(c == '=') {
					return AwkAssignop.A_SUB;
				} else {
					rd.unread(c);
					return AwkOperator.SUB;
				}
			case ASTERISK:
				if((c = rd.read()) < 0) {
					return AwkOperator.MUL;
				} else if(c == '=') {
					return AwkAssignop.A_MUL;
				} else {
					rd.unread(c);
					return AwkOperator.MUL;
				}
			case SLASH:
				if((c = rd.read()) < 0) {
					return AwkOperator.DIV;
				} else if(c == '=') {
					return AwkAssignop.A_DIV;
				} else {
					rd.unread(c);
					return AwkOperator.DIV;
				}
			case PERCENT:
				if((c = rd.read()) < 0) {
					return AwkOperator.MOD;
				} else if(c == '=') {
					return AwkAssignop.A_MOD;
				} else {
					rd.unread(c);
					return AwkOperator.MOD;
				}
			case CARET:
				if((c = rd.read()) < 0) {
					return AwkOperator.POW;
				} else if(c == '=') {
					return AwkAssignop.A_POW;
				} else {
					rd.unread(c);
					return AwkOperator.POW;
				}
			case DOT:
				if((c = rd.read()) < 0) {
					return AwkOperator.NAME;
				} else if(c >= '0' && c <= '9') {
					b1.append('.').append((char)c);
					stat = St1.FLOAT2;
				} else {
					rd.unread(c);
					return AwkOperator.NAME;
				}
			case KEYWORD:
				if((c = rd.read()) < 0) {
					return getr(b1.toString());
				} else if(!isAwkIdentifierPart(c)) {
					rd.unread(c);
					return getr(b1.toString());
				} else {
					b1.appendCodePoint(c);
				}
				break;
			case STRING:
				if((c = rde(rd)) == '\\') {
					stat = St1.STRING_ESC;
				} else if(c == '\"') {
					return AwkStringToken.getInstance(b1.toString());
				} else {
					b1.appendCodePoint(c);
				}
				break;
			case STRING_ESC:
				if((c = rde(rd)) == '\"') {
					b1.append('\"');
				} else if(c == 'n') {
					b1.append('\n');
				} else if(c == 'r') {
					b1.append('\r');
				} else if(c == 'b') {
					b1.append('\b');
				} else if(isnum(c)) {
					rd.unread(c);
					b1.append((char)getoct(rd));
				} else if(c == 'u') {
					b1.append((char)gethex(rd));
				} else {
					b1.appendCodePoint(c);
				}
				stat = St1.STRING;
				break;
			case ZERO:
				if((c = rd.read()) < 0) {
					return new AwkIntegerToken("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 AwkIntegerToken("0", 10);
				}
				break;
			case NUMBER:
				if((c = rd.read()) < 0) {
					return new AwkIntegerToken(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 AwkIntegerToken(b1.toString(), 10);
				}
				break;
			case NUMBER_OCT:
				if((c = rd.read()) < 0) {
					return new AwkIntegerToken(b1.toString(), 8);
				} else if(c >= '0' && c <= '9') {
					b1.append((char)c);
				} else {
					rd.unread(c);
					return new AwkIntegerToken(b1.toString(), 8);
				}
				break;
			case NUMBER_HEX:
				if((c = rd.read()) < 0) {
					return new AwkIntegerToken(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 AwkIntegerToken(b1.toString(), 16);
				}
				break;
			case FLOAT1:
				if((c = rde(rd)) >= '0' && c <= '9') {
					b1.append((char)c);
					stat = St1.FLOAT2;
				} else {
					throw new AwkLexerException(rd.getLineNumber(),
							"invalid float number");
				}
				break;
			case FLOAT2:
				if((c = rd.read()) < 0) {
					return new AwkFloatToken(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 AwkFloatToken(b1.toString());
				}
				break;
			case FLOAT_E1:
				if((c = rde(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 AwkLexerException(rd.getLineNumber(),
							"invalid float number");
				}
				break;
			case FLOAT_E2:
				if((c = rde(rd)) >= '0' && c <= '9') {
					b1.append((char)c);
					stat = St1.FLOAT_E3;
				} else {
					throw new AwkLexerException(rd.getLineNumber(),
							"invalid float number");
				}
				break;
			case FLOAT_E3:
				if((c = rd.read()) < 0) {
					return new AwkFloatToken(b1.toString());
				} else if(c >= '0' && c <= '9') {
					b1.append((char)c);
				} else {
					rd.unread(c);
					return new AwkFloatToken(b1.toString());
				}
				break;
			case BACKSLASH:
				if((c = rd.read()) == '\n') {
					stat = St1.INIT;
				} else if(c != '\r') {
					throw new AwkLexerException(rd.getLineNumber(),
							"invalid token");
				}
				break;
			}
		}
	}

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

	/**
	 * 
	 * @param rd
	 * @return
	 * @throws IOException
	 */
	public static String getPattern(
			PushbackLineNumberReadable rd) throws IOException {
		StringBuffer b = new StringBuffer();
		boolean esc = false;
		int c;

		while(true) {
			if((c = rd.read()) < 0) {
				throw new AwkLexerException(rd.getLineNumber(),
						"unexpected EOF");
			} else if(esc) {
				if(c != '/')  b.append('\\');
				esc = false;
			} else if(c == '/') {
				return b.toString();
			} else {
				esc = c == '\\';
			}
			b.appendCodePoint(c);
		}
	}

}
