/*
Please remember that:
-Lights work only on client side!
-Lights with Brightness or Radius of 0 (or less) are automatically deleted
-Lights are very performance heavy. Especially if they cast shadows. Use them carefully!

Script author: Boris Vacula
*/

class ScriptedLightBase extends EntityLightSource
{
	float 		m_LifetimeStart;
	float 		m_LifetimeEnd = -1; // -1 makes this light permanent
	float 		m_FadeOutTime = -1;
	float 		m_FadeInTime = -1;
	float 		m_Radius;
	float 		m_RadiusTarget;
	float 		m_Brightness;
	float 		m_BrightnessPulse; // flicker effect
	float 		m_BrightnessPulseSpeed;
	float 		m_BrightnessPulseAmplitudeMax;
	float 		m_BrightnessPulseAmplitudeMin;
	float 		m_BrightnessTarget;
	float 		m_BrightnessSpeedOfChange = 1;
	float 		m_RadiusSpeedOfChange = 1;
	float 		m_OptimizeShadowsRadius = 0; // Within this range between the light source and camera the shadows will be automatically disabled to save on performance
	
	float 		m_DancingShadowsAmplitude;
	float 		m_DancingShadowsSpeed;
	
	float		m_BlinkingSpeed;
	protected int m_HiddenSelectionID;
	
	bool 		m_IsDebugEnabled = false;
	
	Object 		m_Parent; // Attachment parent
	vector 		m_LocalPos; // Local position to my attachment parent
	vector 		m_LocalOri; // Local orientation to my attachment parent
	vector 		m_DancingShadowsLocalPos;
		
	ref Timer 	m_DeleteTimer;
	protected ref LightDimming m_LightDimming;
	
	static ref set<ScriptedLightBase> m_NightTimeOnlyLights = new set<ScriptedLightBase>();
	
	//! Constructor. Everything here is executed before the constructor of all children.
	void ScriptedLightBase()
	{
		m_LifetimeStart = GetGame().GetTime();
		SetEnabled(true);
		SetEventMask(EntityEvent.FRAME);
		SetEventMask(EntityEvent.INIT);
	}
	
	void ~ScriptedLightBase()
	{
		if (m_NightTimeOnlyLights)
		{
			int index = m_NightTimeOnlyLights.Find(this);
			if (index != -1)
			{
				m_NightTimeOnlyLights.Remove(index);
			}
		}
	}
	
	override void EOnInit(IEntity other, int extra)
	{
		if (!IsVisibleDuringDaylight())
		{
			PlayerBase player = PlayerBase.Cast(GetGame().GetPlayer());
			if (player && player.m_UndergroundPresence)
				SetVisibleDuringDaylight(true);
			m_NightTimeOnlyLights.Insert(this);
		}
	}
	
	override bool IsScriptedLight()
	{
		return true;
	}
	
	void UpdateMode()
	{
		ItemBase item = ItemBase.Cast(m_Parent);
		if (item)
		{
			InventoryLocation il = new InventoryLocation;
			item.GetInventory().GetCurrentInventoryLocation( il );
			string slotName;
			if (il.GetType() == InventoryLocationType.GROUND)
			{
				slotName = "Ground";
			}
			else if (il.GetSlot() != -1)
			{
				slotName = InventorySlots.GetSlotName(il.GetSlot());
			}
			UpdateLightMode(slotName);			
		}
	}
	
	private void UpdateLightMode(string slotName);
	
	//! Correct way of deleting light from memory. It is necesarry to have this delay due to hierarchy.
	private void DeleteLightWithDelay()
	{
		DetachFromParent(); // This is the reason for the delay
		
		if (GetGame())
		{
			if (!m_DeleteTimer)
				m_DeleteTimer = new Timer( CALL_CATEGORY_SYSTEM );
	
			m_DeleteTimer.Run( 0.03 , this, "DeleteLightNow", NULL, true);
		}
		
	}
	
	// Deletes light now. Do not call this directly. Call Destroy() instead. Otherwise you might get errors related to hierarchy.
	private void DeleteLightNow()
	{
		GetGame().ObjectDelete(this);
	}
	
	//! Attaches this light on the parent entity, with optional position and orientation offset.
	void AttachOnObject(Object parent, vector local_pos = "0 0 0", vector local_ori = "0 0 0")
	{
		if (!parent)
		{
			if (m_Parent)
			{
				m_Parent.RemoveChild(this);
			}
			
			return;
		}
		else
		{
			if (m_Parent)
			{
				m_Parent.RemoveChild(this);
			}
		}
		
		m_Parent = parent;
		m_LocalPos = local_pos;
		m_LocalOri = local_ori;
		SetOrientation(local_ori);
		SetPosition(local_pos);
		parent.AddChild(this, -1);
		parent.Update();
	}
	
	//! Returns attachment parent
	Object GetAttachmentParent()
	{
		return m_Parent;
	}
	
	//! Attaches this light on the parent entity's memory point, with optional direction target memory point.
	void AttachOnMemoryPoint(Object parent, string memory_point_start, string memory_point_target = "")
	{
		if (parent.MemoryPointExists(memory_point_start))
		{
			m_LocalPos = parent.GetMemoryPointPos(memory_point_start);
			vector local_ori;
			
			if (memory_point_target != "" )
			{
				if (parent.MemoryPointExists(memory_point_target))
				{
					vector target_pos = parent.GetSelectionPositionLS(memory_point_target);
					target_pos = vector.Direction(m_LocalPos, target_pos);
					local_ori = target_pos.VectorToAngles();
				}
				else
				{
					ErrorEx("memory point 'memory_point_target' not found when attaching light");
				}
			}
			AttachOnObject(parent, m_LocalPos, local_ori);	
			UpdateMode();
		}
		else
		{
			ErrorEx("memory point 'memory_point_start' not found when attaching light");
		}
	}
	
	//! Detaches this light from its parent entity.
	void DetachFromParent()
	{
		if (!m_Parent)
		{
			m_Parent = Object.Cast( GetParent() );
		}
		
		if (m_Parent)
		{
			if ( !m_Parent.ToDelete()  &&  !ToDelete() )
			{
				m_Parent.RemoveChild(this);
			}
		}
		
		m_Parent = null;
		m_LocalPos = Vector(0,0,0);
		m_LocalOri = Vector(0,0,0);
	}
	
	static ScriptedLightBase CreateLightAtObjMemoryPoint(typename name, notnull Object target, string memory_point_start, string memory_point_target = "", vector global_pos = "0 0 0", float fade_in_time_in_s = 0)
	{
		ScriptedLightBase light;
		if (target.MemoryPointExists(memory_point_start))
		{
			light = CreateLight(name, global_pos, fade_in_time_in_s);
			light.AttachOnMemoryPoint(target, memory_point_start, memory_point_target);
		}
		return light;
	}
	
	//! Creates an instance of light on the given position. Optionally, use fade_in_time_in_s parameter to make the light fade into existence.
	static ScriptedLightBase CreateLight(typename name, vector global_pos = "0 0 0", float fade_in_time_in_s = 0)
	{
		ScriptedLightBase light_instance;
		
		if ( !GetGame().IsServer()  ||  !GetGame().IsMultiplayer() ) // Client side
		{
			light_instance = ScriptedLightBase.Cast( GetGame().CreateObject(name.ToString(), global_pos, true) );
			
			if (!light_instance)
			{
				Error("Error! Light entity of name " + name.ToString() + " cannot be spawned! This name is incorrect or not inherited from ScriptedLightBase." );
				return null;
			}
			
			if (fade_in_time_in_s != 0)
			{
				light_instance.FadeIn(fade_in_time_in_s);
			}
		}
		else // Server side
		{
			if ( GetGame().IsDebug() )
			{
				Error("An instance of ScriptedLightBase was attempted to spawn on server side! Lights are CLIENT SIDE ONLY!");
			}
		}
		
		return light_instance;
	}
	
	//! Sets the brightness of the light
	void SetBrightnessTo(float value)
	{
		m_Brightness = value;
		m_BrightnessTarget = value;
		SetBrightness(m_Brightness * m_BrightnessPulse);
		CorrectLightPulseDuringDaylight();
	}
	
	//! Call this after using SetBrightness(...) to fix light's intensity during daytime
	void CorrectLightPulseDuringDaylight()
	{
		if (m_Brightness < 100)
		{
			float v = m_Brightness * 0.01;
			
			if (v > 0)
			{
				float brightness_compesation = 1 / v;
				float compenset_brightness = (m_Brightness * m_BrightnessPulse) * brightness_compesation;
				SetBrightness(compenset_brightness);
				SetPulseCoef(v);
			}
		}
		else
		{
			SetPulseCoef(1);
		}
	}
	
	//! Fades the brightness of the light to the given value.
	void FadeBrightnessTo( float value, float time_in_s )
	{
		m_BrightnessTarget = value;
		
		if (time_in_s == 0)
		{
			m_BrightnessSpeedOfChange = 9999;
		}
		else
		{
			m_BrightnessSpeedOfChange = Math.AbsFloat(m_Brightness - m_BrightnessTarget) / time_in_s;
		}
	}
	
	//! Sets the radius of the light
	void SetRadiusTo(float value)
	{
		m_Radius = value;
		m_RadiusTarget = value;
		SetRadius(m_Radius);
	}
	
	//! Fades the radius of the light to the given value.
	void FadeRadiusTo( float value, float time_in_s )
	{
		m_RadiusTarget = value;
		
		if (time_in_s == 0)
		{
			m_RadiusSpeedOfChange = 9999;
		}
		else
		{
			m_RadiusSpeedOfChange = Math.AbsFloat(m_Radius - m_RadiusTarget) / time_in_s;
		}
	}
	
	//! Switches off the light and deletes it from memory
	void Destroy()
	{
		ClearEventMask(EntityEvent.FRAME);
		SetEnabled(false);
		if (m_Parent)
			DeleteLightWithDelay();
		else
			DeleteLightNow();
	}
	
	//! Makes the light destroy itself after the given time in seconds. The light will fade out if it's set to do so with SetFadeOutTime(...)
	void SetLifetime(float life_in_s)
	{
		if(GetGame())
			m_LifetimeEnd = GetGame().GetTime() + life_in_s * 1000;
	}
	
	//! Sets the fade out time in seconds. Fade out begins automatically as the light nears the end of its life time, or when method FadeOut() is called.
	void SetFadeOutTime(float time_in_s)
	{
		m_FadeOutTime = time_in_s * 1000;
	}
	
	//! Starts the fade out process and destroys the light when its done. Optional parameter allows you to set time of this fade out in seconds. If not set, then default value (from SetFadeOutTime(...)) is used.
	void FadeOut(float time_in_s = -1)
	{
		float time_in_ms = time_in_s * 1000;
		
		if (time_in_s == -1)
		{
			float kill_time_in_s = m_FadeOutTime*0.001;
			
			FadeBrightnessTo(0, kill_time_in_s);
			FadeRadiusTo(0, kill_time_in_s);
			SetLifetime(kill_time_in_s);
		}
		else
		{
			FadeBrightnessTo(0, time_in_s);
			FadeRadiusTo(0, time_in_s);
			SetLifetime(time_in_s);
		}
	}
	
	//! Makes the light fade into existence. Works only at the moment the light is created. Consider using FadeBrightnessTo(...) and FadeRadiusTo(...) at anytime later during lifetime.
	void FadeIn(float time_in_s)
	{
		float brightness = m_Brightness;
		SetBrightnessTo(0);
		FadeBrightnessTo(brightness, time_in_s);
	}
	
	//! Prolongs the lifetime of the light in seconds. Use negative number to shorten its lifetime.
	void AddLifetime(float life_in_s)
	{
		m_LifetimeEnd += life_in_s * 1000;
	}
	
	//! Override this for custom functionality
	void OnFrameLightSource(IEntity other, float timeSlice)
	{
		// ... 
	}
	
	//! On frame event. If you want to control your light within your own rules then override the event OnFrameLightSource and put your code there.
	override void EOnFrame(IEntity other, float timeSlice)
	{
		// Control lifetime of the light
		int current_time = GetGame().GetTime();
		
		if ( CheckLifetime(current_time) )
		{
			SetRadius(m_Radius);
		}
		else
		{
			return;
		}
		
		HandleFlickering(current_time - m_LifetimeStart, timeSlice);
		HandleDancingShadows(current_time - m_LifetimeStart, timeSlice);
		CheckFadeOut(current_time);
		HandleBrightnessFadeing(timeSlice);
		HandleRadiusFadeing(timeSlice);
		
		if (m_LightDimming)
			m_LightDimming.HandleDimming(timeSlice);
		
		CheckIfParentIsInCargo();
		TryShadowOptimization();
		OnFrameLightSource(other, timeSlice);
		
		HandleBlinking(current_time);
	}
	
	//! Sets the maximum range of the point light within the dancing shadows effect
	void SetDancingShadowsAmplitude(float max_deviation_in_meters)
	{
		m_DancingShadowsAmplitude = Math.AbsFloat(max_deviation_in_meters);
	}
	
	//! Sets the maximum speed of the point light within the dancing shadows effect
	void SetDancingShadowsMovementSpeed(float speed_in_meters_per_frame)
	{
		m_DancingShadowsSpeed = Math.AbsFloat(speed_in_meters_per_frame);
	}
	
	//! Returns max movement range of pointlight within the dancing shadow effect
	float GetDancingShadowsAmplitude()
	{
		return m_DancingShadowsAmplitude;
	}
	
	//! Returns max movement speed of pointlight within the dancing shadow effect
	float GetDancingShadowsMovementSpeed()
	{
		return m_DancingShadowsSpeed;
	}
	
	// Object and hidden selection ID which we can link to light source object
	void SetSelectionID(int id)
	{
		m_HiddenSelectionID = id;
	}
	
	// Update linked source object's material
	void UpdateLightSourceMaterial(string path)
	{
		EntityAI parent = EntityAI.Cast(m_Parent);
		if (parent)
			parent.SetObjectMaterial(m_HiddenSelectionID, path);
	}
	
	//! Enables some debug functionality of this light
	void EnableDebug(bool state)
	{
		m_IsDebugEnabled = state;
	}
	
	// Handles subtle movement of the light point to create the effect of dancing shadows
	void HandleDancingShadows(float time, float timeSlice)
	{
		if (m_DancingShadowsAmplitude > 0)
		{
			for (int i = 0; i < 3; i++ )
			{
				m_DancingShadowsLocalPos[i] = m_DancingShadowsLocalPos[i] +  ( Math.RandomFloat(-m_DancingShadowsSpeed,m_DancingShadowsSpeed) * timeSlice) ;
				
				if (m_DancingShadowsLocalPos[i] > m_DancingShadowsAmplitude)
					m_DancingShadowsLocalPos[i] = m_DancingShadowsAmplitude;
				
				if (m_DancingShadowsLocalPos[i] < -m_DancingShadowsAmplitude)
					m_DancingShadowsLocalPos[i] = -m_DancingShadowsAmplitude;
				
			}
			
			if (m_Parent  &&  !m_Parent.ToDelete())
			{
				// In order to move with the light, it must be detached from its parent first
				
				m_Parent.RemoveChild(this);
				SetPosition(m_LocalPos + m_DancingShadowsLocalPos);
				
				m_Parent.AddChild(this, -1);
				m_Parent.Update();
			}
			
			if (m_IsDebugEnabled)
			{
				Particle p = ParticleManager.GetInstance().PlayInWorld( ParticleList.DEBUG_DOT, GetPosition() );
				p.SetParticleParam( EmitorParam.SIZE, 0.01);
			}
		}
		else
		{
			m_DancingShadowsLocalPos = Vector(0,0,0);
		}
	}
	
	// Updates flickering light
	void HandleFlickering(float time, float timeSlice)
	{
		if (m_BrightnessPulseAmplitudeMax > 0)
		{
			m_BrightnessPulse += ( Math.RandomFloat(-m_BrightnessPulseSpeed, m_BrightnessPulseSpeed) ) * timeSlice;
			
			if (m_BrightnessPulse < m_BrightnessPulseAmplitudeMin + 1)
				m_BrightnessPulse = m_BrightnessPulseAmplitudeMin + 1;
			
			if (m_BrightnessPulse > m_BrightnessPulseAmplitudeMax + 1)
				m_BrightnessPulse = m_BrightnessPulseAmplitudeMax + 1;
			
		}
		else
		{
			m_BrightnessPulse = 1;
		}
	}
	
	//! Sets speed of light flickering (random brightness coefficient change per second)
	void SetFlickerSpeed(float speed)
	{
		m_BrightnessPulseSpeed = speed;
	}
	
	//! Sets the change coefficient of flickering light. (0.0 - 1.0 values, result of greater values are period time of light off )
	void SetFlickerAmplitude(float coef)
	{
		m_BrightnessPulseAmplitudeMax = Math.AbsFloat(coef);
		m_BrightnessPulseAmplitudeMin = -Math.AbsFloat(coef);
	}
	
	void SetFlickerAmplitudeMax(float coef)
	{
		m_BrightnessPulseAmplitudeMax = coef;
	}
	
	void SetFlickerAmplitudeMin(float coef)
	{
		m_BrightnessPulseAmplitudeMin = coef;
	}
	
	//! Returns flicker speed
	float GetFlickerSpeed()
	{
		return m_BrightnessPulseSpeed;
	}
	
	//! Returns flicker amplitude maximum
	float GetFlickerAmplitudeCoefMax()
	{
		return m_BrightnessPulseAmplitudeMax;
	}
	
	//! Returns flicker amplitude minimum
	float GetFlickerAmplitudeCoefMin()
	{
		return m_BrightnessPulseAmplitudeMin;
	}
	
	//! Optimizes shadows by disabling them on this light source while it's within the given radius around the camera.
	void TryShadowOptimization()
	{
		if (m_OptimizeShadowsRadius > 0)
		{
			float distance_to_camera = vector.Distance( GetPosition(), GetGame().GetCurrentCameraPosition() );
			
			if (distance_to_camera < m_OptimizeShadowsRadius)
			{
				SetCastShadow(false);
			}
			else
			{
				SetCastShadow(true);
			}
		}
	}
	
	//! When the light source gets within this radius (radius_in_m) around the camera, then it's shadows are disabled.
	void SetDisableShadowsWithinRadius(float radius_in_m)
	{
		m_OptimizeShadowsRadius = radius_in_m;
	}
	
	//! Returns the range you put inside SetDisableShadowsWithinRadius(...)
	float GetDisableShadowsWithinRadius()
	{
		return m_OptimizeShadowsRadius;
	}
	
	void CheckIfParentIsInCargo()
	{
		// TO DO: OPTIMIZE AND REFACTOR! THIS MUST BE HANDLED IN AN EVENT, NOT PER FRAME!
		
		if (m_Parent)
		{
			EntityAI parent_EAI = EntityAI.Cast( m_Parent );
			
			if (parent_EAI) // Check if the Cast was successfull
			{
				GameInventory GI = parent_EAI.GetInventory();
				
				if (GI) // Prevents handling of light on the parent item when it's projected in inventory as the item in inventory character's hands.
				{
					bool is_in_cargo = GI.IsInCargo();
					
					if (!is_in_cargo)
					{
						EntityAI parent2 = parent_EAI.GetHierarchyParent();
						
						if (parent2  &&  parent2.GetInventory())
						{
							is_in_cargo = parent2.GetInventory().IsInCargo();
						}
					}
					
					if ( is_in_cargo )
					{
						SetEnabled(false);
					}
					else
					{
						SetEnabled(true);
					}
				}
			}
		}
	}
	
	// Destroys this light if it's past it lifetime
	private bool CheckLifetime(int current_time)
	{
		if ( current_time > m_LifetimeEnd  &&  m_LifetimeEnd != -1 )
		{
			Destroy();
			return false;
		}
		
		return true;
	}
	
	// Handles fade out effect at the end of lifetime
	private void CheckFadeOut( int current_time)
	{
		// Control fade out of the light
		if ( m_FadeOutTime != -1  &&  m_LifetimeEnd != -1  &&  current_time > m_LifetimeEnd - m_FadeOutTime )
		{
			m_FadeOutTime = -1;
			m_FadeInTime = -1; // Stop fade in process
			float time_left_in_s = (m_LifetimeEnd - current_time) * 0.001;
			FadeBrightnessTo(0, time_left_in_s);
			FadeRadiusTo(0, time_left_in_s);
		}
	}
	
	// handles fading of brightness
	private void HandleBrightnessFadeing(float timeSlice)
	{
		if ( m_Brightness != m_BrightnessTarget )
		{
			float brightness_difference = m_Brightness - m_BrightnessTarget;
			
			if (brightness_difference > m_BrightnessSpeedOfChange*timeSlice)
				brightness_difference = m_BrightnessSpeedOfChange*timeSlice;
			
			if (brightness_difference < -m_BrightnessSpeedOfChange*timeSlice)
				brightness_difference = -m_BrightnessSpeedOfChange*timeSlice;
			
			m_Brightness -= brightness_difference;
			
			if ( m_Brightness > 0  ||  m_BrightnessTarget > 0)
			{
				SetBrightness(m_Brightness * m_BrightnessPulse);
				CorrectLightPulseDuringDaylight();
			}
			else
			{
				Destroy();
				return;
			}
		}
		else
		{
			SetBrightness(m_Brightness * m_BrightnessPulse);
			CorrectLightPulseDuringDaylight();
		}
	}
	
	// handles fading of radius
	private void HandleRadiusFadeing(float timeSlice)
	{
		if ( m_Radius != m_RadiusTarget )
		{
			float radius_difference = m_Radius - m_RadiusTarget;
			
			if (radius_difference > m_RadiusSpeedOfChange*timeSlice)
				radius_difference = m_RadiusSpeedOfChange*timeSlice;
			
			if (radius_difference < -m_RadiusSpeedOfChange*timeSlice)
				radius_difference = -m_RadiusSpeedOfChange*timeSlice;
			
			m_Radius -= radius_difference;
			
			if ( m_Radius > 0  ||  m_RadiusTarget > 0)
			{
				SetRadius(m_Radius);
			}
			else
			{
				Destroy();
				return;
			}
		}
		else
		{
			SetRadius(m_Radius);
		}
	}
	
	//! Sets blinking speed (no blinking if speed <= 0)
	void SetBlinkingSpeed(float _speed)
	{
		m_BlinkingSpeed = _speed;
	}
	
	//! Returns the speed of blinks
	float GetBlinkingSpeed()
	{
		return m_BlinkingSpeed;
	}
	
	// handles blinking. Turns light  on and off on regular intervals
	private void HandleBlinking(float time)
	{
		if ( m_BlinkingSpeed <= 0 )
			return;
		
		float multiplier;

		multiplier = Math.Sin(time * 0.001 * m_BlinkingSpeed);		// Oscillate the multiplier overtime (time normalized to sec)
		multiplier = (multiplier + 1)/2;							// Normalized the value to 0-1 
		
		multiplier = Math.Round(multiplier);						// Rounding to 0 or 1 to make it blink instantly	
		SetBrightness(m_Brightness * multiplier);
	}
	
	// Dimming
	void EnableDimming(float baseBrightness, DimmingConfig dimCfg)
	{
		if (!m_LightDimming)
			m_LightDimming = new LightDimming(this, baseBrightness, dimCfg);
	}
	
	LightDimming GetDimming()
	{
		return m_LightDimming;
	}
	
	void StopDimming()
	{
		m_LightDimming = null;
	}
};