/*
 * This code is copyright of the author and may not be used or copied
 * with out written permission.
 
 * Author: Michael Buckley
 * Site: http://codefisher.org/
*/

// ids used in the HTML
var EASYRADIO       = 'easy';
var MEDIUMRADIO     = 'medium';
var HARDRADIO       = 'large';
var CUSTOMRADIO     = 'custom';
var HORIZONTALINPUT = 'horizontal';
var VERTICALINPUT   = 'vertical';
var MINESINPUT      = 'mines';
var OK              = 'ok';
var RESET           = 'reset';
var RESETIMAGE      = 'reset-img';
var NEW             = 'new-game';
var CLOCK           = 'clock';
var MINES           = 'mine-count';
// images used
var SURPRISE        = '/images/games/surprise.png';
var CRY             = '/images/games/cry.png';
var COOL            = '/images/games/cool.png';
var FLAG            = '/images/games/flag.png';
var MINE            = '/images/games/mine.png';
  
function setEventListener(item,event,func,bool) {
  if(item.addEventListener) {
    item.addEventListener(event,func,bool);
  } else {
    item.attachEvent('on'+event,func,bool);
  }
}

function PromptControler(promptNode,onClose) {
  var self = this;
  
  this.width = 10;
  this.height = 10;
  this.mines = 10;
  this.custom = false;
  this.onClose = onClose;

  this.prompt          = promptNode;
  this.easyRadio       = document.getElementById(EASYRADIO);
  this.mediumRadio     = document.getElementById(MEDIUMRADIO);
  this.hardRadio       = document.getElementById(HARDRADIO);
  this.customRadio     = document.getElementById(CUSTOMRADIO);
  this.horizontalInput = document.getElementById(HORIZONTALINPUT);
  this.verticalInput   = document.getElementById(VERTICALINPUT);
  this.minesInput      = document.getElementById(MINESINPUT); 

  // set the width, height and mines curently in use
  this.set = function(width, height, mines) {
    this.width = width;
    this.height = height;
    this.mines = mines;
  }
  // return the width
  this.getWidth = function() {
    return this.width;
  }
  // return the height
  this.getHeight = function() {
    return this.height;
  }
  // return the number of mines
  this.getMines = function() {
    return this.mines;
  }
  // update the check boxes and inputs
  this.update = function() {
    this.horizontalInput.value = this.width;
    this.verticalInput.value = this.height;
    this.minesInput.value = this.mines;
    if(this.custom) {
      this.customRadio.checked = true;
    } else if(this.width == 10 && this.height == 10 && this.mines == 10) {
      this.easyRadio.checked = true;
    } else if(this.width == 16 && this.height == 16 && this.mines == 40) {
      this.mediumRadio.checked = true;
    } else if(this.width == 30 && this.height == 19 && this.mines == 99) {
      this.hardRadio.checked = true;
    } else {
      this.customRadio.checked = true;
    }
    if(this.customRadio.checked) {
      this.horizontalInput.disabled = false;
      this.verticalInput.disabled = false;
      this.minesInput.disabled = false;
    } else {
      this.horizontalInput.disabled = true;
      this.verticalInput.disabled = true;
      this.minesInput.disabled = true;
    }
  }
  // close the window and call the callers handler
  this.close = function() {
    this.prompt.style.display = 'none';
    this.refresh();
    this.onClose(this.width,this.height,this.mines);
  }
  // open the prompt window
  this.open = function() {
   this.refresh();
   this.prompt.style.display = 'block';
  }
  // radio has been changed so update
  this.refresh = function() {
    this.custom = false;
    if(this.easyRadio.checked) {
      this.set(10,10,10);
    } else if(this.mediumRadio.checked) {
      this.set(16,16,40);
    } else if(this.hardRadio.checked) {
      this.set(30,19,99);
    } else {
      this.custom = true;
      this.width = parseInt(this.horizontalInput.value);
      this.height = parseInt(this.verticalInput.value);
      this.mines = parseInt(this.minesInput.value);
    }
    if(this.width < 10) {
      this.width = 10;
    }
    if(this.height < 10) {
      this.height = 10;
    }
    if(this.mines < 10) {
      this.mines = 10;
    }
    if(this.mines > (this.width * this.height * 0.8)) {
      this.mines = parseInt(this.width * this.height * 0.8);
    }
    this.update();     
  }
  this.update();
  setEventListener(document.getElementById(OK),'click',function(event) {self.close(event); }, true);
  setEventListener(this.easyRadio,'click',function(event) {self.refresh(event); }, true);
  setEventListener(this.mediumRadio,'click',function(event) {self.refresh(event); }, true);
  setEventListener(this.hardRadio,'click',function(event) {self.refresh(event); }, true);
  setEventListener(this.customRadio,'click',function(event) {self.refresh(event); }, true);
}

function GameGUI(boardNode,promptNode) {

  var self = this;
  
  this.board = boardNode;
  this.prompt = new PromptControler(promptNode,function() { self.newGame(); });
  this.mines = this.prompt.getMines();
  this.width = this.prompt.getWidth();
  this.height = this.prompt.getHeight();
  this.game = new Game(this.width,this.height,this.mines);
  this.cells;
  this.down = false;
  this.button = document.getElementById(RESET);
  this.buttonImage = document.getElementById(RESETIMAGE);
  this.newButton = document.getElementById(NEW);
  this.minesLeft = document.getElementById(MINES);
  
  setEventListener(this.button,'click',function(event) { self.reset(event); }, true);
  setEventListener(this.newButton,'click',function(event) { self.startNew(event); }, true);
  setInterval(function(event) { self.updateClock(); }, 500);
  
  // how long the game has been going
  this.updateClock = function() {
    if(this.game.finished() || this.game.hasLost())
      return;
    var time = this.game.duration();
    if(time > 1000*60*60) {
      document.getElementById(CLOCK).innerHTML = '0:00';
    } else {
      var seconds = parseInt(time/1000)%60;
      if(seconds < 10) {
        seconds = '0'+seconds;
      }
      var minutes = parseInt(time/60000);
      document.getElementById(CLOCK).innerHTML = minutes+':'+seconds;
    }
  }     
  this.fill = function() {
   // update the look of each cell
   for(var i = 0; i < this.width*this.height; i++) {
    this.cells[i].innerHTML = ''; // quick dirty way to clear it
    // it is a mine that has been pressed
    if(this.game.value(i) == 11) {
      this.cells[i].className = 'explode';
    // game lost, show all mines
    } else if(this.game.value(i) == 10 && this.game.hasLost()) {
      this.cells[i].innerHTML = '<img src="'+MINE+'">';
    // show the flags - flag every mine if game finsihed
    } else if(this.game.flagged(i) || (this.game.finished() && !this.game.cleared(i))) {
      this.cells[i].innerHTML = '<img src="'+FLAG+'">';
    // show the value (mine count) of the cell 
    } else if(this.game.cleared(i)) {
     var value = this.game.value(i);
     switch(value) {
       case 1: this.cells[i].style.color = '#00f';break;
       case 2: this.cells[i].style.color = '#090';break;
       case 3: this.cells[i].style.color = '#e00';break;
       case 4: this.cells[i].style.color = '#009';break;
       case 5: this.cells[i].style.color = '#900';break;
       case 6: this.cells[i].style.color = '#0ee';break;
       case 7: this.cells[i].style.color = '#c09';break;
       case 8: this.cells[i].style.color = '#000';break;
     }
     if(value != 0) {
       this.cells[i].innerHTML = value;
     }
     this.cells[i].className = 'cleared';
    }
   }
   // cry if the game is lost
   if(this.game.hasLost()) {
      this.buttonImage.src = CRY
   }
   this.minesLeft.innerHTML = this.game.minesToClear();
  }
  
  this.drawTable = function() {
    // empty the playing area
    while(this.board.childNodes.length > 0) {
      this.board.removeChild(this.board.firstChild);
    }
    // a number of nodes that we will clone later
    var table = document.createElement('table');
    setEventListener(table,'click',function(event) { self.click(event); return false; },true);
    table.oncontextmenu = function(event) { self.click(event); return false; };
    var tr = document.createElement('tr');
    // **** need to do mouse in/out up/down for every td **** //
    var td = document.createElement('td');
    //create a empty row
    for(var i = 0; i < this.width; i++) {
      var node = td.cloneNode(true);
      tr.appendChild(node);
    }  
    for(var i = 0; i < this.height; i++) {
      var node = tr.cloneNode(true);  
      table.appendChild(node);
    }
    this.board.appendChild(table);
    this.cells = this.board.getElementsByTagName('td');
    // set the index and events for each cell
    for(var i = 0; i < this.height*this.width; i++) {
      this.cells[i].index = i;
      setEventListener(this.cells[i],'mouseup',function(event) { self.mouseup(event); return false; },true);
      setEventListener(this.cells[i],'mousedown',function(event) { self.mousedown(event); return false; },true);
      setEventListener(this.cells[i],'mouseout',function(event) { self.mouseout(event); return false; },true);
      setEventListener(this.cells[i],'mouseover',function(event) { self.mouseover(event); return false; },true);
    }
  }
  this.reset = function() {
    this.game = new Game(this.width,this.height,this.mines);
    this.drawTable();
  }
  this.newGame = function(width,height,mines) {
    this.mines = this.prompt.getMines();
    this.width = this.prompt.getWidth();
    this.height = this.prompt.getHeight();
    this.reset();   
  }
  this.startNew = function() {
    this.prompt.open();
  }
  this.mouseup = function(event) {
    // handel a mouse up
    var element = eventNode(event,'td');
    var index = element.index;
    // restore a cells color
    if(element.className == 'mouse-down') {
      element.className = element.classOld;
    } else {
      // or else do it to thoes near it
      var nodes = this.game.uncleard(index);
      for(var i = 0; i < nodes.length; i++) {
        this.cells[nodes[i]].className = this.cells[nodes[i]].classOld;
      }
    }
    // set the image
    if(this.game.hasLost()) {
      this.buttonImage.src = CRY;
    } else {
      this.buttonImage.src = COOL;
    }
    this.down = false;
  }
  this.mousedown = function(event) {
    // that mouse has been pressed game
    if(this.game.hasLost() || this.game.finished())
      return;
    if(!event.button == 0)
      return;
    var element = eventNode(event,'td');
    var index = element.index;
    // if the cell is not cleared change its color
    if(!this.game.cleared(index)) {
      element.classOld = element.className;
      element.className = 'mouse-down';
    } else {
      // else change thoes near it
      var nodes = this.game.uncleard(index);
      for(var i = 0; i < nodes.length; i++) {
        this.cells[nodes[i]].classOld = this.cells[nodes[i]].className;
        this.cells[nodes[i]].className = 'mouse-down';
      }
    }
    this.buttonImage.src = SURPRISE;
    this.down = true;
  }
  this.mouseout = function(event) {
    // handel the mouse out
    if(this.game.hasLost() || this.game.finished())
      return;
    if(this.down)
      this.mouseup(event);
  }
  this.mouseover = function(event) {
    // handel the mouse over
    if(this.game.hasLost() || this.game.finished())
      return;
    if(this.down)
      this.mousedown(event);
  }
  this.click = function(event) {
    // handel a click on the game board
    // if the game has been finished don't do anything
    if(this.game.hasLost() || this.game.finished())
      return;
    var index = eventNode(event,'td').index;
    // right click -- toggle flag
    if(event.button == 2 && this.game.started()) { 
      if(this.game.started()) {
        this.game.toggleFlag(index);
      }
    } else if(event.button == 0) {
      // if it is the first click start the game
      if(!this.game.started()) {
        this.game.start(index);
      }
      // if the cell or thoes near
      if(!this.game.cleared(index)) {
        this.game.clear(index);
      } else {
        this.game.clearNear(index);
      }
    }
    if(this.game.started()) {
      this.fill();
    }
  }
}

function eventNode(event,type) {
  // returns the node the node the event belongs to
  // or if type is set, that parrent node of that type
  if(event.srcElement) {
    var item = event.srcElement;
  } else {
    var item = event.target;
  }
  if(type) {
   while(item.nodeName.toLowerCase() != type && item.parentNode) {
     item = item.parentNode;
   }
  }
  return item;
}

function Game(width,height,mines) {
  this.width = width;
  this.height = height;
  this.mines = mines;
  this.game = []; // the values of the cells
  this.cells = []; // keeps track of what cells have been cleared
  this.flag = []; // if we have flaged the cell
  this.lost = false;
  this.time = 0;
  this.stop = 0;
  this.remaining;
  this.minesLeft;
  
  this.duration = function() {
  // how long this game has been running for;
   if(this.stop != 0) {
     return this.stop-this.time;
   } else {
     return (new Date()).getTime()-this.time;
   }
  }
  // how many mines have not yet been flaged
  this.minesToClear = function() {
    return this.minesLeft;
  }
  this.toggleFlag = function(index) {
    // toggle if that cell has been flaged
    if(!this.cleared(index)) {
      this.flag[index] = !this.flag[index];
      if(this.flag[index]) {
        this.minesLeft--;
      } else {
        this.minesLeft++;
      }
    }
  }
  this.finished = function() {
    // if that game has been finished
    if(this.remaining == this.mines) {
      this.stop = (new Date()).getTime();
      this.minesLeft = 0;
    }
    return this.remaining == this.mines;
  }
  this.flagged = function(i) {
    // if that cell has been flaged
    return this.flag[i];
  }
  this.cleared = function(i) {
    // if that cell has been cleared
    return this.cells[i];
  }
  this.value = function(i) {
    // the value of a the cell
    return this.game[i];
  }     
  this.started = function() {
    // if that game has started
    return !this.game.length == 0;
  }
  this.hasLost = function() {
    // if the game has been lost yet
    return this.lost;
  }
  this.clearNear = function(index) {
    // clear all cells near index
    var near = this.near_cells(index);
    var mines = 0;
    for(var i = 0; i < near.length; i++) {
      if(this.flag[near[i]]) {
        mines++;
      }
    }
    if(this.game[index] == mines) {
      for(var i = 0; i < near.length; i++) {
        if(!this.flag[near[i]] && !this.cells[near[i]]) {
          this.clear(near[i]);
        }
      }       
    }
  }
  this.uncleard = function(index) {
    // return the cells near index that have not been cleared
    var near = this.near_cells(index);
    var value = [];
    for(var i = 0; i < near.length; i++) {
      if(!this.flag[near[i]] && !this.cells[near[i]]) {
        value[value.length] = near[i];
      }       
    }
    return value;
  }
  this.clear = function(index) {
    // don't clear it in these cases
    if(this.cells[index] || this.flag[index])
      return;
    this.cells[index] = true;
    this.remaining--;
    // if completly empty clear thoes near it
    if(this.game[index] == 0) {
      var near = this.near_cells(index);
      for(var i = 0; i < near.length; i++) {
        this.clear(near[i]);
      }
    }
    if(this.game[index] == 10) {
      this.game[index] = 11;
      this.lost = true;
      this.stop = (new Date()).getTime();
    }
  }     
  this.start = function(empty) {
    this.remaining = this.width*this.height;
    this.minesLeft = this.mines;
    // number of empty cells
    var emptyCells = this.near_cells(empty).length;
    var game = [];
    // fill all the first value with mines
    for(var i = 0; i < this.mines; i++) {
     game[i] = 10;
    }
    var size = ((this.width*this.height)-emptyCells-1);
    // fill the rest as empty
    for(var i = this.mines; i < size; i++) {
     game[i] = 0;
    }
    // random sort
    for(var i = 0; i < game.length; i++) {
     var cell = Math.ceil((size-1)*Math.random());
     this.swap(game,i,cell);
    }
    // put the value in, leaveing the certain ones empty
    var j = 0;
    for(var i = 0; i < this.width*this.height; i++) {
      if(this.near_cell(i,empty)) {
        this.game[i] = 0;
      } else {
        this.game[i] = game[j];
        j++;
      }
      // may as well do it here
      this.cells[i] = false;
      this.flag[i] = false;
    }
    // change it so it also has the number of mines near the cell
    for(var i = 0; i < (this.width*this.height); i++) {
      var near = this.near_cells(i);
      var count = 0;
      for(var j = 0; j < near.length; j++) {
        if(this.game[near[j]] == 10) {
          count++;
        }
      }
      if(count && !this.game[i]) {
        this.game[i] = count;
      }
    }
    this.time = (new Date()).getTime();
  }     
  this.swap = function(game, x, y) {
    // swap to value in an array
    var temp = game[x];
    game[x] = game[y];
    game[y] = temp;
  }     
  this.near_cell = function(cell,pos) {
    // the cell it is near
    var cells = this.near_cells(cell);
    cells[cell.length] = cell;
    // now check
    for(var i in cells) {
      if(pos == cells[i]) {
        return true;
      }
    }
    return false;
  }
  this.near_cells = function(i) {
    // the cells above and below
    var cells = [i-this.width,i+this.width];
    // the cells to the right
    if(i%width != width-1) {
      cells[cells.length] = i+1;
      cells[cells.length] = i+this.width+1;
      cells[cells.length] = i-this.width+1;    
    }
    // cells to the left
    if(i%width != 0) {
      cells[cells.length] = i-1;
      cells[cells.length] = i+this.width-1;
      cells[cells.length] = i-this.width-1; 
    }
    // check they are all on the board
    for(var j in cells) {
      if(cells[j] < 0 || cells[j] >= this.width*this.height) {
        cells[j] = undefined;
      }
    }
    // sort them (puts all the undefined's at the end) and then remove
    cells.sort();
    var j = cells.length-1;
    while(cells[j] == undefined) {
      cells.pop();
      j--;
    }
    return cells;
  }
}
