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

import java.io.IOException;
import java.io.PushbackReader;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;

import net.morilib.sed.line.SedLastLineMatcher;
import net.morilib.sed.line.SedNumberLineMatcher;
import net.morilib.sed.line.SedPatternLineMatcher;
import net.morilib.sed.line.SedRelativeLineMatcher;
import net.morilib.sed.line.SedStepLineMatcher;

/**
 * sedコマンド全体のパーサです。
 *
 *
 * @author MORIGUCHI, Yuichiro 2013/03
 */
public class SedParser {

	/**
	 * sedコマンドをパースします。
	 * 
	 * @param b  コマンド集
	 * @param rd コマンド
	 * @return パースされたコマンドのリスト
	 * @throws IOException
	 */
	public static List<SedCommandLine> parse(SedCommandBundle b,
			Reader rd) throws IOException {
		List<SedCommandLine> r = new ArrayList<SedCommandLine>();
		PushbackReader re = new PushbackReader(rd);
		SedCommandLine l;
		int c;

		while((c = re.read()) >= 0) {
			re.unread(c);
			if((l = parseLine(b, re)) != null)  r.add(l);
			if((c = re.read()) >= 0 && c != '\n' && c != ';') {
				throw new SedSyntaxException((char)c + "");
			}
		}
		return r;
	}

	/**
	 * 1行のsedコマンドをパースします。
	 * 
	 * @param b  コマンド集
	 * @param rd コマンド
	 * @return パースされたコマンド
	 */
	public static SedCommandLine parseLine(SedCommandBundle b,
			String rd) {
		PushbackReader re = new PushbackReader(new StringReader(rd));
		SedCommandLine l;

		try {
			l = parseLine(b, re);
			if(re.read() >= 0)  throw new SedSyntaxException();
			return l;
		} catch(IOException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * 1行のsedコマンドをパースします。
	 * 
	 * @param b  コマンド集
	 * @param rd コマンド
	 * @return パースされたコマンド
	 */
	public static SedCommandLine parseLine(SedCommandBundle b,
			PushbackReader rd) throws IOException {
		SedCommandLine r = new SedCommandLine();

		r.matcher = parseLines(rd);
		return (r.command = parsecmd(b, rd, r.matcher)) == null ?
				null : r;
	}

	static SedCommand parsecmd(SedCommandBundle b, PushbackReader rd,
			SedLineOption l) throws IOException {
		SedCommandParser p;
		StringBuffer bf;
		int c;

		if((c = rd.read()) < 0) {
			throw new SedSyntaxException();
		} else if(c == '\'') {
			bf = new StringBuffer();
			while((c = rd.read()) != '\'') {
				if(c < 0)  throw new SedSyntaxException();
				bf.appendCodePoint(c);
			}
			p = b.get(bf.toString());
		} else if(c == '\n' || (p = b.get(c)) == null) {
			throw new SedSyntaxException();
		}
		return p.parse(b, rd, l);
	}

	private static void setneg(PushbackReader rd,
			SedLineOption r) throws IOException {
		int c;

		if((c = rd.read()) == '!') {
			r.negate = true;
		} else {
			if(c >= 0)  rd.unread(c);
		}
	}

	/**
	 * sedの行マッチ部分をパースします。
	 * 
	 * @param rd コマンド
	 * @return 行マッチ部分
	 * @throws IOException
	 */
	public static SedLineOption parseLines(
			PushbackReader rd) throws IOException {
		SedLineMatcher m, n;
		SedLineOption r;
		boolean ci, am;
		String s;
		long a, b;
		int c;

		ci = am = false;
		if((c = rd.read()) == 'g') {
			if((c = rd.read()) < 0 || c == '\n') {
				throw new SedSyntaxException();
			}
			s = getstr(rd, c);
			while((c = rd.read()) >= 0) {
				if(c == 'I' || c == 'i') {
					ci = true;
				} else if(c == 'M' || c == 'm') {
					am = true;
				} else {
					if(c >= 0)  rd.unread(c);
					break;
				}
			}
			r = new SedLineOption(s, ci, am);
			setneg(rd, r);
		} else {
			if(c >= 0)  rd.unread(c);
			if((m = line(rd)) != null) {
				if(m instanceof SedRelativeLineMatcher) {
					throw new SedSyntaxException();
				} else if((c = rd.read()) == ',') {
					n = line(rd);
					if(m instanceof SedNumberLineMatcher &&
							n instanceof SedNumberLineMatcher) {
						a = ((SedNumberLineMatcher)m).getLineno();
						b = ((SedNumberLineMatcher)n).getLineno();
						if(b < a)  throw new SedSyntaxException();
					} else if(m instanceof SedLastLineMatcher &&
							!(n instanceof SedLastLineMatcher)) {
						throw new SedSyntaxException();
					}
					r = new SedLineOption(m, n);
					setneg(rd, r);
				} else if(c == '!') {
					r = new SedLineOption(m);
					r.negate = true;
				} else if(c == '~') {
					if(!(m instanceof SedNumberLineMatcher)) {
						throw new SedSyntaxException();
					}
					m = new SedStepLineMatcher(
							((SedNumberLineMatcher)m).getLineno(),
							nums(rd));
					r = new SedLineOption(m);
					setneg(rd, r);
				} else {
					if(c >= 0)  rd.unread(c);
					r = new SedLineOption(m);
				}
			} else {
				r = new SedLineOption();
			}
		}
		return r;
	}

	private static SedLineMatcher getlinere(PushbackReader rd,
			int d) throws IOException {
		boolean ci, am;
		String s;
		int c;

		ci = am = false;
		s  = getstr(rd, d);
		while((c = rd.read()) >= 0) {
			if(c == 'I' || c == 'i') {
				ci = true;
			} else if(c == 'M' || c == 'm') {
				am = true;
			} else {
				if(c >= 0)  rd.unread(c);
				break;
			}
		}
		return new SedPatternLineMatcher(s, ci, am);
	}

	static SedLineMatcher line(PushbackReader rd) throws IOException {
		SedLineMatcher r;
		int c, d;

		if((c = rd.read()) == '/') {
			return getlinere(rd, '/');
		} else if(c == '$') {
			r = SedLastLineMatcher.INSTANCE;
		} else if(c >= '0' && c <= '9') {
			rd.unread(c);
			r = new SedNumberLineMatcher(nums(rd));
		} else if(c == '+') {
			r = new SedRelativeLineMatcher(nums(rd));
		} else if(c == '\\') {
			if((d = rd.read()) < 0) {
				throw new SedSyntaxException();
			}
			return getlinere(rd, d);
		} else {
			if(c >= 0)  rd.unread(c);
			r = null;
		}
		return r;
	}

	static long nums(PushbackReader rd) throws IOException {
		long r = 0;
		int c;

		while((c = rd.read()) >= '0' && c <= '9') {
			if((r = r * 10 + (c - '0')) < 0) {
				throw new SedSyntaxException();
			}
		}
		if(r <= 0)  throw new SedSyntaxException();
		if(c >= 0)  rd.unread(c);
		return r;
	}

	static String getstr(PushbackReader rd,
			int dlm) throws IOException {
		StringBuffer b = new StringBuffer();
		boolean esc = false;
		int c;

		while((c = rd.read()) != dlm || esc) {
			if(c < 0) {
				throw new SedSyntaxException();
			} else if(esc) {
				esc = false;
			} else if(!(esc = (c == '\\'))) {
				b.appendCodePoint(c);
			}
		}
		return b.toString();
	}

}
