package net.yak.stalk;

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

import net.yak.stalk.Cls.JavMeth;
import net.yak.stalk.Expr.GetLocalVar;
import net.yak.stalk.Expr.Seq;
import net.yak.stalk.Talk.Terp.Frame;
import net.yak.stalk.Talk.Terp.Visitor;

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

	public Pro(Cls cls) {
		this.cls = cls;
		if (cls != null) { // This happens during bootup.
			++cls.countInstances;
			this.instId = cls.countInstances;
		}
	}
	public String toString() {
		return fmt("~%s~%s~", cls.name, this.instId);
	}
	public void visit(Visitor v) {
		v.visitPro(this);
	}
	Terp terp() {
		return cls.terp;
	}
	Num asNum() {
		return null;
	}
	Str asStr() {
		return null;
	}
	Cls asCls() {
		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 %s, 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, "class", "cls") {
			@Override
			public Pro apply(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 class NilObj extends Obj { // Nil
		public NilObj(Cls cls) {
			super(cls);
		}
		NilObj asNil() {
			return this;
		}
		public String toString() {
			return "Nil";
		}
		public void visit(Visitor v) {
			v.visitNilObj(this);
		}
		static void addBuiltinMethodsForFal(Terp terp) {
			// N.B. We are adding these to "FalCls", not "Fal".
			terp.tFalseObj.addMethod(new JavMeth(terp, "ifNilObj:", "ni") {
				public Pro apply(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:", "nn") {
				public Pro apply(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";
		}
		public void visit(Visitor v) {
			v.visitFalseObj(this);
		}
		static void addBuiltinMethodsForFal(Terp terp) {
			// N.B. We are adding these to "FalCls", not "Fal".
			terp.tFalseObj.addMethod(new JavMeth(terp, "ifNo:", "no:") {
				public Pro apply(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, "ifYe:ifNo:", "ye:no:") {
				public Pro apply(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.tFalseObj.addMethod(new JavMeth(terp, "ifNo:ifYe:", "no:ye:") {
				public Pro apply(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, "ifYe", "ye:") {
				public Pro apply(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";
		}
		public void visit(Visitor v) {
			v.visitTrueObj(this);
		}
		static void addBuiltinMethodsForTru(Terp terp) {
			terp.tTrueObj.addMethod(new JavMeth(terp, "ifYe", "ye:") {
				public Pro apply(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, "ifYe:ifNo:", "ye:no:") {
				public Pro apply(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, "ifNo:ifYe:", "no:ye:") {
				public Pro apply(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, "ifNo", "no:") {
				public Pro apply(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;
		Frame f;

		public Blk(Expr body, Frame f) {
			super(f.terp().tBlk);
			this.body = body;
			this.f = f;
		}
		Blk asBlk() {
			return this;
		}
		public String toString() {
			return fmt("Blk(%s)", body);
		}

		static void addBuiltinMethodsForBlk(Terp terp) {
			terp.tBlk.addMethod(new JavMeth(terp, "run", "r",
					"Evaluate the block.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					// Notice the block runs in its frame, not the caller's.
					return blk.body.eval(blk.f);
				}
			});
			terp.tBlk.addMethod(new JavMeth(terp, "runArg:", "ra", "Run the block with one argument." ) {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					assert args.length == 1;
					assert blk.body instanceof Seq;
					Seq seq = (Seq) blk.body;
					assert seq.body.length > 0;
					if (seq.body[0] instanceof GetLocalVar) {
						GetLocalVar glv = (GetLocalVar) seq.body[0];
						blk.f.dict.put(glv.name, args[0]);
					} else {
						toss("Block does not declare an arg variable");
					}
					// Notice the block runs in its frame, not the caller's.
					return blk.evalWithArg(args[0]);
				}
			});
			terp.tBlk.addMethod(new JavMeth(terp, "runArg:with:", "ra:wi:", "Run the block with two arguments.") {
				public Pro apply(Frame _, Pro r, Pro[] args) {
					Blk blk = r.asBlk();
					assert args.length == 2;
					return blk.evalWithArgs2(args[0], args[1]);
				}
			});
		}
		Pro evalWithArg(Pro arg0) {
			assert this.body instanceof Seq;
			Seq seq = (Seq) this.body;
			if (seq.body.length > 0 && seq.body[0] instanceof GetLocalVar) {
				GetLocalVar glv = (GetLocalVar) seq.body[0];
				this.f.dict.put(glv.name, arg0);
			} else {
				toss("Block does not declare an arg variable");
			}
			// Notice the block runs in its frame, not the caller's.
			return this.body.eval(this.f);
		}
		Pro evalWithArgs2(Pro arg0, Pro arg1) {
			assert this.body instanceof Seq;
			Seq seq = (Seq) this.body;
			if (seq.body.length > 0 && seq.body[0] instanceof GetLocalVar) {
				GetLocalVar glv = (GetLocalVar) seq.body[0];
				this.f.dict.put(glv.name, arg0);
			} else {
				toss("Block does not declare an arg variable");
			}
			if (seq.body.length > 1 && seq.body[1] instanceof GetLocalVar) {
				GetLocalVar glv = (GetLocalVar) seq.body[1];
				this.f.dict.put(glv.name, arg1);
			} else {
				toss("Block does not declare a second arg variable");
			}
			// Notice the block runs in its frame, not the caller's.
			return this.body.eval(this.f);
		}
	}

	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(%s)", num);
		}
		@SuppressWarnings("unchecked")
		Comparable innerObject() {
			return new Double(num);
		}
		public void visit(Visitor v) {
			v.visitNum(this);
		}

		abstract static class BinaryNumPredMeth extends JavMeth {
			BinaryNumPredMeth(Terp terp, String name) {
				super(terp, name, null, "");
			}
			public Pro apply(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, null, "");
			}
			public Pro apply(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, null, "");
			}
			public Pro apply(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) {
			terp.tNum.addMethod(new JavMeth(terp, "doZeroTo:", "do:", "Iterate the argument block this many times, with one argument ranging from 0 up to, not including, self.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Num num = r.asNum();
					assert args.length == 1;
					Blk blk = args[0].asBlk();
					double stop = num.num - 0.5;
					for (double i = 0; i < stop; i++) {
						blk.evalWithArg(new Num(f.terp(), i));
					}
					return f.terp().instNil;
				}
			});

			// 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(\"%s\")", str);
		}
		@SuppressWarnings("unchecked")
		Comparable innerObject() {
			return str;
		}
		public void visit(Visitor v) {
			v.visitStr(this);
		}

		abstract static class BinaryStrPredMeth extends JavMeth {
			BinaryStrPredMeth(Terp terp, String name) {
				super(terp, name, null, "");
			}
			public Pro apply(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:", "ap", "Append the argument (as a string) to the string, modifying self.") {
				public Pro apply(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:", "ss:to:", "Substring starting at first index, ending before second index, like Java substr.") { // Substring
						public Pro apply(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;
		}
		public void visit(Visitor v) {
			v.visitVec(this);
		}
		int toNearestIndex(Pro a) {
			final int vlen = vec.size();
			if (vlen == 0) {
				toss("Cannot index into empty Vec");
			}
			Num num = a.asNum();
			if (num == null) {
				toss("Index is a %s, not a Num, in <Vec at:>", a.cls.name);
			}
			int i = (int) Math.floor(num.num + 0.5); // closest int
			i = ((i % vlen) + vlen) % vlen;  // Positive-only Modulus
			return i;
		}
		static void addBuiltinMethodsForVec(final Terp terp) {
			terp.tVec.addMethod(new JavMeth(terp, "len", null, "Length of the Vec.") {
				public Num apply(Frame f, Pro r, Pro[] args) {
					return new Num(terp, r.asVec().vec.size());
				}
			});
			terp.tVec.addMethod(new JavMeth(terp, "at:", null, "Item at given index") {
				public Pro apply(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:pu:", "","") {
				public Pro apply(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, "append:", "ap","") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Vec self = r.asVec();
					self.vec.add(args[0]);
					return r;
				}
			});
			terp.tVec.addMethod(new JavMeth(terp, "setLen:", "sl","") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Vec self = r.asVec();
					int oldSize = self.vec.size();
					Num num = args[0].asNum();
					if (num == null) {
						toss("Expected argument to be a Num; got a <%s>.", args[0].cls.name);
					}
					int newSize = (int) Math.floor(num.num + 0.5); // closest int
					self.vec.setSize(newSize);
					for (int i = oldSize; i < newSize; i++) {
						self.vec.set(i, terp.tNilObj);
					}
					return r;
				}
			});
			terp.tVec.addMethod(new JavMeth(terp, "doWithEach:", "do", "Iterate the block with one argument, for each item in the Vec.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Vec vec = r.asVec();
					assert args.length == 1;
					Blk blk = args[0].asBlk();
					int stop = vec.vec.size();
					for (int i = 0; i < stop; i++) {
						blk.evalWithArg(vec.vec.get(i));
					}
					return f.terp().instNil;
				}
			});
			terp.tVecCls.addMethod(new JavMeth(terp, "new", "","Create a new empty Vec object.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return new Vec(terp);
				}
			});
			terp.tVecCls.addMethod(new JavMeth(terp, "append:", "ap","Create a new Vec of length 1 with the given item in it.  Same as 'new append:'.") {
				// Shortcut for "new append:"
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Vec vec = new Vec(terp);
					vec.vec.add(args[0]);
					return vec;
				}
			});
		}
	}

	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;
		}
		public void visit(Visitor v) {
			v.visitMap(this);
		}
		Assoc[] sortedAssocs() {
			Assoc[] z = new Assoc[map.size()];
			int i = 0;
			for (Pro key : map.keySet()) {
				z[i] = new Assoc(key, map.get(key));
				++i;
			}
			Arrays.sort(z);
			return z;
		}

		static void addBuiltinMethodsForMap(final Terp terp) {
			terp.tMap.addMethod(new JavMeth(terp, "len", "", "Number of entries in the Map.") {
				public Num apply(Frame f, Pro r, Pro[] args) {
					return new Num(terp, r.asMap().map.size());
				}
			});
			terp.tMap.addMethod(new JavMeth(terp, "at:", "", "Return the value stored at the given key, or Nil if it is missing.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					Map self = r.asMap();
					Pro z = self.map.get(args[0]);
					if (z == null) {
						return f.terp().instNil;
					}
					return z;
				}
			});
			terp.tMap.addMethod(new JavMeth(terp, "at:pu:", "", "") {
				public Pro apply(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", "", "Create a new empty Map object.") {
				public Pro apply(Frame f, Pro r, Pro[] args) {
					return new Map(terp);
				}
			});
		}
	}

	public static class Assoc extends Obj {
		Pro key;
		Pro value;
		public Assoc(Pro a, Pro b) {
			super(a.terp().tAssoc);
			this.key = a;
			this.value = b;
		}
		public void visit(Visitor v) {
			v.visitAssoc(this);
		}
		public String toString() {
			return fmt("~ProProAssoc~%s(%s,%s)", instId, key, value);
		}
		@SuppressWarnings("unchecked")
		Comparable innerObject() {
			// Value comparisons are only based on the key.
			return key;
		}
	}
	
}
