/*
 * 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.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
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.expr.AwkExpression;
import net.morilib.awk.io.AwkFiles;
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;

/**
 *
 *
 * @author MORIGUCHI, Yuichiro 2013/03/09
 */
public final class Awk {

	private static final Awk _INS = new Awk();

	/**
	 * 
	 */
	public static final String VERSION = "0.0";

	//
	private Awk() {}

	/**
	 * 
	 * @return
	 */
	public static Awk getInstance() {
		return _INS;
	}

	//
	static AwkNamespace initNamespace(String fn, String... args) {
		AwkNamespace r = new AwkRootNamespace();

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

	/**
	 * 
	 * @param namespace
	 * @param source
	 * @return
	 * @throws IOException
	 */
	public AwkProgram compile(AwkNamespace namespace,
			Reader source) throws IOException {
		AwkProgram p;
		AwkLexer l;

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

	/**
	 * 
	 * @param namespace
	 * @param program
	 * @param filename
	 * @param stdin
	 * @param stdout
	 * @param stderr
	 * @param vars
	 * @param args
	 * @return
	 * @throws IOException
	 */
	public AwkValue execute(AwkNamespace namespace,
			AwkProgram program, String filename,
			Reader stdin, Writer stdout, Writer stderr,
			Map<Object, Object> vars,
			String... args) throws IOException {
		BufferedReader rd = new BufferedReader(stdin);
		AwkFiles f = null;
		String s;

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

		try {
			f = new AwkFiles(rd, stdout, stderr);
			program.executeBegin(namespace, f);
			stdout.flush();  stderr.flush();
			if(!program.isExecuteOnce()) {
				while((s = rd.readLine()) != 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();
		}
	}

	/**
	 * 
	 * @param source
	 * @param filename
	 * @param stdin
	 * @param stdout
	 * @param stderr
	 * @param vars
	 * @param args
	 * @return
	 * @throws IOException
	 */
	public AwkValue execute(Reader source, String filename,
			Reader stdin, Writer stdout, Writer stderr,
			Map<Object, Object> vars,
			String... args) throws IOException {
		AwkNamespace r = initNamespace(filename, args);
		AwkProgram p;

		p = compile(r, source);
		return execute(r, p, filename, stdin, stdout, stderr, vars,
				args);
	}

	/**
	 * 
	 * @param source
	 * @param inputFile
	 * @param outputFile
	 * @param stderr
	 * @param inputEncoding
	 * @param outputEncoding
	 * @param args
	 * @return
	 * @throws IOException
	 */
	public AwkValue execute(Reader source,
			File inputFile,  File outputFile, Writer stderr,
			Charset inputEncoding, Charset outputEncoding,
			Map<Object, Object> vars,
			String... args) throws IOException {
		Writer stdout = null;
		Reader stdin = null;

		try {
			if(inputEncoding != null) {
				stdin = new InputStreamReader(
						new FileInputStream(inputFile),
						inputEncoding);
			} else {
				stdin = new InputStreamReader(
						new FileInputStream(inputFile));
			}

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

	/**
	 * 
	 * @param source
	 * @param inputFile
	 * @param outputFile
	 * @param stderr
	 * @param inputEncoding
	 * @param outputEncoding
	 * @param args
	 * @return
	 * @throws IOException
	 */
	public AwkValue execute(String source,
			File inputFile, File outputFile, Writer stderr,
			Charset inputEncoding, Charset outputEncoding,
			Map<Object, Object> vars,
			String... args) throws IOException {
		StringReader srd = new StringReader(source);

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

	/**
	 * 
	 * @param source
	 * @param inputFile
	 * @param outputFile
	 * @param stderr
	 * @param inputEncoding
	 * @param outputEncoding
	 * @param args
	 * @return
	 * @throws IOException
	 */
	public AwkValue overwrite(Reader source,
			File file, Writer stderr,
			Charset inputEncoding, Charset outputEncoding,
			Map<Object, Object> vars,
			String... args) throws IOException {
		File tf = File.createTempFile("awkium", ".tmp");
		BufferedOutputStream ous = null;
		BufferedInputStream ins = null;
		byte[] b = new byte[4096];
		Writer stdout = null;
		Reader stdin = null;
		AwkValue v;
		int l;

		try {
			if(inputEncoding != null) {
				stdin = new InputStreamReader(
						new FileInputStream(file), inputEncoding);
			} else {
				stdin = new InputStreamReader(
						new FileInputStream(file));
			}

			if(outputEncoding != null) {
				stdout = new OutputStreamWriter(
						new FileOutputStream(tf), outputEncoding);
			} else {
				stdout = new OutputStreamWriter(
						new FileOutputStream(tf));
			}
			v = execute(source, file.toString(), stdin,
					stdout, stderr, vars, args);
		} finally {
			if(stdin  != null)  stdin.close();
			if(stdout != null) {
				stdout.flush();
				stdout.close();
			}
		}

		try {
			ins = new BufferedInputStream(new FileInputStream(tf));
			ous = new BufferedOutputStream(
					new FileOutputStream(file));
			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();
		}
	}

	/**
	 * 
	 * @param source
	 * @param file
	 * @param stderr
	 * @param inputEncoding
	 * @param outputEncoding
	 * @param args
	 * @return
	 * @throws IOException
	 */
	public AwkValue overwrite(String source,
			File file, Writer stderr,
			Charset inputEncoding, Charset outputEncoding,
			Map<Object, Object> vars,
			String... args) throws IOException {
		StringReader srd = new StringReader(source);

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

	/**
	 * 
	 */
	public void usage() {
		System.err.print  ("awkium Ver. ");
		System.err.println(VERSION);
		System.err.println("usage:");
		System.err.println("awk [option] -f program [files ...]");
		System.err.println("awk [option] script [files ...]");
		System.err.println("option:");
		System.err.println("-F file-separator");
	}

	/**
	 * 
	 * @param stdin
	 * @param stdout
	 * @param stderr
	 * @param args
	 * @return
	 * @throws IOException
	 */
	public int invoke(Reader stdin, Writer stdout,
			Writer stderr, String... args) throws IOException {
		AwkNamespace ns;
		List<String> a = new ArrayList<String>();
		InputStream ins = null;
		Map<Object, Object> v;
		AwkValue z = null;
		Reader r = null;
		AwkProgram p;
		int x, i = 1;
		String[] a2;
		String s;

		v = new HashMap<Object, Object>();
		for(; i < args.length; i++) {
			s = args[i];
			if(s.equals("-f") && i + 1 < args.length) {
				s = args[++i];
				r = new InputStreamReader(new FileInputStream(s));
			} 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(r == null) {
				r = new StringReader(s);
			} else {
				break;
			}
		}

		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 = initNamespace("<stdin>", a2);
		if(r == null) {
			usage();
			return 2;
		} else {
			p = compile(ns, r);
			for(int j = 1; j < a.size(); j++) {
				s = a.get(j);
				if((x = s.indexOf('=')) > 0) {
					v.put(s.substring(0, x), s.substring(x + 1));
				} else {
					try {
						ns.assign("FILENAME", AwkString.valueOf(s));
						ins = new FileInputStream(s);
						z = execute(ns, p, s,
								new InputStreamReader(ins),
								stdout,
								stderr,
								v,
								a2);
					} finally {
						if(ins != null)  ins.close();
						ins = null;
					}
				}
			}

			if(z == null) {
				z = execute(ns, p, "<stdin>",
						stdin,
						stdout,
						stderr,
						v,
						a2);
			}
		}
		return z.toInteger().intValue();
	}

	/**
	 * 
	 */
	public void repl() {
		AwkNamespace r = initNamespace("<stdin>");
		AwkValue v = AwkUndefined.UNDEF;
		AwkFiles f = null;
		AwkExpression x;
		AwkLexer l;
		Writer so, se;
		Reader rd;

		try {
			rd = new InputStreamReader(System.in);
			so = new OutputStreamWriter(System.out);
			se = new OutputStreamWriter(System.err);
			l  = new AwkLexer("awk>", "  >>",
					new InputStreamReader(System.in));
			f  = new AwkFiles(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();
		}
	}

}
