package org.neoflock.neocomputers.block

import dev.architectury.registry.menu.MenuRegistry
import net.minecraft.core.BlockPos
import net.minecraft.core.HolderLookup
import net.minecraft.core.NonNullList
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.ContainerHelper
import net.minecraft.world.Containers
import net.minecraft.world.InteractionResult
import net.minecraft.world.MenuProvider
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.entity.player.Player
import net.minecraft.world.inventory.AbstractContainerMenu
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.SoundType
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.phys.BlockHitResult
import org.neoflock.neocomputers.entity.BlockEntities
import org.neoflock.neocomputers.entity.ComponentUser
import org.neoflock.neocomputers.gui.menu.RelayMenu
import org.neoflock.neocomputers.item.RelayUpgrade
import org.neoflock.neocomputers.network.ConventionalNetworkDevice
import org.neoflock.neocomputers.network.DeviceNode
import org.neoflock.neocomputers.network.NNComponent
import org.neoflock.neocomputers.network.Networking
import org.neoflock.neocomputers.network.NodeSynchronizer
import org.neoflock.neocomputers.utils.GenericContainer

class RelayEntity(blockPos: BlockPos, blockState: BlockState): SingleDeviceBlockEntity(BlockEntities.RELAY_ENTITY.get(), blockPos, blockState),
    GenericContainer, ComponentUser, MenuProvider {

    companion object RelaySlots {
        val CARD = 0
        val CPU = 1
        val MEM = 2
        val STORAGE = 3

        val SLOT_COUNT = 4
    }

    val slots = NonNullList.withSize(SLOT_COUNT, ItemStack.EMPTY)!!

    override fun getItems(): NonNullList<ItemStack> = slots

    override fun stillValid(player: Player): Boolean = true

    fun getUpgrade(slot: Int) = slots[slot].item as? RelayUpgrade

    fun computeRelayInterval(): Int = getUpgrade(CPU)?.getRelayInterval(slots[CPU]) ?: 5
    fun computeRelayBufferSize(): Int = getUpgrade(MEM)?.getRelayBufferSize(slots[MEM]) ?: 1
    fun computeRelayQueueSize(): Int = getUpgrade(STORAGE)?.getRelayQueueSize(slots[STORAGE]) ?: 20
    fun getRelaySender(): DeviceNode? = getUpgrade(CARD)?.getComponentNode(slots[CARD])

    fun computeRelayCapacity(): Int = computeRelayBufferSize() + computeRelayQueueSize()

    val queue = mutableListOf<Networking.ClassicPacket>()
    val justReceived = mutableListOf<Networking.ClassicPacket>()
    var activityTickLeft = 0
    var ticksUntilQueue = 0

    override val deviceNode = object : DeviceNode() {
        override var reachability = Networking.Visibility.NONE
        override fun received(message: Networking.Message) {
            super.received(message)
            if(message.sender == this) return
            if(message is Networking.ClassicPacket && message.hopCount < 5 && (queue.size + justReceived.size) < computeRelayCapacity()) {
                justReceived.addLast(message)
                activityTickLeft = 20
                markChanged()
            }
        }

        override fun encodeScreenData(player: ServerPlayer, buf: FriendlyByteBuf) {
            super.encodeScreenData(player, buf)
            buf.writeVarInt(computeRelayInterval())
            buf.writeVarInt(computeRelayBufferSize())
            buf.writeVarInt(computeRelayQueueSize())
            buf.writeVarInt(queue.size + justReceived.size)
        }

        override fun writeFullStateCommit(buf: FriendlyByteBuf) {
            super.writeFullStateCommit(buf)
            buf.writeVarInt(activityTickLeft)
        }

        override fun processCommit(buf: FriendlyByteBuf) {
            super.processCommit(buf)
            activityTickLeft = buf.readVarInt()
        }

        override fun getComponent() = NNComponent("relay")
    }

    fun sendQueuedPacket() {
        if(queue.isEmpty()) return
        val pack = queue.removeFirst()

        for(connection in deviceNode.connections) {
            // skip unwanted loopback
            if(connection in pack.sender.getReachable()) continue
            val hopped = pack.hop(pack.sender)

            if(connection is ConventionalNetworkDevice) {
                connection.sendClassicPacket(hopped)
            } else {
                Networking.emitMessage(connection, hopped, setOf(deviceNode))
            }
        }
    }

    override fun tickDevice(level: Level) {
        super.tickDevice(level)
        if(level !is ServerLevel) return
        if(activityTickLeft > 0) {
            activityTickLeft--
            deviceNode.markChanged()
        }
        queue.addAll(justReceived)
        justReceived.clear()
        val cap = computeRelayCapacity()
        while(queue.size > cap) queue.removeLast()
        ticksUntilQueue--
        if(ticksUntilQueue <= 0) {
            ticksUntilQueue = computeRelayInterval()
            val toSend = computeRelayBufferSize()
            for(i in 0..<toSend) {
                sendQueuedPacket()
            }
        }
        deviceNode.markChanged()
    }

    override fun getMachineBlockPosition() = blockPos!!

    override fun getMachineLevel() = level!!

    override fun getMachineNode() = deviceNode

    override fun getDisplayName() = Component.translatable("block.neocomputers.relay")!!

    override fun createMenu(
        i: Int,
        inventory: Inventory,
        player: Player
    ) = RelayMenu(i, inventory, this)

    override fun loadAdditional(tag: CompoundTag, registries: HolderLookup.Provider) {
        super.loadAdditional(tag, registries)
        ContainerHelper.loadAllItems(tag, slots, registries)
    }

    override fun saveAdditional(tag: CompoundTag, registries: HolderLookup.Provider) {
        super.saveAdditional(tag, registries)
        ContainerHelper.saveAllItems(tag, slots, registries)
    }
}

class RelayBlock: DeviceBlock(Properties.of().sound(SoundType.METAL)) {
    override fun newBlockEntity(pos: BlockPos, state: BlockState) = RelayEntity(pos, state)

    override fun useWithoutItem(
        state: BlockState,
        level: Level,
        pos: BlockPos,
        player: Player,
        hitResult: BlockHitResult
    ): InteractionResult {
        if(!level.isClientSide) {
            val ent = level.getBlockEntity(pos, BlockEntities.RELAY_ENTITY.get()).get()
            // Open menu
            MenuRegistry.openMenu(player as ServerPlayer, ent)
            NodeSynchronizer.registerPlayerScreen(player, ent.deviceNode)
        }
        return InteractionResult.SUCCESS
    }

    override fun onRemove(
        blockState: BlockState,
        level: Level,
        blockPos: BlockPos,
        blockState2: BlockState,
        bl: Boolean
    ) {
        Containers.dropContentsOnDestroy(blockState, blockState2, level, blockPos)
        super.onRemove(blockState, level, blockPos, blockState2, bl)
    }
}