package hxd.net;
import haxe.macro.Expr;
import haxe.macro.Type;
import haxe.macro.Context;
using haxe.macro.TypeTools;
using haxe.macro.ComplexTypeTools;

enum RpcMode {
	/*
		When called on the client: will forward the call on the server, but not execute locally.
		When called on the server: will forward the call to the clients (and force its execution), then execute.
		This is the default behavior.
	*/
	All;
	/*
		When called on the server: will forward the call to the clients, but not execute locally.
		When called on the client: will execute locally.
	*/
	Client;
	/*
		When called on the client: will forward the call the server, but not execute locally.
		When called on the server: will execute locally.
	*/
	Server;
	/*
		When called on the client: will forward the call to the server if not the owner, or else execute locally.
		When called on the server: will forward the call to the owner.
		Will fail if there is no owner.
	*/
	Owner;
}

enum PropTypeDesc<PropType> {
	PInt;
	PFloat;
	PBool;
	PString;
	PBytes;
	PSerializable( name : String );
	PEnum( name : String );
	PMap( k : PropType, v : PropType );
	PArray( k : PropType );
	PObj( fields : Array<{ name : String, type : PropType, opt : Bool }> );
	PAlias( k : PropType );
	PVector( k : PropType );
	PNull( t : PropType );
	PUnknown;
}

typedef PropType = {
	var d : PropTypeDesc<PropType>;
	var t : ComplexType;
	@:optional var isProxy : Bool;
	@:optional var increment : Float;
	@:optional var condSend : Expr;
	@:optional var notMutable : Bool;
}

class Macros {

	static var IN_ENUM_SER = false;

	public static macro function serializeValue( ctx : Expr, v : Expr ) : Expr {
		var t = Context.typeof(v);
		var pt = getPropType(t);
		if( pt == null ) {
			Context.error("Unsupported serializable type " + t.toString(), v.pos);
			return macro { };
		}
		IN_ENUM_SER = StringTools.startsWith(Context.getLocalClass().toString(), "hxd.net.enumSer.");
		return serializeExpr(ctx, v, pt);
	}

	public static macro function unserializeValue( ctx : Expr, v : Expr ) : Expr {
		var t = Context.typeof(v);
		var pt = getPropType(t);
		if( pt == null ) {
			return macro { };
		}
		IN_ENUM_SER = StringTools.startsWith(Context.getLocalClass().toString(), "hxd.net.enumSer.");
		return unserializeExpr(ctx, v, pt);
	}

	public static macro function getFieldType( v : Expr ) {
		var t = Context.typeof(v);
		var pt = getPropType(t);
		if( pt == null )
			return macro null;
		var v = toFieldType(pt);
		return macro $v{v};
	}

	#if macro

	static function toFieldType( t : PropType ) : Schema.FieldType {
		return switch( t.d ) {
		case PInt: PInt;
		case PFloat: PFloat;
		case PBool: PBool;
		case PString: PString;
		case PBytes: PBytes;
		case PSerializable(name): PSerializable(name);
		case PEnum(name): PEnum(name);
		case PMap(k, v): PMap(toFieldType(k), toFieldType(v));
		case PArray(v): PArray(toFieldType(v));
		case PObj(fields): PObj([for( f in fields ) { name : f.name, type : toFieldType(f.type), opt : f.opt }]);
		case PAlias(t): return toFieldType(t);
		case PVector(k): PVector(toFieldType(k));
		case PNull(t): PNull(toFieldType(t));
		case PUnknown: PUnknown;
		};
	}

	static function isSerializable( c : Ref<ClassType> ) {
		while( true ) {
			var cg = c.get();
			for( i in cg.interfaces )
				switch( i.t.toString() ) {
				case "hxd.net.Serializable", "hxd.net.NetworkSerializable":
					return true;
				default:
				}
			var sup = cg.superClass;
			if( sup == null )
				break;
			c = sup.t;
		}
		return false;
	}

	static function getPropField( ft : Type, meta : Metadata ) {
		var t = getPropType(ft);
		if( t == null )
			return null;
		for( m in meta) {
			switch( m.name ) {
			case ":s", ":optional":
				//
			case ":increment":
				var inc : Null<Float> = null;
				if( m.params.length == 1 )
					switch( m.params[0].expr ) {
					case EConst(CInt(i)): inc = Std.parseInt(i);
					case EConst(CFloat(f)): inc = Std.parseFloat(f);
					default:
					}
				if( inc == null )
					Context.error("Increment requires value parameter", m.pos);
				switch( t.d ) {
				case PFloat, PNull({ d : PFloat }):
					t.increment = inc;
				default:
					Context.error("Increment not allowed on " + t.t.toString(), m.pos);
				}
			case ":condSend" if( m.params.length == 1 ):
				t.condSend = m.params[0];
			case ":notMutable":
				t.notMutable = true;
			default:
				Context.error("Unsupported network metadata", m.pos);
			}
		}
		return t;
	}

	static function getPropType( t : haxe.macro.Type ) : PropType {
		var isProxy = false;
		var desc = switch( t ) {
		case TAbstract(a, pl):
			switch( a.toString() ) {
			case "Float":
				PFloat;
			case "Int","UInt":
				PInt;
			case "Bool":
				PBool;
			case "Map":
				var tk = getPropType(pl[0]);
				var tv = getPropType(pl[1]);
				if( tk == null || tv == null )
					return null;
				PMap(tk, tv);
			case "haxe.ds.Vector":
				var tk = getPropType(pl[0]);
				if( tk == null )
					return null;
				PVector(tk);
			case "hxd.net.VectorProxy":
				var t = getPropType(pl[0]);
				if( t == null )
					return null;
				isProxy = true;
				PVector(t);
			case "hxd.net.ArrayProxy", "hxd.net.ArrayProxy2":
				var t = getPropType(pl[0]);
				if( t == null )
					return null;
				isProxy = true;
				PArray(t);
			case "hxd.net.MapProxy", "hxd.net.MapProxy2":
				var k = getPropType(pl[0]);
				var v = getPropType(pl[1]);
				if( k == null || v == null ) return null;
				isProxy = true;
				PMap(k,v);
			case name:
				var t2 = Context.followWithAbstracts(t, true);
				switch( t2 ) {
				case TAbstract(a2, _) if( a2.toString() == name ):
					return null;
				default:
				}
				var pt = getPropType(t2);
				if( pt == null ) return null;
				PAlias(pt);
			}
		case TEnum(e,_):
			PEnum(e.toString());
		case TAnonymous(a):
			var a = a.get();
			var fields = [];
			for( f in a.fields ) {
				if( f.meta.has(":noSerialize") )
					continue;
				var ft = getPropField(f.type, f.meta.get());
				if( ft == null ) return null;
				fields.push( { name : f.name, type : ft, opt : f.meta.has(":optional") } );
			}
			PObj(fields);
		case TInst(c, pl):
			switch( c.toString() ) {
			case "String":
				PString;
			case "Array":
				var at = getPropType(pl[0]);
				if( at == null ) return null;
				PArray(at);
			case "haxe.io.Bytes":
				PBytes;
			case name if( StringTools.startsWith(name, "hxd.net.ObjProxy_") ):
				var fields = c.get().fields.get();
				for( f in fields )
					if( f.name == "__value" ) {
						var t = getPropType(f.type);
						t.isProxy = true;
						return t;
					}
				throw "assert";
			default:
				if( isSerializable(c) )
					PSerializable(c.toString());
				else
					return null;
			}
		case TType(td, pl):
			switch( td.toString() ) {
			case "Null":
				var p = getPropType(pl[0]);
				if( p != null && !isNullable(p) )
					p = { d : PNull(p), t : TPath( { pack : [], name : "Null", params : [TPType(p.t)] } ) };
				return p;
			default:
				var p = getPropType(Context.follow(t, true));
				if( p != null )
					p.t = t.toComplexType(); // more general, still identical
				return p;
			}
		default:
			return null;
		}
		var p : PropType = {
			d : desc,
			t : t.toComplexType(),
		};
		if( isProxy ) p.isProxy = isProxy;
		return p;
	}

	static function isNullable( t : PropType ) {
		switch( t.d ) {
		case PInt, PFloat, PBool:
			return false;
		default:
			return true;
		}
	}

	static function toType( t : ComplexType ) : Type {
		return Context.typeof(macro (null:$t));
	}

	static function serializeExpr( ctx : Expr, v : Expr, t : PropType, skipCheck = false ) {

		if( t.isProxy && !skipCheck )
			return serializeExpr(ctx, { expr : EField(v, "__value"), pos : v.pos }, t, true);

		switch( t.d ) {
		case PFloat:
			return macro $ctx.addFloat($v);
		case PInt:
			return macro $ctx.addInt($v);
		case PBool:
			return macro $ctx.addBool($v);
		case PBytes:
			return macro $ctx.addBytes($v);
		case PMap(kt, vt):
			var kt = kt.t;
			var vt = vt.t;
			var vk = { expr : EConst(CIdent("k")), pos : v.pos };
			var vv = { expr : EConst(CIdent("v")), pos : v.pos };
			return macro $ctx.addMap($v, function(k:$kt) return hxd.net.Macros.serializeValue($ctx, $vk), function(v:$vt) return hxd.net.Macros.serializeValue($ctx, $vv));
		case PEnum(_):
			var et = t.t;
			var ser = "serialize";
			if( IN_ENUM_SER )
				ser += "2";
			return macro (null : hxd.net.Serializable.SerializableEnum<$et>).$ser($ctx,$v);
		case PObj(fields):
			var nullables = [for( f in fields ) if( isNullable(f.type) ) f];
			var ct = t.t;
			if( nullables.length >= 32 )
				Context.error("Too many nullable fields", v.pos);
			return macro {
				var v : $ct = $v;
				if( v == null )
					$ctx.addByte(0);
				else {
					var fbits = 0;
					$b{[
						for( i in 0...nullables.length ) {
							var name = nullables[i].name;
							macro if( v.$name != null ) fbits |= $v{ 1 << i };
						}
					]};
					$ctx.addInt(fbits + 1);
					$b{[
						for( f in fields ) {
							var nidx = nullables.indexOf(f);
							var name = f.name;
							if( nidx < 0 )
								macro hxd.net.Macros.serializeValue($ctx, v.$name);
							else
								macro if( fbits & $v{1<<nidx} != 0 ) hxd.net.Macros.serializeValue($ctx, v.$name);
						}
					]};
				}
			};
		case PString:
			return macro $ctx.addString($v);
		case PArray(t):
			var at = toProxy(t);
			var ve = { expr : EConst(CIdent("e")), pos : v.pos };
			return macro $ctx.addArray($v, function(e:$at) return hxd.net.Macros.serializeValue($ctx, $ve));
		case PVector(t):
			var at = toProxy(t);
			var ve = { expr : EConst(CIdent("e")), pos : v.pos };
			return macro $ctx.addVector($v, function(e:$at) return hxd.net.Macros.serializeValue($ctx, $ve));
		case PSerializable(_):
			return macro $ctx.addKnownRef($v);
		case PAlias(t):
			return serializeExpr(ctx, { expr : ECast(v, null), pos : v.pos }, t);
		case PNull(t):
			var e = serializeExpr(ctx, v, t);
			return macro if( $v == null ) $ctx.addByte(0) else { $ctx.addByte(1); $e; };
		case PUnknown:
			throw "assert";
		}
	}

	static function unserializeExpr( ctx : Expr, v : Expr, t : PropType ) {
		switch( t.d ) {
		case PFloat:
			return macro $v = $ctx.getFloat();
		case PInt:
			return macro $v = $ctx.getInt();
		case PBool:
			return macro $v = $ctx.getBool();
		case PBytes:
			return macro $v = $ctx.getBytes();
		case PMap(k,t):
			var kt = k.t;
			var vt = t.t;
			var vk = { expr : EConst(CIdent("k")), pos : v.pos };
			var vv = { expr : EConst(CIdent("v")), pos : v.pos };
			return macro {
				var k : $kt;
				var v : $vt;
				$v = $ctx.getMap(function() { hxd.net.Macros.unserializeValue($ctx, $vk); return $vk; }, function() { hxd.net.Macros.unserializeValue($ctx, $vv); return $vv; });
			};
		case PEnum(_):
			var et = t.t;
			var unser = "unserialize";
			if( IN_ENUM_SER )
				unser += "2";
			return macro { var __e : $et; __e = (null : hxd.net.Serializable.SerializableEnum<$et>).$unser($ctx); $v = __e; }
		case PObj(fields):
			var nullables = [for( f in fields ) if( isNullable(f.type) ) f];
			if( nullables.length >= 32 )
				Context.error("Too many nullable fields", v.pos);
			var ct = t.t;
			return macro {
				var fbits = $ctx.getInt();
				if( fbits == 0 )
					$v = null;
				else {
					fbits--;
					$b{{
						var exprs = [];
						var vars = [];
						for( f in fields ) {
							var nidx = nullables.indexOf(f);
							var name = f.name;
							var ct = f.type.t;
							vars.push( { field : name, expr : { expr : EConst(CIdent(name)), pos:v.pos } } );
							if( nidx < 0 ) {
								exprs.unshift(macro var $name : $ct);
								exprs.push(macro hxd.net.Macros.unserializeValue($ctx, $i { name } ));
							} else {
								exprs.unshift(macro var $name : $ct = null);
								exprs.push(macro if( fbits & $v { 1 << nidx } != 0 ) hxd.net.Macros.unserializeValue($ctx, $i { name } ));
							}
						}
						exprs.push( { expr : EBinop(OpAssign,v, { expr : EObjectDecl(vars), pos:v.pos } ), pos:v.pos } );
						exprs;
					}};
				}
			};
		case PString:
			return macro $v = $ctx.getString();
		case PArray(at):
			var at = toProxy(at);
			var ve = { expr : EConst(CIdent("e")), pos : v.pos };
			return macro {
				var e : $at;
				$v = $ctx.getArray(function() { hxd.net.Macros.unserializeValue($ctx, e); return e; });
			};
		case PVector(at):
			var at = toProxy(at);
			var ve = { expr : EConst(CIdent("e")), pos : v.pos };
			return macro {
				var e : $at;
				$v = $ctx.getVector(function() { hxd.net.Macros.unserializeValue($ctx, e); return e; });
			};
		case PSerializable(_):
			function loop(t:ComplexType) {
				switch( t ) {
				case TPath( { name : "Null", params:[TPType(t)] } ):
					return loop(t);
				case TPath( p = { params:a } ) if( a.length > 0 ):
					return TPath( { pack : p.pack, name:p.name, sub:p.sub } );
				default:
					return t;
				}
			}
			var cexpr = Context.parse(loop(t.t).toString(), v.pos);
			return macro $v = $ctx.getRef($cexpr,@:privateAccess $cexpr.__clid);
		case PAlias(at):
			var cvt = at.t;
			return macro {
				var v : $cvt;
				${unserializeExpr(ctx,macro v,at)};
				$v = cast v;
			};
		case PNull(t):
			var e = unserializeExpr(ctx, v, t);
			return macro if( $ctx.getByte() == 0 ) $v = null else $e;
		case PUnknown:
			throw "assert";
		}
	}

	static function withPos( e : Expr, p : Position ) {
		e.pos = p;
		haxe.macro.ExprTools.iter(e, function(e) withPos(e, p));
		return e;
	}

	public static function buildSerializable() {
		var cl = Context.getLocalClass().get();
		if( cl.isInterface )
			return null;
		var fields = Context.getBuildFields();
		var toSerialize = [];

		if( !Context.defined("display") )
		for( f in fields ) {
			if( f.meta == null ) continue;
			for( meta in f.meta )
				if( meta.name == ":s" ) {
					toSerialize.push({ f : f, m : meta });
					break;
				}
		}

		var fieldsInits = [];
		for( f in fields ) {
			if( f.access.indexOf(AStatic) >= 0 ) continue;
			switch( f.kind ) {
			case FVar(_, e), FProp(_, _, _, e) if( e != null ):
				// before unserializing
				fieldsInits.push({ expr : EBinop(OpAssign,{ expr : EConst(CIdent(f.name)), pos : e.pos },e), pos : e.pos });
			default:
			}
		}

		var sup = cl.superClass;
		var isSubSer = sup != null && isSerializable(sup.t);

		var pos = Context.currentPos();
		var el = [], ul = [];
		for( f in toSerialize ) {
			var fname = f.f.name;
			el.push(withPos(macro hxd.net.Macros.serializeValue(__ctx, this.$fname),f.f.pos));
			ul.push(withPos(macro hxd.net.Macros.unserializeValue(__ctx, this.$fname),f.f.pos));
		}

		var noCompletion = [{ name : ":noCompletion", pos : pos }];
		var access = [APublic];
		if( isSubSer )
			access.push(AOverride);
		else
			fields.push({
				name : "__uid",
				pos : pos,
				access : [APublic],
				meta : noCompletion,
				kind : FVar(macro : Int, macro @:privateAccess hxd.net.Serializer.allocUID()),
			});

		var clName = StringTools.endsWith(cl.module,"."+cl.name) ? cl.module.split(".") : [cl.name];
		fields.push({
			name : "__clid",
			pos : pos,
			access : [AStatic],
			meta : noCompletion,
			kind : FVar(macro : Int, macro @:privateAccess hxd.net.Serializer.registerClass($p{clName})),
		});
		fields.push({
			name : "getCLID",
			pos : pos,
			access : access,
			meta : noCompletion,
			kind : FFun({ args : [], ret : macro : Int, expr : macro return __clid }),
		});

		var needSerialize = toSerialize.length != 0 || !isSubSer;
		var needUnserialize = needSerialize || fieldsInits.length != 0;

		if( needSerialize ) {
			fields.push({
				name : "serialize",
				pos : pos,
				access : access,
				kind : FFun({
					args : [ { name : "__ctx", type : macro : hxd.net.Serializer } ],
					ret : null,
					expr : macro @:privateAccess { ${ if( isSubSer ) macro super.serialize(__ctx) else macro { } }; $b{el} }
				}),
			});
			var schema = [for( s in toSerialize ) {
				var name = s.f.name;
				macro { schema.fieldsNames.push($v{name}); schema.fieldsTypes.push(hxd.net.Macros.getFieldType(this.$name)); }
			}];
			fields.push({
				name : "getSerializeSchema",
				pos : pos,
				access : access,
				meta : noCompletion,
				kind : FFun({
					args : [],
					ret : null,
					expr : macro { var schema = ${if( isSubSer ) macro super.getSerializeSchema() else macro new hxd.net.Schema()}; $b{schema}; return schema; }
				})
			});
		}

		if( fieldsInits.length > 0 || !isSubSer )
			fields.push({
				name : "unserializeInit",
				pos : pos,
				meta : noCompletion,
				access : access,
				kind : FFun({
					args : [],
					ret : null,
					expr : isSubSer ? macro { super.unserializeInit(); $b{fieldsInits}; } : { expr : EBlock(fieldsInits), pos : pos },
				})
			});

		if( needUnserialize ) {
			var unserExpr = macro @:privateAccess { ${ if( isSubSer ) macro super.unserialize(__ctx) else macro { } }; $b{ul} };

			for( f in fields )
				if( f.name == "unserialize" ) {
					var found = false;
					function repl(e:Expr) {
						switch( e.expr ) {
						case ECall( { expr : EField( { expr : EConst(CIdent("super")) }, "unserialize") }, [ctx]):
							found = true;
							return macro { var __ctx : hxd.net.Serializer = $ctx; $unserExpr; }
						default:
							return haxe.macro.ExprTools.map(e, repl);
						}
					}
					switch( f.kind ) {
					case FFun(f):
						f.expr = repl(f.expr);
					default:
					}
					if( !found ) Context.error("Override of unserialize() with no super.unserialize(ctx) found", f.pos);
					return fields;
				}

			fields.push({
				name : "unserialize",
				pos : pos,
				access : access,
				kind : FFun({
					args : [ { name : "__ctx", type : macro : hxd.net.Serializer } ],
					ret : null,
					expr : unserExpr,
				}),
			});
		}

		return fields;
	}

	public static function buildSerializableEnum() {
		var pt = switch( Context.getLocalType() ) {
		case TInst(_, [pt]): pt;
		default: null;
		}
		if( pt != null )
			pt = Context.follow(pt);
		if( pt != null )
		switch( pt ) {
		case TEnum(e, tparams):
			var e = e.get();
			var name = e.pack.length == 0 ? e.name : e.pack.join("_") + "_" + e.name;
			try {
				return Context.getType("hxd.net.enumSer." + name);
			} catch( _ : Dynamic ) {
				var pos = Context.currentPos();
				var cases = [], ucases = [];
				if( e.names.length >= 256 )
					Context.error("Too many constructors", pos);
				for( f in e.names ) {
					var c = e.constructs.get(f);
					switch( Context.follow(c.type) ) {
					case TFun(args, _):
						var eargs = [for( a in args ) macro hxd.net.Macros.serializeValue(ctx,$i{a.name})];
						cases.push({
							values : [{ expr : ECall({ expr : EConst(CIdent(c.name)), pos : pos },[for( a in args ) { expr : EConst(CIdent(a.name)), pos : pos }]), pos : pos }],
							expr : macro {
								ctx.addByte($v{c.index+1});
								$b{eargs};
							}
						});

						var evals = [];
						for( a in args ) {
							var aname = "_" + a.name;
							var at = haxe.macro.TypeTools.applyTypeParameters(a.t,e.params,tparams).toComplexType();
							evals.push(macro var $aname : $at);
							evals.push(macro hxd.net.Macros.unserializeValue(ctx,$i{aname}));
						}
						evals.push({ expr : ECall({ expr : EConst(CIdent(c.name)), pos : pos },[for( a in args ) { expr : EConst(CIdent("_"+a.name)), pos : pos }]), pos : pos });
						ucases.push({
							values : [macro $v{c.index+1}],
							expr : { expr : EBlock(evals), pos : pos },
						});

					default:
						cases.push({
							values : [ { expr : EConst(CIdent(c.name)), pos : pos } ],
							expr : macro ctx.addByte($v{c.index+1}),
						});
						ucases.push({
							values : [macro $v{c.index+1}],
							expr : { expr : EConst(CIdent(c.name)), pos : pos },
						});
					}
				}
				var t : TypeDefinition = {
					name : name,
					pack : ["hxd","net","enumSer"],
					kind : TDClass(),
					fields : [
					{
						name : "doSerialize",
						access : [AStatic],
						pos : pos,
						kind : FFun( {
							args : [{ name : "ctx", type : macro : hxd.net.Serializer },{ name : "v", type : pt.toComplexType() }],
							expr : macro @:privateAccess if( v == null ) ctx.addByte(0) else ${{ expr : ESwitch(macro v,cases,null), pos : pos }},
							ret : macro : Void,
						}),
					},{
						name : "doUnserialize",
						access : [AStatic],
						pos : pos,
						kind : FFun( {
							args : [{ name : "ctx", type : macro : hxd.net.Serializer }],
							expr : macro @:privateAccess {
								var b = ctx.getByte();
								if( b == 0 )
									return null;
								return ${{ expr : ESwitch(macro b,ucases,macro throw "Invalid enum index "+b), pos : pos }}
							},
							ret : pt.toComplexType(),
						}),

					},{
						name : "serialize",
						access : [AInline, APublic],
						meta : [{name:":extern",pos:pos}],
						pos : pos,
						kind : FFun( {
							args : [{ name : "ctx", type : macro : hxd.net.Serializer },{ name : "v", type : pt.toComplexType() }],
							expr : macro doSerialize(ctx,v),
							ret : null,
						}),
					},{
						name : "unserialize",
						access : [AInline, APublic],
						meta : [{name:":extern",pos:pos}],
						pos : pos,
						kind : FFun( {
							args : [{ name : "ctx", type : macro : hxd.net.Serializer }],
							expr : macro return doUnserialize(ctx),
							ret : null,
						}),
					}],
					pos : pos,
				};

				// hack to allow recursion (duplicate serialize/unserialize for recursive usage)
				var tf = Reflect.copy(t.fields[t.fields.length - 2]);
				tf.name += "2";
				t.fields.push(tf);
				var tf = Reflect.copy(t.fields[t.fields.length - 2]);
				tf.name += "2";
				t.fields.push(tf);

				Context.defineType(t);
				return Context.getType("hxd.net.enumSer." + name);
			}
		default:
		}
		Context.error("Enum expected", Context.currentPos());
		return null;
	}

	static function quickInferType( e : Expr ) {
		if( e == null )
			return null;
		switch( e.expr ) {
		case EConst(CInt(_)):
			return macro : Int;
		case EConst(CFloat(_)):
			return macro : Float;
		case EConst(CString(_)):
			return macro : String;
		case EConst(CIdent("true" | "false")):
			return macro : Bool;
		default:
		}
		return null;
	}

	static function needProxy( t : PropType ) {
		if( t == null || t.isProxy )
			return false;
		switch( t.d ) {
		case PMap(_), PArray(_), PObj(_), PVector(_):
			return !t.notMutable;
		default:
			return false;
		}
	}

	static function checkProxy( p : PropType ) {
		if( needProxy(p) ) {
			p.isProxy = true;
			p.t = toProxy(p);
		}
	}

	static function toProxy( p : PropType ) {
		if( !p.isProxy )
			return p.t;
		var pt = p.t;
		return macro : hxd.net.NetworkSerializable.Proxy<$pt>;
	}


	static var hasRetVal : Bool;
	static function hasReturnVal( e : Expr ) {
		hasRetVal = false;
		checkRetVal(e);
		return hasRetVal;
	}

	static function checkRetVal( e : Expr ) {
		if( hasRetVal )
			return;
		switch( e.expr ) {
		case EReturn(e):
			if( e != null )
				hasRetVal = true;
		case EFunction(_):
			return;
		default:
			haxe.macro.ExprTools.iter(e, checkRetVal);
		}
	}

	static function superImpl( name : String, e : Expr ) {
		switch( e.expr ) {
		case EField( esup = { expr : EConst(CIdent("super")) }, fname) if( fname == name ):
			e.expr = EField(esup, name+"__impl");
		default:
		}
		return haxe.macro.ExprTools.map(e, superImpl.bind(name));
	}

	static function replaceSetter( fname : String, setExpr : Expr -> Expr, e : Expr ) {
		switch( e.expr ) {
		case EBinop(OpAssign, e1 = { expr : (EConst(CIdent(name)) | EField( { expr : EConst(CIdent("this")) }, name)) }, e2) if( name == fname ):
			e.expr = EBinop(OpAssign,e1,setExpr(e2));
		case EBinop(OpAssignOp(_), { expr : (EConst(CIdent(name)) | EField( { expr : EConst(CIdent("this")) }, name)) }, _) if( name == fname ):
			throw "TODO";
		default:
			haxe.macro.ExprTools.iter(e, function(e) replaceSetter(fname,setExpr,e));
		}
	}

	public static function buildNetworkSerializable() {
		var cl = Context.getLocalClass().get();
		if( cl.isInterface )
			return null;
		var fields = Context.getBuildFields();
		var toSerialize = [];
		var rpc = [];
		var superRPC = new Map();
		var superFields = new Map();
		var startFID = 0, rpcID = 0;
		{
			var sup = cl.superClass;
			while( sup != null ) {
				var c = sup.t.get();
				for( m in c.meta.get() )
					switch( m.name)  {
					case ":rpcCalls":
						for( a in m.params )
							switch( a.expr ) {
							case EConst(CIdent(id)):
								rpcID++;
								superRPC.set(id, true);
							default:
								throw "assert";
							}
					case ":sFields":
						for( a in m.params )
							switch( a.expr ) {
							case EConst(CIdent(id)):
								superFields.set(id, true);
								startFID++;
							default:
								throw "assert";
							}
					}
				sup = c.superClass;
			}
		}

		if( !Context.defined("display") )
		for( f in fields ) {

			if( superRPC.exists(f.name) ) {
				switch( f.kind ) {
				case FFun(ff):
					ff.expr = superImpl(f.name, ff.expr);
				default:
				}
				f.name += "__impl";
				continue;
			}

			if( f.access.indexOf(AOverride) >= 0 && StringTools.startsWith(f.name, "set_") && superFields.exists(f.name.substr(4)) ) {
				// overridden setter of network property
				var fname = f.name.substr(4);
				switch( f.kind ) {
				case FFun(ff):
					replaceSetter(fname, function(e) return macro $i{"__net_mark_"+fname}($e), ff.expr);
				default:
				}
				continue;
			}

			if( f.meta == null ) continue;

			for( meta in f.meta ) {
				if( meta.name == ":s" ) {
					toSerialize.push({ f : f, m : meta });
					break;
				}
				if( meta.name == ":rpc" ) {
					var mode : RpcMode = All;
					if( meta.params.length != 0 )
						switch( meta.params[0].expr ) {
						case EConst(CIdent("all")):
						case EConst(CIdent("client")): mode = Client;
						case EConst(CIdent("server")): mode = Server;
						case EConst(CIdent("owner")): mode = Owner;
						default:
							Context.error("Unexpected Rpc mode : should be all|client|server|owner", meta.params[0].pos);
						}
					rpc.push( { f : f, mode:mode } );
					superRPC.set(f.name, true);
					break;
				}
			}
		}

		var sup = cl.superClass;
		var isSubSer = sup != null && isSerializable(sup.t);
		var pos = Context.currentPos();
		if( !isSubSer ) {
			fields = fields.concat((macro class {
				@:noCompletion public var __host : hxd.net.NetworkHost;
				@:noCompletion public var __bits : Int = 0;
				@:noCompletion public var __next : hxd.net.NetworkSerializable;
				@:noCompletion public inline function networkSetBit( b : Int ) {
					if( __host != null && (__next != null || @:privateAccess __host.mark(this)) )
						__bits |= 1 << b;
				}
				public var enableReplication(get, set) : Bool;
				inline function get_enableReplication() return __host != null;
				function set_enableReplication(b) {
					@:privateAccess hxd.net.NetworkHost.enableReplication(this, b);
					return b;
				}
				public inline function networkCancelProperty( props : hxd.net.NetworkSerializable.NetworkProperty ) {
					__bits &= ~props.toInt();
				}
				public inline function networkLocalChange( f : Void -> Void ) {
					var old = __host;
					__host = null;
					f();
					__host = old;
				}
			}).fields);

			if( !Lambda.exists(fields, function(f) return f.name == "networkGetOwner") )
				fields = fields.concat((macro class {
					public function networkGetOwner() : hxd.net.NetworkSerializable { return null; }
				}).fields);
		}

		var firstFID = startFID;
		var flushExpr = [];
		var syncExpr = [];
		var noComplete : Metadata = [ { name : ":noCompletion", pos : pos } ];
		for( f in toSerialize ) {
			var pos = f.f.pos;
			var fname = f.f.name;
			var bitID = startFID++;
			var ftype : PropType;
			switch( f.f.kind ) {
			case FVar(t, e):
				if( t == null ) t = quickInferType(e);
				if( t == null ) Context.error("Type required", pos);
				var tt = Context.resolveType(t, pos);
				ftype = getPropField(tt, f.f.meta);
				if( ftype == null ) ftype = { t : t, d : PUnknown };
				checkProxy(ftype);
				f.f.kind = FProp("default", "set", ftype.t, e);
			case FProp(get, set, t, e):
				if( t == null ) t = quickInferType(e);
				if( t == null ) Context.error("Type required", pos);
				var tt = Context.resolveType(t, pos);
				ftype = getPropField(tt, f.f.meta);
				if( ftype == null ) ftype = { t : t, d : PUnknown };
				checkProxy(ftype);
				if( set == "null" )
					Context.warning("Null setter is not respected when using NetworkSerializable", pos);
				else if( set != "default" && set != "set" )
					Context.error("Invalid setter", pos);
				f.f.kind = FProp(get,"set", ftype.t, e);
			default:
				throw "assert";
			}

			var markExpr = macro networkSetBit($v{ bitID });
			markExpr = makeMarkExpr(fields, fname, ftype, markExpr);

			var markFun = "__net_mark_" + f.f.name;
			fields.push( {
				name : markFun,
				access : [AInline],
				meta : noComplete,
				pos : pos,
				kind : FFun({
					args : [{ name : "v", type : ftype.t }],
					ret : ftype.t,
					expr : macro {
						if( this.$fname != v ) {
							$markExpr;
							${if( ftype.isProxy ) macro (if( v != null ) v.bindHost(this,$v{bitID})) else macro {}};
						}
						return v;
					}
				}),
			});

			var found = false;
			for( set in fields )
				if( set.name == "set_" + f.f.name )
					switch( set.kind ) {
					case FFun(fun):
						replaceSetter(f.f.name, function(e) return macro $i{markFun}($e),fun.expr);
						found = true;
						break;
					default:
					}
			if( !found )
				fields.push({
					name : "set_" + fname,
					pos : pos,
					kind : FFun({
						args : [ { name : "v", type : ftype.t } ],
						expr : macro return this.$fname = $i{markFun}(v),
						ret : ftype.t,
					}),
				});
			var fexpr = { expr : EField({ expr : EConst(CIdent("this")), pos : pos }, fname), pos : pos };
			flushExpr.push(macro if( b & (1 << $v{ bitID } ) != 0 ) hxd.net.Macros.serializeValue(ctx, $fexpr));
			syncExpr.push(macro if( __bits & (1 << $v { bitID } ) != 0 ) hxd.net.Macros.unserializeValue(ctx, $fexpr));

			var prop = "networkProp" + fname.charAt(0).toUpperCase() + fname.substr(1);
			fields.push({
				name : prop,
				pos : pos,
				kind : FProp("get", "never", macro : hxd.net.NetworkSerializable.NetworkProperty),
				access : [APublic],
			});
			fields.push({
				name : "get_"+prop,
				pos : pos,
				kind : FFun( {
					args : [],
					expr : macro return new hxd.net.NetworkSerializable.NetworkProperty(1 << $v{bitID}),
					ret : null,
				}),
				access : [AInline],
			});
		}

		// BUILD RPC
		var firstRPCID = rpcID;
		var rpcCases = [];
		for( r in rpc ) {
			switch( r.f.kind ) {
			case FFun(f):
				var id = rpcID++;
				var hasReturnVal = hasReturnVal(f.expr);
				var name = r.f.name;
				var p = r.f.pos;
				r.f.name += "__impl";

				var cargs = [for( a in f.args ) { expr : EConst(CIdent(a.name)), pos : p } ];
				var fcall = { expr : ECall( { expr : EField( { expr : EConst(CIdent("this")), pos:p }, r.f.name), pos : p }, cargs), pos : p };

				var doCall = fcall;
				var rpcArgs = f.args;
				var resultCall = macro null;

				if( hasReturnVal ) {
					doCall = macro onResult($fcall);
					resultCall = macro function(__ctx) {
						var v = cast null;
						if( false ) v = $fcall;
						hxd.net.Macros.unserializeValue(__ctx, v);
						onResult(v);
					};
					rpcArgs = rpcArgs.copy();
					rpcArgs.push( { name : "onResult", type: f.ret == null ? null : TFunction([f.ret], macro:Void) } );
				}

				var forwardRPC = macro {
					var __ctx = @:privateAccess __host.beginRPC(this,$v{id},$resultCall);
					$b{[
						for( a in f.args )
							withPos(macro hxd.net.Macros.serializeValue(__ctx, $i{a.name}), f.expr.pos)
					] };
					@:privateAccess __host.endRPC();
				};

				var rpcExpr = switch( r.mode ) {
				case All:
					macro {
						if( __host != null ) {
							$forwardRPC;
							if( !__host.isAuth ) return;
						}
						$doCall;
					}
				case Client:
					macro {
						if( __host != null && __host.isAuth ) {
							$forwardRPC;
							return;
						}
						$doCall;
					}
				case Server:
					macro {
						if( __host != null && !__host.isAuth ) {
							$forwardRPC;
							return;
						}
						$doCall;
					}
				case Owner:
					macro {
						if( __host == null ) return;
						var owner = networkGetOwner();
						if( owner == null ) throw "Calling RPC(owner) function on a not owned object";
						if( owner != __host.self.ownerObject ) @:privateAccess {
							var old = __host.targetClient;
							if( __host.setTargetOwner(owner) ) {
								$forwardRPC;
								__host.setTargetOwner(null);
							}
							__host.targetClient = old;
							return;
						}
						$doCall;
					}
				};

				var rpc : Field = {
					name : name,
					access : r.f.access.copy(),
					kind : FFun({
						args : rpcArgs,
						ret : macro : Void,
						expr : rpcExpr,
					}),
					pos : p,
					meta : [{ name : ":final", pos : p }],
				};
				fields.push(rpc);

				r.f.access.remove(APublic);
				r.f.meta.push( { name : ":noCompletion", pos : p } );

				var exprs = [ { expr : EVars([for( a in f.args ) { name : a.name, type : a.type, expr : macro cast null } ]), pos : p } ];
				exprs.push(macro if( false ) $fcall); // force typing
				for( a in f.args )
					exprs.push(macro hxd.net.Macros.unserializeValue(__ctx, $i { a.name } ));

				if( hasReturnVal ) {
					exprs.push({ expr : EVars([ { name : "result", type : f.ret, expr : fcall } ]), pos : p } );
					exprs.push(macro {
						@:privateAccess __clientResult.beginRPCResult();
						hxd.net.Macros.serializeValue(__ctx, result);
					});
				} else
					exprs.push(fcall);

				rpcCases.push({ values : [{ expr : EConst(CInt(""+id)), pos : p }], guard : null, expr : { expr : EBlock(exprs), pos : p } });

			default:
				Context.error("Cannot use @:rpc on non function", r.f.pos);
			}
		}

		// Add network methods
		var access = [APublic];
		if( isSubSer ) access.push(AOverride);

		if( fields.length != 0 || !isSubSer ) {
			if( isSubSer ) {
				flushExpr.unshift(macro super.networkFlush(ctx));
				syncExpr.unshift(macro super.networkSync(ctx));
			} else {
				flushExpr.unshift(macro ctx.addInt(__bits));
				flushExpr.push(macro __bits = 0);
				syncExpr.unshift(macro __bits = ctx.getInt());
			}
			flushExpr.unshift(macro var b = __bits);
			fields.push({
				name : "networkFlush",
				pos : pos,
				access : access,
				meta : noComplete,
				kind : FFun({
					args : [ { name : "ctx", type : macro : hxd.net.Serializer } ],
					ret : null,
					expr : { expr : EBlock(flushExpr), pos : pos },
				}),
			});

			fields.push({
				name : "networkSync",
				pos : pos,
				access : access,
				meta : noComplete,
				kind : FFun({
					args : [ { name : "ctx", type : macro : hxd.net.Serializer } ],
					ret : null,
					expr : macro @:privateAccess $b{syncExpr},
				}),
			});
		}

		if( rpc.length != 0 || !isSubSer ) {
			var swExpr = { expr : ESwitch( { expr : EConst(CIdent("__id")), pos : pos }, rpcCases, macro throw "Unknown RPC identifier " + __id), pos : pos };
			fields.push({
				name : "networkRPC",
				pos : pos,
				access : access,
				meta : noComplete,
				kind : FFun({
					args : [ { name : "__ctx", type : macro : hxd.net.Serializer }, { name : "__id", type : macro : Int }, { name : "__clientResult", type : macro : hxd.net.NetworkHost.NetworkClient } ],
					ret : null,
					expr : if( isSubSer && firstRPCID > 0 ) macro { if( __id < $v { firstRPCID } ) { super.networkRPC(__ctx, __id, __clientResult); return; } $swExpr; } else swExpr,
				}),
			});
		}


		if( toSerialize.length != 0 || rpc.length != 0 || !isSubSer ) {
			var cases = [];
			for( i in 0...toSerialize.length )
				cases.push( { id : i + firstFID, name : toSerialize[i].f.name } );
			for( i in 0...rpc.length )
				cases.push( { id : i + startFID + firstRPCID, name : rpc[i].f.name.substr(0,-6) } );
			var ecases = [for( c in cases ) { values : [ { expr : EConst(CInt("" + c.id)), pos : pos } ], expr : { expr : EConst(CString(c.name)), pos : pos }, guard : null } ];
			var swExpr = { expr : EReturn( { expr : ESwitch(macro isRPC ? id + $v { startFID } : id, ecases, macro null), pos : pos } ), pos : pos };
			fields.push( {
				name : "networkGetName",
				pos : pos,
				access : access,
				meta : noComplete,
				kind : FFun({
					args : [ { name : "id", type : macro : Int }, { name : "isRPC", type : macro : Bool, value:macro false } ],
					ret : macro : String,
					expr : if( isSubSer ) macro { if( id < (isRPC ? $v{ firstRPCID } : $v{ firstFID }) ) return super.networkGetName(id, isRPC); $swExpr; } else swExpr,
				}),
			});
		}


		// add metadata

		if( startFID > 32 ) Context.error("Too many serializable fields", pos);
		if( rpcID > 255 ) Context.error("Too many rpc calls", pos);

		if( rpc.length > 0 )
			cl.meta.add(":rpcCalls", [for( r in rpc ) { expr : EConst(CIdent(r.f.name.substr(0, -6))), pos : pos } ], pos);
		if( toSerialize.length > 0 )
			cl.meta.add(":sFields", [for( r in toSerialize ) { expr : EConst(CIdent(r.f.name)), pos : pos }], pos);

		return fields;
	}

	static function makeMarkExpr( fields : Array<Field>, fname : String, t : PropType, mark : Expr ) {
		var rname = "__ref_" + fname;
		var needRef = false;
		if( t.increment != null ) {
			needRef = true;
			mark = macro if( Math.floor(this.$fname / $v{t.increment}) != this.$rname ) { this.$rname = Math.floor(this.$fname / $v{t.increment}); $mark; };
		}
		if( t.condSend != null ) {
			function loop(e:Expr) {
				switch( e.expr ) {
				case EConst(CIdent("current")):
					return { expr : EConst(CIdent(rname)), pos : e.pos };
				default:
					return haxe.macro.ExprTools.map(e, loop);
				}
			}
			if( t.condSend.expr.match(EConst(CIdent("false"))) )
				return macro {}; // no marking
			var condSend = loop(t.condSend);
			needRef = true;
			mark = macro if( $condSend ) { this.$rname = this.$fname; $mark; };
		}
		if( needRef && fields != null )
			fields.push({
				name : rname,
				pos : mark.pos,
				meta : [{ name : ":noCompletion", pos : mark.pos }],
				kind : FVar(t.t,macro 0),
			});
		return mark;
	}

	static function buildProxyType( p : PropType ) : ComplexType {
		if( !needProxy(p) )
			return p.t;
		switch( p.d ) {
		case PArray(k):
			checkProxy(k);
			var subName = k.isProxy ? "ArrayProxy2" : "ArrayProxy";
			return TPath( { pack : ["hxd", "net"], name : "ArrayProxy", sub : subName, params : [TPType(k.t)] } );
		case PVector(k):
			checkProxy(k);
			var subName = k.isProxy ? "VectorProxy2" : "VectorProxy";
			return TPath( { pack : ["hxd", "net"], name : "VectorProxy", sub : subName, params : [TPType(k.t)] } );
		case PMap(k, v):
			checkProxy(v);
			var subName = k.isProxy ? "MapProxy2" : "MapProxy";
			return TPath( { pack : ["hxd", "net"], name : "MapProxy", sub : subName, params : [TPType(k.t),TPType(v.t)] } );
		case PObj(fields):
			// define type
			var name = "ObjProxy_";
			fields.sort(function(f1, f2) return Reflect.compare(f1.name, f2.name));
			name += [for( f in fields ) f.name+"_" + ~/[<>.]/g.replace(f.type.t.toString(),"_")].join("_");
			try {
				return Context.getType("hxd.net." + name).toComplexType();
			} catch( e : Dynamic ) {
				var pos = Context.currentPos();
				var pt = p.t;
				var myT = TPath( { pack : ["hxd", "net"], name : name } );
				var tfields = (macro class {
					var obj : hxd.net.NetworkSerializable.ProxyHost;
					var bit : Int;
					@:noCompletion public var __value(get, never) : $pt;
					inline function get___value() : $pt return cast this;
					inline function mark() if( obj != null ) obj.networkSetBit(bit);
					@:noCompletion public function networkSetBit(_) mark();
					@:noCompletion public function bindHost(obj, bit) { this.obj = obj; this.bit = bit; }
					@:noCompletion public function unbindHost() this.obj = null;
				}).fields;
				for( f in fields ) {
					var ft = f.type.t;
					var fname = f.name;
					tfields.push({
						name : f.name,
						pos : pos,
						access : [APublic],
						kind : FProp("default","set",ft),
					});

					var markExpr = makeMarkExpr(tfields, fname, f.type, macro mark());

					tfields.push( {
						name : "set_" + f.name,
						pos : pos,
						access : [AInline],
						kind : FFun({
							ret : ft,
							args : [ { name : "v", type : ft } ],
							expr : macro { this.$fname = v; $markExpr; return v; }
						}),
					});
				}
				tfields.push({
					name : "new",
					pos : pos,
					access : [APublic],
					kind : FFun({
						ret : null,
						args : [for( f in fields ) { name : f.name, type : f.type.t, opt : f.opt } ],
						expr : { expr : EBlock([for( f in fields ) { var fname = f.name; macro this.$fname = $i{fname}; }]), pos : pos },
					})
				});
				var t : TypeDefinition = {
					pos : pos,
					pack : ["hxd", "net"],
					meta : [{ name : ":structInit", pos : pos }],
					name : name,
					kind : TDClass([
						{ pack : ["hxd", "net"], name : "NetworkSerializable", sub : "ProxyHost" },
						{ pack : ["hxd", "net"], name : "NetworkSerializable", sub : "ProxyChild" },
					]),
					fields : tfields,
				};
				Context.defineType(t);
				return TPath({ pack : ["hxd","net"], name : name });
			}
		default:
		}
		return null;
	}

	public static function buildSerializableProxy() {
		var t = Context.getLocalType();
		switch( t ) {
		case TInst(_, [pt]):
			var p = getPropType(pt);
			if( p != null ) {
				var t = buildProxyType(p);
				if( t != null ) return toType(t);
			}
			throw "TODO "+pt+" ("+p+")";
		default:
			throw "assert";
		}
	}

	#end

}
