/**
* MooTris
* An implementation of the popular computer game 'Tetris' using the
* MooTools JavaScript framework.
*
* This file consists of the main game functions - responsible for setting up
* the game, triggering shape movement, purging lines and recording the score.
*
* @author  Alan Shaw
* @link    http://www.freestyle-developments.co.uk
* @version 1.0
* @package uk.co.fsd.Tetris
*/

/**
* The Tetris game class.
*
* Usage:
* <code>
*   var game = new Tetris();
*   game.play();
* </code>
*
* If setup as above, the game will be played with all the default settings,
* assuming a DOM element with the Id 'tetris' exists. Many options can however
* be set by passing the Tetris class a MooTools options object. For example:
* <code>
*    var game = new Tetris({width: 12, height: 22, difficulty: 6});
* </code>
*
* @package uk.co.fsd.Tetris
*/
var Tetris = new Class({

    state: null,
    shape: null,
    interval: null,
    gameOver: false,
    score: 0,
    scoreboard: null,
    options: {
        speed: 100, // current speed of the game (ms)
        maxSpeed: 75, // maximum speed the game will increase to (ms)
        difficulty: 5, // how fast the game gets harder
        gravity: 'naive', // gravity type (naive or recursive)
        easySpin: false, // pauses timer whilst rotating/moving if true
        grid: null, // element to be used as the tetris grid
        width: 10, // width of the game, in blocks
        height: 20, // height of the game, in blocks
        size: 20 // size in px of one unit of width/height
    },

    /**
    * Does everything necessary to set up a new tetris game
    * @param object options game options
    */
    initialize: function(options) {
    
        // try to get default grid (this cannot be set in options because the
        // DOM might not be ready)
        this.options.grid = $E('#tetris');

        // set user options
        this.setOptions(options);

        // Check board is big enough
        if(this.options.width < 4) {
            this.options.width = 4;
        }
        if(this.options.height < 4) {
            this.options.height = 4;
        }
        
        // check speed is not too fast
        if(this.options.speed < this.options.maxSpeed) {
            this.options.speed = this.optioins.maxSpeed;
        }
        
        // create the "2D" state array
        this.state = new Array(this.options.width * this.options.height);
        
        // normalise gravity string
        this.options.gravity = this.options.gravity.toLowerCase();

        this.options.grid.setStyles({
            'width': (this.options.width * this.options.size) + 'px',
            'height': (this.options.height * this.options.size) + 'px'
        });

        // On IE and safari/Konqueror, keypress doesn't include arrows
        if(window.ie || window.webkit) {
            document.addEvent('keydown', this.handleKeypress.bindWithEvent(this));
        } else {
            document.addEvent('keypress', this.handleKeypress.bindWithEvent(this));
        }
    },
    
    /**
    * Performs one cycle of the game, as follows:
    *
    * If shape doesn't exist, creates a shape and checks to see if the top of
    * the grid has been reached i.e. game over
    *
    * If shape does exist, move shape down 1 space, or lock the shape into
    * position on the grid if that can't be done
    *
    * Remove complete lines from the grid
    *
    * Re-draw the blocks locked into the grid
    *
    * Re-draw the shape (since until the shape "lands" its not part of the grid)
    *
    * Adjust the game speed
    *
    * Schedule play() to be called again in a few milliseconds
    */
    play: function() {
    
        //this.drawDebugState();

        // is there a shape in play?
        if(this.shape == null) {
 
            // create new shape
            this.shape = new Shape($random(0, 6), 
                                   this.state, 
                                   this.options);

            // game over? try to move
            if(this.shape.tryMove({x:0, y:0}) == false) {
                this.shape = null;
                this.gameOver = true;
            }
            
        } else {
        
            // move shape
            if(this.shape.moveDown() == false) {

                // lock down
                this.shape.blocks.each(function(block) {
                    this.state[block.x + (block.y * this.options.width)] = block;
                }, this);

                // discard
                this.shape = null;
            }
        }
        
        // continue game if not game over
        if(this.gameOver == false) {
        
            // remove complete lines
            this.purge();
            
            // redraw the board
            this.draw(this.state);
    
            // redraw the shape
            if(this.shape != null) {
                this.draw(this.shape.blocks);
            }
    
            // adjust the game speed
            this.speedUp();
            
            // play again after this.speed ms
            this.interval = this.play.delay(this.options.speed, this);
        }
    },
    
    /**
    * Removes comepleted lines from the board and moves all other blocks down
    * according to the type of gravity that has been set in options.
    *
    * Niave gravity moves blocks down by the number of lines that were removed
    *
    * Recursive gravity moves blocks down until they hit the bottom of the grid,
    * or are blocked by another block
    */
    purge: function() {

        var isPurgeable = function(start, end) {
            
            for(var i = start; i < end; i++) {
                if(this.state[i] == null) {
                    return false;
                }
            }
            return true;
        }.bind(this);

        // records the purgable lines
        var lines = new Array();
        
        // get the purgeable lines
        for(var i = 0; i < this.state.length; i = i + this.options.width) {
            if(isPurgeable(i, i + this.options.width) == true) {
                lines.extend([i]);
            }
        }
        
        // deal with the lines depending on gravity type
        switch(this.options.gravity) {
            default:
            case 'naive':

                lines.each(function(number) {
                
                    // for each purgable line, remove the line
                    for(var i = number; i < number + this.options.width; i++) {

                        // erase the block element from the screen
                        this.state[i].erase();

                        // and from the grid
                        this.state[i] = null;
                    }
                    
                    // now move all blocks above this line down by one
                    for(var i = number - 1; i >= 0; i--) {

                        if(this.state[i] != null) {
                        
                            // move the block
                            this.state[i].y++;

                            // move in the array
                            this.state[i+this.options.width] = this.state[i];
                            this.state[i] = null;
                        }
                    }
                    
                    // score points for this
                    this.scorePoints(250);

                }, this);
                
            break;
            case 'recursive':
            
                lines.each(function(number) {
                
                    // for each purgable line, remove the line
                    for(var i = number; i < number + this.options.width; i++) {

                        // erase the block element from the screen
                        this.state[i].erase();

                        // and from the grid
                        this.state[i] = null;
                    }
                    
                    var currentIndex;
                    var nextIndex;
                    
                    // now move all blocks above this line down by one
                    for(var i = number - 1; i >= 0; i--) {

                        if(this.state[i] != null) {
                        
                            currentIndex = i;
                        
                            // Keep looking forward to see if we fall any further
                            while(true) {
                            
                                // look in this index next
                                nextIndex = currentIndex + this.options.width;
                            
                                if(nextIndex < this.state.length &&
                                   this.state[nextIndex] == null) {

                                   // move the block (easier to do this as we go
                                   // along rather than work it out at the end)
                                   this.state[i].y++;
                                   
                                   currentIndex = nextIndex;

                                } else {
                                    break;
                                }
                            }

                            // move in the array
                            this.state[currentIndex] = this.state[i];
                            this.state[i] = null;
                        }
                    }
                    
                    // score points for this
                    this.scorePoints(250);

                }, this);

            break;
        }
    },
    
    /**
    * Gets the passed blocks to redraw (move) the DOM element they are
    * associated with.
    *
    * @param array blocks an array of uk.co.fsd.Block objects
    */
    draw: function(blocks) {
        
        // loop through each row bottom up
        blocks.each(function(block) {
        
            // is there a block here?
            if(block != null) {
                block.draw(this.options.grid);
            }
            
        }, this);
    },
    
    /**
    * Debug method used to draw the positions of uk.co.fsd.Block's according
    * to the state array
    */
    drawDebugState: function() {
        var debug = $E('#debug');
        var html = '';
        for(var i = 0; i < this.state.length; i++) {
            if(this.state[i] == null) {
                html = html + 'o';
            } else {
                html = html + 'x';
            }
            if((i+1) % this.options.width == 0) {
                html = html + '<br />';
            }
        }
        debug.setHTML(html);
    },
    
    /**
    * Alters options.speed, which is used as the period between calls to
    * play().
    */
    speedUp: function() {

        // adjust the speed according to a random number between 0 and the
        // difficulty setting
        this.options.speed = this.options.speed - $random(0, this.options.difficulty);
        
        // Cap at maxSpeed
        if(this.options.speed < this.options.maxSpeed) {
            this.options.speed = this.options.maxSpeed;
        }
    },
    
    /**
    * Increments the players current score.
    * @param integer amount value to increase the current score by
    */
    scorePoints: function(amount) {
    
        this.score += amount;
        
        if(this.scoreboard == null) {
            this.scoreboard = new Element('div');
            this.scoreboard.setStyles({
                'float': 'left',
                'position':'relative',
                'z-index': 1138,
                'padding': '10px'
            });
            this.scoreboard.injectInside(this.options.grid);
        }
        
        this.scoreboard.setHTML('Score:'+this.score);
        
    },
    
    /**
    * Dispatches the necessary functions used to deal with a key press by the
    * player.
    * @param object event MooTools event object (passed automatically)
    */
    handleKeypress: function(event) {

        // only do something if there is a shape to work with
        if(this.shape != null) {
   
            // stop interval while something is happening
            if(this.options.easySpin == true) {
                this.interval = $clear(this.interval);
            }
            
            var moved = false;
            
            // do action
            switch(event.key) {
                case 'up':
                    moved = this.shape.rotateClockwise();
                break;
                case 'right':
                    moved = this.shape.moveRight();
                break;
                case 'down':
                    moved = this.shape.moveDown();
                    
                    // score points for this
                    if(moved == true) {
                        this.scorePoints(5);
                    }
                break;
                case 'left':
                    moved = this.shape.moveLeft();
                break;
            }
            
            // Redraw the shape if it has moved
            if(moved == true) {
                this.draw(this.shape.blocks);
            }
            
            // resume play (sort of)
            if(this.options.easySpin == true) {
                this.interval = this.play.delay(this.options.speed, this);
            }
        }
    }
});

Tetris.implement(new Options);
