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;
	}
	Blk asBlk() {
		return null;
	}
	NilObj asNil() {
		return null;
	}
	Usr asUsr() {
		return null;
	}
	Boolean asBool() {
		return null;
	}
	Vec asVec() {
		return null;
	}
	Map asMap() {
		return null;
	}
	Pro boolPro(Terp t, boolean x) {
		return x ? t.instTrue : t.instFalse;
	}
	@SuppressWarnings("unchecked")
	Comparable<Object> innerObject() {
		return (Comparable<Object>) (Object) this;
	}
	public int hashCode() {
		Object innerThis = this.innerObject();
		if (innerThis == this) {
			// Avoid infinite recursion.
			return System.identityHashCode(this);
		} else {
			return innerObject().hashCode();
		}
	}
	public boolean equals(Object obj) {
		if (obj instanceof Pro) {
			Pro that = (Pro) obj;
			Object innerThis = this.innerObject();
			Object innerThat = that.innerObject();
			if (innerThis != this && innerThat != that) {
				// Both inner objects were different; compare them.
				return this.innerObject().equals(that.innerObject());
			} else {
				// Avoid infinite recursion.
				return this == that;
			}
		} 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)));
		}
	}

	int toNearestInt() {
		Num num = this.asNum();
		if (num == null) {
			toss("Object is a {0}, not a Num", this.cls.name);
		}
		int i = (int) Math.floor(num.num + 0.5); // closest int
		return i;
	}


	static void addBuiltinMethodsForPro(Terp terp) {
		terp.tPro.addMethod(new JavMeth(terp, "cls") {
			@Override
			public Pro eval(Frame f, Pro r, Pro[] args) {
				return r.cls;
			}
		});
	}

	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 NilObj extends Obj { // Nil
		public NilObj(Cls cls) {
			super(cls);
		}
		NilObj asNil() {
			return this;
		}
		public String toString() {
			return "Nil";
		}
		static void addBuiltinMethodsForFal(Terp terp) {
			// N.B.  We are adding these to "FalCls", not "Fal".
			terp.tFalseObj.addMethod(new JavMeth(terp, "ifNil:") {
				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);
				}
			});
			terp.tFalseObj.addMethod(new JavMeth(terp, "ifNotNil:") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					return f.terp().instNil;
				}
			});
		}
	}

	// TODO: shouldn't this be "class FalCls" instead?
	public static class FalseObj extends Obj { // False
		public FalseObj(Cls cls) {
			super(cls);
		}
		Boolean asBool() {
			return Boolean.FALSE;
		}
		public String toString() {
			return "Fal";
		}
		static void addBuiltinMethodsForFal(Terp terp) {
			// N.B.  We are adding these to "FalCls", not "Fal".
			terp.tFalseObj.addMethod(new JavMeth(terp, "no:") {
				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);
				}
			});
			terp.tTrueObj.addMethod(new JavMeth(terp, "yes:no:") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					assert args.length == 2;
					Blk blk = args[1].asBlk();
					// TODO: Create child frame.
					return blk.body.eval(f);
				}
			});
			terp.tTrueObj.addMethod(new JavMeth(terp, "no:yes:") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					assert args.length == 2;
					Blk blk = args[0].asBlk();
					// TODO: Create child frame.
					return blk.body.eval(f);
				}
			});
			terp.tFalseObj.addMethod(new JavMeth(terp, "yes:") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					// Don't eval the block. Return Nil.
					return f.terp().instNil;
				}
			});
		}
	}

	public static class TrueObj extends Obj { // True
		public TrueObj(Cls cls) {
			super(cls);
		}
		Boolean asBool() {
			return Boolean.TRUE;
		}
		public String toString() {
			return "Tru";
		}
		static void addBuiltinMethodsForTru(Terp terp) {
			terp.tTrueObj.addMethod(new JavMeth(terp, "yes:") {
				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);
				}
			});
			terp.tTrueObj.addMethod(new JavMeth(terp, "yes:no:") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					assert args.length == 2;
					Blk blk = args[0].asBlk();
					// TODO: Create child frame.
					return blk.body.eval(f);
				}
			});
			terp.tTrueObj.addMethod(new JavMeth(terp, "no:yes:") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					assert args.length == 2;
					Blk blk = args[1].asBlk();
					// TODO: Create child frame.
					return blk.body.eval(f);
				}
			});
			terp.tTrueObj.addMethod(new JavMeth(terp, "no:") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					// Don't eval the block. Return Nil.
					return f.terp().instNil;
				}
			});
		}
	}

	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 void addBuiltinMethodsForBlk(Terp terp) {
			terp.tBlk.addMethod(new JavMeth(terp, "run") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					// TODO: Create child frame.
					return blk.body.eval(f);
				}
			});
		}
	}

	public static class Usr extends Obj {
		Hashtable<String, Pro> vars;

		public Usr(Cls cls) {
			super(cls);
			this.vars = new Hashtable<String, Pro>();
		}
		Usr asUsr() {
			return this;
		}
		public void setVar(String i, Pro value) {
			vars.put(i, value);
		}
		public Pro getVar(String i) {
			Pro z = vars.get(i);
			if (z == null) {
				toss("Getting unset instance var <{0}> of Usr object", i);
			}
			return z;
		}

		static void addBuiltinMethodsForUsr(Terp terp) {
			terp.tUsrCls.addMethod(new JavMeth(terp, "new") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					return new Usr(r.asCls());
				}
			});
		}
	}

	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 BinaryNumPredMeth extends JavMeth {
			BinaryNumPredMeth(Terp terp, String name) {
				super(terp, name);
			}
			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 relop not a Num");
				}
				if (a == null) {
					toss("Argument of Num relop not a Num");
				}
				return boolPro(f.terp(), compute(s.num, a.num));
			}
			abstract boolean compute(double snum, double anum);
		}

		abstract static class BinaryNumMeth extends JavMeth {
			BinaryNumMeth(Terp terp, String name) {
				super(terp, name);
			}
			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 binop not a Num");
				}
				if (a == null) {
					toss("Argument of Num binop 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(Terp terp, String name) {
				super(terp, name);
			}
			public Pro eval(Frame f, Pro r, Pro[] args) {
				assert args.length == 0;
				Num s = r.asNum();
				if (s == null) {
					toss("Receiver of Num unary op not a Num");
				}
				return new Num(f.terp(), compute(s.num));
			}
			abstract double compute(double anum);
		}

		static void addBuiltinMethodsForNum(Terp terp) {

			// TODO: move these up to Obj.
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "eq:") {
				boolean compute(double snum, double anum) {
					return snum == anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "ne:") {
				boolean compute(double snum, double anum) {
					return snum != anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "lt:") {
				boolean compute(double snum, double anum) {
					return snum < anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "le:") {
				boolean compute(double snum, double anum) {
					return snum <= anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "gt:") {
				boolean compute(double snum, double anum) {
					return snum > anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumPredMeth(terp, "ge:") {
				boolean compute(double snum, double anum) {
					return snum >= anum;
				}
			});

			terp.tNum.addMethod(new BinaryNumMeth(terp, "pl:") {
				double compute(double snum, double anum) {
					return snum + anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumMeth(terp, "mi:") {
				double compute(double snum, double anum) {
					return snum - anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumMeth(terp, "ti:") {
				double compute(double snum, double anum) {
					return snum * anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumMeth(terp, "fdiv:") {
				double compute(double snum, double anum) {
					return snum / anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumMeth(terp, "idiv:") {
				double compute(double snum, double anum) {
					return (long) snum / (long) anum;
				}
			});
			terp.tNum.addMethod(new BinaryNumMeth(terp, "imod:") {
				double compute(double snum, double anum) {
					return (long) snum % (long) anum;
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "neg") {
				double compute(double snum) {
					return -snum;
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "sgn") {
				double compute(double snum) {
					return snum < 0 ? -1 : snum > 0 ? 1 : 0;
				}
			});
			terp.tNum.addMethod(new UnaryNumMeth(terp, "int") {
				double compute(double snum) {
					return (long) snum;
				}
			});
		}
	}

	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;
		}

		abstract static class BinaryStrPredMeth extends JavMeth {
			BinaryStrPredMeth(Terp terp, String name) {
				super(terp, name);
			}
			public Pro eval(Frame f, Pro r, Pro[] args) {
				assert args.length == 1;
				Str s = r.asStr();
				Str a = args[0].asStr();
				if (s == null) {
					toss("Receiver of Str relop not a Str");
				}
				if (a == null) {
					toss("Argument of Str relop not a Str");
				}
				return boolPro(f.terp(), compute(s.str, a.str));
			}
			abstract boolean compute(String sstr, String astr);
		}

		static void addBuiltinMethodsForStr(Terp terp) {
			terp.tStr.addMethod(new JavMeth(terp, "append:") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					assert args.length == 1;
					Str s = r.asStr();
					Str a = args[0].asStr();
					if (s == null) {
						toss("Receiver of Str>>ap: not a Str");
					}
					if (a == null) {
						toss("Argument of Str>>ap: not a Str");
					}
					return new Str(f.terp(), s.str + a.str);
				}
			});
			terp.tStr.addMethod(new JavMeth(terp, "substr:to:") {  // Substring
				public Pro eval(Frame f, Pro r, Pro[] args) {
					assert args.length == 2;
					Str s = r.asStr();
					assert (s != null);
					int a = args[0].toNearestInt();
					int b = args[1].toNearestInt();
					return new Str(f.terp(), s.str.substring(a, b));
				}
			});
			// TODO: move these up to Obj.
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "eq:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) == 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "ne:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) != 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "lt:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) < 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "le:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) <= 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "gt:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) > 0;
				}
			});
			terp.tStr.addMethod(new BinaryStrPredMeth(terp, "ge:") {
				boolean compute(String sstr, String astr) {
					return sstr.compareTo(astr) >= 0;
				}
			});
		}
	}

	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 toNearestIndex(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 void addBuiltinMethodsForVec(final Terp terp) {
			terp.tVec.addMethod(new JavMeth(terp, "len") {
				public Num eval(Frame f, Pro r, Pro[] args) {
					return new Num(terp, r.asVec().vec.size());
				}
			});
			terp.tVec.addMethod(new JavMeth(terp, "at:") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					Vec self = r.asVec();
					return self.vec.get(self.toNearestIndex(args[0]));
				}
			});
			terp.tVec.addMethod(new JavMeth(terp, "at:put:") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					Vec self = r.asVec();
					self.vec.set(self.toNearestIndex(args[0]), args[1]);
					return r;
				}
			});
			terp.tVec.addMethod(new JavMeth(terp, "setLen:") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					Vec self = r.asVec();
					int oldSize = self.vec.size();
					int newSize = self.toNearestIndex(args[0]);
					self.vec.setSize(newSize);
					for (int i = oldSize; i < newSize; i++) {
						self.vec.set(i, terp.tNilObj);
					}
					return r;
				}
			});
			terp.tVecCls.addMethod(new JavMeth(terp, "new") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					return new Vec(terp);
				}
			});
		}
	}

	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 void addBuiltinMethodsForMap(final Terp terp) {
			terp.tMap.addMethod(new JavMeth(terp, "len") {
				public Num eval(Frame f, Pro r, Pro[] args) {
					return new Num(terp, r.asMap().map.size());
				}
			});
			terp.tMap.addMethod(new JavMeth(terp, "at:") {
				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;
				}
			});
			terp.tMap.addMethod(new JavMeth(terp, "at:put:") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					Map self = r.asMap();
					self.map.put(args[0], args[1]);
					return r;
				}
			});
			terp.tMapCls.addMethod(new JavMeth(terp, "new") {
				public Pro eval(Frame f, Pro r, Pro[] args) {
					return new Map(terp);
				}
			});
		}
	}
}
