package ru.dbotthepony.mc.otm.core.util

import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap
import it.unimi.dsi.fastutil.objects.ObjectAVLTreeSet
import org.apache.logging.log4j.LogManager
import ru.dbotthepony.mc.otm.core.addSorted
import java.util.concurrent.atomic.AtomicInteger

class TickList : ITickable {
	private val conditional = ArrayList<IConditionalTickable>()
	private val conditionalQueued = ArrayList<IConditionalTickable>()

	private val once = ArrayList<ITickable>()
	private val onceQueued = ArrayList<ITickable>()

	private val always = ArrayList<ITickable>()
	private val alwaysQueued = ArrayList<ITickable>()
	private val toRemoveFromAlways = ArrayList<ITickable>()

	private val timers = ObjectAVLTreeSet<Timer>()
	private val nextTimerID = AtomicInteger()
	private val namedTimers = Object2ObjectOpenHashMap<Any, Timer>(0)

	private var shouldTick = false

	var inTicker = false
		private set
	var ticks = 0
		private set

	private var nextSometime = 0

	inner class Timer(val timerTicks: Int, val runnable: Runnable) : Comparable<Timer> {
		val ringAt = ticks + timerTicks
		val timerID = nextTimerID.incrementAndGet()

		var finished = false
			private set

		init {
			shouldTick = true
			timers.add(this)
		}

		override fun compareTo(other: Timer): Int {
			var c = ringAt.compareTo(other.ringAt)
			if (c != 0) return c
			c = timerID.compareTo(other.timerID)
			return c
		}

		fun execute() {
			if (finished) return
			runnable.run()
			finished = true
		}
	}

	/**
	 * Calling this method while timer already exists removes old timer
	 */
	fun namedTimer(name: Any?, ticks: Int, callback: Runnable): Timer {
		val remove = namedTimers.remove(name)

		if (remove != null)
			timers.remove(remove)

		val timer = Timer(ticks, callback)
		namedTimers[name] = timer
		return timer
	}

	inner class Ticker(parent: ITickable) : ITickable by parent {
		init {
			add(this, always, alwaysQueued)
		}

		var isEnabled = true
			set(value) {
				if (field != value) {
					field = value

					if (value) {
						add(this, always, alwaysQueued)
					} else {
						alwaysQueued.remove(this)

						if (inTicker) {
							toRemoveFromAlways.add(this)
						} else {
							always.remove(this)
						}
					}
				}
			}

		fun disable() {
			isEnabled = false
		}

		fun enable() {
			isEnabled = true
		}
	}

	private fun <T : Any> add(value: T, regular: MutableList<T>, queue: MutableList<T>) {
		shouldTick = true

		if (inTicker) {
			queue.add(value)
		} else {
			regular.add(value)
		}
	}

	fun add(ticker: IConditionalTickable) {
		add(ticker, conditional, conditionalQueued)
	}

	fun add(ticker: IConditionalTickable, condition: Boolean, reason: String) {
		if (!condition) {
			LOGGER.error("Refusing to add tickable $ticker because we $reason", IllegalStateException(reason))
			return
		}

		return add(ticker)
	}

	fun once(ticker: ITickable) {
		add(ticker, once, onceQueued)
	}

	fun once(ticker: ITickable, condition: Boolean, reason: String) {
		if (!condition) {
			LOGGER.error("Refusing to add tickable $ticker because we $reason", IllegalStateException(reason))
			return
		}

		return once(ticker)
	}

	fun always(ticker: ITickable) {
		add(ticker, always, alwaysQueued)
	}

	fun timer(timerTicks: Int, action: Runnable, condition: Boolean, reason: String): Timer? {
		if (!condition) {
			LOGGER.error("Refusing to add timer $action in $timerTicks ticks because we $reason", IllegalStateException(reason))
			return null
		}

		return Timer(timerTicks, action)
	}

	fun timer(timerTicks: Int, action: Runnable): Timer {
		return Timer(timerTicks, action)
	}

	/**
	 * Schedules execution of Runnable somewhere in the future,
	 * at discretion of this tick list
	 */
	fun sometime(action: Runnable): Timer {
		if (nextSometime < ticks || nextSometime - ticks >= 40) {
			nextSometime = ticks
		}

		return Timer((++nextSometime) - ticks, action)
	}

	override fun tick() {
		if (inTicker) {
			throw ConcurrentModificationException("Already ticking")
		}

		ticks++
		if (!shouldTick) return

		inTicker = true
		shouldTick = timers.isNotEmpty()

		try {
			if (conditional.isNotEmpty()) {
				shouldTick = true
				conditional.removeIf { !it.tick() }
			}

			if (once.isNotEmpty()) {
				shouldTick = true

				for (ticker in once) ticker.tick()
				once.clear()
			}

			if (toRemoveFromAlways.isNotEmpty()) {
				shouldTick = true

				for (v in toRemoveFromAlways) always.remove(v)
				toRemoveFromAlways.clear()
			}

			if (always.isNotEmpty()) {
				shouldTick = true

				for (ticker in always) {
					ticker.tick()
				}
			}

			if (alwaysQueued.isNotEmpty()) {
				shouldTick = true

				always.ensureCapacity(always.size + alwaysQueued.size)
				for (v in alwaysQueued) always.add(v) // avoid toArray()
				alwaysQueued.clear()
			}

			if (conditionalQueued.isNotEmpty()) {
				shouldTick = true

				for (ticker in conditionalQueued) conditional.add(ticker)
				conditionalQueued.clear()
			}

			if (onceQueued.isNotEmpty()) {
				shouldTick = true

				for (ticker in onceQueued) once.add(ticker)
				onceQueued.clear()
			}

			while (timers.isNotEmpty()) {
				val head = timers.first()

				if (head.ringAt <= ticks) {
					head.execute()
					timers.remove(head)
				} else {
					break
				}
			}
		} finally {
			inTicker = false
		}
	}

	fun clear() {
		if (inTicker) throw ConcurrentModificationException()

		conditional.clear()
		conditionalQueued.clear()

		once.clear()
		onceQueued.clear()

		always.clear()
		alwaysQueued.clear()

		timers.clear()
	}

	companion object {
		private val LOGGER = LogManager.getLogger()
	}
}
