/*	kd:
	
	The Sandman's targeting is just a special effect. It'll autoaim at targets
	regardless of the crosshair's position. The rest is just fluff! You wouldn't
	believe it. (Though functionality for a superscope-mode does exist!)
	
	I thought something MGS-like might be interesting! Guncaster is a mod with
	all sorts of weird technology.
	
	*/

class Gc_SandmanVisor : EventHandler {
	const open_anim_tics		= 10;
	
	private RenderBox					box;
	private Gc_SandmanCrossTime			cross_timer;
	private bool						is_cross_animating;
	private bool						old_is_active;
	private bool						is_active;
	private bool						just_activated;
	private bool						just_deactivated;
	private double						line_draw_alpha;
	private FadeTick					anim_t;
	
	// Colour cycling:
	private int							lockon_rgb_index;
	// private Color						line_draw_color;
	
	// Properties set at start up:
	private int							normal_rgb_index;
	private int							locked_rgb_index;
	private int							normal_to_locked_rgb;
	
	private Color						frame_color;
	
	private Gc_GlScreen					gl_proj;
	private Gc_SwScreen					sw_proj;
	private Gc_ProjScreen				proj;
	private Gc_DefaultCvar				cvar_fov;
	private Gc_DefaultCvar				cvar_render_mode;
	private FadeTick					cross_t;
	
	const max_line_alpha = 130;
	
	override void OnRegister () {
		// kd: For the crosshair pointing to target effect.
		gl_proj = new("Gc_GlScreen");
		sw_proj = new("Gc_SwScreen");
		proj = gl_proj;
		
		// kd: Using the rgb cycler I've already used for the prizm lazer, I
		// can make a more interesting colour fade.
		normal_rgb_index		= Gc_PulseFunc.color_green;
		locked_rgb_index		= Gc_PulseFunc.color_red;
		normal_to_locked_rgb	= locked_rgb_index - normal_rgb_index;
		
		ResetAnimation();
	}
	
	void ResetAnimation () {
		anim_t.Force(0);
		cross_t.Force(0);
		cross_timer.Reset();
		ForceCrossPosition((center, center));
	}
	
	private bool is_using_sandman;
	private bool is_using_strat;
	
	private void SetActiveForSandman () {
		old_is_active	= is_active;
		is_active		= false;
		
		if(player_mo) {
			is_using_strat		= 0 < player_mo.CountInv("SandmanStratActive");
			is_using_sandman	= sandman_weap is "Sandman" && sandman_weap != NULL;
			is_active			= is_using_strat && is_using_sandman;
		}
		
		just_activated		=  is_active && !old_is_active;
		just_deactivated	= !is_active &&  old_is_active;
	}
	
	private bool has_target;
	private bool old_has_target;
	private bool just_got_target;
	private bool just_lost_target;
	private bool just_changed_target;
	private bool is_ejecting_target;
	private Actor target;
	private Actor old_target;
	
	void SetTargetState (Actor locked_mo, bool is_ejecting) {
		old_has_target		= has_target;
		old_target			= target;
		
		target = locked_mo;
		is_ejecting_target = is_ejecting;
		
		has_target			= target != NULL;
		just_got_target		=  has_target && !old_has_target;
		just_lost_target	= !has_target &&  old_has_target;
		just_changed_target	=  has_target && (old_target != target);
	}
	
	void ResetTargetState () {
		has_target				= false;
		old_has_target			= false;
		just_got_target			= false;
		just_lost_target		= false;
		just_changed_target		= false;
	}
	
	/* private void CacheTargetState () {
		old_has_target		= has_target;
		old_target			= target;
		let sandman_weap	= Sandman(players [consoleplayer].readyweapon);
		
		if(sandman_weap) {
			target = sandman_weap.locked_mo;
		}
		
		else {
			target = NULL;
		}
		
		has_target			= target != NULL;
		just_got_target		=  has_target && !old_has_target;
		just_lost_target	= !has_target &&  old_has_target;
		just_changed_target	=  has_target && (old_target != target);
	} */
	
	// kd: The health viewer should show the target marked.
	private void NotifyHealthViewer () {
		if(target) {
			let handler = Gc_HealthView(Gc_HealthView.Find("Gc_HealthView"));
			
			if(handler) {
				handler.OverrideTarget(target);
			}
		}
	}
	
	// kd: DEMONITISED. I mean, this triggers the colour switching animations.
	private void HandleCrossCode () {
		if(just_got_target || just_changed_target) {
			if(target.bismonster) {
				cross_timer.TriggerCodeRed();
			}
			
			else {
				cross_timer.TriggerCodeYellow();
			}
		}
		
		else if(just_lost_target) {
			cross_timer.TriggerCodeGreen();
		}
	}
	
	// kd: Convenience. Since this is a visual effect, I can use all the
	// consoleplayer business I like, but I never like typing it out repeatedly.
	
	private Actor							player_mo;
	private PlayerInfo						player;
	private Sandman							sandman_weap;
	
	private void CachePlayerInfo () {
		if(playeringame [consoleplayer]) {
			player			= players [consoleplayer];
			player_mo		= player.mo;
			sandman_weap	= Sandman(player.readyweapon);
		}
		
		else {
			player_mo		= NULL;
			player			= NULL;
			sandman_weap	= NULL;
		}
	}
	
	// kd: This handles finding the position of the crosshair on the screen,
	// using a projection, and cooling down crosshair movement if the target
	// died or disappeared.
	
	// cross_target is the position the cross will smoothly move towards.
	const cross_death_wait_tics	= 30;
	const cross_move_rate		= 0.41337;
	
	private vector2 old_cross_normal;
	private vector2 cross_normal;
	private vector2 cross_target;
	
	private void ForceCrossPosition (vector2 normal) {
		old_cross_normal	= normal;
		cross_normal		= normal;
		cross_target		= normal;
	}
	
	private void MoveCross () {
	
		// kd: If there's no longer any target, wait a moment. (It just looks
		// kinda cool to do this.)
		if(just_lost_target) {
			cross_t.To(cross_death_wait_tics);
		}
		
		if(has_target) {
			// kd: Project the target's centre to our screen (big).
			proj.OrientForPlayer(player);
			proj.BeginProjection();
			proj.ProjectWorldPos(target.vec3offset(0, 0, target.height * 0.5));
			
			// kd: We have to "compensate" for the virtual 4:3 box in the
			// screen centre, because the projector assumes we're playing with
			// the entire screen, not just a rectangle in the middle.
			
			if(proj.IsInFront() && !is_ejecting_target) {
				vector2 normal	= proj.ProjectToNormal();
				normal.x		= box.CompensateXscale(normal.x);
				cross_target	= ((1, 1) + normal) * 0.5;
			}
		}
		
		else {
			cross_t.Next();
			
			if(cross_t.IsDone()) {
				cross_target = (0.5, 0.5);
			}
		}
		
		// kd: Move the cross. Keep old position for interpolation in
		// RenderOverlay and make sure the cross stays within the target finder
		// boundary thingy.
		old_cross_normal	= cross_normal;
		cross_normal		+= cross_move_rate * (cross_target - cross_normal);
		cross_normal		= Gc_Vec.Restrain2(
			cross_normal,
			(0.15, 0.3),
			(0.85, 0.7));
	}
	
	private void TriggerScreenOpen () {
		ResetAnimation();
		ResetTargetState();
		anim_t.ContinueTo(open_anim_tics);
	}
	
	private void TriggerScreenClose () {
		anim_t.ContinueTo(0);
	}
	
	// kd: This is both gl and software compatible. Most folks won't use
	// software, but I have just made the projector, so I don't see a reason
	// we shouldn't do this, given it's a pretty nice effect already.
	private void HandleRenderer () {
		switch(cvar_render_mode.GetInt(def_val: 5)) {
		default:
			proj = gl_proj;
			break;
		case 0:
		case 1:
			proj = sw_proj;
			break;
		}
	}
	
	// kd: This will let us draw the screen the same size, no matter how big
	// your game window is. That means the Sandman will cover the same fov,
	// no matter your display settings, which is fair. (Also, none of us like
	// network desyncs!)
	private void HandleScreenResize () {
		double fov = cvar_fov.GetDouble(def_val: 90);
		
		// kd: Let our projection know ASAP.
		proj.CacheResolution();
		proj.CacheFov(fov);
		
		// kd: This creates a virtual 4:3 screen in the middle of your actual
		// screen. Neat, huh? If this rings any bells: HudMessage.
		box.ByAspect(
			aspect_x:		4.0,
			aspect_y:		3.0,
			virtual_height:	Screen.GetHeight());
		
		// kd: Finally, the size should depend on your fov. I play with 90°,
		// but somebody with 110° will get a relatively smaller looking
		// visor. Again, cover the same fov.
		box.RescaleForFov(fov);
		
		// kd: Regardless of your resolutions, lines should always have the
		// same relative thickness, too.
		standard_line_width = max(1, box.NormalWidthToScreen(0.001777));
	}
	
	void Update () {
		
		CachePlayerInfo();
		SetActiveForSandman();
		
		// cvar_fov = Cvar.GetCvar("fov", player);
		cvar_fov.Get("fov", player);
		cvar_render_mode.Get("vid_rendermode", player);
		
		// kd: Handle the screen opening and closing animation timing:
		anim_t.Next();
		
		if(just_activated) {
			TriggerScreenOpen();
		}
		
		else if(just_deactivated) {
			TriggerScreenClose();
		}
		
		// kd: This allows the screen to persist while the state is technically
		// no longer active, because you might remove your strat active token
		// but keep the weapon up.
		
		// I kinda like having opening and closing animations for everything!
		if(0 < anim_t.Now()) {
			// CacheTargetState();
			HandleCrossCode();
			HandleRenderer();
			HandleScreenResize();
			NotifyHealthViewer();
			MoveCross();
			
			cross_timer.Next();
		}
	}
	
	const top		= 0.24;
	const bottom	= 1.0 - top;
	const left		= 0.075;
	const right		= 1.0 - left;
	const center	= 0.5;
	const radius	= 0.5 * (right - left);
	
	const screen_alpha = 0.0666;
	
	const tall_bit_length	= 0.015;
	const tall_bit_spacing	= 0.250;
	const short_bit_length	= 0.005;
	const short_bit_spacing	= 0.025;
	
	override void RenderOverlay (RenderEvent event) {
		if(anim_t.Now() < 1 || automapactive) {
			return;
		}
		
		// kd: The bottom line moves in and the alpha fades in, too.
		double t		= event.fractic;
		double anim_f	= anim_t.RenderStandardDelta(open_anim_tics, t);
		
		double bottom_f	= center + anim_f * (bottom - center);
		double top_f	= center + anim_f * (top    - center);
		double alpha_f	= screen_alpha * anim_f;
		
		// Fade all lines in:
		int alpha_if	= int(anim_f * max_line_alpha);
		
		// Current alert code colour. It cycles over the colour spectrum, which
		// should look pretty great.
		int rgb_i =
			normal_rgb_index + cross_timer.Alpha(t) * normal_to_locked_rgb;
		Color fading_color = Gc_PulseFunc.RgbCycle(rgb_i);
		
		// Boundary lines:
		DrawLine((left, top_f),		(right, top_f),		fading_color, alpha_if);
		DrawLine((left, bottom_f),	(right, bottom_f),	fading_color, alpha_if);
		
		// The screen:
		DrawFillRect(
			(left	+ 0.01, top_f),
			(right	- 0.01, bottom_f),
			fading_color,
			alpha_f);
		
		// Compass bits for boundary lines:
		double offset = event.viewangle / 360;
		
		// Short lined:
		DrawCompass(
			y:				top_f,
			bit_offset:		offset,
			col:			fading_color,
			alpha_i:		alpha_if,
			t:				t);
		
		DrawCompass(
			y:				bottom_f,
			bit_offset:		offset,
			col:			fading_color,
			alpha_i:		alpha_if,
			t:				t);
		
		// Tall lined:
		DrawCompass(
			y:				top_f,
			bit_spacing:	tall_bit_spacing,
			bit_length:		tall_bit_length,
			bit_offset:		offset,
			col:			fading_color,
			alpha_i:		alpha_if,
			t:				t);
		
		DrawCompass(
			y:				bottom_f,
			bit_spacing:	tall_bit_spacing,
			bit_length:		tall_bit_length,
			bit_offset:		offset,
			col:			fading_color,
			alpha_i:		alpha_if,
			t:				t);
		
		// Crosshair:
		vector2 cross_interpol_pos =
			old_cross_normal + t * (cross_normal - old_cross_normal);
		
		double cross_f = cross_timer.Alpha(t);
		
		vector2 max_rad = (0.0555, 0.0555);
		vector2 min_rad = (0.0444, 0.0444);
		vector2 cross_rad_f = max_rad + cross_f * (min_rad - max_rad);
		
		DrawCross(
			cross_interpol_pos,
			radius:			cross_rad_f,
			col:			fading_color,
			alpha_i:		alpha_if,
			t:				t);
		
		// And the lockon lines:
		DrawLockon(
			cross_interpol_pos,
			col:			fading_color,
			alpha_i:		alpha_if,
			t:				t);
	}
	
	private ui void DrawCompass (
	double	center_x	= center,
	double	y			= center,
	double	radius		= radius,
	double	bit_spacing	= short_bit_spacing,
	double	bit_length	= short_bit_length,
	double	bit_offset	= 0,
	Color	col			= 0xffffffff,
	int		alpha_i		= max_line_alpha,
	double	t			= 1) const {
		bit_offset			= (radius + bit_offset) % bit_spacing;
		double x			= center_x - radius + bit_offset;
		double right_bound	= center_x + radius;
		
		while(x <= right_bound) {
			DrawLine((x, y - bit_length), (x, y + bit_length), col, alpha_i);
			x += bit_spacing;
		}
	}
	
	private ui void DrawCross (
	vector2	normal,
	vector2	radius		= (0.06, 0.06),
	double	line_offset	= 0.02,
	double	line_length	= 0.05,
	Color	col			= 0xffffffff,
	int		alpha_i		= max_line_alpha,
	double	t			= 1) const {
		
		// kd: Select the colour we're cycling through right now. This is a
		// tad different from linearly interpolating two colours by their
		// rgb-components. For example, starting at green and ending at red,
		// we also head over yellow and orange, which looks cool.
		
		// Rectangle:
		if(cross_timer.IsDone()) {
			t = 0;
		}
		
		// Box:
		DrawLineRect(normal - radius, normal + radius, col, alpha_i);
		
		// Cross:
		double max_offset = line_offset + line_length;
		
		DrawLine(
			normal + (-max_offset,  0),
			normal + (-line_offset, 0), col, alpha_i);
		DrawLine(
			normal + ( max_offset,  0),
			normal + ( line_offset, 0), col, alpha_i);
		DrawLine(
			normal + ( 0, -max_offset),
			normal + ( 0, -line_offset), col, alpha_i);
		DrawLine(
			normal + ( 0,  line_offset),
			normal + ( 0,  max_offset), col, alpha_i);
	}
	
	private ui void DrawLockon (
	vector2	normal,
	Color	col		= 0xffffffff,
	int		alpha_i	= max_line_alpha,
	double	t		= 1) const {
		vector2 vel			= (0.01, 0.010);
		vector2 pos			= (-0.04, -0.04);
		vector2 vel_hor		= (-vel.x, vel.y);
		vector2 pos_hor		= (-pos.x, pos.y);
		vector2 offset		= (0, -2 * pos.y);
		int sign			= 1;
		
		pos += normal;
		pos_hor += normal;
		
		for(int i = 0; i < cross_timer.pieces; i++) {
			DrawLine(pos, pos + offset * cross_timer.PieceAlpha(i), col, alpha_i);
			DrawLine(pos_hor, pos_hor + offset * cross_timer.PieceAlpha(i), col, alpha_i);
			
			pos			+= vel;
			pos_hor		+= vel_hor;
			offset.y	-= 2 * vel.y;
			sign		*= -1;
		}
	}
	
	// Some bonus draw functions!
	
	private double						standard_line_width;
	
	private ui void DrawLine (
	vector2	start,
	vector2	end,
	Color	col,
	int		alpha_i = max_line_alpha) const {
		Screen.DrawThickLine(
			box.NormalXtoScreen(start.x),
			box.NormalYtoScreen(start.y),
			box.NormalXtoScreen(end.x),
			box.NormalYtoScreen(end.y),
			standard_line_width, col, alpha_i);
	}
	
	// tl = Top left
	// br = Bottom Right
	
	// I didn't wanna use newline here.
	private ui void DrawLineRect (
	vector2	tl,
	vector2	br,
	Color	col		= 0xffffffff,
	int		alpha_i	= max_line_alpha) const {
		DrawLine((tl.x, tl.y),	(br.x, tl.y), col, alpha_i);
		DrawLine((br.x, tl.y),	(br.x, br.y), col, alpha_i);
		DrawLine((br.x, br.y),	(tl.x, br.y), col, alpha_i);
		DrawLine((tl.x, br.y),	(tl.x, tl.y), col, alpha_i);
	}
	
	// kd: Not the most efficient by a long shot, but since we're just gonna
	// draw like 40 lines...
	
	// Anyway, sometimes you may want a line to grow in length. This does just
	// that, but you're gonna need to use a fader. You can find plenty of
	// examples here or in the health viewer code.
	
	// It has ridiculous naming btw, so beware.
	private ui void DrawFadeTickLine (
	FadeTick	fade,
	double		delta,
	vector2		start,
	vector2		offset,
	Color		col		= 0xffffffff,
	int			alpha_i	= max_line_alpha,
	double		t		= 1) const {
		if(fade.IsDone()) {
			t = 0;
		}
		
		double	f	= fade.RenderStandardDelta(delta, t);
		vector2	end	= start + f * offset;
		
		DrawLine(start, end, col, alpha_i);
	}
	
	private ui void DrawFillRect (
	vector2	start,
	vector2	end,
	Color	col,
	double	alpha = 1) const {
		Screen.Dim(
			col, alpha,
			box.NormalXtoScreen(start.x),
			box.NormalytoScreen(start.y),
			box.NormalWidthToScreen(end.x - start.x),
			box.NormalHeightToScreen(end.y - start.y));
	}
}

/*	kd:
	
	Here's the handling for the lockon animation. It causes the crosshair to
	contract and draw additional lines that look kinda cool.
	
	*/

struct Gc_SandmanCrossTime {
	const pieces			= 4;
	const piece_anim_time	= 4;
	const anim_time			= 20;
	const anim_time_2		= anim_time / 2;
	
	private FadeTick				tics;
	private FadeTick				piece_fade [pieces];
	
	bool IsDone () const {
		return tics.IsDone();
	}
	
	// gzd cannot return struct as of yet.
	/* FadeTick FadeAt (int i) const {
		return corner_fade [i];
	} */
	
	double PieceAlpha (int i = 0, double t = 1) const {
		if(0 <= i && i < pieces) {
			return piece_fade [i].RenderStandardDelta(piece_anim_time, t);
		}
		
		return 0;
	}
	
	double Alpha (double t = 1) const {
		return tics.RenderStandardDelta(anim_time, t);
	}
	
	void Reset () {
		tics.Force(0);
		
		for(int i = 0; i < pieces; i++) {
			piece_fade [i].Force(0);
		}
	}
	
	void TriggerCodeYellow () {
		tics.ContinueTo(anim_time_2);
	}
	
	void TriggerCodeGreen () {
		tics.ContinueTo(0);
	}
	
	void TriggerCodeRed () {
		tics.ContinueTo(anim_time);
	}
	
	void Next () {
		
		// Activate the corner faders one after another.
		int crossed_piece = -1;
		
		tics.Next();
		
		switch(tics.Now()) {
		case 4:
		case 8:
		case 12:
		case 16:
			crossed_piece = tics.Now() / 4 - 1;
			break;
		}
		
		// Activate or deactivate faders we crossed:
		if(0 <= crossed_piece && crossed_piece < pieces) {
			if(tics.IsCountDown()) {
				piece_fade [crossed_piece].ContinueTo(0);
			}
			
			else {
				piece_fade [crossed_piece].ContinueTo(piece_anim_time);
			}
		}
		
		// Update the faders, too.
		for(int i = 0; i < pieces; i++) {
			piece_fade [i].Next();
		}
	}
}
