package terse.vm;

import java.io.BufferedReader;
import java.io.File;
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.Ur.Blk;
import terse.vm.Ur.Buf;
import terse.vm.Ur.Dict;
import terse.vm.Ur.Undefined;
import terse.vm.Ur.Num;
import terse.vm.Ur.Obj;
import terse.vm.Ur.Str;
import terse.vm.Ur.Sys;
import terse.vm.Ur.Vec;
import terse.vm.Usr.Tmp;

public abstract class Terp extends Static {

	public static Pattern WORLD_P = Pattern.compile(
			"^[a-z][a-z][a-z][0-9]{0,3}$", Pattern.CASE_INSENSITIVE);

	static Pattern TXTFILE_P = Pattern.compile("^[a-z][a-z0-9_]{1,31}\\.txt$");

	protected static String YAK_WEB_PAGE = "http://wiki.yak.net/1017";

	String worldName = "";
	String worldFilename = "";
	boolean loadingWorldFile = false;
	HashMap<String, Cls> clss = new HashMap<String, Cls>();
	
	long sendCountDown = Long.MAX_VALUE;
	
	Wrap wrap;
	Cls tUr;
	Cls tUrCls;
	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;
	Ur.Super instSuper;

	static boolean tolerateNullClass = false;

	/**
	 * The Interpreter.
	 * 
	 * @throws IOException
	 */
	protected Terp(boolean pleaseLoadPrelude, String worldName)
			throws IOException {
		if (worldName.length() > 0) {
			if (!WORLD_P.matcher(worldName).matches()) {
				toss("Bad world name <%s>", worldName);
			}
			this.worldName = worldName;
			this.worldFilename = "w_" + worldName + ".txt";
		}

		tolerateNullClass = true;
		this.tUrCls = new Cls(null/* Metacls */, this, "UrCls", null/* Cls */);
		this.tUr = new Cls(tUrCls, this, "Ur", null/* Ur has no super */);

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

		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.tUrCls.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.tUrCls.supercls = tCls;
		this.tObjCls.supercls = tCls;

		tolerateNullClass = false;

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

		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 Ur.Super(this);

		// TODO: Convert all methods to "wrap" style, and get rid of all these
		// static inits.
		Obj.addBuiltinMethodsForObj(this);
		Cls.addBuiltinMethodsForCls(this);
		Num.addBuiltinMethodsForNum(this);
		Buf.addBuiltinMethodsForBuf(this);
		Str.addBuiltinMethodsForStr(this);
		Vec.addBuiltinMethodsForVec(this);
		Dict.addBuiltinMethodsForDict(this);
		Usr.addBuiltinMethodsForUsr(this);

		tolerateNullClass = true;
		loadingWorldFile = true;
		wrap = new Wrap();
		wrap.installClasses(this);
		wrap.installMethods(this);
		loadingWorldFile = false;
		tolerateNullClass = false;

		if (pleaseLoadPrelude) {
			loadPrelude();
		}

		if (worldName.length() > 0) {
			loadFile(worldFilename);
		}
	}
	
	public void tick() {
		--sendCountDown;
		if (sendCountDown < 1) {
			throw new Error("TooManyTicks");
		}
		
	}

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

	public class Frame extends Obj { // Important: non-static, tied to a Terp.

		// =get Frame . prev prevFrame
		Frame prev; // for listing the stack
		// =get Frame . self receiver
		Ur self;
		// =get Frame Ur[] locals localVars
		Ur[] locals;
		// =get Frame . top currentMethodExpr
		Expr.Top top;
		// =get Frame int level level
		int level;

		// =cls "Sys" Frame Obj
		private Frame(Frame prev, Ur self, Expr.Top top) {
			super(wrap.clsFrame);
			this.prev = prev;
			this.self = self;
			this.top = top;
			this.level = (prev == null) ? 0 : prev.level + 1;
			this.locals = new Ur[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();
		}
	}

	final public Terp getTerp() {
		return this;
	}

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

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

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

	final public Dict newDict(Ur[] 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;
	}

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

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

	final public Vec mkSingletonStrVecVec(String... strs) {
		Ur[] arr = new Ur[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;
		Ur[] queryArr = new Ur[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";
			Ur queryKey = newStr(k);
			Ur queryValue = newStr(v.replaceAll("\r\n", "\n"));
			queryArr[i] = new Vec(this, urs(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.
			Ur result_ur = instNil;
			int id = 0;

			Obj inst = null;
			try {
				id = Integer.parseInt(word[1]);
				inst = (Obj) cls.insts.get(id);
			} catch (Exception _) {
				// pass.
			}
			long before = sendCountDown;
			long nanosBefore = System.nanoTime();
			if (inst != null) {
				result_ur = 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_ur = cls.eval(fmt("self handle: (%s) query: (%s)",
						urlRepr, qDictRepr));
			} else {
				Ur 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_ur = instance.asObj().eval(
						fmt("self handle: (%s) query: (%s)",
								newStr(url).repr(), qDict.repr()));
			}
			result = result_ur.asDict();
			if (result == null) {
				toss("Sending <handle:query:> to instance of <%s> did not return a Dict: <%s>",
						appName, result_ur);
			}
			result.dict.put(newStr("id"), newStr(Integer.toString(id)));
			long after = sendCountDown;
			long nanosAfter = System.nanoTime();
			result.dict.put(newStr("ticks"), newNum(before - after));
			result.dict.put(newStr("nanos"), newNum(nanosAfter - nanosBefore));
			say("<handle:query:> used %d ticks and %.9f secs.", before - after, (double)(nanosAfter - nanosBefore) / 1000000000.0);

		} catch (Exception ex) {
			StringBuffer sb = new StringBuffer(ex.toString());
			StackTraceElement[] elems = ex.getStackTrace();
			for (StackTraceElement e : elems) {
				sb.append("\n  * ");
				sb.append(e.toString());
			}
			Ur[] dict_arr = urs(
					new Vec(this, urs(newStr("type"), newStr("text"))),
					new Vec(this, urs(newStr("title"), newStr(ex.toString()))),
					new Vec(this, urs(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);
		loadingWorldFile = 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];
					Cls cls = clss.get(className.toLowerCase());
					if (cls == null) {
						toss("loadInitFile: Cannot find class <%s>", className);
					}
					cls.defVars(more);
				} 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] + " ";
					}
					Ur p1 = newTmp().eval(expected);
					Ur p2 = newTmp().eval(more);
					if (!p1.equals(p2)) {
						toss("loadInitFile: eq check failed: line %d: <%s> --> <%s> but <%s> --> <%s>",
								lineNum, expected, p1, more, p2);
					}
				} else if (command.equals("stop")) {
					break; // For testing partial files.
				} else {
					toss("loadInitFile: Unknown command: <%s>", line);
				}
			}
		//} finally {
		//	loadingWorldFile = false;
		//}
	}

	void appendWorldFile(String firstLine, String[] rest) throws IOException {
		if (loadingWorldFile) {
			return; // Don't cause infinite loop!
		}
		if (worldFilename.length() == 0) {
			return; // For unit tests.
		}
		FileOutputStream fos = openFileAppend(worldFilename);
		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 Ur toss(String s, Object... objects) {
		try {
			say("TOSSING <%s>", s);
		} catch (Exception ex) {
			Math.abs(0);
		}
		throw new TalkException(this, s, objects);
	}

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

	public void checkTxtFileNameSyntax(String filename) {
		if (!TXTFILE_P.matcher(filename).matches()) {
			toss("Bad filename, should match <%s> : <%s>", TXTFILE_P, filename);
		}
	}

	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 abstract File getFilesDir();

	public abstract Vec listOfWebFiles();

	public abstract void pushToWeb(String filename, String content);

	public abstract String pullFromWeb(String filename);

	public static class PosixTerp extends Terp {

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

		@Override
		public String say(String s, Object... objects) {
			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);
		}

		@Override
		public File getFilesDir() {
			return new File(".");
		}

		@Override
		public void pushToWeb(String filename, String content) {
			// import org.apache.http.impl.client.DefaultHttpClient;
			toss("Not Implemented in PosixTerp: pushToWeb");
		}

		@Override
		public String pullFromWeb(String filename) {
			// import org.apache.http.impl.client.DefaultHttpClient;
			toss("Not Implemented in PosixTerp: pullFromWeb");
			return null;
		}

		@Override
		public Vec listOfWebFiles() {
			toss("Not Implemented in PosixTerp: listOfWebFiles");
			return null;
		}
	}

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