// Swirl text game.
// Copyright (C) 2007 Tom Tromey <tromey@peakpeak.com>

// Swirl is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2, or (at your option)
// any later version.
 
// Swirl is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with Swirl; see the file COPYING.  If not, write to the
// Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
// 02110-1301 USA.

// Linking this library statically or dynamically with other modules is
// making a combined work based on this library.  Thus, the terms and
// conditions of the GNU General Public License cover the whole
// combination.

// As a special exception, the copyright holders of this library give you
// permission to link this library with independent modules to produce an
// executable, regardless of the license terms of these independent
// modules, and to copy and distribute the resulting executable under
// terms of your choice, provided that you also meet, for each linked
// independent module, the terms and conditions of the license of that
// module.  An independent module is a module which is not derived from
// or based on this library.  If you modify this library, you may extend
// this exception to your version of the library, but you are not
// obligated to do so.  If you do not wish to do so, delete this
// exception statement from your version.

//
// Globals.
//

// Size of the answer array.
var answerWidth = 15;
var answerHeight = 10;
var maxAnswerLen = 7;

// The time remaining in the game, in seconds.
var timeRemaining;

// True when the game is in play.
var gameInPlay;

// The word array.  Usually we pass this as a parameter, but we can't
// always.
var theWords;

// The answer word.
var theAnswerWord;

// Maps a word to either its index in theWords, or to -1, meaning it
// was already found.
var foundWords;

// Maps Nx and Ny, where N is an index into theWords, to the X and Y
// coordinates of a word in the AnswerWords table.
var answerCoordinates;

// Count of words user got.
var foundCount;

// The contents of the Construction and Letters tables.
var constructionContents;
var lettersContents;

// The last word the user wrote.
var lastWord;

// The score.
var score;

// True if user got the longest word.
var gotLongest;

//
// Utility.
//

function getCell (name)
{
  return document.getElementById (name);
}

function rand (n)
{
  return Math.floor (n * Math.random ());
}

function permute (len)
{
  var permutation = new Array ();
  var i;
  // Knuth shuffle.
  for (i = 0; i < len; ++i)
    {
      permutation[i] = i;
    }
  for (i = 0; i < len - 1; ++i)
    {
      var r = i + 1 + rand (len - i - 1);
      var t = permutation[r];
      permutation[r] = permutation[i];
      permutation[i] = t;
    }
  return permutation;
}

function swirlAlert (text)
{
  var cell = getCell ("SwirlAlert");
  cell.innerHTML = text;
}

function swirlSound (resourceName)
{
  // FIXME: implement.
}

//
// Word handling.  For now we do it all here.
// FIXME: download a word at a time.
//

// All the words.
var swirlWords;

// We make a permutation and then simply walk through it linearly.
// This way we don't repeat in a given game for maximum amount of
// time, and also we don't play in a deterministic order.
var wordIndex;
var wordPermutation;

function computeWordIndex ()
{
  wordPermutation = permute (swirlWords.length);
  wordIndex = 0;
}

function getWordArray ()
{
  if (wordIndex == wordPermutation.length)
    wordIndex = 0;
  var result = swirlWords[wordPermutation[wordIndex]];
  ++wordIndex;
  return result;
}

//
// Timer handling.
//

function updateGameTimer()
{
  if (! gameInPlay)
    {
      return;
    }

  --timeRemaining;

  if (timeRemaining < 0)
    {
      if (gotLongest)
	endGame ("On to the next round!");
      else
	endGame ("You lose!");
      showWordsOnLose ();
      return;
    }

  var element = getCell ("Timer");
  var seconds = timeRemaining % 60;
  var minutes = Math.floor (timeRemaining / 60);
  var text = "Time Remaining: " + minutes + ":";
  if (seconds < 10)
    text += "0";
  text += seconds;
  element.innerHTML = text;

  // Update again shortly.
  setTimeout ("updateGameTimer ()", 1000);
}

function startGameTimer()
{
  // Start with two minutes.  But since we call
  // updateGameTimer, we need one more.
  timeRemaining = 121;
  updateGameTimer();
}

//
// Game play.
//

function updateScore ()
{
  var cell = getCell ("Score");
  cell.innerHTML = "Score: " + score;
}

// Note that the name is important here as we construct a function
// call when making the tables.  Despite the name this only moves a
// single letter.
function moveLetters (index)
{
  var j;
  for (j = 0; constructionContents[j] != " "; ++j)
    {
      // Nothing.
    }

  constructionContents[j] = lettersContents[index];
  lettersContents[index] = " ";

  var cell = getCell ("Letters" + index);
  cell.innerHTML = "&nbsp;&nbsp;";
  cell = getCell ("Construction" + j);
  cell.innerHTML = constructionContents[j];
}

// Note that the name is important here as we construct a function
// call when making the tables.
function moveConstruction (index)
{
  // Arguably it should go back where it came.  But, it probably
  // doesn't matter.
  var j;
  for (j = 0; lettersContents[j] != " "; ++j)
    {
      // Nothing.
    }

  lettersContents[j] = constructionContents[index];
  constructionContents[index] = " ";

  var cell = getCell ("Construction" + index);
  cell.innerHTML = "&nbsp;&nbsp;";
  cell = getCell ("Letters" + j);
  cell.innerHTML = lettersContents[j];
}

function handleOrdinaryKey (key)
{
  var i;
  for (i = 0; i < lettersContents.length; ++i)
    {
      if (lettersContents[i] == key)
	{
	  moveLetters (i);
	  swirlSound ("key");
	  return;
	}
    }

  swirlSound ("oops");
}

function handleBackSpace ()
{
  var i;
  for (i = constructionContents.length - 1; i >= 0; --i)
    {
      if (constructionContents[i] != " ")
	{
	  moveConstruction (i);
	  swirlSound ("key");
	  return;
	}
    }

  swirlSound ("oops");
}

function showWord (text, index, color)
{
  var i;
  var x = answerCoordinates[index + "x"];
  var y = answerCoordinates[index + "y"];
  for (i = 0; i < text.length; ++i)
    {
      var cell = getCell ("AnswerWordCell" + x + "_" + y);
      ++x;
      cell.innerHTML = text.substr (i, 1);
      if (color) {
	cell.style.backgroundColor = color;
      }
    }
}

function clearConstruction ()
{
  var i;
  var j = 0;
  for (i = 0;
       i < constructionContents.length && constructionContents[i] != " ";
       ++i)
    {
      // We know this will always stop.
      while (lettersContents[j] != " ")
	++j;

      lettersContents[j] = constructionContents[i];
      constructionContents[i] = " ";

      var cell = getCell ("Construction" + i);
      cell.innerHTML = "&nbsp;&nbsp;";
      cell = getCell ("Letters" + j);
      cell.innerHTML = lettersContents[j];
    }
}

function handleEnter ()
{
  var attempt = "";
  var i;
  for (i = 0;
       i < constructionContents.length && constructionContents[i] != " ";
       ++i)
    {
      attempt += constructionContents[i];
    }

  if (! foundWords.hasOwnProperty (attempt))
    {
      swirlAlert ("Not a word!");
      swirlSound ("oops");
    }
  else
    {
      var index = foundWords[attempt];
      if (index == -1)
	{
	  swirlAlert ("You already got that one!");
	  swirlSound ("oops");
	}
      else
	{
	  // Got a word.
	  foundWords[attempt] = -1;
	  ++foundCount;
	  showWord (attempt, index, false);

	  score += 30 * attempt.length;
	  updateScore ();

	  if (foundCount == theWords.length)
	    {
	      // You get a bonus of 10 * #answers * (1 + seconds left).
	      var bonus = 10 * theWords.length * (1 + timeRemaining);
	      score += bonus;
	      updateScore ();

	      endGame ("You win -- " + bonus + " bonus!!");
	      swirlSound ("youWin");
	    }
	  else
	    {
	      if (attempt.length == theAnswerWord.length)
		{
		  gotLongest = true;
		  swirlAlert ("You will advance to the next screen!");
		}
	      swirlSound ("gotOne");
	    }
	}
    }

  clearConstruction ();

  lastWord = attempt;
}

function showLastWord ()
{
  if (lastWord)
    {
      clearConstruction ();
      var i;
      for (i = 0; i < lastWord.length; ++i)
	{
	  // A hack...
	  handleOrdinaryKey (lastWord.substr (i, 1));
	}
    }
}

function swirlText ()
{
  var i;
  var nv = new Array ();
  var j = 0;
  for (i = 0; i < lettersContents.length; ++i)
    {
      if (lettersContents[i] != " ")
	nv[j++] = lettersContents[i];
    }
  var perm = permute (nv.length);
  j = 0;
  for (i = 0; i < lettersContents.length; ++i)
    {
      if (lettersContents[i] != " ")
	{
	  lettersContents[i] = nv[perm[j++]];
	  var cell = getCell ("Letters" + i);
	  cell.innerHTML = lettersContents[i];
	}
    }
}

function showWordsOnLose ()
{
  for (var i = 0; i < theWords.length; ++i)
    {
      if (foundWords[theWords[i]] != -1)
	showWord (theWords[i], i, "#e4eb3b");
    }
}

function handleKeyPress (evt)
{
  if (! gameInPlay)
    {
      // Nothing to do.
      return;
    }

  // Clear the display.
  swirlAlert ("&nbsp;");

  var which;
  // IE sets window.event.
  if (window.event)
    which = window.event.keyCode;
  else
    which = evt.which;
  var key = String.fromCharCode (which).toLowerCase ();
  if (key >= "a" && key <= "z")
    handleOrdinaryKey (key);
  else if (which == 8)
    handleBackSpace ();
  else if (which == 10 || which == 13)
    {
      if (constructionContents[0] == " ")
	showLastWord();
      else
	handleEnter ();
    }
  else if (which == 32)
    swirlText ();
  else if (which == 38)
    {
      // Up arrow.
      showLastWord ();
    }
  else
    {
      // Ignore the rest.
      return;
    }

  evt.preventDefault ();
}

//
// Initialization.
//

// Initialize one of the "word" tables.
function initWord (cell)
{
  var i;
  var row = getCell (cell);
  for (i = 0; i < maxAnswerLen; ++i)
    {
      row.insertCell(i).innerHTML = ("<div align=center class=cellinuse "
				     + "id=" + cell + i
				     + " onClick=\"move"
				     + cell + "(" + i + ");\">&nbsp;</div>");
    }
}

// Initialize the table of answers.
function initAnswerWords ()
{
  // We just make a fixed size table. Then we make sure that no answer
  // can possibly overflow this table... gross, but workable, and
  // doesn't make us have to handle some complicated automatic layout.
  var x, y;
  var result = "<table>"
  for (y = 0; y < answerHeight; ++y)
    {
      result += "<tr>";
      for (x = 0; x < answerWidth; ++x)
	{
	  result += ("<td><div class=cellnotinuse align=center "
		     + "id=AnswerWordCell" + x + "_" + y
		     + ">&nbsp;</div></td>");
	}
      result += "</tr>";
    }
  result += "</table>";
  var cell = getCell ("AnswerWords");
  cell.innerHTML = result;
}

function clearAnswerBackground ()
{
  var x, y;
  var result = "<table>"
  for (y = 0; y < answerHeight; ++y)
    {
      for (x = 0; x < answerWidth; ++x)
	{
	  var cell = getCell ("AnswerWordCell" + x + "_" + y);
	  cell.className = "cellnotinuse";
	  cell.innerHTML = "&nbsp;";
	  cell.style.backgroundColor = "#b4f464";
	}
    }
}

function reformatWordTable (cellName, wordArray)
{
  var len = theAnswerWord.length;
  for (i = 0; i < len; ++i)
    {
      var cell = getCell (cellName + i);
      cell.innerHTML = "&nbsp;";
      cell.className = "cellinuse";
    }
  for (; i < maxAnswerLen; ++i)
    {
      var cell = getCell (cellName + i);
      cell.innerHTML = "&nbsp;";
      cell.className = "bigcellnotinuse";
    }
}

// Set up the AnswerWords array for use with a particular answer set.
function reformatAnswerWords (wordArray)
{
  // The length of an array only includes numeric indices.  Silly, but
  // hey -- we don't have to subtract one to avoid the "answer"
  // element.
  var len = wordArray.length;

  // Also initialize globals.
  foundWords = new Array ();
  answerCoordinates = new Array ();
  foundCount = 0;

  var x = 0, y = 0;
  // Make computation come out ok the first time through.
  var prevWordLen = -1;
  for (i = 0; i < len; ++i)
    {
      var word = wordArray[i];
      var wordLen = word.length;

      foundWords[word] = i;

      // Move over by the word length plus a space.
      x += prevWordLen + 1;
      if (x + wordLen >= answerWidth)
	{
	  x = 0;
	  ++y;
	  if (y >= answerHeight)
	    {
	      // Oops.
	    }
	}

      var iter;
      for (iter = 0; iter < wordLen; ++iter)
	{
	  var cell = getCell ("AnswerWordCell" + (iter + x) + "_" + y);
	  cell.className = "cellinuse";
	}
      answerCoordinates[i + "x"] = x;
      answerCoordinates[i + "y"] = y;

      prevWordLen = wordLen;
    }
}

// Initialize the construction and letters arrays.
function initArrays (answer)
{
  constructionContents = new Array ();
  lettersContents = new Array ();

  var permutation = permute (answer.length);
  var i;
  for (i = 0; i < permutation.length; ++i)
    {
      constructionContents[i] = " ";
      lettersContents[i] = answer.substr (permutation[i], 1);

      var cell = getCell ("Letters" + i);
      cell.innerHTML = lettersContents[i];
    }
}

function initEvents ()
{
  document.onkeypress = handleKeyPress;
}

function tweakButtons (playing)
{
  var cell = getCell ("StartButton");
  cell.disabled = playing;
  cell.onfocus = function ()
    {
      cell.blur ();
    };

  cell = getCell ("GuessButton");
  cell.disabled = ! playing;
  cell.onfocus = function ()
    {
      cell.blur ();
    };
  cell = getCell ("SwirlButton");
  cell.disabled = ! playing;
  cell.onfocus = function ()
    {
      cell.blur ();
    };
  cell = getCell ("BackUpButton");
  cell.disabled = ! playing;
  cell.onfocus = function ()
    {
      cell.blur ();
    };
  cell = getCell ("LastWordButton");
  cell.disabled = ! playing;
  cell.onfocus = function ()
    {
      cell.blur ();
    };
}

//
// Entry point.
//

function setupSwirl ()
{
  gameInPlay = false;

  initEvents ();

  // We want to make sure a few of the UI elements look ok-ish.
  // Anything will do here, we reset it when making a new game.
  theWords = new Array();
  theAnswerWord = "trouble";

  initWord ("Construction");
  initWord ("Letters");

  initAnswerWords ();

  tweakButtons (false);
}

function startNextLevel ()
{
  gotLongest = false;

  swirlAlert ("");

  theWords = getWordArray ();
  var i, max = 0;
  for (i = 1; i < theWords.length; ++i)
    {
      if (theWords[i].length > theWords[max].length)
	max = i;
    }
  theAnswerWord = theWords[max];

  reformatWordTable ("Construction", theWords);
  reformatWordTable ("Letters", theWords);
  clearAnswerBackground ();
  reformatAnswerWords (theWords);

  initArrays (theAnswerWord);

  gameInPlay = true;
  tweakButtons (true);
  startGameTimer ();
}

function startNewGame ()
{
  score = 0;
  updateScore ();
  computeWordIndex ();
  startNextLevel ();
}

function endGame (text)
{
  swirlAlert (text);
  gameInPlay = false;
  tweakButtons (false);

  var cell = getCell ("StartButton");
  if (gotLongest)
    {
      cell.value = "Start Next Level";
      cell.onclick = function ()
	{
	  startNextLevel ();
	};
    }
  else
    {
      cell.value = "Start Game";
      cell.onclick = function ()
	{
	  startNewGame ();
	};
    }
}

// Local Variables:
// javascript-indent-level: 2
// End:
