// guide.js
// Copyright (c) 2010 'Ili Butterfield. No permission is granted to copy,
// modify, or distribute this code in full or in part without prior written
// permission from the author.
//
// Guide main game code.

//
// CLASSES
//

// Representation of any moving object on the field.
function Mover(type, row, col, direction) {
    this.type = type;
    this.img = new Image();
    this.changeDirection(direction);
    this.moveTo(row, col);
}

Mover.prototype.changeDirection = function(newDir) {
    this.direction = newDir;
    this.img.src = this.type + "-" + this.direction + ".png";
};

Mover.prototype.moveTo = function(newRow, newCol) {
    this.row = newRow;
    this.col = newCol;
    var cell = grid.rows[this.row].cells[this.col];
    cell.appendChild(this.img);
};

// The location a Mover would move into.
function Target(mover, row, col) {
    this.mover = mover;
    this.row = row;
    this.col = col;
}

// The goal for a type of Mover.
function Goal(mover, row, col) {
    this.mover = mover;
    this.row = row;
    this.col = col;
}


//
// GLOBALS
//

var grid; // HTML table element housing the grid
var currentPuzzle; // index of the current puzzle
var solved; // solved puzzle data
var guide; // Mover representing the guide
var followers, rebels; // arrays of Movers representing non-player arrows
var goals; // goals for each Mover
var canMove = false; // whether we're accepting keyboard input or not
var moveCount; // number of moves made in current puzzle attempt
var tiles = ["empty.png", "square.png", "guide-goal.png", "follower-goal.png", "rebel-goal.png"]; // background tiles indexed for guidepuzzles.js format


//
// INITIALIZATION
//

// After the page has finished loading, start up the preloader and do some
// initial setup.
window.onload = init;
function init() {
    // Set up the grid for displaying preloading progress.
    grid = document.getElementById("grid");
    clearGrid();
    grid.style.width = "200px";
    var row = grid.insertRow(-1);
    var cell = row.insertCell(-1);
    cell.style.textAlign = "center";
    cell.innerHTML = "preloading images";
    row = grid.insertRow(-1);
    cell = row.insertCell(-1);
    cell.style.textAlign = "center";
    
    // Start up the prelodaer.
    var urls = ["empty.png", "follower-0.5.png", "follower-0.png", "follower-1.5.png", "follower-1.png", "follower-2.5.png", "follower-2.png", "follower-3.5.png", "follower-3.png", "follower-goal.png", "guide-0.5.png", "guide-0.png", "guide-1.5.png", "guide-1.png", "guide-2.5.png", "guide-2.png", "guide-3.5.png", "guide-3.png", "guide-goal.png", "rebel-0.5.png", "rebel-0.png", "rebel-1.5.png", "rebel-1.png", "rebel-2.5.png", "rebel-2.png", "rebel-3.5.png", "rebel-3.png", "rebel-goal.png", "square.png"];
    var loadCallback = function(preloader, index) {
        cell.innerHTML = (index + 1) + "/" + urls.length + ": " + urls[index];
    };
    var errorCallback = function(preloader, index) {
        cell.innerHTML = "error loading image: " + urls[index];
        return true;
    };
    
    var preloader = new ImagePreloader(urls, {loadCallback: loadCallback, errorCallback: errorCallback, allCallback: startup});
    
    // Load saved puzzle data and activate the instructions/credits toggling.
    loadSolved();
    document.getElementById("instructionslink").onmouseup = function() { toggleBox("instructionstext", "instructionslink", "instructions"); };
    document.getElementById("creditslink").onmouseup = function() { toggleBox("creditstext", "creditslink", "credits"); };
    
    // Calculate how large we want the window to be, based on the sizes of the
    // tallest and widest puzzles.
    var maxWidth = 0;
    var maxHeight = 0;
    for (var i = 0; i < puzzles.length; i++) {
        if ((puzzles[i].length - 1) > maxHeight) {
            maxHeight = puzzles[i].length - 1;
        }
        if (puzzles[i][0].length > maxWidth) {
            maxWidth = puzzles[i][0].length;
        }
    }
    var desiredWidth = 32 * maxWidth + 150;
    var desiredHeight = 32 * maxHeight + 85;
    
    // Resize the window by calculating the difference between the width and
    // height of the inner window and how large we actually want the window.
    // This is pretty kludgy because o browsers u.
    var currInnerWidth, currInnerHeight;
    if (window.innerWidth) {
        currInnerWidth = window.innerWidth;
        currInnerHeight = window.innerHeight;
    } else if (document.body && document.body.clientWidth) {
        currInnerWidth = document.body.clientWidth;
        currInnerHeight = document.body.clientHeight;
    }
    
    if (currInnerWidth) {
        window.resizeBy(desiredWidth - currInnerWidth, desiredHeight - currInnerHeight);
    } else {
        // If we couldn't find the inner dimensions, then just punt with some
        // arbitrary padding for scrollbars, etc.
        window.resizeTo(desiredWidth + 75, desiredHeight + 75);
    }
    
    // Roughly center the window. This will just get it in the general vicinity
    // of center in most browsers, because being pixel perfect isn't really that
    // important.
    var newLeft, newTop
    if (window.outerWidth) {
        newLeft = (window.screen.availWidth - window.outerWidth) / 2;
        newTop = (window.screen.availHeight - window.outerHeight) / 2;
    } else {
        newLeft = (window.screen.availWidth - document.body.offsetWidth) / 2;
        newTop = (window.screen.availHeight - document.body.offsetHeight) / 2;
    }
    window.moveTo(newLeft, newTop);
}

// Expands/collapses one of the information boxes.
function toggleBox(textId, linkId, label) {
    var text = document.getElementById(textId);
    if (text.style.display == "block") {
        text.style.display = "none";
        document.getElementById(linkId).innerHTML = "show " + label;
    } else {
        text.style.display = "block";
        document.getElementById(linkId).innerHTML = "hide " + label;
    }
}

// After we've preloaded the images, start up the game.
function startup(preloader, imagesHandled, errors) {
    if (errors.length) {
        return;
    }
    
    grid.style.width = "";
    
    loadPuzzle(0);

    document.onkeydown = handleKeyEvent;
}

// Initializes the list of solution progress with saved values if the user
// has played before.
function loadSolved() {
    solved = getCookie("guide_solved");
    if (solved === null) {
        toggleBox("instructionstext", "instructionslink", "instructions");
        solved = [];
        for (var i = 0; i < puzzles.length; i++) solved.push("0");
        saveSolved();
    } else {
        solved = solved.split("");
    }
}

// Removes the current puzzle and loads a new one.
function loadPuzzle(puzNum) {
    canMove = false;
    clearGrid();
    makeGrid(puzNum);
    makeMovers(puzNum);

    currentPuzzle = puzNum;
    document.getElementById("levelnum").innerHTML = currentPuzzle + 1
    moveCount = 0;
    refreshMoveCounter();

    if (solved[currentPuzzle] == 1) {
        document.getElementById("status").innerHTML = "puzzle solved";
    } else {
        document.getElementById("status").innerHTML = "";
    }

    canMove = true;
}

// Updates the displayed move counter with the current count.
function refreshMoveCounter() {
    document.getElementById("movecount").innerHTML = moveCount;
}


//
// GRID CREATION/MANIPULATION
//

// Removes all cells from the puzzle grid.
function clearGrid() {
    while (grid.firstChild) {
        grid.removeChild(grid.firstChild);
    }
}

// Creates and fills the puzzle grid as a table, where the background image of
// each cell is the appropriate game tile.
function makeGrid(puzNum) {
    goals = [];

    var puzzle = puzzles[puzNum];
    var height = puzzle.length - 1;
    var width = puzzle[0].length;

    for (var i = 0; i < height; i++) {
        var row = grid.insertRow(-1);
        for (var j = 0; j < width; j++) {
            var cell = row.insertCell(-1);
            var tile = puzzle[i][j];
            cell.style.background = "url(\"" + tiles[tile] + "\")";
            if (isGoal(tile)) {
                addGoal(tile, i, j);
            }
	   }
    }
}

// Determines if the tile number represents a goal for a Mover.
function isGoal(tile) {
    return (tile == 2 || tile == 3 || tile == 4);
}

// Adds a goal to the goal list.
function addGoal(tile, row, col) {
    if (tile == 2) {
        goals[goals.length] = new Goal("guide", row, col);
    } else if (tile == 3) {
        goals[goals.length] = new Goal("follower", row, col);
    } else if (tile == 4) {
        goals[goals.length] = new Goal("rebel", row, col);
    }
}


//
// MOVER CREATION/MANIPULATION
//

// Creates all the movers for the puzzle and puts them in their initial
// positions.
function makeMovers(puzNum) {
    var puzzle = puzzles[puzNum];
    var movers = puzzle[puzzle.length - 1];
    followers = [];
    rebels = [];
    movers.forEach(function(mover) { makeMover(mover[0], mover[1], mover[2], mover[3]); });
}

// Creates a specific mover and classifies it.
function makeMover(mover, row, col, direction) {
    var newMover = new Mover(mover, row, col, direction);
    if (mover == "guide") {
        guide = newMover;
    } else if (mover == "follower") {
        followers[followers.length] = newMover;
    } else if (mover == "rebel") {
        rebels[rebels.length] = newMover;
    }
}

// Clears all movers from the grid.
function clearMovers() {
    clearMover(guide);
    followers.forEach(clearMover);
    rebels.forEach(clearMover);
}

// Clears a specific mover from the grid.
function clearMover(mover) {
    var cell = grid.rows[mover.row].cells[mover.col];
    cell.removeChild(cell.firstChild);
}


//
// MOVEMENT
//

// Handles various key inputs.
function handleKeyEvent(evt) {
    // Don't hijack keyboard shortcuts.
    if (evt.ctrlKey || evt.altKey || evt.metaKey) {
        return;
    }

    // If we're loading or in the process of turning, then break.
    if (!canMove) {
        return;
    }

    if (evt.keyCode == 37) {
        // left arrow
        turn(-1);
    } else if (evt.keyCode == 39) {
        // right arrow
        turn(1);
    } else if (evt.keyCode == 38) {
        // up arrow
        moveForward();
    } else if (evt.keyCode == 82) {
        // r
        clearMovers();
        makeMovers(currentPuzzle);
        moveCount = 0;
        refreshMoveCounter();
    } else if (evt.keyCode == 78) {
        // n
        loadPuzzle((currentPuzzle + 1) % puzzles.length);
    } else if (evt.keyCode == 80) {
        // p
        loadPuzzle((currentPuzzle - 1 + puzzles.length) % puzzles.length);
    } else if (evt.keyCode == 71) {
        // g
        var desiredLevel = prompt("Enter a level number (1-" + puzzles.length + "):");
        if ((parseInt(desiredLevel).toString() == desiredLevel) && (desiredLevel >= 1) && (desiredLevel <= puzzles.length)) {
            loadPuzzle(desiredLevel - 1);
        }
    }
}

// Turns each mover with a two-frame animation.
function turn(dir) {
    halfTurnMovers(dir);
    setTimeout(halfTurnMovers, 50, dir);
}

// Turns each mover forty-five degrees in the proper direction.
function halfTurnMovers(dir) {
    // Letting movers move forward while turning only leads to bad things.
    canMove = !canMove;

    halfTurnMover(guide, dir);
    followers.forEach(function(follower) { halfTurnMover(follower, dir); });
    rebels.forEach(function(rebel) { halfTurnMover(rebel, -dir); });
}

// Turns a specific mover forty-five degrees in the proper direction.
function halfTurnMover(mover, dir) {
    mover.changeDirection((mover.direction + dir * .5 + 4) % 4);
}

// Moves each Mover forward, providing they're allowed to.
function moveForward() {
    var targets = [];
    var stationary = [];

    calculateTargets(targets, stationary);
    extractCollisions(targets, stationary);
    propagateStationary(targets, stationary);

    // If the guide can't move, then no Mover can.
    if (containsGuide(stationary)) {
        return;
    }

    targets.forEach(function(target) { target.mover.moveTo(target.row, target.col); });

    moveCount++;
    refreshMoveCounter();

    checkGoals();
}

// Determine the movement targets for each Mover.
function calculateTargets(targets, stationary) {
    addTarget(guide, targets, stationary);
    followers.forEach(function(follower) { addTarget(follower, targets, stationary); });
    rebels.forEach(function(rebel) { addTarget(rebel, targets, stationary); });
}

// Calculate the target of the Mover and add it to the appropriate list.
function addTarget(mover, targets, stationary) {
    var row = mover.row;
    var col = mover.col;

    if (mover.direction == 0) {
        col -= 1; // left
    } else if (mover.direction == 1) {
        row -= 1; // up
    } else if (mover.direction == 2) {
        col += 1; // right
    } else {
        row += 1; // down
    }

    var target = new Target(mover, row, col);

    // If the target is not a square any Mover can be in, then add it to the
    // list of invalid targets.
    if (openSquare(row, col)) {
        targets[targets.length] = target;
    } else {
        stationary[stationary.length] = target;
    }
}

// Check if a square in the grid can actually hold a Mover.
function openSquare(row, col) {
    if (row < 0 || row >= grid.rows.length) {
        return false;
    }
    if (col < 0 || col >= grid.rows[0].cells.length) {
        return false;
    }

    var tile = puzzles[currentPuzzle][row][col];
    if (tile == 0) {
        return false;
    }

    return true;
}

// Moves the targets of all Movers that would collide if they moved to the
// stationary list.
function extractCollisions(targets, stationary) {
    // For each target, check if it will end with a collision.
    for (var curr = 0; curr < targets.length - 1; curr++) {
        var dupe = false;

        for (var check = curr + 1; check < targets.length; check++) {
            // If the movement of the current mover will cause a collision,
            // then move the target of the mover it would collide with to the
            // stationary list.
            if (collide(targets[curr], targets[check])) {
                dupe = true;
                stationary[stationary.length] = targets[check];
                delete targets[check];
            }
        }

        // If the current mover would collide with any other mover, then add
        // it to the stationary list.
        if (dupe) {
            stationary[stationary.length] = targets[curr];
            delete targets[curr];
        }
    }
}

// Check if two targets will cause a collision.
function collide(t1, t2) {
    // If one of the targets is undefined, then clearly it can't cause a
    // collision.
    if (!t1 || !t2) {
        return false;
    }

    // If both targets are on the same square in the grid, then the two Movers
    // will collide if they try to move there.
    if (sameLocation(t1, t2)) {
        return true;
    }

    // If two Movers are facing each other and try to move into each other's
    // square, then they'll collide.
    if (sameLocation(t1.mover, t2) && sameLocation(t1, t2.mover)) {
        return true;
    }

    return false;
}

// Check if two elements with locations occupy the same square in the grid.
function sameLocation(left, right) {
    if (!left || !right) {
        return false;
    }
    return ((left.row == right.row) && (left.col == right.col));
}

// Determines which Movers should remain stationary because other Movers
// will remain stationary and block their movement paths.
function propagateStationary(targets, stationary) {
    var propagated;
    do {
        propagated = false;
        for (var curr = 0; curr < targets.length; curr++) {
            for (var check = 0; check < stationary.length; check++) {
                // If a mover remains stationary, then any Mover that would
                // move into its square would collide with it and so should
                // remain stationary as well.
                if (sameLocation(targets[curr], stationary[check].mover)) {
                    propagated = true;
                    stationary[stationary.length] = targets[curr];
                    delete targets[curr];
                    break;
                }
            }
        }
    } while (propagated);
}

// Checks if the guide's target is in a list of targets.
function containsGuide(targets) {
    for (var curr = 0; curr < targets.length; curr++) {
        if (sameLocation(guide, targets[curr].mover)) {
            return true;
        }
    }
    return false;
}


//
// PUZZLE COMPLETION
//

// Checks if each goal is occupied by an appropriate Mover.
function checkGoals() {
    for (var i = 0; i < goals.length; i++) {
        if (!checkGoal(goals[i])) {
            return;
        }
    }

    completePuzzle();
}

// Checks if a specific goal is occupied by an appropriate Mover.
function checkGoal(goal) {
    var checkList;
    if (goal.mover == "guide") {
        return sameLocation(guide, goal);
    } else if (goal.mover == "follower") {
        checkList = followers;
    } else {
        checkList = rebels;
    }

    for (var i = 0; i < checkList.length; i++) {
        if (sameLocation(goal, checkList[i])) {
            return true;
        }
    }

    return false;
}

// Markes the current puzzle as solved and saves its completion status.
function completePuzzle() {
    solved[currentPuzzle] = 1;
    document.getElementById("status").innerHTML = "puzzle solved";
    saveSolved();
}

// Saves the solved puzzle data in a cookie.
function saveSolved() {
    setCookie("guide_solved", solved.join(""), 10 * 365 * 24 * 60 * 60 * 1000);
}

