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

import java.io.IOException;
import java.io.PrintStream;
import java.io.PushbackReader;

public class ShLexer {

	private static enum S1 {
		INIT, ESC1, SQUT, DQUT, ESC2, DQT2, DQT3, DQT4,
		KEYW, KEY2, ESC3, VAR1, VAR2, VAR3, ANDP, OR_P,
		SMCL, HERE, HRE2, HRE3, HRE4, HRE5, HRE6, HRE7,
		CMD1, CMD2, CMD3, CMD4, NUM1, NUM2, NUM3, NUM4,
		R_IN, ROUT, RIN2, ROT2, RIN3, ROT3, RNUM,
	}

	private PushbackReader reader;
	private ShPromptReader prompt;
	private ShToken token;
	private ShEnvironment env;
	private PrintStream verbose;

	/**
	 * 
	 * @param reader
	 */
	public ShLexer(PushbackReader reader) {
		this.prompt = null;
		this.reader = reader;
	}

	/**
	 * 
	 * @param reader
	 */
	public ShLexer(ShEnvironment env, ShPromptReader reader,
			PrintStream verbose) {
		this.prompt  = reader;
		this.reader  = new PushbackReader(reader);
		this.env     = env;
		this.verbose = verbose;
	}

	/**
	 * 
	 * @param c
	 * @return
	 */
	public static boolean isId(int c) {
		return ((c >= 'A' && c <= 'Z') ||
				(c >= 'a' && c <= 'z') ||
				(c >= '0' && c <= '9') || (c == '_') || (c > 0xff));
	}

	/**
	 * 
	 * @param c
	 * @return
	 */
	public static boolean isBound(int c) {
		return (Character.isWhitespace(c) ||
				c == '|'  || c == '&'  || c == ';'  ||
				c == '>'  || c == '<'  || c == '('  ||
				c == ')'  || c <  0);
	}

	ShToken lex() throws IOException, ShSyntaxException {
		StringBuffer b = null, l = null;
		S1 stat = S1.INIT;
		String d = null;
		int c, m = -1;

		skipsp();
		while(true) {
			c = reader.read();
			switch(stat) {
			case INIT:
				if(c == '\'') {
					b = new StringBuffer().append((char)c);
					addPrompt();
					stat = S1.SQUT;
				} else if(c == '\"') {
					b = new StringBuffer().append((char)c);
					addPrompt();
					stat = S1.DQUT;
				} else if(c == '\\') {
					stat = S1.ESC1;
				} else if(c == '$') {
					b = new StringBuffer().append((char)c);
					stat = S1.VAR1;
				} else if(c == '`') {
					b = new StringBuffer("$(");
					addPrompt();
					stat = S1.CMD3;
				} else if(c == '\n') {
					return ShToken.NEWLINE;
				} else if(c == '|') {
					stat = S1.OR_P;
				} else if(c == '&') {
					stat = S1.ANDP;
				} else if(c == ';') {
					stat = S1.SMCL;
				} else if(c == '<') {
					stat = S1.R_IN;  m = 0;
				} else if(c == '>') {
					stat = S1.ROUT;  m = 1;
				} else if(c == '(') {
					return ShToken.PAREN_B;
				} else if(c == ')') {
					return ShToken.PAREN_E;
				} else if(c >= '0' && c <= '9') {
					b = new StringBuffer().append((char)c);
					stat = S1.RNUM;
				} else if(c < 0) {
					return ShToken.EOF;
				} else {
					b = new StringBuffer().append((char)c);
					stat = S1.KEYW;
				}
				break;
			case ESC1:
				if(c < 0)  return new ShKeyword("\\");
				b = new StringBuffer().append('\\').append((char)c);
				stat = S1.KEY2;
				break;
			case SQUT:
				b.append((char)c);
				if(c == '\'') {
					removePrompt();
					stat = S1.KEY2;
				} else if(c < 0) {
					throw new ShSyntaxException();
				}
				break;
			case DQUT:
				if(c == '`') {
					b.append("$(");
					addPrompt();
					stat = S1.CMD4;
				} else if(c == '\\') {
					stat = S1.ESC2;
				} else if(c < 0) {
					throw new ShSyntaxException();
				} else {
					b.append((char)c);
					if(c == '\"') {
						removePrompt();
						stat = S1.KEY2;
					} else if(c == '$') {
						stat = S1.DQT2;
					}
				}
				break;
			case ESC2:
				if(c < 0) {
					throw new ShSyntaxException();
				}
				b.append("\\\\\\").append((char)c);
				stat = S1.DQUT;
				break;
			case DQT2:
				if(c == '\"') {
					removePrompt();
					stat = S1.KEY2;
				} else if(c == '{') {
					stat = S1.DQT3;
				} else if(c == '(') {
					addPrompt();
					stat = S1.CMD1;
				} else if(c < 0) {
					throw new ShSyntaxException();
				} else if(isId(c)) {
					b.append('{');
					stat = S1.DQT4;
				} else {
					stat = S1.DQUT;
				}
				b.append((char)c);
				break;
			case DQT3:
				if(c < 0 || c == '\"') {
					throw new ShSyntaxException();
				} else {
					b.append((char)c);
					if(c == '}')  stat = S1.DQUT;
				}
				break;
			case DQT4:
				if(c == '\"') {
					b.append('}');
					stat = S1.KEY2;
				} else if(!isId(c)) {
					b.append('}');
					stat = S1.DQUT;
				}
				b.append((char)c);
				break;
			case KEYW:
				if(isBound(c)) {
					if(c >= 0)  reader.unread(c);
					return new ShKeyword(b.toString());
				} else if(c == '`') {
					b.append("$(");
					stat = S1.CMD3;
				} else if(c == '\\') {
					stat = S1.ESC3;
				} else {
					b.append((char)c);
					if(c == '$') {
						stat = S1.VAR1;
					} else if(c == '\'') {
						addPrompt();
						stat = S1.SQUT;
					} else if(c == '\"') {
						addPrompt();
						stat = S1.DQUT;
					}
				}
				break;
			case KEY2:
				if(isBound(c)) {
					if(c >= 0)  reader.unread(c);
					return new ShString(b.toString());
				} else if(c == '`') {
					addPrompt();
					b.append("$(");
					stat = S1.CMD3;
				} else if(c == '\\') {
					stat = S1.ESC3;
				} else {
					b.append((char)c);
					if(c == '$') {
						stat = S1.VAR1;
					} else if(c == '\'') {
						addPrompt();
						stat = S1.SQUT;
					} else if(c == '\"') {
						addPrompt();
						stat = S1.DQUT;
					}
				}
				break;
			case ESC3:
				b.append('\\');
				if(c < 0)  return new ShString(b.toString());
				b.append((char)c);
				stat = S1.KEY2;
				break;
			case VAR1:
				if(c == '(') {
					b.append((char)c);
					addPrompt();
					stat = S1.CMD2;
				} else if(isBound(c)) {
					if(c >= 0)  reader.unread(c);
					return new ShString(b.toString());
				} else if(c == '{') {
					b.append((char)c);
					stat = S1.VAR2;
				} else {
					b.append('{').append((char)c);
					stat = S1.VAR3;
				}
				break;
			case VAR2:
				if(c < 0) {
					throw new ShSyntaxException();
				} else {
					b.append((char)c);
					if(c == '}')  stat = S1.KEY2;
				}
				break;
			case VAR3:
				if(isBound(c)) {
					if(c >= 0)  reader.unread(c);
					return new ShString(b.append('}').toString());
				} else if(!isId(c)) {
					b.append('}');
					stat = S1.KEY2;
				}
				b.append((char)c);
				break;
			case ANDP:
				if(c == '&') {
					return ShToken.AND;
				} else if(c == '>') {
					return new ShRedirector(ShRedirectType.OUT, -1,
							-1);
				} else {
					if(c >= 0)  reader.unread(c);
					return ShToken.BG;
				}
			case OR_P:
				if(c == '|') {
					return ShToken.OR;
				} else {
					if(c >= 0)  reader.unread(c);
					return ShToken.PIPE;
				}
			case SMCL:
				if(c == ';') {
					return ShToken.CASEEND;
				} else {
					if(c >= 0)  reader.unread(c);
					return ShToken.FG;
				}
			case HERE:
				addPrompt();
				b = new StringBuffer();
				if(!Character.isWhitespace(c)) {
					b.append((char)c);
					stat = S1.HRE2;
				}
				break;
			case HRE2:
				if(c < 0 || c == '\n') {
					d = b.toString();
					b = new StringBuffer();
					l = new StringBuffer();
					stat = S1.HRE3;
				} else {
					b.append((char)c);
				}
				break;
			case HRE3:
				if(c == '$') {
					l.append((char)c);
					stat = S1.HRE4;
				} else if(c >= 0 && c != '\n') {
					l.append((char)c);
				} else if(d.equals(l.toString())) {
					if(c >= 0)  reader.unread((char)c);
					removePrompt();
					return new ShRedirector(b.toString());
				} else {
					b.append(l).append('\n');
					l = new StringBuffer();
				}
				break;
			case HRE4:
				if(c == '(') {
					l.append((char)c);
					stat = S1.HRE7;
				} else if(isBound(c)) {
					l.append('$').append((char)c);
					stat = S1.HRE3;
				} else if(c == '{') {
					l.append((char)c);
					stat = S1.HRE5;
				} else if(c >= 0 && c != '\n') {
					l.append('{').append((char)c);
					stat = S1.HRE6;
				} else if(d.equals(l.toString())) {
					if(c >= 0)  reader.unread((char)c);
					removePrompt();
					return new ShRedirector(b.toString());
				} else {
					b.append(l).append('\n');
					l = new StringBuffer();
					stat = S1.HRE3;
				}
				break;
			case HRE5:
				if(c >= 0 && c != '\n') {
					l.append((char)c);
					if(c == '}')  stat = S1.HRE3;
				} else if(d.equals(l.toString())) {
					if(c >= 0)  reader.unread((char)c);
					removePrompt();
					return new ShRedirector(b.toString());
				} else {
					b.append(l).append('\n');
					l = new StringBuffer();
					stat = S1.HRE3;
				}
				break;
			case HRE6:
				if(isId(c)) {
					l.append((char)c);
				} else if(c >= 0 && c != '\n') {
					if(c >= 0)  reader.unread(c);
					l.append('}');
					stat = S1.HRE3;
				} else if(d.equals(l.toString())) {
					if(c >= 0)  reader.unread((char)c);
					removePrompt();
					return new ShRedirector(b.toString());
				} else {
					b.append(l).append('}').append('\n');
					l = new StringBuffer();
					stat = S1.HRE3;
				}
				break;
			case HRE7:
				if(c >= 0 && c != '\n') {
					l.append((char)c);
					if(c == ')')  stat = S1.HRE3;
				} else if(d.equals(l.toString())) {
					if(c >= 0)  reader.unread((char)c);
					removePrompt();
					return new ShRedirector(b.toString());
				} else {
					b.append(l).append('\n');
					l = new StringBuffer();
				}
				break;
			case CMD1:
				b.append((char)c);
				if(c == '(')  stat = S1.NUM1;
				if(c == ')') {
					removePrompt();
					stat = S1.DQUT;
				} else if(c < 0) {
					throw new ShSyntaxException();
				}
				break;
			case CMD2:
				b.append((char)c);
				if(c == '(')  stat = S1.NUM3;
				if(c == ')') {
					removePrompt();
					stat = S1.KEY2;
				} else if(c < 0) {
					throw new ShSyntaxException();
				}
				break;
			case CMD3:
				if(c == '`') {
					b.append(')');
					removePrompt();
					stat = S1.KEY2;
				} else if(c < 0) {
					throw new ShSyntaxException();
				} else {
					b.append((char)c);
				}
				break;
			case CMD4:
				if(c == '`') {
					b.append(')');
					removePrompt();
					stat = S1.DQUT;
				} else if(c < 0) {
					throw new ShSyntaxException();
				} else {
					b.append((char)c);
				}
				break;
			case NUM1:
				b.append((char)c);
				if(c == ')')  stat = S1.NUM2;
				if(c < 0) {
					throw new ShSyntaxException();
				}
				break;
			case NUM2:
				b.append((char)c);
				if(c == ')') {
					removePrompt();
					stat = S1.DQUT;
				} else if(c < 0) {
					throw new ShSyntaxException();
				}
				break;
			case NUM3:
				b.append((char)c);
				if(c == ')')  stat = S1.NUM4;
				if(c < 0) {
					throw new ShSyntaxException();
				}
				break;
			case NUM4:
				b.append((char)c);
				if(c == ')') {
					removePrompt();
					stat = S1.KEY2;
				} else if(c < 0) {
					throw new ShSyntaxException();
				}
				break;
			case R_IN:
				if(c == '<') {
					stat = S1.HERE;
				} else if(c == '&') {
					stat = S1.RIN3;
				} else if(c >= 0) {
					if(c >= 0)  reader.unread(c);
					return ShRedirector.IN;
				}
				break;
			case ROUT:
				if(c == '>') {
					return ShRedirector.APPEND;
				} else if(c == '&') {
					stat = S1.ROT3;
				} else {
					if(c >= 0)  reader.unread(c);
					return ShRedirector.OUT;
				}
				break;
			case RIN2:
				if(c == '&') {
					stat = S1.RIN3;
				} else if(c >= 0) {
					if(c >= 0)  reader.unread(c);
					return new ShRedirector(ShRedirectType.IN, m, -1);
				}
				break;
			case ROT2:
				if(c == '>') {
					return new ShRedirector(ShRedirectType.APPEND, m,
							-1);
				} else if(c == '&') {
					stat = S1.ROT3;
				} else {
					if(c >= 0)  reader.unread(c);
					return new ShRedirector(ShRedirectType.OUT, m, -1);
				}
				break;
			case RIN3:
				if(c >= '0' && c <= '9') {
					b.append((char)c);
				} else if(b.length() == 0) {
					throw new ShSyntaxException();
				} else {
					if(c >= 0)  reader.unread(c);
					return new ShRedirector(ShRedirectType.IN, m,
							Integer.parseInt(b.toString()));
				}
				break;
			case ROT3:
				if(c >= '0' && c <= '9') {
					b.append((char)c);
				} else if(b.length() == 0) {
					throw new ShSyntaxException();
				} else {
					if(c >= 0)  reader.unread(c);
					return new ShRedirector(ShRedirectType.OUT, m,
							Integer.parseInt(b.toString()));
				}
				break;
			case RNUM:
				if(c >= '0' && c <= '9') {
					b.append((char)c);
				} else if(c == '<') {
					m = Integer.parseInt(b.toString());
					b = new StringBuffer();
					stat = S1.RIN2;
				} else if(c == '>') {
					m = Integer.parseInt(b.toString());
					b = new StringBuffer();
					stat = S1.ROT2;
				} else if(isBound(c)) {
					if(c >= 0)  reader.unread(c);
					return new ShKeyword(b.toString());
				} else if(c == '`') {
					b.append("$(");
					stat = S1.CMD3;
				} else {
					b.append((char)c);
					if(c == '$')   stat = S1.VAR1;
					if(c == '\'')  stat = S1.SQUT;
					if(c == '\"')  stat = S1.DQUT;
				}
				break;
			}
		}
	}

	ShToken lexws() throws IOException, ShSyntaxException {
		skipws();
		return lex();
	}

	public ShToken getToken() {
		return token;
	}

	public ShToken nextToken() throws IOException, ShSyntaxException {
		token = lex();
		if(env == null || !env.isSet("verbose") || verbose == null) {
			// do nothing
		} else if(token == ShToken.NEWLINE || token == ShToken.EOF) {
			verbose.print('\n');
		} else {
			verbose.print(token);
			verbose.print(' ');
		}
		return token;
	}

	public ShToken nextId() throws IOException, ShSyntaxException {
		if(!(token = lex()).isString()) {
			throw new ShSyntaxException();
		}
		return token;
	}

	public void eat(ShToken s) throws IOException, ShSyntaxException {
		if(!getToken().equals(s)) {
			throw new ShSyntaxException();
		}
	}

	public void eatId(
			String s) throws IOException, ShSyntaxException {
		if(!getToken().equalsKeyword(s)) {
			throw new ShSyntaxException();
		}
	}

	public void skipsp() throws IOException {
		boolean comment = false;
		int c;

//		while((c = reader.read()) >= 0 &&
//				Character.isWhitespace(c) && c != '\n');
		while((c = reader.read()) >= 0) {
			if(comment) {
				if(!(comment = c != '\n'))  reader.unread(c);
			} else if(c == '#') {
				comment = true;
			} else if(c == '\n' || !Character.isWhitespace(c)) {
				break;
			}
		}
		if(c >= 0)  reader.unread(c);
	}

	public void skipws() throws IOException {
		boolean comment = false;
		int c;

//		while((c = reader.read()) >= 0 && Character.isWhitespace(c));
		while((c = reader.read()) >= 0) {
			if(comment) {
				comment = c != '\n';
			} else if(c == '#') {
				comment = true;
			} else if(!Character.isWhitespace(c)) {
				break;
			}
		}
		if(c >= 0)  reader.unread(c);
	}

	public void addPrompt() {
		if(prompt != null)  prompt.count++;
	}

	public void removePrompt() {
		if(prompt != null)  prompt.count--;
	}

	public void resetPrompt() {
		if(prompt != null)  prompt.resetPrompt();
	}

	public boolean isEof() {
		return prompt == null || prompt.isEof();
	}

}
