package ru.dbotthepony.kstarbound.world

import ru.dbotthepony.kommons.io.VarIntValueCodec
import ru.dbotthepony.kommons.io.map
import ru.dbotthepony.kommons.math.linearInterpolation
import ru.dbotthepony.kommons.util.getValue
import ru.dbotthepony.kommons.util.setValue
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.Globals
import ru.dbotthepony.kstarbound.defs.world.FlyingType
import ru.dbotthepony.kstarbound.defs.world.SkyGlobalConfig
import ru.dbotthepony.kstarbound.defs.world.SkyParameters
import ru.dbotthepony.kstarbound.defs.world.SkyType
import ru.dbotthepony.kstarbound.defs.world.WarpPhase
import ru.dbotthepony.kstarbound.network.syncher.MasterElement
import ru.dbotthepony.kstarbound.network.syncher.NetworkedGroup
import ru.dbotthepony.kstarbound.network.syncher.networkedBoolean
import ru.dbotthepony.kstarbound.network.syncher.networkedData
import ru.dbotthepony.kstarbound.network.syncher.networkedDouble
import ru.dbotthepony.kstarbound.network.syncher.networkedEnum
import ru.dbotthepony.kstarbound.network.syncher.networkedEnumStupid
import ru.dbotthepony.kstarbound.network.syncher.networkedFloat
import ru.dbotthepony.kstarbound.network.syncher.networkedJson
import ru.dbotthepony.kstarbound.network.syncher.networkedVec2f
import ru.dbotthepony.kstarbound.util.IClock
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin

class Sky() {
	val networkedGroup = MasterElement(NetworkedGroup())

	var skyParameters by networkedGroup.upstream.add(networkedJson(SkyParameters()))
	var skyType by networkedGroup.upstream.add(networkedEnumStupid(SkyType.ORBITAL))

	var time by networkedGroup.upstream.add(networkedDouble())
		private set
	var flyingType by networkedGroup.upstream.add(networkedEnum(FlyingType.NONE))
		private set
	var enterHyperspace by networkedGroup.upstream.add(networkedBoolean())
		private set
	var startInWarp by networkedGroup.upstream.add(networkedBoolean())
		private set
	var worldMoveOffset by networkedGroup.upstream.add(networkedVec2f())
		private set
	var starMoveOffset by networkedGroup.upstream.add(networkedVec2f())
		private set
	var warpPhase by networkedGroup.upstream.add(networkedData(WarpPhase.MAINTAIN, VarIntValueCodec.map({ WarpPhase.entries[this - 1] }, { ordinal - 1 })))
		private set
	var flyingTimer by networkedGroup.upstream.add(networkedFloat())
		private set

	var flashTimer = 0.0
		private set

	var starOffset = Vector2d.ZERO
		private set
	var worldOffset = Vector2d.ZERO
		private set
	var pathOffset = Vector2d.ZERO
		private set

	var worldRotation: Double = 0.0
		private set
	var starRotation: Double = 0.0
		private set
	var pathRotation: Double = 0.0
		private set

	var destination: SkyParameters? = null
		private set

	var referenceClock: IClock? = null
		set(value) {
			field = value
			time = value?.time ?: time
		}

	val speedupTime: Double get() {
		if (enterHyperspace) {
			return Globals.sky.hyperspaceSpeedupTime.coerceAtLeast(0.01)
		} else {
			return Globals.sky.speedupTime.coerceAtLeast(0.01)
		}
	}

	val slowdownTime: Double get() {
		if (enterHyperspace) {
			return Globals.sky.hyperspaceSlowdownTime.coerceAtLeast(0.01)
		} else {
			return Globals.sky.slowdownTime.coerceAtLeast(0.01)
		}
	}

	constructor(parameters: SkyParameters) : this() {
		skyParameters = parameters.copy()
		skyType = parameters.skyType
	}

	fun startFlying(enterHyperspace: Boolean, startInWarp: Boolean = false) {
		if (startInWarp)
			flyingType = FlyingType.WARP
		else if (flyingType == FlyingType.NONE)
			flyingType = FlyingType.DISEMBARKING

		// flyingTimer = 0.0
		this.enterHyperspace = enterHyperspace
		this.startInWarp = startInWarp
	}

	fun stopFlyingAt(destination: SkyParameters) {
		this.destination = destination

		if (skyType != SkyType.WARP)
			skyType = SkyType.ORBITAL
	}

	private var lastFlyingType = FlyingType.NONE
	private var lastWarpPhase = WarpPhase.MAINTAIN
	private var sentSFX = false

	private fun stateUpdate() {
		if (flyingType != lastFlyingType) {
			flyingTimer = 0.0

			if (flyingType == FlyingType.WARP) {
				warpPhase = WarpPhase.SPEEDING_UP

				if (startInWarp) {
					if (enterHyperspace) {
						warpPhase = WarpPhase.MAINTAIN
					} else {
						flyingTimer = speedupTime
					}

					lastWarpPhase = warpPhase
				}

				worldMoveOffset = Vector2d(cos(pathRotation), sin(pathRotation)) * Globals.sky.flyMaxVelocity / 2.0 * speedupTime
				starMoveOffset = Vector2d(x = Globals.sky.flyMaxVelocity * Globals.sky.starVelocityFactor / 2.0 * speedupTime)
			} else if (flyingType == FlyingType.ARRIVING) {
				sentSFX = false
				worldOffset = Vector2d.ZERO
				starOffset = Vector2d.ZERO
			}
		}

		if (warpPhase != lastWarpPhase) {
			flyingTimer = 0.0

			when (warpPhase) {
				WarpPhase.SLOWING_DOWN -> {
					skyType = SkyType.ORBITAL
					//flashTimer = GlobalDefaults.sky.flashTimer
					sentSFX = false

					val origin = if (skyType == SkyType.SPACE) Globals.sky.spaceArrivalOrigin else Globals.sky.arrivalOrigin
					val path = if (skyType == SkyType.SPACE) Globals.sky.spaceArrivalPath else Globals.sky.arrivalPath

					pathOffset = origin.offset
					pathRotation = origin.rotationRad

					var exitDistance = Globals.sky.flyMaxVelocity / 2.0 * slowdownTime
					worldMoveOffset = Vector2d(x = exitDistance)
					worldOffset = worldMoveOffset

					exitDistance *= Globals.sky.starVelocityFactor
					starMoveOffset = Vector2d(x = exitDistance)
					starOffset = starMoveOffset

					worldRotation = 0.0
					starRotation = 0.0
					flyingTimer = 0.0
				}

				WarpPhase.MAINTAIN -> {
					//flashTimer = GlobalDefaults.sky.flashTimer

					skyType = SkyType.WARP
					sentSFX = false
				}

				WarpPhase.SPEEDING_UP -> {
					flyingTimer = 0.0
				}
			}
		}

		lastFlyingType = flyingType
		lastWarpPhase = warpPhase
	}


	fun tick(delta: Double) {
		time = referenceClock?.time ?: (time + delta)
		flashTimer = (flashTimer - delta).coerceAtLeast(0.0)

		if (flyingType != FlyingType.NONE) {
			flyingTimer += delta

			if (flyingType == FlyingType.DISEMBARKING) {
				val finished = if (skyParameters.skyType == SkyType.SPACE)
					controlledMovement(Globals.sky.spaceDisembarkPath, Globals.sky.spaceDisembarkOrigin, flyingTimer)
				else
					controlledMovement(Globals.sky.disembarkPath, Globals.sky.disembarkOrigin, flyingTimer)

				if (finished) {
					flyingType = FlyingType.WARP
				}
			} else if (flyingType == FlyingType.ARRIVING) {
				val finished = if (skyParameters.skyType == SkyType.SPACE)
					controlledMovement(Globals.sky.spaceArrivalPath, Globals.sky.spaceArrivalOrigin, flyingTimer)
				else
					controlledMovement(Globals.sky.arrivalPath, Globals.sky.arrivalOrigin, flyingTimer)

				if (finished) {
					flyingType = FlyingType.NONE
				}

				starOffset -= starOffset * Globals.sky.correctionPower
				worldOffset -= worldOffset * Globals.sky.correctionPower
			} else if (flyingType == FlyingType.WARP) {
				val percentage = when (warpPhase) {
					WarpPhase.SLOWING_DOWN -> (flyingTimer / speedupTime).pow(2.0)
					WarpPhase.MAINTAIN -> 1.0
					WarpPhase.SPEEDING_UP -> (1.0 - flyingTimer / speedupTime).pow(2.0)
				}

				if (percentage < 1.0) {
					val dir = warpPhase.ordinal - 1.0
					starOffset = (starMoveOffset * percentage * dir).rotate(-(starRotation + pathRotation))
					worldOffset = (worldMoveOffset * percentage * dir).rotate(-(starRotation + pathRotation))
				} else {
					val angle = -(starRotation + pathRotation)
					val angleVec = Vector2d(cos(angle), sin(angle))
					starOffset += angleVec * Globals.sky.flyMaxVelocity * delta * Globals.sky.starVelocityFactor
					worldOffset = worldMoveOffset
				}

				if (warpPhase == WarpPhase.SPEEDING_UP && flyingTimer >= speedupTime && !enterHyperspace && destination != null) {
					skyParameters = destination!!
					destination = null
					warpPhase = WarpPhase.SLOWING_DOWN
				} else if (warpPhase == WarpPhase.SPEEDING_UP && flyingTimer >= speedupTime && enterHyperspace) {
					warpPhase = WarpPhase.MAINTAIN
				} else if (warpPhase == WarpPhase.MAINTAIN && flyingTimer >= Globals.sky.flyingTimer && destination != null) {
					skyParameters = destination!!
					destination = null
					warpPhase = WarpPhase.SLOWING_DOWN
				} else if (warpPhase == WarpPhase.SLOWING_DOWN && flyingTimer >= slowdownTime) {
					flyingType = FlyingType.ARRIVING
				}
			}
		} else {
			starOffset = Vector2d.ZERO
			worldOffset = Vector2d.ZERO
			pathOffset = Vector2d.ZERO
		}

		stateUpdate()
	}

	private fun controlledMovement(path: List<SkyGlobalConfig.PathPiece>, origin: SkyGlobalConfig.PathPiece, timeOffset: Double): Boolean {
		var previous = origin
		var stepTime = 0.0

		for (entry in path) {
			stepTime += entry.time

			if (timeOffset <= stepTime) {
				val percentage = (timeOffset - previous.time) / (stepTime - previous.time)
				pathOffset = linearInterpolation(percentage, previous.offset, entry.offset)
				pathRotation = linearInterpolation(percentage, previous.rotation, entry.rotation)
				return false
			}

			previous = entry
		}

		return true
	}
}
