/**
    Interactive cellular automata.

    Michael Soukup 2014.
*/


function zeros(m, n) {
    var mat = [];
    for (var i=0; i<m; i++) {
        mat.push([]);
        for (var j=0; j<n; j++) {
            mat[i].push(0);
        }
    }
    return mat
}

function nearestMod0(f, n) {
    var mod = f % n;
    if (mod < n/2) { // floor
        return f-mod
    }
    else { // ceil
        return f+n-mod
    }
}




var CELL_T = {
    'empty': 0,
    'wall': 1,
    'player': 2,
    'enemy': 3,
    'conway': 4
};

var CELLCOL = {
    0: '#fff',
    1: '#222',
    2: '#484',
    3: '#a44',
    4: '#8a8a8a'
};

// Note: Cellsets appear transposed.
var CELLSETS = {
    "dead_4": [
        [4,4],
        [4,4]
    ],
    "dead_9": [
        [0,4,0],
        [4,0,4],
        [0,4,0]
    ],
    "finite14_64": [
        [0,0,0,0,0,0,0,0],
        [0,4,0,4,4,4,0,0],
        [0,0,0,0,4,4,0,0],
        [0,0,4,4,0,4,0,0],
        [0,0,4,4,0,0,0,0],
        [0,0,0,4,4,4,0,0],
        [0,4,4,0,4,4,0,0],
        [0,4,4,0,0,0,0,0]
    ],
    "infinite_64": [
        [0,0,0,4,0,0,0,0],
        [0,4,4,4,4,4,4,0],
        [4,0,0,0,4,4,0,0],
        [0,4,4,4,0,4,0,0],
        [0,4,0,0,4,4,0,4],
        [0,0,0,4,0,0,0,0],
        [0,4,0,4,0,4,0,4],
        [0,4,4,4,0,0,0,0]
    ],
    "player_0": [
        [2,2,2,2],
        [2,0,4,2],
        [2,0,0,2],
        [2,2,2,2]
    ],
    "enemy_0": [
        [3,3,3,3],
        [3,0,0,3],
        [3,0,0,3],
        [3,3,3,3]
    ]
}

var WORLD_0 = {
    "width": 80,                      // tilewidth
    "heigth": 60,                     // tileheight
    "walls": true,                    // sets boundaries if true
    "player": ["player_0", 20, 20],   // tileset, x, y
    "enemies": [
        ["enemy_0", 50, 40]
    ],
    "organisms": [
        ["finite14_64", 25, 25],
        ["infinite_64", 65, 40],
        ["dead_4", 10, 5],
        ["dead_4", 5, 10],
        ["dead_4", 30, 5],
        ["dead_9", 17, 40],
    ]
};




/**
    Update rules

    c - cell identifier
    X - neighbourhood of c. Array of length 8 starting in upper left corner.
*/

function _ndist(X) {
    var dist = [0,0,0,0,0];
    for (var i=0; i<X.length; i++) {
        dist[X[i]]++;
    }
    return dist
}

function cell_0_update(X) {
    // count neighbours for now
    var dist = _ndist(X);
    if (dist[4] == 3) {
        return 4
    }
    else  {
        return 0
    }
}

function cell_1_update(X) {
    return 1
}

function cell_2_update(X) {
    // Player cell
    var dist = _ndist(X);
    if (dist[2] < 1 || dist[2] > 7) {
        return 0;
    }
    else if (dist[2] == 1 && dist[4] < 3) {
        return 0;
    }
    else {
        return 2;
    }
}

function cell_3_update(X) {
    // enemy cell
    return 3
}

function cell_4_update(X) {
    // conway live cells
    var dist = _ndist(X);
    if (dist[2] == 3 && dist[4] > 2) {
        return 2
    }
    else if (dist[2] > 0 && (dist[4] != 5 || dist[4] == 4)) {
        return 4
    }
    else if (dist[4] == 2 || dist[4] == 3) {
        return 4
    }
    else {
        return 0
    }
}

function cell_next(c, X) {

    switch (c) {
        case 0:
            return cell_0_update(X);
        case 1:
            return cell_1_update(X);
        case 2:
            return cell_2_update(X);
        case 3:
            return cell_3_update(X);
        case 4:
            return cell_4_update(X);
        case 5:
            return cell_0_update(X);
    }
}




/**
    Board holds the world variables and a complete map over all tiles.
    All tiles on the board are simlulated in each step.
*/
function Board() {
    this.x = 0;
    this.y = 0;
}

Board.prototype.on_board = function(x, y) {
    // TODO check walls
    return (x >= this.x && y >= this.y && x < this.x+this.w && y < this.y+this.h);
}

Board.prototype.set_tile = function(val, x, y) {
    if (this.on_board(x, y)) {
        this.map[x][y] = val;
    }
}

Board.prototype.get_tile = function(x, y) {
    if (this.on_board(x, y)) {
        return this.map[x][y];
    }
    else {
        return 0;
    }
}

Board.prototype.load_tileset = function(tileset, x, y) {
    for (var i=0; i<tileset[0].length; i++) {
        for (var j=0; j<tileset.length; j++) {
            this.set_tile(tileset[j][i], x+i, y+j);
        }
    }
}

Board.prototype.load_world = function(world) {
    this.w = world.width;
    this.h = world.heigth;
    this.map = zeros(this.w, this.h);
    this.map_buf = zeros(this.w, this.h); // Buffer for simulation

    this.load_tileset(CELLSETS[world.player[0]], world.player[1], world.player[2]);
    for (var e=0; e<world.enemies.length; e++) {
        var _enemy = world.enemies[e];
        this.load_tileset(CELLSETS[_enemy[0]], _enemy[1], _enemy[2]);
    }
    for (var o=0; o<world.organisms.length; o++) {
        var _organism = world.organisms[o];
        this.load_tileset(CELLSETS[_organism[0]], _organism[1], _organism[2]);
    }

    if (world.walls) {
        for (var i=this.x; i<this.x+this.w; i++) {
            this.set_tile(1, i, 0);
            this.set_tile(1, i, this.h-1);
        }
        for (var j=this.y; j<this.y+this.h; j++) {
            this.set_tile(1, 0, j);
            this.set_tile(1, this.w-1, j);
        }
    }
}

/**
    Move the set of tiles defined by frame to (x+dx, y+dy).
    Not responsible for checking if the move is a legal operation.
*/
Board.prototype.move_frame = function(frame, dx, dy) {
    var x_start, x_end, x_step,
        y_start, y_end, y_step,
        x, y;

    if (dx < 0) {
        x_start = frame.x;
        x_end = frame.x+frame.w;
        x_step = 1;
    }
    else {
        x_start = frame.x+frame.w-1;
        x_end = frame.x-1;
        x_step = -1;
    }
    if (dy < 0) {
        y_start = frame.y;
        y_end = frame.y+frame.h;
        y_step = 1;
    }
    else {
        y_start = frame.y+frame.h-1;
        y_end = frame.y-1;
        y_step = -1;
    }

    x = x_start;
    while (x != x_end) {
        y = y_start;
        while (y != y_end) {
            this.set_tile(this.map[x][y], x+dx, y+dy);
            this.set_tile(0, x, y);
            y += y_step;
        }
        x += x_step;
    }
}

Board.prototype.neighbourhood = function(cx, cy) {
    return [
        this.get_tile(cx-1, cy-1),    // upper left corner
        this.get_tile(cx, cy-1),
        this.get_tile(cx+1, cy-1),
        this.get_tile(cx-1, cy),
        this.get_tile(cx+1, cy),
        this.get_tile(cx-1, cy+1),
        this.get_tile(cx, cy+1),
        this.get_tile(cx+1, cy+1),
    ]
}

Board.prototype.simulate = function(frame) {
    var X;
    for (var i=this.x; i<this.x+this.w; i++) {
        for (var j=this.y; j<this.y+this.h; j++) {
            X = this.neighbourhood(i, j);
            this.map_buf[i][j] =  cell_next(this.map[i][j], X);
        }
    }
    this.map = this.map_buf;
}




/**
    TileFrame defines a square area of tiles with the upper left corner
    in (x, y) and lower right corner in (x+w, y+h).

    Coordinates in tile space.
*/
function TileFrame(x, y, w, h) {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
}

TileFrame.prototype.move = function(dx, dy) {
    this.x += dx;
    this.y += dy;
}

TileFrame.prototype.contains = function(frame) {
    return (this.x < frame.x && this.y < frame.y && this.x+this.w > frame.x+frame.w && this.y+this.h > frame.y+frame.h);
}

TileFrame.prototype.intersects = function(frame) {
    return (((this.x > frame.x && this.x < frame.x+frame.w) || (frame.x > this.x && frame.x < this.x+this.w)) &&
            ((this.y > frame.y && this.y < frame.y+frame.h) || (frame.y > this.y && frame.y < this.y+this.h)));
}




/**
    Holds the canvas context and camera coordinates. Implements methods for
    rendering tile sets.

    Coordinates are continuous and in tile space.
*/
function Camera(canvas_id) {
    this.canvas = document.getElementById(canvas_id);
    this.ctx = this.canvas.getContext('2d');
    this.width = this.canvas.width;
    this.height = this.canvas.height;

    this.tilesize = 5; // Minimum tilesize (px).
    this.tw = this.width/this.tilesize;
    this.th = this.height/this.tilesize;
}

Camera.prototype.zoom = function(tilesize) {
    var factor = tilesize/this.tilesize,
        _tw = this.width/tilesize,
        _th = this.height/tilesize,
        diff_x = (this.tw - _tw)/2,
        diff_y = (this.th - _th)/2;

    this.x += diff_x;
    this.tw = _tw;
    this.y += diff_y;
    this.th = _th;
    this.tilesize = tilesize;
}

Camera.prototype.center = function(x, y) {
    this.x = x - this.tw/2;
    this.y = y - this.th/2;
}

Camera.prototype.draw_grid = function() {
    var i = (Math.ceil(this.x) - this.x)*this.tilesize,
        j = (Math.ceil(this.y) - this.y)*this.tilesize;

    this.ctx.strokestyle = 'black';
    this.ctx.lineWidth = 0.1;

    // Vertical lines
    while (i < this.width) {
        this.ctx.beginPath();
        this.ctx.moveTo(i, 0);
        this.ctx.lineTo(i, this.height);
        this.ctx.closePath();
        this.ctx.stroke();
        i += this.tilesize;
    }
    // Horizontal lines
    while (j < this.height) {
        this.ctx.beginPath();
        this.ctx.moveTo(0, j);
        this.ctx.lineTo(this.width, j);
        this.ctx.closePath();
        this.ctx.stroke();
        j += this.tilesize;
    }
}

Camera.prototype.draw_tiles = function(board) {
    var x, y, i, j, tile_margin = this.tilesize*0.01;

    x = Math.floor(this.x);
    i = (x - this.x) * this.tilesize;
    while (i < this.width) {
        y = Math.floor(this.y);
        j = (y - this.y)*this.tilesize;

        while (j < this.height) {
            if (board.get_tile(x,y) != 0) {
                this.ctx.fillStyle = CELLCOL[board.get_tile(x, y)];
                this.ctx.beginPath();
                this.ctx.rect(i+tile_margin, j+tile_margin, this.tilesize-2*tile_margin, this.tilesize-2*tile_margin);
                this.ctx.closePath();
                this.ctx.fill();
            }
            y++;
            j += this.tilesize;
        }
        x++;
        i += this.tilesize;
    }
}

Camera.prototype.clear = function()  {
    this.ctx.clearRect(0, 0, this.width, this.height);
}




function testsim(canvas_id, world, dt) {
    var board = new Board(),
        player = new TileFrame(world.player[1], world.player[2], CELLSETS[world.player[0]][0].length, CELLSETS[world.player[0]].length),
        cam = new Camera(canvas_id),
        tilesize = 8;

    board.load_world(world);
    cam.set(world.player[1]-1.5, world.player[1]-1.5, tilesize); // cam.center(...,
    cam.draw_grid();
    cam.draw_tiles(board);
    setInterval(function (){
        board.simulate();
        cam.clear();
        cam.draw_grid();
        cam.draw_tiles(board);
    }, dt);
}

function testmove(canvas_id, world, dt) {
    var board = new Board(),
        player = new TileFrame(world.player[1], world.player[2], CELLSETS[world.player[0]][0].length, CELLSETS[world.player[0]].length),
        cam = new Camera(canvas_id),
        tilesize = 8.8;

    board.load_world(world);
    cam.zoom(tilesize);
    cam.center(player.x, player.y);
    cam.draw_grid();
    cam.draw_tiles(board);

    function expand_player() {
        var X;
        console.log('exp');
        for (var x=player.x; x<player.x+player.w; x++) {
            X = board.neighbourhood(x, player.y);
            console.log(X);
            if (X[0] == 2 || X[1] == 2 || X[2] == 2) {
                player.y--;
                player.h++;
                break;
            }
            X = board.neighbourhood(x, player.y+player.h-1);
            if (X[4] == 2 || X[5] == 2 || X[6] == 2) {
                player.h++;
                break;
            }
        }

        for (var y=player.y; y<player.y+player.h; y++) {
            X = board.neighbourhood(player.x, y);
            if (X[6] == 2 || X[7] == 2 || X[0] == 2) {
                player.x--;
                player.w++;
                break;
            }
            X = board.neighbourhood(player.x+player.w-1, y);
            if (X[2] == 2 || X[3] == 2 || X[4] == 2) {
                player.w++;
                break;
            }
        }
    }

    var i=0;
    addEventListener('keydown', function(e) {
        var vx = vy = 0,
            key = e.keyCode;

        if (key == 37) {
            vx--;
        }
        else if (key == 39){
            vx++
        }
        else if (key == 40){
            vy++;
        }
        else if (key == 38) {
            vy--;
        }
        else {
            return
        }
        board.move_frame(player, vx, vy);
        player.move(vx, vy);
        board.simulate();

        cam.center(player.x, player.y);
        cam.clear();
        cam.draw_grid();
        cam.draw_tiles(board);

        // Expand frames
        expand_player();

        i++;
    });

    addEventListener('wheel', function(e) {
        if (e.deltaY > 0) {
            tilesize *= 0.9;
            cam.zoom(tilesize);
        }
        else {
            tilesize *= 1.1;
            cam.zoom(tilesize);
        }
        cam.clear();
        cam.draw_grid();
        cam.draw_tiles(board);
    });
}



// Run
//testsim("canvas", WORLD_0, 1000);
testmove("canvas", WORLD_0, 1000);



