/******************************************************************************
 * Copyright (c) 2002,2003 Peter 'Merlin' Balsiger and Fredy 'Mythos' Dobler
 * All rights reserved
 *
 * This file is part of Dungeon Master Assistant.
 *
 * Dungeon Master Assistant 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 of the License, or
 * (at your option) any later version.
 *
 * Dungeon Master Assistant 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 Dungeon Master Assistant; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *****************************************************************************/

//------------------------------------------------------------------- header

/**
 * This is a small collection of utility javascript methods.
 * 
 * @file          utility.js
 * 
 * @author        Peter Balsiger
 *
 */

//..........................................................................

//__________________________________________________________________________

// if we get a request with a '#', we make a redirect
if(document.location.hash)
  document.location = escape(document.location.hash.substring(1));

//---------------------------------------------------------------- variables

/** All the functions to call on load. */
var onloadFuncs = [];

/** The dates that are available (lazy init). */
var dates = null;
var MONTHS = [ "January", "February", "March", "April", "May", "June", "July",
               "August", "September", "October", "November", "December" ];
var SHORT_MONTHS = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
                     "Sep", "Oct", "Nov", "Dec" ];
var DAYS = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", 
             "Saturday" ];
var YEAR_START = 1971;
var YEAR_END   = 2010;

/** The last page loaded */
var lastPage = null;

//..........................................................................

//----------------------------------- $ ------------------------------------

/** A simple function to get an element according to its id. If no string is
 * given, the argument is simply returned, allows for subsequent calls to this
 * method without adverse effects.
 *
 */

function $(inElement) 
{ 
  if(typeof inElement == 'string') 
    return document.getElementById(inElement);

  return inElement;
}

//..........................................................................

//------------------------------ hasExtension ------------------------------

/** Determine if the given string, representing a file has an extension.
 *
 * @param  the String given
 *
 * @return true if an extension is present, false if not
 *
 */
function hasExtension(inFile)
{
  return inFile.lastIndexOf(".") >= 0 &&
         inFile.lastIndexOf(".") > inFile.lastIndexOf("/");
}

//..........................................................................
//------------------------------ moveElement -------------------------------

/** Move an element inplace in an array.
 *
 * @param inArray the array to move in
 * @param inFrom  the element to move
 * @param inTo    the end position of the element
 *
 */
function moveElement(inArray, inFrom, inTo)
{
  if(inFrom < 0 || inTo < 0 || inFrom > inArray.length - 1 
     || inTo > inArray.length - 1)
    return;

  var element = inArray[inFrom];

  inArray.splice(inFrom, 1);
  inArray.splice(inTo, 0, element);  
}

//..........................................................................
//-------------------------------- removeHTML ------------------------------

/** Remove everything html from the given String.
 *
 * @param  inText the text to remove from
 *
 * @return the text with all html tags removed
 *
 */
function removeHTML(inText)
{
  if(typeof inText != "string")
    return inText;

  return inText.replace(/<.*?>/g, "");
}  

//..........................................................................
//------------------------------- addOnLoad --------------------------------

/** Add the given function to be executed after page is loaded.
 *
 * @param inFunction the function to add
 *
 */
function addOnLoad(inFunction)
{
  onloadFuncs.push(inFunction);
}

window.onload = function()
{
  for(var i = 0; i < onloadFuncs.length; i++)
    onloadFuncs[i]();
};

//..........................................................................
//---------------------------- compareWithNumber ---------------------------

/** Sort with correctly sorting embedded numbers. 
 *
 * @param a the first value to compare
 * @param b the second value to compare
 *
 * returns > 0 if a > b, < 0 if b > a or 0 if equal
 *
 */
function compareWithNumber(a, b, debug)
{
  // just for speed
  if(a == b)
    return 0;

  // replace number with a unicode character for sorting (does only work up to
  // 16-bit numbers...), using a leading 'a' to make sure numbers come first
  a = a.replace(/\s*(\d+)\s*/g, function(ignore, number) 
    { return "a" + String.fromCharCode(Number(number)); });
  b = b.replace(/\s*(\d+)\s*/g, function(ignore, number)
    { return "a" + String.fromCharCode(Number(number)); });

  for(var i = 0; i < b.length; i++)
  {
    if(i >= a.length)
      return +1;

    var cA = a.charCodeAt(i);
    var cB = b.charCodeAt(i);

    if(cA == cB)
      continue;

    return cA - cB;
  }

  return -1;
}

//..........................................................................
//------------------------------ elapsedTime -------------------------------

function elapsedTime(inStart)
{
  var delay = new Date().getTime() - inStart.getTime();

  return (delay / 1000) + "s";  
}

//..........................................................................
//------------------------------- attributes -------------------------------

/* Determine the number of attributes of an object */
function attributes(inObject)
{
  var count = 0;

  for(var attribute in inObject)
    if(attribute.charAt(0) != '_')
      count++;

  return count;
}

//..........................................................................
//---------------------------------- names ---------------------------------

/* Determine the names available in the given objects. */
function names(inObject)
{
  var result = [];

  for(var i in inObject)
    result.push(i);

  return result;
}

function utilNames(inObject)
{
  var result = [];

  for(var i in inObject)
    result.push(i);

  return result;
}

//..........................................................................
//------------------------------ insertAfter -------------------------------

/** Insert the given node after the one mentioned.
 *
 * @param inNew   the new node to add
 * @param inAfter the node to add after
 *
 */
function insertAfter(inNew, inAfter)
{
  if(inAfter.nextSibling)
    inAfter.parentNode.insertBefore(inNew, inAfter.nextSibling);
  else
    inAfter.parentNode.appendChild(inNew);
}

//..........................................................................
//-------------------------------- contains --------------------------------

/** Check if an array contains a specific value.
 *
 * @param  inValue the value to check for
 *
 * @return true if the value is present, false if not
 * 
 */
function contains(inArray, inValue)
{
  if(!inValue)
    return false;

  for(var i in inArray)
    if(inArray[i] == inValue)
      return true;

  return false;
}

//..........................................................................

//---------------------------------- link ----------------------------------

/**
  * Goto the specified target.
  *
  * @param       inEvent     the event that lead to this invocation
  * @param       inTarget    the URL to go to
  * @param       inFunction  an optional function to be called when the new
  *                          page was loaded

  */
function link(inEvent, inTarget, inFunction)
{
  if(inEvent) 
  {
    inEvent.preventDefault(); 
    inEvent.cancelBubble = true;
  }

  if(inTarget == null)
    inTarget = location.pathname;

  // only link via ajax if its an html file
  var matched = inTarget.match(/\/.*\.(.*?)$/);

  if(matched && matched[1] != "html" && matched[1] != "")
  {
    location.href = inTarget;

    return true;
  }

  // check if we have prevented moving away 
  if(window.onbeforeunload)
    if(!confirm("Do you really want to leave the page?\n\n"
                + window.onbeforeunload()))
    {
      
      return true;
    }
  
  window.onbeforeunload = null;

  lastPage = "#" + unescape(inTarget);
  location.hash = inTarget;

  var busy = new gui.Busy("Please wait while ", ["loading page"]);

  // remove all current actions
  $('actions').innerHTML = '';

  // inform google analytics about this page change
  if(location.hostname != "localhost")
    pageTracker._trackPageview(inTarget); 

  if(inTarget.match(/\?/))
    inTarget += "&body";
  else
    inTarget += "?body";

  ajax(inTarget, null, function(inText) 
  { 
    $('main').innerHTML = inText; 

    busy.done("loading page");
    delete busy;

    // Adding something to the innerHTML will not execute any javascript in it.
    inText.replace(/<script.*?>((\n|.)*?)<\/script>/g, 
                   function(match, group) { eval(group) });

    if(inFunction)
      inFunction();

    // make all editable, if necessary
    gui.makeEditable();
  });

  return false;
}

//..........................................................................
//-------------------------------- paginate --------------------------------

/**
  *
  * Goto the page with the given entry.
  *
  * @param inEvent  the event that lead to this invocation
  * @param inStart The starting element to show.
  *
  */
function paginate(inEvent, inStart)
{
  var path = lastPage;

  if(!path)
    path = document.location.pathname + document.location.search;
  else
    path = path.substring(1);

  path = path.replace(/(\?|\&)?start=\d+/, "");

  if(path.match(/\?$/))
    path += 'start=' + inStart;
  else
    if(path.match(/\?/))
      path += '&start=' + inStart;
    else
      path += '?start=' + inStart;

  link(inEvent, path);
}

//..........................................................................
//------------------------------- fixReload --------------------------------

/**
  *
  * Fix reloading of page with value of cookie.
  *
  */
function fixReload()
{
  var page = location.hash.substring(1);
  
  if(page)
    link(null, page);
}

//..........................................................................
//-------------------------------- fixBack ---------------------------------

/**
  *
  * Fix back and forward buttons
  *
  */
function fixBack()
{
  if(lastPage && lastPage != location.hash)
    link(null, location.hash.substring(1));

  window.setTimeout(fixBack, 1000);
}

//..........................................................................

//---------------------------------- ajax ----------------------------------

/** Send a request to a server.
 *
 * @param  inURL      the url to send to
 * @param  inValues   the key value pairs with the data to send
 * @param  inFunction the function to call on an asynchronous request
 *
 * @return the result from the server (or an empty string for asynchronous
 *         requests)
 *
 */
function ajax(inURL, inValues, inFunction)
{
  var request;

  if(window.XMLHttpRequest)
    request = new XMLHttpRequest();
  else 
    if(window.ActiveXObject)
    {
      try
      {
        request = new ActiveXObject("MSXML2.XMLHTTP");
      }
      catch(e)
      {
        try
        {
          request = new ActiveXObject("Microsoft.XMLHTTP");
        }
        catch(e2)
        {
          alert("Your browser is currently not supported!");
        }
      }
    }

  // build up the data
  var data = "";
  for(var key in inValues)
    data += key + "=" + encodeURIComponent(inValues[key]) + "\n";
  
  request.open("POST", inURL, inFunction !=  null);
  request.setRequestHeader("Content-Type", 
                           "application/x-www-form-urlencoded");
  request.setRequestHeader("Content-Length", data.length);
  request.setRequestHeader("Connection", "close");

  request.send(data);

  if(!inFunction)
    return request.responseText;

  // build up the arguments array 
  var args = [ "<nothing yet>" ];

  for(var i = 3, argument; argument = arguments[i]; i++)
    args.push(argument);

  // we have an asynchronous request
  request.onreadystatechange = function()
    {
      if(request.readyState == 4)
      {        
        args[0] = request.responseText;

        inFunction.apply(null, args);
      }
    }

  return request;
}

//..........................................................................
//---------------------------------- mail ----------------------------------

/** Mail some text to a recipient using a client mail to.
 *
 * @param  inDestination the desination email address
 * @param  inSubject     the subject
 * @param  inText        the mail text
 *
 */
function mail(inDestination, inSubject, inText)
{
  var mailForm = document.createElement("form"); 

  mailForm.action  = "mailto:" + inDestination + "?subject=" + inSubject 
    + "&body=" + encodeURIComponent(inText);
  mailForm.method  = "POST";
  mailForm.enctype = "multipart/form-data"
  mailForm.style.display = "none";

  document.body.appendChild(mailForm);

  mailForm.submit();

  document.body.removeChild(mailForm);
}

//..........................................................................
//------------------------------ storeCookie -------------------------------

/**
  *
  * Store a cookie value.
  *
  * @param       inName  the name of the cookie to store
  * @param       inValue the value of the cookie to store
  *
  */
function storeCookie(inName, inValue)
{
  document.cookie = inName + "=" + escape(inValue);
}

//..........................................................................
//------------------------------- readCookie -------------------------------

/** Read the value of the name cookie.
 *
 * @param  inName the name of the cookie to read
 *
 * @return the value of the cookie as a string, or null if not set
 *
 */
function readCookie(inName) 
{
  if(!document.cookie)
    return null;

  var matches = document.cookie.match("\\b" + inName + "=(.*)(;|$)");

  if(!matches)
    return null;

  return unescape(matches[1]);
}

//..........................................................................
//------------------------------ deleteCookie ------------------------------

/** Delete the named cookie.
 *
 * @param  inName the name of the cookie to delete
 * @param  inPath the path to the cookie
 *
 */
function deleteCookie(inName, inPath)
{
  document.cookie = inName + "=deleted" 
    + (inPath ? ";path=" + inPath : "")
    + ";expires=" + new Date().toGMTString() + ";";
}

//..........................................................................

//-------------------------------- allDates --------------------------------

/**
  * Get all the valid dates as Month Year strings.
  * 
  * @return      an array of strings containing month and year
  *
  */
function allDates()
{
  if(!dates)
  {
    dates = [];

    for(var i = YEAR_START; i <= YEAR_END; i++)
      for(var j = 0; j < MONTHS.length; j++)
        dates.push(i + " " + MONTHS[j] + "::" + MONTHS[j] + " " + i);
  }

  return dates;
}

//..........................................................................
//--------------------------------- extend ---------------------------------

/**
  * Copy all properties from the given source object to the destination object,
  * thus gaining a crude, static kind of inheritance.
  *
  * @param  ioDestination the destination to copy to
  * @param  inSource      the source object to copy from
  * @param  inSuper       the name of the function to allow access to the base
  *                       class (defaults to '_super')
  *
  * @return the destination object again
  * 
  */
function extend(inSubClass, inBaseClass, inSuper) 
{
  if(!inSuper)
    inSuper = "_super";

  inSubClass.prototype[inSuper] = inBaseClass.prototype;

  for(var p in inBaseClass.prototype) 
    if(!inSubClass.prototype[p])
      inSubClass.prototype[p] = inBaseClass.prototype[p];
}

//..........................................................................
//--------------------------------- inside ---------------------------------

/**
 * Determine if the given element is or is residing in a given tag name.
 * 
 * @param       inElement the element to check for
 * @param       inTag     the name of the tag to check for
 * @param       inClass   the name of the class to check for
 * @param       inLevels  the maximal number of levels to check for
 *                        (undefined for unlimited)
 *
 * @return      true if the element is inside the given tag, false if not
 *
 */

function inside(inElement, inTag, inClass, inLevels)
{
  if(inElement.tagName && inElement.tagName == inTag && 
     (!inClass 
      || inElement.className.match(new RegExp("\\b" + inClass + "\\b"))))
    return true;

  if(inLevels)
    if(inLevels > 0 && inElement.parentNode)
      return inside(inElement.parentNode, inTag, inClass, inLevels - 1);
    else
      return false;
  else
    if(inElement.parentNode)
      return inside(inElement.parentNode, inTag, inClass);

  return false;
}

//..........................................................................
//-------------------------------- isInside --------------------------------

/**
 * Check if the given element is inside another one.
 *
 * @param       inElement the element to check for
 * @param       inParent  the lement to check in
 *
 * @return      true if the element is in the other, false if not
 *
 * @undefined   never
 *
 */
function isInside(inElement, inParent)
{
  if(inElement == inParent)
    return true;

  if(!inElement.parentNode )
    return false;

  return isInside(inElement.parentNode, inParent);
}

//..........................................................................
//--------------------------------- number ---------------------------------

/** Convert the given string to a number, returning 0 if not convertable.
 *
 * @param inString the string with the number to convert
 *
 * @return the number represented in the string or 0 if no number found.
 *
 */
function number(inString)
{
  var number = parseInt(inString);

  if(number)
    return number;

  return 0;
}

//..........................................................................
//-------------------------------- niceDate --------------------------------

/**
 * Format the given time in seconds ago from now as a nice date.
 * 
 * @param       inSeconds the date in seconds from now back
 *
 * @return      a string with a nicely formatted date
 *
 */
function niceDate(inSeconds)
{
  if(inSeconds >= 0)
  {
    if(inSeconds == 0)
      return "Now";

    if(inSeconds == 1)
      return "1 second ago";

    if(inSeconds < 60)
      return inSeconds + " seconds ago";
    
    var minutes = Math.round(inSeconds / 60);

    if(minutes == 1)
      return "1 minute ago";

    if(minutes < 60)
      return minutes + " minutes ago";

    var hours =  Math.round(minutes / 60);

    if(hours == 1)
      return "1 hour ago";

    if(hours < 12)
      return hours  + " hours ago";

    var current = new Date();
    var date = new Date(current.getTime() - inSeconds * 1000);

    if(hours < 7 * 24)
    {
      if(current.getDay() == date.getDay())
        return "Today";
      
      if((current.getDay() - 1) % 7 == date.getDay())
        return "Yesterday";

      return "last " + DAYS[date.getDay()];
    }
  }
  else
  {
    if(inSeconds == -1)
      return "in 1 second";

    if(inSeconds > -60)
      return "in " + -inSeconds + " seconds";
    
    var minutes = -Math.round(inSeconds / 60);

    if(minutes == 1)
      return "in 1 minute";

    if(minutes < 60)
      return "in " + minutes + " minutes";

    var hours =  Math.round(minutes / 60);

    if(hours == 1)
      return "in 1 hour";

    if(hours < 12)
      return "in " + hours  + " hours";

    var current = new Date();
    var date = new Date(current.getTime() - inSeconds * 1000);

    if(hours < 7 * 24)
    {
      if((current.getDay() + 1) % 7 == date.getDay())
        return "Tomorrow";
       
      if(current.getDay() == date.getDay())
        return "later Today";

      return "next " + DAYS[date.getDay()];
    }
  }

  return SHORT_MONTHS[date.getMonth()] + " " + date.getDate();
}

//..........................................................................

//------------------------------------------------------- extending existing

//------------------------- String.removeNewlines --------------------------

/**
  * Compact the white space in the string and return it.
  *
  * @return the same string, but with all newlines removed.
  *
  */
String.prototype.removeNewlines = function()
{
  return this.replace(/\s*\n\s*/g, " "); 
};

//..........................................................................
//------------------------------ String.trim -------------------------------

/**
  * Remove leading and trailing white space.
  *
  * @return the same string, but trimmed
  *
  */
String.prototype.trim = function()
{
  return this.replace(/^\s+/, "").replace(/\s+$/, ""); 
};

//..........................................................................
//------------------------------ Array.remove ------------------------------

/**
  * Remove the given value from the array.
  *
  * @param  inValue  the value to remove from the array
  *
  * @return the value removed or null if not found
  *
  */
Array.prototype.remove = function(inValue)
{
  for(var i = 0; i < this.length; i++)
    if(this[i] == inValue)
    {
      var deleted = this[i];

      this.splice(i, 1);

      return deleted;
    }
  
  return null;
};

//..........................................................................
//------------------------------- Array.from -------------------------------

/**
  * Create an array from the given iterable.
  *
  * @param  inIterable the iterable to create the array from
  *
  * @return an array with the values of the iterable
  *
  */
Array.from = function(inIterable) 
{
  if(!inIterable) 
    return [];

  if(inIterable.toArray) 
    return inIterable.toArray();

  var results = [];

  for(var i = 0, length = inIterable.length; i < length; i++)
    results.push(inIterable[i]);

  return results;
};

//..........................................................................
//----------------------------- Array.contains -----------------------------

/**
  * Check if an array contains the given element.
  *
  * @param  inElement the element to check for
  *
  * @return true if the element is there, false if not
  *
  */
Array.prototype.contains = function(inElement) 
{
  if(!inElement) 
    return false;

  for(var i = 0, length = this.length; i < length; i++)
    if(inElement == this[i])
      return true;

  return false;
};

//..........................................................................
//----------------------------- Function.bind ------------------------------

/**
 * Bind the function to the given object and arguments.
 *
 * @param  the object to bind to
 * @param  ... any other arguments
 *
 * @return a function that can be called with the appropriate binding
 * 
 */
Function.prototype.bind = function() 
{
  var method   = this;
  var args     = Array.from(arguments);
  var object   = args.shift();

  return function() 
  {
    return method.apply(object, args.concat(Array.from(arguments)));
  }
};

//..........................................................................
//---------------------------- Node.insertAfter ----------------------------

/**
 * Insert the new node after the given one.
 *
 * @param inNewChild the new node to add
 * @param inRefChild the node to add after (will add at the end if this null)
 *
 * @param the node added
 *
 */
Node.prototype.insertAfter = function(inNewChild, inRefChild)
{
  if(!inRefChild || !inRefChild.nextSibling)
    return this.appendChild(inNewChild);

  return this.insertBefore(inNewChild, inRefChild.nextSibling)
}

//..........................................................................

window.setTimeout(fixBack, 1000);

// remove urchin tracker (google analytics) if on localhost
if(document.location.hostname == 'localhost')
  function pageTracker() { };
