package indi.key.mipsemulator.controller.component;

import indi.key.mipsemulator.core.controller.Machine;
import indi.key.mipsemulator.core.controller.TimingRenderer;
import indi.key.mipsemulator.model.info.BitArray;
import indi.key.mipsemulator.model.interfaces.TickCallback;
import indi.key.mipsemulator.storage.AlternativeMemory;
import indi.key.mipsemulator.storage.MemorySelectedCallback;
import indi.key.mipsemulator.storage.MemoryType;
import indi.key.mipsemulator.storage.RegisterMemory;
import javafx.application.Platform;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

// FIXME: In some case the canvas will be killed and cannot draw anything. It seems to result from JavaFX internal widgets.
public class SegmentController implements TickCallback {

    private Machine machine;
    private GraphicsContext gc;
    private double length, padding;
    private SegmentMemory segmentMem;

    private static final double LINE_WIDTH = 4.5;
    private static final Color RED = Color.RED;
    private static final Color GRAY = Color.rgb(220, 220, 220);
    private static final int[] SEG_MAP_2 = new int[]{
            0, 4, 16, 25, 17, 5, 12, 24,
            1, 6, 18, 27, 19, 7, 13, 26,
            2, 8, 20, 29, 21, 9, 14, 28,
            3, 10, 22, 31, 23, 11, 15, 30
    };
    private static final int[] SEG_MAP = new int[]{
            3, 10, 22, 31, 23, 11, 15, 30,
            2, 8, 20, 29, 21, 9, 14, 28,
            1, 6, 18, 27, 19, 7, 13, 26,
            0, 4, 16, 25, 17, 5, 12, 24
    };
//    private static final int[] SEG_MAP = new int[]{
//            0, 5, 17, 25, 16, 4, 12, 24,
//            1, 7, 19, 27, 18, 6, 13, 26,
//            2, 9, 21, 29, 20, 8, 14, 28,
//            3, 11, 23, 31, 22, 10, 15, 30
//    };
    private static final int[] LINE_INDEX = new int[]{
            0, 0, 0,
            1, 0, 1,
            1, 1, 1,
            0, 2, 0,
            0, 1, 1,
            0, 0, 1,
            0, 1, 0
    };
    private static final int[] HEX_SEG_MAP = new int[]{
            0b1000000, 0b1111001, 0b0100100, 0b0110000,
            0b0011001, 0b0010010, 0b0000010, 0b1111000,
            0b0000000, 0b0010000, 0b0001000, 0b0000011,
            0b1000110, 0b0100001, 0b0000110, 0b0001110
    };

    public SegmentController(Canvas canvas, Machine machine) {
        this.machine = machine;
        this.gc = canvas.getGraphicsContext2D();
        gc.setStroke(Color.RED);
        gc.setLineWidth(LINE_WIDTH);
        length = canvas.getWidth() / 16;
        padding = canvas.getHeight() / 2 - length;
        segmentMem = (SegmentMemory) machine.getAddressRedirector().getMemory(MemoryType.SEGMENT);
        TimingRenderer.register(this);
    }


    /**
     * show 32 * 2 bits as graph.
     *
     * @param low  the right side of graph, 32bits
     * @param high the left side of graph, 32bits
     */
    private void showGraph(int low, int high) {
        gc.clearRect(0, 0, gc.getCanvas().getWidth(), gc.getCanvas().getHeight());
        BitArray lowBitArray = BitArray.ofValue(low);
        BitArray highBitArray = BitArray.ofValue(high);
        byte[] lowBytes = mapToGraph(lowBitArray); // 4*8 bits
        byte[] highBytes = mapToGraph(highBitArray); // 4*8 bits
        for (int i = 0; i < 4; i++) {
            showNumber(lowBytes[i], 7 - i);
        }
        for (int i = 4; i < 8; i++) {
            showNumber(highBytes[i - 4], 7 - i);
        }
    }

    private byte[] mapToGraph(BitArray raw) {
        BitArray result = BitArray.ofLength(32);
        for (int i = 0; i < 32; i++) {
            result.set(i, raw.get(SEG_MAP[i % 32]));
        }
        return result.bytes();
    }

    /**
     * show 32 bits as text.
     *
     * @param hexes 32 bits data
     * @param point 8 bits points
     */
    private void showText(int hexes, byte point) {
        gc.clearRect(0, 0, gc.getCanvas().getWidth(), gc.getCanvas().getHeight());
        BitArray bitArray = BitArray.ofValue(hexes);
        BitArray pointBits = BitArray.of(point, 8);
        for (int i = 0; i < 8; i++) {
            int hex = bitArray.subArray(i * 4, i * 4 + 4).value();
            BitArray hexBitArray = BitArray.of(HEX_SEG_MAP[hex], 8);
            hexBitArray.set(7, pointBits.get(i));
            showNumber(hexBitArray.bytes()[0], 7 - i);
        }
    }

    /**
     * showNumber a given number with its point to the segment canvas.
     *
     * @param data  the 8-bits data represents a number.
     * @param index the index from left
     */
    private void showNumber(byte data, int index) {
        int k = 1;
        for (int i = 0; i < 21; i += 3) {
            drawLine(data & k, index, LINE_INDEX[i], LINE_INDEX[i + 1], LINE_INDEX[i + 2]);
            k <<= 1;
        }
        drawPoint(data & k, index);
    }

    @SuppressWarnings("SuspiciousNameCombination") // stupid warnings from IDEA, orz...
    private void drawLine(int flag, int index, int xIndex1, int yIndex1, int orientation) {
        double x1 = (index * 2 + xIndex1) * length;
        double y1 = padding + yIndex1 * length;
        double x2, y2;
        if (orientation == 1) { // Vertical
            x2 = x1;
            y2 = y1 + length;
            y1 += LINE_WIDTH;
            y2 -= LINE_WIDTH;
        } else {
            y2 = y1;
            x2 = x1 + length;
            x1 += LINE_WIDTH;
            x2 -= LINE_WIDTH;
        }
        gc.setStroke((flag == 0) ? RED : GRAY);
        gc.strokeLine(x1 + LINE_WIDTH, y1, x2 + LINE_WIDTH, y2);
    }

    private void drawPoint(int flag, int index) {
        gc.setFill((flag == 0) ? RED : GRAY);
        double x = (index * 2 + 1) * length + 2 * LINE_WIDTH;
        double y = padding + length * 2 + LINE_WIDTH / 2;
        gc.fillOval(x, y - LINE_WIDTH, LINE_WIDTH, LINE_WIDTH);
    }

    @Override
    public void onTick(long ticks) {
        Platform.runLater(() -> {
            boolean text = machine.getSwitches().get(0);
            if (text) {
                showText(segmentMem.getText(), (byte) ~0);
            } else {
                showGraph(segmentMem.getGraphLow(), segmentMem.getGraphHigh());
            }
        });
    }

    public static class SegmentMemory extends AlternativeMemory {

        private RegisterMemory highGraph, lowGraph, text;

        public SegmentMemory(int depth) {
            highGraph = new RegisterMemory();
            lowGraph = new RegisterMemory();
            text = new RegisterMemory();
        }

        @Override
        protected int selectMemory(long address, boolean isRead, int length, MemorySelectedCallback callback, int param) {
            if (isRead) {
                return callback.onMemorySelected(text, address, param);
            } else {
                if ((address & 1) == 0) {
                    callback.onMemorySelected(lowGraph, 0, param);
                } else {
                    callback.onMemorySelected(highGraph, 0, param);
                }
                return callback.onMemorySelected(text, address, param);
            }
        }

        int getText() {
            return text.loadWord(0);
        }

        int getGraphLow() {
            return lowGraph.loadWord(0);
        }

        int getGraphHigh() {
            return highGraph.loadWord(0);
        }

        @Override
        public void reset() {
            highGraph.reset();
            lowGraph.reset();
            text.reset();
        }
    }
}