/*
	FIXME:
		overloads can be done as an object representing the overload set
		tat opCall does the dispatch. Then other overloads can actually
		be added more sanely.

	FIXME:
		instantiate template members when reflection with certain
		arguments if marked right...


	FIXME:
		pointer to member functions can give a way to wrap things

		we'll pass it an opaque object as this and it will unpack and call the method

		we can also auto-generate getters and setters for properties with this method

		and constructors, so the script can create class objects too
*/


/++
	jsvar provides a D type called [var] that works similarly to the same in Javascript.

	It is weakly (even weaker than JS, frequently returning null rather than throwing on
	an invalid operation) and dynamically typed, but interops pretty easily with D itself:

	---
	var a = 10;
	a ~= "20";
		assert(a == "1020");

	var a = function(int b, int c) { return b+c; };
	// note the second set of () is because of broken @property
	assert(a()(10,20) == 30);

	var a = var.emptyObject;
	a.foo = 30;
	assert(a["foo"] == 30);

	var b = json!q{
		"foo":12,
		"bar":{"hey":[1,2,3,"lol"]}
	};

	assert(b.bar.hey[1] == 2);
	---


	You can also use [var.fromJson], a static method, to quickly and easily
	read json or [var.toJson] to write it.

	Also, if you combine this with my [arsd.script] module, you get pretty
	easy interop with a little scripting language that resembles a cross between
	D and Javascript - just like you can write in D itself using this type.

	Please note that function default arguments are NOT likely to work in the script.
	You'd have to use a helper thing that I haven't written yet. opAssign can never
	do it because that information is lost when it becomes a pointer. ParamDefault
	is thus commented out for now.


	Properties:
	$(LIST
		* note that @property doesn't work right in D, so the opDispatch properties
		  will require double parenthesis to call as functions.

		* Properties inside a var itself are set specially:
			obj.propName._object = new PropertyPrototype(getter, setter);
	)

	D structs can be turned to vars, but it is a copy.

	Wrapping D native objects is coming later, the current ways suck. I really needed
	properties to do them sanely at all, and now I have it. A native wrapped object will
	also need to be set with _object prolly.

	Author: Adam D Ruppe

	History: Started in July 2013.
+/
module arsd.jsvar;

version=new_std_json;

static import std.array;
import std.traits;
import std.conv;
import std.json;

version(jsvar_throw)
	/++
		Variable to decide if jsvar throws on certain invalid
		operations or continues on propagating null vars.
	+/
	bool jsvar_throw = true;
else
	/// ditto
	bool jsvar_throw = false;

// uda for wrapping classes
enum scriptable = "arsd_jsvar_compatible";

/*
	PrototypeObject FIXME:
		make undefined variables reaction overloadable in PrototypeObject, not just a switch

	script FIXME:

	the Expression should keep scriptFilename and lineNumber around for error messages

	it should consistently throw on missing semicolons

	*) in operator

	*) nesting comments, `` string literals
	*) opDispatch overloading
	*) properties???//
		a.prop on the rhs => a.prop()
		a.prop on the lhs => a.prop(rhs);
		if opAssign, it can just do a.prop(a.prop().opBinary!op(rhs));

		But, how do we mark properties in var? Can we make them work this way in D too?
	0) add global functions to the object (or at least provide a convenience function to make a pre-populated global object)
	1) ensure operator precedence is sane
	2) a++ would prolly be nice, and def -a
	4) switches?
	10) __FILE__ and __LINE__ as default function arguments should work like in D
	16) stack traces on script exceptions
	17) an exception type that we can create in the script

	14) import???????/ it could just attach a particular object to the local scope, and the module decl just giving the local scope a name
		there could be a super-global object that is the prototype of the "global" used here
		then you import, and it pulls moduleGlobal.prototype = superGlobal.modulename... or soemthing.

		to get the vars out in D, you'd have to be aware of this, since you pass the superglobal
		hmmm maybe not worth it

		though maybe to export vars there could be an explicit export namespace or something.


	6) gotos? labels? labeled break/continue?
	18) what about something like ruby's blocks or macros? parsing foo(arg) { code } is easy enough, but how would we use it?

	var FIXME:

	user defined operator overloading on objects, including opCall, opApply, and more
	flesh out prototype objects for Array, String, and Function

	looserOpEquals

	it would be nice if delegates on native types could work
*/

static if(__VERSION__ <= 2076) {
	// compatibility shims with gdc
	enum JSONType {
		object = JSON_TYPE.OBJECT,
		null_ = JSON_TYPE.NULL,
		false_ = JSON_TYPE.FALSE,
		true_ = JSON_TYPE.TRUE,
		integer = JSON_TYPE.INTEGER,
		float_ = JSON_TYPE.FLOAT,
		array = JSON_TYPE.ARRAY,
		string = JSON_TYPE.STRING,
		uinteger = JSON_TYPE.UINTEGER
	}
}


/*
	Script notes:

	the one type is var. It works just like the var type in D from arsd.jsvar.
	(it might be fun to try to add other types, and match D a little better here! We could allow implicit conversion to and from var, but not on the other types, they could get static checking. But for now it is only var. BTW auto is an alias for var right now)

	There is no comma operator, but you can use a scope as an expression: a++, b++; can be written as {a++;b++;}
*/

version(test_script)
	struct Foop {
		int a = 12;
		string n = "hate";
		void speak() { writeln(n, " ", a); n = "love"; writeln(n, " is what it is now"); }
		void speak2() { writeln("speak2 ", n, " ", a); }
	}
version(test_script)
void main() {
import arsd.script;
writeln(interpret("x*x + 3*x;", var(["x":3])));

	{
	var a = var.emptyObject;
	a.qweq = 12;
	}

	// the WrappedNativeObject is disgusting
	// but works. sort of.
	/*
	Foop foop2;

	var foop;
	foop._object = new WrappedNativeObject!Foop(foop2);

	foop.speak()();
	foop.a = 25;
	writeln(foop.n);
	foop.speak2()();
	return;
	*/

	import arsd.script;
	struct Test {
		int a = 10;
		string name = "ten";
	}

	auto globals = var.emptyObject;
	globals.lol = 100;
	globals.rofl = 23;

	globals.arrtest = var.emptyArray;

	globals.write._function = (var _this, var[] args) {
		string s;
		foreach(a; args)
			s ~= a.get!string;
		writeln("script said: ", s);
		return var(null);
	};

	// call D defined functions in script
	globals.func = (var a, var b) { writeln("Hello, world! You are : ", a, " and ", b); };

	globals.ex = () { throw new ScriptRuntimeException("test", 1); };

	globals.fun = { return var({ writeln("hello inside!"); }); };

	import std.file;
	writeln(interpret(readText("scripttest_code.d"), globals));

	globals.ten = 10.0;
	globals.five = 5.0;
	writeln(interpret(q{
		var a = json!q{ };
		a.b = json!q{ };
		a.b.c = 10;
		a;
	}, globals));

	/*
	globals.minigui = json!q{};
	import arsd.minigui;
	globals.minigui.createWindow = {
		var v;
		auto mw = new MainWindow();
		v._object = new OpaqueNativeObject!(MainWindow)(mw);
		v.loop = { mw.loop(); };
		return v;
	};
	*/

	repl(globals);

	writeln("BACK IN D!");
	globals.c()(10); // call script defined functions in D (note: this runs the interpreter)

	//writeln(globals._getMember("lol", false));
	return;

	var k,l ;

	var j = json!q{
		"hello": {
			"data":[1,2,"giggle",4]
		},
		"world":20
	};

	writeln(j.hello.data[2]);


	Test t;
	var rofl = t;
	writeln(rofl.name);
	writeln(rofl.a);

	rofl.a = "20";
	rofl.name = "twenty";

	t = rofl.get!Test;
	writeln(t);

	var a1 = 10;
	a1 -= "5";
	a1 /= 2;

	writeln(a1);

	var a = 10;
	var b = 20;
	a = b;

	b = 30;
	a += 100.2;
	writeln(a);

	var c = var.emptyObject;
	c.a = b;

	var d = c;
	d.b = 50;

	writeln(c.b);

	writeln(d.toJson());

	var e = a + b;
	writeln(a, " + ", b, " = ", e);

	e = function(var lol) {
		writeln("hello with ",lol,"!");
		return lol + 10;
	};

	writeln(e("15"));

	if(var("ass") > 100)
		writeln(var("10") / "3");
}

template json(string s) {
	// ctfe doesn't support the unions std.json uses :(
	//enum json = var.fromJsonObject(s);

	// FIXME we should at least validate string s at compile time
	var json() {
		return var.fromJson("{" ~ s ~ "}");
	}
}

// literals

// var a = varArray(10, "cool", 2);
// assert(a[0] == 10); assert(a[1] == "cool"); assert(a[2] == 2);
var varArray(T...)(T t) {
	var a = var.emptyArray;
	foreach(arg; t)
		a ~= var(arg);
	return a;
}

// var a = varObject("cool", 10, "bar", "baz");
// assert(a.cool == 10 && a.bar == "baz");
var varObject(T...)(T t) {
	var a = var.emptyObject;

	string lastString;
	foreach(idx, arg; t) {
		static if(idx % 2 == 0) {
			lastString = arg;
		} else {
			assert(lastString !is null);
			a[lastString] = arg;
			lastString = null;
		}
	}
	return a;
}


private double stringToNumber(string s) {
	double r;
	try {
		r = to!double(s);
	} catch (Exception e) {
		r = double.nan;
	}

	return r;
}

private bool doubleIsInteger(double r) {
	return (r == cast(long) r);
}

// helper template for operator overloading
private var _op(alias _this, alias this2, string op, T)(T t) if(op == "~") {
	static if(is(T == var)) {
		if(t.payloadType() == var.Type.Array)
			return _op!(_this, this2, op)(t._payload._array);
		else if(t.payloadType() == var.Type.String)
			return _op!(_this, this2, op)(t._payload._string);
		//else
			//return _op!(_this, this2, op)(t.get!string);
	}

	if(this2.payloadType() == var.Type.Array) {
		auto l = this2._payload._array;
		static if(isArray!T && !isSomeString!T)
			foreach(item; t)
				l ~= var(item);
		else
			l ~= var(t);

		_this._type = var.Type.Array;
		_this._payload._array = l;
		return _this;
	} else if(this2.payloadType() == var.Type.String) {
		auto l = this2._payload._string;
		l ~= var(t).get!string; // is this right?
		_this._type = var.Type.String;
		_this._payload._string = l;
		return _this;
	} else {
		auto l = this2.get!string;
		l ~= var(t).get!string;
		_this._type = var.Type.String;
		_this._payload._string = l;
		return _this;
	}

	assert(0);

}

// FIXME: maybe the bitops should be moved out to another function like ~ is
private var _op(alias _this, alias this2, string op, T)(T t) if(op != "~") {
	static if(is(T == var)) {
		if(t.payloadType() == var.Type.Integral)
			return _op!(_this, this2, op)(t._payload._integral);
		if(t.payloadType() == var.Type.Floating)
			return _op!(_this, this2, op)(t._payload._floating);
		if(t.payloadType() == var.Type.String)
			return _op!(_this, this2, op)(t._payload._string);
		throw new Exception("Attempted invalid operator `" ~ op ~ "` on variable of type " ~ to!string(t.payloadType()));
	} else {
		if(this2.payloadType() == var.Type.Integral) {
			auto l = this2._payload._integral;
			static if(isIntegral!T) {
				mixin("l "~op~"= t;");
				_this._type = var.Type.Integral;
				_this._payload._integral = l;
				return _this;
			} else static if(isFloatingPoint!T) {
				static if(op == "&" || op == "|" || op == "^" || op == "<<" || op == ">>" || op == ">>>") {
					this2._type = var.Type.Integral;
					long f = l;
					mixin("f "~op~"= cast(long) t;");
					_this._type = var.Type.Integral;
					_this._payload._integral = f;
				} else {
					this2._type = var.Type.Floating;
					double f = l;
					mixin("f "~op~"= t;");
					_this._type = var.Type.Floating;
					_this._payload._floating = f;
				}
				return _this;
			} else static if(isSomeString!T) {
				auto rhs = stringToNumber(t);
				if(doubleIsInteger(rhs)) {
					mixin("l "~op~"= cast(long) rhs;");
					_this._type = var.Type.Integral;
					_this._payload._integral = l;
				} else{
					static if(op == "&" || op == "|" || op == "^" || op == "<<" || op == ">>" || op == ">>>") {
						long f = l;
						mixin("f "~op~"= cast(long) rhs;");
						_this._type = var.Type.Integral;
						_this._payload._integral = f;
					} else {
						double f = l;
						mixin("f "~op~"= rhs;");
						_this._type = var.Type.Floating;
						_this._payload._floating = f;
					}
				}
				return _this;

			}
		} else if(this2.payloadType() == var.Type.Floating) {
			auto f = this._payload._floating;

			static if(isIntegral!T || isFloatingPoint!T) {
				static if(op == "&" || op == "|" || op == "^" || op == "<<" || op == ">>" || op == ">>>") {
					long argh = cast(long) f;
					mixin("argh "~op~"= cast(long) t;");
					_this._type = var.Type.Integral;
					_this._payload._integral = argh;
				} else {
					mixin("f "~op~"= t;");
					_this._type = var.Type.Floating;
					_this._payload._floating = f;
				}
				return _this;
			} else static if(isSomeString!T) {
				auto rhs = stringToNumber(t);

				static if(op == "&" || op == "|" || op == "^" || op == "<<" || op == ">>" || op == ">>>") {
					long pain = cast(long) f;
					mixin("pain "~op~"= cast(long) rhs;");
					_this._type = var.Type.Integral;
					_this._payload._floating = pain;
				} else {
					mixin("f "~op~"= rhs;");
					_this._type = var.Type.Floating;
					_this._payload._floating = f;
				}
				return _this;
			} else static assert(0);
		} else if(this2.payloadType() == var.Type.String) {
			static if(op == "&" || op == "|" || op == "^" || op == "<<" || op == ">>" || op == ">>>") {
				long r = cast(long) stringToNumber(this2._payload._string);
				long rhs;
			} else {
				double r = stringToNumber(this2._payload._string);
				double rhs;
			}

			static if(isSomeString!T) {
				rhs = cast(typeof(rhs)) stringToNumber(t);
			} else {
				rhs = to!(typeof(rhs))(t);
			}

			mixin("r " ~ op ~ "= rhs;");

			static if(is(typeof(r) == double)) {
				_this._type = var.Type.Floating;
				_this._payload._floating = r;
			} else static if(is(typeof(r) == long)) {
				_this._type = var.Type.Integral;
				_this._payload._integral = r;
			} else static assert(0);
			return _this;
		} else {
			// the operation is nonsensical, we should throw or ignore it
			var i = 0;
			return i;
		}
	}

	assert(0);
}


///
struct var {
	public this(T)(T t) {
		static if(is(T == var))
			this = t;
		else
			this.opAssign(t);
	}

	// used by the script interpreter... does a .dup on array, new on class if possible, otherwise copies members.
	public var _copy_new() {
		if(payloadType() == Type.Object) {
			var cp;
			if(this._payload._object !is null) {
				auto po = this._payload._object.new_(null);
				cp._object = po;
			}
			return cp;
		} else if(payloadType() == Type.Array) {
			var cp;
			cp = this._payload._array.dup;
			return cp;
		} else {
			return this._copy();
		}
	}

	public var _copy() {
		final switch(payloadType()) {
			case Type.Integral:
			case Type.Boolean:
			case Type.Floating:
			case Type.Function:
			case Type.String:
				// since strings are immutable, we can pretend they are value types too
				return this; // value types don't need anything special to be copied

			case Type.Array:
				var cp;
				cp = this._payload._array[];
				return cp;
			case Type.Object:
				var cp;
				if(this._payload._object !is null)
					cp._object = this._payload._object.copy;
				return cp;
		}
	}

	/// `if(some_var)` will call this and give behavior based on the dynamic type. Shouldn't be too surprising.
	public bool opCast(T:bool)() {
		final switch(this._type) {
			case Type.Object:
				return this._payload._object !is null;
			case Type.Array:
				return this._payload._array.length != 0;
			case Type.String:
				return this._payload._string.length != 0;
			case Type.Integral:
				return this._payload._integral != 0;
			case Type.Floating:
				return this._payload._floating != 0;
			case Type.Boolean:
				return this._payload._boolean;
			case Type.Function:
				return this._payload._function !is null;
		}
	}

	/// You can foreach over a var.
	public int opApply(scope int delegate(ref var) dg) {
		foreach(i, item; this)
			if(auto result = dg(item))
				return result;
		return 0;
	}

	/// ditto
	public int opApply(scope int delegate(var, ref var) dg) {
		if(this.payloadType() == Type.Array) {
			foreach(i, ref v; this._payload._array)
				if(auto result = dg(var(i), v))
					return result;
		} else if(this.payloadType() == Type.Object && this._payload._object !is null) {
			// FIXME: if it offers input range primitives, we should use them
			// FIXME: user defined opApply on the object
			foreach(k, ref v; this._payload._object)
				if(auto result = dg(var(k), v))
					return result;
		} else if(this.payloadType() == Type.String) {
			// this is to prevent us from allocating a new string on each character, hopefully limiting that massively
			static immutable string chars = makeAscii!();

			foreach(i, dchar c; this._payload._string) {
				var lol = "";
				if(c < 128)
					lol._payload._string = chars[c .. c + 1];
				else
					lol._payload._string = to!string(""d ~ c); // blargh, how slow can we go?
				if(auto result = dg(var(i), lol))
					return result;
			}
		}
		// throw invalid foreach aggregate

		return 0;
	}


	/// Alias for [get]. e.g. `string s = cast(string) v;`
	public T opCast(T)() {
		return this.get!T;
	}

	/// Calls [get] for a type automatically. `int a; var b; b.putInto(a);` will auto-convert to `int`.
	public auto ref putInto(T)(ref T t) {
		return t = this.get!T;
	}

	/++
		Assigns a value to the var. It will do necessary implicit conversions
		and wrapping.

		You can make a method `toArsdJsvar` on your own objects to override this
		default. It should return a [var].

		History:
			On April 20, 2020, I changed the default mode for class assignment
			to [wrapNativeObject]. Previously it was [wrapOpaquely].

			With the new [wrapNativeObject] behavior, you can mark methods
			@[scriptable] to expose them to the script.
	+/
	public var opAssign(T)(T t) if(!is(T == var)) {
		static if(__traits(compiles, this = t.toArsdJsvar())) {
			static if(__traits(compiles, t is null)) {
				if(t is null)
					this = null;
				else
					this = t.toArsdJsvar();
			} else
				this = t.toArsdJsvar();
		} else static if(is(T : PrototypeObject)) {
			// support direct assignment of pre-made implementation objects
			// so prewrapped stuff can be easily passed.
			this._type = Type.Object;
			this._payload._object = t;
		} else static if(is(T == enum)) {
			this._type = Type.String;
			this._payload._string = to!string(t);
		} else static if(isFloatingPoint!T) {
			this._type = Type.Floating;
			this._payload._floating = t;
		} else static if(isIntegral!T) {
			this._type = Type.Integral;
			this._payload._integral = t;
		} else static if(isCallable!T) {
			this._type = Type.Function;
			static if(is(T == typeof(this._payload._function))) {
				this._payload._function = t;
			} else
			this._payload._function = delegate var(var _this, var[] args) {
				var ret;

				ParameterTypeTuple!T fargs;

				// default args? nope they can't work cuz it is assigning a function pointer by here. alas.
				enum lol = static_foreach(fargs.length, 1, -1,
					`t(`,``,` < args.length ? args[`,`].get!(typeof(fargs[`,`])) : typeof(fargs[`,`]).init,`,`)`);
					//`t(`,``,` < args.length ? args[`,`].get!(typeof(fargs[`,`])) : ParamDefault!(T, `,`)(),`,`)`);
				/+
				foreach(idx, a; fargs) {
					if(idx == args.length)
						break;
					cast(Unqual!(typeof(a))) fargs[idx] = args[idx].get!(typeof(a));
				}
				+/

				static if(is(ReturnType!t == void)) {
					//t(fargs);
					mixin(lol ~ ";");
				} else {
					//ret = t(fargs);
					ret = mixin(lol);
				}

				return ret;
			};
		} else static if(isSomeString!T) {
			this._type = Type.String;
			this._payload._string = to!string(t);
		} else static if(is(T == class) || is(T == interface)) {
			if(t !is null && (cast(Object) t) is null)
				throw new Exception("Unsupported class or interface");
			this._type = Type.Object;
			this._payload._object = t is null ? null : wrapNativeObject(t);
		} else static if(.isScriptableOpaque!T) {
			// auto-wrap other classes with reference semantics
			this._type = Type.Object;
			this._payload._object = wrapOpaquely(t);
		} else static if(is(T == struct) || isAssociativeArray!T) {
			// copy structs and assoc arrays by value into a var object
			this._type = Type.Object;
			auto obj = new PrototypeObject();
			this._payload._object = obj;

			static if(is(T == struct))
			foreach(member; __traits(allMembers, T)) {
				static if(__traits(compiles, __traits(getMember, t, member))) {
					static if(is(typeof(__traits(getMember, t, member)) == function)) {
						// skipping these because the delegate we get isn't going to work anyway; the object may be dead and certainly won't be updated
						//this[member] = &__traits(getMember, proxyObject, member);

						// but for simple toString, I'll allow it by recreating the object on demand
						// and then calling the original function. (I might be able to do that for more but
						// idk, just doing simple thing first)
						static if(member == "toString" && is(typeof(&__traits(getMember, t, member)) == string delegate())) {
							this[member]._function =  delegate(var _this, var[] args) {
								auto val = _this.get!T;
								return var(val.toString());
							};
						}
					} else static if(is(typeof(__traits(getMember, t, member)))) {
						static if(!is(typeof(__traits(getMember, t, member)) == void))
							this[member] = __traits(getMember, t, member);
					}
				}
			} else {
				// assoc array
				foreach(l, v; t) {
					this[var(l)] = var(v);
				}
			}
		} else static if(isArray!T) {
			this._type = Type.Array;
			var[] arr;
			arr.length = t.length;
			static if(!is(T == void[])) // we can't append a void array but it is nice to support x = [];
				foreach(i, item; t)
					arr[i] = var(item);
			this._payload._array = arr;
		} else static if(is(T == bool)) {
			this._type = Type.Boolean;
			this._payload._boolean = t;
		} else static if(isSomeChar!T) {
			this._type = Type.String;
			this._payload._string = "";
			import std.utf;
			char[4] ugh;
			auto size = encode(ugh, t);
			this._payload._string = ugh[0..size].idup;
		}// else static assert(0, "unsupported type");

		return this;
	}

	public size_t opDollar() {
		return this.length().get!size_t;
	}

	public var opOpAssign(string op, T)(T t) {
		if(payloadType() == Type.Object) {
			if(auto pt = cast(PropertyPrototype) this._payload._object) {
				auto propValue = pt.get;
				auto result = propValue.opOpAssign!(op)(t);

				pt.set(result);

				return result;
			} else if(this._payload._object !is null) {
				var* operator = this._payload._object._peekMember("opOpAssign", true);
				if(operator !is null && operator._type == Type.Function)
					return operator.call(this, op, t);
			}
		}

		return _op!(this, this, op, T)(t);
	}

	public var opUnary(string op : "-")() {
		static assert(op == "-");
		final switch(payloadType()) {
			case Type.Object:
			case Type.Array:
			case Type.Boolean:
			case Type.String:
			case Type.Function:
				assert(0); // FIXME
			//break;
			case Type.Integral:
				return var(-this.get!long);
			case Type.Floating:
				return var(-this.get!double);
		}
	}

	public var opBinary(string op, T)(T t) {
		var n;
		if(payloadType() == Type.Object) {
			if(this._payload._object is null)
				return var(null);
			var* operator = this._payload._object._peekMember("opBinary", true);
			if(operator !is null && operator._type == Type.Function) {
				return operator.call(this, op, t);
			}
		}
		return _op!(n, this, op, T)(t);
	}

	public var opBinaryRight(string op, T)(T s) {
		return var(s).opBinary!op(this);
	}

	// this in foo
	public var* opBinary(string op : "in", T)(T s) {
		var rhs = var(s);
		return rhs.opBinaryRight!"in"(this);
	}

	// foo in this
	public var* opBinaryRight(string op : "in", T)(T s) {
		// this needs to be an object
		if(this._object is null)
			return null;
		return var(s).get!string in this._object._properties;
	}

	public var apply(var _this, var[] args) {
		if(this.payloadType() == Type.Function) {
			if(this._payload._function is null) {
				if(jsvar_throw)
					throw new DynamicTypeException(this, Type.Function);
				else
					return var(null);
			}
			return this._payload._function(_this, args);
		} else if(this.payloadType() == Type.Object) {
			if(this._payload._object is null) {
				if(jsvar_throw)
					throw new DynamicTypeException(this, Type.Function);
				else
					return var(null);
			}
			var* operator = this._payload._object._peekMember("opCall", true);
			if(operator !is null && operator._type == Type.Function)
				return operator.apply(_this, args);
		}

		if(this.payloadType() == Type.Integral || this.payloadType() == Type.Floating) {
			if(args.length)
				return var(this.get!double * args[0].get!double);
			else
				return this;
		} else if(jsvar_throw) {
			throw new DynamicTypeException(this, Type.Function);
		}

		//return this;
		return var(null);
	}

	public var call(T...)(var _this, T t) {
		var[] args;
		foreach(a; t) {
			args ~= var(a);
		}
		return this.apply(_this, args);
	}

	public var opCall(T...)(T t) {
		return this.call(this, t);
	}

	/*
	public var applyWithMagicLocals(var _this, var[] args, var[string] magicLocals) {

	}
	*/

	public string toString() {
		return this.get!string;
	}

	public T getWno(T)() {
		if(payloadType == Type.Object) {
			if(auto wno = cast(WrappedNativeObject) this._payload._object) {
				auto no = cast(T) wno.getObject();
				if(no !is null)
					return no;
			}
		}
		return null;
	}

	/++
		Gets the var converted to type `T` as best it can. `T` may be constructed
		from `T.fromJsVar`, or through type conversions (coercing as needed). If
		`T` happens to be a struct, it will automatically introspect to convert
		the var object member-by-member.

		History:
			On April 21, 2020, I changed the behavior of

			---
			var a = null;
			string b = a.get!string;
			---

			Previously, `b == "null"`, which would print the word
			when writeln'd. Now, `b is null`, which prints the empty string,
			which is a bit less user-friendly, but more consistent with
			converting to/from D strings in general.

			If you are printing, you can check `a.get!string is null` and print
			null at that point if you like.

			I also wrote the first draft of this documentation at that time,
			even though the function has been public since the beginning.

			On January 1, 2021, I changed `get!some_struct` to call properties
			on the var, if a member looks like a function or object, to try to
			get plain-old-data out. Since the functions are only ever put there
			by you or by you allowing script, I don't feel too bad about it, but
			it still might not be ideal for all circumstances, so idk if I'll leave
			it this way or not.

			One thing it helps for though is taking scripted subclasses back into D
			structs, since the parent class thing is likely to be virtual properties.
			And having that just work in argument lists is really cool...

			Search function for the comment "property getter support" to see the impl.
	+/
	public T get(T)() if(!is(T == void)) {
		static if(is(T == var)) {
			return this;
		} else static if(__traits(compiles, T.fromJsVar(var.init))) {
			return T.fromJsVar(this);
		} else static if(__traits(compiles, T(this))) {
			return T(this);
		} else static if(__traits(compiles, new T(this))) {
			return new T(this);
		} else
		final switch(payloadType) {
			case Type.Boolean:
				static if(is(T == bool))
					return this._payload._boolean;
				else static if(isFloatingPoint!T || isIntegral!T)
					return cast(T) (this._payload._boolean ? 1 : 0); // the cast is for enums, I don't like this so FIXME
				else static if(isSomeString!T)
					return to!T(this._payload._boolean ? "true" : "false");
				else
				return T.init;
			case Type.Object:
				static if(isAssociativeArray!T) {
					T ret;
					if(this._payload._object !is null)
					foreach(k, v; this._payload._object._properties)
						ret[to!(KeyType!T)(k)] = v.get!(ValueType!T);

					return ret;
				} else static if(is(T : PrototypeObject)) {
					// they are requesting an implementation object, just give it to them
					return cast(T) this._payload._object;
				} else static if(isScriptableOpaque!(Unqual!T)) {
					if(auto wno = cast(WrappedOpaque!(Unqual!T)) this._payload._object) {
						return wno.wrapping();
					}
					static if(is(T == R*, R))
					if(auto wno = cast(WrappedOpaque!(Unqual!(R))) this._payload._object) {
						return wno.wrapping();
					}
					throw new DynamicTypeException(this, Type.Object); // FIXME: could be better
				} else static if(is(T == struct) || is(T == class) || is(T == interface)) {
					// first, we'll try to give them back the native object we have, if we have one
					static if(is(T : Object) || is(T == interface)) {
						auto t = this;
						// need to walk up the prototype chain too
						while(t != null) {
							assert(t.payloadType == Type.Object);
							if(auto wno = cast(WrappedNativeObject) t._payload._object) {
								auto no = cast(T) wno.getObject();

								if(no !is null) {
									auto sc = cast(ScriptableSubclass) no;
									if(sc !is null) {
										sc.setScriptVar(this);
									}

									return no;
								}
							}
							t = t.prototype;
						}

						// FIXME: this is kinda weird.
						return null;
					} else {

						// failing that, generic struct or class getting: try to fill in the fields by name
						T t;
						bool initialized = true;
						static if(is(T == class)) {
							static if(__traits(compiles, new T())) {
								t = new T();
							} else {
								initialized = false;
							}
						}


						if(initialized)
						foreach(i, a; t.tupleof) {
							var possibility = this[t.tupleof[i].stringof[2..$]];
							// FIXME: so there is the possibility of getting some data getting all caught
							// up in a script function doing weird things. If I can prevent that, I'd like to...
							// but it is also really useful for this to work for some scenarios...
							static if(!is(typeof(a) == return)) // if it is callable, just assign the func ref
							if(isCallableJsvarObject(possibility))
								possibility = possibility.apply(this, null); // crude approximation of property getter support
							cast(Unqual!(typeof((a)))) t.tupleof[i] = possibility.get!(typeof(a));
						}

						return t;
					}
				} else static if(isSomeString!T) {
					if(this._object !is null)
						return to!T(this._object.toString());
					return null;// "null";
				} else
					return T.init;
			case Type.Integral:
				static if(isFloatingPoint!T || isIntegral!T)
					return to!T(this._payload._integral);
				else static if(isSomeString!T)
					return to!T(this._payload._integral);
				else
					return T.init;
			case Type.Floating:
				static if(isFloatingPoint!T || isIntegral!T)
					return to!T(this._payload._floating);
				else static if(isSomeString!T)
					return to!T(this._payload._floating);
				else
					return T.init;
			case Type.String:
				static if(__traits(compiles, to!T(this._payload._string))) {
					try {
						return to!T(this._payload._string);
					} catch (Exception e) { return T.init; }
				} else
					return T.init;
			case Type.Array:
				import std.range;
				auto pl = this._payload._array;
				static if(isSomeString!T) {
					return to!T(pl);
				} else static if(is(T == E[N], E, size_t N)) {
					T ret;
					foreach(i; 0 .. N) {
						if(i >= pl.length)
							break;
						ret[i] = pl[i].get!E;
					}
					return ret;
				} else static if(is(T == E[], E)) {
					T ret;
					static if(is(ElementType!T == void)) {
						static assert(0, "try wrapping the function to get rid of void[] args");
						//alias getType = ubyte;
					} else
						alias getType = ElementType!T;
					foreach(item; pl)
						ret ~= item.get!(getType);
					return ret;
				} else
					return T.init;
				// is it sane to translate anything else?
			case Type.Function:
				static if(isSomeString!T) {
					return to!T("<function>");
				} else static if(isDelegate!T) {
					// making a local copy because otherwise the delegate might refer to a struct on the stack and get corrupted later or something
					auto func = this._payload._function;

					// the static helper lets me pass specific variables to the closure
					static T helper(typeof(func) func) {
						return delegate ReturnType!T (ParameterTypeTuple!T args) {
							var[] arr;
							foreach(arg; args)
								arr ~= var(arg);
							var ret = func(var(null), arr);
							static if(is(ReturnType!T == void))
								return;
							else
								return ret.get!(ReturnType!T);
						};
					}

					return helper(func);

				} else
					return T.init;
				// FIXME: we just might be able to do better for both of these
			//break;
		}
	}

	public T get(T)() if(is(T == void)) {}

	public T nullCoalesce(T)(T t) {
		if(_type == Type.Object && _payload._object is null)
			return t;
		return this.get!T;
	}

	public double opCmp(T)(T t) {
		auto f = this.get!double;
		static if(is(T == var))
			auto r = t.get!double;
		else
			auto r = t;
		return f - r;
	}

	public bool opEquals(T)(T t) {
		return this.opEquals(var(t));
	}

	public bool opEquals(T:var)(T t) const {
		// int and float can implicitly convert
		if(this._type == Type.Integral && t._type == Type.Floating)
			return _payload._integral == t._payload._floating;
		if(t._type == Type.Integral && this._type == Type.Floating)
			return t._payload._integral == this._payload._floating;

		// but the others are kinda strict
		// FIXME: should this be == or === ?

		if(this._type != t._type)
			return false;
		final switch(this._type) {
			case Type.Object:
				return _payload._object is t._payload._object;
			case Type.Integral:
				return _payload._integral == t._payload._integral;
			case Type.Boolean:
				return _payload._boolean == t._payload._boolean;
			case Type.Floating:
				return _payload._floating == t._payload._floating; // FIXME: approxEquals?
			case Type.String:
				return _payload._string == t._payload._string;
			case Type.Function:
				return _payload._function is t._payload._function;
			case Type.Array:
				return _payload._array == t._payload._array;
		}
		assert(0);
	}

	public enum Type {
		Object, Array, Integral, Floating, String, Function, Boolean
	}

	public Type payloadType() {
		return _type;
	}

	private Type _type;

	private union Payload {
		PrototypeObject _object;
		var[] _array;
		long _integral;
		double _floating;
		string _string;
		bool _boolean;
		var delegate(var _this, var[] args) _function;
	}

	package VarMetadata _metadata;

	public void _function(var delegate(var, var[]) f) {
		this._payload._function = f;
		this._type = Type.Function;
	}

	/*
	public void _function(var function(var, var[]) f) {
		var delegate(var, var[]) dg;
		dg.ptr = null;
		dg.funcptr = f;
		this._function = dg;
	}
	*/

	public void _object(PrototypeObject obj) {
		this._type = Type.Object;
		this._payload._object = obj;
	}

	public PrototypeObject _object() {
		if(this._type == Type.Object)
			return this._payload._object;
		return null;
	}

	package Payload _payload;

	private void _requireType(Type t, string file = __FILE__, size_t line = __LINE__){
		if(this.payloadType() != t)
			throw new DynamicTypeException(this, t, file, line);
	}

	public var opSlice(var e1, var e2) {
		return this.opSlice(e1.get!ptrdiff_t, e2.get!ptrdiff_t);
	}

	public var opSlice(ptrdiff_t e1, ptrdiff_t e2) {
		if(this.payloadType() == Type.Array) {
			if(e1 > _payload._array.length)
				e1 = _payload._array.length;
			if(e2 > _payload._array.length)
				e2 = _payload._array.length;
			return var(_payload._array[e1 .. e2]);
		}
		if(this.payloadType() == Type.String) {
			if(e1 > _payload._string.length)
				e1 = _payload._string.length;
			if(e2 > _payload._string.length)
				e2 = _payload._string.length;
			return var(_payload._string[e1 .. e2]);
		}
		if(this.payloadType() == Type.Object) {
			var operator = this["opSlice"];
			if(operator._type == Type.Function) {
				return operator.call(this, e1, e2);
			}
		}

		// might be worth throwing here too
		return var(null);
	}

	/// Forwards to [opIndex]
	public @property ref var opDispatch(string name, string file = __FILE__, size_t line = __LINE__)() {
		return this[name];
	}

	/// Forwards to [opIndexAssign]
	public @property ref var opDispatch(string name, string file = __FILE__, size_t line = __LINE__, T)(T r) {
		return this.opIndexAssign!T(r, name);
	}

	/// Looks up a sub-property of the object
	public ref var opIndex(var name, string file = __FILE__, size_t line = __LINE__) {
		return opIndex(name.get!string, file, line);
	}

	/// Sets a sub-property of the object
	public ref var opIndexAssign(T)(T t, var name, string file = __FILE__, size_t line = __LINE__) {
		return opIndexAssign(t, name.get!string, file, line);
	}

	public ref var opIndex(string name, string file = __FILE__, size_t line = __LINE__) {
		// if name is numeric, we should convert to int for arrays
		if(name.length && name[0] >= '0' && name[0] <= '9' && this.payloadType() == Type.Array)
			return opIndex(to!size_t(name), file, line);

		if(this.payloadType() != Type.Object && name == "prototype")
			return prototype();

		if(name == "typeof") {
			var* tmp = new var;
			*tmp = to!string(this.payloadType());
			return *tmp;
		}

		if(name == "toJson") {
			var* tmp = new var;
			*tmp = to!string(this.toJson());
			return *tmp;
		}

		if(name == "length" && this.payloadType() == Type.String) {
			var* tmp = new var;
			*tmp = _payload._string.length;
			return *tmp;
		}
		if(name == "length" && this.payloadType() == Type.Array) {
			var* tmp = new var;
			*tmp = _payload._array.length;
			return *tmp;
		}
		if(name == "__prop" && this.payloadType() == Type.Object) {
			var* tmp = new var;
			(*tmp)._function = delegate var(var _this, var[] args) {
				if(args.length == 0)
					return var(null);
				if(args.length == 1) {
					auto peek = this._payload._object._peekMember(args[0].get!string, false);
					if(peek is null)
						return var(null);
					else
						return *peek;
				}
				if(args.length == 2) {
					auto peek = this._payload._object._peekMember(args[0].get!string, false);
					if(peek is null) {
						this._payload._object._properties[args[0].get!string] = args[1];
						return var(null);
					} else {
						*peek = args[1];
						return *peek;
					}

				}
				throw new Exception("too many args");
			};
			return *tmp;
		}

		PrototypeObject from;
		if(this.payloadType() == Type.Object)
			from = _payload._object;
		else {
			var pt = this.prototype();
			assert(pt.payloadType() == Type.Object);
			from = pt._payload._object;
		}

		if(from is null) {
			if(jsvar_throw)
				throw new DynamicTypeException(var(null), Type.Object, file, line);
			else
				return *(new var);
		}
		return from._getMember(name, true, false, false, file, line);
	}

	public ref var opIndexAssign(T)(T t, string name, string file = __FILE__, size_t line = __LINE__) {
		if(this.payloadType == Type.Array && name.appearsNumeric()) {
			try {
				auto i = to!size_t(name);
				return opIndexAssign(t, i, file, line);
			} catch(Exception)
				{} // ignore bad index, use it as a string instead lol
		}
		_requireType(Type.Object); // FIXME?
		if(_payload._object is null)
			throw new DynamicTypeException(var(null), Type.Object, file, line);

		return this._payload._object._setMember(name, var(t), false, false, false, file, line);
	}

	public ref var opIndexAssignNoOverload(T)(T t, string name, string file = __FILE__, size_t line = __LINE__) {
		if(name.length && name[0] >= '0' && name[0] <= '9')
			return opIndexAssign(t, to!size_t(name), file, line);
		_requireType(Type.Object); // FIXME?
		if(_payload._object is null)
			throw new DynamicTypeException(var(null), Type.Object, file, line);

		return this._payload._object._setMember(name, var(t), false, false, true, file, line);
	}


	public ref var opIndex(size_t idx, string file = __FILE__, size_t line = __LINE__) {
		if(_type == Type.Array) {
			auto arr = this._payload._array;
			if(idx < arr.length)
				return arr[idx];
		} else if(_type == Type.Object) {
			// objects might overload opIndex
			var* n = new var();
			if("opIndex" in this)
				*n = this["opIndex"](idx);
			return *n;
		}
		if(jsvar_throw) {
			throw new DynamicTypeException(this, Type.Array, file, line);
		} else {
			var* n = new var();
			return *n;
		}
	}

	public ref var opIndexAssign(T)(T t, size_t idx, string file = __FILE__, size_t line = __LINE__) {
		if(_type == Type.Array) {
			if(idx >= this._payload._array.length)
				this._payload._array.length = idx + 1;
			this._payload._array[idx] = t;
			return this._payload._array[idx];
		} else if(_type == Type.Object) {
			return opIndexAssign(t, to!string(idx), file, line);
		}
		if(jsvar_throw) {
			throw new DynamicTypeException(this, Type.Array, file, line);
		} else {
			var* n = new var();
			return *n;
		}
	}

	ref var _getOwnProperty(string name, string file = __FILE__, size_t line = __LINE__) {
		if(_type == Type.Object) {
			if(_payload._object !is null) {
				auto peek = this._payload._object._peekMember(name, false);
				if(peek !is null)
					return *peek;
			}
		}
		//if(jsvar_throw)
			//throw new DynamicTypeException(this, Type.Object, file, line);
		var* n = new var();
		return *n;
	}

	@property static var emptyObject(PrototypeObject prototype = null) {
		var v;
		v._type = Type.Object;
		v._payload._object = new PrototypeObject();
		v._payload._object.prototype = prototype;
		return v;
	}

	///
	@property static var emptyObject(var prototype) {
		if(prototype._type == Type.Object)
			return var.emptyObject(prototype._payload._object);
		return var.emptyObject();
	}

	@property PrototypeObject prototypeObject() {
		var v = prototype();
		if(v._type == Type.Object)
			return v._payload._object;
		return null;
	}

	// what I call prototype is more like what Mozilla calls __proto__, but tbh I think this is better so meh
	@property ref var prototype() {
		static var _arrayPrototype;
		static var _functionPrototype;
		static var _stringPrototype;

		final switch(payloadType()) {
			case Type.Array:
				assert(_arrayPrototype._type == Type.Object);
				if(_arrayPrototype._payload._object is null) {
					_arrayPrototype._object = new PrototypeObject();
				}

				return _arrayPrototype;
			case Type.Function:
				assert(_functionPrototype._type == Type.Object);
				if(_functionPrototype._payload._object is null) {
					_functionPrototype._object = new PrototypeObject();
				}

				return _functionPrototype;
			case Type.String:
				assert(_stringPrototype._type == Type.Object);
				if(_stringPrototype._payload._object is null) {
					auto p = new PrototypeObject();
					_stringPrototype._object = p;

					var replaceFunction;
					replaceFunction._type = Type.Function;
					replaceFunction._function = (var _this, var[] args) {
						string s = _this.toString();
						import std.array : replace;
						return var(std.array.replace(s,
							args[0].toString(),
							args[1].toString()));
					};

					p._properties["replace"] = replaceFunction;
				}

				return _stringPrototype;
			case Type.Object:
				if(_payload._object)
					return _payload._object._prototype;
				// FIXME: should we do a generic object prototype?
			break;
			case Type.Integral:
			case Type.Floating:
			case Type.Boolean:
				// these types don't have prototypes
		}


		var* v = new var(null);
		return *v;
	}

	///
	@property static var emptyArray() {
		var v;
		v._type = Type.Array;
		return v;
	}

	///
	static var fromJson(const(char)[] json) {
		auto decoded = parseJSON(json);
		return var.fromJsonValue(decoded);
	}

	///
	static var fromJsonFile(const(char)[] filename) {
		import std.file;
		return var.fromJson(readText(filename));
	}

	///
	static var fromJsonValue(JSONValue v) {
		var ret;

		final switch(v.type) {
			case JSONType.string:
				ret = v.str;
			break;
			case JSONType.uinteger:
				ret = v.uinteger;
			break;
			case JSONType.integer:
				ret = v.integer;
			break;
			case JSONType.float_:
				ret = v.floating;
			break;
			case JSONType.object:
				ret = var.emptyObject;
				foreach(k, val; v.object) {
					ret[k] = var.fromJsonValue(val);
				}
			break;
			case JSONType.array:
				ret = var.emptyArray;
				ret._payload._array.length = v.array.length;
				foreach(idx, item; v.array) {
					ret._payload._array[idx] = var.fromJsonValue(item);
				}
			break;
			case JSONType.true_:
				ret = true;
			break;
			case JSONType.false_:
				ret = false;
			break;
			case JSONType.null_:
				ret = null;
			break;
		}

		return ret;
	}

	///
	string toJson() {
		auto v = toJsonValue();
		return toJSON(v);
	}

	///
	JSONValue toJsonValue() {
		JSONValue val;
		final switch(payloadType()) {
			case Type.Boolean:
				version(new_std_json)
					val = this._payload._boolean;
				else {
					if(this._payload._boolean)
						val.type = JSONType.true_;
					else
						val.type = JSONType.false_;
				}
			break;
			case Type.Object:
				version(new_std_json) {
					if(_payload._object is null) {
						val = null;
					} else {
						val = _payload._object.toJsonValue();
					}
				} else {
					if(_payload._object is null) {
						val.type = JSONType.null_;
					} else {
						val.type = JSONType.object;
						foreach(k, v; _payload._object._properties) {
							val.object[k] = v.toJsonValue();
						}
					}
				}
			break;
			case Type.String:
				version(new_std_json) { } else {
					val.type = JSONType.string;
				}
				val.str = _payload._string;
			break;
			case Type.Integral:
				version(new_std_json) { } else {
					val.type = JSONType.integer;
				}
				val.integer = _payload._integral;
			break;
			case Type.Floating:
				version(new_std_json) { } else {
					val.type = JSONType.float_;
				}
				val.floating = _payload._floating;
			break;
			case Type.Array:
				auto a = _payload._array;
				JSONValue[] tmp;
				tmp.length = a.length;
				foreach(i, v; a) {
					tmp[i] = v.toJsonValue();
				}

				version(new_std_json) {
					val = tmp;
				} else {
					val.type = JSONType.array;
					val.array = tmp;
				}
			break;
			case Type.Function:
				version(new_std_json)
					val = null;
				else
					val.type = JSONType.null_; // ideally we would just skip it entirely...
			break;
		}
		return val;
	}
}

class PrototypeObject {
	string name;
	var _prototype;

	package PrototypeObject _secondary; // HACK don't use this

	PrototypeObject prototype() {
		if(_prototype.payloadType() == var.Type.Object)
			return _prototype._payload._object;
		return null;
	}

	PrototypeObject prototype(PrototypeObject set) {
		this._prototype._object = set;
		return set;
	}

	override string toString() {

		var* ts = _peekMember("toString", true);
		if(ts) {
			var _this;
			_this._object = this;
			return (*ts).call(_this).get!string;
		}

		JSONValue val;
		version(new_std_json) {
			JSONValue[string] tmp;
			foreach(k, v; this._properties)
				tmp[k] = v.toJsonValue();
			val.object = tmp;
		} else {
			val.type = JSONType.object;
			foreach(k, v; this._properties)
				val.object[k] = v.toJsonValue();
		}

		return toJSON(val);
	}

	var[string] _properties;

	PrototypeObject copy() {
		auto n = new PrototypeObject();
		n.prototype = this.prototype;
		n.name = this.name;
		foreach(k, v; _properties) {
			n._properties[k] = v._copy;
		}
		return n;
	}

	bool isSpecial() { return false; }

	PrototypeObject new_(PrototypeObject newThis) {
		// if any of the prototypes are D objects, we need to try to copy them.
		auto p = prototype;

		PrototypeObject[32] stack;
		PrototypeObject[] fullStack = stack[];
		int stackPos;

		while(p !is null) {

			if(p.isSpecial()) {
				auto n = new PrototypeObject();

				auto proto = p.new_(n);

				while(stackPos) {
					stackPos--;
					auto pr = fullStack[stackPos].copy();
					pr.prototype = proto;
					proto = pr;
				}

				n.prototype = proto;
				n.name = this.name;
				foreach(k, v; _properties) {
					n._properties[k] = v._copy;
				}

				return n;
			}

			if(stackPos >= fullStack.length)
				fullStack ~= p;
			else
				fullStack[stackPos] = p;
			stackPos++;

			p = p.prototype;
		}

		return copy();
	}

	PrototypeObject copyPropertiesFrom(PrototypeObject p) {
		foreach(k, v; p._properties) {
			this._properties[k] = v._copy;
		}
		return this;
	}

	var* _peekMember(string name, bool recurse) {
		if(name == "prototype")
			return &_prototype;

		auto curr = this;

		// for the secondary hack
		bool triedOne = false;
		// for the secondary hack
		PrototypeObject possibleSecondary;

		tryAgain:
		do {
			auto prop = name in curr._properties;
			if(prop is null) {
				// the secondary hack is to do more scoping in the script, it is really hackish
				if(possibleSecondary is null)
					possibleSecondary = curr._secondary;

				if(!recurse)
					break;
				else
					curr = curr.prototype;
			} else
				return prop;
		} while(curr);

		if(possibleSecondary !is null) {
			curr = possibleSecondary;
			if(!triedOne) {
				triedOne = true;
				goto tryAgain;
			}
		}

		return null;
	}

	// FIXME: maybe throw something else
	/*package*/ ref var _getMember(string name, bool recurse, bool throwOnFailure, bool returnRawProperty = false, string file = __FILE__, size_t line = __LINE__) {
		var* mem = _peekMember(name, recurse);

		if(mem !is null) {
			// If it is a property, we need to call the getter on it
			if((*mem).payloadType == var.Type.Object && cast(PropertyPrototype) (*mem)._payload._object) {
				if(returnRawProperty)
					return *mem;
				auto prop = cast(PropertyPrototype) (*mem)._payload._object;
				return prop.get;
			}
			return *mem;
		}

		mem = _peekMember("opIndex", recurse);
		if(mem !is null) {
			auto n = new var;
			*n = ((*mem)(name));
			return *n;
		}

		// if we're here, the property was not found, so let's implicitly create it
		if(throwOnFailure)
			throw new DynamicTypeException("no such property " ~ name, file, line);
		var n;
		this._properties[name] = n;
		return this._properties[name];
	}

	// FIXME: maybe throw something else
	/*package*/ ref var _setMember(string name, var t, bool recurse, bool throwOnFailure, bool suppressOverloading, string file = __FILE__, size_t line = __LINE__) {
		var* mem = _peekMember(name, recurse);

		bool onParent = false;

		if(mem is null && !recurse) {
			mem = _peekMember(name, true); // properties need the check anyway as a setter might be on a prototype
			onParent = true;
		}

		if(mem !is null) {
			// Property check - the setter should be proxied over to it
			if((*mem).payloadType == var.Type.Object && cast(PropertyPrototype) (*mem)._payload._object) {
				auto prop = cast(PropertyPrototype) (*mem)._payload._object;
				return prop.set(t);
			}
			if(!onParent) {
				*mem = t;
				return *mem;
			} else {
				mem = null;
			}
		}

		if(!suppressOverloading) {
			mem = _peekMember("opIndexAssign", true);
			if(mem !is null) {
				auto n = new var;
				*n = ((*mem)(t, name));
				return *n;
			}
		}

		// if we're here, the property was not found, so let's implicitly create it
		if(throwOnFailure)
			throw new DynamicTypeException("no such property " ~ name, file, line);
		this._properties[name] = t;
		return this._properties[name];
	}

	JSONValue toJsonValue() {
		JSONValue val;
		JSONValue[string] tmp;
		foreach(k, v; this._properties) {
			// if it is an overload set and/or a function, just skip it.
			// or really if it is a wrapped native object it should prolly just be skipped anyway
			// unless it actually defines a toJson.
			if(v.payloadType == var.Type.Function)
				continue;
			if(v.payloadType == var.Type.Object) {
				// I'd love to get the json value out but idk. FIXME
				if(v._payload._object is null) continue;
				if(auto wno = cast(WrappedNativeObject) v._payload._object) {
					auto obj = wno.getObject();
					if(obj is null)
						tmp[k] = null;
					else
						tmp[k] = obj.toString();
					continue;
				} else if(typeid(PrototypeObject) !is typeid(v._payload._object))
					continue;
			}

			tmp[k] = v.toJsonValue();
		}

		val = tmp;
		return val;
	}

	public int opApply(scope int delegate(var, ref var) dg) {
		foreach(k, v; this._properties) {
			if(v.payloadType == var.Type.Object && cast(PropertyPrototype) v._payload._object)
				v = (cast(PropertyPrototype) v._payload._object).get;
			if(auto result = dg(var(k), v))
				return result;
		}
		return 0;
	}
}

// A property is a special type of object that can only be set by assigning
// one of these instances to foo.child._object. When foo.child is accessed and it
// is an instance of PropertyPrototype, it will return the getter. When foo.child is
// set (excluding direct assignments through _type), it will call the setter.
class PropertyPrototype : PrototypeObject {
	var delegate() getter;
	void delegate(var) setter;
	this(var delegate() getter, void delegate(var) setter) {
		this.getter = getter;
		this.setter = setter;
	}

	override string toString() {
		return get.toString();
	}

	ref var get() {
		var* g = new var();
		*g = getter();
		return *g;
	}

	ref var set(var t) {
		setter(t);
		return get;
	}

	override JSONValue toJsonValue() {
		return get.toJsonValue();
	}
}

///
struct ScriptLocation {
	string scriptFilename; ///
	int lineNumber; ///
}

class DynamicTypeException : Exception {
	this(string msg, string file = __FILE__, size_t line = __LINE__) {
		super(msg, file, line);
	}
	this(var v, var.Type required, string file = __FILE__, size_t line = __LINE__) {
		import std.string;
		if(v.payloadType() == required)
			super(format("Tried to use null as a %s", required), file, line);
		else {
			super(format("Tried to use %s%s as a %s", v == null ? "null ": "", v.payloadType(), required), file, line);
		}
	}

	override void toString(scope void delegate(in char[]) sink) const {
		import std.format;
		if(varName.length)
			sink(varName);
		if(callStack.length) {
			sink("arsd.jsvar.DynamicTypeException@");
			sink(file);
			sink("(");
			sink(to!string(line));
			sink("): ");
			sink(msg);
			foreach(cs; callStack)
				sink(format("\n\t%s:%s", cs.scriptFilename, cs.lineNumber));

			bool found;
			void hiddenSink(in char[] str) {
				// I just want to hide the call stack until the interpret call...
				// since the script stack above is more meaningful to users.
				//
				// but then I will go back to the D functions once on the outside.
				import std.string;
				if(found)
					sink(str);
				else if(str.indexOf("arsd.script.interpret(") != -1)
					found = true;
			}

			sink("\n--------");

			super.toString(&hiddenSink);
		} else {
			super.toString(sink);
		}
	}

	ScriptLocation[] callStack;
	string varName;
}

template makeAscii() {
	string helper() {
		string s;
		foreach(i; 0 .. 128)
			s ~= cast(char) i;
		return s;
	}

	enum makeAscii = helper();
}

package interface VarMetadata { }

interface ScriptableSubclass {
	void setScriptVar(var);
	var  getScriptVar();
	final bool methodOverriddenByScript(string name) {
		PrototypeObject t = getScriptVar().get!PrototypeObject;
		// the top one is the native object from subclassable so we don't want to go all the way there to avoid endless recursion
		//import std.stdio; writeln("checking ", name , " ...", "wtf");
		if(t !is null)
		while(!t.isSpecial) {
			if(t._peekMember(name, false) !is null)
				return true;
			t = t.prototype;
		}
		return false;
	}
}

/++
	EXPERIMENTAL

	Allows you to make a class available to the script rather than just class objects.
	You can subclass it in script and then call the methods again through the original
	D interface. With caveats...


	Assumes ALL $(I virtual) methods and constructors are scriptable, but requires
	`@scriptable` to be present on final or static methods. This may change in the future.

	Note that it has zero support for `@safe`, `pure`, `nothrow`, and other attributes
	at this time and will skip that use those. I may be able to loosen this in the
	future as well but I have no concrete plan to at this time. You can still mark
	them as `@scriptable` to call them from the script, but they can never be overridden
	by script code because it cannot verify those guarantees hold true.

	Ditto on `const` and `immutable`.

	Its behavior on overloads is currently undefined - it may keep only any random
	overload as the only one and do dynamic type conversions to cram data into it.
	This is likely to change in the future but for now try not to use this on classes
	with overloaded methods.

	It also does not wrap member variables unless explicitly marked `@scriptable`; it
	is meant to communicate via methods.

	History:
	Added April 25, 2020
+/
static if(__traits(compiles, mixin(q{  () { static foreach(i; [1,2]) {} } }) ))
mixin(q{
var subclassable(T)() if(is(T == class) || is(T == interface)) {
	import std.traits;

	static final class ScriptableT : T, ScriptableSubclass {
		var _this;
		void setScriptVar(var v) { _this = v; }
		var getScriptVar() { return _this; }
		bool _next_devirtualized;

		// @scriptable size_t _nativeHandle_() { return cast(size_t) cast(void*) this;}

		static if(__traits(compiles,  __traits(getOverloads, T, "__ctor")))
		static foreach(ctor; __traits(getOverloads, T, "__ctor"))
			@scriptable this(Parameters!ctor p) { super(p); }

		static foreach(memberName; __traits(allMembers, T)) {
		static if(memberName != "toHash")
		static foreach(overload; __traits(getOverloads, T, memberName))
		static if(__traits(isVirtualMethod, overload))
		static if(!__traits(isFinalFunction, overload))
		static if(!__traits(isDeprecated, overload))
		// note: overload behavior undefined
		static if(!(functionAttributes!(overload) & (FunctionAttribute.pure_ | FunctionAttribute.safe | FunctionAttribute.trusted | FunctionAttribute.nothrow_ | FunctionAttribute.const_ | FunctionAttribute.inout_)))
		static if(!hasRefParam!overload)
		static if(__traits(getFunctionVariadicStyle, overload) == "none")
		static if(__traits(identifier, overload) == memberName) // to filter out aliases
		mixin(q{
			@scriptable
			override ReturnType!(overload)
			}~memberName~q{
			(Parameters!(overload) p)
			{
			//import std.stdio; writeln("calling ", T.stringof, ".", memberName, " - ", methodOverriddenByScript(memberName), "/", _next_devirtualized, " on ", cast(size_t) cast(void*) this);
				if(_next_devirtualized || !methodOverriddenByScript(memberName)) {
					_next_devirtualized = false;
					return __traits(getMember, super, memberName)(p);
				}
				return _this[memberName].call(_this, p).get!(typeof(return));
			}
		});
		}

		// I don't want to necessarily call a constructor but I need an object t use as the prototype
		// hence this faked one. hopefully the new operator will see void[] and assume it can have GC ptrs...
		static ScriptableT _allocate_(PrototypeObject newThis) {
			void[] store = new void[](__traits(classInstanceSize, ScriptableT));
			store[] = typeid(ScriptableT).initializer[];
			ScriptableT dummy = cast(ScriptableT) store.ptr;
			dummy._this = var(newThis);
			//import std.stdio; writeln("Allocating new ", cast(ulong) store.ptr);
			return dummy;
		}
	}

	ScriptableT dummy = ScriptableT._allocate_(null);

	var proto = wrapNativeObject!(ScriptableT, true)(dummy);

	var f = var.emptyObject;
	f.prototype = proto;

	return f;
}

template hasRefParam(alias overload) {
	bool helper() {
		static if(is(typeof(overload) P == __parameters))
		foreach(idx, p; P)
		foreach(thing; __traits(getParameterStorageClasses, overload, idx))
			if(thing == "ref")
				return true;
		return false;
	}

	enum hasRefParam = helper();
}

});

/// Demonstrates tested capabilities of [subclassable]
version(with_arsd_script)
unittest {
	interface IFoo {
		string method();
		int method2();
		int args(int, int);
	}
	// note the static is just here because this
	// is written in a unittest; it shouldn't actually
	// be necessary under normal circumstances.
	static class Foo : IFoo {
		ulong handle() { return cast(ulong) cast(void*) this; }
		string method() { return "Foo"; }
		int method2() { return 10; }
		int args(int a, int b) {
			//import std.stdio; writeln(a, " + ", b, " + ", member_, " on ", cast(ulong) cast(void*) this);
			return member_+a+b; }

		int member_;
		@property int member(int i) { return member_ = i; }
		@property int member() { return member_; }

		@scriptable final int fm() { return 56; }
	}
	static class Bar : Foo {
		override string method() { return "Bar"; }
		string test1() { return test2(); }
		string test2() { return "test2"; }

		int acceptFoo(Foo f) {
			return f.method2();
		}
	}
	static class Baz : Bar {
		override int method2() { return 20; }
	}

	static class WithCtor {
		// constructors work but are iffy with overloads....
		this(int arg) { this.arg = arg; }
		@scriptable int arg; // this is accessible cuz it is @scriptable
		int getValue() { return arg; }
	}

	var globals = var.emptyObject;
	globals.Foo = subclassable!Foo;
	globals.Bar = subclassable!Bar;
	globals.Baz = subclassable!Baz;
	globals.WithCtor = subclassable!WithCtor;

	import arsd.script;

	interpret(q{
		// can instantiate D classes added via subclassable
		var foo = new Foo();
		// and call its methods...
		assert(foo.method() == "Foo");
		assert(foo.method2() == 10);

		foo.member(55);

		// proves the new operator actually creates new D
		// objects as well to avoid sharing instance state.
		var foo2 = new Foo();
		assert(foo2.handle() != foo.handle());

		// passing arguments works
		assert(foo.args(2, 4) == 6 + 55); // (and sanity checks operator precedence)

		var bar = new Bar();
		assert(bar.method() == "Bar");
		assert(bar.method2() == 10);

		assert(bar.acceptFoo(new Foo()) == 10);
		assert(bar.acceptFoo(new Baz()) == 20);

		// this final member is accessible because it was marked @scriptable
		assert(bar.fm() == 56);

		assert(bar.test1() == "test2");
		assert(bar.test2() == "test2");

		// the script can even subclass D classes!
		class Amazing : Bar {
			// and override its methods
			var inst = 99;
			function method() {
				return "Amazing";
			}

			// note: to access instance members or virtual call lookup you MUST use the `this` keyword
			// otherwise the function will be called with scope limited to this class itself (similar to javascript)
			function other() {
				// this.inst is needed to get the instance variable (otherwise it would only look for a static var)
				// and this.method triggers dynamic lookup there, so it will get children's overridden methods if there is one
				return this.inst ~ this.method();
			}

			function args(a, b) {
				// calling parent class method still possible
				return super.args(a*2, b*2);
			}

			function test2() {
				return "script test";
			}
		}

		var amazing = new Amazing();
		assert(amazing.method() == "Amazing");
		assert(amazing.method2() == 10); // calls back to the parent class
		amazing.member(5);

		assert(amazing.test1() == "script test"); // even the virtual method call from D goes into the script override

		// this line I can paste down to interactively debug the test btw.
		//}, globals); repl!true(globals); interpret(q{

		assert(amazing.args(2, 4) == 12+5);

		var wc = new WithCtor(5); // argument passed to constructor
		assert(wc.getValue() == 5);

		// confirm the property read works too
		assert(wc.arg == 5);

		// but property WRITING is currently not working though.


		class DoubleChild : Amazing {
			function method() {
				return "DoubleChild";
			}
		}

		// can also do a child of a child class
		var dc = new DoubleChild();
		assert(dc.method() == "DoubleChild");
		assert(dc.other() == "99DoubleChild"); // the `this.method` means it uses the replacement now
		assert(dc.method2() == 10); // back to the D grandparent
		assert(dc.args(2, 4) == 12); // but the args impl from above
	}, globals);

	Foo foo = globals.foo.get!Foo; // get the native object back out
	assert(foo.member == 55); // and see mutation via properties proving object mutability
	assert(globals.foo.get!Bar is null); // cannot get the wrong class out of it
	assert(globals.foo.get!Object !is null); // but can do parent classes / interfaces
	assert(globals.foo.get!IFoo !is null);
	assert(globals.bar.get!Foo !is null); // the Bar can also be a Foo

	Bar amazing = globals.amazing.get!Bar; // instance of the script's class is still accessible through parent D class or interface
	assert(amazing !is null); // object exists
	assert(amazing.method() == "Amazing"); // calls the override from the script
	assert(amazing.method2() == 10); // non-overridden function works as expected

	IFoo iamazing = globals.amazing.get!IFoo; // and through just the interface works the same way
	assert(iamazing !is null);
	assert(iamazing.method() == "Amazing");
	assert(iamazing.method2() == 10);

	// and here ensuring the interface we got out can go back in and back out too
	var globals2 = var.emptyObject;
	globals2.opDispatch!"iamazing2" = iamazing;
	// though inside, it is treated as an opaque reference since nothing is @scriptable
	// so nothing to test there...
	// however getting it back out should work fine
	IFoo gotten = globals2.iamazing2.get!IFoo;
	assert(gotten !is null);
	assert(gotten is iamazing);
	//import std.stdio; writeln("wtf", iamazing.method());

	// now this seems obvious, but adding the interface can actually overwrite the dynamic
	// prototype.... i think
	//assert(gotten.method() == "Amazing");

	//https://issues.dlang.org/show_bug.cgi?id=22011 necessitated the static!
	static class CFoo : IFoo {
		@scriptable string method() { return "CFoo"; }
		int method2() { return 55; }
		int args(int, int) { return 6 + 5; }
	}

	globals.opDispatch!"iamazing3" = new CFoo; // and also just testing a standard class assign
	IFoo input = new CFoo();
	globals.opDispatch!"iamazing4" = input; // and interface
	interpret(q{
		var a = iamazing3.method();
		assert(a == "CFoo"); // works cuz the class is scriptable
		// but the following line is NOT available because the interface itself is not marked scriptable
		//assert(iamazing4.method() == "CFoo");
	}, globals);

	// but we can get it right back out
	IFoo got2 = globals.iamazing4.get!IFoo;
	assert(got2 is input);
	assert(got2.method() == "CFoo");
}

unittest {
	static struct Foo {
		struct Bar {
			int[] x;
		}

		Bar bar;
	}

	assert(
		var.fromJson(`{
			"bar":{ "x": [1,2,3]}
		}`).get!Foo
		==
		Foo(Foo.Bar([1,2,3]))
	);
}

version(with_arsd_script)
unittest {
	import arsd.script;
	static class Test {
		@scriptable int a;
	}

	Test test = new Test;

	test.a = 15;

	var globals = var.emptyObject;
	globals.test = test;
	globals.Test = subclassable!Test;

	interpret(q{
		  test.a = 4;
		  var wtf = test.a += 5;
		  assert(wtf == 9);
		  assert(test.a == 9);
		  var test2 = new Test;
		  test2.a = 2;
		  test2.a += 5;
		  assert(test2.a == 7);
	}, globals);

	assert(test.a == 9);
	assert(globals.test2.get!Test.a == 7);
}

// just a base class we can reference when looking for native objects
class WrappedNativeObject : PrototypeObject {
	TypeInfo wrappedType;
	abstract Object getObject();
}

template helper(alias T) { alias helper = T; }

/++
	Wraps a class. If you are manually managing the memory, remember the jsvar may keep a reference to the object; don't free it!

	To use this: `var a = wrapNativeObject(your_d_object);` OR `var a = your_d_object`;

	By default, it will wrap all methods and members with a public or greater protection level. The second template parameter can filter things differently. FIXME implement this

	History:
		This became the default after April 24, 2020. Previously, [var.opAssign] would [wrapOpaquely] instead.
+/
WrappedNativeObject wrapNativeObject(Class, bool special = false)(Class obj) if(is(Class == class) || is(Class == interface)) {
	import std.meta;
	static class WrappedNativeObjectImpl : WrappedNativeObject {
		override Object getObject() {
			return cast(Object) obj;
		}

		override bool isSpecial() { return special; }

		static if(special)
		override WrappedNativeObject new_(PrototypeObject newThis) {
			return new WrappedNativeObjectImpl(obj._allocate_(newThis));
		}

		Class obj;

		this(Class objIn) {
			this.obj = objIn;
			wrappedType = typeid(cast(Object) obj);
			// wrap the other methods
			// and wrap members as scriptable properties

			foreach(memberName; __traits(allMembers, Class)) static if(!mixin(deprecationCheck()) && is(typeof(__traits(getMember, obj, memberName)) type)) {
				static if(is(type == function)) {
					auto os = new OverloadSet();
					foreach(idx, overload; AliasSeq!(__traits(getOverloads, obj, memberName))) static if(.isScriptable!(__traits(getAttributes, overload))()) {
						var gen;
						gen._function = delegate (var vthis_, var[] vargs) {
							Parameters!(__traits(getOverloads, Class, memberName)[idx]) fargs;


							enum lol = static_foreach(fargs.length, 1, -1,
								`__traits(getOverloads, obj, memberName)[idx](`,``,` < vargs.length ? vargs[`,`].get!(typeof(fargs[`,`])) : ParamDefault!(__traits(getOverloads, Class, memberName)[idx], `,`)(),`,`)`);
							/*
							enum lol = static_foreach(fargs.length, 1, -1,
								`__traits(getOverloads, obj, memberName)[idx](`,``,` < vargs.length ? vargs[`,`].get!(typeof(fargs[`,`])) :
								typeof(fargs[`,`]).init,`,`)`);
							*/

							// FIXME: what if there are multiple @scriptable overloads?!
							// FIXME: what about @properties?

							static if(special) {
								Class obj;
								//if(vthis_.payloadType() != var.Type.Object) { import std.stdio; writeln("getwno on ", vthis_); }
								// the native object might be a step or two up the prototype
								// chain due to script subclasses, need to find it...
								while(vthis_ != null) {
									obj = vthis_.getWno!Class;
									if(obj !is null)
										break;
									vthis_ = vthis_.prototype;
								}

								if(obj is null) throw new Exception("null native object");
							}

							static if(special) {
								obj._next_devirtualized = true;
								scope(exit) obj._next_devirtualized = false;
							}

							var ret;

							static if(!is(typeof(__traits(getOverloads, obj, memberName)[idx](fargs)) == void))
								ret = mixin(lol);
							else
								mixin(lol ~ ";");

							return ret;
						};

						Parameters!(overload) fargs;
						// FIXME: if an argument type is a class, we need to actually look it up in the script context somehow
						var[] definition;
						foreach(arg; fargs) {
							definition ~= var(arg);
						}

						//import std.stdio; writeln(Class.stringof, ".", memberName, " ", definition);
						os.addOverload(OverloadSet.Overload(definition, gen));
					}

					_properties[memberName] = var(os);
				} else {
					alias fuckThisLanguage = __traits(getOverloads, Class, memberName, true);
					static if(fuckThisLanguage.length)
						alias fuckYourPointlessDeprecations = fuckThisLanguage[0];
					else
						alias fuckYourPointlessDeprecations = __traits(getMember, Class, memberName);
					static if(.isScriptable!(__traits(getAttributes, fuckYourPointlessDeprecations))())
					// if it has a type but is not a function, it is prolly a member
					_properties[memberName] = new PropertyPrototype(
						() => var(__traits(getMember, obj, memberName)),
						(var v) {
							// read-only property hack
							static if(__traits(compiles, __traits(getMember, obj, memberName) = v.get!(type)))
							__traits(getMember, obj, memberName) = v.get!(type);
						});
				}
			}
		}
	}

	return new WrappedNativeObjectImpl(obj);
}

import std.traits;
class WrappedOpaque(T) : PrototypeObject if(isPointer!T || is(T == class) || is(T == interface)) {
	T wrapped;
	this(T t) {
		wrapped = t;
	}
	T wrapping() {
		return wrapped;
	}
}
class WrappedOpaque(T) : PrototypeObject if(!isPointer!T && !is(T == class) && !is(T == interface)) {
	T* wrapped;
	this(T t) {
		wrapped = new T;
		(cast() *wrapped) = t;
	}
	this(T* t) {
		wrapped = t;
	}
	T* wrapping() {
		return wrapped;
	}
}

WrappedOpaque!Obj wrapOpaquely(Obj)(Obj obj) {
	static if(is(Obj == class)) {
		if(obj is null)
			return null;
	}
	return new WrappedOpaque!Obj(obj);
}

/**
	Wraps an opaque struct pointer in a module with ufcs functions
*/
WrappedNativeObject wrapUfcs(alias Module, Type)(Type obj) {
	import std.meta;
	return new class WrappedNativeObject {
		override Object getObject() {
			return null; // not actually an object! but close to
		}

		this() {
			wrappedType = typeid(Type);
			// wrap the other methods
			// and wrap members as scriptable properties

			foreach(memberName; __traits(allMembers, Module)) static if(is(typeof(__traits(getMember, Module, memberName)) type)) {
				static if(is(type == function)) {
					foreach(idx, overload; AliasSeq!(__traits(getOverloads, Module, memberName))) static if(.isScriptable!(__traits(getAttributes, __traits(getOverloads, Module, memberName)[idx]))()) { // lol  i have to do the second getOverload to stop the compiler from deprecating
						auto helper = &__traits(getOverloads, Module, memberName)[idx];
						static if(Parameters!helper.length >= 1 && is(Parameters!helper[0] == Type)) {
							// this staticMap is a bit of a hack so it can handle `in float`... liable to break with others, i'm sure
							_properties[memberName] = (staticMap!(Unqual, Parameters!helper[1 .. $]) args) {
								return __traits(getOverloads, Module, memberName)[idx](obj, args);
							};
						}
					}
				}
			}
		}
	};
}

bool isCallableJsvarObject(var possibility) {
	if(possibility.payloadType == var.Type.Function)
		return true;
	if(possibility.payloadType == var.Type.Object) {
		if(possibility._payload._object is null)
			return false;

		if(var* test = possibility._payload._object._peekMember("opCall", true)) {
			if(isCallableJsvarObject(*test))
				return true;
		}

	}
	return false;
}


bool isScriptable(attributes...)() {
	bool nonConstConditionForWorkingAroundASpuriousDmdWarning = true;
	foreach(attribute; attributes) {
		static if(is(typeof(attribute) == string)) {
			static if(attribute == scriptable) {
				if(nonConstConditionForWorkingAroundASpuriousDmdWarning)
				return true;
			}
		}
	}
	return false;
}

bool isScriptableOpaque(T)() {
	static if(is(typeof(T.isOpaqueStruct) == bool))
		return T.isOpaqueStruct == true;
	else
		return false;
}

int typeCompatibilityScore(var arg, var type) {
	int thisScore = 0;

	if(type.payloadType == var.Type.Object && type._payload._object is null) {
		thisScore += 1; // generic match
		return thisScore;
	}
	if(arg.payloadType == var.Type.Integral && type.payloadType == var.Type.Floating) {
		thisScore += 2; // match with implicit conversion
	// FIXME: want to support implicit from whatever to bool?
	} else if(arg.payloadType != type.payloadType) {
		thisScore = 0;
		return thisScore;
	} else {
		// exact type category match
		if(type.payloadType == var.Type.Array) {
			// arrays not really supported here....
			// so just like if both are arrays i'll take
			// it as a bare minimum but i don't love it otherwise
			if(arg.payloadType == var.Type.Array)
				thisScore = 1;
			else
				thisScore = 0;
			return thisScore;
		} else if(type.payloadType == var.Type.Object) {
			// objects are the interesting one...
			// want to see if it matches by seeing if the
			// given type is identical or its prototype is one of the given type's prototype chain

			int depth = 0;
			PrototypeObject pt = type._payload._object;
			while(pt !is null) {
				depth++;
				pt = pt.prototype;
			}

			if(type._payload._object is arg._payload._object)
				thisScore += 2 + depth;
			else {
				if(arg._payload._object is null)
					return 0; // null sucks.

				auto test = type._payload._object.prototype;
				// generic object compared against generic object matches
				if(test is null && type._payload._object.prototype is null)
					thisScore += 1;
				else {
					pt = arg._payload._object;
					while(pt !is null) {
						if(pt is test) {
							thisScore += 1 + depth;
							break;
						}
						pt = pt.prototype;
					}
				}
			}
		} else {
			thisScore += 3; // exact match without implicit conversion
		}
	}

	return thisScore;
}

/++
	Does dynamic dispatch to overloads in a jsvar function set.

	History:
		Added September 1, 2020.
+/
class OverloadSet : PrototypeObject {
	this() {
		_properties["opCall"] = &opCall;
		_properties["apply"] = &apply;
	}

	///
	void addIndividualOverload(alias f)() {
		var func = &f;
		var[] argTypes;
		static if(is(typeof(f) Params == __parameters))
		foreach(param; Params)
			argTypes ~= var(param.init);
		//import std.stdio; writeln("registered ", argTypes);
		overloads ~= Overload(argTypes, func);
	}

	///
	static if(__traits(compiles, mixin(q{  () { static foreach(i; [1,2]) {} } }) ))
	mixin(q{
	void addOverloadsOf(alias what)() {
		foreach(alias f; __traits(getOverloads, __traits(parent, what), __traits(identifier, what)))
			addIndividualOverload!f;
	}
	});

	static struct Overload {
		// I don't even store the arity of a function object
		// so argTypes is the nest best thing.
		var[] argTypes;
		var func;
	}
	Overload[] overloads;

	private bool exactMatch(var[] a, var[] b) {
		if(a.length != b.length)
			return false;
		foreach(i; 0 .. a.length) {
			if(a[i] !is b[i])
				return false;
		}
		return true;
	}

	void addOverload(Overload o) {
		foreach(ref ov; overloads)
			if(exactMatch(ov.argTypes, o.argTypes)) {
				ov.func = o.func;
				return;
			}
		overloads ~= o;
	}

	/*
		I might have to add Object, Exception, and others to jsvar to represent types.
		maybe even int and such.

		An object should probably have a secret property that gives its name...
	*/
	var apply(var this_, var[] arguments) {
		return opCall(arguments[0], arguments[1].get!(var[]));
	}

	var opCall(var this_, var[] arguments) {
		// remember script.d supports default args too.
		int bestScore = int.min;
		Overload bestMatch;

		if(overloads.length == 0) {
			return var(null);
		}

		foreach(overload; overloads) {
			if(overload.argTypes.length == 0) {
				if(arguments.length == 0) {
					bestScore = 0;
					bestMatch = overload;
					break;
				}
				if(bestScore < 0) {
					bestScore = 0;
					bestMatch = overload;
					continue;
				}
			}

			int thisScore = 0;
			foreach(idx, arg; arguments) {
				if(idx >= overload.argTypes.length) {
					thisScore = 0;
					break;
				}

				// now if it is an object, if we can match, add score based on how derived the specified type is.
				// if it is the very same object, that's the best match possible. (prototype chain length + 1. and the one base point for matching at all.)
				// then if not, if the arg given can implicitly convert to the arg specified, give points based on how many prototypes the arg specified has. (plus one base point for matching at all)

				// otherwise just give one point.

				auto s = typeCompatibilityScore(arg, overload.argTypes[idx]);
				if(s == 0) {
					thisScore = 0;
					break;
				}
				thisScore += s;
			}

			if(thisScore > 0 && thisScore > bestScore) {
				bestScore = thisScore;
				bestMatch = overload;
			}
		}

		if(bestScore < 0)
			throw new Exception("no matching overload found " ~ to!string(arguments) ~ " " ~ to!string(overloads));


		return bestMatch.func.apply(this_, arguments);
	}
}

unittest {
	struct A {
		static:
		string foo(var arg) { return "generic"; }
		string foo(string s) { return "string"; }
		string foo(int i) { return "int"; }
		string foo(float i) { return "float"; }
	}

        auto os = new OverloadSet();
        os.addOverloadsOf!(A.foo);
        var g = var.emptyObject;
        g.foo = os;

        //g.foo()();
        assert(g.foo()("for me") == "string");
        //g.foo()("for me", "lol");
        assert(g.foo()(1) == "int");
        assert(g.foo()(5.4) == "float");
        assert(g.foo()(new Object) == "generic");
}

bool appearsNumeric(string n) {
	if(n.length == 0)
		return false;
	foreach(c; n) {
		if(c < '0' || c > '9')
			return false;
	}
	return true;
}


/// Wraps a struct by reference. The pointer is stored - be sure the struct doesn't get freed or go out of scope!
///
/// BTW: structs by value can be put in vars with var.opAssign and var.get. It will generate an object with the same fields. The difference is changes to the jsvar won't be reflected in the original struct and native methods won't work if you do it that way.
WrappedNativeObject wrapNativeObject(Struct)(Struct* obj) if(is(Struct == struct)) {
	return null; // FIXME
}

/+
	_IDX_

	static_foreach(T.length, q{
		mixin(q{
		void
		} ~ __traits(identifier, T[_IDX_]) ~ q{

		}
	});
+/

private
string static_foreach(size_t length, int t_start_idx, int t_end_idx, string[] t...) pure {
	assert(__ctfe);
	int slen;
	int tlen;
	foreach(idx, i; t[0 .. t_start_idx])
		slen += i.length;
	foreach(idx, i; t[t_start_idx .. $ + t_end_idx]) {
		if(idx)
			tlen += 5;
		tlen += i.length;
	}
	foreach(idx, i; t[$ + t_end_idx .. $])
		slen += i.length;

	char[] a = new char[](tlen * length + slen);

	int loc;
	char[5] stringCounter;
	stringCounter[] = "00000"[];

	foreach(part; t[0 .. t_start_idx]) {
		a[loc .. loc + part.length] = part[];
		loc += part.length;
	}

	foreach(i; 0 .. length) {
		foreach(idx, part; t[t_start_idx .. $ + t_end_idx]) {
			if(idx) {
				a[loc .. loc + stringCounter.length] = stringCounter[];
				loc += stringCounter.length;
			}
			a[loc .. loc + part.length] = part[];
			loc += part.length;
		}

		auto pos = stringCounter.length;
		while(pos) {
			pos--;
			if(stringCounter[pos] == '9') {
				stringCounter[pos] = '0';
			} else {
				stringCounter[pos] ++;
				break;
			}
		}
		while(pos)
			stringCounter[--pos] = ' ';
	}

	foreach(part; t[$ + t_end_idx .. $]) {
		a[loc .. loc + part.length] = part[];
		loc += part.length;
	}

	return a;
}

private string deprecationCheck() {
	//https://issues.dlang.org/show_bug.cgi?id=22011 this hack sucks
	static if(__VERSION__ >= 2077)
		return "__traits(isDeprecated, __traits(getMember, obj, memberName == `this` ? `__ctor` : memberName))";
	else
		return "false";
}

// LOL this can't work because function pointers drop the default :(
private
auto ParamDefault(alias T, size_t idx)() {
	static if(is(typeof(T) Params == __parameters)) {
		auto fn(Params[idx .. idx + 1] _args__) { // if i used plain `args` and one of the args in the list was also called `args`, this fails to compile and that error will be dropped by opDispatch.  LOL.
			return _args__[0];
		}
		static if(__traits(compiles, fn())) {
			return fn();
		} else {
			return Params[idx].init;
		}
	} else static assert(0);
}
