package ru.dbotthepony.kstarbound.defs

import com.google.common.collect.ImmutableSet
import ru.dbotthepony.kommons.io.readCollection
import ru.dbotthepony.kommons.io.writeBinaryString
import ru.dbotthepony.kommons.io.writeCollection
import ru.dbotthepony.kommons.vector.Vector2d
import ru.dbotthepony.kstarbound.io.readDouble
import ru.dbotthepony.kstarbound.io.readInternedString
import ru.dbotthepony.kstarbound.io.readNullableDouble
import ru.dbotthepony.kstarbound.io.readVector2d
import ru.dbotthepony.kstarbound.io.writeDouble
import ru.dbotthepony.kstarbound.io.writeNullableDouble
import ru.dbotthepony.kstarbound.io.writeStruct2d
import ru.dbotthepony.kstarbound.math.Line2d
import ru.dbotthepony.kstarbound.network.syncher.legacyCodec
import ru.dbotthepony.kstarbound.network.syncher.nativeCodec
import ru.dbotthepony.kstarbound.world.physics.Poly
import java.io.DataInputStream
import java.io.DataOutputStream
import java.util.function.Predicate

sealed class PhysicsForceRegion {
	abstract fun write(stream: DataOutputStream, isLegacy: Boolean)

	// enum class Type int32_t
	// type -> isBlacklist
	class Filter(val isBlacklist: Boolean, val categories: Set<String>) : Predicate<String> {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(if (isLegacy) stream.readInt() > 0 else stream.readBoolean(), ImmutableSet.copyOf(stream.readCollection { readInternedString() }))

		fun write(stream: DataOutputStream, isLegacy: Boolean) {
			if (isLegacy)
				stream.writeInt(if (isBlacklist) 1 else 0)
			else
				stream.writeBoolean(isBlacklist)

			stream.writeCollection(categories) { writeBinaryString(it) }
		}

		fun test(categories: Collection<String>): Boolean {
			if (isBlacklist) {
				return categories.any { it in this.categories }
			} else {
				return categories.any { it in this.categories }
			}
		}

		override fun test(t: String): Boolean {
			if (isBlacklist) {
				return t !in this.categories
			} else {
				return t in this.categories
			}
		}
	}

	data class Directional(val region: Poly, val xTargetVelocity: Double?, val yTargetVelocity: Double?, val controlForce: Double, val filter: Filter) : PhysicsForceRegion() {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(
			Poly.read(stream, isLegacy),
			stream.readNullableDouble(isLegacy),
			stream.readNullableDouble(isLegacy),
			stream.readDouble(isLegacy),
			Filter(stream, isLegacy)
		)

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(0)
			region.write(stream, isLegacy)
			stream.writeNullableDouble(xTargetVelocity, isLegacy)
			stream.writeNullableDouble(yTargetVelocity, isLegacy)
			stream.writeDouble(controlForce, isLegacy)
			filter.write(stream, isLegacy)
		}
	}

	data class Radial(val center: Vector2d, val outerRadius: Double, val innerRadius: Double, val targetRadialVelocity: Double, val controlForce: Double, val filter: Filter) : PhysicsForceRegion() {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(
			stream.readVector2d(isLegacy),
			stream.readDouble(isLegacy),
			stream.readDouble(isLegacy),
			stream.readDouble(isLegacy),
			stream.readDouble(isLegacy),
			Filter(stream, isLegacy)
		)

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(1)
			stream.writeStruct2d(center, isLegacy)
			stream.writeDouble(outerRadius, isLegacy)
			stream.writeDouble(innerRadius, isLegacy)
			stream.writeDouble(targetRadialVelocity, isLegacy)
			stream.writeDouble(controlForce, isLegacy)
			filter.write(stream, isLegacy)
		}
	}

	data class Gradient(val region: Poly, val gradient: Line2d, val baseTargetVelocity: Double, val baseControlForce: Double, val filter: Filter) : PhysicsForceRegion() {
		constructor(stream: DataInputStream, isLegacy: Boolean) : this(
			Poly.read(stream, isLegacy),
			Line2d(stream, isLegacy),
			stream.readDouble(isLegacy),
			stream.readDouble(isLegacy),
			Filter(stream, isLegacy)
		)

		override fun write(stream: DataOutputStream, isLegacy: Boolean) {
			stream.writeByte(2)
			region.write(stream, isLegacy)
			gradient.write(stream, isLegacy)
			stream.writeDouble(baseTargetVelocity, isLegacy)
			stream.writeDouble(baseControlForce, isLegacy)
			filter.write(stream, isLegacy)
		}
	}

	companion object {
		val CODEC = nativeCodec(::read, PhysicsForceRegion::write)
		val LEGACY_CODEC = legacyCodec(::read, PhysicsForceRegion::write)

		fun read(stream: DataInputStream, isLegacy: Boolean): PhysicsForceRegion {
			return when (val type = stream.readUnsignedByte()) {
				0 -> Directional(stream, isLegacy)
				1 -> Radial(stream, isLegacy)
				2 -> Gradient(stream, isLegacy)
				else -> throw IllegalArgumentException("Unknown force region type $type!")
			}
		}
	}
}
