package terse.vm;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.Reader;
import java.util.HashMap;
import java.util.regex.Pattern;

import terse.vm.Expr.Send;
import terse.vm.Pro.Blk;
import terse.vm.Pro.Buf;
import terse.vm.Pro.Dict;
import terse.vm.Pro.Undefined;
import terse.vm.Pro.Num;
import terse.vm.Pro.Obj;
import terse.vm.Pro.Str;
import terse.vm.Pro.Sys;
import terse.vm.Pro.Vec;
import terse.vm.Usr.Tmp;

public abstract class Terp extends Static {
	String initFilename;
	boolean loadingInitFile = false;
	HashMap<String, Cls> clss;
	HashMap<String, String> env;
	Wrap wrap;
	Cls tPro;
	Cls tProCls;
	Cls tObj;
	Cls tObjCls;
	Cls tCls;
	Cls tClsCls;
	Cls tMetacls;
	Cls tMetaclsCls;
	Cls tSuper;
	Cls tSuperCls;
	Cls tMeth;
	Cls tMethCls;
	Cls tJavMeth;
	Cls tJavMethCls;
	Cls tUsrMeth;
	Cls tUsrMethCls;
	Cls tSys;
	Cls tSysCls;
	Cls tNum;
	Cls tNumCls;
	Cls tBuf;
	Cls tBufCls;
	Cls tStr;
	Cls tStrCls;
	Cls tUndefined;
	Cls tUndefinedCls;
	Cls tBlk;
	Cls tBlkCls;
	Cls tVec;
	Cls tVecCls;
	Cls tDict;
	Cls tDictCls;
	Cls tExpr;
	Cls tExprCls;
	Cls tUsr;
	Cls tUsrCls;
	Cls tTmp;
	Cls tTmpCls;
	Num instTrue;
	Num instFalse;
	Undefined instNil;
	Pro.Super instSuper;

	static boolean tolerateNullClass = false;

	/**
	 * The Interpreter with an init file.
	 * 
	 * @throws IOException
	 */
	protected Terp(boolean pleaseLoadPrelude, String imageName,
			String initFilename) throws IOException {
		this.initFilename = initFilename;
		this.clss = new HashMap<String, Cls>();
		this.env = new HashMap<String, String>();
		this.env.put("image", imageName);

		tolerateNullClass = true;
		this.tProCls = new Cls(null/* Metacls */, this, "ProCls", null/* Cls */);
		this.tPro = new Cls(tProCls, this, "Pro", null/* Pro has no super */);

		this.tObjCls = new Cls(null/* Metacls */, this, "ObjCls", null/* Cls */);
		this.tObj = new Cls(tObjCls, this, "Obj", tPro);

		this.tClsCls = new Cls(null/* MetaCls */, this, "ClsCls", tObjCls);
		this.tCls = new Cls(tClsCls, this, "Cls", tObj);

		this.tMetaclsCls = new Cls(null/* MetaCls */, this, "MetaclsCls",
				tObjCls);
		this.tMetacls = new Cls(tMetaclsCls, this, "Metacls", tCls);

		// Patch up: All *Cls's are instances of Metacls.
		this.tProCls.cls = this.tMetacls;
		this.tObjCls.cls = this.tMetacls;
		this.tClsCls.cls = this.tMetacls;
		this.tMetaclsCls.cls = this.tMetacls;

		// Patch up: All *Cls's ultimately derive from Cls.
		this.tProCls.supercls = tCls;
		this.tObjCls.supercls = tCls;

		tolerateNullClass = false;

		// Super is a very special class, from Pro, not from Obj.
		this.tSuperCls = new Cls(tMetaclsCls, this, "SuperCls", tProCls);
		this.tSuper = new Cls(tSuperCls, this, "Super", tPro);

		this.tMethCls = new Cls(tMetaclsCls, this, "MethCls", tObjCls);
		this.tMeth = new Cls(tMethCls, this, "Meth", tObj);
		this.tJavMethCls = new Cls(tMetaclsCls, this, "JavMethCls", tObjCls);
		this.tJavMeth = new Cls(tJavMethCls, this, "JavMeth", tObj);
		this.tUsrMethCls = new Cls(tMetaclsCls, this, "UsrMethCls", tObjCls);
		this.tUsrMeth = new Cls(tUsrMethCls, this, "UsrMeth", tObj);
		this.tSysCls = new Cls(tMetaclsCls, this, "SysCls", tObjCls);
		this.tSys = new Cls(tSysCls, this, "Sys", tObj);
		this.tNumCls = new Cls(tMetaclsCls, this, "NumCls", tObjCls);
		this.tNum = new Cls(tNumCls, this, "Num", tObj);
		this.tBufCls = new Cls(tMetaclsCls, this, "BufCls", tObjCls);
		this.tBuf = new Cls(tBufCls, this, "Buf", tObj);
		this.tStrCls = new Cls(tMetaclsCls, this, "StrCls", tObjCls);
		this.tStr = new Cls(tStrCls, this, "Str", tObj);
		this.tUsrCls = new Cls(tMetaclsCls, this, "UsrCls", tObjCls);
		this.tUsr = new Cls(tUsrCls, this, "Usr", tObj);

		this.tUsr.myVars = strs("p", "q");  // TODO: temp hack.
		this.tUsr.recalculateAllVarsHereAndBelow();
		
		this.tTmpCls = new Cls(tMetaclsCls, this, "TmpCls", tUsrCls);
		this.tTmp = new Cls(tTmpCls, this, "Tmp", tUsr);
		this.tUndefinedCls = new Cls(tMetaclsCls, this, "UndefinedCls", tObjCls);
		this.tUndefined = new Cls(tUndefinedCls, this, "Undefined", tObj);
		this.tBlkCls = new Cls(tMetaclsCls, this, "BlkCls", tObjCls);
		this.tBlk = new Cls(tBlkCls, this, "Blk", tObj);
		this.tVecCls = new Cls(tMetaclsCls, this, "VecCls", tObjCls);
		this.tVec = new Cls(tVecCls, this, "Vec", tObj);
		this.tDictCls = new Cls(tMetaclsCls, this, "DictCls", tObjCls);
		this.tDict = new Cls(tDictCls, this, "Dict", tObj);

		this.tExprCls = new Cls(tMetaclsCls, this, "ExprCls", tObjCls);
		this.tExpr = new Cls(tExprCls, this, "Expr", tObj);

		this.instTrue = newNum(1);
		this.instFalse = newNum(0);
		this.instNil = new Undefined(tUndefined);
		this.instSuper = new Pro.Super(this);

		Pro.addBuiltinMethodsForPro(this);
		Obj.addBuiltinMethodsForObj(this);
		Cls.addBuiltinMethodsForCls(this);
		Sys.addBuiltinMethodsForSys(this);
		Num.addBuiltinMethodsForNum(this);
		Buf.addBuiltinMethodsForBuf(this);
		Str.addBuiltinMethodsForStr(this);
		Blk.addBuiltinMethodsForBlk(this);
		Vec.addBuiltinMethodsForVec(this);
		Dict.addBuiltinMethodsForDict(this);
		Usr.addBuiltinMethodsForUsr(this);
		
		loadingInitFile = true;
		wrap = new Wrap();
		wrap.installClasses(this);
		wrap.installMethods(this);
		loadingInitFile = false;

		if (pleaseLoadPrelude) {
			loadPrelude();
		}

		if (initFilename.length() > 0) {
			loadFile(initFilename);
		}
	}

	public Frame newFrame(Frame prev, Pro self, Expr.Top top) {
		return new Frame(prev, self, top);
	}

	public class Frame { // Important: non-static, tied to a Terp.
		Frame prev; // for listing the stack
		Pro self;
		Pro[] locals;
		Expr.Top top;
		int level;

		// If we did Lexical Binding, we would have a parent frame.
		// But we're not doing that (yet) so I deleted it.

		private Frame(Frame prev, Pro self, Expr.Top top) {
			this.prev = prev;
			this.self = self;
			this.top = top;
			this.level = (prev == null) ? 0 : prev.level + 1;
			this.locals = new Pro[top.numLocals];
			for (int i = 0; i < locals.length; i++) {
				this.locals[i] = getTerp().instNil;
			}
		}

		@Override
		public String toString() {
			StringBuffer sb = new StringBuffer("FRAME{");
			sb.append(fmt("self=<%s>; ", self));
			for (int i = 0; i < locals.length; i++) {
				sb.append(fmt("\"%d\"<%s>; ", i, locals[i]));
			}
			sb.append("}");
			return sb.toString();
		}

		public Terp terp() {
			return getTerp();
		}
	}

	public Terp getTerp() {
		return this;
	}

	public Num newNum(double a) {
		return new Num(this, a);
	}

	public Str newStr(String s) {
		return new Str(this, s);
	}

	public Tmp newTmp() {
		return new Tmp(this);
	}

	public Dict newDict(Pro[] arr) {
		Dict z = new Dict(this);
		for (int i = 0; i < arr.length; i++) {
			if (arr[i] instanceof Vec) {
				Vec v = (Vec) arr[i];
				if (v.vec.size() == 2) {
					z.dict.put(v.vec.get(0), v.vec.get(1));
				} else {
					toss("To initialize assoc in Dict, expected Vec of length 2, but got <%s#%d#%s>; inside <%s>",
							v.cls, v.vec.size(), v, arrayToString(arr));
				}
			} else {
				toss("To initialize assoc in Dict, expected Vec of length 2, but got <%s#%s>; inside <%s>",
						arr[i].cls, arr[i], arrayToString(arr));
			}
		}
		return z;
	}

	public Vec newVec(Pro[] a) {
		Vec z = new Vec(this);
		for (int i = 0; i < a.length; ++i) {
			z.vec.add(a[i]);
		}
		return z;
	}

	public Vec mkStrVec(String... strs) {
		Pro[] arr = new Pro[strs.length];
		for (int i = 0; i < strs.length; i++) {
			arr[i] = newStr(strs[i]);
		}
		return newVec(arr);
	}

	public Vec mkSingletonStrVecVec(String... strs) {
		Pro[] arr = new Pro[strs.length];
		for (int i = 0; i < strs.length; i++) {
			arr[i] = mkStrVec(strs[i]);
		}
		return newVec(arr);
	}

	public Dict handleUrl(String url, HashMap<String, String> query) {
		say("runUrl: %s", url);
		query = (query == null) ? new HashMap<String, String>() : query;
		Pro[] queryArr = new Pro[query.size()];
		int i = 0;
		for (String k : query.keySet()) {
			String v = query.get(k);
			say("  query <%s> = <%s>", k, v);
			if (k == null)
				k = "HOW_DID_WE_GET_A_NULL_KEY";
			if (v == null)
				v = "HOW_DID_WE_GET_A_NULL_VALUE";
			Pro queryKey = newStr(k);
			Pro queryValue = newStr(v.replaceAll("\r\n", "\n"));
			queryArr[i] = new Vec(pros(queryKey, queryValue));
			++i;
		}
		Dict qDict = newDict(queryArr);
		assert url.startsWith("/");
		if (url.equals("/")) {
			url = "/Home";
		}

		// To get app name, skip the initial '/', and split on dots.
		String[] word = url.substring(1).split("[.]");
		assert word.length > 0;
		String appName = word[0];

		Dict result = null;
		try {
			Cls cls = getTerp().clss.get(appName.toLowerCase());
			if (cls == null) {
				toss("Rendering class does not exist: <%s>", appName);
			}

			String urlRepr = newStr(url).repr();
			String qDictRepr = qDict.repr();  // Inefficient. TODO.
			Pro result_pro = instNil;
			int id = 0;

			Obj inst = null;
			try {
				id = Integer.parseInt(word[1]);
				inst = (Obj) cls.insts.get(id);
			} catch (Exception _) {
				// pass.
			}
			if (inst != null) {
				result_pro = inst.eval(fmt("self handle: (%s) query: (%s)", urlRepr,
						qDictRepr));
			} else if (Send.understands(cls, "handle:query:")) {
				say("CLS understands <%s>", cls);
				// First try sending to the class.
				result_pro = cls.eval(fmt("self handle: (%s) query: (%s)", urlRepr,
						qDictRepr));
			} else {
				Pro instance = cls.eval("self new");
				Usr usrInst = instance.asUsr();
				// TODO: LRU & mention() conflict with Cls.insts map.
				id = usrInst == null ? 0 : usrInst.mention(); // LRU Cache

				// Next try creating new instance, and send to it.
				result_pro = instance.asObj().eval(
						fmt("self handle: (%s) query: (%s)",
								newStr(url).repr(), qDict.repr()));
			}
			result = result_pro.asDict();
			if (result == null) {
				toss("Sending <handle:query:> to instance of <%s> did not return a Dict: <%s>",
						appName, result_pro);
			}
			result.dict.put(newStr("id"), newStr(Integer.toString(id)));

		} catch (Exception ex) {
			StringBuffer sb = new StringBuffer(ex.toString());
			StackTraceElement[] elems = ex.getStackTrace();
			for (StackTraceElement e : elems) {
				sb.append("\n  * ");
				sb.append(e.toString());
			}
			Pro[] dict_arr = pros(
					new Vec(pros(newStr("type"), newStr("text"))), new Vec(
							pros(newStr("title"), newStr(ex.toString()))),
					new Vec(pros(newStr("value"), newStr(sb.toString()))));
			result = newDict(dict_arr);
		}
		return result;

	}

	Pattern INITIAL_WHITE = Pattern.compile("^\\s");

	public void loadFile(String initFilename) throws IOException {
		try {
			FileInputStream fis = openFileRead(initFilename);
			loadReader(new InputStreamReader(fis));
		} catch (IOException ex) {
			// Ignore for now... TODO what to do?
		}
	}

	public void loadReader(InputStreamReader isr) throws IOException {
		BufferedReader br = new BufferedReader(isr);
		loadingInitFile = true;
		try {
			int lineNum = 0;
			String line = br.readLine();
			while (true) {
				// Break at EOF.
				if (line == null)
					break;
				++lineNum;
				// Skip blank lines.
				String trimmed = line.trim();
				if (trimmed.length() == 0) {
					line = br.readLine();
					continue;
				}
				// Skip comments.
				if (line.charAt(0) == '#') {
					line = br.readLine();
					continue;
				}

				if (line.charAt(0) < 'a' || line.charAt(0) > 'z') {
					toss("loadInitFile: Should have started with some lowercase letter a-z: <%s>",
							line);
				}

				// Store the command line in words[] and command.
				String[] words = line.trim().split("\\s+");
				if (words.length < 1) {
					toss("loadInitFile: Not enough words: <%s>", line.trim());
				}
				String command = words[0];

				// Now slurp any lines beginning with white space into more.
				StringBuffer sb = new StringBuffer();
				line = br.readLine();
				while (line != null
						&& (INITIAL_WHITE.matcher(line).lookingAt() || line
								.length() == 0)) {
					sb.append(line.length() > 0 ? line.substring(1) : "");
					sb.append("\n");
					line = br.readLine();
				}
				// Keep line for next time through big loop.
				String more = sb.toString();

				if (command.equals("meth")) {
					String className = words[1];
					String methodName = words[2];

					Cls cls = clss.get(className.toLowerCase());
					if (cls == null) {
						toss("loadInitFile: Global <%s> not a class, for meth <%s> :: %s",
								className, methodName);
					}
					cls.eval(fmt(
							"self definemethod: %s abbrev: %s doc: %s code: %s",
							newStr(methodName).repr(), newStr("").repr(),
							newStr("").repr(), newStr(more).repr()));
				} else if (command.equals("vars")) {
						String className = words[1];
						String methodName = words[2];

						// TODO:
//						Cls cls = clss.get(className.toLowerCase());
//						if (cls == null) {
//							toss("loadInitFile: Global <%s> not a class, for meth <%s> :: %s",
//									className, methodName);
//						}
//						cls.eval(fmt(
//								"self definemethod: %s abbrev: %s doc: %s code: %s",
//								newStr(methodName).repr(), newStr("").repr(),
//								newStr("").repr(), newStr(more).repr()));
				} else if (command.equals("class")) {
					String className = words[1];
					String superName = words[2];
					Cls clsObj = clss.get(className.toLowerCase());
					Cls supObj = clss.get(superName.toLowerCase());
					if (more.trim().length() > 0) {
						toss("Was not expecting more: <%s>");
					}
					if (supObj == null) {
						toss("loadInitFile: Cannot define subclass <%s> of <%s>: class <%s> does not exist.",
								className, superName, superName);
					}

					if (clsObj == null && supObj != null) {
						// We can create the class.
						supObj.defineSubclass(className);
					} else {
						toss("loadInitFile: Cannot define subclass <%s> of <%s>",
								className, superName);
					}
				} else if (command.equals("equals")) {
					String expected = "";
					for (int i = 1; i < words.length; i++) {
						expected += words[i] + " ";
					}
					Pro p1 = newTmp().eval2(expected);
					Pro p2 = newTmp().eval2(more);
					if (!p1.equals(p2)) {
						toss("loadInitFile: eq check failed: line %d: <%s> --> <%s> but <%s> --> <%s>",
								lineNum, expected, p1, more, p2);
					}
				} else {
					toss("loadInitFile: Unknown command: <%s>", line);
				}
			}
		} finally {
			loadingInitFile = false;
		}
	}

	void appendImageFile(String firstLine, String[] rest) throws IOException {
		if (loadingInitFile) {
			return; // Don't cause infinite loop!
		}
		if (initFilename.length() == 0) {
			return; // For unit tests.
		}
		FileOutputStream fos = openFileAppend(initFilename);
		PrintStream ps = new PrintStream(fos);
		ps.println(firstLine);
		if (rest != null) {
			for (String s : rest) {
				ps.println(" " + s); // Stuff 1 space before each line.
			}
		}
		ps.flush();
		fos.flush();
		fos.close();
	}

	public Pro toss(String s, Object... objects) {
		try {
			say("TOSSING <%s>", s);
		} catch (Exception ex) {
			Math.abs(0);
		}
		throw new TalkException(this, s, objects);
	}

	public Pro tossNotUnderstood(Cls c, String msg) {
		return toss("Message <%s> not understood by class <%s>", msg, c.name);
	}

	public abstract String say(String s, Object... objects);

	public abstract void loadPrelude() throws IOException;

	public abstract FileInputStream openFileRead(String filename)
			throws FileNotFoundException;

	public abstract FileOutputStream openFileWrite(String filename)
			throws FileNotFoundException;

	public abstract FileOutputStream openFileAppend(String filename)
			throws FileNotFoundException;

	//
	// public FileInputStream openFileRead(String filename) throws
	// FileNotFoundException {
	// return new FileInputStream(filename);
	// }
	// public FileOutputStream openFileWrite(String filename) throws
	// FileNotFoundException {
	// return new FileOutputStream(filename, false);
	// }
	// public FileOutputStream openFileAppend(String filename) throws
	// FileNotFoundException {
	// return new FileOutputStream(filename, true);
	// }

	public static class PosixTerp extends Terp {

		protected PosixTerp(boolean loadPrelude, String imageName,
				String initFilename) throws IOException {
			super(loadPrelude, imageName, initFilename);
		}

		@Override
		public String say(String s, Object... objects) {
			// TODO Auto-generated method stub
			String msg = fmt(s, objects);
			System.err.println(msg);
			return msg;
		}

		@Override
		public void loadPrelude() throws IOException {
			loadFile("prelude.txt");
		}

		@Override
		public FileInputStream openFileRead(String filename)
				throws FileNotFoundException {
			return new FileInputStream(filename);
		}

		@Override
		public FileOutputStream openFileWrite(String filename)
				throws FileNotFoundException {
			return new FileOutputStream(filename, false);
		}

		@Override
		public FileOutputStream openFileAppend(String filename)
				throws FileNotFoundException {
			return new FileOutputStream(filename, true);
		}
	}

	public interface Factory {
		Terp createTerp(boolean loadPrelude, String imageName,
				String initFilename) throws IOException;
	}
}
