library Units initializer ini
	import Table
	import Players
	import DummyUnitStack

	native UnitAlive takes unit u returns boolean

	struct LightningBallDat
		unit u
		effect fx
		thistype prev = null
		thistype next = null
	end

	struct BeastLightningDat
		unit u
		integer ticksUntilNextCheck
		integer balls
		real phase = 0.
		LightningBallDat ballHead = null
		integer typ
		thistype prev = null
		thistype next = null
	end

	public struct Units
		private static constant string LIGHTNING_BALL_FX = "Abilities\\Weapons\\FarseerMissile\\FarseerMissile.mdl"

		private static constant real CLOCK_PERIOD           =   1. / 30.
		private static constant real LIGHTNING_CHECK_PERIOD =   5.
		private static constant real BALL_OFF           = 64.
		private static constant real PHASE_ANGULAR_VELOCITY = bj_PI / 8. * CLOCK_PERIOD
		private static constant real EFFECT_SCALE           =   .5
		private static constant real EFFECT_HEIGHT          = 60.

		private static constant integer BLINK_PASSIVE_ID    = 'A00E'
		private static constant integer BLINK_ID            = 'A001'
		private static constant integer HEMISECT_PASSIVE_ID = 'A00H'
		private static constant integer HEMISECT_ID         = 'A00G'
		private static constant integer CONCAT_PASSIVE_ID   = 'A00J'
		private static constant integer CONCAT_ID           = 'A00K'

		static constant integer HUNTER_ID 			 = 'H00L'
		static constant integer BEAST_ID    		 = 'H00M'
		static constant integer LESSER_BEAST_ID      = 'H001'
		static constant integer HUNTER_PROJECTILE_ID = 'h000'
		static constant integer ARTIFACT_ID          = 'I000'
		static constant integer GAS_ACTION           = 'I003'

		static constant integer INITIAL_BALLS        = 0
		static constant integer MAX_BALLS			 = 5
		static constant integer MAX_BALLS_LESSER     = 3


		private static group grp = CreateGroup()
		private static timer clock = CreateTimer()
		private static BeastLightningDat beastHead = null
		private static Table fromBeast = new Table()

		static unit array hero
		static unit array subHero

		static Table thunderCd    = new Table() // cooldown for AI
		static Table thunderClock = new Table() // clock for max duration
		static Table beastState   = new Table()
		static Table sitePref     = new Table() // A/B preference for Hunter AI

		static item artifact
		static item artifact2

		private static method pushBall takes unit u returns nothing
			string fxString = LIGHTNING_BALL_FX
			BeastLightningDat ua = fromBeast.loadInt(u.getHandleId()) castTo BeastLightningDat
			LightningBallDat  lbd = new LightningBallDat
			LightningBallDat  swap

			lbd.u = DummyUnitStack.get()
			SetUnitFlyHeight(lbd.u, EFFECT_HEIGHT, 0.)
			SetUnitScale(lbd.u, EFFECT_SCALE, EFFECT_SCALE, EFFECT_SCALE)

			// risky removal of effect
			if Players.locl != GetOwningPlayer(u) then
				fxString = ""
			end
			lbd.fx = AddSpecialEffectTarget(fxString, lbd.u, "origin")
			ua.balls++

			if ua.balls == 1 then

				// beast received its first ball
				UnitRemoveAbility(ua.u, BLINK_PASSIVE_ID)
				UnitAddAbility(   ua.u, BLINK_ID)

			// beast received its third ball
			elseif ua.balls == 3 and ua.typ == BEAST_ID then
				UnitRemoveAbility(ua.u, HEMISECT_PASSIVE_ID)
				UnitAddAbility(   ua.u, HEMISECT_ID)
			elseif ua.balls == 3 and ua.typ == LESSER_BEAST_ID then
				UnitRemoveAbility(ua.u, CONCAT_PASSIVE_ID)
				UnitAddAbility(   ua.u, CONCAT_ID)
			end

			if (ua.ballHead castTo integer) == 0 then
				ua.ballHead = lbd
			else
				swap = ua.ballHead
				swap.prev = lbd
				lbd.next = swap
				ua.ballHead = lbd
			end
		end

		static method countBalls takes unit u returns integer
			BeastLightningDat ua
			if fromBeast.hasInt(u.getHandleId()) then
				ua = fromBeast.loadInt(u.getHandleId()) castTo BeastLightningDat
				return ua.balls
			end
			debug BJDebugMsg("Units.countBalls: Tried to count balls on unindexed unit")
			return -1
		end

		static method dischargeBall takes unit u returns nothing
			BeastLightningDat ua  = fromBeast.loadInt(u.getHandleId()) castTo BeastLightningDat
			LightningBallDat  lbd = ua.ballHead
			LightningBallDat  swap

			DestroyEffect(lbd.fx)
			DummyUnitStack.release(lbd.u)
			swap = lbd.next
			destroy lbd
			ua.ballHead = swap
			ua.balls--

			// beast lost its third ball
			if ua.balls == 2 and ua.typ == BEAST_ID then
				UnitRemoveAbility(ua.u, HEMISECT_ID)
				UnitAddAbility(   ua.u, HEMISECT_PASSIVE_ID)
			elseif ua.balls == 2 and ua.typ == LESSER_BEAST_ID then
				UnitRemoveAbility(ua.u, CONCAT_ID)
				UnitAddAbility(   ua.u, CONCAT_PASSIVE_ID)
			end

			if (swap castTo int) == 0 then

				// beast no longer has any lightning balls
				UnitRemoveAbility(ua.u, BLINK_ID)
				UnitAddAbility(   ua.u, BLINK_PASSIVE_ID)
			else
				swap.prev = null
			end
		end

		private static method beastLightningTick takes nothing returns nothing
			BeastLightningDat ua = beastHead
			BeastLightningDat uaSwap
			LightningBallDat lbd
			LightningBallDat swap
			integer index
			real facing

			loop
				exitwhen ua == (null)
				ua.ticksUntilNextCheck--
				facing = GetUnitFacing(ua.u) * bj_DEGTORAD
				ua.phase = ua.phase + PHASE_ANGULAR_VELOCITY

				if ua.ticksUntilNextCheck <= 0 then

					// push lightning ball if applicable
					if ua.balls < MAX_BALLS and ua.typ == BEAST_ID then
						pushBall(ua.u)
					elseif ua.balls < MAX_BALLS_LESSER and ua.typ == LESSER_BEAST_ID then
						pushBall(ua.u)
					end

					ua.ticksUntilNextCheck = R2I(LIGHTNING_CHECK_PERIOD / CLOCK_PERIOD)
				end

				// update all ball locations
				index = 0
				lbd = ua.ballHead
				loop
					exitwhen index >= ua.balls

					SetUnitX(lbd.u, GetUnitX(ua.u) + BALL_OFF* Cos(facing + ua.phase + index * 2. * bj_PI / ua.balls))
					SetUnitY(lbd.u, GetUnitY(ua.u) + BALL_OFF* Sin(facing + ua.phase + index * 2. * bj_PI / ua.balls))

					lbd = lbd.next
					index++
				end

				// check for destruct
				if not (UnitAlive(ua.u) and GetUnitTypeId(ua.u) > 0) then

					// destroy all balls
					lbd = ua.ballHead
					loop
						exitwhen lbd == null
						swap = lbd.next
						DestroyEffect(lbd.fx)
						DummyUnitStack.release(lbd.u)
						destroy lbd
						lbd = swap
					end

					// destroy self, re-linking the list
					if ua.prev != null then
						uaSwap = ua.prev
						uaSwap.next = ua.next
					else
						beastHead = ua.next
						if ua.next == null then
							PauseTimer(clock)
						end
					end
					if ua.next != null then
						uaSwap = ua.next
						uaSwap.prev = ua.prev
					end
					destroy ua
				end

				ua = ua.next
			end
		end

		static method initializeBeast takes unit u returns nothing
			BeastLightningDat ua = new BeastLightningDat
			BeastLightningDat swap
			integer index = 0
			integer id = GetUnitTypeId(u)

			ua.typ = id

			ua.u = u
			ua.ticksUntilNextCheck = R2I(LIGHTNING_CHECK_PERIOD / CLOCK_PERIOD)
			ua.balls = 0

			loop
				exitwhen index >= INITIAL_BALLS
				pushBall(u)
				index++
			end

			fromBeast.saveInt(u.getHandleId(), ua castTo int)

			// push dat
			if beastHead == null then
				beastHead = ua
				TimerStart(clock, CLOCK_PERIOD, true, function beastLightningTick)
			else
				swap = beastHead
				swap.prev = ua
				ua.next   = swap
				beastHead = ua
			end
		end

		static method initializeBeastEx takes unit u, integer balls returns nothing
			BeastLightningDat ua = new BeastLightningDat
			BeastLightningDat swap
			integer index = 0
			integer id = GetUnitTypeId(u)

			ua.typ = id

			ua.u = u
			ua.ticksUntilNextCheck = R2I(LIGHTNING_CHECK_PERIOD / CLOCK_PERIOD)
			ua.balls = 0

			fromBeast.saveInt(u.getHandleId(), ua castTo int)

			// push dat
			if beastHead == null then
				beastHead = ua
				TimerStart(clock, CLOCK_PERIOD, true, function beastLightningTick)
			else
				swap = beastHead
				swap.prev = ua
				ua.next   = swap
				beastHead = ua
			end

			loop
				exitwhen index >= balls
				pushBall(u)
				index++
			end
		end

		static method clearAll takes nothing returns nothing
			unit iter
			integer index = 0

			// enumerate by player to catch decayed heroes
			loop
				exitwhen index > Players.LAST_BEAST
				GroupEnumUnitsOfPlayer(grp, Players.fromId[index], null)
				loop
					iter = FirstOfGroup(grp)
					exitwhen iter == null
					GroupRemoveUnit(grp, iter)
					RemoveUnit(iter)
				end
				index++
			end
		end

		private static method filterItems takes nothing returns nothing
			item f = GetEnumItem()
			if GetItemTypeId(f) != ARTIFACT_ID then
				RemoveItem(f)
			end
		end

		static method clearExtraItems takes nothing returns nothing
			EnumItemsInRect(bj_mapInitialPlayableArea, null, function filterItems)
		end

		static method clearNonHunters takes nothing returns nothing
			unit iter
			integer index = 0

			// enumerate by player to catch decayed heroes
			loop
				exitwhen index > Players.LAST_BEAST
				GroupEnumUnitsOfPlayer(grp, Players.fromId[index], null)
				loop
					iter = FirstOfGroup(grp)
					exitwhen iter == null
					GroupRemoveUnit(grp, iter)

					if GetUnitTypeId(iter) != HUNTER_ID or not UnitAlive(iter) then
						RemoveUnit(iter)
					end
				end
				index++
			end
		end

		static method endRoundVfx takes nothing returns nothing
			unit iter

			// pause existing units
			GroupEnumUnitsInRect(grp, bj_mapInitialPlayableArea, null)
			loop
				iter = FirstOfGroup(grp)
				exitwhen iter == null
				if GetPlayerId(GetOwningPlayer(iter)) <= Players.LAST_BEAST then
					PauseUnit(iter, true)
					SetUnitInvulnerable(iter, true)
				end
				GroupRemoveUnit(grp, iter)
			end
		end
	end

	function ini takes nothing returns nothing
		integer index = 0

		// reset subHero array
		loop
			exitwhen index >= Players.COUNT

			Units.subHero[index] = null

			index++
		end
	end
endlibrary
