/*
 * 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.InputStream;
import java.io.InputStreamReader;
import java.io.PushbackReader;
import java.io.Reader;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

public class ShParser {

	static void eatHeadId(ShLexer r,
			String s) throws IOException, ShSyntaxException {
		while(r.getToken().isEndOfLine())  r.nextToken();
		if(!r.getToken().equalsKeyword(s)) {
			throw new ShSyntaxException();
		}
	}

	static ShTree parseIf(
			ShLexer r) throws IOException, ShSyntaxException {
		List<ShTree> c, b, l;
		ShToken t;

		c = new ArrayList<ShTree>();
		b = new ArrayList<ShTree>();
		l = new ArrayList<ShTree>();
		c.add(parseCommand(r, r.nextToken()));
		while((t = r.nextToken()).isEndOfLine());
		r.eatId("then");
		while((t = r.nextToken()).isEndOfLine());
		while(true) {
			if(t == ShToken.EOF) {
				throw new ShSyntaxException();
			} else if(t.equalsKeyword("fi")) {
				b.add(new ShTreeBlock(l));
				return new ShTreeIf(c, b, null);
			} else if(t.equalsKeyword("else")) {
				b.add(new ShTreeBlock(l));
				return new ShTreeIf(c, b, parseUntilKeyword(r, "fi"));
			} else if(t.equalsKeyword("elif")) {
				b.add(new ShTreeBlock(l));
				l = new ArrayList<ShTree>();
				c.add(parseCommand(r, r.nextToken()));
				while((t = r.nextToken()).isEndOfLine());
				eatHeadId(r, "then");
				while((t = r.nextToken()).isEndOfLine());
			} else {
				l.add(parseCommand(r, t));
				t = r.nextToken();
			}
		}
	}

	static ShPattern parsePattern(ShLexer r,
			ShToken t) throws IOException, ShSyntaxException {
		List<ShToken> l = new ArrayList<ShToken>();
		ShToken s = t;

		while(s != ShToken.PAREN_E) {
			if(s.isString()) {
				l.add(s);
				if((s = r.nextToken()) == ShToken.PIPE) {
					s = r.nextToken();
				} else if(s != ShToken.PAREN_E) {
					throw new ShSyntaxException();
				}
			} else {
				throw new ShSyntaxException();
			}
		}
		return new ShPattern(l);
	}

	static ShTree parseCase(
			ShLexer r) throws IOException, ShSyntaxException {
		List<ShPattern> p = new ArrayList<ShPattern>();
		List<ShTree> l = new ArrayList<ShTree>();
		ShToken s, t;

		if(!(s = r.nextToken()).isString()) {
			throw new ShSyntaxException();
		} else {
			r.nextToken();  r.eatId("in");
			while(r.nextToken().isEndOfLine());
			while(!(t = r.getToken()).equalsKeyword("esac")) {
				p.add(parsePattern(r, t));
				l.add(parseUntilToken(r, ShToken.CASEEND));
				while(r.nextToken().isEndOfLine());
			}
			return new ShTreeCase(s, p, l);
		}
	}

	static ShTree parseWhileUntil(ShLexer r,
			boolean until) throws IOException, ShSyntaxException {
		ShTree c, b;

		c = parseCommand(r, r.nextToken());
		while(r.nextToken().isEndOfLine());
		eatHeadId(r, "do");
		b = parseUntilKeyword(r, "done");
		return new ShTreeWhileUntil(c, b, until);
	}

	static List<ShToken> parseIdList(
			ShLexer r) throws IOException, ShSyntaxException {
		List<ShToken> l = new ArrayList<ShToken>();
		ShToken s;

		while(true) {
			if((s = r.nextToken()).precedence() > 0)  return l;
			l.add(s);
		}
	}

	static ShTree parseFor(
			ShLexer r) throws IOException, ShSyntaxException {
		List<ShToken> l;
		ShToken v;
		ShTree h;

		if(!((v = r.nextToken()) instanceof ShKeyword)) {
			throw new ShSyntaxException();
		}
		r.nextToken();  r.eatId("in");
		l = parseIdList(r);
		while(r.nextToken().isEndOfLine());
		eatHeadId(r, "do");
		h = parseUntilKeyword(r, "done");
		return new ShTreeFor(v, l, h);
	}

	static ShTree parseSimpleCommand(ShLexer r,
			ShToken t) throws IOException, ShSyntaxException {
		List<ShRedirector> d = new ArrayList<ShRedirector>();
		List<ShToken> f = new ArrayList<ShToken>();
		List<ShToken> l = new ArrayList<ShToken>();
		List<ShToken> b = new ArrayList<ShToken>();
		ShToken s = t;
		ShTree x;

		for(;; s = r.nextToken()) {
			if(s.isFileRedirector()) {
				d.add((ShRedirector)s);
				f.add(r.nextId());
			} else if(s.isDescriptorCopier()) {
				d.add((ShRedirector)s);
				f.add(null);
			} else if(s.isHereDocument()) {
				d.add((ShRedirector)s);
				f.add(null);
			} else if(l.size() == 0 && s.isBind()) {
				b.add(s);
			} else if(l.size() == 1 &&
					l.get(0) instanceof ShKeyword &&
					s == ShToken.PAREN_B) {
				r.nextToken();  r.eat(ShToken.PAREN_E);
				while((t = r.nextToken()).isEndOfLine());
				r.eatId("{");
				x = parseUntilKeyword(r, "}");
				x = parseStatRedirect(r, x);
				return new ShFunction(l.get(0).toString(), x);
			} else if(s.precedence() <= 0) {
				l.add(s);
			} else if(l.size() == 0) {
				return new ShTreeBind(b);
			} else if(l.get(0).equalsKeyword("break")) {
				return new ShTreeBreak();
			} else if(l.get(0).equalsKeyword("continue")) {
				return new ShTreeContinue();
			} else if(l.get(0).equalsKeyword("return")) {
				if(l.size() > 1) {
					return new ShTreeReturn(l.get(1));
				} else {
					return new ShTreeReturn();
				}
			} else if(l.get(0).equalsKeyword(".") ||
					l.get(0).equalsKeyword("source")) {
				if(l.size() > 1) {
					return new ShTreeSource(l.get(1));
				} else {
					return new ShTreeSource(null);
				}
			} else if(l.get(0).equalsKeyword("type")) {
				if(l.size() > 1) {
					return new ShTreeType(l.get(1));
				} else {
					return new ShTreeType(new ShKeyword(""));
				}
			} else if(l.get(0).equalsKeyword("eval")) {
				return new ShTreeEval(l);
			} else if(l.get(0).equalsKeyword("sh")) {
				return new ShTreeSh(l.subList(1, l.size()));
			} else if(l.get(0).equalsKeyword("builtin")) {
				return new ShTreeSimpleCommand(l.subList(1, l.size()),
						d, f, b, ShTreeSimpleCommand.BUILTIN);
			} else if(l.get(0).equalsKeyword("command")) {
				return new ShTreeSimpleCommand(l.subList(1, l.size()),
						d, f, b, ShTreeSimpleCommand.COMMAND);
			} else {
				return new ShTreeSimpleCommand(l, d, f, b,
						ShTreeSimpleCommand.ALL);
			}
		}
	}

	static ShTree parseTest(ShLexer r,
			String k) throws IOException, ShSyntaxException {
		List<ShRedirector> d = new ArrayList<ShRedirector>();
		List<ShToken> f = new ArrayList<ShToken>();
		List<ShToken> l = new ArrayList<ShToken>();
		ShToken s;

		l.add(new ShKeyword("test"));
		for(;;) {
			s = r.nextToken();
			if(s == ShToken.NEWLINE) {
				throw new ShSyntaxException();
			} else if(s.equalsKeyword(k)) {
				return new ShTreeSimpleCommand(l, d, f,
						ShTreeSimpleCommand.ALL);
			} else {
				l.add(s);
			}
		}
	}

	static ShTree parseStatement(ShLexer r,
			ShToken s) throws IOException, ShSyntaxException {
		ShToken t = s;
		ShTree h;

		while(t == ShToken.NEWLINE)  t = r.nextToken();
		if(t.equalsKeyword("{")) {
			r.addPrompt();
			h = parseUntilKeyword(r, "}");
			r.removePrompt();
		} else if(t == ShToken.PAREN_B) {
			r.addPrompt();
			h = new ShTreeSubprocess(
					parseUntilToken(r, ShToken.PAREN_E));
			r.removePrompt();
		} else if(t.equalsKeyword("if")) {
			r.addPrompt();
			h = parseIf(r);
			r.removePrompt();
		} else if(t.equalsKeyword("case")) {
			r.addPrompt();
			h = parseCase(r);
			r.removePrompt();
		} else if(t.equalsKeyword("while")) {
			r.addPrompt();
			h = parseWhileUntil(r, false);
			r.removePrompt();
		} else if(t.equalsKeyword("until")) {
			r.addPrompt();
			h = parseWhileUntil(r, true);
			r.removePrompt();
		} else if(t.equalsKeyword("for")) {
			r.addPrompt();
			h = parseFor(r);
			r.removePrompt();
		} else if(t.equalsKeyword("[")) {
			h = parseTest(r, "]");
		} else if(t.equalsKeyword("[[")) {
			h = parseTest(r, "]]");
		} else if(t.equalsKeyword("function")) {
			if(!((t = r.nextToken()) instanceof ShKeyword)) {
				throw new ShSyntaxException();
			}
			while(r.nextToken().isEndOfLine());
			r.eatId("{");
			h = parseUntilKeyword(r, "}");
			h = parseStatRedirect(r, h);
			return new ShFunction(t.toString(), h);
		} else {
			return parseSimpleCommand(r, t);
		}
		return parseStatRedirect(r, h);
	}

	static ShTree parsePipeCommand(ShLexer r,
			ShToken t) throws IOException, ShSyntaxException {
		List<ShTree> l = new ArrayList<ShTree>();
		ShToken s = t;

		for(;; s = r.nextToken()) {
			l.add(parseStatement(r, s));
			if((s = r.getToken()).precedence() > 10) {
				return l.size() < 2 ? l.get(0) : new ShTreePipe(l);
			}
		}
	}

	static ShTree parseNot(ShLexer r,
			ShToken t) throws IOException, ShSyntaxException {
		boolean n = false;
		ShToken s = t;
		ShTree h;

		if(s.equalsKeyword("!")) {
			n = true;
			s = r.nextToken();
		}
		h = parsePipeCommand(r, s);
		return n ? new ShTreeNot(h) : h;
	}

	static ShTree parseAnd(ShLexer r,
			ShToken t) throws IOException, ShSyntaxException {
		List<ShTree> l = new ArrayList<ShTree>();
		ShToken s = t;

		for(;; s = r.nextToken()) {
			l.add(parseNot(r, s));
			if((s = r.getToken()).precedence() > 30) {
				return l.size() < 2 ? l.get(0) : new ShTreeAnd(l);
			}
		}
	}

	static ShTree parseOr(ShLexer r,
			ShToken t) throws IOException, ShSyntaxException {
		List<ShTree> l = new ArrayList<ShTree>();
		ShToken s = t;

		for(;; s = r.nextToken()) {
			l.add(parseAnd(r, s));
			if((s = r.getToken()).precedence() > 40) {
				return l.size() < 2 ? l.get(0) : new ShTreeOr(l);
			}
		}
	}

	/**
	 * 
	 * @param r
	 * @param t
	 * @return
	 * @throws IOException
	 * @throws ShSyntaxException
	 */
	public static ShTree parseCommand(ShLexer r,
			ShToken t) throws IOException, ShSyntaxException {
		ShToken s = t;
		ShTree h;

		while(true) {
			h = parseOr(r, s);
			if((s = r.getToken()) == ShToken.FG ||
					s == ShToken.NEWLINE ||
					s == ShToken.CASEEND ||
					s == ShToken.EOF) {
				return h;
			} else if(s == ShToken.BG) {
				return new ShTreeBackground(h);
			} else {
				throw new ShSyntaxException();
			}
		}
	}

	static ShTree parseStatRedirect(ShLexer r,
			ShTree h) throws IOException, ShSyntaxException {
		List<ShRedirector> d = new ArrayList<ShRedirector>();
		List<ShToken> f = new ArrayList<ShToken>();
		ShToken s = r.nextToken();

		for(;; s = r.nextToken()) {
			if(s.isFileRedirector()) {
				d.add((ShRedirector)s);
				f.add(r.nextId());
			} else if(s.isDescriptorCopier()) {
				d.add((ShRedirector)s);
				f.add(null);
			} else if(d.size() > 0) {
				return new ShTreeRedirect(h, d, f);
			} else {
				return h;
			}
		}
	}

	static ShTree parseUntilKeyword(ShLexer r,
			String s) throws IOException, ShSyntaxException {
		List<ShTree> l = new ArrayList<ShTree>();
		ShToken t;

		while(true) {
			while((t = r.nextToken()).isEndOfLine());
			if(t == ShToken.EOF) {
				throw new ShSyntaxException();
			} else if(t.equalsKeyword(s)) {
				break;
			} else {
				l.add(parseCommand(r, t));
			}
		}
		return new ShTreeBlock(l);
	}

	static ShTree parseUntilToken(ShLexer r,
			ShToken s) throws IOException, ShSyntaxException {
		List<ShTree> l = new ArrayList<ShTree>();
		ShToken t;

		while(true) {
			while((t = r.nextToken()).isEndOfLine());
			if(t == ShToken.EOF) {
				throw new ShSyntaxException();
			} else if(r.getToken().equals(s)) {
				break;
			} else {
				l.add(parseStatement(r, t));
				if(r.getToken().equals(s))  break;
			}
		}
		return new ShTreeBlock(l);
	}

	static ShTree parseScript(
			ShLexer r) throws IOException, ShSyntaxException {
		List<ShTree> l = new ArrayList<ShTree>();
		ShToken t;

		while(true) {
			while((t = r.nextToken()) == ShToken.NEWLINE);
			if(t == ShToken.EOF) {
				return new ShTreeBlock(l);
			} else {
				l.add(parseCommand(r, t));
			}
		}
	}

	/**
	 * 
	 * @param reader
	 * @return
	 * @throws IOException
	 * @throws ShSyntaxException
	 */
	public static ShTree parse(
			Reader reader) throws IOException, ShSyntaxException {
		ShLexer l = new ShLexer(new PushbackReader(reader));

		l.skipws();
		return parseScript(l);
	}

	/**
	 * 
	 * @param input
	 * @return
	 * @throws IOException
	 * @throws ShSyntaxException
	 */
	public static ShTree parse(InputStream input,
			Charset cset) throws IOException, ShSyntaxException {
		return parse(new InputStreamReader(input, cset));
	}

	/**
	 * 
	 * @param reader
	 * @return
	 * @throws IOException
	 * @throws ShSyntaxException
	 */
	public static ShTree parse(
			String script) throws IOException, ShSyntaxException {
		ShLexer l;

		l = new ShLexer(new PushbackReader(new StringReader(script)));
		l.skipws();
		return parseScript(l);
	}

}
