package net.yak.stalk;

import java.util.Hashtable;
import java.util.Vector;

import net.yak.stalk.Cls.JavMeth;
import net.yak.stalk.Cls.Meth;
import net.yak.stalk.Talk.Terp.Frame;

public class Pro extends Talk implements Comparable<Pro> {
	Cls cls;

	public Pro(Cls cls) {
		this.cls = cls;
	}
	Terp terp() {
		return cls.terp;
	}
	Num asNum() {
		return null;
	}
	Str asStr() {
		return null;
	}
	Cls asCls() {
		return null;
	}
	Exc asExc() {
		return null;
	}
	Err asErr() {
		return null;
	}
	Rtx asRtx() {
		return null;
	}
	Rtn asRtn() {
		return null;
	}
	Brk asBrk() {
		return null;
	}
	Blk asBlk() {
		return null;
	}
	Nil asNil() {
		return null;
	}
	Usr asUsr() {
		return null;
	}
	Boolean asBool() {
		return null;
	}
	Vec asVec() {
		return null;
	}
	Map asMap() {
		return null;
	}
	@SuppressWarnings("unchecked")
	Comparable<Object> innerObject() {
		return (Comparable<Object>) (Object) this;
	}
	public int hashCode() {
		return innerObject().hashCode();
	}
	public boolean equals(Object obj) {
		if (obj instanceof Pro) {
			Pro that = (Pro) obj;
			return this.innerObject().equals(that.innerObject());
		} else {
			return false;
		}
	}
	public int compareTo(Pro that) {
		if (this.cls != that.cls) {
			return this.cls.name.compareTo(that.cls.name);
		}
		Comparable<Object> a = this.innerObject();
		Comparable<Object> b = that.innerObject();
		if (a != (Object) this) {
			// Not Equal because innerObject returned Value-like objects.
			return a.compareTo(b);
		} else {
			return new Integer(System.identityHashCode(this))
					.compareTo(new Integer(System.identityHashCode(that)));
		}
	}

	static Hashtable<String, Meth> builtinObjMethods(Terp t) {
		Hashtable<String, Meth> z = new Hashtable<String, Meth>();
		z.put("cls", new JavMeth(t.tJavMeth) {
			public Pro eval(Frame f, Pro r, Pro[] args) {
				return r.cls;
			}
		});
		return z;
	}

	public static abstract class Obj extends Pro {
		public Obj(Cls cls) {
			super(cls);
		}
		Obj asObj() {
			return this;
		}
	}

	public static abstract class Exc extends Pro { // abstract Exception
		Pro guts;

		public Exc(Cls cls, Pro guts) {
			super(cls);
			this.guts = guts;
		}
		Exc asExc() {
			return this;
		}
	}

	public static class Err extends Exc { // Error Exception
		public Err(Terp t, Pro guts) {
			super(t.tErr, guts);
		}
		public Err(Terp t, String msg, Object... objects) {
			super(t.tErr, new Str(t, fmt(msg, objects)));
		}
		Err asErr() {
			return this;
		}
		public String toString() {
			return fmt("Err({0})", guts);
		}
	}

	public static class Rtx extends Exc { // Java Runtime Exception Error
		public Rtx(Terp t, RuntimeException x) {
			super(t.tRtx, new Str(t, x.toString()));
		}
		Rtx asRtx() {
			return this;
		}
		public String toString() {
			return fmt("Rtx({0})", guts);
		}
	}

	public static class Rtn extends Exc { // Return Exception
		public Rtn(Terp t, Pro guts) {
			super(t.tRtn, guts);
		}
		Rtn asRtn() {
			return this;
		}
		public String toString() {
			return fmt("Rtn({0})", guts);
		}
	}

	public static class Brk extends Exc { // Break Exception
		public Brk(Terp t, Pro guts) {
			super(t.tBrk, guts);
		}
		Brk asBrk() {
			return this;
		}
		public String toString() {
			return fmt("Brk({0})", guts);
		}
	}

	public Err xErr(Pro guts) {
		return new Err(cls.terp, guts);
	}
	public Err xRtx(Pro guts) {
		return new Err(cls.terp, guts);
	}
	public Err xRtn(Pro guts) {
		return new Err(cls.terp, guts);
	}
	public Err xBrk(Pro guts) {
		return new Err(cls.terp, guts);
	}

	public static class Nil extends Obj { // Nil
		public Nil(Cls cls) {
			super(cls);
		}
		Nil asNil() {
			return this;
		}
		public String toString() {
			return "/N";
		}
	}

	public static class Fal extends Obj { // False
		public Fal(Cls cls) {
			super(cls);
		}
		Boolean asBool() {
			return Boolean.FALSE;
		}
		public String toString() {
			return "/F";
		}
		static Hashtable<String, Meth> builtinFalMethods(Terp t) {
			Hashtable<String, Meth> z = new Hashtable<String, Meth>();
			z.put("no:", new JavMeth(t.tJavMeth) {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					Blk blk = args[0].asBlk();
					// TODO: Create child frame.
					return blk.body.eval(f);
				}
			});
			z.put("ye:", new JavMeth(t.tJavMeth) {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					// Don't eval the block. Return Nil.
					return f.terp().instNil;
				}
			});
			return z;
		}
	}

	public static class Tru extends Obj { // True
		public Tru(Cls cls) {
			super(cls);
		}
		Boolean asBool() {
			return Boolean.TRUE;
		}
		public String toString() {
			return "/T";
		}
		static Hashtable<String, Meth> builtinTruMethods(Terp t) {
			Hashtable<String, Meth> z = new Hashtable<String, Meth>();
			z.put("ye:", new JavMeth(t.tJavMeth) {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					Blk blk = args[0].asBlk();
					// TODO: Create child frame.
					return blk.body.eval(f);
				}
			});
			z.put("no:", new JavMeth(t.tJavMeth) {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					// Don't eval the block. Return Nil.
					return f.terp().instNil;
				}
			});
			return z;
		}
	}

	public static class Blk extends Obj {
		Expr body;
		String params[];
		Frame parent;

		public Blk(Cls cls, Expr body, String params[], Frame parent) {
			super(cls);
			this.body = body;
			this.params = params;
			this.parent = parent;
		}
		Blk asBlk() {
			return this;
		}
		public String toString() {
			return fmt("Blk({0}, {1})", params, body);
		}

		static Hashtable<String, Meth> builtinBlkMethods(Terp t) {
			Hashtable<String, Meth> z = new Hashtable<String, Meth>();
			z.put("run", new JavMeth(t.tJavMeth) {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					// TODO: Create child frame.
					return blk.body.eval(f);
				}
			});
			return z;
		}
	}

	public static class Usr extends Obj {
		Pro[] vars;

		public Usr(Cls cls) {
			super(cls);
			this.vars = new Pro['z' - 'p' + 1];
		}
		Usr asUsr() {
			return this;
		}
		public void setVar(int i, Pro value) {
			vars[i] = value;
		}
		public Pro getVar(int i) {
			Pro z = vars[i];
			if (z == null) {
				return new Err(terp(),
						"Getting unset instance var <{0}> of Usr object",
						Character.toString((char) ('p' + i)));
			}
			return z;
		}

		static Hashtable<String, Meth> builtinUsrClsMethods(Terp t) {
			Hashtable<String, Meth> z = new Hashtable<String, Meth>();
			z.put("new", new JavMeth(t.tJavMeth) {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					return new Usr(r.asCls());
				}
			});
			return z;
		}
	}

	public static class Num extends Obj {
		double num;

		Num(Terp t, double num) {
			super(t.tNum);
			this.num = num;
		}
		Num asNum() {
			return this;
		}
		public String toString() {
			return fmt("Num({0})", num);
		}
		Comparable innerObject() {
			return new Double(num);
		}

		abstract static class BinaryNumMeth extends JavMeth {
			BinaryNumMeth(Cls cls) {
				super(cls);
			}
			public Pro eval(Frame f, Pro r, Pro[] args) {
				assert args.length == 1;
				Num s = r.asNum();
				Num a = args[0].asNum();
				if (s == null) {
					toss("Receiver of Num::pl not a Num");
				}
				if (a == null) {
					toss("Argument of Num::pl not a Num");
				}
				return new Num(f.terp(), compute(s.num, a.num));
			}
			abstract double compute(double snum, double anum);
		}

		abstract static class UnaryNumMeth extends JavMeth {
			UnaryNumMeth(Cls cls) {
				super(cls);
			}
			public Pro eval(Frame f, Pro r, Pro[] args) {
				assert args.length == 0;
				Num s = r.asNum();
				if (s == null) {
					toss("Receiver of Num::pl not a Num");
				}
				return new Num(f.terp(), compute(s.num));
			}
			abstract double compute(double anum);
		}

		static Hashtable<String, Meth> builtinNumMethods(Terp t) {
			Hashtable<String, Meth> z = new Hashtable<String, Meth>();
			z.put("pl:", new BinaryNumMeth(t.tJavMeth) {
				double compute(double snum, double anum) {
					return snum + anum;
				}
			});
			z.put("mi:", new BinaryNumMeth(t.tJavMeth) {
				double compute(double snum, double anum) {
					return snum - anum;
				}
			});
			z.put("ti:", new BinaryNumMeth(t.tJavMeth) {
				double compute(double snum, double anum) {
					return snum * anum;
				}
			});
			z.put("fdiv:", new BinaryNumMeth(t.tJavMeth) {
				double compute(double snum, double anum) {
					return snum / anum;
				}
			});
			z.put("idiv:", new BinaryNumMeth(t.tJavMeth) {
				double compute(double snum, double anum) {
					return (long) snum / (long) anum;
				}
			});
			z.put("imod:", new BinaryNumMeth(t.tJavMeth) {
				double compute(double snum, double anum) {
					return (long) snum % (long) anum;
				}
			});
			z.put("neg", new UnaryNumMeth(t.tJavMeth) {
				double compute(double snum) {
					return -snum;
				}
			});
			z.put("sgn", new UnaryNumMeth(t.tJavMeth) {
				double compute(double snum) {
					return snum < 0 ? -1 : snum > 0 ? 1 : 0;
				}
			});
			z.put("int", new UnaryNumMeth(t.tJavMeth) {
				double compute(double snum) {
					return (long) snum;
				}
			});
			return z;
		}
	}

	public static class Str extends Obj {
		String str;

		Str(Terp t, String str) {
			super(t.tStr);
			this.str = str;
		}
		Str asStr() {
			return this;
		}
		public String toString() {
			return fmt("Str(\"{0}\")", str);
		}
		@SuppressWarnings("unchecked")
		Comparable innerObject() {
			return str;
		}
	}

	public static class Vec extends Obj {
		Vector<Pro> vec;

		Vec(Terp t) {
			super(t.tVec);
			this.vec = new Vector<Pro>();
		}
		Vec asVec() {
			return this;
		}
		int convertIndex(Pro a) {
			Num num = a.asNum();
			if (num == null) {
				toss("Index is a {0}, not a Num, in <Vec at:>", a.cls.name);
			}
			int i = (int) Math.floor(num.num + 0.5); // closest int
			if (i < 0) { // Allow small negative shortcuts.
				i += vec.size();
			}
			return i;
		}
		static Hashtable<String, Meth> builtinVecMethods(final Terp t) {
			Hashtable<String, Meth> z = new Hashtable<String, Meth>();
			z.put("n", new JavMeth(t.tJavMeth) {
				public Num eval(Frame f, Pro r, Pro[] args) {
					return new Num(t, r.asVec().vec.size());
				}
			});
			z.put("at:", new JavMeth(t.tJavMeth) {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					Vec self = r.asVec();
					return self.vec.get(self.convertIndex(args[0]));
				}
			});
			z.put("at:pu:", new JavMeth(t.tJavMeth) {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					Vec self = r.asVec();
					self.vec.set(self.convertIndex(args[0]), args[1]);
					return r;
				}
			});
			z.put("setn:", new JavMeth(t.tJavMeth) {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					Vec self = r.asVec();
					int oldSize = self.vec.size();
					int newSize = self.convertIndex(args[0]);
					self.vec.setSize(newSize);
					for (int i = oldSize; i < newSize; i++) {
						self.vec.set(i, t.tNil);
					}
					return r;
				}
			});
			return z;
		}
		static Hashtable<String, Meth> builtinVecClsMethods(final Terp t) {
			Hashtable<String, Meth> z = new Hashtable<String, Meth>();
			z.put("new", new JavMeth(t.tJavMeth) {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					return new Vec(t);
				}
			});
			return z;
		}
	}

	public static class Map extends Obj {
		Hashtable<Pro, Pro> map;

		Map(Terp t) {
			super(t.tMap);
			this.map = new Hashtable<Pro, Pro>();
		}
		Map asMap() {
			return this;
		}

		static Hashtable<String, Meth> builtinMapMethods(final Terp t) {
			Hashtable<String, Meth> z = new Hashtable<String, Meth>();
			z.put("n", new JavMeth(t.tJavMeth) {
				public Num eval(Frame f, Pro r, Pro[] args) {
					return new Num(t, r.asMap().map.size());
				}
			});
			z.put("at:", new JavMeth(t.tJavMeth) {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					Map self = r.asMap();
					Pro z = self.map.get(args[0]);
					if (z == null) {
						toss("Did not find key <{0}> in Map", args[0]);
					}
					return z;
				}
			});
			z.put("at:pu:", new JavMeth(t.tJavMeth) {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					Map self = r.asMap();
					self.map.put(args[0], args[1]);
					return r;
				}
			});
			return z;
		}
		static Hashtable<String, Meth> builtinMapClsMethods(final Terp t) {
			Hashtable<String, Meth> z = new Hashtable<String, Meth>();
			z.put("new", new JavMeth(t.tJavMeth) {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					return new Map(t);
				}
			});
			return z;
		}
	}
}
