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

import it.unimi.dsi.fastutil.objects.ObjectArrayList
import it.unimi.dsi.fastutil.objects.ObjectArraySet
import org.apache.logging.log4j.LogManager
import java.util.*

class TickList : ITickable {
	private val tickers = LinkedList<IConditionalTickable>()
	private val queuedTickers = ObjectArraySet<IConditionalTickable>()

	private val timers = PriorityQueue<Timer>()
	private val namedTimers = HashMap<Any?, Timer>()

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

	private var nextSometime = 0

	inner class Timer(timerTicks: Int, private var runnable: Runnable?) : Comparable<Timer> {
		val ringAt = ticks + timerTicks

		init {
			timers.add(this)
		}

		override fun compareTo(other: Timer): Int {
			return ringAt.compareTo(other.ringAt)
		}

		fun cancel() {
			runnable = null
			timers.remove(this)
		}

		fun execute() {
			runnable?.run()
			runnable = null
		}
	}

	/**
	 * 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(private val parent: ITickable) : IConditionalTickable {
		init {
			add(this)
		}

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

					if (value) {
						add(this)
					} else {
						queuedTickers.remove(this)

						if (!inTicker) {
							tickers.remove(this)
						}
					}
				}
			}

		override fun tick(): Boolean {
			if (!isEnabled)
				return false

			parent.tick()
			return true
		}

		fun disable() {
			isEnabled = false
		}

		fun enable() {
			isEnabled = true
		}
	}

	fun add(value: IConditionalTickable) {
		if (inTicker) {
			queuedTickers.add(value)
		} else {
			tickers.add(value)
		}
	}

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

		return add(ticker)
	}

	fun once(ticker: ITickable) {
		add(IConditionalTickable { ticker.tick(); false })
	}

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

		return once(ticker)
	}

	fun always(ticker: ITickable) {
		add(IConditionalTickable { ticker.tick(); true })
	}

	fun timer(timerTicks: Int, action: Runnable, condition: Boolean, reason: String): Timer? {
		if (!condition) {
			LOGGER.debug("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++

		inTicker = true

		try {
			tickers.removeIf { !it.tick() }

			if (queuedTickers.isNotEmpty()) {
				tickers.addAll(queuedTickers)
				queuedTickers.clear()
			}

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

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

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

		tickers.clear()
		queuedTickers.clear()

		timers.clear()
	}

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