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

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import net.morilib.sh.ShEnvironment;
import net.morilib.sh.ShFileSystem;
import net.morilib.sh.ShProcess;
import net.morilib.sh.misc.IOs;
import net.morilib.sh.misc.KeywordMatcher;
import net.morilib.sh.misc.RingBuffer;
import net.morilib.unix.misc.OptionIterator;
import net.morilib.unix.regex.BasicPattern;

/**
 *
 *
 * @author MORIGUCHI, Yuichiro 2013/06/01
 */
public class ShGrep implements ShProcess {

	private static abstract class Mt {

		abstract boolean matches(String s);

	}

	private static class MtJ1 extends Mt {

		private Pattern mt;

		MtJ1(String p, boolean ci) {
			mt = Pattern.compile(p, ci ? Pattern.CASE_INSENSITIVE : 0);
		}

		@Override
		boolean matches(String s) {
			return mt.matcher(s).find();
		}

	}

	private static class MtJ2 extends Mt {

		private Pattern mt;

		MtJ2(String p, boolean ci) {
			mt = Pattern.compile(p, ci ? Pattern.CASE_INSENSITIVE : 0);
		}

		@Override
		boolean matches(String s) {
			return mt.matcher(s).matches();
		}

	}

	private static class MtJ3 extends Mt {

		private Pattern mt;

		MtJ3(String p, boolean ci) {
			mt = Pattern.compile(p, ci ? Pattern.CASE_INSENSITIVE : 0);
		}

		@Override
		boolean matches(String s) {
			Matcher m = mt.matcher(s);
			int c;

			return (m.find() &&
					((c = m.start()) == 0 ||
							Character.isWhitespace(s.charAt(--c))) &&
					((c = m.end()) == s.length() ||
							Character.isWhitespace(s.charAt(++c))));
		}

	}

	private static class MtF1 extends Mt {

		private KeywordMatcher m;

		MtF1(String p, boolean ci) {
			String[] a;
			String s;

			s = p.replaceFirst("\n+$", "");
			a = s.split("\n+");
			m = KeywordMatcher.compile(ci, a);
		}

		@Override
		boolean matches(String s) {
			return m.find(s) != null;
		}

	}

	private static class MtF2 extends Mt {

		private KeywordMatcher m;

		MtF2(String p, boolean ci) {
			String[] a;
			String s;

			s = p.replaceFirst("\n+$", "");
			a = s.split("\n+");
			m = KeywordMatcher.compile(ci, a);
		}

		@Override
		boolean matches(String s) {
			return m.matches(s) != null;
		}

	}

	private static class MtF3 extends Mt {

		private KeywordMatcher m;

		MtF3(String p, boolean ci) {
			String[] a;
			String s;

			s = p.replaceFirst("\n+$", "");
			a = s.split("\n+");
			m = KeywordMatcher.compile(ci, a);
		}

		@Override
		boolean matches(String s) {
			return m.findWholeWord(s) != null;
		}

	}

	private static class MtG1 extends Mt {

		private BasicPattern mt;

		MtG1(String p, boolean ci) {
			mt = BasicPattern.compile(
					p, ci ? Pattern.CASE_INSENSITIVE : 0);
		}

		@Override
		boolean matches(String s) {
			return mt.matcher(s).matches();
		}

	}

	private static class MtG2 extends Mt {

		private BasicPattern mt;

		MtG2(String p, boolean ci) {
			mt = BasicPattern.compile(
					p, ci ? Pattern.CASE_INSENSITIVE : 0);
		}

		@Override
		boolean matches(String s) {
			return mt.matcher(s).matchesAll();
		}

	}

	private static class MtG3 extends Mt {

		private BasicPattern mt;

		MtG3(String p, boolean ci) {
			mt = BasicPattern.compile(
					p, ci ? Pattern.CASE_INSENSITIVE : 0);
		}

		@Override
		boolean matches(String s) {
			return mt.matcher(s).matchesWholeWord();
		}

	}

	private static class Flags {
		String pattern;
		int flags = 0;
		int around = 1;
	}

	private static final int RE_JAVA   = 0;
	private static final int RE_BASIC  = 1;
	private static final int RE_EXTEND = 2;
	private static final int RE_FIXED  = 3;
	private static final int RE_MASK   = 3;
	private static final int PRINT_BEFORE = 4;
	private static final int PRINT_AFTER = 8;
	private static final int PRINT_OFFSET = 16;
	private static final int PRINT_LINENO = 32;
	private static final int PRINT_FILENAME = 64;
	private static final int IGNORE_CASE = 128;
	private static final int SUPPRESS_FILENAME = 256;
	private static final int QUIET = 512;
	private static final int SUPPRESS_ERROR = 1024;
	private static final int INVERT_MATCH = 2048;
	private static final int WHOLE_WORD = 4096;
	private static final int WHOLE_LINE = 8192;
	private static final int PRINT_MATCH_LINES = 16384;
	private static final int PRINT_ONLY_FILENAME = 32768;
	private static final String STDIN = "(stdin)";

	private void perror(PrintStream err, Flags f, String n, String s) {
		if((f.flags & SUPPRESS_ERROR) == 0) {
			err.print(n);
			err.print(": ");
			err.println(s);
		}
	}

	private void print(PrintStream out, Flags f, String s) {
		if((f.flags & QUIET) == 0)  out.print(s);
	}

	private void print(PrintStream out, Flags f, int s) {
		if((f.flags & QUIET) == 0)  out.print(s);
	}

	private void println(PrintStream out, Flags f, String s) {
		if((f.flags & QUIET) == 0)  out.println(s);
	}

	private void println(PrintStream out, Flags f, int s) {
		if((f.flags & QUIET) == 0)  out.println(s);
	}

	private void printhead(PrintStream out, Flags f, int n, String s) {
		if((f.flags & PRINT_FILENAME) != 0) {
			print(out, f, s);
			print(out, f, ":");
		}

		if((f.flags & PRINT_LINENO) != 0) {
			print(out, f, n);
			print(out, f, ":");
		}
	}

	Mt _matcher(Flags f) {
		switch(f.flags & (RE_MASK | WHOLE_LINE | WHOLE_WORD)) {
		case RE_JAVA:
			return new MtJ1(f.pattern, (f.flags & IGNORE_CASE) != 0);
		case RE_JAVA | WHOLE_LINE:
		case RE_JAVA | WHOLE_LINE | WHOLE_WORD:
			return new MtJ2(f.pattern, (f.flags & IGNORE_CASE) != 0);
		case RE_JAVA | WHOLE_WORD:
			return new MtJ3(f.pattern, (f.flags & IGNORE_CASE) != 0);
		case RE_BASIC:
			return new MtG1(f.pattern, (f.flags & IGNORE_CASE) != 0);
		case RE_BASIC | WHOLE_LINE:
		case RE_BASIC | WHOLE_LINE | WHOLE_WORD:
			return new MtG2(f.pattern, (f.flags & IGNORE_CASE) != 0);
		case RE_BASIC | WHOLE_WORD:
			return new MtG3(f.pattern, (f.flags & IGNORE_CASE) != 0);
		case RE_EXTEND:
			return new MtJ1(f.pattern, (f.flags & IGNORE_CASE) != 0);
		case RE_EXTEND | WHOLE_LINE:
		case RE_EXTEND | WHOLE_LINE | WHOLE_WORD:
			return new MtJ2(f.pattern, (f.flags & IGNORE_CASE) != 0);
		case RE_EXTEND | WHOLE_WORD:
			return new MtJ3(f.pattern, (f.flags & IGNORE_CASE) != 0);
		case RE_FIXED:
			return new MtF1(f.pattern, (f.flags & IGNORE_CASE) != 0);
		case RE_FIXED | WHOLE_LINE:
		case RE_FIXED | WHOLE_LINE | WHOLE_WORD:
			return new MtF2(f.pattern, (f.flags & IGNORE_CASE) != 0);
		case RE_FIXED | WHOLE_WORD:
			return new MtF3(f.pattern, (f.flags & IGNORE_CASE) != 0);
		default:  throw new RuntimeException();
		}
	}

	boolean _grep(Flags f, InputStream in, PrintStream out,
			String n, Charset cset) throws IOException {
		int a = -1, l = 1, c = 0;
		RingBuffer<String> r;
		Mt m = _matcher(f);
		BufferedReader d;
		boolean b;
		String s;

		d = new BufferedReader(new InputStreamReader(in, cset));
		r = new RingBuffer<String>(
				(f.flags & PRINT_BEFORE) != 0 ? f.around + 1: 1);
		for(; (s = d.readLine()) != null; l++) {
			r.add(s);
			b = m.matches(s);
			if(b == ((f.flags & INVERT_MATCH) == 0)) {
				c++;
				if((f.flags & PRINT_ONLY_FILENAME) != 0) {
					println(out, f, n);
					return true;
				} else if((f.flags & PRINT_MATCH_LINES) == 0 &&
						(f.flags & QUIET) == 0) {
					for(String t : r.toList()) {
						printhead(out, f, l, n);
						println(out, f, t);
					}
				}
				r.clear();
				a = 0;
			} else if(a >= 0 && a < f.around &&
					(f.flags & PRINT_AFTER) != 0) {
				printhead(out, f, l, n);
				println(out, f, s);
				a++;
			} else {
				a = -1;
			}
		}

		if((f.flags & PRINT_MATCH_LINES) != 0) {
			if((f.flags & PRINT_FILENAME) != 0) {
				print(out, f, n);
				print(out, f, ":");
			}
			println(out, f, c);
		}
		return c > 0;
	}

	/* (non-Javadoc)
	 * @see net.morilib.sh.ShProcess#main(net.morilib.sh.ShEnvironment, net.morilib.sh.ShFileSystem, java.io.InputStream, java.io.PrintStream, java.io.PrintStream, java.lang.String[])
	 */
	public int main(ShEnvironment env, ShFileSystem fs, InputStream in,
			PrintStream out, PrintStream err,
			String... args) throws IOException {
		String[] a = new String[args.length - 1];
		Flags f = new Flags();
		InputStream m = null;
		Iterator<String> q;
		OptionIterator o;
		boolean z = false;
		String s;
		Reader r;
		int x;

		System.arraycopy(args, 1, a, 0, a.length);
		o = new OptionIterator("A:B:C:EFGJbchHilnqsvwxe:f:", a);
		while(o.hasNext()) {
			switch(o.nextChar()) {
			case 'A':
				try {
					f.flags |= PRINT_AFTER;
					f.around = Integer.parseInt(o.getArgument());
				} catch(NumberFormatException e) {
					perror(err, f, args[0], "number required");
				}
				break;
			case 'B':
				try {
					f.flags |= PRINT_BEFORE;
					f.around = Integer.parseInt(o.getArgument());
				} catch(NumberFormatException e) {
					perror(err, f, args[0], "number required");
				}
				break;
			case 'C':
				try {
					f.flags |= PRINT_BEFORE | PRINT_AFTER;
					f.around = Integer.parseInt(o.getArgument());
				} catch(NumberFormatException e) {
					perror(err, f, args[0], "number required");
					return 2;
				}
				break;
			case 'E':
				f.flags = (f.flags & ~RE_MASK) | RE_EXTEND;
				break;
			case 'F':
				f.flags = (f.flags & ~RE_MASK) | RE_FIXED;
				break;
			case 'G':
				f.flags = (f.flags & ~RE_MASK) | RE_BASIC;
				break;
			case 'J':
				f.flags = (f.flags & ~RE_MASK) | RE_JAVA;
				break;
			case 'b':  f.flags |= PRINT_OFFSET;  break;
			case 'n':  f.flags |= PRINT_LINENO;  break;
			case 'c':  f.flags |= PRINT_MATCH_LINES;  break;
			case 'e':  f.pattern = o.getArgument();  break;
			case 'f':
				r = new InputStreamReader(
						fs.getFile(o.getArgument()).getInputStream());
				f.pattern = IOs.toString(r);
				f.pattern = f.pattern.replaceFirst("\n+$", "");
				break;
			case 'h':  f.flags |= SUPPRESS_FILENAME;  break;
			case 'H':  f.flags |= PRINT_FILENAME;  break;
			case 'i':  f.flags |= IGNORE_CASE;  break;
			case 'l':  f.flags |= PRINT_ONLY_FILENAME;  break;
			case 'q':  f.flags |= QUIET;  break;
			case 's':  f.flags |= SUPPRESS_ERROR;  break;
			case 'v':  f.flags |= INVERT_MATCH;  break;
			case 'w':  f.flags |= WHOLE_WORD;  break;
			case 'x':  f.flags |= WHOLE_LINE;  break;
			default:
				perror(err, f, args[0], "invalid argument");
				return 2;
			}
		}

		x = o.getIndex();
		q = o.filenameIterator();
		if(f.pattern == null) {
			if(!q.hasNext()) {
				perror(err, f, args[0], "pattern required");
				return 2;
			}
			f.pattern = q.next();
			x++;
		}

		if(a.length - x >= 1 && (f.flags & SUPPRESS_FILENAME) == 0) {
			f.flags |= PRINT_FILENAME;
		}

		if(q.hasNext()) {
			while(q.hasNext()) {
				s = q.next();
				if(s.equals("-")) {
					z = _grep(f, in, out, STDIN, env.getCharset()) | z;
				} else {
					try {
						m = fs.getFile(s).getInputStream();
						z = _grep(f, m, out, s, env.getCharset()) | z;
					} finally {
						if(m != null)  m.close();
						m = null;
					}
				}
			}
		} else {
			z = _grep(f, in, out, STDIN, env.getCharset());
		}
		return z ? 0 : 1;
	}

}
