// Copyright Daniel Ratcliffe, 2011-2022. Do not distribute without permission.
//
// SPDX-License-Identifier: LicenseRef-CCPL

package dan200.computercraft.shared.turtle.blocks;

import com.mojang.authlib.GameProfile;
import dan200.computercraft.api.component.ComputerComponents;
import dan200.computercraft.api.peripheral.IPeripheral;
import dan200.computercraft.api.turtle.ITurtleAccess;
import dan200.computercraft.api.turtle.ITurtleUpgrade;
import dan200.computercraft.api.turtle.TurtleSide;
import dan200.computercraft.api.upgrades.UpgradeData;
import dan200.computercraft.core.computer.ComputerSide;
import dan200.computercraft.shared.ModRegistry;
import dan200.computercraft.shared.computer.blocks.AbstractComputerBlockEntity;
import dan200.computercraft.shared.computer.blocks.ComputerPeripheral;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.core.ComputerState;
import dan200.computercraft.shared.computer.core.ServerComputer;
import dan200.computercraft.shared.computer.core.TerminalSize;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.container.BasicContainer;
import dan200.computercraft.shared.platform.PlatformHelper;
import dan200.computercraft.shared.turtle.core.TurtleBrain;
import dan200.computercraft.shared.turtle.inventory.TurtleMenu;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.NonNullList;
import net.minecraft.core.component.DataComponentGetter;
import net.minecraft.core.component.DataComponentMap;
import net.minecraft.core.component.DataComponents;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
import net.minecraft.resources.Identifier;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.ProblemReporter;
import net.minecraft.world.Container;
import net.minecraft.world.ContainerHelper;
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.item.component.DyedItemColor;
import net.minecraft.world.item.component.TooltipDisplay;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.storage.TagValueOutput;
import net.minecraft.world.level.storage.ValueInput;
import net.minecraft.world.level.storage.ValueOutput;
import net.minecraft.world.phys.Vec3;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collections;
import java.util.function.IntSupplier;

public class TurtleBlockEntity extends AbstractComputerBlockEntity implements BasicContainer {
    private static final Logger LOG = LoggerFactory.getLogger(TurtleBlockEntity.class);

    public static final int INVENTORY_SIZE = 16;
    public static final int INVENTORY_WIDTH = 4;
    public static final int INVENTORY_HEIGHT = 4;

    enum MoveState {
        NOT_MOVED,
        IN_PROGRESS,
        MOVED
    }

    private final NonNullList<ItemStack> inventory = NonNullList.withSize(INVENTORY_SIZE, ItemStack.EMPTY);
    private final NonNullList<ItemStack> inventorySnapshot = NonNullList.withSize(INVENTORY_SIZE, ItemStack.EMPTY);
    private boolean inventoryChanged = false;

    private final IntSupplier fuelLimit;

    private TurtleBrain brain = new TurtleBrain(this);
    private MoveState moveState = MoveState.NOT_MOVED;
    private @Nullable IPeripheral peripheral;

    public TurtleBlockEntity(BlockEntityType<? extends TurtleBlockEntity> type, BlockPos pos, BlockState state, IntSupplier fuelLimit, ComputerFamily family) {
        super(type, pos, state, family);
        this.fuelLimit = fuelLimit;
    }

    boolean hasMoved() {
        return moveState == MoveState.MOVED;
    }

    @Override
    protected ServerComputer createComputer(int id) {
        var computer = new ServerComputer((ServerLevel) getLevel(), getBlockPos(), ServerComputer.properties(id, getFamily())
            .label(getLabel())
            .terminalSize(new TerminalSize(Config.TURTLE_TERM_WIDTH, Config.TURTLE_TERM_HEIGHT))
            .storageCapacity(storageCapacity)
            .addComponent(ComputerComponents.TURTLE, brain)
        );
        brain.setupComputer(computer);
        return computer;
    }

    @Override
    protected void unload() {
        if (!hasMoved()) super.unload();
    }

    @Override
    protected float getInteractRange() {
        return Container.DEFAULT_DISTANCE_BUFFER + 4;
    }

    @Override
    protected void serverTick() {
        super.serverTick();
        brain.update();
        if (inventoryChanged) {
            var computer = getServerComputer();
            if (computer != null) computer.queueEvent("turtle_inventory");
            inventoryChanged = false;
        }
    }

    protected void clientTick() {
        brain.update();
    }

    @Override
    protected void updateBlockState(ComputerState newState) {
    }

    @Override
    public void preRemoveSideEffects(BlockPos blockPos, BlockState blockState) {
        if (!hasMoved()) super.preRemoveSideEffects(blockPos, blockState);
    }

    @Override
    public void neighborChanged() {
        if (moveState == MoveState.NOT_MOVED) super.neighborChanged();
    }

    public void notifyMoveStart() {
        if (moveState == MoveState.NOT_MOVED) moveState = MoveState.IN_PROGRESS;
    }

    public void notifyMoveEnd() {
        // MoveState.MOVED is final
        if (moveState == MoveState.IN_PROGRESS) moveState = MoveState.NOT_MOVED;
    }

    @Override
    public void loadServer(ValueInput nbt) {
        super.loadServer(nbt);

        // Read inventory
        ContainerHelper.loadAllItems(nbt, inventory);
        for (var i = 0; i < inventory.size(); i++) inventorySnapshot.set(i, inventory.get(i).copy());

        // Read state
        brain.readFromNBT(nbt);
    }

    @Override
    public void saveAdditional(ValueOutput nbt) {
        // Write inventory
        ContainerHelper.saveAllItems(nbt, inventory);

        // Write brain
        brain.writeToNBT(nbt);

        super.saveAdditional(nbt);
    }

    @Override
    protected void applyImplicitComponents(DataComponentGetter component) {
        super.applyImplicitComponents(component);

        var colour = component.get(DataComponents.DYED_COLOR);
        if (colour != null) brain.setColour(colour.rgb());

        brain.setFuelLevel(component.getOrDefault(ModRegistry.DataComponents.FUEL.get(), 0));
        brain.setOverlay(component.get(ModRegistry.DataComponents.OVERLAY.get()));
        brain.setUpgrade(TurtleSide.LEFT, component.get(ModRegistry.DataComponents.LEFT_TURTLE_UPGRADE.get()));
        brain.setUpgrade(TurtleSide.RIGHT, component.get(ModRegistry.DataComponents.RIGHT_TURTLE_UPGRADE.get()));
    }

    @Override
    protected void collectSafeComponents(DataComponentMap.Builder builder) {
        super.collectSafeComponents(builder);

        if (brain.getColour() != -1) {
            builder.set(DataComponents.DYED_COLOR, new DyedItemColor(brain.getColour()));
            builder.set(DataComponents.TOOLTIP_DISPLAY, TooltipDisplay.DEFAULT.withHidden(DataComponents.DYED_COLOR, true));
        }
        builder.set(ModRegistry.DataComponents.OVERLAY.get(), brain.getOverlay());
        builder.set(ModRegistry.DataComponents.FUEL.get(), brain.getFuelLevel());
        builder.set(ModRegistry.DataComponents.LEFT_TURTLE_UPGRADE.get(), withPersistedData(brain.getUpgradeWithData(TurtleSide.LEFT)));
        builder.set(ModRegistry.DataComponents.RIGHT_TURTLE_UPGRADE.get(), withPersistedData(brain.getUpgradeWithData(TurtleSide.RIGHT)));
    }

    private static @Nullable UpgradeData<ITurtleUpgrade> withPersistedData(@Nullable UpgradeData<ITurtleUpgrade> upgrade) {
        return upgrade == null ? null : UpgradeData.of(upgrade.holder(), upgrade.upgrade().getPersistedData(upgrade.data()));
    }

    @Override
    @Deprecated
    public void removeComponentsFromTag(ValueOutput tag) {
        super.removeComponentsFromTag(tag);
        tag.discard(TurtleBrain.NBT_COLOUR);
        tag.discard(TurtleBrain.NBT_FUEL);
        tag.discard(TurtleBrain.NBT_OVERLAY);
        tag.discard(TurtleBrain.NBT_LEFT_UPGRADE);
        tag.discard(TurtleBrain.NBT_RIGHT_UPGRADE);
    }

    @Override
    protected boolean isPeripheralBlockedOnSide(ComputerSide localSide) {
        return hasPeripheralUpgradeOnSide(localSide);
    }

    @Override
    public Direction getDirection() {
        return getBlockState().getValue(TurtleBlock.FACING);
    }

    public void setDirection(Direction dir) {
        if (dir.getAxis() == Direction.Axis.Y) dir = Direction.NORTH;
        getLevel().setBlockAndUpdate(worldPosition, getBlockState().setValue(TurtleBlock.FACING, dir));

        updateRedstone();
        updateInputsImmediately();

        onTileEntityChange();
    }

    public @Nullable ITurtleUpgrade getUpgrade(TurtleSide side) {
        return brain.getUpgrade(side);
    }

    public int getColour() {
        return brain.getColour();
    }

    public @Nullable Identifier getOverlay() {
        return brain.getOverlay();
    }

    public ITurtleAccess getAccess() {
        return brain;
    }

    public Vec3 getRenderOffset(float f) {
        return brain.getRenderOffset(f);
    }

    public float getRenderYaw(float f) {
        return brain.getVisualYaw(f);
    }

    public float getToolRenderAngle(TurtleSide side, float f) {
        return brain.getToolRenderAngle(side, f);
    }

    void setOwningPlayer(GameProfile player) {
        brain.setOwningPlayer(player);
        onTileEntityChange();
    }

    // IInventory

    @Override
    public NonNullList<ItemStack> getItems() {
        return inventory;
    }

    public ItemStack getItemSnapshot(int slot) {
        return slot >= 0 && slot < inventorySnapshot.size() ? inventorySnapshot.get(slot) : ItemStack.EMPTY;
    }

    @Override
    public void setChanged() {
        super.setChanged();

        for (var slot = 0; slot < getContainerSize(); slot++) {
            var item = getItem(slot);
            if (ItemStack.matches(item, inventorySnapshot.get(slot))) continue;

            inventoryChanged = true;
            inventorySnapshot.set(slot, item.copy());
        }
    }

    @Override
    public boolean stillValid(Player player) {
        return isUsable(player);
    }

    public void onTileEntityChange() {
        super.setChanged();
    }

    // Networking stuff

    @Override
    public final ClientboundBlockEntityDataPacket getUpdatePacket() {
        return ClientboundBlockEntityDataPacket.create(this);
    }

    @Override
    public CompoundTag getUpdateTag(HolderLookup.Provider registries) {
        try (var problems = new ProblemReporter.ScopedCollector(problemPath(), LOG)) {
            var nbt = TagValueOutput.createWithContext(problems, registries);
            if (label != null) nbt.putString(NBT_LABEL, label);
            brain.writeDescription(nbt);
            return nbt.buildResult();
        }
    }

    @Override
    public void loadClient(ValueInput nbt) {
        super.loadClient(nbt);
        label = nbt.getString(NBT_LABEL).orElse(null);
        brain.readDescription(nbt);
    }

    // Privates

    public int getFuelLimit() {
        return fuelLimit.getAsInt();
    }

    private boolean hasPeripheralUpgradeOnSide(ComputerSide side) {
        ITurtleUpgrade upgrade;
        switch (side) {
            case RIGHT:
                upgrade = getUpgrade(TurtleSide.RIGHT);
                break;
            case LEFT:
                upgrade = getUpgrade(TurtleSide.LEFT);
                break;
            default:
                return false;
        }
        return upgrade != null && upgrade.getUpgradeType().isPeripheral();
    }

    public void transferStateFrom(TurtleBlockEntity copy) {
        super.transferStateFrom(copy);
        Collections.copy(inventory, copy.inventory);
        Collections.copy(inventorySnapshot, copy.inventorySnapshot);
        inventoryChanged = copy.inventoryChanged;
        brain = copy.brain;
        brain.setOwner(this);

        // Mark the other turtle as having moved, and so its peripheral is dead.
        copy.moveState = MoveState.MOVED;
        PlatformHelper.get().invalidateComponent(this);
    }

    @Nullable
    public IPeripheral peripheral() {
        if (hasMoved()) return null;
        if (peripheral != null) return peripheral;
        return peripheral = new ComputerPeripheral("turtle", this);
    }

    @Nullable
    @Override
    public AbstractContainerMenu createMenu(int id, Inventory inventory, Player player) {
        return TurtleMenu.ofBrain(id, inventory, brain);
    }
}
