package net.hareworks.ghostdisplays.internal.controller import java.util.HashSet import java.util.UUID import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.atomic.AtomicBoolean import net.hareworks.ghostdisplays.api.DisplayController import net.hareworks.ghostdisplays.api.audience.AudiencePredicate import net.hareworks.ghostdisplays.api.click.ClickPriority import net.hareworks.ghostdisplays.api.click.ClickSurface import net.hareworks.ghostdisplays.api.click.DisplayClickContext import net.hareworks.ghostdisplays.api.click.DisplayClickHandler import net.hareworks.ghostdisplays.api.click.HandlerRegistration import org.bukkit.Bukkit import org.bukkit.entity.Display import org.bukkit.entity.Entity import org.bukkit.entity.Interaction import org.bukkit.entity.Player import org.bukkit.event.player.PlayerInteractEntityEvent import org.bukkit.plugin.java.JavaPlugin internal class BaseDisplayController( private val plugin: JavaPlugin, override val display: T, override val interaction: Interaction?, private val registry: DisplayRegistry ) : DisplayController { private val destroyed = AtomicBoolean(false) private val viewerCounts = ConcurrentHashMap() private val handlers = CopyOnWriteArrayList() private val audiences = CopyOnWriteArrayList() override fun show(player: Player) { runSync { val uuid = player.uniqueId val newCount = viewerCounts.compute(uuid) { _, count -> (count ?: 0) + 1 } ?: 1 if (newCount == 1) { player.showEntity(plugin, display) interaction?.let { player.showEntity(plugin, it) } } } } override fun hide(player: Player) { runSync { val uuid = player.uniqueId val current = viewerCounts[uuid] ?: return@runSync if (current <= 1) { viewerCounts.remove(uuid) player.hideEntity(plugin, display) interaction?.let { player.hideEntity(plugin, it) } } else { viewerCounts[uuid] = current - 1 } } } override fun isViewing(playerId: UUID): Boolean = viewerCounts.containsKey(playerId) override fun viewerIds(): Set = HashSet(viewerCounts.keys) override fun applyEntityUpdate(mutator: (T) -> Unit) { callSync { mutator(display) display.updateDisplay(false) } } override fun destroy() { if (!destroyed.compareAndSet(false, true)) return registry.unregister(this) val viewers = viewerIds().mapNotNull { Bukkit.getPlayer(it) } runSync { viewers.forEach { it.hideEntity(plugin, display) interaction?.let { interactionEntity -> it.hideEntity(plugin, interactionEntity) } } display.remove() interaction?.remove() viewerCounts.clear() } handlers.clear() audiences.forEach { it.clear() } audiences.clear() } override fun onClick(priority: ClickPriority, handler: DisplayClickHandler): HandlerRegistration { val entry = HandlerEntry(priority, handler) handlers += entry return HandlerRegistration { handlers.remove(entry) } } override fun addAudience(predicate: AudiencePredicate): HandlerRegistration { val binding = AudienceBindingImpl(predicate) audiences += binding refreshAudienceInternal(binding = binding) return HandlerRegistration { binding.unregister() } } private fun refreshAudienceInternal(target: Player? = null, binding: AudienceBindingImpl? = null) { runSync { val players = target?.let { listOf(it) } ?: Bukkit.getOnlinePlayers() val targets = binding?.let { listOf(it) } ?: audiences if (players.isEmpty() || targets.isEmpty()) return@runSync players.forEach { player -> targets.forEach { it.evaluate(player) } } } } override fun refreshAudience(target: Player?) { refreshAudienceInternal(target, null) } internal fun handleClick(event: PlayerInteractEntityEvent, clicked: Entity, surface: ClickSurface) { val sortedHandlers = handlers.sortedBy { it.priority.ordinal } if (sortedHandlers.isEmpty()) return val context = DisplayClickContext( player = event.player, hand = event.hand, clickedEntity = clicked, surface = surface, controller = this, event = event ) sortedHandlers.forEach { entry -> entry.handler.handle(context) if (event.isCancelled) return } } internal fun handlePlayerQuit(player: Player) { val uuid = player.uniqueId viewerCounts.remove(uuid) audiences.forEach { it.forget(uuid) } } private fun runSync(action: () -> Unit) { if (Bukkit.isPrimaryThread()) { action() } else { Bukkit.getScheduler().runTask(plugin, action) } } private fun callSync(action: () -> R): R { return if (Bukkit.isPrimaryThread()) { action() } else { val future = CompletableFuture() Bukkit.getScheduler().runTask(plugin) { future.complete(action()) } future.join() } } private inner class AudienceBindingImpl( private val predicate: AudiencePredicate ) { private val activeViewers = ConcurrentHashMap.newKeySet() fun evaluate(player: Player) { val uuid = player.uniqueId val matches = runCatching { predicate.test(player) }.getOrDefault(false) if (matches) { if (activeViewers.add(uuid)) { show(player) } } else { if (activeViewers.remove(uuid)) { hide(player) } } } fun forget(playerId: UUID) { activeViewers.remove(playerId) } fun unregister() { audiences.remove(this) val targets = activeViewers.toList() targets.mapNotNull { Bukkit.getPlayer(it) }.forEach { hide(it) } activeViewers.clear() } fun clear() { activeViewers.clear() } } private data class HandlerEntry( val priority: ClickPriority, val handler: DisplayClickHandler ) }