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

import java.io.IOException;
import java.io.InputStream;
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.nano.builtin.AwkBuiltInLoader;
import net.morilib.awk.nano.io.AwkFiles;
import net.morilib.awk.nano.io.RecordReader;
import net.morilib.awk.nano.misc.FilesReader;
import net.morilib.awk.nano.misc.RecordInputStream;
import net.morilib.awk.nano.namespace.AwkNamespace;
import net.morilib.awk.nano.namespace.AwkRootNamespace;
import net.morilib.awk.nano.parser.AwkLexer;
import net.morilib.awk.nano.parser.AwkParser;
import net.morilib.awk.nano.statement.AwkProgram;
import net.morilib.awk.nano.value.AwkArray;
import net.morilib.awk.nano.value.AwkInteger;
import net.morilib.awk.nano.value.AwkString;
import net.morilib.awk.nano.value.AwkValue;
import net.morilib.c.pre.CPreprocessorReader;
import net.morilib.sh.ShFile;
import net.morilib.sh.ShFileGetter;
import net.morilib.sh.ShFileSystem;

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

	private static final Awk _INS = new Awk();

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

	//
	private Awk() {}

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

	/**
	 * 名前空間を生成します。
	 * 
	 * @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 {
			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();
					}
				}
			}
			return AwkInteger.ZERO;
		} finally {
			if(f != null)  f.closeAll();
		}
	}

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

		try {
			if(!program.isExecuteOnce()) {
				while((s = rd.readRecord(rs)) != null) {
					try {
						program.execute(namespace, f, s);
					} catch(AwkNextException e) {
						// ignore
					} catch(AwkExitException e) {
						program.executeEnd(namespace, f);
						return e.getValue();
					} finally {
						f.getStdout().flush();
						f.getStderr().flush();
					}
				}
			}
			return AwkInteger.ZERO;
		} finally {
			if(f != null)  f.closeAll();
		}
	}

	//
	private ShFile searchAwkPath(ShFileGetter g, ShFileSystem fs,
			String s) {
		if(g.get(fs, s).isFile()) {
			return g.get(fs, s);
		} else {
			return null;
		}
	}

	//
	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));
			}
		}
	}

	/**
	 * 引数を解析し、awkiumを実行します。
	 * 
	 * @param stdin  標準入力
	 * @param stdout 標準出力
	 * @param stderr 標準エラー出力
	 * @param args   引数
	 * @return 終了コード
	 * @throws IOException IOエラー
	 */
	public int invoke(ShFileSystem filesystem, ShFileGetter get,
			InputStream stdin, Writer stdout,
			Writer stderr, String... args) throws IOException {
		AwkNamespace ns;
		AwkFiles fs = new AwkFiles(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(get, filesystem, s)) == null) {
					throw new RuntimeException();
				}
				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(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) {
			throw new RuntimeException();
		} 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(get.get(filesystem, s));
						ch  = Charset.defaultCharset();
						ins = get.get(filesystem, 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();
	}

}
