/*
 * Copyright 2009-2010 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;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.morilib.awk.builtin.AwkBuiltInLoader;
import net.morilib.awk.code.AwkProgram;
import net.morilib.awk.encoding.EncodingDetector;
import net.morilib.awk.encoding.EncodingDetectorFactory;
import net.morilib.awk.expr.AwkExpression;
import net.morilib.awk.io.AwkFiles;
import net.morilib.awk.misc.FilesReader;
import net.morilib.awk.misc.RecordInputStream;
import net.morilib.awk.namespace.AwkNamespace;
import net.morilib.awk.namespace.AwkRootNamespace;
import net.morilib.awk.parser.AwkLexer;
import net.morilib.awk.parser.AwkParser;
import net.morilib.awk.value.AwkArray;
import net.morilib.awk.value.AwkInteger;
import net.morilib.awk.value.AwkString;
import net.morilib.awk.value.AwkUndefined;
import net.morilib.awk.value.AwkValue;
import net.morilib.c.pre.CPreprocessorReader;
import net.morilib.sh.ShEnvironment;
import net.morilib.sh.ShFile;
import net.morilib.sh.ShFileSystem;
import net.morilib.sh.ShProcess;

/**
 * awkiumのFacadeです。
 * 
 * @author MORIGUCHI, Yuichiro 2013/03
 */
public final class Awk implements ShProcess {

	private static final Awk _INS = new Awk();
	private static final String FILENOTFOUND =
		"fatal: script not found: ";

//	private static final String[] AWKPATH_UNIX = new String[] {
//		".", "/usr/local/share/awk"
//	};

	//	private static final String[] AWKPATH_WINDOWS = new String[] {
//		"."
//	};

	//	private static final String[] AWKPATH_OTHERS = new String[] {
//		"."
//	};

	/**
	 * awkiumのバージョンです。
	 */
	public static final String VERSION = "0.2.0";

	//
//	private transient String[] awkpath = null;

	//
	private Awk() {}

	/**
	 * awkiumのFacadeを取得します。
	 * 
	 * @return awkiumのFacade
	 */
	public static Awk getInstance() {
		return _INS;
	}

	//
	private static void initvar(AwkNamespace r,
			Map<Object, Object> vars) {
		String s;

		if(vars != null) {
			for(Map.Entry<Object, Object> o : vars.entrySet()) {
				s = o.getValue().toString();
				s = AwkLexer.escape(s);
				r.assign(o.getKey().toString(), AwkString.valueOf(s));
			}
		}
	}

	/**
	 * 名前空間を生成します。
	 * 
	 * @param filename ファイル名
	 * @param args     引数
	 * @return 名前空間
	 */
	public static AwkNamespace newNamespace(String filename,
			String... args) {
		AwkNamespace r = new AwkRootNamespace();
		AwkValue a;
		String s;

		AwkBuiltInLoader.load(r);
		r.assign("FILENAME", AwkString.valueOf(filename));
		r.assign("ARGC", AwkInteger.valueOf(args.length));
		r.assign("ARGV", new AwkArray(0, args));

		// environment
		a = new AwkArray();
		for(Map.Entry<String, String> e : System.getenv().entrySet()) {
			a.putArray(e.getKey(), AwkString.valueOf(e.getValue()));
		}
		r.assign("ENVIRON", a);

		// properties
		a = new AwkArray();
		for(Object o : System.getProperties().keySet()) {
			s = o.toString();
			a.putArray(s, AwkString.valueOf(System.getProperty(s)));
		}
		r.assign("PROPERTIES", a);
		return r;
	}

	/**
	 * awkiumプログラムをコンパイルします。
	 * 
	 * @param namespace 名前空間
	 * @param source ソースのReader
	 * @return コンパイル結果の中間表現
	 * @throws IOException IOエラー
	 */
	public AwkProgram compile(AwkNamespace namespace,
			Reader source) throws IOException {
		AwkProgram p;
		AwkLexer l;

		l = new AwkLexer(source);
		p = AwkParser.parse(namespace, l);
		return p;
	}

	/**
	 * awkiumプログラムを実行します。
	 * 
	 * @param namespace 名前空間
	 * @param f ファイルオブジェクト
	 * @param program コンパイルされたawkiumプログラム
	 * @param filename 標準入力のファイル名
	 * @param stdin  標準入力
	 * @param stdout 標準出力
	 * @param stderr 標準エラー出力
	 * @param inputEncoding 入力エンコード
	 * @param args   引数
	 * @return 結果
	 * @throws IOException IOエラー
	 */
	@SuppressWarnings("resource")
	public AwkValue execute(AwkNamespace namespace,
			AwkFiles f, AwkProgram program, String filename,
			InputStream stdin, Writer stdout, Writer stderr,
			Charset inputEncoding, String... args) throws IOException {
		RecordInputStream rd;
		String s;

		rd = new RecordInputStream(stdin, inputEncoding, namespace);
		try {
//			f = new AwkFiles(rd, stdout, stderr);
//			program.executeBegin(namespace, f);
//			stdout.flush();  stderr.flush();
			if(!program.isExecuteOnce()) {
				while((s = rd.readRecord()) != null) {
					try {
						program.execute(namespace, f, s);
					} catch(AwkNextException e) {
						// ignore
					} catch(AwkExitException e) {
						program.executeEnd(namespace, f);
						return e.getValue();
					} finally {
						stdout.flush();  stderr.flush();
					}
				}
			}
//			program.executeEnd(namespace, f);
//			stdout.flush();  stderr.flush();
			return AwkInteger.ZERO;
		} finally {
			if(f != null)  f.closeAll();
		}
	}

	/**
	 * awkiumプログラムを実行します。
	 * 
	 * @param source ソースのReader
	 * @param filename 標準入力のファイル名
	 * @param stdin  標準入力
	 * @param stdout 標準出力
	 * @param stderr 標準エラー出力
	 * @param inputEncoding 入力エンコード
	 * @param vars   事前定義された変数
	 * @param args   引数
	 * @return 結果
	 * @throws IOException IOエラー
	 */
	public AwkValue execute(ShFileSystem filesystem,
			Reader source, String filename,
			InputStream stdin, Writer stdout, Writer stderr,
			Charset inputEncoding, Map<Object, Object> vars,
			String... args) throws IOException {
		AwkNamespace r = newNamespace(filename, args);
		AwkFiles f = new AwkFiles(filesystem, stdin, stdout, stderr);
		AwkProgram p;
		AwkValue rt;

		// initialize variables
		initvar(r, vars);

		// execute
		p = compile(r, source);
		p.executeBegin(r, f);
		if(p.isExecuteOnce())  return AwkInteger.ZERO;
		rt = execute(r, f, p, filename, stdin, stdout, stderr,
				inputEncoding, args);
		p.executeEnd(r, f);
		stdout.flush();  stderr.flush();
		return rt;
	}

	/**
	 * awkiumプログラムを実行します。
	 * 
	 * @param source ソースのReader
	 * @param inputFile  標準入力ファイル名
	 * @param outputFile 標準出力ファイル名
	 * @param stderr     標準エラー出力
	 * @param inputEncoding  入力の文字コード名
	 * @param outputEncoding 出力の文字コード名
	 * @param vars   事前定義された変数
	 * @param args   引数
	 * @return 結果
	 * @throws IOException IOエラー
	 */
	public AwkValue execute(ShFileSystem filesystem,
			Reader source,
			ShFile inputFile,  ShFile outputFile, Writer stderr,
			Charset inputEncoding, Charset outputEncoding,
			Map<Object, Object> vars,
			String... args) throws IOException {
		Writer stdout = null;
		InputStream stdin = null;

		try {
			stdin = inputFile.getInputStream();
			if(outputEncoding != null) {
				stdout = new OutputStreamWriter(
						outputFile.getPrintStream(false),
						outputEncoding);
			} else {
				stdout = new OutputStreamWriter(
						outputFile.getPrintStream(false));
			}
			return execute(filesystem, source, inputFile.toString(),
					stdin, stdout, stderr, inputEncoding, vars, args);
		} finally {
			if(stdin  != null)  stdin.close();
			if(stdout != null) {
				stdout.flush();
				stdout.close();
			}
		}
	}

	/**
	 * awkiumプログラムを実行します。
	 * 
	 * @param source ソース
	 * @param stdin  標準入力
	 * @param stdout 標準出力
	 * @param stderr 標準エラー出力
	 * @param inputEncoding 入力エンコード
	 * @param vars   事前定義された変数
	 * @param args   引数
	 * @return 結果
	 * @throws IOException IOエラー
	 */
	public AwkValue execute(ShFileSystem filesystem, String source,
			InputStream stdin, Writer stdout, Writer stderr,
			Charset inputEncoding, Map<Object, Object> vars,
			String... args) throws IOException {
		StringReader srd = new StringReader(source);

		return execute(filesystem, srd, "<no name>", stdin, stdout,
				stderr, inputEncoding, vars, args);
	}

	/**
	 * awkiumプログラムを実行します。
	 * 
	 * @param source ソース
	 * @param stdin  標準入力
	 * @param stdout 標準出力
	 * @param stderr 標準エラー出力
	 * @param inputEncoding 入力エンコード
	 * @param vars   事前定義された変数
	 * @param args   引数
	 * @return 結果
	 * @throws IOException IOエラー
	 */
	public AwkValue execute(ShFileSystem filesystem, String source,
			InputStream stdin, OutputStream stdout,
			OutputStream stderr,
			Charset inputEncoding, Map<Object, Object> vars,
			String... args) throws IOException {
		StringReader srd = new StringReader(source);

		return execute(filesystem, srd, "<no name>",
				stdin,
				new OutputStreamWriter(stdout),
				new OutputStreamWriter(stderr),
				inputEncoding, vars, args);
	}

	/**
	 * awkiumプログラムを実行します。
	 * 
	 * @param source ソース
	 * @param stdin  標準入力
	 * @param stdout 標準出力
	 * @param stderr 標準エラー出力
	 * @param inputEncoding 入力エンコード
	 * @return 結果
	 * @throws IOException IOエラー
	 */
	public AwkValue execute(ShFileSystem filesystem, String source,
			InputStream stdin, Writer stdout,
			Writer stderr, Charset inputEncoding) throws IOException {
		return execute(filesystem, source, stdin, stdout, stderr,
				inputEncoding, new HashMap<Object, Object>());
	}

	/**
	 * awkiumプログラムを実行します。
	 * 
	 * @param source ソース
	 * @param stdin  標準入力
	 * @param stdout 標準出力
	 * @param stderr 標準エラー出力
	 * @param inputEncoding 入力エンコード
	 * @return 結果
	 * @throws IOException IOエラー
	 */
	public AwkValue execute(ShFileSystem filesystem, String source,
			InputStream stdin, OutputStream stdout,
			OutputStream stderr,
			Charset inputEncoding) throws IOException {
		return execute(filesystem, source, stdin, stdout, stderr,
				inputEncoding, new HashMap<Object, Object>());
	}

	/**
	 * awkiumプログラムを実行します。
	 * 
	 * @param source ソース
	 * @param stdin  標準入力
	 * @param stdout 標準出力
	 * @param stderr 標準エラー出力
	 * @return 結果
	 * @throws IOException IOエラー
	 */
	public AwkValue execute(ShFileSystem filesystem, String source,
			InputStream stdin, OutputStream stdout,
			OutputStream stderr) throws IOException {
		return execute(filesystem, source, stdin, stdout, stderr,
				Charset.defaultCharset(),
				new HashMap<Object, Object>());
	}

	/**
	 * awkiumプログラムを実行します。
	 * 
	 * @param source ソース
	 * @param inputFile  標準入力ファイル名
	 * @param outputFile 標準出力ファイル名
	 * @param stderr     標準エラー出力
	 * @param inputEncoding  入力の文字コード名
	 * @param outputEncoding 出力の文字コード名
	 * @param vars   事前定義された変数
	 * @param args   引数
	 * @return 結果
	 * @throws IOException IOエラー
	 */
	public AwkValue execute(ShFileSystem filesystem, String source,
			ShFile inputFile, ShFile outputFile, Writer stderr,
			Charset inputEncoding, Charset outputEncoding,
			Map<Object, Object> vars,
			String... args) throws IOException {
		StringReader srd = new StringReader(source);

		return execute(filesystem, srd, inputFile, outputFile, stderr,
				inputEncoding, outputEncoding, vars, args);
	}

	/**
	 * awkiumプログラムをファイル上書きで実行します。<br />
	 * fileの内容は上書きされます。
	 * 
	 * @param source ソースのReader
	 * @param file       標準入力および標準出力ファイル名
	 * @param stderr     標準エラー出力
	 * @param inputEncoding  入力の文字コード名
	 * @param outputEncoding 出力の文字コード名
	 * @param vars   事前定義された変数
	 * @param args   引数
	 * @return 結果
	 * @throws IOException IOエラー
	 */
	public AwkValue overwrite(ShFileSystem filesystem, Reader source,
			ShFile file, Writer stderr,
			Charset inputEncoding, Charset outputEncoding,
			Map<Object, Object> vars,
			String... args) throws IOException {
		AwkNamespace ns;
		java.io.File tf =
				java.io.File.createTempFile("awkium", ".tmp");
		BufferedOutputStream ous = null;
		BufferedInputStream ins = null;
		byte[] b = new byte[4096];
		Writer stdout = null;
		InputStream stdin = null;
		AwkProgram p;
		AwkValue v;
		AwkFiles fs;
		int l;

		try {
			stdin = file.getInputStream();
			if(outputEncoding != null) {
				stdout = new OutputStreamWriter(
						new java.io.FileOutputStream(tf),
						outputEncoding);
			} else {
				stdout = new OutputStreamWriter(
						new java.io.FileOutputStream(tf));
			}

			// initialize
			ns = newNamespace("<stdin>");
			p  = compile(ns, source);
			fs = new AwkFiles(filesystem, stdin, stdout, stderr);

			// initialize variables
			initvar(ns, vars);

			// execute BEGIN action
			p.executeBegin(ns, fs);
			stdout.flush();  stderr.flush();
			if(p.isExecuteOnce())  return AwkInteger.ZERO;

			// execute each files
			v = execute(ns, fs, p, file.toString(), stdin,
					stdout, stderr, inputEncoding, args);

			// execute END action
			p.executeEnd(ns, fs);
			stdout.flush();  stderr.flush();
		} finally {
			if(stdin  != null)  stdin.close();
			if(stdout != null) {
				stdout.flush();
				stdout.close();
			}
		}

		try {
			ins = new BufferedInputStream(
					new java.io.FileInputStream(tf));
			ous = new BufferedOutputStream(
					file.getPrintStream(false));
			while((l = ins.read(b)) >= 0)  ous.write(b, 0, l);
			return v;
		} finally {
			if(ins != null)  ins.close();
			if(ous != null) {
				ous.flush();
				ous.close();
			}
			tf.delete();
		}
	}

	/**
	 * awkiumプログラムをファイル上書きで実行します。<br />
	 * fileの内容は上書きされます。
	 * 
	 * @param source ソース
	 * @param file       標準入力および標準出力ファイル名
	 * @param stderr     標準エラー出力
	 * @param inputEncoding  入力の文字コード名
	 * @param outputEncoding 出力の文字コード名
	 * @param vars   事前定義された変数
	 * @param args   引数
	 * @return 結果
	 * @throws IOException IOエラー
	 */
	public AwkValue overwrite(ShFileSystem filesystem, String source,
			ShFile file, Writer stderr,
			Charset inputEncoding, Charset outputEncoding,
			Map<Object, Object> vars,
			String... args) throws IOException {
		StringReader srd = new StringReader(source);

		return overwrite(filesystem, srd, file, stderr, inputEncoding,
				outputEncoding, vars, args);
	}

	void setawkpath() {
//		String s, d;

//		s = System.getenv("AWKPATH");
//		d = System.getProperty("path.separator");
//		if(s != null) {
//			awkpath = s.split(d);
//		} else if(OSInfo.OS.isUNIX()) {
//			awkpath = AWKPATH_UNIX;
//		} else if(OSInfo.OS.isWindows()) {
//			awkpath = AWKPATH_WINDOWS;
//		} else {
//			awkpath = AWKPATH_OTHERS;
//		}
	}

	ShFile searchAwkPath(String s) {
//		File f;
//
//		if(awkpath == null)  setawkpath();
//		if((f = new File(s)).isAbsolute() && f.isFile()) {
//			return new File(s);
//		} else {
//			for(int i = 0; i < awkpath.length; i++) {
//				f = new File(awkpath[i], s);
//				if(f.isFile())  return f;
//			}
//		}
//		return null;
		return null;
	}

	/**
	 * 使用法を出力します。
	 */
	public void usage() {
		System.err.print  ("awkium Ver. ");
		System.err.println(VERSION);
		System.err.println("usage:");
		System.err.println("awk [option] script [files ...]");
		System.err.println("awk [option] [files ...]");
		System.err.println("awk");
		System.err.println("awk --repl");
		System.err.println("");
		System.err.println("option:");
		System.err.println("-F file-separator");
		System.err.println("-f script-file");
		System.err.println("-v variable=value");
		System.err.println("--source script");
		System.err.println("--version");
		System.err.println("--help (--usage)");
		System.err.println("--with-cpp");
	}

	/**
	 * REPL(Read-Eval-Print-Loop)を起動します。
	 */
	public void repl(ShFileSystem filesystem) {
		AwkNamespace r = newNamespace("<stdin>");
		AwkValue v = AwkUndefined.UNDEF;
		AwkFiles f = null;
		AwkExpression x;
		AwkLexer l;
		Writer so, se;
		InputStream rd;

		try {
			r.setField("");
			rd = System.in;
			so = new OutputStreamWriter(System.out);
			se = new OutputStreamWriter(System.err);
			l  = new AwkLexer("awk>", "  >>",
					new InputStreamReader(System.in));
			f  = new AwkFiles(filesystem, rd, so, se);
			for(;;) {
				try {
					v = AwkUndefined.UNDEF;
					x = AwkParser.parseStatement(l);
					x = x.compileInternally();
					v = x.eval(r, f);
				} catch(AwkNextException e) {
					// do nothing
				} catch(AwkCompilationException e) {
					System.err.print  ("Error: ");
					System.err.println(e.getMessage());
				} catch(AwkException e) {
					System.err.print  ("Error: ");
					System.err.println(e.getMessage());
				} finally {
					so.flush();  se.flush();
					System.out.println(v.toString());
					l.resetPrompt();
				}
			}
		} catch(AwkExitException e) {
			System.exit(e.getValue().toInteger().intValue());
		} catch (IOException e) {
			e.printStackTrace(System.err);
			System.exit(2);
		} finally {
			if(f != null)  f.closeAll();
		}
	}

	/**
	 * 引数を解析し、awkiumを実行します。
	 * 
	 * @param stdin  標準入力
	 * @param stdout 標準出力
	 * @param stderr 標準エラー出力
	 * @param args   引数
	 * @return 終了コード
	 * @throws IOException IOエラー
	 */
	public int invoke(ShFileSystem filesystem,
			InputStream stdin, Writer stdout,
			Writer stderr, String... args) throws IOException {
		AwkNamespace ns;
		AwkFiles fs = new AwkFiles(filesystem, stdin, stdout, stderr);
		List<String> a = new ArrayList<String>();
		List<Object> f = new ArrayList<Object>();
		String s, pfn = "<command line>";
		InputStream ins = null;
		Map<Object, Object> v;
		EncodingDetector en;
		boolean pre = false;
		AwkValue z = null;
		Reader r = null;
		AwkProgram p;
		int x, i = 1, k;
		String[] a2;
		Charset ch;
		ShFile d;

		v = new HashMap<Object, Object>();
		for(; i < args.length; i++) {
			s = args[i];
			if(s.equals("--with-cpp")) {
				pre = true;
			} else if(s.equals("-f") && i + 1 < args.length) {
				s = pfn = args[++i];
				if((d = searchAwkPath(s)) == null) {
					System.out.println(FILENOTFOUND + pfn);
					return 2;
				}
				f.add(d);
			} else if(s.equals("-F") && i + 1 < args.length) {
				v.put("FS", args[++i]);
			} else if(s.startsWith("-F")) {
				v.put("FS", s.substring(2));
			} else if(s.equals("-v") && i + 1 < args.length) {
				s = args[++i];
				k = s.indexOf('=');
				if(k >= 0) {
					v.put(s.substring(0, k), s.substring(k + 1));
				}
			} else if(s.equals("--source") && i + 1 < args.length) {
				s = args[++i];
				f.add(s);
			} else if(s.equals("--version")) {
				System.err.println("awkium Version " + VERSION);
				return 1;
			} else if(s.equals("--help") || s.equals("--usage")) {
				usage();
				return 1;
			} else if(r == null) {
				r = new StringReader(s);
			} else {
				break;
			}
		}

		if(f.size() > 0)  r = FilesReader.newFiles(f);
		if(pre)  r = new CPreprocessorReader(pfn, r);
		if(args.length == 0)  throw new IllegalArgumentException();
		a.add(args[0]);
		for(; i < args.length; i++)  a.add(args[i]);

		a2 = a.toArray(new String[0]);
		ns = newNamespace("<stdin>", a2);
		initvar(ns, v);

		en = EncodingDetectorFactory.getInstance();
		if(r == null) {
			usage();
			return 2;
		} else {
			p = compile(ns, r);

			// execute BEGIN action
			p.executeBegin(ns, fs);
			stdout.flush();  stderr.flush();
			if(p.isExecuteOnce())  return 0;

			// execute each files
			for(int j = 1; j < a.size(); j++) {
				s = a.get(j);
				if((x = s.indexOf('=')) > 0) {
					ns.assign(s.substring(0, x),
							AwkString.valueOf(s.substring(x + 1)));
				} else {
					try {
						ns.assign("FILENAME", AwkString.valueOf(s));
						ns.assign("ARGIND", AwkInteger.valueOf(j));
						ch  = en.detect(filesystem.getFile(s));
						ins = filesystem.getFile(s).getInputStream();
						z   = execute(ns, fs, p, s, ins, stdout,
								stderr, ch, a2);
					} finally {
						if(ins != null)  ins.close();
						ins = null;
					}
				}
			}

			if(z == null) {
				z = execute(ns, fs, p, "<stdin>", stdin, stdout,
						stderr, Charset.defaultCharset(), a2);
			}

			// execute END action
			p.executeEnd(ns, fs);
			stdout.flush();  stderr.flush();
		}
		return z.toInteger().intValue();
	}

	/* (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 {
		return invoke(fs, in, new OutputStreamWriter(out),
				new OutputStreamWriter(err), args);
	}

}
