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

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.io.Reader;
import java.text.SimpleDateFormat;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

import net.morilib.automata.DFA;
import net.morilib.automata.DFAState;
import net.morilib.automata.dfa.ConvertedDFA;
import net.morilib.automata.dfa.ConvertedRangeDFA;
import net.morilib.automata.dfa.MinimizedRangeDFA;
import net.morilib.nina.DFABuilder;
import net.morilib.nina.NFABuilder;
import net.morilib.nina.Nina;
import net.morilib.nina.NinaAction;
import net.morilib.nina.NinaNFA;
import net.morilib.nina.NinaParser;
import net.morilib.nina.Quadro;
import net.morilib.nina.cmd.NinatOptions;
import net.morilib.nina.metalang.NinaMetaLanguage;
import net.morilib.nina.translate.sh.ShNinatBuiltInCommands;
import net.morilib.nina.translate.sh.ShNinatFileSystem;
import net.morilib.sh.DefaultShRuntime;
import net.morilib.sh.ShEnvironment;
import net.morilib.sh.ShFacade;
import net.morilib.sh.ShFileSystem;
import net.morilib.sh.ShProcess;
import net.morilib.sh.ShRootEnvironment;
import net.morilib.sh.ShRuntime;
import net.morilib.sh.ShSyntaxException;
import net.morilib.sh.misc.XtraceStream;

/**
 *
 *
 * @author MORIGUCHI, Yuichiro 2013/10/23
 */
public abstract class AbstractNinaTranslator
implements NinaTranslator {

	//
	private static final SimpleDateFormat YEAR =
			new SimpleDateFormat("yyyy");

	/**
	 * 
	 */
	protected DFA<Object, ?, Void> dfa;

	/**
	 * 
	 */
	protected Quadro quadro;

	/**
	 * 
	 */
	protected NinaAction builder;

	/**
	 * 
	 */
	protected NinatOptions options;

	/**
	 * 
	 */
	protected AbstractNinaTranslator parent;

	//
	private int stateNo = 0;
	private Map<DFAState<Object, ?, Void>, Integer> states =
			new IdentityHashMap
			<DFAState<Object, ?, Void>, Integer>();
	private Set<Integer> accepts = new HashSet<Integer>();
	private Stack<DFAState<Object, ?, Void>> trz =
			new Stack<DFAState<Object, ?, Void>>();

	//
	private Map<String, AbstractNinaTranslator> subfas;
	private String fragment;
	private String machine;
	private String mainname;
	private String name;

	//
	NinaMetaLanguage meta;

	/**
	 * 
	 * @param lang
	 * @return
	 */
	public static AbstractNinaTranslator getTranslator(String lang) {
		Class<?> c;
		String p;

		try {
			p = AbstractNinaTranslator.class.getPackage().getName();
			c = Class.forName(p + ".NinaTranslator" + lang);
			return (AbstractNinaTranslator)c.newInstance();
		} catch(ClassNotFoundException e) {
			return null;
		} catch(InstantiationException e) {
			throw new RuntimeException(e);
		} catch(IllegalAccessException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * 
	 * @param state
	 * @return
	 */
	protected int getStateNo(DFAState<Object, ?, Void> state) {
		if(states.containsKey(state)) {
			return states.get(state);
		} else {
			states.put(state, stateNo);
			if(state.isAccepted())  accepts.add(stateNo);
			trz.push(state);
			return stateNo++;
		}
	}

	/**
	 * 
	 * @return
	 */
	protected boolean isStackEmpty() {
		return trz.isEmpty();
	}

	/**
	 * 
	 * @return
	 */
	protected DFAState<Object, ?, Void> popStack() {
		return trz.pop();
	}

	/**
	 * 
	 * @return
	 */
	protected int acceptsSize() {
		return accepts.size();
	}

	/**
	 * 
	 * @return
	 */
	protected Iterable<Integer> acceptsIterable() {
		return accepts;
	}

	/**
	 * 
	 * @param state
	 * @return
	 */
	protected boolean containsState(Object state) {
		return states.containsKey(state);
	}

	/**
	 * 
	 * @param state
	 * @return
	 */
	protected Integer stateNo(Object state) {
		return states.get(state);
	}

	/**
	 * 
	 * @return
	 */
	protected Set<DFAState<Object, ?, Void>> stateKeys() {
		return states.keySet();
	}

	/**
	 * 
	 * @return
	 */
	protected int stateSize() {
		return states.size();
	}

	/**
	 * 
	 * @return
	 */
	protected NinatOptions getOptions() {
		return options;
	}

	/**
	 * 
	 * @return
	 */
	protected String getMachine() {
		return machine;
	}

	/**
	 * 
	 * @return
	 */
	protected String getName() {
		return name;
	}

	/**
	 * 
	 * @param s
	 * @return
	 */
	protected Object getAutomaton(String s) {
		return subfas.get(s);
	}

	/**
	 * 
	 * @return
	 */
	protected Map<String, ?> getAutomata() {
		return subfas;
	}

	/**
	 * 
	 * @param out
	 */
	public abstract void printStates(PrintStream out);

	/**
	 * 
	 * @param out
	 */
	public abstract void printObjectStates(PrintStream out);

	/**
	 * 
	 * @param out
	 */
	public abstract void printClassStates(PrintStream out);

	/**
	 * 
	 * @param out
	 */
	public abstract void printAcceptStates(PrintStream out);

	/**
	 * 
	 * @param out
	 */
	public abstract void printAcceptToken(PrintStream out);

	/**
	 * 
	 * @param out
	 */
	public abstract void printActions(PrintStream out);

	/**
	 * 
	 * @param imp
	 * @param out
	 */
	public abstract void printImports(List<String> imp,
			PrintStream out);

	/**
	 * 
	 * @param out
	 */
	public abstract void printIsEnd(PrintStream out);

	/**
	 * 
	 * @return
	 * @throws IOException
	 */
	protected abstract InputStream openScript() throws IOException;

	/**
	 * 
	 * @return
	 * @throws IOException
	 */
	protected abstract PrintStream openOutput() throws IOException;

	/**
	 * 
	 * @return
	 */
	protected abstract AbstractNinaTranslator newPrototype();

	/**
	 * 
	 * @param out
	 */
	protected abstract void printRecover(PrintStream out);

	/**
	 * 
	 * @param n
	 * @param out
	 */
	protected abstract void printDeadState(String n, PrintStream out);

	/**
	 * 
	 * @param n
	 * @param out
	 */
	protected abstract void printAttrs(PrintStream out);

	/**
	 * 
	 * @param out
	 */
	protected abstract void printFinallyState(PrintStream out);

	/**
	 * 
	 * @param out
	 */
	protected abstract void printInitTrap(PrintStream out);

	/**
	 * 
	 * @param out
	 */
	protected abstract void printNFADeadState(PrintStream out);

	/**
	 * 
	 * @param ot
	 * @param b1
	 */
	protected abstract void appendValue(StringBuffer ot,
			StringBuffer b1);

	/**
	 * 
	 * @param ot
	 * @param b1
	 */
	protected abstract void appendLvalue(StringBuffer ot,
			StringBuffer b1);

	/**
	 * 
	 * @param ot
	 * @param cn
	 */
	protected abstract void appendMyPosition(StringBuffer ot, String ln,
			int cn);

	/**
	 * 
	 * @param ot
	 */
	protected abstract void appendReturn(StringBuffer ot);

	/**
	 * 
	 * @param ot
	 * @param b
	 */
	protected abstract void appendYield(StringBuffer ot, String b);

	/**
	 * 
	 * @param ot
	 */
	protected abstract String getConstName(String b);

	//
	private void printStateSize(String n, PrintStream out) {
		out.printf("\t\t\treturn %d;\n", stateSize());
	}

	/**
	 * 
	 * @param o
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public AbstractNinaTranslator prototype(String name, Object o) {
		AbstractNinaTranslator x;

		x = newPrototype();
		x.parent   = parent;
		x.fragment = fragment;
		x.machine  = machine;
		x.options  = options;
		x.subfas   = subfas;
		x.name     = name;
		x.meta     = meta;
		if(o instanceof DFABuilder) {
			x.builder = (DFABuilder)o;
			x.dfa = (DFA<Object, ?, Void>)x.builder.getMachine();
		}
		return x;
	}

	String executeMeta(String s) {
		if(meta == null) {
			return s;
		} else {
			return meta.execute(s).toString();
		}
	}

	//
	private void processNFA(NinaNFA n, String p, Quadro q,
			ShNinatBuiltInCommands cmd,
			ShEnvironment env) throws IOException {
		PrintStream ous = null;
		InputStream ins = null;
		ShFileSystem fs;
		XtraceStream qs;
		ShRuntime run;

		if(q.getType() == null) {
			dfa = ConvertedRangeDFA.convertDFA(n);
		} else {
			dfa = ConvertedDFA.convertDFA(n);
		}

		if(!options.getOption("minimize").equalsIgnoreCase("no")) {
			dfa = MinimizedRangeDFA.newInstance(dfa);
		}

		try {
			ins  = openScript();
			ous  = openOutput();
			fs   = new ShNinatFileSystem(fragment, q, options);
			cmd.putCommand("print_token", new ShProcess() {

				@Override
				public int main(ShEnvironment env, ShFileSystem fs,
						InputStream stdin, PrintStream stdout,
						PrintStream stderr,
						String... args) throws IOException {
					printAcceptToken(stdout);
					return 0;
				}

			});
			cmd.putCommand("print_nfadead", new ShProcess() {

				@Override
				public int main(ShEnvironment env, ShFileSystem fs,
						InputStream stdin, PrintStream stdout,
						PrintStream stderr,
						String... args) throws IOException {
					printNFADeadState(stdout);
					return 0;
				}

			});
			run  = new DefaultShRuntime(cmd);
			qs   = new XtraceStream(System.out);
			ShFacade.execute(env, fs, cmd, run, ins, System.in, ous,
					System.err, qs);
		} catch(ShSyntaxException e) {
			throw new RuntimeException(e);
		} finally {
			if(ins != null)  ins.close();
			if(ous != null)  ous.close();
		}
	}

	//
	private AbstractNinaTranslator getsubfa(String[] args) {
		if(args.length > 1) {
			return subfas.get(args[1]);
		} else {
			return this;
		}
	}

	//
	private void processDFA(DFA<Object, Object, Void> d, String p,
			Quadro q, ShNinatBuiltInCommands cmd,
			ShEnvironment env) throws IOException {
		PrintStream ous = null;
		InputStream ins = null;
		ShFileSystem fs;
		XtraceStream qs;
		ShRuntime run;

		dfa = d;
		try {
			ins  = openScript();
			ous  = openOutput();
			fs   = new ShNinatFileSystem(fragment, q, options);
			cmd.putCommand("print_actions", new ShProcess() {

				@Override
				public int main(ShEnvironment env, ShFileSystem fs,
						InputStream stdin, PrintStream stdout,
						PrintStream stderr,
						String... args) throws IOException {
					getsubfa(args).printActions(stdout);
					return 0;
				}

			});
			cmd.putCommand("print_recover", new ShProcess() {

				@Override
				public int main(ShEnvironment env, ShFileSystem fs,
						InputStream stdin, PrintStream stdout,
						PrintStream stderr,
						String... args) throws IOException {
					getsubfa(args).printRecover(stdout);
					return 0;
				}

			});
			cmd.putCommand("print_deadstate", new ShProcess() {

				@Override
				public int main(ShEnvironment env, ShFileSystem fs,
						InputStream stdin, PrintStream stdout,
						PrintStream stderr,
						String... args) throws IOException {
					getsubfa(args).printDeadState(args[1], stdout);
					return 0;
				}

			});
			cmd.putCommand("print_statesize", new ShProcess() {

				@Override
				public int main(ShEnvironment env, ShFileSystem fs,
						InputStream stdin, PrintStream stdout,
						PrintStream stderr,
						String... args) throws IOException {
					getsubfa(args).printStateSize(args[1], stdout);
					return 0;
				}

			});
			cmd.putCommand("print_attrs", new ShProcess() {

				@Override
				public int main(ShEnvironment env, ShFileSystem fs,
						InputStream stdin, PrintStream stdout,
						PrintStream stderr,
						String... args) throws IOException {
					getsubfa(args).printAttrs(stdout);
					return 0;
				}

			});
			cmd.putCommand("print_finallystate", new ShProcess() {

				@Override
				public int main(ShEnvironment env, ShFileSystem fs,
						InputStream stdin, PrintStream stdout,
						PrintStream stderr,
						String... args) throws IOException {
					getsubfa(args).printFinallyState(stdout);
					return 0;
				}

			});
			cmd.putCommand("print_inittrap", new ShProcess() {

				@Override
				public int main(ShEnvironment env, ShFileSystem fs,
						InputStream stdin, PrintStream stdout,
						PrintStream stderr,
						String... args) throws IOException {
					printInitTrap(stdout);
					return 0;
				}

			});
			run  = new DefaultShRuntime(cmd);
			qs   = new XtraceStream(System.out);
			ShFacade.execute(env, fs, cmd, run, ins, System.in, ous,
					System.err, qs);
		} catch(ShSyntaxException e) {
			throw new RuntimeException(e);
		} finally {
			if(ins != null)  ins.close();
			if(ous != null)  ous.close();
		}
	}

	private ShEnvironment getenv(String p, Quadro q,
			Map<String, Object> sub) {
		ShEnvironment env;
		StringBuffer b;
		String s, d;
		char[] a;

		env  = new ShRootEnvironment();
		a    = options.getOutputFilename().toCharArray();
		a[0] = Character.toUpperCase(a[0]);
		env.bind("IFS", " \t");
		env.bind("CLASSNAME", new String(a));
		env.bind("PACKAGE", p);
		env.bind("FILENAME", options.getFilename());
		env.bind("OUTPUT_FILENAME", options.getOutputFilename());
		s = (s = options.getOption("bufsize")).equals("") ?
				"1024" : s;
		env.bind("BUFSIZE", s);
		env.bind("TEMPLATE", q.getOption("template"));
		env.bind("EXTENDS", q.getOption("extends"));
		env.bind("IMPLEMENTS", q.getOption("implements"));
		env.bind("ABSTRACT", q.isOptionDefined("abstract") ?
				"abstract" : "");
		env.bind("TOKENERROR", q.isOptionDefined("tokenerror") ?
				q.getOption("tokenerror") : "TokenException");
		env.bind("ITERATOR", q.getOptionBoolean("iterator") ?
				"true" : "");
		env.bind("MAINNAME", mainname);
		env.bind("DEFAULTAT", q.getOption("default"));
		env.bind("ENABLE_LOOKAHEAD", q.getOption("enableLookahead"));
		env.bind("ENABLE_BACKTRACK", q.getOption("enableBacktrack"));
		env.bind("ENABLE_MATCHER", q.getOption("enableMatcher"));
		env.bind("ENABLE_LOG", q.getOption("enableLog"));
		env.bind("API_ACCESS_MODIFIER",
				q.getOption("APIAccessModifier"));
		env.bind("READER", q.getOption("reader"));
		env.bind("LICENSE", q.getOptionNvl(
				"license", "default").toLowerCase());
		env.bind("OWNER", q.getOptionNvl("owner", ""));
		env.bind("YEAR", q.getOptionNvl("year",
				YEAR.format(new java.util.Date())));
		env.bind("ORGANIZATION", q.getOptionNvl("organization",
				q.getOptionNvl("owner", "")));
		env.bind("NEWLINEMODE",
				"binary".equals(q.getOption("newline")) ? "on" : "");
		env.bind("READSLOW", q.getOptionBoolean("readSlow") ?
				"true" : "");
		env.bind("READSLEEP", q.getOptionInt("readSleep", -1) + "");

		if((s = q.getType()) == null || s.equals("char")) {
			env.bind("TYPE", "");
			env.bind("CTYPE", "int");
		} else if(s.equals("string")) {
			env.bind("TYPE", s);
			env.bind("CTYPE", "String");
		} else if(s.equals("class")) {
			env.bind("TYPE", s);
			env.bind("CTYPE", "Object");
		} else {
			env.bind("TYPE", s);
			env.bind("CTYPE", s);
		}

		b = new StringBuffer();  d = "";
		for(String x : sub.keySet()) {
			b.append(d).append(x);
			d = " ";
		}
		env.bind("SUBAUTOMATA", b.toString());
		return env;
	}

	/**
	 * 
	 * @param name
	 * @param o
	 * @param opts
	 * @param prms
	 * @param sub
	 * @throws IOException
	 */
	@SuppressWarnings("unchecked")
	protected void translateQuadro(String name, Object o,
			NinatOptions opts, Map<String, String> prms,
			final Map<String, Object> sub) throws IOException {
		ShNinatBuiltInCommands cmd;
		ShEnvironment env;
		String p, q, m;

		mainname = name;
		subfas   = new HashMap<String, AbstractNinaTranslator>();
		options  = opts;
		if((m = quadro.getOption("machine")) == null) {
			// do nothing
		} else {
			m = "unknown";
		}

		fragment = quadro.getFragment();
		if((p = opts.getOption("package")) != null &&
				!p.equals("")) {
			// do nothing
		} else if((p = quadro.getRootPackage()) != null &&
				!p.equals("")) {
			// do nothing
		} else {
			p = "";
		}

		if((q = quadro.getOption("metaLanguage")) != null &&
				!q.equals("")) {
			meta = new NinaMetaLanguage(q);
		}

		if((q = quadro.getFragmentByName("meta")) != null &&
				!q.equals("") &&
				meta != null) {
			meta.execute(q);
		}

		env = getenv(p, quadro, sub);
		cmd = new ShNinatBuiltInCommands();
		cmd.putCommand("print_states", new ShProcess() {

			@Override
			public int main(ShEnvironment env, ShFileSystem fs,
					InputStream stdin, PrintStream stdout,
					PrintStream stderr,
					String... args) throws IOException {
				AbstractNinaTranslator t;

				if(args.length > 1) {
					t = prototype(args[1], sub.get(args[1]));
					subfas.put(args[1], t);
				} else {
					t = AbstractNinaTranslator.this;
				}

				if(quadro.getType() == null) {
					t.printStates(stdout);
				} else if(quadro.getType().equals("class")) {
					t.printClassStates(stdout);
				} else {
					t.printObjectStates(stdout);
				}
				return 0;
			}

		});
		cmd.putCommand("print_accepts", new ShProcess() {

			@Override
			public int main(ShEnvironment env, ShFileSystem fs,
					InputStream stdin, PrintStream stdout,
					PrintStream stderr,
					String... args) throws IOException {
				getsubfa(args).printAcceptStates(stdout);
				return 0;
			}

		});
		cmd.putCommand("print_imports", new ShProcess() {

			@Override
			public int main(ShEnvironment env, ShFileSystem fs,
					InputStream stdin, PrintStream stdout,
					PrintStream stderr,
					String... args) throws IOException {
				getsubfa(args).printImports(quadro.getImports(),
						stdout);
				return 0;
			}

		});
		cmd.putCommand("print_isend", new ShProcess() {

			@Override
			public int main(ShEnvironment env, ShFileSystem fs,
					InputStream stdin, PrintStream stdout,
					PrintStream stderr,
					String... args) throws IOException {
				getsubfa(args).printIsEnd(stdout);
				return 0;
			}

		});

		if(o instanceof NFABuilder) {
			machine = "nfa";
			builder = (NFABuilder)o;
			processNFA((NinaNFA)builder.getMachine(), p, quadro, cmd,
					env);
		} else if(o instanceof DFABuilder) {
			machine = "dfa";
			builder = (DFABuilder)o;
			processDFA((DFA<Object, Object, Void>)builder.getMachine(),
					p, quadro, cmd, env);
		} else {
			opts.perror("machinenotsupport", m);
		}
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.compiler.NinaTranslater#translate(java.io.Reader, java.io.PrintWriter)
	 */
	@Override
	public void translate(String name, Reader rd, NinatOptions opts,
			Map<String, String> prms) throws IOException {
		Map<String, Object> s;
		String n;
		Object o;

		s = new HashMap<String, Object>();
		n = Nina.getFAName(name);
		s.put(n, null);
		quadro = Quadro.read(n, rd);
		o = NinaParser.complie(quadro, opts.getLibraryList(), prms, s,
				opts.getConsole());
		translateQuadro(n, o, opts, prms, s);
	}

	/* (non-Javadoc)
	 * @see net.morilib.nina.translate.NinaTranslator#compile(java.lang.String, java.io.Reader, net.morilib.nina.cmd.NinatOptions, java.util.Map)
	 */
	@Override
	public Object compile(String name, Reader rd, NinatOptions opts,
			Map<String, String> prms) throws IOException {
		Map<String, Object> s;
		Quadro q;
		String n;

		s = new HashMap<String, Object>();
		n = Nina.getFAName(name);
		s.put(n, null);
		q = Quadro.read(n, rd);
		return NinaParser.complie(q, opts.getLibraryList(), prms, s,
				opts.getConsole());
	}

}
