package net.yak.stalk;

import java.util.Hashtable;

import net.yak.stalk.Pro.Obj;
import net.yak.stalk.Talk.Terp.Frame;

public class Cls extends Obj {
	final Terp terp;
	final String name;
	Cls supercls; // tObjCls.supercls will be patched up, so not final.
	final Hashtable<String, Meth> meths;
	boolean trace;
	long countInstances;

	Cls(Cls cls, Terp terp, String name, Cls supercls) {
		super(cls);
		this.terp = terp;
		this.name = name;
		this.supercls = supercls;
		this.meths = new Hashtable<String, Meth>();
		this.trace = false;
		this.countInstances = 0;

		String key = name.toLowerCase();
		if (terp.glob.containsKey(key)) {
			Pro existing = terp.glob.get(name);
			if (existing instanceof Cls) {
				toss("Class named <%s> already exists", ((Cls) existing).name);
			} else {
				toss("Object named <%s> already exists in Globals: <%s>", name,
						existing.toString());
			}
		}
		// TODO: check abbreviation, and register abbrev.
		terp.glob.put(name.toLowerCase(), this);
	}
	Cls asCls() {
		return this;
	}
	public String toString() {
		return name;
	}
	public boolean instOf(Cls query) {
		for (Cls p = this; p != null; p = p.supercls) {
			if (p == query)
				return true;
		}
		return false;
	}
	void addMethod(Meth m) {
		assert m != null;
		assert m.abbrev != null;
		assert meths != null;
		meths.put(m.abbrev.toLowerCase(), m);
		meths.put(m.name.toLowerCase(), m);
	}
	Cls defineSubclass(String newname) {
		if (newname.endsWith("Cls")) {
			toss(
					"Don't define classes ending with 'Cls' using defineClass: name=<%s>",
					newname);
		}
		if (this.cls == terp().tMetacls) {
			toss(
					"Don't define classes on Metaclasses using defineClass: self=<%s>",
					this.cls.name);
		}
		if (terp.glob.containsKey(newname.toLowerCase())) {
			toss("Name <%s> already exists in Global Frame", newname);
		}
		if (terp.glob.containsKey(newname.toLowerCase() + "cls")) {
			toss("Name <%sCls> already exists in Global Frame", newname);
		}
		Cls newMetaCls = new Cls(terp.tMetacls, terp, newname + "Cls", this.cls);
		Cls newCls = new Cls(newMetaCls, terp, newname, this);
		terp.glob.dict.put(newMetaCls.name.toLowerCase(), newMetaCls);
		terp.glob.dict.put(newCls.name.toLowerCase(), newCls);
		return newCls;
	}
	static void addBuiltinMethodsForCls(final Terp terp) {
		terp.tCls.addMethod(new JavMeth(terp, "nam", null,
				"Return the name of the class.") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				Cls self = r.asCls();
				assert self != null;
				return new Str(terp, self.name);
			}
		});
		terp.tCls.addMethod(new JavMeth(terp, "superClas", "sup",
				"Return the superClass of the class.") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				Cls self = r.asCls();
				assert self != null;
				return self.supercls == null ? terp.instNil : self.supercls;
			}
		});
		terp.tCls.addMethod(new JavMeth(terp, "methodNames", "meths",
				"Return a Vec of the method names defined on the class.") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				Cls self = r.asCls();
				assert self != null;
				Vec z = new Vec(terp);
				for (String s : self.meths.keySet()) {
					z.vec.add(new Str(terp, s));
				}
				return z;
			}
		});
		terp.tCls.addMethod(new JavMeth(terp, "methodsDefs", "methd",
				"Return a Map of the method names defined on the class.") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				Cls self = r.asCls();
				assert self != null;
				Map z = new Map(terp);
				for (String s : self.meths.keySet()) {
					z.map.put(terp.newStr(s), self.meths.get(s));
				}
				return z;
			}
		});
		terp.tCls.addMethod(new JavMeth(terp, "defineSubclass:", "defsub:",
				"Define a new subclass of the class.") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				Terp terp = f.terp();
				assert args.length == 1;
				Str newnameStr = args[0].asStr();
				assert newnameStr != null;
				String newname = newnameStr.str;
				Cls self = r.asCls();
				return self.defineSubclass(newname);
			}
		});
		terp.tCls
				.addMethod(new JavMeth(terp,
						"defineMethod:abbrev:docStr:code:", "defmet:ab:ds:co:",
						"Define a method with name, abbrev, docString, and source code.") {
					public Pro apply(Frame f, Pro r, Pro[] args) {
						assert args.length == 4;

						Str methNameStr = args[0].asStr();
						assert methNameStr != null;
						String methName = methNameStr.str;

						Str abbrevStr = args[1].asStr();
						String abbrev = abbrevStr == null ? "" : abbrevStr.str; // e.g.
						// for
						// Nil.

						Str docStr = args[2].asStr();
						String doc = docStr == null ? "" : docStr.str; // e.g.
						// for
						// Nil

						Str methBodyStr = args[3].asStr();
						assert methBodyStr != null;
						String methBody = methBodyStr.str;

						Expr bodyExpr = Parser.parse(terp, methBody);
						Meth m = new UsrMeth(terp, methName, abbrev, doc,
								methBody, bodyExpr);
						r.asCls().addMethod(m);
						return m;
					}
				});
		terp.tCls.addMethod(new JavMeth(terp, "setTrace:", "trac:",
				"Set true to trace all calls of this method.") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				assert args.length == 1;
				r.asCls().trace = args[0].asBool();
				return r;
			}
		});
	}

	public static abstract class Meth extends Obj {
		String name;
		String abbrev;
		String doc;
		boolean trace;
		long countMessages;

		public Meth(Cls cls, String name, String abbrev, String doc) {
			super(cls);
			this.name = fixMethodName(name);
			this.abbrev = abbrev == null || abbrev.length() == 0 ? name
					: fixMethodName(abbrev);
			this.doc = doc;
			this.trace = false;
			this.countMessages = 0;
			assert this.name != null;
			assert this.abbrev != null;

		}
		public abstract Pro apply(Frame f, Pro r, Pro[] args);

		static String fixMethodName(String s) {
			if (s.contains(":")) {
				// Obviously it's intended to be a keyword method.
				String[] w = s.split(":");
				String z = "";
				for (int i = 0; i < w.length; i++) {
					if (w[i].length() == 0) {
						continue;
					}
					if (w[i].length() % 2 == 1) {
						System.err.println(s + "  |  " + w[i]);
						toss(
								"Component of keyword message <%s> has odd length; it should be even.",
								s);
					}
					z += w[i] + ':';
				}
				if (z.length() == 0) {
					toss("Empty keyword message not allowed.");
				}
				if (!Parser.multiKeywordP.matcher(z.toLowerCase()).matches()) {
					System.err.println(s + "<" + z + ">");
				}
				assert Parser.multiKeywordP.matcher(z.toLowerCase()).matches();
				return z;
			} else {
				// No colon -- depending on length, it could be unary or
				// keyword.
				if (Parser.unaryP.matcher(s).matches()) {
					// Unary it is!
					return s;
				} else {
					s = s + ':'; // It's going to need a colon.
					assert Parser.keywordP.matcher(s).matches();
					return s;
				}
			}
		}

	}

	public static abstract class JavMeth extends Meth {
		JavMeth(Terp t, String name, String abbrev) {
			super(t.tJavMeth, name, abbrev, "");
		}
		JavMeth(Terp t, String name, String abbrev, String doc) {
			super(t.tJavMeth, name, abbrev, doc);
		}
		public abstract Pro apply(Frame f, Pro r, Pro[] args);
		public String toString() {
			return fmt("\"Implemented in Java\"");
		}
	}

	public static class UsrMeth extends Meth {
		String src;
		Expr expr;

		UsrMeth(Terp terp, String name, String abbrev, String doc, String src,
				Expr expr) {
			// N.B. cls is the class of the UsrMeth instance (probably
			// terp.tUsrMeth),
			// not the class that it is a method on.
			super(terp.tJavMeth, name, abbrev, doc);
			this.src = src;
			this.expr = expr;
		}
		@Override
		public Pro apply(Frame f, Pro r, Pro[] args) {
			Frame f2 = f.terp().newFrame(f, r);
			f2.dict.put("self", r);
			for (int i = 0; i < args.length; i++) {
				f2.dict
						.put(new Character((char) ('a' + i)).toString(),
								args[i]);
			}
			Pro z = expr.eval(f2);
			return z;
		}
		public String toString() {
			return fmt("%s \n\n########## %s", src, expr.toString());
		}
	}
}
