/**
	Fire.c
	The fire effect ported from the engine. Functions to be used as documented in the docs.
	
	@author Zapper, Maikel		
*/

static const FIRE_LIGHT_COLOR = 0xc86400;

// fire drawing modes
static const C4Fx_FireMode_Default   = 0; // determine mode by category
static const C4Fx_FireMode_LivingVeg = 2; // C4D_Living and C4D_StaticBack
static const C4Fx_FireMode_StructVeh = 1; // C4D_Structure and C4D_Vehicle
static const C4Fx_FireMode_Object    = 3; // other (C4D_Object and no bit set (magic))
static const C4Fx_FireMode_Last      = 3; // largest valid fire mode


// Returns whether the object is burning.
// documented in /docs/sdk/script/fn
global func OnFire()
{
	if (!this)
		return false;
	var effect;
	if (!(effect=GetEffect("Fire", this)))
		return false;
	return effect.strength;
}

// Extinguishes the calling object with specified strength.
// documented in /docs/sdk/script/fn
global func Extinguish(strength /* strength between 0 and 100 */)
{
	if (!this)
		return false;
	if (strength == nil)
		strength = 100;
	else if (!strength)
		return false;
	
	var effect = GetEffect("Fire", this);
	if (!effect)
		return false;
	effect.strength = BoundBy(effect.strength - strength, 0, 100);
	if (effect.strength == 0)
		RemoveEffect(nil, this, effect);
	return true;
}

// Incinerates the calling object with specified strength.
// documented in /docs/sdk/script/fn
global func Incinerate(
	strength /* strength between 0 and 100 */
	, int caused_by /* the player that caused the incineration */
	, blasted /* whether the object was incinerated by an explosion */
	, incinerating_object /* the object that caused the incineration */)
{
	if (!this)
		return false;
	if (strength == nil)
		strength = 100;
	else if (!strength)
		return false;
	
	var effect = GetEffect("Fire", this);
	if (effect) 
	{
		effect.strength = BoundBy(effect.strength + strength, 0, 100);
		return true;
	}
	else
	{
		effect = AddEffect("Fire", this, 100, 4, this, nil, caused_by, !!blasted, incinerating_object, strength);
		return !!effect;
	}
}

// Called if the object is submerged in incendiary material (for example in lava).
global func OnInIncendiaryMaterial()
{
	if (this->IsInFireproofContainer())
		return;
	this->DoEnergy(-7, false, FX_Call_EngFire, NO_OWNER);
	// The object might have removed itself.
	if (!this) return true;
	return this->Incinerate(15, NO_OWNER);
}

global func IsInFireproofContainer()
{
	if (!this) return false;
	if (this->Contained())
	{
		if (this->Contained().FireproofContainer)
			return true;
		return this->Contained()->IsInFireproofContainer();
	}
	return false;
}

// Makes the calling object non flammable.
global func MakeNonFlammable()
{
	if (!this) 
		return;
	return AddEffect("IntNonFlammable", this, 300);
}

/*-- Fire Effects --*/

global func FxIntNonFlammableEffect(string new_name)
{
	// Block fire effects.
	if (WildcardMatch(new_name, "*Fire*")) 
		return -1;
	// All other effects are okay.
	return 0;
}


global func FxFireStart(object target, proplist effect, int temp, int caused_by, bool blasted, object incinerating_object, strength)
{
	// safety
	if (!target) return -1;
	// temp readd
	if (temp) return 1;
	// fail if already on fire
	//if (target->OnFire()) return -1;
	// Fail if target is dead and has NoBurnDecay, to prevent eternal fires.
	if (target->GetCategory() & C4D_Living && !target->GetAlive() && target.NoBurnDecay)
		return -1;
	
	// structures must eject contents now, because DoCon is not guaranteed to be executed!
	// In extinguishing material
	var fire_caused = true;
	var burn_to = target.BurnTo;
	var mat;
	if (MaterialName(mat = GetMaterial(target->GetX(), target->GetY())))
		if (GetMaterialVal("Extinguisher", "[Material]", mat))
		{
			// blasts should changedef in water, too!
			if (blasted) 
				if (burn_to != nil)
					target->ChangeDef(burn_to);
			// no fire caused
			fire_caused = false;
		}
	// BurnTurnTo
	if (fire_caused) 
		if (burn_to != nil)
			target->ChangeDef(burn_to);
	
	// eject contents
	var obj;
	if (!target->GetDefCoreVal("IncompleteActivity", "DefCore") && !target.NoBurnDecay)
	{
		for (var i = target->ContentsCount(); obj = target->Contents(--i);)
		{
			if (target->Contained()) 
				obj->Enter(target->Contained());
			else
				obj->Exit();
		}		
		// Detach attached objects
		for (obj in FindObjects(Find_ActionTarget(target)))
		{
			if (obj->GetProcedure() == "ATTACH")
				obj->SetAction(nil);
		}
	}
	
	// fire caused?
	if (!fire_caused)
	{
		// if object was blasted but not incinerated (i.e., inside extinguisher), do a script callback
		if (blasted)
			target->~IncinerationEx(caused_by);
		return -1;
	}
	// determine fire appearance
	var fire_mode;
	if (!(fire_mode = target->~GetFireMode()))
	{
		// set default fire modes
		var cat = target->GetCategory();
		if (cat & (C4D_Living | C4D_StaticBack)) // Tiere, Bäume
			fire_mode = C4Fx_FireMode_LivingVeg;
		else if (cat & (C4D_Structure | C4D_Vehicle)) // Gebäude und Fahrzeuge sind unten meist kantig
			fire_mode = C4Fx_FireMode_StructVeh;
		else
			fire_mode = C4Fx_FireMode_Object;
	}
	else if (!Inside(fire_mode, 1, C4Fx_FireMode_Last))
	{
		DebugLog("Warning: FireMode %d of object %s (%i) is invalid!", fire_mode, target->GetName(), target->GetID());
		fire_mode = C4Fx_FireMode_Object;
	}
	// store causes in effect vars
	effect.strength = strength;
	effect.mode = fire_mode;
	effect.caused_by = caused_by; // used in C4Object::GetFireCause and timer! <- fixme?
	effect.blasted = blasted;
	effect.incinerating_obj = incinerating_object;
	effect.no_burn_decay = target.NoBurnDecay;
	
	// store fire particles
	effect.smoke =
	{
		R = 255,
		G = 255,
		B = 255,
		CollisionVertex = 1000,
		OnCollision = PC_Stop(),
		ForceX = PV_Wind(50),
		DampingX = PV_Linear(900,999),
		ForceY = -7,
		DampingY = PV_Linear(1000,PV_Random(650,750)),
		Rotation = PV_Random(0, 359),
		Phase = PV_Random(0, 3)
	};
	effect.chaoticspark =
	{
		Size = PV_Linear(1, 0),
		ForceX = PV_KeyFrames(10, 0, PV_Random(-6, 6), 333, PV_Random(-6, -6), 666, PV_Random(6, 6), 1000, PV_Random(-6, 6)),
		ForceY = PV_KeyFrames(10, 0, PV_Random(-8, 5), 333, PV_Random(-8, 5), 666, PV_Random(-10, 10), 1000, PV_Random(-10, 15)),
		Stretch = PV_Speed(1000, 500),
		Rotation = PV_Direction(),
		CollisionVertex = 0,
		OnCollision = PC_Die(),
		R = 255,
		G = PV_Linear(255,200),
		B = PV_Random(0, 100),
		DampingX=950,
		DampingY=950,
		Alpha = PV_Random(40,140),
		BlitMode = GFX_BLIT_Additive
	};
	effect.redfiredense =
	{
		R = PV_Random(0, 255),
		G = 0,
		B = 0,
		Alpha = 120,
		Rotation = PV_Random(0, 359),
		Phase = PV_Random(0, 3),
		OnCollision = 0
	};
	effect.flameborder =
	{
		R = 255,
		G = PV_Linear(255,0),
		B = 0,
		Phase = PV_Random(0, 4),
		OnCollision = PC_Die(),
		OnCollision = 0,
		Alpha = PV_KeyFrames(0, 0, 0, 100, 80, 600, 80, 1000, 1)
	};
	effect.sharpflame =
	{
		R = 255,
		G = PV_KeyFrames(0, 0, 255, 666, 0, 627, 0),
		B = 0,
		Rotation = PV_Random(-5, 5),
		Phase = PV_Random(0, 5),
		OnCollision = 0,
		BlitMode = GFX_BLIT_Additive
	};
	
	// Optimization: Only for living beings the flame actually follows the object. That means for all other objects, the flames can be rendered as one batch.
	if (target->GetOCF() & OCF_Alive)
	{
		effect.sharpflame.Attach = ATTACH_MoveRelative;
		effect.flameborder.Attach = ATTACH_MoveRelative;
	}
	
	// Prepare some more values.
	EffectCall(target, effect, "UpdateEffectProperties");
		
	// Reduce checks, smoke and a few other particles if there are other burning objects around.
	effect.FreqReduction = 1;
	for (var nearby in FindObjects (Find_Distance(effect.height)))
		if (nearby->OnFire())
			++effect.FreqReduction;
	effect.FreqReduction = effect.Interval * BoundBy(effect.FreqReduction / 3, 1, 6);

	// Set values
	if ((4 * effect.width * effect.height) > 500)
		target->Sound("Fire::Inflame", false, 100);
	if (target->GetMass() >= 100)
		if (target->Sound("Fire::Fire", false, 100, nil, 1))
			effect.fire_sound = true;
	
	// callback
	target->~Incineration(effect.caused_by);
	
	// Done, success
	return FX_OK;
}


global func FxFireUpdateEffectProperties(object target, proplist effect)
{
	effect.width = target->GetObjWidth() / 2;
	effect.height = target->GetObjHeight() / 2;
	
	effect.fire_width = effect.width * effect.strength / 100 + 1;
	effect.fire_height = effect.height * effect.strength / 100 + 2;
	
	effect.pspeed = -effect.fire_height/2;
	
	effect.redfiredense.Size = PV_KeyFrames(0, 0, 1, 500, PV_Random(effect.fire_width/10, effect.fire_width/6), 1000, 1);
	
	var smokesize = effect.fire_width * 2;
	if (smokesize > 60) smokesize = 50;
	effect.smoke.Size = PV_Linear(smokesize, smokesize * 5);
	effect.smoke.Alpha = PV_KeyFrames(0, 0, 1, 50, effect.strength, 1000, 0);
	
	var stretch = 500 * effect.fire_height / effect.fire_width;
	effect.maxhgt = effect.fire_height - effect.fire_width * stretch / 1500;
	var sharpbottom = 0;
	if (GetCategory() & C4D_Structure || GetCategory() & C4D_Vehicle) sharpbottom++;
	
	effect.flameborder.Stretch = stretch;
	effect.flameborder.Size = PV_KeyFrames(0, 0, effect.fire_width * (1 + sharpbottom), 500, effect.fire_width * 2, 1000, effect.fire_width);
	effect.border_active = (effect.strength > 49) && (effect.fire_height > 10);
	effect.sharpflame.Size = PV_KeyFrames(0, 0, effect.fire_width * (2 + sharpbottom), 500, effect.fire_width * 3, 1000, effect.fire_width*2);
	effect.sharpflame.Alpha = PV_KeyFrames(0, 0, 0, 350, 190 * effect.strength/100, 700, 190 * effect.strength/100, 1000, 0);
	effect.sharpflame.Stretch = stretch;
}

global func FxFireTimer(object target, proplist effect, int time)
{
	// safety
	if (!target) return FX_Execute_Kill;
	
	// get cause
	//if(!GetPlayerName(effect.caused_by)) effect.caused_by=NO_OWNER;;
		
	// strength changes over time
	if ((effect.strength < 100) && (time % 8 == 0))
	{
		if (effect.strength < 50)
		{
			if (time % 8 == 0)
			{
				effect.strength = Max(effect.strength - 1, 0);
				if (effect.strength <= 0)
			 		return FX_Execute_Kill;
			}
		}
		else
		{
			effect.strength = Min(100, effect.strength + 2);
			
			if (effect.strength > 49)
			{
				effect.smoke.R = 127;
				effect.smoke.G = 127;
				effect.smoke.B = 127;
			}
		}
			
		// update particle properties that depend on the effect strength
		EffectCall(target, effect, "UpdateEffectProperties");
	}
		
	// target is in liquid?
	if (time % (20 + effect.FreqReduction) == 0)
	{	
		// Extinguish when in water-like materials.
		var mat;
		if (mat = GetMaterial())
		{
			if (GetMaterialVal("Extinguisher", "Material", mat))
			{
				var steam =
				{
					Size = PV_Linear(effect.width*2, effect.width*4),
					Alpha = PV_Linear(87,0),
					R = 255,
					G = 255,
					B = 255,
					Rotation = PV_Random(0, 359)
				};
				CreateParticle("Dust", 0, -5, 0, 0, 180, steam, 2);
				return FX_Execute_Kill;
			}
		}
		
		// Incinerate landscape if possible.
		target->IncinerateLandscape(0, 0, effect.caused_by);
	
		// check spreading of fire
		if (effect.strength > 10)
		{
			var container = target->Contained(), inc_objs;
			// If contained do not incinerate anything. This would mostly affect inventory items and burns the Clonk too easily while lacking feedback.
			if (container)
			{
				inc_objs = [];
			}
			// Or if not contained incinerate all objects outside the burning object.
			// Do not incinerate inventory objects, since this, too, would mostly affect Clonks and losing one's bow in a fight without noticing it is nothing but annoying to the player.
			else
			{
				inc_objs = FindObjects(Find_AtRect(-effect.width/2, -effect.height/2, effect.width, effect.height), Find_Exclude(target), Find_Layer(target->GetObjectLayer()), Find_NoContainer());
			}
			// Loop through the selected set of objects and check contact incinerate.
			for (var obj in inc_objs)
			{
				// Check if the obj still exists, an object could be have been removed in this loop.
				if (!obj)
					continue;
				
				if (obj->GetCategory() & C4D_Living)
					if (!obj->GetAlive()) 
						continue;
				
				var inf = obj.ContactIncinerate;
				if (!inf)
					continue;
				var amount = (effect.strength / 3) / Max(1, inf);
	
				if (!amount)
					continue;
					
				var old_fire_value = obj->OnFire();
				if(old_fire_value < 100) // only if the object was not already fully ablaze
				{
					// incinerate the other object a bit
					obj->Incinerate(Max(10, amount), effect.caused_by, false, effect.incinerating_obj);
				
					// Incinerating other objects weakens the own fire.
					// This is done in order to make fires spread slower especially as a chain reaction.
					var min = Min(10, effect.strength);
					if(effect.strength > 50) min = 50;
					effect.strength = BoundBy(effect.strength - amount/2, min, 100);
				}
			}
		}
	}
	
	// causes on object
	//target->ExecFire(effect_number, caused_by);
	if (target->GetAlive())
	{
		target->DoEnergy(-effect.strength*4, true, FX_Call_EngFire, effect.caused_by); 
	}
	else 
	{
		if ((time*10) % 120 <= effect.strength)
		{
			target->DoDamage(2, FX_Call_DmgFire, effect.caused_by);
			if (target && !Random(2) && !effect.no_burn_decay)
			{
				target->DoCon(-1);
				if (target)
					EffectCall(target, effect, "UpdateEffectProperties");
			}
		} 
	}
	if (!target) 
		return FX_Execute_Kill;
	if (target->Contained()) 
		return FX_OK;

	// Fire particles
	// Fire denses, smoke, chaotic sparks
	if (time % effect.FreqReduction == 0)
	{
		// A few red fire particles popping up in the higher area. Does not affect very small flames.
		if (effect.fire_width + effect.fire_height > 40)
		{
			if (!Random(2)) CreateParticle("FireDense", PV_Random(-effect.fire_width, effect.pspeed), PV_Random(effect.pspeed, effect.fire_height), PV_Random(-3,3), effect.pspeed, effect.fire_height/2+6, effect.redfiredense);
			if (!Random(2)) CreateParticle("FireDense", PV_Random(effect.fire_width/2, effect.fire_width), PV_Random(effect.pspeed, effect.fire_height), PV_Random(-3,3), effect.pspeed, effect.fire_height/2+6, effect.redfiredense);
		}

		// Smoke
		CreateParticle("SmokeThick", PV_Random(-effect.fire_width,effect.fire_width), -effect.fire_height, 0, -6, 300, effect.smoke);

		// Chaotic particles
		if (!Random(3) && effect.FreqReduction < 14)
			CreateParticle("Magic", PV_Random(-effect.fire_width, effect.fire_width), PV_Random(-effect.fire_height, effect.fire_height), PV_Random(25, -25), PV_Random(-25, 12), 50, effect.chaoticspark);
		
		// Flameborders and sharp flames

		// Flame borders
		if (effect.border_active)
		{
			CreateParticle("FireBorder", 0, effect.maxhgt, 0, effect.pspeed, 30, effect.flameborder);
		}
		
		// Sharp flames
		CreateParticle("FireSharp", 0, effect.maxhgt, 0, effect.pspeed, 30, effect.sharpflame);
	}

	return FX_OK;
}

global func FxFireStop(object target, proplist effect, int reason, bool temp)
{
	// safety
	if (!target) 
		return false;
	// only if real removal is done
	if (temp)
	{
		return true;
	}
	// stop sound
	if (effect.fire_sound)
		target->Sound("Fire::Fire", false, 100, nil, -1);
	// callback
	target->~Extinguishing();
	// done, success
	return true;
}

global func FxFireInfo(object target,  effect)
{
	return Format("Is on %d%% fire.", effect.strength); // todo: <--
}
