"use strict";

// {{{1 Initialize matrix with zeros
function init(version) {
    var N = version * 4 + 17;
    var matrix = [];
    var zeros = new Buffer(N);
    zeros.fill(0);
    zeros = [].slice.call(zeros);
    for (var i = 0; i < N; i++) {
        matrix[i] = zeros.slice();
    }
    return matrix;
}

// {{{1 Put finders into matrix
function fillFinders(matrix) {
    var N = matrix.length;
    for (var i = -3; i <= 3; i++) {
        for (var j = -3; j <= 3; j++) {
            var max = Math.max(i, j);
            var min = Math.min(i, j);
            var pixel = (max == 2 && min >= -2) || (min == -2 && max <= 2) ? 0x80 : 0x81;
            matrix[3 + i][3 + j] = pixel;
            matrix[3 + i][N - 4 + j] = pixel;
            matrix[N - 4 + i][3 + j] = pixel;
        }
    }
    for (var i = 0; i < 8; i++) {
        matrix[7][i] = matrix[i][7] =
        matrix[7][N - i - 1] = matrix[i][N - 8] =
        matrix[N - 8][i] = matrix[N - 1 - i][7] = 0x80;
    }
}

// {{{1 Put align and timinig
function fillAlignAndTiming(matrix) {
    var N = matrix.length;
    if (N > 21) {
        var len = N - 13;
        var delta = Math.round(len / Math.ceil(len / 28));
        if (delta % 2) delta++;
        var res = [];
        for (var p = len + 6; p > 10; p -= delta) {
            res.unshift(p);
        }
        res.unshift(6);
        for (var i = 0; i < res.length; i++) {
            for (var j = 0; j < res.length; j++) {
                var x = res[i], y = res[j];
                if (matrix[x][y]) continue;
                for (var r = -2; r <=2 ; r++) {
                    for (var c = -2; c <=2 ; c++) {
                        var max = Math.max(r, c);
                        var min = Math.min(r, c);
                        var pixel = (max == 1 && min >= -1) || (min == -1 && max <= 1) ? 0x80 : 0x81;
                        matrix[x + r][y + c] = pixel;
                    }
                }
            }
        }
    }
    for (var i = 8; i < N - 8; i++) {
        matrix[6][i] = matrix[i][6] = i % 2 ? 0x80 : 0x81;
    }
}

// {{{1 Fill reserved areas with zeroes
function fillStub(matrix) {
    var N = matrix.length;
    for (var i = 0; i < 8; i++) {
        if (i != 6) {
            matrix[8][i] = matrix[i][8] = 0x80;
        }
        matrix[8][N - 1 - i] = 0x80;
        matrix[N - 1 - i][8] = 0x80;
    }
    matrix[8][8] = 0x80;
    matrix[N - 8][8] = 0x81;

    if (N < 45) return;

    for (var i = N - 11; i < N - 8; i++) {
        for (var j = 0; j < 6; j++) {
            matrix[i][j] = matrix[j][i] = 0x80;
        }
    }
}

// {{{1 Fill reserved areas
var fillReserved = (function() {
    var FORMATS = Array(32);
    var VERSIONS = Array(40);

    var gf15 = 0x0537;
    var gf18 = 0x1f25;
    var formats_mask = 0x5412;

    for (var format = 0; format < 32; format++) {
        var res = format << 10;
        for (var i = 5; i > 0; i--) {
            if (res >>> (9 + i)) {
                res = res ^ (gf15 << (i - 1));
            }
        }
        FORMATS[format] = (res | (format << 10)) ^ formats_mask;
    }

    for (var version = 7; version <= 40; version++) {
        var res = version << 12;
        for (var i = 6; i > 0; i--) {
            if (res >>> (11 + i)) {
                res = res ^ (gf18 << (i - 1));
            }
        }
        VERSIONS[version] = (res | (version << 12));
    }

    var EC_LEVELS = { L: 1, M: 0, Q: 3, H: 2 };

    return function fillReserved(matrix, ec_level, mask) {
        var N = matrix.length;
        var format = FORMATS[EC_LEVELS[ec_level] << 3 | mask];
        function F(k) { return format >> k & 1 ? 0x81 : 0x80 };
        for (var i = 0; i < 8; i++) {
            matrix[8][N - 1 - i] = F(i);
            if (i < 6) matrix[i][8] = F(i);
        }
        for (var i = 8; i < 15; i++) {
            matrix[N - 15 + i][8] = F(i);
            if (i > 8) matrix[8][14 - i] = F(i);
        }
        matrix[7][8] = F(6);
        matrix[8][8] = F(7);
        matrix[8][7] = F(8);

        var version = VERSIONS[(N - 17)/4];
        if (!version) return;
        function V(k) { return version >> k & 1 ? 0x81 : 0x80 };
        for (var i = 0; i < 6; i++) {
            for (var j = 0; j < 3; j++) {
                matrix[N - 11 + j][i] = matrix[i][N - 11 + j] = V(i * 3 + j);
            }
        }
    }
})();

// {{{1 Fill data
var fillData = (function() {
    var MASK_FUNCTIONS = [
        function(i, j) { return (i + j) % 2 == 0 },
        function(i, j) { return i % 2 == 0 },
        function(i, j) { return j % 3 == 0 },
        function(i, j) { return (i + j) % 3 == 0 },
        function(i, j) { return (Math.floor(i / 2) + Math.floor(j / 3) ) % 2 == 0 },
        function(i, j) { return (i * j) % 2 + (i * j) % 3 == 0 },
        function(i, j) { return ( (i * j) % 2 + (i * j) % 3) % 2 == 0 },
        function(i, j) { return ( (i * j) % 3 + (i + j) % 2) % 2 == 0 }
    ];

    return function fillData(matrix, data, mask) {
        var N = matrix.length;
        var row, col, dir = -1;
        row = col = N - 1;
        var mask_fn = MASK_FUNCTIONS[mask];
        var len = data.blocks[data.blocks.length - 1].length;

        for (var i = 0; i < len; i++) {
            for (var b = 0; b < data.blocks.length; b++) {
                if (data.blocks[b].length <= i) continue;
                put(data.blocks[b][i]);
            }
        }

        len = data.ec_len;
        for (var i = 0; i < len; i++) {
            for (var b = 0; b < data.ec.length; b++) {
                put(data.ec[b][i]);
            }
        }

        if (col > -1) {
            do {
                matrix[row][col] = mask_fn(row, col) ? 1 : 0;
            } while (next());
        }

        function put(byte) {
            for (var mask = 0x80; mask; mask = mask >> 1) {
                var pixel = !!(mask & byte);
                if (mask_fn(row, col)) pixel = !pixel;
                matrix[row][col] = pixel ? 1 : 0;
                next();
            }
        }

        function next() {
            do {
                if ((col % 2) ^ (col < 6)) {
                    if (dir < 0 && row == 0 || dir > 0 && row == N - 1) {
                        col--;
                        dir = -dir;
                    } else {
                        col++;
                        row += dir;
                    }
                } else {
                    col--;
                }
                if (col == 6) {
                    col--;
                }
                if (col < 0) {
                    return false;
                }
            } while (matrix[row][col] & 0xf0);
            return true;
        }
    }
})();

// {{{1 Calculate penalty
function calculatePenalty(matrix) {
    var N = matrix.length;
    var penalty = 0;
    // Rule 1
    for (var i = 0; i < N; i++) {
        var pixel = matrix[i][0] & 1;
        var len = 1;
        for (var j = 1; j < N; j++) {
            var p = matrix[i][j] & 1;
            if (p == pixel) {
                len++;
                continue;
            }
            if (len >= 5) {
                penalty += len - 2;
            }
            pixel = p;
            len = 1;
        }
        if (len >= 5) {
            penalty += len - 2;
        }
    }
    for (var j = 0; j < N; j++) {
        var pixel = matrix[0][j] & 1;
        var len = 1;
        for (var i = 1; i < N; i++) {
            var p = matrix[i][j] & 1;
            if (p == pixel) {
                len++;
                continue;
            }
            if (len >= 5) {
                penalty += len - 2;
            }
            pixel = p;
            len = 1;
        }
        if (len >= 5) {
            penalty += len - 2;
        }
    }

    // Rule 2
    for (var i = 0; i < N - 1; i++) {
        for (var j = 0; j < N - 1; j++) {
            var s = matrix[i][j] + matrix[i][j + 1] + matrix[i + 1][j] + matrix[i + 1][j + 1] & 7;
            if (s == 0 || s == 4) {
                penalty += 3;
            }
        }
    }

    // Rule 3
    function I(k) { return matrix[i][j + k] & 1 };
    function J(k) { return matrix[i + k][j] & 1 };
    for (var i = 0; i < N; i++) {
        for (var j = 0; j < N; j++) {
            if (j < N - 6 && I(0) && !I(1) && I(2) && I(3) && I(4) && !I(5) && I(6)) {
                if (j >= 4 && !(I(-4) || I(-3) || I(-2) || I(-1))) {
                    penalty += 40;
                }
                if (j < N - 10 && !(I(7) || I(8) || I(9) || I(10))) {
                    penalty += 40;
                }
            }

            if (i < N - 6 && J(0) && !J(1) && J(2) && J(3) && J(4) && !J(5) && J(6)) {
                if (i >= 4 && !(J(-4) || J(-3) || J(-2) || J(-1))) {
                    penalty += 40;
                }
                if (i < N - 10 && !(J(7) || J(8) || J(9) || J(10))) {
                    penalty += 40;
                }
            }
        }
    }

    // Rule 4
    var numDark = 0;
    for (var i = 0; i < N; i++) {
        for (var j = 0; j < N; j++) {
            if (matrix[i][j] & 1) numDark++;
        }
    }
    penalty += 10 * Math.floor(Math.abs(10 - 20 * numDark/(N * N)));

    return penalty;
}

// {{{1 All-in-one function
function getMatrix(data) {
    var matrix = init(data.version);
    fillFinders(matrix);
    fillAlignAndTiming(matrix);
    fillStub(matrix);

    var penalty = Infinity;
    var bestMask = 0;
    for (var mask = 0; mask < 8; mask++) {
        fillData(matrix, data, mask);
        fillReserved(matrix, data.ec_level, mask);
        var p = calculatePenalty(matrix);
        if (p < penalty) {
            penalty = p;
            bestMask = mask;
        }
    }

    fillData(matrix, data, bestMask);
    fillReserved(matrix, data.ec_level, bestMask);

    return matrix.map(function(row) {
        return row.map(function(cell) {
            return cell & 1;
        });
    });
}

// {{{1 export functions
module.exports = {
    getMatrix: getMatrix,
    init: init,
    fillFinders: fillFinders,
    fillAlignAndTiming: fillAlignAndTiming,
    fillStub: fillStub,
    fillReserved: fillReserved,
    fillData: fillData,
    calculatePenalty: calculatePenalty,
}