jewel.board = (function() {
    var settings,
        jewels,
        cols,
        rows,
        baseScore,
        numJewelTypes;

    function randomJewel() {
        return Math.floor(Math.random() * numJewelTypes);
    }

    function getJewel(x, y) {
        if (x < 0 || x > cols-1 || y < 0 || y > rows-1) {
            return -1;
        } else {
            return jewels[x][y];
        }
    }
    
    function fillBoard() {
        var x, y,
            type;
        jewels = [];
        for (x = 0; x < cols; x++) {
            jewels[x] = [];
            for (y = 0; y < rows; y++) {
                type = randomJewel();
                while ((type === getJewel(x-1, y) &&
                        type === getJewel(x-2, y)) ||
                       (type === getJewel(x, y-1) &&
                        type === getJewel(x, y-2))) {
                    type = randomJewel();
                }
                jewels[x][y] = type;
            }
        }
        // Rekurencyjnie wypełnia planszę jeśli nie ma na niej żadnych ruchów.
        if (!hasMoves()) {
            fillBoard();
        }
    }
    
    // Zwraca liczbę w najdłuższym łańcuchu,
    // który zawiera kamień o współrzędnych (x,y). 
    function checkChain(x, y) {
        var type = getJewel(x, y),
            left = 0, right = 0,
            down = 0, up = 0;

        // Sprawdza kamienie po prawej.
        while (type === getJewel(x + right + 1, y)) {
            right++;
        }

        // Sprawdza kamienie po lewej.
        while (type === getJewel(x - left - 1, y)) {
            left++;
        }

        // Sprawdza kamienie u góry.
        while (type === getJewel(x, y + up + 1)) {
            up++;
        }

        // Sprawdza kamienie u dołu.
        while (type === getJewel(x, y - down - 1)) {
            down++;
        }

        return Math.max(left + 1 + right, up + 1 + down);
    }

    // Zwraca wartość true jeśli klejnot (x1,y1) może zostać zamieniony miejscami
    // z (x2,y2) tworząc dopasowanie.
    function canSwap(x1, y1, x2, y2) {
        var type1 = getJewel(x1,y1),
            type2 = getJewel(x2,y2),
            chain;

        if (!isAdjacent(x1, y1, x2, y2)) {
            return false;
        }

        // Tymczasowo zamienia miejscami wybrane kamienie.
        jewels[x1][y1] = type2;
        jewels[x2][y2] = type1;

        chain = (checkChain(x2, y2) > 2 
              || checkChain(x1, y1) > 2);

        // Ustawia klejnoty na poprzednie miejsce.
        jewels[x1][y1] = type1;
        jewels[x2][y2] = type2;

        return chain;
    }

    // Zwraca wartość true jeśli klejnot (x1,y1) sąsiaduje z kamieniem (x2,y2).
    function isAdjacent(x1, y1, x2, y2) {
        var dx = Math.abs(x1 - x2),
            dy = Math.abs(y1 - y2);
        return (dx + dy === 1);
    }
    
    // Zwraca dwuwymiarową mapę długości łańcuchów. 
    function getChains() {
        var x, y,
            chains = [];

        for (x = 0; x < cols; x++) {
            chains[x] = [];
            for (y = 0; y < rows; y++) {
                chains[x][y] = checkChain(x, y);
            }
        }
        return chains;
    }
    
    // Tworzy kopię planszy z klejnotami.
    function getBoard() {
        var copy = [],
            x;
        for (x = 0; x < cols; x++) {
            copy[x] = jewels[x].slice(0);
        }
        return copy;
    }


    // Zwraca wartość true jeśli znajdzie przynajmniej jeden możliwy ruch.
    function hasMoves() {
        for (var x = 0; x < cols; x++) {
            for (var y = 0; y < rows; y++) {
                if (canJewelMove(x, y)) {
                    return true;
                }
            }
        }
        return false;
    }
    
    function check(events) {
        var chains = getChains(), 
            hadChains = false, score = 0,
            removed = [], moved = [], gaps = [];

        for (var x = 0; x < cols; x++) {
            gaps[x] = 0;
            for (var y = rows-1; y >= 0; y--) {
                if (chains[x][y] > 2) {
                    hadChains = true;
                    gaps[x]++;
                    removed.push({
                        x : x, y : y,
                        type : getJewel(x, y)
                    });
                    
                    // Dodaje punkty do wyniku.
                    score += baseScore
                           * Math.pow(2, (chains[x][y] - 3));

                } else if (gaps[x] > 0) {
                    moved.push({
                        toX : x, toY : y + gaps[x],
                        fromX : x, fromY : y,
                        type : getJewel(x, y)
                    });
                    jewels[x][y + gaps[x]] = getJewel(x, y);
                }
            }
            
            // Wypełnia planszę od góry.
            for (y = 0; y < gaps[x]; y++) {
                jewels[x][y] = randomJewel();
                moved.push({
                    toX : x, toY : y,
                    fromX : x, fromY : y - gaps[x],
                    type : jewels[x][y]
                });
            }
        }
        
        events = events || [];

        if (hadChains) {
            events.push({
                type : "remove",
                data : removed
            }, {
                type : "score",
                data : score
            }, {
                type : "move",
                data : moved
            });
            
            // Wypełnia planszę ponownie, jeśli gracz nie będzie miał żadnego ruchu.
            if (!hasMoves()) {
                fillBoard();
                events.push({
                    type : "refill",
                    data : getBoard()
                });
            }

            return check(events);
        } else {
            return events;
        }

    }
    
    // Zwraca true, jeśli współrzędne (x,y) są odwzorowane na planszy i
    // jeśli kamień w tym ponkcie może pozstać zamieniony z sąsiednim.
    function canJewelMove(x, y) {
        return ((x > 0 && canSwap(x, y, x-1 , y)) ||
                (x < cols-1 && canSwap(x, y, x+1 , y)) ||
                (y > 0 && canSwap(x, y, x , y-1)) ||
                (y < rows-1 && canSwap(x, y, x , y+1)));
    }

    // Jeśli istnieje taka możliwość zmienia miejscami klejnot w komórce (x1, y1)
    // z klejnotem w komórce (x2, y2).
    function swap(x1, y1, x2, y2, callback) {
        var tmp,
            events;

        if (canSwap(x1, y1, x2, y2)) {

            // Zamienia klejnoty miejscami. 
            tmp = getJewel(x1, y1);
            jewels[x1][y1] = getJewel(x2, y2);
            jewels[x2][y2] = tmp;

            // Sprawdza planszę i pobiera listę zdarzeń.
            events = check();

            callback(events);
        } else {
            callback(false);
        }
    }

    function initialize(callback) {
        settings = jewel.settings;
        numJewelTypes = settings.numJewelTypes;
        baseScore = settings.baseScore;
        cols = settings.cols;
        rows = settings.rows;
        fillBoard();
        callback();
    }
   
    function print() {
        var str = "";
        for (var y = 0; y < rows; y++) {
            for (var x = 0; x < cols; x++) {
                str += getJewel(x, y) + " ";
            }
            str += "\r\n";
        }
        console.log(str);
    }

    return {
        initialize : initialize,
        swap : swap,
        canSwap : canSwap,
        getBoard : getBoard,
        print : print
    };

})();
