/src/PixelEditor.js
import Controller from './Controller';

export default class PixelEditor extends Controller {
    constructor(appState) {
        super(appState);
        this.node = document.getElementById('pixelEditor');

        this.node.addEventListener('mousedown', event => this.penDown(event));
        this.node.addEventListener('mousemove', event => this.penMove(event));
        this.node.addEventListener('mouseup', event => this.penUp(event));

        this.lastMoveLocation = {x: null, y: null};
        this.saveStack = [];
        this.gridShowing = true;
        this.setSize(16, 16);
        this.refresh();
    }
    setSize(w, h) {
        this.width = w;
        this.height = h;
        this.pixels = new Uint8Array(this.width * this.height);

        this.node.width = w * 10;
        this.node.height = h * 10;
        this.ctx = this.node.getContext('2d');
        this.ctx.imageSmoothingEnabled = false;
        this.ctx.mozImageSmoothingEnabled = false;
        this.ctx.webkitImageSmoothingEnabled = false;
        this.ctx.msImageSmoothingEnabled = false;

        this.grid = document.createElement('canvas');
        this.grid.width = w * 10;
        this.grid.height = h * 10;
        let gridctx = this.grid.getContext('2d');
        gridctx.strokeStyle = 'white';
        for (let i = 0; i < this.width; i++) {
            gridctx.beginPath();
            gridctx.moveTo(i * 10, 0);
            gridctx.lineTo(i * 10, h * 10);
            gridctx.stroke();

            gridctx.beginPath();
            gridctx.moveTo(0, i * 10);
            gridctx.lineTo(w * 10, i * 10);
            gridctx.stroke();
        }

        let previewContainer = document.getElementById('preview');
        if (this.preview) {
            previewContainer.removeChild(this.preview);
        }
        this.preview = document.createElement('canvas');
        this.preview.width = w;
        this.preview.height = h;
        this.pctx = this.preview.getContext('2d');
        this.pctx.fillStyle = this.state.palette.getCSSColor(0);
        this.pctx.fillRect(0, 0, w, h);
        previewContainer.appendChild(this.preview);
    }
    loadFromImage(img) {
        let validSize = false;
        let sizes = [8, 16, 24, 32, 48, 64, 96, 128];
        for (let i in sizes) {
            if (img.width <= sizes[i]) {
                this.setSize(sizes[i], sizes[i]);
                validSize = true;
                break;
            }
        }
        if (!validSize) {
            alert("Image too large. Must be less than 128x128");
            return;
        }

        this.pctx.fillStyle = this.state.palette.getCSSColor(0);
        this.pctx.fillRect(0, 0, this.width, this.height);
        this.pctx.drawImage(img, 0, 0);
        let imageData = this.pctx.getImageData(0, 0, this.width, this.height);
        for (let i = 0; i < imageData.data.length; i += 4) {
            let c = this.state.palette.findClosestColor(imageData.data.slice(i, i + 4));
            this.pixels[i / 4] = c;
        }

        this.redraw();
        this.refresh();
    }
    getFile() {
        return new Promise((resolve, reject) => {
            this.preview.toBlob(blob => {
                let f = new File([blob], 'image.png', {type: 'application/octet-stream'});
                resolve(f);
            });
        });
    }

    redraw(x, y, w, h) {
        if (x == undefined) {
            x = 0;
            y = 0; 
            w = this.width;
            h = this.height;
        }
        this.pctx.clearRect(x, y, w, h);
        for (let iy = y; iy < y + h; iy++) {
            for (let ix = x; ix < x + w; ix++) {
                let c = this.pixels[iy * this.width + ix];
                this.pctx.fillStyle = this.state.palette.getCSSColor(c);
                this.pctx.fillRect(ix, iy, 1, 1);
            }
        }
    }
    refresh(x, y, w, h) {
        if (x == undefined) {
            x = 0;
            y = 0; 
            w = this.width;
            h = this.height;
        }
        this.ctx.drawImage(this.preview, x, y, w, h, x * 10, y * 10, w * 10, h * 10);
        if (this.gridShowing) {
            this.ctx.save();
            this.ctx.globalCompositeOperation = 'exclusion';
            this.ctx.drawImage(this.grid, x * 10, y * 10, w * 10, h * 10, x * 10, y * 10, w * 10, h * 10);
            this.ctx.restore();
        }
    }
    setPixel(x, y, v = this.state.palette.color) {
        if (x < 0 || y < 0 || x >= this.width || y >= this.height)
            return;

        let addr = y * this.width + x;
        this.pixels[addr] = v;
    }
    drawPixel(x, y, v = this.state.palette.color) {
        this.setPixel(x, y, v);
        this.redraw(x, y, 1, 1);
        this.refresh(x, y, 1, 1);
    }
    setHLine(x, y, l, v = this.state.palette.color) {
        if (x < 0 || y < 0 || x >= this.width || y >= this.height)
            return;
        
        let base = y * this.width + x;
        let end = base + l;
        if (end > this.pixels.length) {
            end = this.pixels.length;
        }
        for (let i = base; i < end; i++) {
            this.pixels[i] = v;
        }
    }
    drawHLine(x, y, l, v = this.state.palette.color) {
        this.setHLine(x, y, l, v);
        this.redraw(x, y, l, 1);
        this.refresh(x, y, l, 1);
    }
    setVLine(x, y, l, v = this.state.palette.color) {
        if (x < 0 || y < 0 || x >= this.width || y >= this.height)
            return;

        let end = y + l;
        if (end > this.height) {
            end = this.height;
        }
        for (let i = y; i < end; i++) {
            this.pixels[i * this.width + x] = v;
        }
    }
    drawVLine(x, y, l, v = this.state.palette.color) {
        this.setVLine(x, y, l, v);
        this.redraw(x, y, l, 1);
        this.refresh(x, y, l, 1);
    }
    setLine(x1, y1, x2, y2) {
        if (x1 == undefined || x1 == undefined || x2 == undefined || y2 == undefined) {
            throw Error("Undefined arguments to setLine");
        }
        if (x < 0 || y < 0 || x >= this.width || y >= this.height)
            return;

        // Props to Rosetta Code
        // http://rosettacode.org/wiki/Bitmap/Bresenham's_line_algorithm
        const dx = Math.abs(x2 - x1);
        const dy = Math.abs(y2 - y1);
        const sx = x1 < x2 ? 1 : -1;
        const sy = y1 < y2 ? 1 : -1;
        let [x, y] = [x1, y1];
        let err = (dx > dy ? dx : -dy) / 2;

        while (true) {
            this.state.pixelEditor.setPixel(x, y);
            if (x == x2 && y == y2)
                break;
            let e2 = err;
            if (e2 > -dx) {
                err -= dy;
                x += sx;
            }
            if (e2 < dy) {
                err += dx;
                y += sy;
            }
        }
    }
    drawLine(x1, y1, x2, y2) {
        const x = x1 < x2 ? x1 : x2;
        const y = y1 < y2 ? y1 : y2;
        const w = Math.abs(x2 - x1) + 1;
        const h = Math.abs(y2 - y1) + 1;
        this.setLine(x1, y1, x2, y2);
        this.redraw(x, y, w, h);
        this.refresh(x, y, w, h);
    }
    getPixel(x, y) {
        if (x < 0 || y < 0 || x >= this.width || y >= this.height)
            return -1;

        let addr = y * this.width + x;
        return this.pixels[addr];
    }
    paletteChanged() {
        this.redraw();
        this.refresh();
    }
    toggleGrid() {
        this.gridShowing = !this.gridShowing;
        this.refresh();
        document.getElementById('button-grid').className = this.gridShowing ? 'active' : '';
    }
    pushState() {
        this.saveStack.push({
            pixels: new Uint8Array(this.pixels),
        });
    }
    resetState() {
        if (this.saveStack.length == 0) {
            return;
        }

        let state = this.saveStack[this.saveStack.length - 1];
        this.pixels = new Uint8Array(state.pixels);
    }
    popState() {
        if (this.saveStack.length == 0) {
            return;
        }

        let state = this.saveStack.pop();
        this.pixels = state.pixels;
    }

    getPixelCoords(event) {
        let rect = this.node.getBoundingClientRect();
        let scale = rect.width / this.width;
        return {
            x: Math.floor(event.offsetX / scale),
            y: Math.floor(event.offsetY / scale),
        };
    }
    penDown(event) {
        let { x, y } = this.getPixelCoords(event);
        this.state.toolbox.getTool().down(x, y);
    }
    penMove(event) {
        let { x, y } = this.getPixelCoords(event);
        if (x != this.lastMoveLocation.x || y != this.lastMoveLocation.y) {
            this.state.toolbox.getTool().move(x, y);
            this.lastMoveLocation.x = x;
            this.lastMoveLocation.y = y;
        }
    }
    penUp(event) {
        let { x, y } = this.getPixelCoords(event);
        this.state.toolbox.getTool().up(x, y);
    }
}