/**
	Storm
*/

local Name = "$Name$";
local Description = "$Description$";

local storm_debug = false;
local streams, n_streams;
local execution_index = 0, n_exec_per_loop = 1;
local max_stream_len = 1000; // maximum number of intervals to be calculated for each stream of this storm
local exec_interval = 10; // exec every n frames
local find_mask; // Find_-condition for objects to be flung by wind
local strength; // storm strength
local stream_density = 20;
local stream_border_dist = 20;
local map, map_res1, map_res2, map_size1, map_size2, map_off1, map_off2;
//local debug_map;
local storm_particles; // storm particle definition, for pretty visuals

local StormStream;

static g_storm;

func Initialize()
{
	// singleton
	g_storm = this;
	SetPosition();
	// defaults
	storm_particles = 
	{
		Size = 1,
		Stretch = PV_Speed(PV_Linear(4000, 0), 0),
		Alpha = PV_KeyFrames(0, 0, 0, 100, 255, 1000, 255),
		Rotation = PV_Direction(),
		CollisionVertex = 1000,
		OnCollision = PC_Die()
	};
	StormStream = {
		max_segment_stretch = 100, // maximum number of pixels per segment that can be deviated from dir in either direction
		max_segment_stretch_want = 5, // maximum movement back into original position that is preferred (i.e.: speed at which gaps behind sky islands close)
		search_steps = 10,        // steps in pixels in which to search for holes to blow through
		search_steps_mult = 200,  // multiplyer, in percent, by which search steps get larger with each iteration
	};
	find_mask = Find_And(Find_Category(C4D_Vehicle | C4D_Living | C4D_Object), Find_Not(Find_Func("IsEnvironment")));
	SetStorm(20,0, 2000);
}

func Clear()
{
	// timer
	RemoveEffect("IntExecute", this);
	// helper objects
	for (var i=0; i<n_streams; ++i)
		if (streams[i].debug) streams[i].debug->RemoveObject();
	streams = nil;
	n_streams = 0;
	map = nil;
}

private func InitMap()
{
	// init empty wind map according to parameters
	// determine coordinate borders
	var wdt=LandscapeWidth()-1, hgt = LandscapeHeight()-1;
	var w1_min = Min(Min(MapXYToW1(0,0),MapXYToW1(0,hgt)),Min(MapXYToW1(wdt,0),MapXYToW1(wdt,hgt)));
	var w1_max = Max(Max(MapXYToW1(0,0),MapXYToW1(0,hgt)),Max(MapXYToW1(wdt,0),MapXYToW1(wdt,hgt)));
	var w2_min = Min(Min(MapXYToW2(0,0),MapXYToW2(0,hgt)),Min(MapXYToW2(wdt,0),MapXYToW2(wdt,hgt)));
	var w2_max = Max(Max(MapXYToW2(0,0),MapXYToW2(0,hgt)),Max(MapXYToW2(wdt,0),MapXYToW2(wdt,hgt)));
	// implement to cover complete border range
	map_res1 = StormStream.dir_len;
	map_res2 = stream_density;
	map_off1 = w1_min - map_res1/2;
	map_off2 = w2_min - map_res2/2;
	map_size1 = (w1_max - map_off1) / map_res1 + 1;
	map_size2 = (w2_max - map_off2) / map_res2 + 1;
	// allocate map
	map = CreateArray(map_size1 * map_size2);
	//debug_map = CreateArray(map_size1 * map_size2);
	return true;
}

private func MapXYToIdx(int x, int y)
{
	if (x<0 || x>=LandscapeWidth() || y<0 || y>=LandscapeHeight()) return -1;
	return (MapXYToW1(x, y)-map_off1)/map_res1 + ((MapXYToW2(x, y)-map_off2)/map_res2) * map_size1;
}

private func MapXYToW1(int x, int y)
{
	// coordinate transform from x/y space to in-wind-direction coordinate
	return (x*StormStream.dir_x + y*StormStream.dir_y) / StormStream.dir_len;
}

private func MapXYToW2(int x, int y)
{
	// coordinate transform from x/y space to perpendicular-to-wind-direction coordinate
	return (x*StormStream.dir_y - y*StormStream.dir_x) / StormStream.dir_len;
}

// dir_*: vector pointing in storm direction. vector length is equal to segment intervals.
// strength: how much to fling objects
func SetStorm(int dir_x, int dir_y, int astrength)
{
	// clear old
	Clear();
	// add new
	var d = Distance(dir_x, dir_y);
	if (!astrength || !d) return;
	strength = astrength;
	StormStream.dir_x = dir_x;
	StormStream.dir_y = dir_y;
	StormStream.dir_len = d;
	// init map
	InitMap();
	// create streams
	n_streams = ((Abs(LandscapeWidth()*dir_y) + Abs(LandscapeHeight()*dir_x))/d - 2*stream_border_dist) / stream_density;
	streams = CreateArray(n_streams);
	var i_stream = 0, s, x0, y0, sgn_x=1, sgn_y=1;
	var wdt = LandscapeWidth()-1, hgt = LandscapeHeight()-1;
	if (dir_y<0) { y0 = hgt; sgn_y = -1; }
	if (dir_x<0) { x0 = wdt; sgn_x = -1; }
	//Log("creating %d streams", n_streams);
	for (var i = 0; i<n_streams; ++i)
	{
		var pos = stream_border_dist + i * stream_density;
		var x0,y0;
		if (dir_y)
		{
			var dpos=Abs(pos*d/dir_y);
			if (dpos<=wdt)
			{
				// streams from horizontal border of landscape
				if (s=CreateStream(wdt-x0-dpos*sgn_x, y0)) streams[i_stream++] = s;
				continue;
			}
			pos -= Abs(wdt*dir_y/d);
		}
		// streams from vertical border of landscape
		pos=Abs(pos*d/dir_x);
		//Log("@%d", pos);
		if (s=CreateStream(x0, y0+pos*sgn_y)) streams[i_stream++] = s;
	}
	n_streams=i_stream;
	//Log("%d total", n_streams);
	streams = streams[0:n_streams];
	// create timer for stream execution
	n_exec_per_loop = n_streams;
	AddEffect("IntExecute", this, 1, exec_interval, this);
}

func FxIntExecuteTimer()
{
	for (var i_exec=0; i_exec<n_exec_per_loop; ++i_exec)
	{
		// exec current stream
		//Log("exec %d", execution_index);
		ExecuteStream(streams[execution_index]);
		// next stream (++execution_index %= n_streams may or may not work)
		++execution_index;
		execution_index %= n_streams;
	}
}

private func CreateStream(int x0, int y0)
{
	//Log("stream at %d/%d", x0,y0);
	// Not in earth
	if (GBackSolid(x0, y0)) return nil;
	// Determine length
	var len_x = max_stream_len, len_y = max_stream_len;
	if (StormStream.dir_x < 0)
		len_x = -(x0-1) / StormStream.dir_x + 1;
	else if (StormStream.dir_x)
		len_x = (LandscapeWidth()-x0-1) / StormStream.dir_x + 1;
	if (StormStream.dir_y < 0)
		len_y = -(y0-1) / StormStream.dir_y + 1;
	else if (StormStream.dir_y)
		len_y = (LandscapeHeight()-y0-1) / StormStream.dir_y + 1;
	var len = Min(len_x, len_y);
	// Initialize as laminar stream along desired path
	var x = CreateArray(len), y = CreateArray(len);
	var is_blocked = CreateArray(len);
	for (var i=0; i<len; ++i)
	{
		x[i] = x0+i*StormStream.dir_x;
		y[i] = y0+i*StormStream.dir_y;
		is_blocked[i] = (i>0); // initial stream is blocked and will be unblocked on first execution
	}
	// Create stream data struct
	var stream_debug;
	if (storm_debug) stream_debug = CreateObjectAbove(Storm_DebugDisplay,0,0,NO_OWNER);
	var new_stream = {
		Prototype = StormStream,
		"x0" = x0, "y0" = y0, // "a"=a because Guenther said so
		"len" = len,
		"x" = x, "y" = y,
		"is_blocked" = is_blocked,
		"debug" = stream_debug,
	};
	return new_stream;
}

private func ExecuteStream(proplist s)
{
	//Log("ExecStream %v", s);
	// Execute stream against wind direction, so changes dont propagate immediately but only segment-by-segment
	var do_particles = !Random(3);
	for (var i_segment = s.len-2; i_segment>=0; --i_segment)
	{
		// propagate block
		if (s.is_blocked[i_segment])
		{
			//Log("segment %d", i_segment);
			if (!s.is_blocked[i_segment+1]) StreamBlockVertex(s, i_segment+1);
			if (storm_debug)
				CreateParticle("SphereSpark", s.x[i_segment], s.y[i_segment], 0, 0, 36, {Size =  12});
			continue;
		}
		// current segment base point
		var x = s.x[i_segment], y = s.y[i_segment];
		var tx = s.x[i_segment+1], ty = s.y[i_segment+1];
		// determine direction of current segment
		var vx = tx - x;
		var vy = ty - y;

		// determine where we want to go
		var want_vx = s.x0+(i_segment+1)*s.dir_x - x;
		var want_vy = s.y0+(i_segment+1)*s.dir_y - y;

		var want_stretch = (s.dir_x*want_vy-s.dir_y*want_vx) / s.dir_len;
		//if (i_segment==8) Log("%v", want_stretch);
		// can turn?
		if (Abs(want_stretch) > s.max_segment_stretch_want)
		{
			// We cannot go all the way...turn as much as we can
			var stretch_dir = Abs(want_stretch)/want_stretch; // sign of direction
			want_stretch = s.max_segment_stretch_want * stretch_dir;
		}
		// check from want_v alternating in both directions for a free path
		var search_range = (Abs(want_stretch) + s.max_segment_stretch);
		var search_off, has_found = false;
		for (var search_offset = 0; search_offset <= search_range; search_offset = search_offset * s.search_steps_mult/100 + s.search_steps)
		{
			// search up
			search_off = want_stretch - search_offset;
			if (search_off >= -s.max_segment_stretch)
				if (StreamCheckPathFree(s,x,y,search_off)) { has_found=true; break; }
			if (!search_offset) continue; // don't check direction -0 and +0 twice
			// search down
			search_off = want_stretch + search_offset;
			if (search_off <= s.max_segment_stretch)
				if (StreamCheckPathFree(s,x,y,search_off)) { has_found=true; break; }
		}
		// did we find a path?
		if (has_found)
		{
			// path found
			if (s.is_blocked[i_segment+1]) StreamUnblockVertex(s, i_segment+1);
			var new_tx = x + s.dir_x - search_off * s.dir_y / s.dir_len;
			var new_ty = y + s.dir_y + search_off * s.dir_x / s.dir_len;
			if (new_tx != tx || new_ty != ty) StreamMoveVertex(s, i_segment+1, tx, ty, new_tx, new_ty);
			tx = new_tx; ty = new_ty;
			// determine storm density at this position
			var map_idx = MapXYToIdx(tx, ty), local_strength;
			if (map_idx>=0) local_strength = map[map_idx]; else local_strength=1;
			// fling objects along path
			vx = vx * strength / s.dir_len;
			vy = vy * strength / s.dir_len; // - 20;
			var fling_objs = FindObjects(find_mask, Find_OnLine(x,y,new_tx,new_ty)), obj;
			for (obj in fling_objs) if (obj->GetID()==ElevatorCase) { fling_objs = []; break; } // do not fling stuff in elevator case
			for (obj in fling_objs)
			{
				// check if object can be pushed
				if (obj->Stuck()) continue;
				if (!PathFree(x,y,obj->GetX(),obj->GetY())) continue; // don't push through solid
				// determine push strength. subsequent pushes of overlapping storm pathes stack diminishingly
				var push_strength = strength/20,pushfx;
				if (pushfx=GetEffect("StormPush",obj))
				{
					push_strength /= pushfx.count++;
					if (!push_strength) continue;
				}
				else
				{
					pushfx=AddEffect("StormPush", obj, 1, 5, this);
					if (pushfx) pushfx.count = 1;
				}
				// now push
				var ovx = obj->GetXDir(100);
				var ovy = obj->GetYDir(100);
				// check max speed
				if (Distance(ovx,ovy,vx,vy) > push_strength*6)
				{
					if (Distance(ovx,ovy) > 500)
						obj->Fling(BoundBy(vx-ovx,-push_strength,push_strength),BoundBy(vy-ovy,-push_strength,push_strength),100,true);
					else
					{
						obj->SetXDir(ovx+BoundBy(vx-ovx,-push_strength,push_strength),100);
						obj->SetYDir(ovy+BoundBy(vy-ovy,-push_strength,push_strength),100);
					}
				}
			}
			// Gfx
			if (do_particles && map_idx>=0)
			{
				if (local_strength >= 1)
				{
					// Two streams coincide here. Gfx!
					vx = tx-x; vy = ty-y;
					var v = Distance(vx,vy);
					vx = vx * s.dir_len / v;
					vy = vy * s.dir_len / v / 2;
					CreateParticle("Dust", PV_Random(x - 10, x + 10), PV_Random(y - 10, y + 10), PV_Random(vx * 80 / 100, vx * 120 / 100), PV_Random(vy, vy * 140 / 100), PV_Random(20, 40), storm_particles,local_strength); 
				}
			}
		}
		else
		{
			// path not found. segment blocked.
			if (!s.is_blocked[i_segment+1]) StreamBlockVertex(s, i_segment+1);
		}
	}
	if (s.debug) s.debug->ShowData(s.x, s.y);
}

private func StreamCheckPathFree(proplist s, int x, int y, int offset)
{
	// determine target coordinates
	var tx = x + s.dir_x - offset * s.dir_y / s.dir_len;
	var ty = y + s.dir_y + offset * s.dir_x / s.dir_len;
	// check path
	return PathFree(x,y,tx,ty);
}

private func StreamMoveVertex(proplist s, int i, int old_x, int old_y, int new_x, int new_y)
{
	//Log("moving %d/%d to %d/%d", old_x, old_y, new_x, new_y);
	// adjust vertex
	s.x[i] = new_x; s.y[i] = new_y;
	// adjust map
	var idx = MapXYToIdx(old_x, old_y);
	if (idx>=0) --map[idx];
	//DebugMapAdd(idx, Format("m%d.%d", s.y0, i));
	idx = MapXYToIdx(new_x, new_y);
	if (idx>=0) ++map[idx];
	//DebugMapAdd(idx, Format("M%d.%d", s.y0, i));
	return true;
}

private func StreamBlockVertex(proplist s, int i)
{
	//Log("blocking at %d/%d", s.x[i], s.y[i]);
	// adjust vertex
	s.is_blocked[i] = true;
	// adjust map
	var idx = MapXYToIdx(s.x[i], s.y[i]);
	if (idx>=0) --map[idx];
	//DebugMapAdd(idx, Format("X%d.%d", s.y0, i));
	return true;
}

private func StreamUnblockVertex(proplist s, int i)
{
	//Log("unblocking at %d/%d", s.x[i], s.y[i]);
	// adjust vertex
	s.is_blocked[i] = false;
	// adjust map
	var idx = MapXYToIdx(s.x[i], s.y[i]);
	if (idx>=0) ++map[idx];
	//DebugMapAdd(idx, Format("O%d.%d", s.y0, i));
	return true;
}

private func DumpStreamInfo(int i)
{
	var s = streams[i], q=[], idcs = [];
	for (var j=0; j<s.len; ++j)
	{
		var idx = MapXYToIdx(s.x[j], s.y[j]);
		if (idx<0) q[j] = []; else q[j] = map[idx];
		idcs[j] = idx;
	}
	Log("%v", idcs);
	return q;
}

/*private func DebugMapAdd(int i, string s)
{
	if (i<0) return;
	if (debug_map[i]) debug_map[i] = Format("%s %s", debug_map[i], s); else debug_map[i] = s;
	return true;
}*/

func GetWindEx(int x, int y)
{
	if (!map) return 0; // not initialized or zero storm
	var idx = MapXYToIdx(x, y);
	if (idx<0) return 0; // outside landscape
	// check storm density map
	return -BoundBy(map[idx]*strength/10, 0,100);
}

global func GetWind(int x, int y)
{
	if (g_storm) return g_storm->GetWindEx(x+GetX(),y+GetY());
	return _inherited(x,y,...);
}
