//  TODO:
//    * touch controls
//    * allow late piece rotation
//    * code cleanup


//--------------------------------------------------//
//    PAGE OBJECT & LOGIC                           //
//--------------------------------------------------//

var Page = {  
  IsSetup: false,
  
  body: document.getElementsByTagName('body')[0],
  cvs: document.createElement('canvas'),
  ctx: 0,
  
  unitSize: 0,
  AreaArr: [], 
  
  // calculates the unit size, canvas bounds, and canvas positioning
  WindowChanged: function() {
    
    // Calulcate the unitSize based on window width and height.
    // The minimum of these calculations will be used.
    
    var bodyW = document.documentElement.clientWidth,
        bodyH = document.documentElement.clientHeight,
        newUnitW = (bodyW - (bodyW % 80)) / 16,
        newUnitH = (bodyH - (bodyH % 100)) / 20,
        newUnitMin = Math.max(Math.min(newUnitW, newUnitH), 20);    
    
    // if the calcUnitMin != unitSize, update unitSize, recalculate
    // all DrawAreaObjs, and update the canvas element bounds
    
    this.unitSize = newUnitMin;

    // store Right-most & Bottom-most points for canvas bounds
    var rightLimit = 0,
        bottomLimit = 0;

    for(var i = 0; i < Page.AreaArr.length; i++){
      Page.AreaArr[i].CalculateBounds();

      var newRightLimit = Page.AreaArr[i].left + Page.AreaArr[i].W,
          newBottomLimit = Page.AreaArr[i].top + Page.AreaArr[i].H;

      rightLimit = Math.max(newRightLimit, rightLimit);
      bottomLimit = Math.max(newBottomLimit, bottomLimit);
    }

    this.cvs.width = rightLimit;
    this.cvs.height = bottomLimit;

    // left pos uses Game.W because ideally that area is centered
    var topPos = (bodyH - bottomLimit) / 2,
        leftPos = (bodyW / 2) - (this.Game.W / 2),
        rightOffset = bodyW - (leftPos + rightLimit) - this.unitSize * 0.5;       

    // if default canvas positioning extends beyond screen, adjust it
    if (rightOffset < 0){
      leftPos = Math.max(this.unitSize * 0.5, leftPos + rightOffset);
    }

    this.cvs.style.left = leftPos + 'px';
    this.cvs.style.top =  topPos + 'px';    
  },
  
  // performs the page setup
  Initialize: function(){
    
    // if page has not been setup, do initial setup
    if (this.IsSetup === false){
      document.body.appendChild(Page.cvs);

      this.body.style.overflow = 'hidden';
      this.body.style.backgroundColor = 'rgb(19,21,25)';
      this.cvs.style.position = 'absolute';     
      this.ctx = this.cvs.getContext('2d');
    
      this.IsSetup = true;
    }
    
    this.WindowChanged();
    
    // dirty all draw areas
    for(var i = 0; i < Page.AreaArr.length; i++){
      Page.AreaArr[i].IsDirty = true;
    }
  },

  // redraws canvas visuals whenever the page is marked as dirty
  Update: function() {
    for(var i = 0; i < Page.AreaArr.length; i++){
      if (Page.AreaArr[i].IsDirty){
        Page.AreaArr[i].Draw();
        Page.AreaArr[i].IsDirty = false;
      }
    }
  }
};
 

// Definition for Area objects. Bounds are in UNITS
function DrawAreaObj(Left,Top,Width,Height,DrawFunction){

  // bounds in UNITS
  this.leftBase = Left;
  this.topBase = Top;
  this.widthBase = Width;
  this.heightBase = Height;
  
  // bounds in PIXELS
  this.left = 0;
  this.top = 0;
  this.W = 0;
  this.H = 0;
  
  // dirty flag (clean yourself up flag, you're better than that)
  this.IsDirty = false;
  
  // bounds recalculated and area dirtied when unitSize changes
  this.CalculateBounds = function(){
    this.left = this.leftBase * Page.unitSize;
    this.top = this.topBase * Page.unitSize;
    this.W = this.widthBase * Page.unitSize;
    this.H = this.heightBase * Page.unitSize;
    
    this.IsDirty = true;
  };
  
  // draw function as passed in by the callee
  this.Draw = DrawFunction;
  
  // push this area into the area arr    
  Page.AreaArr.push(this);
}


Page.Game = new DrawAreaObj(0,0,10,20,function(){
  
  // unitSize minus a couple pixels of separation
  var uDrawSize = Page.unitSize - 2,
      drawL,
      drawT;

  // redraws the background elements for game area
  Page.ctx.fillStyle = 'rgb(28,30,34)';
  Page.ctx.fillRect(this.left, this.top, this.W, this.H); 
  
  // draw the static unit blocks
  for(var i = 0; i < GM.StaticUnits.length; i++){
    for(var j = 0; j < GM.StaticUnits[i].length; j++){
      
      // get the unit value for this index pair
      var uValue = GM.StaticUnits[i][j];
      
      // if this unit value is not 0, draw the unit
      if (uValue !== 0){
        drawL = i * Page.unitSize + 1;
        drawT = j * Page.unitSize + 1;

        // fill this square with color based on player alive status        
        Page.ctx.fillStyle = (GM.IsAlive) ? uValue : 'rgb(34,36,42)';      
        Page.ctx.fillRect(drawL,drawT,uDrawSize,uDrawSize);
      }
    }
  }
  
  // draw the current active projection and piece (if exists)
  if (GM.Pc.Cur !== 0 && GM.IsAlive){
    var projColor = ColorWithAlpha(GM.Pc.Cur.color,0.1);   
    
    for(var k = 0; k < GM.Pc.Cur.UO.arr.length; k++){
      drawL = (GM.Pc.Cur.x + GM.Pc.Cur.UO.arr[k].x) * Page.unitSize + 1;
      drawT = (GM.Pc.Cur.y + GM.Pc.Cur.UO.arr[k].y) * Page.unitSize + 1;
      
      Page.ctx.fillStyle = GM.Pc.Cur.color;
      Page.ctx.fillRect(drawL,drawT,uDrawSize,uDrawSize); 
      
      // also draw the projection (if one exists)
      if (GM.IsAlive && GM.Pc.ProjY !== 0){
        drawT += GM.Pc.ProjY * Page.unitSize;
        
        Page.ctx.fillStyle = projColor;
        Page.ctx.fillRect(drawL,drawT,uDrawSize,uDrawSize);
      }
    }
  }
    
  // if the player is dead, draw the game over text
  if (!GM.IsAlive){
    DrawText("GAME OVER",'rgb(255,255,255)','500',
             'center',uDrawSize,this.W/2,this.H/4);
  }
});


Page.UpcomingA = new DrawAreaObj(10.5,2.6,2.5,2.5,function(){
  
  var uDrawSize = Math.floor(Page.unitSize / 2),
      pcA = GM.Pc.Upcoming[0];
  
  // next box background
  Page.ctx.fillStyle = 'rgb(28,30,34)';
  Page.ctx.fillRect(this.left, this.top, this.W, this.H);  
  
  // draw the upcoming piece (if one exists)
  if (pcA !== 0){
    Page.ctx.fillStyle = pcA.color;
    
    var totalL = 0, 
        totalT = 0, 
        countedL = [], 
        countedT = [];
    
    // calculate average positions of units in order to center
    for(var i = 0; i < pcA.UO.arr.length; i++){
      var curX = pcA.UO.arr[i].x,
          curY = pcA.UO.arr[i].y;
      
      if (countedL.indexOf(curX) < 0){
        countedL.push(curX);
        totalL += curX;
      }
      if (countedT.indexOf(curY) < 0){
        countedT.push(curY);
        totalT += curY;
      }
    }
    
    var avgL = uDrawSize * (totalL / countedL.length + 0.5),
        avgT = uDrawSize * (totalT / countedT.length + 0.5),
        offsetL = this.left + this.W/2,
        offsetT = this.top + this.H/2;
    
    console.log(avgL + ", " + avgT);
    
    // now draw the upcoming piece, using avg vars to center
    for(var j = 0; j < pcA.UO.arr.length; j++){
      var drawL = Math.floor(offsetL - avgL + pcA.UO.arr[j].x * uDrawSize),
          drawT = Math.floor(offsetT - avgT + pcA.UO.arr[j].y * uDrawSize); 
      
      Page.ctx.fillRect(drawL,drawT,uDrawSize - 1,uDrawSize - 1);
    }
  }
});


Page.UpcomingB = new DrawAreaObj(10.5,5.2,2.5,2.5,function(){
  
  var uDrawSize = Math.floor(Page.unitSize / 2),
      pcB = GM.Pc.Upcoming[1];
  
  // next box background
  Page.ctx.fillStyle = 'rgb(28,30,34)';
  Page.ctx.fillRect(this.left, this.top, this.W, this.H);
  
  // draw the upcoming piece (if one exists)
  if (pcB !== 0){
    Page.ctx.fillStyle = pcB.color;
    
    var totalL = 0, 
        totalT = 0, 
        countedL = [], 
        countedT = [];
    
    // calculate average positions of units in order to center
    for(var i = 0; i < pcB.UO.arr.length; i++){
      var curX = pcB.UO.arr[i].x,
          curY = pcB.UO.arr[i].y;
      
      if (countedL.indexOf(curX) < 0){
        countedL.push(curX);
        totalL += curX;
      }
      if (countedT.indexOf(curY) < 0){
        countedT.push(curY);
        totalT += curY;
      }
    }
    
    var avgL = uDrawSize * (totalL / countedL.length + 0.5),
        avgT = uDrawSize * (totalT / countedT.length + 0.5),
        offsetL = this.left + this.W/2,
        offsetT = this.top + this.H/2;
    
    console.log(avgL + ", " + avgT);
    
    // now draw the upcoming piece, using avg vars to center
    for(var j = 0; j < pcB.UO.arr.length; j++){
      var drawL = Math.floor(offsetL - avgL + pcB.UO.arr[j].x * uDrawSize),
          drawT = Math.floor(offsetT - avgT + pcB.UO.arr[j].y * uDrawSize); 
      
      Page.ctx.fillRect(drawL,drawT,uDrawSize - 1,uDrawSize - 1);
    }
  }
});


Page.UpcomingC = new DrawAreaObj(10.5,7.8,2.5,2.5,function(){
  
  var uDrawSize = Math.floor(Page.unitSize / 2),
      pcC = GM.Pc.Upcoming[2];
  
  // next box background
  Page.ctx.fillStyle = 'rgb(28,30,34)';
  Page.ctx.fillRect(this.left, this.top, this.W, this.H);
  
  // draw the upcoming piece (if one exists)
  if (pcC !== 0){
    Page.ctx.fillStyle = pcC.color;
    
    var totalL = 0, 
        totalT = 0, 
        countedL = [], 
        countedT = [];
    
    // calculate average positions of units in order to center
    for(var i = 0; i < pcC.UO.arr.length; i++){
      var curX = pcC.UO.arr[i].x,
          curY = pcC.UO.arr[i].y;
      
      if (countedL.indexOf(curX) < 0){
        countedL.push(curX);
        totalL += curX;
      }
      if (countedT.indexOf(curY) < 0){
        countedT.push(curY);
        totalT += curY;
      }
    }
    
    var avgL = uDrawSize * (totalL / countedL.length + 0.5),
        avgT = uDrawSize * (totalT / countedT.length + 0.5),
        offsetL = this.left + this.W/2,
        offsetT = this.top + this.H/2;
    
    console.log(avgL + ", " + avgT);
    
    // now draw the upcoming piece, using avg vars to center
    for(var j = 0; j < pcC.UO.arr.length; j++){
      var drawL = Math.floor(offsetL - avgL + pcC.UO.arr[j].x * uDrawSize),
          drawT = Math.floor(offsetT - avgT + pcC.UO.arr[j].y * uDrawSize); 
      
      Page.ctx.fillRect(drawL,drawT,uDrawSize - 1,uDrawSize - 1);
    }
  }
});


Page.ScoreBarHigh = new DrawAreaObj(10.5,0,4.5,1,function(){
  
  // draw the score area back bar
  Page.ctx.fillStyle = 'rgb(28,30,34)';
  Page.ctx.fillRect(this.left,this.top,this.W,this.H);
  
  
  // Draw the trophy symbol
  
  var miniUnit, left, top, width, height;
  
  miniUnit = Page.unitSize * 0.01;
  Page.ctx.fillStyle = 'rgb(255,232,96)';
    
  // trophy base
  left = Math.floor(this.left + miniUnit * 33);
  top = Math.floor(this.top + this.H - miniUnit * 28);
  width = Math.floor(miniUnit * 30);
  height = Math.floor(miniUnit * 12);
  Page.ctx.fillRect(left,top,width,height);
  
  // trophy trunk
  left = Math.floor(this.left + miniUnit * 42);
  top = Math.floor(this.top + this.H - miniUnit * 50);
  width = Math.floor(miniUnit * 12);
  height = Math.floor(miniUnit * 32);  
  Page.ctx.fillRect(left,top,width,height);
  
  // trophy bowl
  left = Math.floor(this.left + miniUnit * 48);
  top = Math.floor(this.top + this.H - miniUnit * 68);
  Page.ctx.arc(left, top, miniUnit * 24, 0, Math.PI);
  Page.ctx.fill();
  
  // draw the player's current score
  text = ("00000000" + GM.ScoreHigh).slice(-7);
  left = this.left + this.W - 4;
  top = this.top + Page.unitSize * 0.8;
  size = Math.floor(Page.unitSize * 0.8) + 0.5;
  
  DrawText(text, 'rgb(255,232,96)', '500', 'right', size, left, top);
});

Page.ScoreBarCur = new DrawAreaObj(10.5,1.1,4.5,1,function(){
  
  // draw the score area back bar
  Page.ctx.fillStyle = 'rgb(28,30,34)';
  Page.ctx.fillRect(this.left,this.top,this.W,this.H);
  
  // draw the player's current level
  var text, left, top, size, miniUnit;
  miniUnit = Page.unitSize * 0.01;
  
  text = ('00' + GM.Level).slice(-2);
  left = this.left + Math.floor(miniUnit * 50);
  top = this.top + Page.unitSize * 0.8;
  size = Math.floor(Page.unitSize * 0.5);
  
  DrawText(text, 'rgb(128,128,128)', '900', 'center', size, left, top);
  
  
  // draw the player's current score
  text = ("00000000" + GM.ScoreCur).slice(-7);
  left = this.left + this.W - 4;
  top = this.top + Page.unitSize * 0.8;
  size =  Math.floor(Page.unitSize * 0.8) + 0.5;
  
  DrawText(text, 'rgb(255,255,255)', '500', 'right', size, left, top);
});


//--------------------------------------------------//
//    GAME MANAGER OBJECT & LOGIC                   //
//--------------------------------------------------//

var GM = {
  
  //-- VARS ---------*/
  
  // timers
  TimeCur:0, TimeEvent:0, TickRate:0,
  
  // player status & score
  IsAlive:0, Level:0, PiecesRemaining:0,
  
  // score count and current piece score modifiers
  ScoreHigh: 0, ScoreCur:0, ScoreBonus:0, DifficultFlag: 0,

  // array of grid squares
  StaticUnits: [],
  
  
  /*-- FCNS ---------*/
  
  // Set up intial game var values
  Initialize: function(){
    
    // reset current piece vars
    this.Pc.Next = this.Pc.Cur = this.Pc.ProjY = 0;

    // populate the GM's static unit array with 0's (empty)
    for(var i = 0; i < 10; i++){
      this.StaticUnits[i] = [];
      for(var j = 0; j < 20; j++){
        this.StaticUnits[i][j] = 0;
      }
    }

    // reset timer
    this.TimeCur = this.TimeEvent = 0;
    this.TickRate = 500;

    // set up level values for level 1
    this.PiecesRemaining = 10;
    this.Level = 1;

    // reset the score and set player to alive
    this.ScoreCur = 0;
    this.IsAlive = true;
  },

  // updates time each frame and executing logic if a tick has passed
  Update: function(){
    this.TimeCur = new Date().getTime();
  
    if (this.TimeCur >= this.TimeEvent){
      
      if (GM.Pc.Cur === 0 && this.IsAlive){
        this.Pc.Generate();
      }
      else{
        this.Pc.DoGravity();
        this.Pc.ProjY = this.Pc.TryProject();
        Page.Game.IsDirty = true;
      }      
      
      this.RefreshTimer();
    }
  },
  
  // reset the tick timer (generates a new TimeEvent target)
  RefreshTimer: function(){
    this.TimeEvent = new Date().getTime() + this.TickRate;
  },
  
  // called when a piece is spawned, advances level if needed
  PieceSpawned: function(){
    this.PiecesRemaining--;
    if (this.PiecesRemaining <= 0){
      this.AdvanceLevel();
    }
  },
  
  // advance level, recalculate TickRate, reset pieces remaining
  AdvanceLevel: function(){
    this.Level++;
    
    this.TickRate = Math.floor(555 * Math.exp(this.Level / -10));
    this.PiecesRemaining = Math.floor((5000 / this.TickRate));
    
    Page.ScoreBarCur.IsDirty = true;
  },
  
  // check specified rows to see if any can be cleared
  CheckUnits: function(checkRowsRaw){
    var scoreMult = 0,
        pieceScore = 0,
        checkRows = [];
    
    // add the scoreBonus for dropping
    if (this.ScoreBonus > 0){
      pieceScore += this.ScoreBonus;      
    }
    
    // sort the rows
    for(var a = 0; a < 20; a++){
      if (checkRowsRaw.indexOf(a) >= 0){
        checkRows.push(a);
      }
    }
    
    for(var i = 0; i < checkRows.length; i++){
      var hasGap = false,
          checkIndex = checkRows[i]; 
      
      for(var j = 0; j < GM.StaticUnits.length; j++){
        if (GM.StaticUnits[j][checkIndex] === 0){
          hasGap = true;
          break;
        }
      }
      
      
      if (hasGap === false){
        for(var k = 0; k < GM.StaticUnits.length; k++){
          GM.StaticUnits[k].splice(checkIndex,1);
          GM.StaticUnits[k].unshift(0);          
        }
        
        pieceScore += 100 + 200 * scoreMult;
        if (scoreMult > 2){
          pieceScore += 100;
        }        
        scoreMult++;
      }
    }
    
    if(this.DifficultFlag === 1){
      pieceScore = Math.floor(pieceScore * 1.5);
      this.DifficultFlag = 0;
    }
    
    if (pieceScore > 0){      
      this.ScoreCur += pieceScore;
      Page.ScoreBarCur.IsDirty = true;
      
      this.ScoreBonus = 0;
      
      if (scoreMult > 3){
        this.DifficultFlag = 1;
      }    
    }
  },
  
  GameOver: function(){
    Page.Game.IsDirty = Page.ScoreBarCur.IsDirty = true;
    
    if (this.ScoreCur > this.ScoreHigh){
      this.ScoreHigh = this.ScoreCur;
      Page.ScoreBarHigh.IsDirty = true;
      console.log(this.ScoreHigh);
    }    
    
    this.IsAlive = false;
  }
};


//--------------------------------------------------//
//    PIECE OBJECT BUILDER                          //
//--------------------------------------------------//

// PcObj is used to create new piece object instances based on the
// passed in parameters. PcObj is called by predefined templates

GM.PcObj = function(color, rotCount, units){  
  this.x = 5;
  this.y = 0;
  this.color = color;
  this.UO = {};

  // rotate this piece by advancing to next unit obj of linked list
  this.Rotate = function(){
    this.UO = this.UO.nextUO;
  };

  // set up the piece unit object linked list to define rotations
  this.SetUO = function(rotCount, units){    
    var linkedListUO = [];
    
    linkedListUO[0] = { nextUO: 0, arr:[] };
    linkedListUO[0].arr = units;

    for(var i = 0; i < rotCount; i++){
      var nextI = (i + 1 < rotCount) ? i + 1 : 0;
      linkedListUO[i] = { nextUO: 0, arr:[]};
      
      if (i > 0){
        linkedListUO[i-1].nextUO = linkedListUO[i];
      }

      for(var j = 0; j < units.length; j++){
        var unX,
            unY;
        
        if (i === 0){
          unX = units[j].x;
          unY = units[j].y;
        }
        else{
          unX = linkedListUO[i-1].arr[j].y * -1;
          unY = linkedListUO[i-1].arr[j].x;  
        }

        linkedListUO[i].arr[j] = { x: unX, y: unY };        
      }      
    }
    
    linkedListUO[rotCount - 1].nextUO = linkedListUO[0];
    this.UO = linkedListUO[0];
  };
  this.SetUO(rotCount, units);
};


//--------------------------------------------------//
//    PIECE TYPE TEMPLATES                          //
//--------------------------------------------------//

// Templates create a new piece object instance based on
// their color, rotation count, and unit block definitions.

// O - Square piece definition
GM.O = function(){
  return new GM.PcObj('rgb(255,232,51)', 1,                
                      [{x:-1,y: 0},
                       {x: 0,y: 0},
                       {x:-1,y: 1}, 
                       {x: 0,y: 1}]);
};

// I - Line piece definition
GM.I = function(){
  return new GM.PcObj('rgb(51,255,209)', 2,  
                      [{x:-2,y: 0},
                       {x:-1,y: 0},
                       {x: 0,y: 0},
                       {x: 1,y: 0}]);
};

// S - Right facing zigzag piece definition
GM.S = function(){ 
  return new GM.PcObj('rgb(106,255,51)', 2, 
                      [{x: 0,y: 0},
                       {x: 1,y: 0}, 
                       {x:-1,y: 1},
                       {x: 0,y: 1}]);
};

// Z - Left facing zigzag piece definition
GM.Z = function(){ 
  return new GM.PcObj('rgb(255,51,83)', 2,
                      [{x:-1,y: 0},
                       {x: 0,y: 0},
                       {x: 0,y: 1},
                       {x: 1,y: 1}]);
};

// L - Right facing angle piece definition
GM.L = function(){
  return new GM.PcObj('rgb(255,129,51)', 4,
                      [{x:-1,y: 0},
                       {x: 0,y: 0},
                       {x: 1,y: 0},
                       {x:-1,y:-1}]);
};

// J - Left facing angle piece definition
GM.J = function(){
  return new GM.PcObj('rgb(64,100,255)', 4,
                      [{x:-1,y: 0},
                       {x: 0,y: 0},
                       {x: 1,y: 0},
                       {x: 1,y:-1}]);
};

// T - Hat shaped piece definition
GM.T = function(){
  return new GM.PcObj('rgb(160,62,255)', 4,
                      [{x:-1,y: 0},
                       {x: 0,y: 0},
                       {x: 1,y: 0},
                       {x: 0,y:-1}]);
};


//--------------------------------------------------//
//    ACTIVE PIECE CONTROLLER                       //
//--------------------------------------------------//

// Controls the generation, movement, and placement of piece 
// objects. Monitors the current piece and upcoming piece

GM.Pc = {
  
  //-- VARS ---------*/
  
  // current piece, projected Y pos of cur piece  
  Cur:0, ProjY:0,
  
  // upcoming pieces
  Upcoming: [0,0,0],
  
  
  //-- FCNS ---------*/
  
  // push upcoming piece to current & randomize new upcoming piece
  Generate: function(){
    
    // push upcoming piece to current and push down other upcomings
    this.Cur = this.Upcoming[0];
    this.Upcoming[0] = this.Upcoming[1];
    this.Upcoming[1] = this.Upcoming[2];    
           
    // check if the player lost
    if (this.Cur !== 0){
      var spawnCollisions = this.CheckCollisions(0,0,0);
      if (spawnCollisions > 0){
        GM.GameOver();
        this.Freeze();
      }
    }
    
    // if player is alive, generate random upcoming piece
    if (GM.IsAlive !== 0){
      var randInt = Math.floor(Math.random() * 7);

      switch(randInt){
        case 0: this.Upcoming[2] = GM.O(); break;
        case 1: this.Upcoming[2] = GM.I(); break;
        case 2: this.Upcoming[2] = GM.S(); break;
        case 3: this.Upcoming[2] = GM.Z(); break; 
        case 4: this.Upcoming[2] = GM.L(); break;
        case 5: this.Upcoming[2] = GM.J(); break;
        case 6: this.Upcoming[2] = GM.T(); break;
        default: break;      
      }

      // if a current piece was set, inform the GM
      if (this.Cur !== 0){
        GM.PieceSpawned();
        Page.Game.IsDirty = true;
      }
      
      Page.UpcomingA.IsDirty = Page.UpcomingB.IsDirty =
        Page.UpcomingC.IsDirty = true;
    }
  },
  
  // freeze the current piece's position and rotation
  Freeze: function(){
    
    if (GM.IsAlive){
      var affectedRows = [];    
    
      for(var i = 0; i < this.Cur.UO.arr.length; i++){
        var staticX = this.Cur.x + this.Cur.UO.arr[i].x,
            staticY = this.Cur.y + this.Cur.UO.arr[i].y;

        if (staticY >= 0 && staticY <= GM.StaticUnits[0].length){
          GM.StaticUnits[staticX][staticY] = this.Cur.color;
        }

        if (affectedRows.indexOf(staticY) < 0){
          affectedRows.push(staticY);
        }
      }

      GM.CheckUnits(affectedRows);
      this.Generate();
    }
  },
  
  // apply gravity to the current piece, checking for collisions
  DoGravity: function(){    
    if (this.Cur !== 0){
      var collisions = this.CheckCollisions(0,0,1);
      
      if (collisions === 0){
        this.Cur.y++;
      }
      else{
        this.Freeze();
      }
    }
    GM.RefreshTimer();
  },
  
  // attempt to rotate the current piece, returns bool
  TryRotate: function(){
    if (this.Cur !== 0){    
      var collisions = this.CheckCollisions(1,0,0);

      if (collisions === 0){
        this.Cur.Rotate();
        return true;
      }
    }
    return false;
  },
  
  // attempt to move current piece base on given XY, returns bool
  TryMove: function(moveX, moveY){    
    if (this.Cur !== 0){
      var collisions = this.CheckCollisions(0,moveX,moveY);

      if (collisions === 0){
        this.Cur.x += moveX;
        this.Cur.y += moveY;

        if (moveY > 0){
          GM.RefreshTimer();
          GM.ScoreBonus++;
        }
        return true;
      }
    }
    return false;
  },
  
  // attempt to drop the current piece until it collides, returns bool
  TryDrop: function(){ 
    var squaresDropped = 0;
    
    if (this.Cur !== 0){
      while(this.TryMove(0,1) === true && squaresDropped < 22){
        squaresDropped++;
      }      
    }

    if (squaresDropped > 0){
      GM.ScoreBonus += 2 * squaresDropped;
      this.Freeze();
      return true;
    }
    else{
      return false;
    }
  },
  
  // attempt to find (and return) projected drop point of current piece
  TryProject: function(){
    var squaresDropped = 0;
    
    if (this.Cur !== 0){
      while(this.CheckCollisions(0,0,squaresDropped) === 0 &&
            squaresDropped < 22){
        squaresDropped++;
      }
    }
    return squaresDropped - 1;    
  },
      
  // return collision count OR -1 if test piece out of bounds
  CheckCollisions: function(doRot, offsetX, offsetY){
    var unitArr,
        collisionCount = 0;    
    
    if (doRot === 1){
      unitArr = this.Cur.UO.nextUO.arr;
    }
    else{
      unitArr = this.Cur.UO.arr;
    }

    for(var i = 0; i < unitArr.length; i++){
      var testX = this.Cur.x + unitArr[i].x + offsetX,
          testY = this.Cur.y + unitArr[i].y + offsetY,
          limitX = GM.StaticUnits.length,
          limitY = GM.StaticUnits[0].length;
      

      if (testX < 0 || testX >= limitX || testY >= limitY){
        return -1;
      }      
      else if (testY > 0){
        if (GM.StaticUnits[testX][testY] !== 0){
          collisionCount++;
        }
      }
    }    
    return collisionCount;
  }
};


//--------------------------------------------------//
//    EVENT LISTENERS                               //
//--------------------------------------------------//

// Event for keyboard calls the corresponding manipulation functions
// in GM.Pc based on user inputs. If manipulation is successful,
// the page is marked as dirty.

document.addEventListener('keydown', function(evt){
  var key = event.keyCode || event.which;

  if (GM.IsAlive){
    switch(key){

        // Up arrow OR W = rotate     
      case 38: 
      case 87: 
        Page.Game.IsDirty = GM.Pc.TryRotate(); 
        break;

        // Left arrow OR A = move left
      case 37: 
      case 65: 
        Page.Game.IsDirty = GM.Pc.TryMove(-1,0);
        break;

        // Right arrow OR D = move right  
      case 39:
      case 68:
        Page.Game.IsDirty = GM.Pc.TryMove(1,0);
        break;

        // Down arrow OR S = move down  
      case 40:     
      case 83:
        Page.Game.IsDirty = GM.Pc.TryMove(0,1);
        break;

        // Spacebar to drop the current piece
      case 32:
        Page.Game.IsDirty = GM.Pc.TryDrop();
        break;

      default: break;
    }

    //if board was dirtied, cast fresh projection for current piece
    if (Page.Game.IsDirty){
      GM.Pc.ProjY = GM.Pc.TryProject();
    }
  }
  
  // if player not alive, reset the game
  else{
    Init();
  }
  
}, false);


// Window resize event calls Page function to update the canvas 
// size/position, area bounds within the canvas, and the unitSize

window.onresize = function(event) {
  Page.WindowChanged();
};


//--------------------------------------------------//
//    INITIALAZATION AND GAME LOOP                  //
//--------------------------------------------------//

// Called on page load / game reset, Init fcn initializes 
// the Page and GM objects, then starts the main game loop.

function Init () { 
  
  // initialize the page object
  Page.Initialize();
  
  // initialize the GM object
  GM.Initialize();
}
Init();


// Main game loop. Updates GM object to check if tick can be
// performed. Then, if the page is dirty, performs a Draw.

function Loop() {  
  
  // always update Page
  Page.Update();
  
  // only need to update GM if the player is alive
  if (GM.IsAlive){
    GM.Update();
  }
    
  window.requestAnimationFrame(Loop);
}
Loop();


//--------------------------------------------------//
//    HELPER FUNCTIONS                              //
//--------------------------------------------------//

function DrawText(text, color, weight, alignment, size, left, top){  
  Page.ctx.font = weight + ' ' + size + 'px "Jura", sans-serif';
  Page.ctx.textAlign = alignment;
  Page.ctx.fillStyle = color;
  Page.ctx.fillText(text, left ,top);  
}


function ColorWithAlpha(color, alpha){
  var retColor = 'rgba' + color.substring(3,color.length - 1);
  retColor += ',' + alpha + ')';
  return retColor;
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.