package net.yak.stalk;

import net.yak.stalk.Cls.Meth;
import net.yak.stalk.Pro.Blk;
import net.yak.stalk.Talk.Terp.Frame;

public abstract class Expr extends Talk {

	abstract Pro eval(Frame f);

	static String arrayToString(Expr[] a) {
		String z = "[ ";
		for (Expr e : a) {
			z += fmt("%s, ", e.toString());
		}
		return z + "]";
	}
	static String arrayToString(Pro[] a) {
		String z = "[ ";
		for (Pro e : a) {
			z += fmt("%s, ", e.toString());
		}
		return z + "]";
	}
	static String arrayToString(String[] a) {
		if (a == null)
			return "<NULL[]>";
		String z = "[ ";
		for (String e : a) {
			z += fmt("\"%s\", ", e);
		}
		return z + "]";
	}

	static class PutInstVar extends Expr {
		String name;
		Expr expr;

		public PutInstVar(String name, Expr expr) {
			super();
			this.name = name;
			this.expr = expr;
		}
		Pro eval(Frame f) {
			Usr u = f.self.asUsr();
			if (u == null) {
				toss("Putting inst var on non-Usr object");
			}
			Pro x = expr.eval(f);
			if (u.cls.trace)
				say("PutInstVar: %s#%s /%s := %s#%s", u.cls.name, u.toString(),
						name, x.cls.name, x.toString());
			u.setVar(name, x);
			return x;
		}
		public String toString() {
			return fmt("/%s= %s", name, expr);
		}
	}

	static class GetInstVar extends Expr {
		String name;

		public GetInstVar(String name) {
			super();
			this.name = name;
		}
		Pro eval(Frame f) {
			Usr u = f.self.asUsr();
			if (u == null) {
				toss("Putting inst var on non-Usr object");
			}
			Pro z = u.getVar(name);
			if (z == null) {
				toss("Instance Variable <%s> was never set.", name);
			}
			return z;
		}
		public String toString() {
			return fmt("%s ", name);
		}
	}

	static class GetLocalVar extends Expr {
		String name;

		GetLocalVar(String key) {
			this.name = key;
		}
		Pro eval(Frame f) {
			Pro z = f.get(name);
			if (z == null) {
				toss("Local Variable <%s> was never set.", name);
			}
			return z;
		}
		public String toString() {
			return fmt("%s ", name);
		}
	}

	static class GetGlobalVar extends Expr {
		String key;

		GetGlobalVar(String key) {
			this.key = key;
		}
		Pro eval(Frame f) {
			Pro z = f.globals().get(key);
			if (z == null) {
				toss("Global Variable <%s> was never set.", key);
			}
			return z;
		}
		public String toString() {
			return fmt("%s ", key);
		}
	}

	static class PutLocalVar extends Expr {
		String key;
		Expr expr;

		PutLocalVar(String key, Expr expr) {
			this.key = key;
			this.expr = expr;
		}
		Pro eval(Frame f) {
			Pro value = expr.eval(f);
			f.put(key, value);
			// say("PutLocalVar: Putting <%s> to %s.", value, key);
			return value;
		}
		public String toString() {
			return fmt("%s= %s", key, expr);
		}
	}

	static class PutGlobalVar extends Expr {
		String key;
		Expr expr;

		PutGlobalVar(String key, Expr expr) {
			this.key = key;
			this.expr = expr;
		}
		Pro eval(Frame f) {
			Pro value = expr.eval(f);
			f.globals().put(key, value);
			return value;
		}
		public String toString() {
			return fmt("%s= %s", key, expr);
		}
	}

	static class Send extends Expr {
		Expr rcvr;
		String msg;
		Expr args[];

		Send(Expr rcvr, String msg, Expr args[]) {
			this.rcvr = rcvr;
			this.msg = msg;
			this.args = args;
		}

		Meth findMeth(Pro r) {
			for (Cls c = r.cls; c != null; c = c.supercls) {
				Meth m = c.meths.get(msg);
				if (m != null)
					return m;
			}
			// If above fails, list the possibilities.
			for (Cls c = r.cls; c != null; c = c.supercls) {
				say("Visiting class <%s>", c.name);
				for (String k : c.meths.keySet()) {
					say("Method <%s> exits on class <%s>", k, c.name);
				}
			}
			say("Couldn't find method <%s> on class <%s>", msg, r.cls.name);
			return null;
		}

		@Override
		Pro eval(Frame f) {
			Pro r = rcvr.eval(f);
			assert r != null;
			Pro a[] = new Pro[args.length];
			for (int i = 0; i < args.length; i++) {
				a[i] = args[i].eval(f);
				assert a[i] != null;
			}
			Meth m = findMeth(r);
			if (m == null) {
				toss(
						"MessageNotUnderstood: message %s to instance of class %s: <%s>",
						msg, r.cls.name, r);
			}
			if (r.cls.trace || m.trace) {
				say("Sending message %s to %s#%s with %s", m.name, r.cls.name,
						r.toString(), arrayToString(args));
			}
			Pro z = null;
			try {
				z = m.apply(f, r, a);
			} catch (Exception ex) {
				String what = fmt("\n  * During SEND <%s ^ %s> TO %s WITH %s",
						r.cls.name, m.name, r.toString(), arrayToString(a));
				toss(ex.toString() + what);
			}
			if (r.cls.trace || m.trace) {
				say("Message %s to %s#%s returns %s#%s", m.name, r.cls.name, r
						.toString(), z.cls.name, z.toString());
			}
			return z;
		}
		
		public String toString() {
			if (args.length == 0) {
				return fmt("( %s ) %s ", rcvr, msg);
			} else {
				StringBuffer sb = new StringBuffer();
				sb.append(fmt("( %s ) ", rcvr.toString()));
				String[] words = msg.split(":");
				for (int i = 0; i < args.length; i++) {

					sb.append(fmt("%s: ( %s ) ", words[i], args[i].toString()));
				}
				return sb.toString();
			}
		}
	}

	static class Block extends Expr {
		Expr body;
		String[] params;

		public Block(Expr body, String[] params) {
			this.body = body;
			this.params = params;
		}
		@Override
		Pro eval(Frame f) {
			return new Blk(body, f);
		}
		public String toString() {
			return "[ " + body.toString() + "] ";
		}
	}

	static class Seq extends Expr {
		Expr[] body; // Eval these in order; return value of last.

		public Seq(Expr[] body) {
			this.body = body;
		}
		Pro eval(Frame f) {
			Pro z = f.terp().instNil;
			for (Expr expr : body) {
				z = expr.eval(f);
				assert z != null;
			}
			return z;
		}
		public String toString() {
			StringBuffer sb = new StringBuffer();
			for (int i = 0; i < body.length; i++) {
				sb.append(body[i].toString());
				sb.append(" . ");
			}
			return sb.toString();
		}
	}

	static class Lit extends Expr {
		Pro value; // Literal value.

		public Lit(Pro value) {
			this.value = value;
		}
		Pro eval(Frame f) {
			return value;
		}
		public String toString() {
			return fmt("%s", value);
		}
	}
}
