/*
 * Copyright (C) 2012 Brendan Robert (BLuRry) brendan.robert@gmail.com.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301  USA
 */
package jace.core;

import jace.state.Stateful;
import jace.config.ConfigurableField;
import jace.config.InvokableAction;
import javafx.scene.image.Image;
import javafx.scene.image.WritableImage;

/**
 * Generic abstraction of a 560x192 video output device which renders 40 columns
 * per scanline. This also triggers VBL and updates the physical screen.
 * Subclasses are used to manage actual rendering via ScreenWriter
 * implementations.
 * Created on November 10, 2006, 4:29 PM
 *
 * @author Brendan Robert (BLuRry) brendan.robert@gmail.com 
 */
@Stateful
public abstract class Video extends Device {

    @Stateful
    WritableImage video;
    WritableImage visible;
    VideoWriter currentWriter;
    private byte floatingBus = 0;
    private int width = 560;
    private int height = 192;
    @Stateful
    public int x = 0;
    @Stateful
    public int y = 0;
    @Stateful
    public int scannerAddress;
    @Stateful
    public int vPeriod = 0;
    @Stateful
    public int hPeriod = 0;
    static final public int CYCLES_PER_LINE = 65;
    static final public int TOTAL_LINES = 262;
    static final public int APPLE_CYCLES_PER_LINE = 40;
    static final public int APPLE_SCREEN_LINES = 192;
    static final public int HBLANK = CYCLES_PER_LINE - APPLE_CYCLES_PER_LINE;
    static final public int VBLANK = (TOTAL_LINES - APPLE_SCREEN_LINES) * CYCLES_PER_LINE;
    static public int[] textOffset;
    static public int[] hiresOffset;
    static public int[] textRowLookup;
    static public int[] hiresRowLookup;
    private boolean screenDirty;
    private boolean lineDirty;
    private boolean isVblank = false;
    static VideoWriter[][] writerCheck = new VideoWriter[40][192];

    static {
        textOffset = new int[192];
        hiresOffset = new int[192];
        textRowLookup = new int[0x0400];
        hiresRowLookup = new int[0x02000];
        for (int i = 0; i < 192; i++) {
            textOffset[i] = calculateTextOffset(i >> 3);
            hiresOffset[i] = calculateHiresOffset(i);
        }
        for (int i = 0; i < 0x0400; i++) {
            textRowLookup[i] = identifyTextRow(i);
        }
        for (int i = 0; i < 0x2000; i++) {
            hiresRowLookup[i] = identifyHiresRow(i);
        }
    }
    private int forceRedrawRowCount = 0;
    Thread updateThread;

    /**
     * Creates a new instance of Video
     * @param computer
     */
    public Video(Computer computer) {
        super(computer);
        suspend();
        video = new WritableImage(560, 192);
        visible = new WritableImage(560, 192);
        vPeriod = 0;
        hPeriod = 0;
        forceRefresh();
    }

    public void setWidth(int w) {
        width = w;
    }
    
    public int getWidth() {
        return width;
    }
    
    public void setHeight(int h) {
        height = h;
    }
    
    public int getHeight() {
        return height;
    }

    public VideoWriter getCurrentWriter() {
        return currentWriter;
    }

    public void setCurrentWriter(VideoWriter currentWriter) {
        if (this.currentWriter != currentWriter || currentWriter.isMixed()) {
            this.currentWriter = currentWriter;
            forceRedrawRowCount = APPLE_SCREEN_LINES + 1;
        }
    }
    @ConfigurableField(category = "video", name = "Min. Screen Refesh", defaultValue = "15", description = "Minimum number of miliseconds to wait before trying to redraw.")
    public static int MIN_SCREEN_REFRESH = 15;

    Runnable redrawScreen = () -> {
        if (computer.getRunningProperty().get()) {
            visible.getPixelWriter().setPixels(0, 0, 560, 192, video.getPixelReader(), 0, 0);
        }
    };
    public void redraw() {
        screenDirty = false;
        javafx.application.Platform.runLater(redrawScreen);
    }

    public void vblankStart() {
        if (screenDirty && isRunning()) {
            redraw();
        }
    }

    abstract public void vblankEnd();

    abstract public void hblankStart(WritableImage screen, int y, boolean isDirty);

    public void setScannerLocation(int loc) {
        scannerAddress = loc;
    }

    @Override
    public void tick() {
        setScannerLocation(currentWriter.getYOffset(y));
        setFloatingBus(computer.getMemory().readRaw(scannerAddress + x));
        if (hPeriod > 0) {
            hPeriod--;
            if (hPeriod == 0) {
                x = -1;
            }
        } else {
            if (!isVblank && x < (APPLE_CYCLES_PER_LINE-1)) {
                draw();
            }
            if (x >= APPLE_CYCLES_PER_LINE - 1) {
                int yy = y + hblankOffsetY;
                if (yy < 0) {
                    yy += APPLE_SCREEN_LINES;
                }
                if (yy >= APPLE_SCREEN_LINES) {
                    yy -= (TOTAL_LINES - APPLE_SCREEN_LINES);
                }
                x = hblankOffsetX - 1;
                if (!isVblank) {
                    if (lineDirty) {
                        screenDirty = true;
                        currentWriter.clearDirty(y);
                    }
                    hblankStart(video, y, lineDirty);
                    lineDirty = false;
                    forceRedrawRowCount--;
                }
                hPeriod = HBLANK;
                y++;
                if (y >= APPLE_SCREEN_LINES) {
                    if (!isVblank) {
                        y = APPLE_SCREEN_LINES - (TOTAL_LINES - APPLE_SCREEN_LINES);
                        isVblank = true;
                        vblankStart();
                        computer.getMotherboard().vblankStart();
                    } else {
                        y = 0;
                        isVblank = false;
                        vblankEnd();
                        computer.getMotherboard().vblankEnd();
                    }
                }
            }
        }
        x++;
    }

    abstract public void configureVideoMode();

    protected static int byteDoubler(byte b) {
        int num =
                // Skip hi-bit because it's not used in display
                //                ((b&0x080)<<7) |
                ((b & 0x040) << 6)
                | ((b & 0x020) << 5)
                | ((b & 0x010) << 4)
                | ((b & 0x08) << 3)
                | ((b & 0x04) << 2)
                | ((b & 0x02) << 1)
                | (b & 0x01);
        return num | (num << 1);
    }
    @ConfigurableField(name = "Waits per cycle", category = "Advanced", description = "Adjust the delay for the scanner")
    public static int waitsPerCycle = 0;
    @ConfigurableField(name = "Hblank X offset", category = "Advanced", description = "Adjust where the hblank period starts relative to the start of the line")
    public static int hblankOffsetX = -29;
    @ConfigurableField(name = "Hblank Y offset", category = "Advanced", description = "Adjust which line the HBLANK starts on (0=current, 1=next, etc)")
    public static int hblankOffsetY = 1;

    private void draw() {
        if (lineDirty || forceRedrawRowCount > 0 || currentWriter.isRowDirty(y)) {
            lineDirty = true;
            currentWriter.displayByte(video, x, y, textOffset[y], hiresOffset[y]);
        }
        setWaitCycles(waitsPerCycle);
        doPostDraw();
    }

    static public int calculateHiresOffset(int y) {
        return calculateTextOffset(y >> 3) + ((y & 7) << 10);
    }

    static public int calculateTextOffset(int y) {
        return ((y & 7) << 7) + 40 * (y >> 3);
    }

    static public int identifyTextRow(int y) {
        //floor((x-1024)/128) + floor(((x-1024)%128)/40)*8
        // Caller must check result is <= 23, if so then they are in a screenhole!
        return (y >> 7) + (((y & 0x7f) / 40) << 3);
    }

    static public int identifyHiresRow(int y) {
        int blockOffset = identifyTextRow(y & 0x03ff);
        // Caller must check results is > 0, if not then they are in a screenhole!
        if (blockOffset > 23) {
            return -1;
        }
        return ((y >> 10) & 7) + (blockOffset << 3);
    }

    public abstract void doPostDraw();

    public byte getFloatingBus() {
        return floatingBus;
    }

    private void setFloatingBus(byte floatingBus) {
        this.floatingBus = floatingBus;
    }

    @InvokableAction(name = "Refresh screen",
    category = "display",
    description = "Marks screen contents as changed, forcing full screen redraw",
    alternatives = "redraw",
    defaultKeyMapping = "ctrl+shift+r")
    public final void forceRefresh() {
        lineDirty = true;
        screenDirty = true;
        forceRedrawRowCount = APPLE_SCREEN_LINES + 1;
    }

    @Override
    public String getShortName() {
        return "vid";
    }

    public Image getFrameBuffer() {
        return visible;
    }
}
