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

import org.apache.logging.log4j.LogManager

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

	private val once = ArrayDeque<ITickable>()
	private val onceValveTime = ArrayList<ITickable>()

	private val always = ArrayList<ITickable>()
	private val alwaysValveTime = ArrayList<ITickable>()

	private val timers = ArrayDeque<Timer>()

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

	private var nothingToDo = true

	inner class Timer(val timerTicks: Int, val runnable: Runnable) {
		val ringAt = ticks + timerTicks

		var finished = false
			private set

		init {
			nothingToDo = false

			if (timers.isEmpty()) {
				timers.addLast(this)
			} else {
				val iterator = timers.listIterator()
				var hit = false

				for (value in iterator) {
					if (value.ringAt == ringAt) {
						hit = true
						iterator.add(this)
						break
					} else if (value.ringAt > ringAt) {
						if (iterator.hasPrevious()) {
							iterator.previous()
							iterator.add(this)
						} else {
							timers.addFirst(this)
						}

						hit = true
						break
					}
				}

				if (!hit) {
					timers.addLast(this)
				}
			}
		}

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

	fun add(ticker: IConditionalTickable) {
		if (inTicker) {
			conditionalValveTime.add(ticker)
		} else {
			conditional.addFirst(ticker)
			nothingToDo = false
		}
	}

	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) {
		if (inTicker) {
			onceValveTime.add(ticker)
		} else {
			once.addFirst(ticker)
			nothingToDo = false
		}
	}

	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) {
		if (inTicker) {
			alwaysValveTime.add(ticker)
		} else {
			always.add(ticker)
			nothingToDo = false
		}
	}

	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)
	}

	fun until(ticker: () -> Boolean) = add(IConditionalTickable.wrap(ticker))
	fun `while`(tickerCondition: () -> Boolean, ticker: () -> Unit) = add(
		IConditionalTickable.wrap(
			tickerCondition,
			ticker
		)
	)

	fun until(ticker: () -> Boolean, condition: Boolean, reason: String) = add(IConditionalTickable.wrap(ticker), condition, reason)
	fun `while`(tickerCondition: () -> Boolean, ticker: () -> Unit, condition: Boolean, reason: String) = add(
		IConditionalTickable.wrap(tickerCondition, ticker), condition, reason)

	override fun tick() {
		if (nothingToDo) {
			return
		}

		if (inTicker) {
			throw ConcurrentModificationException("Already ticking")
		}

		ticks++
		inTicker = true

		try {
			var nothingToDo = true
			val conditional = conditional
			val once = once
			val always = always
			val alwaysValveTime = alwaysValveTime
			val conditionalValveTime = conditionalValveTime
			val onceValveTime = onceValveTime
			val timers = timers

			if (conditional.isNotEmpty()) {
				val iterator = conditional.iterator()

				for (ticker in iterator) {
					if (!ticker.tick()) {
						iterator.remove()
					} else {
						nothingToDo = false
					}
				}
			}

			if (once.isNotEmpty()) {
				for (ticker in once) {
					ticker.tick()
				}

				once.clear()
			}

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

				nothingToDo = false
			}

			if (alwaysValveTime.isNotEmpty()) {
				always.addAll(alwaysValveTime)
				alwaysValveTime.clear()
				nothingToDo = false
			}

			if (conditionalValveTime.isNotEmpty()) {
				for (ticker in conditionalValveTime) {
					conditional.addFirst(ticker)
				}

				conditionalValveTime.clear()
				nothingToDo = false
			}

			if (onceValveTime.isNotEmpty()) {
				for (ticker in onceValveTime) {
					once.addFirst(ticker)
				}

				onceValveTime.clear()
				nothingToDo = false
			}

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

				if (head.ringAt <= ticks) {
					head.execute()
					timers.removeFirst()
				} else {
					break
				}
			}

			this.nothingToDo = nothingToDo
		} finally {
			inTicker = false
		}
	}

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

		conditional.clear()
		conditionalValveTime.clear()

		once.clear()
		onceValveTime.clear()

		always.clear()
		alwaysValveTime.clear()

		timers.clear()
	}

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