package net.yak.stalk;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Hashtable;
import java.util.regex.Pattern;

import net.yak.stalk.Cls.Meth;
import net.yak.stalk.Expr.Send;
import net.yak.stalk.Pro.Assoc;
import net.yak.stalk.Pro.Blk;
import net.yak.stalk.Pro.Buf;
import net.yak.stalk.Pro.False;
import net.yak.stalk.Pro.Map;
import net.yak.stalk.Pro.Undefined;
import net.yak.stalk.Pro.Num;
import net.yak.stalk.Pro.Obj;
import net.yak.stalk.Pro.Str;
import net.yak.stalk.Pro.Sys;
import net.yak.stalk.Pro.True;
import net.yak.stalk.Pro.Vec;

public class Talk {
	static Pro toss(String s, Object... objects) {
		throw new TalkException(s, objects);
	}
	static Pro tossNotUnderstood(Cls c, String msg) {
		throw new TalkException("Message <%s> not understood by class <%s>",
				msg, c.name);
	}
	static void say(String s, Object... objects) {
		System.out.println(fmt(s, objects));
	}
	static String fmt(String s, Object... objects) {
		return String.format(s, objects);
	}
	static String htmlEscape(String s) {
		return s.replace("&", "&amp;").replace("<", "&lt;")
				.replace(">", "&gt;").replace("\"", "&quot;");
	}

	// static Pattern unaryNameP;
	// static Pattern keywordNameP;
	static Pattern htmlTagP;
	static {
		// // Unary name cannot be 2 letters. Can be 1 letter, or 3 or more.
		// // Must begin with a lower.
		// unaryNameP = Pattern.compile("[a-z]|[a-zA-Z0-9]{3,}");
		// // Keyword name must have 2 or more letters in each part, beginning
		// with
		// // lower.
		// keywordNameP = Pattern.compile("([a-z][a-zA-Z0-9]+[:])+");
		// Tags in HTML: safe chars in names of tags and params.
		htmlTagP = Pattern.compile("[-A-Za-z0-9_.:]+");
	}

	// static String AbbreviateMethodName(String s) {
	// final int slen = s.length();
	// StringBuffer sb = new StringBuffer();
	// say("AbbreviateMethodName <%s>", s);
	// if (Parser.unaryP.matcher(s).matches()) {
	// assert slen == 1 || slen >= 3; // unaryP should assure this.
	// assert s.length() >= 3;
	// int keep = 3; // Keep 3 initial chars.
	// for (int i = 0; i < slen; i++, keep--) {
	// char c = s.charAt(i);
	// if (keep > 0) {
	// sb.append(c);
	// } else if (Character.isUpperCase(c)) {
	// sb.append(c);
	// keep = 1; // One more in addition to c.
	// }
	// }
	// return s.substring(0, 3).toLowerCase();
	// } else if (Parser.multiKeywordP.matcher(s).matches()) {
	// for (int i = 0; i < slen; ++i) {
	// // State: start of word
	// while (i < slen && s.charAt(i) != ':') {
	// sb.append(s.charAt(i));
	// ++i;
	// sb.append(s.charAt(i));
	// ++i;
	// while (i < slen && s.charAt(i) != ':') {
	// if ('A' <= s.charAt(i) && s.charAt(i) <= 'Z') {
	// if (s.length() >= i + 2) {
	// if (Character.isLetter(s.charAt(i + 1))) {
	// break; // break inner but continue outer.
	// }
	// }
	// }
	// ++i; // drop the char.
	// }
	// }
	// assert (i == slen || s.charAt(i) == ':');
	// sb.append(':');
	// // The ++i in for() will advance past the colon.
	// }
	// return sb.toString().toLowerCase();
	// } else {
	// toss("Bad method name syntax: <%s>", s);
	// return "NOTREACHED";
	// }
	// }

	public static class TalkException extends RuntimeException {
		private static final long serialVersionUID = 1L;
		String msg;
		Object[] args;

		public TalkException(String s, Object... args) {
			this.msg = s;
			this.args = args;
			System.err.println("TalkException: msg=" + msg);
			for (int i = 0; i < args.length; i++) {
				System.err.println("TalkException: arg:" + args[i]);
			}
		}

		@Override
		public String toString() {
			return fmt(msg, args);
		}
	}

	public static class Terp {
		Frame glob;
		Cls tPro;
		Cls tProCls;
		Cls tObj;
		Cls tObjCls;
		Cls tCls;
		Cls tClsCls;
		Cls tMetacls;
		Cls tMetaclsCls;
		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 tExc;
		Cls tExcCls;
		Cls tErr;
		Cls tErrCls;
		Cls tRtx;
		Cls tRtxCls;
		Cls tRtn;
		Cls tRtnCls;
		Cls tBrk;
		Cls tBrkCls;
		Cls tUndefined;
		Cls tUndefinedCls;
		Cls tFalse;
		Cls tFalseCls;
		Cls tTrue;
		Cls tTrueCls;
		Cls tBlk;
		Cls tBlkCls;
		Cls tVec;
		Cls tVecCls;
		Cls tMap;
		Cls tMapCls;
		Cls tAssoc;
		Cls tAssocCls;
		Cls tUsr;
		Cls tUsrCls;
		// Cls tPage;
		// Cls tPageCls;
		// Cls tApp;
		// Cls tAppCls;
		True instTrue;
		False instFalse;
		Undefined instNil;

		public Terp() {
			this.glob = new Frame(null, null);
			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;

			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.tExcCls = new Cls(tMetaclsCls, this, "ExcCls", tObjCls);
			this.tExc = new Cls(tExcCls, this, "Exc", tObj);
			this.tErrCls = new Cls(tMetaclsCls, this, "ErrCls", tObjCls);
			this.tErr = new Cls(tErrCls, this, "Err", tObj);
			this.tRtxCls = new Cls(tMetaclsCls, this, "RtxCls", tObjCls);
			this.tRtx = new Cls(tRtxCls, this, "Rtx", tObj);
			this.tRtnCls = new Cls(tMetaclsCls, this, "RtnCls", tObjCls);
			this.tRtn = new Cls(tRtnCls, this, "Rtn", tObj);
			this.tBrkCls = new Cls(tMetaclsCls, this, "BrkCls", tObjCls);
			this.tBrk = new Cls(tBrkCls, this, "Brk", tObj);
			this.tUndefinedCls = new Cls(tMetaclsCls, this, "UndefinedCls", tObjCls);
			this.tUndefined = new Cls(tUndefinedCls, this, "Undefined", tObj);
			this.tFalseCls = new Cls(tMetaclsCls, this, "FalseCls",
					tObjCls);
			this.tFalse = new Cls(tFalseCls, this, "False", tObj);
			this.tTrueCls = new Cls(tMetaclsCls, this, "TrueCls", tObjCls);
			this.tTrue = new Cls(tTrueCls, this, "True", 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.tMapCls = new Cls(tMetaclsCls, this, "MapCls", tObjCls);
			this.tMap = new Cls(tMapCls, this, "Map", tObj);
			this.tAssocCls = new Cls(tMetaclsCls, this, "AssocCls", tObjCls);
			this.tAssoc = new Cls(tAssocCls, this, "Assoc", tObj);

			// this.tPageCls = new Cls(tMetaclsCls, this, "PageCls", tUsrCls);
			// this.tPage = new Cls(tPageCls, this, "Page", tUsr);
			// this.tAppCls = new Cls(tMetaclsCls, this, "AppCls", tPageCls);
			// this.tApp = new Cls(tAppCls, this, "App", tPage);

			this.instTrue = new True(tTrue);
			this.glob.put("tru", instTrue);
			this.instFalse = new False(tFalse);
			this.glob.put("fal", instFalse);
			this.instNil = new Undefined(tUndefined);
			this.glob.put("nil", instNil);

			Pro.addBuiltinMethodsForPro(this);
			Obj.addBuiltinMethodsForObj(this);
			Cls.addBuiltinMethodsForCls(this);
			Sys.addBuiltinMethodsForSys(this);
			Num.addBuiltinMethodsForNum(this);
			Buf.addBuiltinMethodsForBuf(this);
			Str.addBuiltinMethodsForStr(this);
			True.addBuiltinMethodsForTru(this);
			False.addBuiltinMethodsForFal(this);
			Blk.addBuiltinMethodsForBlk(this);
			Vec.addBuiltinMethodsForVec(this);
			Map.addBuiltinMethodsForMap(this);
			Usr.addBuiltinMethodsForUsr(this);

			try {
				loadInitFile();
			} catch (IOException e) {
				e.printStackTrace();
				toss("Cannot loadInitFile: " + e.toString());
			}
		}
		Frame newFrame(Frame prev, Pro self) {
			return new Frame(prev, self);
		}

		public class Frame { // Important: non-static, tied to a Terp.
			Frame prev; // for listing the stack
			Pro self;
			Hashtable<String, Pro> dict;

			// 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) {
				this.prev = prev;
				this.self = self;
				this.dict = new Hashtable<String, Pro>();
			}

			Pro get(String key) {
				Pro value = this.dict.get(key);
				// if (value == null) {
				// toss("No variable <%s> in %s Frame.", key,
				// this == glob ? "Global" : "Local");
				// }
				return value;
			}
			boolean containsKey(String key) {
				return this.dict.containsKey(key);
			}

			void put(String key, Pro value) {
				this.dict.put(key, value);
			}
			Frame globals() {
				return glob;
			}
			Terp terp() {
				return getTerp();
			}
		}

		Terp getTerp() {
			return this;
		}
		Num newNum(double a) {
			return new Num(this, a);
		}
		Str newStr(String s) {
			return new Str(this, s);
		}
		Map newMap(Pro[] kvArray) {
			Map z = new Map(this);
			for (int i = 0; i < kvArray.length; i += 2) {
				z.map.put(kvArray[i], kvArray[i + 1]);
			}
			return z;
		}
		Vec newVec(Pro[] a) {
			Vec z = new Vec(this);
			for (int i = 0; i < a.length; ++i) {
				z.vec.add(a[i]);
			}
			return z;
		}
		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);
		}
		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);
		}
		Map handleUrl(String url, Hashtable<String, String> query) {
			say("runUrl: %s", url);
			Pro[] kvArray = new Pro[query.size() * 2];
			int i = 0;
			for (String k : query.keySet()) {
				say("  query %s = %s", k, query.get(k));
				kvArray[i] = newStr(k);
				kvArray[i+1] = newStr(query.get(k));
				i += 2;
			}
			Map qMap = newMap(kvArray);
			assert url.startsWith("/");
			if (url.equals("/")) {
				url = "/TopApp";
			};
			
			String[] word = url.substring(1).split("[.]");
			assert word.length > 0;
			String appName = word[0];

			Map result = null;
			try {
				Pro app = glob.get(appName.toLowerCase());
				if (app == null) {
					toss("Cls not found: <%s>", appName);
				}
				Cls cls = app.asCls();
				Pro self;
				if (cls != null) {
					// It's a class, so make an instance.
					self = new Expr.Send(new Expr.Lit(app), "new",
							Parser.emptyExprArray).eval(new Frame(null, cls));
				} else {
					// It's not a class, so it's an instance.
					self = app;
				}
				Expr.Send sendHandle = new Expr.Send(new Expr.Lit(self),
						"handle:quer:", new Expr[] { new Expr.Lit(newStr(url)), new Expr.Lit(qMap) });

				Pro result_pro = sendHandle.eval(newFrame(null, self));
				result = result_pro.asMap();
				if (result == null) {
					toss(
							"Sending <handle:> to instance of <%s> did not return a Map: <%s>",
							appName, result_pro);
				}

			} 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[] map_arr = new Pro[] { newStr("type"), newStr("text"),
						newStr("title"), newStr(ex.toString()),
						newStr("value"), newStr(sb.toString()) };
				result = newMap(map_arr);
			}
			return result;

		}
		void loadInitFile() throws IOException {
			Frame frame = newFrame(null, null);
			BufferedReader in = new BufferedReader(new FileReader(
					"/home/strick/workspace/one/src/net/yak/stalk/init.txt"));
			while (true) {
				String line = in.readLine();
				if (line == null)
					break;
				if (line.trim().length() == 0)
					continue;

				String[] words = line.split("\\s+");
				assert words.length > 2;
				assert words[0].equals("{");
				String command = words[1];

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

					Pro cx = glob.get(className.toLowerCase());
					if (cx == null) {
						toss("Class <%s> not found, for meth <%s> :: %s",
								className, methodName, glob.dict);
					}
					Cls cls = (Cls) cx;
					if (cx == null) {
						toss("Global <%s> not a class, for meth <%s> :: %s",
								className, methodName, cx);
					}
					StringBuffer sb = new StringBuffer();
					line = in.readLine();
					while (!line.startsWith("}")) {
						sb.append(line);
						line = in.readLine();
					}
					String body = sb.toString();
					Meth m = tCls.meths.get("definemethod:abbrev:docstr:code:");
					cx = m.apply(frame, cls, new Pro[] { newStr(methodName),
							newStr(""), newStr(""), newStr(body) });
				} else if (command.equals("class")) {
					String className = words[2];
					String superName = words[3];
					Pro classObj = glob.get(className.toLowerCase());
					Pro superObj = glob.get(superName.toLowerCase());
					Cls clsObj = classObj != null ? classObj.asCls() : null;
					Cls supObj = superObj != null ? superObj.asCls() : null;

					if (clsObj == null && supObj != null) {
						// We can create the class.
						supObj.defineSubclass(className);
					} else {
						toss("Cannot define subclass <%s> of <%s>", className,
								superName);
					}
				}
			}
		}

		public static class Visitor {
			void visitPro(Pro a) {
				toss("SubclassResponsibility(Visitor::visitPro)");
			}
			void visitCls(Cls a) {
				visitPro(a);
			}
			void visitNum(Num a) {
				visitPro(a);
			}
			void visitStr(Str a) {
				visitPro(a);
			}
			void visitFalse(False a) {
				visitPro(a);
			}
			void visitTrue(True a) {
				visitPro(a);
			}
			void visitUndefined(Undefined a) {
				visitPro(a);
			}
			void visitVec(Vec a) {
				visitPro(a);
			}
			void visitMap(Map a) {
				visitPro(a);
			}
			void visitAssoc(Assoc a) {
				visitPro(a);
			}
			void visitUsr(Usr a) {
				visitPro(a);
			}
		}
	}
}
