//*         API
//*     DamageType.NULLED           Value returned when DamageType cannot conclusively
//*                                     recognize damage type
//*     DamageType.ATTACK           Value returned when DamageType recognizes the
//*                                     damage type as a physical attack
//*     DamageType.SPELL            Value returned when DamageType recognizes the
//*                                     damage type as a spell
//*     DamageType.CODE             Value returned when DamageType recognizes the
//*                                     damage type as having been dealt via dealCodeDamage()
//*     DamageType.get()            Requests what type of damage was dealt in the
//*                                     scope of a StructuredDD handler
//*     DamageType.dealCodeDamage() Deals an exact amount of damage to a unit. Arguments
//*                                     are of the form (unit,unit,real) where the
//*                                     first unit deals real damage to the second
//*                                     unit.
library DamageType initializer ini
	import StructuredDD

	//* This struct contains data to deal damage to a target that would otherwise
	//* die when hit with double damage in correction of spell damage recognition.
	//* This is to counter the effect of a unit instantly healing some damage due
	//* to negative spell damage, without using any unnecessary triggers.
	struct DelayDat
		unit target
		unit source
		real damage
	end

	//* This is a struct shim which won't be instanciated; it is only useful for
	//* making the pretty API like DamageType.doSomething()
	public struct DamageType

		///< BEGIN CUSTOMIZE SECTION

		//* One limitation of the spell damage reduction ability is that if a unit
		//* is meant to receive bonus damage due to the damage table, such that the
		//* result is greater than 100% damage, the unit would incorrectly take
		//* damage capped at 100%. Using standard melee armor tables, this can only
		//* occur when a damaged unit is ethereal; a simple ethereal shim no more
		//* than 20 lines can be created, but for maps with custom damage tables,
		//* this can be a useful option. If USE_BONUS_CALCULATOR is true, units that
		//* would take greater than 100% spell damage will properly exhibit such
		//* properly. (Disable this feature to improve performance)
		private static constant boolean USE_BONUS_CALCULATOR = false

		//* Make sure this matches the rawcode/id of your version of the DamageTypeCheck
		//* ability (you must copy/paste it from the editor to your map)
		static constant integer DAMAGE_TYPE_CHECK_ID = 'A008'

		///> END CUSTOMIZE SECTION

		static constant integer NULLED = -1
		static constant integer ATTACK =  0
		static constant integer SPELL  =  1
		static constant integer CODE   =  2

		//ConvertAttackType(7) is a hidden attack type with some unique properties
		//that make it very useful for us. For more information see goo.gl/9k8tn
		private static constant attacktype ATTACK_TYPE_UNIVERSAL = ConvertAttackType(7)

		//How long to delay before dealing the second half of the "correction" damage.
		//Significant tests have shown that a delay of 0. will have no issues.
		private static constant real DELAY_AMOUNT = 0.

		private static integer lastDamageType = NULLED

		//* Use this to get damage type in a damage event handler.
		static method get takes nothing returns integer
			real sourceDamage = GetEventDamage()
			if lastDamageType == CODE then
				return CODE
			elseif sourceDamage > 0. then
				return ATTACK
			elseif sourceDamage < 0. then
				return SPELL
			end
			return NULLED
		end

		//* Use this to damage units by trigger in your map without causing an infinite
		//* loop.
		static method dealCodeDamage takes unit who, unit target, real damage returns nothing
			integer prevType = lastDamageType
			real hp = GetWidgetLife(target) - .405
			real d = damage
			lastDamageType = CODE
			if hp > d then
				SetWidgetLife(target, hp - d + .405)
				UnitDamageTarget(who, target, 0. , true, false, ATTACK_TYPE_UNIVERSAL, DAMAGE_TYPE_UNIVERSAL, null)
			else
				UnitDamageTarget(who, target, 1000000. + d, true, false, ATTACK_TYPE_UNIVERSAL, DAMAGE_TYPE_UNIVERSAL, null)

				//Also deal magic damage for the special (unmodifiable) case of ethereal
				//units.
				UnitDamageTarget(who, target, 1000000. + d, true, false, ATTACK_TYPE_MAGIC, DAMAGE_TYPE_UNIVERSAL, null)
			end

			lastDamageType = prevType
		end

		//* Auxillary function for adding the dummy ability to all units.
		static method c takes nothing returns boolean
			unit tU = GetTriggerUnit()
			UnitAddAbility(tU, DAMAGE_TYPE_CHECK_ID)
			UnitMakeAbilityPermanent(tU, true, DAMAGE_TYPE_CHECK_ID)
			return false
		end

		//* This is the function that occurs 0 seconds after damaging a unit, for the
		//* case that damaging it by twice as much at the same time would kill it
		//* unexpectedly.
		private static method after takes nothing returns nothing
			timer time = GetExpiredTimer()
			DelayDat tempDat = time.getData() castTo DelayDat

			dealCodeDamage(tempDat.source, tempDat.target, tempDat.damage)

			destroy tempDat
			DestroyTimer(time)
		end

		private static method getUnitBonusSpellResistance takes unit u returns real
			integer prevType = lastDamageType
			real life = GetWidgetLife(u)
			real scale = GetUnitState(u, UNIT_STATE_MAX_LIFE)
			SetWidgetLife(u, scale)
			lastDamageType = CODE
			UnitDamageTarget(u, u, -scale/2., false, false, null, DAMAGE_TYPE_UNIVERSAL, null)
			scale = 2.*(scale - GetWidgetLife(u))/scale
			SetWidgetLife(u, life)
			lastDamageType = prevType
			return scale
		end

		//* This is the method that will invert any negative spell damage. It MUST be
		//* StructuredDD.conditions[0] to function properly; thus, any library using a
		//* StructuredDD handler should 'requires DamageType' (!)
		static method handler takes nothing returns boolean
			timer time
			DelayDat tempDat
			real attemptedDamage=-GetEventDamage()
			unit tU
			real sampledLife
			real scale
			if get() == SPELL then
				tU = GetTriggerUnit()
				if USE_BONUS_CALCULATOR then
					scale = getUnitBonusSpellResistance(tU)
					if scale > 1. then
						attemptedDamage = attemptedDamage*(scale+1.)/2.
					end
				end
				sampledLife = GetWidgetLife(tU) - .405
				if sampledLife >= attemptedDamage and sampledLife <= 2.*attemptedDamage then
					SetWidgetLife(tU, sampledLife - attemptedDamage)
					time = CreateTimer()
					tempDat = new DelayDat
					tempDat.target = tU
					tempDat.source = GetEventDamageSource()
					tempDat.damage = attemptedDamage
					time.setData(tempDat castTo int)
					TimerStart(time, DELAY_AMOUNT, false, function after)
				else
					dealCodeDamage(GetEventDamageSource(), tU, 2.*attemptedDamage)
				end
			end

			return false
		end
	end

	function ini takes nothing returns nothing
		group grp = CreateGroup()
		region reg = CreateRegion()
		trigger addBracer = CreateTrigger()
		unit iter

		RegionAddRect(reg, bj_mapInitialPlayableArea)
		TriggerRegisterEnterRegion(addBracer, reg, null)
		TriggerAddCondition(addBracer, Condition(function DamageType.c))
		StructuredDD.addHandler(function DamageType.handler)
		GroupEnumUnitsInRect(grp, bj_mapInitialPlayableArea, null)
		loop
			iter = FirstOfGroup(grp)
			exitwhen iter == null
			UnitAddAbility(iter, DamageType.DAMAGE_TYPE_CHECK_ID)
			UnitMakeAbilityPermanent(iter, true, DamageType.DAMAGE_TYPE_CHECK_ID)
			GroupRemoveUnit(grp, iter)
		end
		DestroyGroup(grp)
	end
endlibrary
