// From http://www.redblobgames.com/x/1613-convchain/
// This is a port of the C# code https://github.com/mxgmn/ConvChain
// and then I built a UI on top of it (see convchain-ui.js)



/** 2D array implemented on top of a 1D array. 
    It's ok to access the underlying 1D array.
    Be excellent to each other.
 */
class Array2D {
    size: number;
    data: Int32Array;
    
    constructor (public w:number, public h:number) {
        this.size = w * h;
        this.data = new Int32Array(this.size);
    }
    
    index(x, y) { return x + this.w*y; }
    x(index) { return index % this.w; }
    y(index) { return (index / this.w) | 0; }
    get(x, y) { return this.data[this.index(x, y)]; }
    set(x, y, value) { this.data[this.index(x, y)] = value; }
}


/** Pattern class keeps a small bitmap and can convert it into an integer
    where each bit of the integer is one of the pixels in the bitmap.
 */
class Pattern {
    data: Array2D;
    
    constructor (public size:number, init:(i:number, j:number) => number) {
        this.data = new Array2D(size, size);
        for (var j = 0; j < size; j++) {
            for (var i = 0; i < size; i++) {
                this.data.set(i, j, init(i, j));
            }
        }
    }

    static init_from_array(field:Array2D, x:number, y:number) {
        return (i, j) => field.get((x + i + field.w) % field.w, (y + j + field.h) % field.h);
    }

    rotated() { return new Pattern(this.size, (x, y) => this.data.get(this.size - 1 - y, x)); }
    reflected() { return new Pattern(this.size, (x, y) => this.data.get(this.size - 1 - x, y)); }
        
    index() {
        var result = 0;
        for (var i = 0; i < this.data.size; i++) {
            result += this.data.data[i] << i;
        }
        return result;
    }
}

        
/** Sample from a discrete distribution with given weights.
 *
 * @param weights:number[N]
 * @param choice:number from 0.0 to 1.0
 * 
 * @return an index I into the weights array so that 
 *           sum(weights[0...I-1])
 *        <= choice * sum(weights[0...N-1])
 *        <= sum(weights[0...I])
 * or if that isn't possible, return 0 if choice is low or N-1 
 * if choice is high.
 *
 * Related: http://www.keithschwarz.com/darts-dice-coins/
 */
function weighted_choice(weights:number[], choice:number):number {
    var sum = 0.0;
    for (var i = 0; i < weights.length; i++) { sum += weights[i]; }

    var cumulative_weight = 0.0;
    for (var i = 0; i < weights.length; i++) {
        cumulative_weight += weights[i];
        if (choice * sum <= cumulative_weight) { return i; }
    }
    return weights.length - 1;
}

function test_weighted_choice() {
    test_equal(weighted_choice([1.0], 0.0), 0);
    test_equal(weighted_choice([1.0, 1.0], 0.5), 0);
    test_equal(weighted_choice([1.0, 1.0], 1.5), 1);
    test_equal(weighted_choice([1.0, 1.0], 2.0), 1);
    test_equal(weighted_choice([5.0, 1.0], 2.0), 0)
    test_equal(weighted_choice([5.0, 1.0], 5.1), 1)
}

function test_equal(a, b) {
    if (a != b) {
        console.trace("FAIL: ", a, " != ", b);
    }
}

test_weighted_choice();


/** Replace this random generator object with another if you'd like
 */
var system_random = {
    int: function(N:number) { return (Math.random() * N) | 0; },
    double: function() { return Math.random(); }
}


/** ConvChain is the main logic. It's a straight port from the C# code.
 *
 * From XML: N = receptorSize, size = outputSize
 */
function conv_chain(random_gen, sample:Array2D, N:number, temperature:number, size:number, iterations:number):Array2D {
    var field = new Array2D(size, size);
    for (var i = 0; i < field.size; i++) { field.data[i] = random_gen.int(2); }
    
    var weights = new Float64Array(1 << (N * N));
    weights.fill(0);
    
    for (var y = 0; y < sample.h; y++) {
        for (var x = 0; x < sample.w; x++) {
            var p:Pattern[] = [];
            p[0] = new Pattern(N, Pattern.init_from_array(sample, x, y));
            p[1] = p[0].rotated();
            p[2] = p[1].rotated();
            p[3] = p[2].rotated();
            p[4] = p[0].reflected();
            p[5] = p[1].reflected();
            p[6] = p[2].reflected();
            p[7] = p[3].reflected();

            for (var k = 0; k < 8; k++) {
                weights[p[k].index()] += 1;
            }
        }
    }

    var sum = 8 * sample.w * sample.h;
    for (var k = 0; k < weights.length; k++) { weights[k] /= sum; }

    function energy(color:number, i, j):number {
        var value = 0.0;
        var old_color = field.get(i, j);
        field.set(i, j, color);

        // NOTE: the profiler says this is the slowest part of the
        // code. It seems like we should be able to compute an index
        // more cheaply if we already have the index calculated before
        // field[i,j] is set to the new color. I don't know for sure
        // though, and I didn't pursue this.
        for (var y = j - N + 1; y <= j + N - 1; y++) {
            for (var x = i - N + 1; x <= i + N - 1; x++) {
                value += weights[new Pattern(N, Pattern.init_from_array(field, x, y)).index()];
            }
        }

        field.set(i, j, old_color);
        return value;
    }

    function heat_bath(i, j) {
        var probabilities:number[] = [];
        for (var color = 0; color < 2; color++) {
            probabilities[color] = Math.exp(energy(color, i, j) / temperature);
        }
        field.set(i, j, weighted_choice(probabilities, random_gen.double()));
    }

    for (var k = 0; k < iterations * size * size; k++) {
        heat_bath(random_gen.int(size), random_gen.int(size));
    }

    return field;
}
