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

package dan200.computercraft.client.render.text;

import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import dan200.computercraft.client.FrameInfo;
import dan200.computercraft.core.terminal.Palette;
import dan200.computercraft.core.terminal.Terminal;
import dan200.computercraft.core.terminal.TextBuffer;
import dan200.computercraft.core.util.Colour;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.client.renderer.rendertype.RenderType;
import net.minecraft.client.renderer.rendertype.RenderTypes;
import net.minecraft.resources.Identifier;
import net.minecraft.util.ARGB;
import net.minecraft.util.LightCoordsUtil;
import org.joml.Matrix4f;
import org.joml.Vector3f;

/**
 * Handles rendering fixed width text and computer terminals.
 * <p>
 * This class has several modes of usage:
 * <ul>
 * <li>{@link #drawString}: Drawing basic text without a terminal (such as for printouts). Unlike the other methods,
 * this accepts a lightmap coordinate as, unlike terminals, printed pages render fullbright.</li>
 * <li>{@link #drawTerminal}: Draw a terminal with a cursor. This is used by the various computer GUIs to render the
 * whole term.</li>
 * </ul>
 *
 * <strong>IMPORTANT: </strong> When making changes to this class, please check if you need to make the same changes to
 * {@link DirectFixedWidthFontRenderer}.
 */
public final class FixedWidthFontRenderer {
    public static final Identifier FONT = Identifier.fromNamespaceAndPath("computercraft", "textures/gui/term_font.png");

    /**
     * A render type for terminal text.
     */
    public static final RenderType TERMINAL_TEXT = RenderTypes.text(FONT);

    public static final RenderType TERMINAL_TEXT_OFFSET = RenderTypes.textPolygonOffset(FONT);

    public static final int FONT_HEIGHT = 9;
    public static final int FONT_WIDTH = 6;
    static final float WIDTH = 256.0f;

    static final float BACKGROUND_START = (WIDTH - 6.0f) / WIDTH;
    static final float BACKGROUND_END = (WIDTH - 4.0f) / WIDTH;

    private static final int BLACK = ARGB.color(255, byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()), byteColour(Colour.BLACK.getR()));
    private static final float Z_OFFSET = 1e-4f;

    private FixedWidthFontRenderer() {
    }

    private static byte byteColour(float c) {
        return (byte) (int) (c * 255);
    }

    public static float toGreyscale(double[] rgb) {
        return (float) ((rgb[0] + rgb[1] + rgb[2]) / 3);
    }

    public static int getColour(char c, Colour def) {
        return 15 - Terminal.getColour(c, def);
    }

    private static void drawChar(Matrix4f matrix, VertexConsumer buffer, float x, float y, int index, int colour, int light) {
        // Short circuit to avoid the common case - the texture should be blank here after all.
        if (index == '\0' || index == ' ') return;

        var column = index % 16;
        var row = index / 16;

        var xStart = 1 + column * (FONT_WIDTH + 2);
        var yStart = 1 + row * (FONT_HEIGHT + 2);

        quad(
            matrix, buffer, x, y, x + FONT_WIDTH, y + FONT_HEIGHT, 0, colour,
            xStart / WIDTH, yStart / WIDTH, (xStart + FONT_WIDTH) / WIDTH, (yStart + FONT_HEIGHT) / WIDTH, light
        );
    }

    public static void drawQuad(Matrix4f matrix, VertexConsumer buffer, float x, float y, float z, float width, float height, int colour, int light) {
        quad(matrix, buffer, x, y, x + width, y + height, z, colour, BACKGROUND_START, BACKGROUND_START, BACKGROUND_END, BACKGROUND_END, light);
    }

    private static void drawQuad(Matrix4f matrix, VertexConsumer buffer, float x, float y, float width, float height, Palette palette, char colourIndex, int light) {
        var colour = palette.getRenderColours(getColour(colourIndex, Colour.BLACK));
        drawQuad(matrix, buffer, x, y, 0, width, height, colour, light);
    }

    private static void drawBackground(
        Matrix4f matrix, VertexConsumer buffer, float x, float y, TextBuffer backgroundColour, Palette palette,
        float leftMarginSize, float rightMarginSize, float height, int light
    ) {
        if (leftMarginSize > 0) {
            drawQuad(matrix, buffer, x - leftMarginSize, y, leftMarginSize, height, palette, backgroundColour.charAt(0), light);
        }

        if (rightMarginSize > 0) {
            drawQuad(matrix, buffer, x + backgroundColour.length() * FONT_WIDTH, y, rightMarginSize, height, palette, backgroundColour.charAt(backgroundColour.length() - 1), light);
        }

        // Batch together runs of identical background cells.
        var blockStart = 0;
        var blockColour = '\0';
        for (var i = 0; i < backgroundColour.length(); i++) {
            var colourIndex = backgroundColour.charAt(i);
            if (colourIndex == blockColour) continue;

            if (blockColour != '\0') {
                drawQuad(matrix, buffer, x + blockStart * FONT_WIDTH, y, FONT_WIDTH * (i - blockStart), height, palette, blockColour, light);
            }

            blockColour = colourIndex;
            blockStart = i;
        }

        if (blockColour != '\0') {
            drawQuad(matrix, buffer, x + blockStart * FONT_WIDTH, y, FONT_WIDTH * (backgroundColour.length() - blockStart), height, palette, blockColour, light);
        }
    }

    public static void drawString(Matrix4f matrix, VertexConsumer buffer, float x, float y, TextBuffer text, TextBuffer textColour, Palette palette, int light) {
        for (var i = 0; i < text.length(); i++) {
            var colour = palette.getRenderColours(getColour(textColour.charAt(i), Colour.BLACK));

            int index = text.charAt(i);
            if (index > 255) index = '?';
            drawChar(matrix, buffer, x + i * FONT_WIDTH, y, index, colour, light);
        }

    }

    public static void drawTerminalForeground(Matrix4f matrix, VertexConsumer buffer, float x, float y, Terminal terminal) {
        var palette = terminal.getPalette();
        var height = terminal.getHeight();

        // The main text
        for (var i = 0; i < height; i++) {
            var rowY = y + FONT_HEIGHT * i;
            drawString(
                matrix, buffer, x, rowY, terminal.getLine(i), terminal.getTextColourLine(i),
                palette, LightCoordsUtil.FULL_BRIGHT
            );
        }
    }

    public static void drawTerminalBackground(
        Matrix4f matrix, VertexConsumer buffer, float x, float y, Terminal terminal,
        float topMarginSize, float bottomMarginSize, float leftMarginSize, float rightMarginSize
    ) {
        var palette = terminal.getPalette();
        var height = terminal.getHeight();

        // Top and bottom margins
        drawBackground(
            matrix, buffer, x, y - topMarginSize, terminal.getBackgroundColourLine(0), palette,
            leftMarginSize, rightMarginSize, topMarginSize, LightCoordsUtil.FULL_BRIGHT
        );

        drawBackground(
            matrix, buffer, x, y + height * FONT_HEIGHT, terminal.getBackgroundColourLine(height - 1), palette,
            leftMarginSize, rightMarginSize, bottomMarginSize, LightCoordsUtil.FULL_BRIGHT
        );

        // The main text
        for (var i = 0; i < height; i++) {
            var rowY = y + FONT_HEIGHT * i;
            drawBackground(
                matrix, buffer, x, rowY, terminal.getBackgroundColourLine(i), palette,
                leftMarginSize, rightMarginSize, FONT_HEIGHT, LightCoordsUtil.FULL_BRIGHT
            );
        }
    }

    public static boolean isCursorVisible(Terminal terminal) {
        if (!terminal.getCursorBlink()) return false;

        var cursorX = terminal.getCursorX();
        var cursorY = terminal.getCursorY();
        return cursorX >= 0 && cursorX < terminal.getWidth() && cursorY >= 0 && cursorY < terminal.getHeight();
    }

    public static void drawCursor(Matrix4f matrix, VertexConsumer buffer, float x, float y, Terminal terminal) {
        if (isCursorVisible(terminal) && FrameInfo.getGlobalCursorBlink()) {
            var colour = terminal.getPalette().getRenderColours(15 - terminal.getTextColour());
            drawChar(matrix, buffer, x + terminal.getCursorX() * FONT_WIDTH, y + terminal.getCursorY() * FONT_HEIGHT, '_', colour, LightCoordsUtil.FULL_BRIGHT);
        }
    }

    public static void drawTerminal(
        Matrix4f matrix, VertexConsumer buffer, float x, float y, Terminal terminal,
        float topMarginSize, float bottomMarginSize, float leftMarginSize, float rightMarginSize
    ) {
        drawTerminalBackground(
            matrix, buffer, x, y, terminal,
            topMarginSize, bottomMarginSize, leftMarginSize, rightMarginSize
        );

        // Render the foreground with a slight offset. By calling .translate() on the matrix itself, we're translating
        // in screen space, rather than in model/view space.
        // It's definitely not perfect, but better than z fighting!
        var offsetMatrix = new Matrix4f(matrix).translate(new Vector3f(0, 0, Z_OFFSET));

        drawTerminalForeground(offsetMatrix, buffer, x, y, terminal);
        drawCursor(offsetMatrix, buffer, x, y, terminal);
    }

    public static void drawEmptyTerminal(PoseStack transform, SubmitNodeCollector collector, float x, float y, float width, float height) {
        collector.submitCustomGeometry(transform, FixedWidthFontRenderer.TERMINAL_TEXT, (pose, buffer) ->
            drawQuad(pose.pose(), buffer, x, y, 0, width, height, BLACK, LightCoordsUtil.FULL_BRIGHT));
    }

    private static void quad(Matrix4f matrix, VertexConsumer buffer, float x1, float y1, float x2, float y2, float z, int colour, float u1, float v1, float u2, float v2, int light) {
        buffer.addVertex(matrix, x1, y1, z).setColor(colour).setUv(u1, v1).setLight(light);
        buffer.addVertex(matrix, x1, y2, z).setColor(colour).setUv(u1, v2).setLight(light);
        buffer.addVertex(matrix, x2, y2, z).setColor(colour).setUv(u2, v2).setLight(light);
        buffer.addVertex(matrix, x2, y1, z).setColor(colour).setUv(u2, v1).setLight(light);
    }
}
