package terse.vm;

import java.io.IOException;
import java.util.HashMap;
import java.util.SortedSet;
import java.util.TreeSet;

import terse.vm.Pro.Obj;
import terse.vm.Terp.Frame;

public final class Cls extends Obj {
	public final Terp terp;
	public final String name;
	public Cls supercls; // tObjCls.supercls will be patched up, so not final.
	final HashMap<String, Meth> meths;
	public boolean trace;
	long countInstances;
	HashMap<String, Integer> allVars; // Calculated.
	String[] myVars; // Authoritative.
	SortedSet<String> subclasses; // keep them lower.

	static int generationCounter = 1;
	public int generation;
	
	// Quick hack. TODO.
	HashMap<Integer, Pro> insts; 

	Cls(Cls cls, Terp terp, String name, Cls supercls) {
		super(cls);
		this.terp = terp;
		this.name = name;
		this.supercls = supercls;
		this.meths = new HashMap<String, Meth>();
		this.trace = false;
		this.countInstances = 0;
		this.subclasses = new TreeSet<String>();
		this.myVars = emptyStrs;
		this.insts = new HashMap<Integer, Pro>();

		String key = name.toLowerCase();
		if (terp.clss.containsKey(key)) {
			Pro existing = terp.clss.get(name);
			if (existing instanceof Cls) {
				terp.toss("Class named <%s> already exists",
						((Cls) existing).name);
			} else {
				terp.toss("Object named <%s> already exists in Globals: <%s>",
						name, existing.toString());
			}
		}
		terp.clss.put(name.toLowerCase(), this);

		this.allVars = new HashMap<String, Integer>();
		if (supercls != null) {
			// Object has no supercls, and during bootstrapping it can be null.
			supercls.subclasses.add(this.name.toLowerCase());
			recalculateAllVars(); // Not used.
		}
	}

	private void setNextGeneration() {
		this.generation = generationCounter;
		++generationCounter;
	}

	private void recalculateAllVars() {
		this.allVars.clear();
		if (supercls != null) {
			this.allVars.putAll(supercls.allVars);
		}
		int varNum = this.allVars.size();
		for (String k : myVars) {
			this.allVars.put(k, varNum);
			++varNum;
		}
	}

	// Send this to tPro, and let it recurse.
	void recalculateAllVarsHereAndBelow() {
		// First do my own (trusting that supercls.allVars is correct).
		recalculateAllVars();
		// Bust cache.
		// TODO: Right idea, but wrong method. Really we need to recompile all
		// methods to get correct field indices.
		setNextGeneration();
		// Recurse to subclasses.
		for (String s : subclasses) {
			Pro p = terp.clss.get(s.toLowerCase());
			if (p == null) {
				terp.toss("Cls <%s> not found in globals", s);
			}
			Cls c = p.asCls();
			if (c == null) {
				terp.toss("Cls <%s> is not a Cls in globals", s);
			}
			c.recalculateAllVarsHereAndBelow();
		}
	}

	public 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);

		if (m instanceof UsrMeth) {
			String[] lines = ((UsrMeth) m).src.split("\n");
			try {
				terp.appendImageFile(fmt("meth %s %s", name, m.name), lines);
				if (m.abbrev.length() > 0 && !m.abbrev.equals(m.name)) {
					terp.appendImageFile(fmt("meth %s %s", name, m.abbrev),
							lines);
				}
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
				terp.toss("Cannot write image file: " + e);
			}
		}
	}

	Cls defineSubclass(String newname) {
		if (newname.endsWith("Cls")) {
			terp
					.toss(
							"Don't define classes ending with 'Cls' using defineClass: name=<%s>",
							newname);
		}
		if (this.cls == terp().tMetacls) {
			terp
					.toss(
							"Don't define classes on Metaclasses using defineClass: self=<%s>",
							this.cls.name);
		}
		if (terp.clss.containsKey(newname.toLowerCase())) {
			terp.toss("Name <%s> already exists in Global Frame", newname);
		}
		if (terp.clss.containsKey(newname.toLowerCase() + "cls")) {
			terp.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.clss.put(newMetaCls.name.toLowerCase(), newMetaCls);
		terp.clss.put(newCls.name.toLowerCase(), newCls);

		try {
			terp.appendImageFile(fmt("class %s %s", newname, name), null);
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			terp.toss("Cannot write image file: " + e);
		}

		return newCls;
	}

	static void addBuiltinMethodsForCls(final Terp terp) {
		terp.tCls.addMethod(new JavaMeth(terp.tCls, "findInst:", null,
				"Return the name of the class.") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				Cls self = r.asCls();
				Num num = args[0].asNum();
				Pro z = num == null? terp.instNil : self.insts.get(num.num);
				return z == null ? terp.instNil : z;
			}
		});
		terp.tCls.addMethod(new JavaMeth(terp.tCls, "name", null,
				"Return the name of the class.") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				Cls self = r.asCls();
				assert self != null;
				return terp.newStr(self.name);
			}
		});
		terp.tCls.addMethod(new JavaMeth(terp.tCls, "superClass", "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 JavaMeth(terp.tCls, "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 JavaMeth(terp.tCls, "methodsDefs", "methDefs",
				"Return a Dict of the method names defined on the class.") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				Cls self = r.asCls();
				assert self != null;
				Dict z = new Dict(terp);
				for (String s : self.meths.keySet()) {
					z.dict.put(terp.newStr(s), self.meths.get(s));
				}
				return z;
			}
		});
		terp.tCls.addMethod(new JavaMeth(terp.tCls, "defineSubclass:",
				"defSub:", "Define a new subclass of the class.") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				Str newnameStr = args[0].asStr();
				assert newnameStr != null;
				String newname = newnameStr.str;
				Cls self = r.asCls();
				return self.defineSubclass(newname);
			}
		});
		terp.tCls.addMethod(new JavaMeth(terp.tCls, "defineInstanceVars:",
				"defVars:", "Define instance variables, "
						+ "using space-separated string.") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				Str varsStr = args[0].asStr();
				if (varsStr == null) {
					terp.say("Expected Str with inst var names");
				}
				String[] myVarNames = filterOutEmptyStrings(varsStr.str
						.split("\\s+"));
				Cls self = r.asCls();

				// Check for Ignore-Case uniqueness.
				for (int i = 0; i < myVarNames.length; i++) {
					String iStr = myVarNames[i];
					String iLow = iStr.toLowerCase();
					for (Cls c = self.supercls; c != null; c = c.supercls) {
						for (String y : c.myVars) {
							if (iLow.equals(y.toLowerCase())) {
								terp
										.toss(
												"Cannot add inst var <%s> because Cls <%s> also has <%s>",
												iStr, c.name, y);
							}
						}
						for (int j = i + 1; j < myVarNames.length; j++) {
							if (iLow.equals(myVarNames[j].toLowerCase())) {
								terp
										.toss(
												"Cannot add 2 inst vars <%s> <%s> with same name.",
												iStr, myVarNames[j]);
							}
						}
					}
				}

				self.myVars = myVarNames;
				terp.tPro.recalculateAllVarsHereAndBelow();

				try {
					terp.appendImageFile(fmt("instvars %s", self.name), Terp.strs(varsStr.str));
				} catch (IOException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
					terp.toss("Cannot write image file: " + e);
				}

				return r;
			}
		});
		terp.tCls.addMethod(new JavaMeth(terp.tCls,
				"defineMethod:abbrev:doc:code:", "defMeth:a:d:c:", "") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				assert args.length == 4;
				Cls self = r.asCls();

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

				// I've been indicisive about using "/" or ":". Allow both.
				methName = methName.replaceAll("/", ":");

				Str abbrevStr = args[1].asStr();
				String abbrev = abbrevStr == null ? "" : abbrevStr.str;

				Str docStr = args[2].asStr();
				String doc = docStr == null ? "" : docStr.str;

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

				Expr.Top top = Parser.parseMethod(self, methName, methBody);
				Meth m = new UsrMeth(self, methName, abbrev, doc, methBody, top);
				self.addMethod(m);
				return m;
			}
		});
		terp.tCls.addMethod(new JavaMeth(terp.tCls, "setTrace:", "trace:",
				"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].truth();
				return r;
			}
		});
		terp.tClsCls.addMethod(new JavaMeth(terp.tClsCls, "allClassesDict",
				"all") {
			public Pro apply(Frame f, Pro r, Pro[] args) {
				assert args.length == 0;
				Pro[] arr = new Pro[terp.clss.size()];
				int i = 0;
				for (String k : terp.clss.keySet()) {
					Pro key = terp.newStr(k);
					Pro value = terp.clss.get(k);
					arr[i] = new Vec(pros(key, value));
					++i;
				}
				return terp.newDict(arr);
			}
		});
	}

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

		public Meth(Cls cls, Cls onCls, String name, String abbrev, String doc) {
			super(cls);
			this.onCls = onCls;
			this.name = name;
			this.abbrev = abbrev == null || abbrev.length() == 0 ? name
					: 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);
	}

	/**
	 * Meth implemented in Java. Notice that the CTOR automatically adds the
	 * method to onCls!
	 */
	public static abstract class JavaMeth extends Meth {
		JavaMeth(Cls onCls, String name, String abbrev) {
			super(onCls.terp.tJavMeth, onCls, name, abbrev, "");
			onCls.addMethod(this); // NOTA BENE
		}

		JavaMeth(Cls onCls, String name, String abbrev, String doc) {
			super(onCls.terp.tJavMeth, onCls, name, abbrev, doc);
		}

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

		public String toString() {
			return fmt("\"<%s %s> Built-In Method\"", onCls, name);
		}
	}

	public static abstract class StrMeth extends JavaMeth {
		StrMeth(Cls onCls, String name, String abbrev, String doc) {
			super(onCls, name, abbrev, doc);
		}

		public Pro apply(Frame f, Pro r, Pro[] args) {
			assert args.length == 1;
			return apply1(f, r, (Str) args[0]);
		}

		public abstract Pro apply1(Frame f, Pro r, Str a);
	}

	public static class UsrMeth extends Meth {
		String src;
		Expr.Top top;

		UsrMeth(Cls onCls, String name, String abbrev, String doc, String src,
				Expr.Top expr) {
			// N.B. cls is the class of the UsrMeth instance (probably
			// terp.tUsrMeth),
			// not the class that it is a method on.
			super(onCls.terp.tJavMeth, onCls, name, abbrev, doc);
			this.src = src;
			this.top = expr;
		}

		@Override
		public Pro apply(Frame f, Pro r, Pro[] args) {
			assert args.length == top.numArgs;

			Frame f2 = f.terp().newFrame(f, r, top);

			for (int i = 0; i < args.length; i++) {
				f2.locals[i] = args[i];
			}

			Pro z = top.eval(f2);
			return z;
		}

		public String toString() {
			// return fmt("%s \n\n##########\n\n%s", src, top.toPrettyString());
			return fmt("%s", src, top.toPrettyString());
		}
	}
}
