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

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringReader;
import java.util.LinkedList;
import java.util.Locale;
import java.util.logging.Logger;

import net.morilib.lisp.nano.exlib.SubrError;
import net.morilib.lisp.nano.subr.Add;
import net.morilib.lisp.nano.subr.Car;
import net.morilib.lisp.nano.subr.Cdr;
import net.morilib.lisp.nano.subr.IsEq;
import net.morilib.lisp.nano.subr.IsEqv;
import net.morilib.lisp.nano.subr.IsNull;
import net.morilib.lisp.nano.subr.IsPair;
import net.morilib.lisp.nano.subr.IsString;
import net.morilib.lisp.nano.subr.IsSymbol;
import net.morilib.lisp.nano.subr.IsVector;
import net.morilib.lisp.nano.subr.MakeCons;
import net.morilib.lisp.nano.subr.MakeVector;
import net.morilib.lisp.nano.subr.NumGreaterThan;
import net.morilib.lisp.nano.subr.NumLessThan;
import net.morilib.lisp.nano.subr.NumberToString;
import net.morilib.lisp.nano.subr.StringAppend;
import net.morilib.lisp.nano.subr.StringEqual;
import net.morilib.lisp.nano.subr.StringLength;
import net.morilib.lisp.nano.subr.StringRef;
import net.morilib.lisp.nano.subr.StringToSymbol;
import net.morilib.lisp.nano.subr.Sub;
import net.morilib.lisp.nano.subr.Substring;
import net.morilib.lisp.nano.subr.SymbolToString;
import net.morilib.lisp.nano.subr.VectorLength;
import net.morilib.lisp.nano.subr.VectorRef;
import net.morilib.lisp.nano.util.LogEnv;
import net.morilib.lisp.nano.util.PeekableReader;

/**
 * 
 *
 *
 * @author MORIGUCHI, Yuichiro 2009
 */
public final class Scheme {

	/**
	 * 
	 */
	public static final int SCHEME_VERSION = 5;

	/**
	 * 
	 */
	public static final String SCHLUSH_VERSION = "0.4.5";

	//
	private static final String MA2 =
			"/net/morilib/lisp/nano/macro2.scm";
	private static final Scheme MACRO;

	//
	private static Logger _log;

	//
	private static void _bind(Environment e, String s, Datum d) {
		e.bindDatum(Symbol.getSymbol(s), d);
	}

	//
	static {
		Environment e;

		_log = LogEnv.init("schlush.main");
		e = new Environment();
		_bind(e, "if",             new SynIf());
		_bind(e, "set!",           new SynSetS());
		_bind(e, "lambda",         new SynLambda());
		_bind(e, "define",         new SynDefine());
		_bind(e, "quote",          new SynQuote());
		_bind(e, "begin",          new SynBegin());
		_bind(e, "cons",           new MakeCons());
		_bind(e, "car",            new Car());
		_bind(e, "cdr",            new Cdr());
		_bind(e, "null?",          new IsNull());
		_bind(e, "pair?",          new IsPair());
		_bind(e, "symbol?",        new IsSymbol());
		_bind(e, "eq?",            new IsEq());
		_bind(e, "eqv?",           new IsEqv());
		_bind(e, "+",              new Add());
		_bind(e, "-",              new Sub());
		_bind(e, ">",              new NumGreaterThan());
		_bind(e, "<",              new NumLessThan());
		_bind(e, "set-car!",       new SubrSetCarS());
		_bind(e, "set-cdr!",       new SubrSetCdrS());
		_bind(e, "vector?",        new IsVector());
		_bind(e, "make-vector",    new MakeVector());
		_bind(e, "vector-ref",     new VectorRef());
		_bind(e, "vector-set!",    new SubrVectorSetS());
		_bind(e, "vector-length",  new VectorLength());
		_bind(e, "string?",        new IsString());
		_bind(e, "string=?",       new StringEqual());
		_bind(e, "string-ref",     new StringRef());
		_bind(e, "string-length",  new StringLength());
		_bind(e, "string-append",  new StringAppend());
		_bind(e, "substring",      new Substring());
		_bind(e, "number->string", new NumberToString());
		_bind(e, "string->symbol", new StringToSymbol());
		_bind(e, "symbol->string", new SymbolToString());
		_bind(e, "error",          new SubrError());
		MACRO = new Scheme(e, LispMessage.getInstance());

		try {
			MACRO.set("macroenv", new Cons(Nil.NIL, Nil.NIL));
			NanoParser.read(MACRO,
					Scheme.class.getResourceAsStream(MA2));
		} catch (IOException e1) {
			throw new RuntimeException(e1);
		}
	}

	//
	private static PrintWriter transcript = null;

	//
	private Environment  global;
	private LispCompiler comp;
	private CodeExecutor exec;
	private IntStack     memento;
	private LispMessage  message;
	String stackTrace;

	//
	private Scheme(LispMessage msg) {
		if(msg == null) {
			throw new NullPointerException();
		}

		global  = new Environment();
		message = msg;
		comp    = CompilerFactory.getInstance(message);
		exec    = CodeExecutorFactory.getInstance(message);
		memento = exec.newMemento();
	}

	/**
	 * 
	 * @param env
	 * @param msg
	 */
	public Scheme(Environment env, LispMessage msg) {
		if(env == null) {
			throw new NullPointerException();
		} else if(msg == null) {
			throw new NullPointerException();
		}

		global  = env;
		message = msg;
		comp    = CompilerFactory.getInstance(message);
		exec    = CodeExecutorFactory.getInstance(message);
		memento = exec.newMemento();
	}

	/**
	 * 
	 * @param lc
	 * @return
	 */
	public static Scheme newInstance(Locale lc) {
		Scheme res = new Scheme(LispMessage.getInstance(lc));

		InitSubrLoader.load(res.global);
		InitLispLoader.load(res);
		return res;
	}

	/**
	 * 
	 * @return
	 */
	public static Scheme newInstance() {
		return newInstance(Locale.getDefault());
	}

	/**
	 * 
	 * @param lc
	 * @return
	 * @throws InitLoadException 
	 */
	public static Scheme newInstance(
			String[] pkg, Locale lc) throws InitLoadException {
		Scheme res = new Scheme(LispMessage.getInstance(lc));

		for(String s : pkg)  InitSubrLoader.load(s, res.global);
		InitLispLoader.load(res);
		return res;
	}

	/**
	 * 
	 * @return
	 * @throws InitLoadException 
	 */
	public static Scheme newInstance(
			String[] pkg) throws InitLoadException {
		return newInstance(pkg, Locale.getDefault());
	}

	/**
	 * 
	 * @param ver
	 * @return
	 */
	public static Environment newNullEnv(int ver) {
		return newNull(ver).global;
	}

	/**
	 * 
	 * @param ver
	 * @return
	 */
	public static Environment newRnRSEnv(int ver) {
		return newRnRS(ver).global;
	}

	/**
	 * 
	 * @param ver
	 * @return
	 */
	public static Scheme newEmpty() {
		return new Scheme(LispMessage.getInstance());
	}

	/**
	 * 
	 * @param ver
	 * @return
	 */
	public static Scheme newNull(int ver) {
		return newNull(ver, null);
	}

	/**
	 * 
	 * @param ver
	 * @param lc
	 * @return
	 */
	public static Scheme newNull(int ver, Locale lc) {
		Scheme res = new Scheme(LispMessage.getInstance(lc));

		InitSubrLoader.loadNullEnv(res.global, ver);
		InitLispLoader.loadNullEnv(res, ver);
		return res;
	}

	/**
	 * 
	 * @param ver
	 * @return
	 */
	public static Scheme newRnRS(int ver) {
		return newRnRS(ver, null);
	}

	/**
	 * 
	 * @param ver
	 * @param lc
	 * @return
	 */
	public static Scheme newRnRS(int ver, Locale lc) {
		Scheme res = new Scheme(LispMessage.getInstance(lc));

		InitSubrLoader.loadRnRSEnv(res.global, ver);
		InitLispLoader.loadRnRSEnv(res, ver);
		return res;
	}

	/**
	 * 
	 * @param pw
	 */
	public static void setTranscript(PrintWriter pw) {
		synchronized(Scheme.class) {
			if(transcript != null) {
				transcript.close();
			}
			transcript = pw;
		}
	}

	/**
	 * 
	 * @return
	 */
	public Environment getGlobalEnvironment() {
		return global;
	}

	/**
	 * 
	 * @param env
	 * @param sexp
	 * @return
	 */
	public Datum input(Environment env, Datum sexp) {
		try {
			CompiledCode.Builder b = new CompiledCode.Builder();
			Datum d = sexp;
			long t;

			// macro extension
			t = System.currentTimeMillis();
			IntLispUtils.timelog(_log, "Expand Macro : ", t);
			if(this != MACRO && this != LispUtils.DISP) {
				try {
					d = MACRO.call("eval-macro", d);
				} catch(LispException e) {
					throw message.getError("err.macro");
				} catch(NoSuchMethodException e) {
					throw message.getError("err.macro");
				}
			}

			// compile
			t = System.currentTimeMillis();
			comp.compile(d, env, b, true, new Cons(), true,
					new LinkedList<Cons>(), exec, memento);
			b.addReturnOp();
			IntLispUtils.timelog(_log, "Compile : ", t);

			// execute
			t = System.currentTimeMillis();
			Datum res = exec.exec(b.getCodeRef(), env, memento);
			IntLispUtils.timelog(_log, "Execute : ", t);

			// display the stack
			_log.fine(memento.toString());
			return res;
		} finally {
			stackTrace = memento.getStackTrace();
			memento    = exec.newMemento();
			MACRO.memento = MACRO.exec.newMemento();
		}
	}

	/**
	 * 
	 * @param sexp
	 * @return
	 */
	public Datum input(Datum sexp) {
		return input(global, sexp);
	}

	/**
	 * 
	 * @param env
	 * @param rd1
	 * @return
	 * @throws IOException
	 */
	public Datum read(Environment env, Reader rd1) throws IOException {
		Datum d;

		d = NanoParser.read(new PeekableReader(rd1));
		return input(env, d);
	}

	/**
	 * 
	 * @param rd1
	 * @return
	 * @throws IOException
	 */
	public Datum read(Reader rd1) throws IOException {
		return read(global, rd1);
	}

	/**
	 * 
	 * @param rd1
	 * @throws IOException
	 */
	public Datum input(String s) {
		Datum d;

		try {
			d = NanoParser.read(new PeekableReader(
					new StringReader(s)));
			return d != null ? input(d) : null;
		} catch(IOException e) {
			throw new LispIOException(e);
		}
	}

	/**
	 * 
	 * @param rd1
	 * @throws IOException
	 */
	public void readEvalPrintLoop(PeekableReader rd1) throws IOException {
		NanoParser.repl(this, rd1, message);
	}

	/**
	 * 
	 * @param var
	 * @param o
	 */
	public void set(String var, Object o) {
		global.bindDatum(Symbol.getSymbol(var), LispUtils.toDatum(o));
	}

	/**
	 * 
	 * @param var
	 * @param cdr
	 * @param lst
	 */
	public void setDotList(String var, Object cdr, Object... lst) {
		ConsListBuilder b = new ConsListBuilder();

		for(Object o : lst) {
			b.append(LispUtils.toDatum(o));
		}
		global.bindDatum(
				Symbol.getSymbol(var), b.get(LispUtils.toDatum(cdr)));
	}

	/**
	 * 
	 * @param var
	 * @param lst
	 */
	public void setList(String var, Object... lst) {
		setDotList(var, Nil.NIL, lst);
	}

	/**
	 * 
	 * @param var
	 * @return
	 */
	public Datum get(String var) {
		return global.findDatum(Symbol.getSymbol(var));
	}

	/**
	 * 
	 * @param var
	 * @param lst
	 * @return
	 * @throws NoSuchMethodException 
	 */
	public Datum call(String var,
			Object... lst) throws NoSuchMethodException {
		CompiledCode.Builder cd = new CompiledCode.Builder();
		Datum fn = get(var);

		if(fn == null) {
			throw new NoSuchMethodException();
		}
		cd.addPush(fn);
		cd.addPush(LispUtils.toConsList(lst));
		cd.addCall();
		cd.addReturnOp();
		return exec.exec(cd.getCodeRef(), global, memento);
	}

	/**
	 * @return the message
	 */
	public LispMessage getMessage() {
		return message;
	}

}
