/**
 * jSokoban
 * Nicholas Wright (http://www.nicholaswright.org/games/jsokoban)
 * Created 06 / 22 / 2008
 *
 * Minicosmos levels from http://members.aol.com/SokobanMac/levels/minicosmosText.html
 * Sasquatch IV levels from http://members.aol.com/SokobanMac/levels/sasquatch4Text.html
 */
var DefaultMaps = [
    // 'EASY'
    // Minicosmos levels
    '  ##### \n' +
    '###   # \n' +
    '# $ # ##\n' +
    '# #  . #\n' +
    '#    # #\n' +
    '##$#.  #\n' +
    ' #@  ###\n' +
    ' #####  ',

    '  ##### \n' +
    '###   # \n' +
    '# $ # ##\n' +
    '# #  . #\n' +
    '# .  # #\n' +
    '##$#.$ #\n' +
    ' #@  ###\n' +
    ' #####  ',

    '    ####\n' +
    '#####  #\n' +
    '#   $  #\n' +
    '#  .#  #\n' +
    '## ## ##\n' +
    '#      #\n' +
    '# @#   #\n' +
    '#  #####\n' +
    '####    ',
    
    '    ####\n' +
    '#####  #\n' +
    '#   $  #\n' +
    '# *.#  #\n' +
    '## ## ##\n' +
    '#      #\n' +
    '# @#   #\n' +
    '#  #####\n' +
    '####    ',

    '    ####\n' +
    '#####  #\n' +
    '#   *  #\n' +
    '# *.#  #\n' +
    '## ## ##\n' +
    '# $    #\n' +
    '# @#   #\n' +
    '#  #####\n' +
    '####    ',

    ' #####  \n' +
    ' #   ## \n' +
    '## #$ ##\n' +
    '# $    #\n' +
    '#. .#  #\n' +
    '### @ ##\n' +
    '  # # # \n' +
    '  #   # \n' +
    '  ##### ',

    ' #####  \n' +
    ' #   ## \n' +
    '##.#$ ##\n' +
    '# $    #\n' +
    '#. .#$ #\n' +
    '### @ ##\n' +
    '  # # # \n' +
    '  #   # \n' +
    '  ##### ',

    ' #####  \n' +
    ' #   #  \n' +
    '##$# ###\n' +
    '#   $@ #\n' +
    '# #  # #\n' +
    '# #. . #\n' +
    '#   ####\n' +
    '#####   ',


    // 'HARD'
    // sasquatch 4 Level 6
    '  ####           \n' +
    '###  #           \n' +
    '#  ..# #######   \n' +
    '# #..# #     ####\n' +
    '# #. ###   $    #\n' +
    '# #.   # $ $ $$ #\n' +
    '# #  @ ### $##  #\n' +
    '#           #####\n' +
    '##  #########    \n' +
    ' ####            ',

    // sasquatch 4 Level 7
    ' ############### \n' +
    '## $.       .$ ##\n' +
    '#  # ####### #  #\n' +
    '# #           # #\n' +
    '#  .***$#$***.  #\n' +
    '###     #     ###\n' +
    '  # ####@#### #  \n' +
    '  #           #  \n' +
    '  #############  ',

    // sasquatch 4 Level 8
    '#######################\n' +
    '#      #   #   #      #\n' +
    '# $@$$ # $     # .. ..#\n' +
    '## ## ### ### ### ## ##\n' +
    ' # #       #       # # \n' +
    ' # #   #   #   #   # # \n' +
    ' # ################# # \n' +
    ' #                   # \n' +
    ' ##################### ',

    // sasquatch 4 Level 9
    '########### \n' +
    '#@  #  #  # \n' +
    '#  $#$   $# \n' +
    '##  #..#  # \n' +
    ' #  #..#  # \n' +
    ' #  #..#  ##\n' +
    ' #$   $#$  #\n' +
    ' #  #  #   #\n' +
    ' ###########',

    // sasquatch 4 Level 10
    '######  \n' +
    '#    #  \n' +
    '# .$ #  \n' +
    '# ** #  \n' +
    '##$. #  \n' +
    ' #  ####\n' +
    ' # ##  #\n' +
    ' #  #  #\n' +
    ' #     #\n' +
    ' #.**$@#\n' +
    ' #  #  #\n' +
    ' #######',

    // sasquatch 4 Level 11
    '  ########\n' +
    ' ##.... @#\n' +
    ' #  # .  #\n' +
    '## #  # ##\n' +
    '#  #$ # # \n' +
    '# $   # ##\n' +
    '###$ ##  #\n' +
    '  #   $$ #\n' +
    '  #   #  #\n' +
    '  ########',

    // sasquatch 4 Level 12
    ' ######   \n' +
    ' #    #   \n' +
    ' #    ### \n' +
    ' ##*#   # \n' +
    '## . ## # \n' +
    '#     # ##\n' +
    '# #.#  $ #\n' +
    '# $.###$ #\n' +
    '### ##   #\n' +
    '  #   $$@#\n' +
    ' ##..##  #\n' +
    ' #   #####\n' +
    ' #   #    \n' +
    ' #####    ',

    // sasquatch 4 Level 13
    '      ####  \n' +
    '  #####  #  \n' +
    '###.  #$ ## \n' +
    '#  *   .*.# \n' +
    '# $.$ #$  # \n' +
    '### ###   # \n' +
    ' #   ### ###\n' +
    ' #  $# $.$ #\n' +
    ' #.*.@  *  #\n' +
    ' ## $#  .###\n' +
    '  #  #####  \n' +
    '  ####      ',

    // sasquatch 4 Level 14
    ' ######    \n' +
    ' #    #####\n' +
    ' # $ *#   #\n' +
    ' #  * * $ #\n' +
    '###* . *  #\n' +
    '# * .@. * #\n' +
    '#  * . *###\n' +
    '# $ * *  # \n' +
    '#   #* $ # \n' +
    '#####    # \n' +
    '    ###### ',

    // sasquatch 4 Level 15
    '###########\n' +
    '#    *    #\n' +
    '# $$ ## $ #\n' +
    '#  $..#$$ #\n' +
    '# ##*.*.  #\n' +
    '#*#..@..#*#\n' +
    '#  .*.*## #\n' +
    '# $$#..$  #\n' +
    '# $ ## $$ #\n' +
    '#    *    #\n' +
    '###########',

    // sasquatch 4 Level 16
    '#############\n' +
    '#     $   . #\n' +
    '#.$ $### *$ #\n' +
    '# ** ## .*  #\n' +
    '#  .$#..$ $ #\n' +
    '# # .$.$### #\n' +
    '#$##..@..##$#\n' +
    '# ###$.$. # #\n' +
    '# $ $..#$.  #\n' +
    '#  *. ## ** #\n' +
    '# $* ###$ $.#\n' +
    '# .   $     #\n' +
    '#############',

    // sasquatch 4 Level 17
    '###############\n' +
    '#             #\n' +
    '# $.$.$.$.$.$ #\n' +
    '# .$.$.#.$.$. #\n' +
    '# $.$.$ $.$.$ #\n' +
    '# .$.$.#.$.$. #\n' +
    '# $.$.$ $.$.$ #\n' +
    '# .# # @ # #. #\n' +
    '# $.$.$ $.$.$ #\n' +
    '# .$.$.#.$.$. #\n' +
    '# $.$.$ $.$.$ #\n' +
    '# .$.$.#.$.$. #\n' +
    '# $.$.$.$.$.$ #\n' +
    '#             #\n' +
    '###############',

    // sasquatch 4 Level 18
    ' ############### \n' +
    '##  #   #   #  ##\n' +
    '#   **.. ..**   #\n' +
    '#  *   $$$   *  #\n' +
    '##* .### ###. *##\n' +
    '# * ## $@$ ## * #\n' +
    '# . #       # . #\n' +
    '# .$#$ ### $#$. #\n' +
    '## $   # #   $ ##\n' +
    '# .$#$ ### $#$. #\n' +
    '# . #       # . #\n' +
    '# * ## $ $ ## * #\n' +
    '##* .### ###. *##\n' +
    '#  *   $$$   *  #\n' +
    '#   **.. ..**   #\n' +
    '##  #   #   #  ##\n' +
    ' ############### ',

    // sasquatch 4 Level 19
    ' ######### \n' +
    ' #   *   # \n' +
    ' # ## ## # \n' +
    ' #  * *  # \n' +
    '###  #  ###\n' +
    '#  .$#$.  #\n' +
    '# #  @  # #\n' +
    '#  .$#$.  #\n' +
    '###  #  ###\n' +
    '  #######  ',

    // sasquatch 4 Level 20
    '####         \n' +
    '#  ######### \n' +
    '#    ##    # \n' +
    '# $$$#     # \n' +
    '##...# #$$$# \n' +
    ' #...# #...# \n' +
    ' #$$$  #...##\n' +
    ' #    ##$$$ #\n' +
    ' ####### @  #\n' +
    '       ######',

    // sasquatch 4 Level 21
    '  #########  \n' +
    ' ##   #   #  \n' +
    '## $# # #$###\n' +
    '#   #. .#   #\n' +
    '# $ *.@.* $ #\n' +
    '#   #. .#   #\n' +
    '###$# # #$ ##\n' +
    '  #   #   ## \n' +
    '  #########  ',

    // sasquatch 4 Level 22
    '  ####  ####    \n' +
    '  #  ####  #### \n' +
    '###     $ $   # \n' +
    '#    # $  *...##\n' +
    '# $ # #### #.. #\n' +
    '##   #         #\n' +
    ' # $$ $$@ #...##\n' +
    ' #   #  ####### \n' +
    ' ####   # #     \n' +
    '    #####       ',

    // sasquatch 4 Level 23
    '       #### \n' +
    '  ######  # \n' +
    ' ##  $  $ # \n' +
    '##  $ #$  # \n' +
    '#  $ #   $# \n' +
    '#   # ##  ##\n' +
    '###$  ..#  #\n' +
    '  #  #*...@#\n' +
    '  #   ..####\n' +
    '  #  ####   \n' +
    '  ####      '
];

function Piece( elTD ) {
    this.elTD = elTD;
    this.setCoords();
}

Piece.prototype = {
    setCoords: function() {
        var aryCoords = this.elTD.id.split( "-" );
        this.x = parseInt( aryCoords.first() );
        this.y = parseInt( aryCoords.last() );
    },

    addClass: function( strClass ) {
        Element.addClassName( this.elTD, strClass );
    },

    removeClass: function( strClass ) {
        Element.removeClassName( this.elTD, strClass );
    },

    canBePushed: function() {
        return this.elTD.hasClassName( 'ruby' );
    },

    isBlocked: function() {
        return ![ "", "storage" ].include( this.elTD.className );
    }
}

function MapGeneration( strMap ) {

    this.hshClassMap = $H( {
        " ": "",
        "#": "wall",
        "@": "player",
        "$": "ruby",
        ".": "storage",
        "*": "storage ruby",
        "+": "storage player"
    } );


    this.elBoardContainer = $( 'jsokoban' ); 
    this.elMapEditorInput = $( 'input-map' );
    this.aryValidTiles = this.hshClassMap.keys();

    this.intCurrentMap = null;
    this.setup();
    this.generate();
}

MapGeneration.prototype = {
    aryNodes: [],
    hshGameBoard: {},

    setup: function( strMap ) {
        this.aryRows = this.loadSuppliedMapOrRandom( strMap );
        this.elMapEditorInput.value = this.aryRows.join( "\n" );
        this.intRows = this.aryRows.length;
        this.intCols = this.aryRows.first().length;
    },

    reset: function( strMap ) {
        this.aryNodes.each( Element.remove );
        this.aryNodes.clear();
        this.setup( strMap );
        this.generate();
    },

    loadSuppliedMapOrRandom: function( strMap ) {
        if ( typeof( strMap ) == 'undefined' ) {
            strMap = this.randomMap();
        }
        return strMap.split( '\n' )
    },

    randomMap: function() {
        var intIndex = Math.floor( Math.random() * DefaultMaps.length );
        this.intCurrentMap = intIndex;
        return DefaultMaps[ intIndex ];
    },

    fatal: function( strMessage ) {
        strMessage += '\n\nLoading a random map...';
        alert( strMessage );
        this.reset();
        return -1;
    },

    createElement: function( elParent, strTagName, hshAttributes ) {
        var elNewNode = document.createElement( strTagName );
        $H( hshAttributes ).each( function( aryValues ) {
            elNewNode.setAttribute( aryValues.first(), aryValues.last() );
        } );
        // push references to the beginning of the nodes list so we delete children first
        this.aryNodes.unshift( elNewNode );
        elParent.appendChild( elNewNode );
        return elNewNode;
    },

    generateColID: function( intRow, intCol ) {
        return intRow + '-' + intCol;
    },

    findSquare: function( intRow, intCol ) {
        return this.hshGameBoard[ this.generateColID( intRow, intCol ) ];
    },

    checkForWin: function() {
        return $$( '.storage' ).all( function( el ) {
            return  el.hasClassName( 'ruby' );
        } );
    },

    generate: function() {
        var elCurrentTable = this.createElement( this.elBoardContainer, 'table', { 'id': 'jsokoban-board', 'cellspacing': '0' } );
        var elCurrentTbody = this.createElement( elCurrentTable, 'tbody', {} );
        var intRow = 0;
        var context = this;
        this.aryRows.each( function( strRow ) {
            var intCol = 0;
            var elCurrentTr = context.createElement( elCurrentTbody, 'tr', {} );
            var aryCols = strRow.toArray();

            if ( aryCols.length != context.intCols ) {
                context.fatal( "Your map must form a box! This means the columns and rows may not be staggered." );
            }

            aryCols.each( function( strCol ) {
                var strTileClasses = context.hshClassMap[ strCol ]; 
                if ( strTileClasses != null ) {
                    var strCurrentId = context.generateColID( intRow, intCol );
                    var elCurrentTd = context.createElement( elCurrentTr, 'td', { 'id': strCurrentId, 'class': strTileClasses } );
                    context.hshGameBoard[ strCurrentId ] =  new Piece( elCurrentTd );
                } else {
                    context.fatal( "Your map may only contain valid tiles characters!" );
                } 
                intCol++;
            } );
            intRow++;
        } );
        return this.hshGameBoard;
    }
}

function Player( board ) {
    this.board = board;
    this.setup();
}

Player.prototype = {
    setup: function() {
        this.intPushes = 0;
        this.intMoves = 0;
        this.findPlayer();
    },

    reset: function() {
        this.setup();
        $( 'moves' ).innerHTML = this.intMoves;
        $( 'pushes' ).innerHTML = this.intPushes;
    },

    findPlayer: function() {
        var playerFound = $$( '.player' ).first();
        if ( playerFound ) {
            var aryCoords = playerFound.id.split( "-" );
            this.row = parseInt( aryCoords.first() );
            this.col = parseInt( aryCoords.last() );
        }
    },

    withinBounds: function( intRow, intCol ) {
        if ( ( intRow < 0 ) ||
             ( intRow > ( this.board.intRows - 1 ) ) || 
             ( intCol < 0 ) ||
             ( intCol > ( this.board.intCols - 1 ) ) ) {
            return false;
        }
        return this.board.findSquare( intRow, intCol );
    },

    incrementMoves: function() {
        this.intMoves++;
        $( 'moves' ).innerHTML = this.intMoves;
    },

    incrementPushes: function() {
        this.intPushes++;
        $( 'pushes' ).innerHTML = this.intPushes;
    },

    checkForWin: function() {
        if ( this.board.checkForWin() ) {
            var strMsg = "Congratulations! You've managed to solve this puzzle!\n\n" +
                         "Press OK to try another, or hit Cancel to stay where you are.\n\n" + 
                         "If you stay where you are, click 'Restart Map' and beat it with less pushes than before!";
            if ( confirm( strMsg ) ) {
                this.board.reset();
                this.reset();
            }
        }
    },

    pushRuby: function( neighbors ) {
        neighbors.near.removeClass( 'ruby' );
        neighbors.far.addClass( 'ruby' );
        this.incrementPushes();
    },

    updatePlayerPosition: function( neighbor ) {
        this.board.findSquare( this.row, this.col ).removeClass( 'player' );
        neighbor.addClass( 'player' );
        this.row = neighbor.x;
        this.col = neighbor.y;
        this.incrementMoves();
    },

    move: function( strDirection ) {
        var neighbors = this.neighbors( strDirection );
        if ( !neighbors.near ) {
            return false;
        }

        if ( neighbors.near.isBlocked() ) {
            if ( ( neighbors.near.canBePushed() ) && ( !neighbors.far.isBlocked() ) ) { 
                this.pushRuby( neighbors );
                this.updatePlayerPosition( neighbors.near );
                this.checkForWin();
            }
        } else {
            this.updatePlayerPosition( neighbors.near );
        }
    },

    constructNeighborsHash: function( intNearRow, intNearCol, intFarRow, intFarCol ) {
        return { near: this.withinBounds( this.row + intNearRow, this.col + intNearCol ), far: this.withinBounds( this.row + intFarRow, this.col + intFarCol ) };
    },

    neighbors: function( strDirection ) {
        if ( strDirection == "left" ) {
            return this.constructNeighborsHash( 0, -1, 0, -2 );
        } else if ( strDirection == "right" ) {
            return this.constructNeighborsHash( 0, 1, 0, 2 );
        } else if ( strDirection == "up" ) {
            return this.constructNeighborsHash( -1, 0, -2, 0 );
        } else if ( strDirection == "down" ) {
            return this.constructNeighborsHash( 1, 0, 2, 0 );
        }

    },

    handleKeyPress: function( event ) {
        var keyCode = event.keyCode;
        if  ( ( keyCode >= 37 ) || ( keyCode <= 40 ) ) {
            // left 37
            if ( keyCode == 37 ) {
                this.move( "left" );

            // up 38
            } else if ( keyCode == 38 ) {
                this.move( "up" );

            // right 39 
            } else if ( keyCode == 39 ) {
                this.move( "right" );

            // down 40
            } else if ( keyCode == 40 ) {
                this.move(  "down" );
            }
        }
    },

    handleKeyDepress: function( event ) {
    }
};


function setupEventHandlers( player, board ) {
    var hideMapEditor = function() {
        $( 'edit-this-map' ).innerHTML = 'Edit This Map';
        $( 'map-editor' ).hide();
    }

    Event.observe( $( 'edit-this-map' ), 'click', function( event ) {
        var strLinkText = 'Edit This Map';
        this.innerHTML = ( this.innerHTML == strLinkText ) ?  'Cancel Editing Map' : strLinkText;
        $( 'jsokoban-board' ).toggle();
        $( 'map-editor' ).toggle();
    } );

    Event.observe( $( 'load-map' ), 'click', function( event ) {
        hideMapEditor();
        board.reset( $( 'input-map' ).value.gsub( /\r/, '' ) );
        player.reset();
    } );

    Event.observe( $( 'retry-map' ), 'click', function( event ) {
        hideMapEditor();
        board.reset( $( 'input-map' ).value.gsub( /\r/, '' ) );
        player.reset();
    } );

    Event.observe( $( 'randomize-map' ), 'click', function( event ) {
        event.preventDefault();
        hideMapEditor();
        board.reset();
        player.reset();
    } );


    Event.observe( document, 'keydown', function( event ) {
        event.preventDefault();
        player.handleKeyPress( event );
    } );

    Event.observe( document, 'keyup', function( event ) {
        player.handleKeyDepress( event );
    } );
}
