
//*     API:
//* boolean ADD_ALL_UNITS: If enabled, a trigger turns on which automatically
//*     registers all units in the map.
//* integer BUCKET_SIZE: How many units to add to each 'bucket' - a larger
//*     bucket will have their trigger refresh less frequently but will be
//*     more computationally expensive. A good starting value is about 20.
//* real PER_CLEANUP_TIMEOUT: How many seconds to wait in between each
//*     scan for empty buckets. This value should be lower if units die often
//*     in your map. A good starting value is about 60.
//* static method addHandler: Registers a callback function to the generic
//*     unit damage event. Example: StructuredDD.addHandler(function h)
//* static method add: Adds a unit to a bucket. If ADD_ALL_UNITS is enabled,
//*     this method need not be used.
library StructuredDD initializer ini

	//<< BEGIN SETTINGS SECTION

	//* Set this to true if you want all units in your map to be
	//* automatically added to StructuredDD. Otherwise you will have to
	//* manually add them with StructuredDD.add(u).
	public constant boolean ADD_ALL_UNITS = true

	//* This is the amount of units that exist in each trigger bucket.
	//* This number should be something between 5 and 30. A good starting
	//* value will be an estimate of your map's average count of units,
	//* divided by 10. When in doubt, just use 20.
	constant integer BUCKET_SIZE = 5

	//* This is how often StructuredDD will search for empty buckets. If
	//* your map has units being created and dying often, a lower value
	//* is better. Anything between 10 and 180 is good. When in doubt,
	//* just use 60.
	constant real PER_CLEANUP_TIMEOUT = 60.

	//>> END SETTINGS SECTION

	//* Our bucket struct which contains a trigger and its associated contents.
	struct Bucket
		integer bucketIndex = 0
		trigger trig = CreateTrigger()
		unit array members[BUCKET_SIZE]
	end

	//* Our wrapper struct. We never intend to actually instanciate "a
	//* StructuredDD", we just use this for a pretty, java-like API :3
	public struct StructuredDD
		private static boolexpr array conditions
		private static Bucket array bucketDB
		private static integer conditionsIndex = -1
		private static integer dbIndex = -1
		private static integer maxDBIndex = -1

		//* This method gets a readily available bucket for a unit to be added.
		//* If the "current" bucket is full, it returns a new one, otherwise
		//* it just returns the current bucket.
		private static method getBucket takes nothing returns integer
			integer index = 0
			Bucket tempDat
			if dbIndex != -1 and bucketDB[dbIndex].bucketIndex < BUCKET_SIZE then
				return dbIndex
			else
				maxDBIndex++
				dbIndex = maxDBIndex
				tempDat = new Bucket()
				bucketDB[maxDBIndex] = tempDat

				loop
					exitwhen index > conditionsIndex
					TriggerAddCondition(tempDat.trig, conditions[index])
					index++
				end
				return dbIndex
			end
		end

		static method disable takes nothing returns nothing
			integer index = 0
			Bucket b

			loop
				exitwhen index > maxDBIndex
				b = bucketDB[index]
				DisableTrigger(b.trig)
				index++
			end
		end

		static method enable takes nothing returns nothing
			integer index = 0
			Bucket b
			loop
				exitwhen index > maxDBIndex
				b = bucketDB[index]
				EnableTrigger(b.trig)
				index++
			end
		end

		//* This method is for adding a handler to the system. Whenever a
		//* handler is added, damage detection will immediately trigger that
		//* handler. There is no way to deallocate a handler, so don't try to
		//* do this dynamically (!) Support for handler deallocation is
		//* feasible (please contact me)
		static method addHandler takes code func returns nothing
			Bucket tempDat
			integer index = 0
			conditionsIndex++
			conditions[conditionsIndex] = Condition(func)

			loop
				exitwhen index > maxDBIndex
				tempDat = bucketDB[index]
				TriggerAddCondition(tempDat.trig, conditions[conditionsIndex])
				index++
			end
		end

		//* This method adds a unit to the damage detection system. If
		//* ADD_ALL_UNITS is enabled, this method need not be used.
		static method add takes unit member returns nothing
			Bucket tempDat
			integer whichBucket = getBucket()

			tempDat = bucketDB[whichBucket]
			tempDat.members[tempDat.bucketIndex] = member
			tempDat.bucketIndex++

			TriggerRegisterUnitEvent(tempDat.trig, member, EVENT_UNIT_DAMAGED)
		end

		//* This is just an auxillary function for ADD_ALL_UNITS' implementation
		static method autoAddC takes nothing returns boolean
			add(GetTriggerUnit())
			return false
		end

		//* This method is used to check if a given bucket is empty (and thus
		//* can be deallocated) - this is an auxillary reoutine for the
		//* periodic cleanup system.
		private static method bucketIsEmpty takes integer which returns boolean
			Bucket tempDat = bucketDB[which]
			integer index = 0
			loop
				exitwhen index == BUCKET_SIZE

				//GetUnitTypeId(unit) == 0 means that the unit has been removed.
				if GetUnitTypeId(tempDat.members[index]) != 0 then
					return false
				end

				index++
			end
			return true
		end

		//* This method cleans up any empty buckets periodically by checking
		//* if it has been fully allocated and then checking if all its
		//* members no longer exist.
		static method perCleanup takes nothing returns nothing
			integer index = 0

			loop
				exitwhen index > maxDBIndex
				if index != dbIndex and bucketIsEmpty(index) then
					DestroyTrigger(bucketDB[index].trig)
					destroy bucketDB[index]

					bucketDB[index] = bucketDB[maxDBIndex]
					maxDBIndex--

					if maxDBIndex == dbIndex then
						dbIndex = index
					end
					index--
				end
				index++
			end
		end
	end

	//* This is a initialization function necessary for the setup of
	//* StructuredDD.
	function ini takes nothing returns nothing
		group grp
		region reg
		trigger autoAddUnits
		timer perCleanup
		unit iter

		if ADD_ALL_UNITS then
			//Add starting units
			grp = CreateGroup()
			GroupEnumUnitsInRect(grp, bj_mapInitialPlayableArea, null)
			loop
				iter = FirstOfGroup(grp)
				exitwhen iter == null

				StructuredDD.add(iter)

				GroupRemoveUnit(grp, iter)
			end

			//Add entering units
			autoAddUnits = CreateTrigger()
			reg = CreateRegion()
			RegionAddRect(reg, bj_mapInitialPlayableArea)
			TriggerRegisterEnterRegion(autoAddUnits, reg, null)
			TriggerAddCondition(autoAddUnits, Condition(function StructuredDD.autoAddC))
		end

		//enable periodic cleanup:
		perCleanup = CreateTimer()
		TimerStart(perCleanup, PER_CLEANUP_TIMEOUT, true, function StructuredDD.perCleanup)
	end
endlibrary
