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 = 10;  // Skip to 10; we may need to backfill a couple.

		String key = name.toLowerCase();
		if (terp.glob.contains(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) {
		// say("Add %s Method as %s", name, m.name);
		meths.put(m.name.toLowerCase(), m);
		if (m.abbrev != null) {
			// say("Add %s Method ALSO as %s", name, m.abbrev);
			meths.put(m.abbrev, m);
		}
	}

	static void addBuiltinMethodsForCls(final Terp terp) {
		terp.tCls.addMethod(new JavMeth(terp, "name") {
			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, "superclass") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				Cls self = r.asCls();
				assert self != null;
				return self.supercls;
			}
		});
		terp.tCls.addMethod(new JavMeth(terp, "methods") {
			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, "defineSubclass:") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				assert args.length == 1;
				Str newnameStr = args[0].asStr();
				assert newnameStr != null;
				String newname = newnameStr.str;
				Cls newMetaCls = new Cls(terp.tMetacls, terp, newname + "Cls",
						r.cls);
				Cls newCls = new Cls(newMetaCls, terp, newname, r.asCls());
				terp.glob.dict.put(newMetaCls.name.toLowerCase(), newMetaCls);
				terp.glob.dict.put(newCls.name.toLowerCase(), newCls);
				return newCls;
			}
		});
		terp.tCls.addMethod(new JavMeth(terp, "defineMethod:as:") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				assert args.length == 2;

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

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

				Expr body = Parser.parse(terp, methBody);
				Meth m = new UsrMeth(terp, methName, methBody, body);
				r.asCls().addMethod(m);
				return m;
			}
		});
		terp.tCls.addMethod(new JavMeth(terp, "trac:") {
			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;
		boolean trace;
		long countMessages;

		public Meth(Cls cls, String name) {
			super(cls);
			this.name = name;
			this.abbrev = AbbreviateMethodName(name);
			this.trace = false;
			this.countMessages = 0;
		}
		public abstract Pro apply(Frame f, Pro r, Pro[] args);
	}

	public static abstract class JavMeth extends Meth {
		JavMeth(Terp t, String name) {
			super(t.tJavMeth, name);
			this.name = name;
		}
		public abstract Pro apply(Frame f, Pro r, Pro[] args);
	}

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

		UsrMeth(Terp terp, String name, 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);
			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;
		}
	}
}
