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

package dan200.computercraft.client.gui;

import dan200.computercraft.client.gui.widgets.ComputerSidebar;
import dan200.computercraft.client.gui.widgets.DynamicImageButton;
import dan200.computercraft.client.gui.widgets.TerminalWidget;
import dan200.computercraft.client.network.ClientNetworking;
import dan200.computercraft.core.input.UserComputerInput;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.shared.computer.core.ComputerFamily;
import dan200.computercraft.shared.computer.inventory.AbstractComputerMenu;
import dan200.computercraft.shared.computer.upload.FileUpload;
import dan200.computercraft.shared.computer.upload.UploadResult;
import dan200.computercraft.shared.config.Config;
import dan200.computercraft.shared.network.server.UploadFileMessage;
import net.minecraft.ChatFormatting;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.events.GuiEventListener;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.client.input.KeyEvent;
import net.minecraft.client.input.MouseButtonEvent;
import net.minecraft.network.chat.Component;
import net.minecraft.util.Util;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
import org.jspecify.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static dan200.computercraft.core.util.Nullability.assertNonNull;

/**
 * The base class of all screens with a computer terminal (i.e. {@link ComputerScreen}). This works with
 * {@link AbstractComputerMenu} to handle the common behaviour such as the terminal, input and file uploading.
 *
 * @param <T> The concrete type of the associated menu.
 */
public abstract class AbstractComputerScreen<T extends AbstractComputerMenu> extends AbstractContainerScreen<T> {
    private static final Logger LOG = LoggerFactory.getLogger(AbstractComputerScreen.class);

    private static final Component OK = Component.translatable("gui.ok");
    private static final Component NO_RESPONSE_TITLE = Component.translatable("gui.computercraft.upload.no_response");
    private static final Component NO_RESPONSE_MSG = Component.translatable("gui.computercraft.upload.no_response.msg",
        Component.literal("import").withStyle(ChatFormatting.DARK_GRAY));

    protected @Nullable TerminalWidget terminal;
    protected Terminal terminalData;
    protected final ComputerFamily family;
    protected final UserComputerInput computerInput;
    protected final ClientComputerActions computerActions;

    protected final int sidebarYOffset;

    private long uploadNagDeadline = Long.MAX_VALUE;
    private final int uploadMaxSize;
    private final ItemStack displayStack;

    public AbstractComputerScreen(T container, Inventory player, Component title, int sidebarYOffset, int width, int height) {
        super(container, player, title, width, height);
        terminalData = container.getTerminal();
        family = container.getFamily();
        displayStack = container.getDisplayStack();
        uploadMaxSize = container.getUploadMaxSize();
        computerInput = new UserComputerInput(new ClientComputerInput(menu), menu.getTerminal());
        computerActions = new ClientComputerActions(menu);
        this.sidebarYOffset = sidebarYOffset;
    }

    protected abstract TerminalWidget createTerminal();

    protected final TerminalWidget getTerminal() {
        if (terminal == null) throw new IllegalStateException("Screen has not been initialised yet");
        return terminal;
    }

    @Override
    protected void init() {
        super.init();

        terminal = addRenderableWidget(createTerminal());
        ComputerSidebar.addButtons(menu::isOn, computerActions, this::addRenderableWidget, leftPos, topPos + sidebarYOffset);
        setFocused(terminal);
    }

    @Override
    public void containerTick() {
        super.containerTick();
        getTerminal().update();

        if (uploadNagDeadline != Long.MAX_VALUE && Util.getNanos() >= uploadNagDeadline) {
            new ItemToast(minecraft, displayStack, NO_RESPONSE_TITLE, NO_RESPONSE_MSG, ItemToast.TRANSFER_NO_RESPONSE_TOKEN)
                .showOrReplace(minecraft.gui.toastManager());
            uploadNagDeadline = Long.MAX_VALUE;
        }
    }

    @Override
    public boolean keyPressed(KeyEvent event) {
        // Forward the tab key to the terminal, rather than moving between controls.
        if (event.key() == GLFW.GLFW_KEY_TAB && getFocused() != null && getFocused() == terminal) {
            return getFocused().keyPressed(event);
        }

        return super.keyPressed(event);
    }

    @Override
    public boolean mouseReleased(MouseButtonEvent event) {
        // Reimplement ContainerEventHandler.mouseReleased, as it's not called in vanilla (it is in Forge, but that
        // shouldn't matter).
        setDragging(false);
        var child = getChildAt(event.x(), event.y());
        if (child.isPresent() && child.get().mouseReleased(event)) return true;

        return super.mouseReleased(event);
    }

    @Override
    public boolean mouseScrolled(double mouseX, double mouseY, double scrollX, double scrollY) {
        // Reimplement ContainerEventHandler.mouseScrolled, as AbstractContainerScreen overrides it.
        var child = getChildAt(mouseX, mouseY);
        if (child.isPresent() && child.get().mouseScrolled(mouseX, mouseY, scrollX, scrollY)) return true;

        return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY);
    }

    @Override
    public boolean mouseClicked(MouseButtonEvent event, boolean doubleClick) {
        var changed = super.mouseClicked(event, doubleClick);
        // Clicking the terminate/shutdown button steals focus, which means then pressing "enter" will click the button
        // again. Restore the focus to the terminal in these cases.
        if (getFocused() instanceof DynamicImageButton) setFocused(terminal);
        return changed;
    }

    @Override
    public boolean mouseDragged(MouseButtonEvent event, double deltaX, double deltaY) {
        return (getFocused() != null && getFocused().mouseDragged(event, deltaX, deltaY))
            || super.mouseDragged(event, deltaX, deltaY);
    }

    @Override
    public void setFocused(@Nullable GuiEventListener listener) {
        // Don't clear and re-focus if we're already focused.
        if (listener != getFocused()) super.setFocused(listener);
    }

    @Override
    protected void extractLabels(GuiGraphicsExtractor graphics, int mouseX, int mouseY) {
        // Skip rendering labels.
    }

    @Override
    public void onFilesDrop(List<Path> files) {
        if (files.isEmpty()) return;

        if (!menu.isOn()) {
            alert(UploadResult.FAILED_TITLE, UploadResult.COMPUTER_OFF_MSG);
            return;
        }

        long size = 0;

        List<FileUpload> toUpload = new ArrayList<>();
        for (var file : files) {
            // TODO: Recurse directories? If so, we probably want to shunt this off-thread.
            if (!Files.isRegularFile(file)) continue;

            try (var sbc = Files.newByteChannel(file)) {
                var fileSize = sbc.size();
                if (fileSize > uploadMaxSize || (size += fileSize) >= uploadMaxSize) {
                    alert(UploadResult.FAILED_TITLE, UploadResult.TOO_MUCH_MSG);
                    return;
                }

                var name = file.getFileName().toString();
                if (name.length() > UploadFileMessage.MAX_FILE_NAME) {
                    alert(UploadResult.FAILED_TITLE, Component.translatable("gui.computercraft.upload.failed.name_too_long"));
                    return;
                }

                var buffer = ByteBuffer.allocateDirect((int) fileSize);
                sbc.read(buffer);
                buffer.flip();

                var digest = FileUpload.getDigest(buffer);
                if (digest == null) {
                    alert(UploadResult.FAILED_TITLE, Component.translatable("gui.computercraft.upload.failed.corrupted"));
                    return;
                }

                toUpload.add(new FileUpload(name, buffer, digest));
            } catch (IOException e) {
                LOG.error("Failed uploading files", e);
                alert(UploadResult.FAILED_TITLE, Component.translatable("gui.computercraft.upload.failed.generic", "Cannot compute checksum"));
            }
        }

        if (toUpload.size() > UploadFileMessage.MAX_FILES) {
            alert(UploadResult.FAILED_TITLE, Component.translatable("gui.computercraft.upload.failed.too_many_files"));
            return;
        }

        if (!toUpload.isEmpty()) UploadFileMessage.send(menu, toUpload, ClientNetworking::sendToServer);
    }

    public void uploadResult(UploadResult result, @Nullable Component message) {
        switch (result) {
            case QUEUED -> {
                if (Config.uploadNagDelay > 0) {
                    uploadNagDeadline = Util.getNanos() + TimeUnit.SECONDS.toNanos(Config.uploadNagDelay);
                }
            }
            case CONSUMED -> uploadNagDeadline = Long.MAX_VALUE;
            case ERROR -> alert(UploadResult.FAILED_TITLE, assertNonNull(message));
        }
    }

    private void alert(Component title, Component message) {
        OptionScreen.show(minecraft, this, title, message,
            List.of(OptionScreen.newButton(OK, b -> minecraft.gui.setScreen(this)))
        );
    }
}
