/**
 * (c) 2013 Ben Bucksch
 */

/**
 * @param test {Boolean}
 * @param errorMsg {String}
 */
function assert(test, errorMsg) {
  errorMsg = errorMsg || "assertion failed";
  if ( !test) {
    throw new Exception(errorMsg);
  }
}

function ddebug(msg) {
  if (console) {
    console.debug(msg);
  }
}


/**
 * Create a subtype.
 */
function extend(child, supertype)
{
  var properties = Object.create(null);
  Object.getOwnPropertyNames(child.prototype).forEach(function(key) {
    properties[key] = Object.getOwnPropertyDescriptor(child.prototype, key);
  });
  child.prototype = Object.create(supertype.prototype, properties);
}

/**
 * Creates a callback that calls a function with the given parameters.
 *
 * E.g. addEventListener("click", makeCallbackParams(setTitle, title), true);
 * will call: setTitle(title);
 * makeCallbackParams(func, foo, bar, baz)
 * will call: func(foo, bar, baz);
 *
 * @param func {Function}   to be called
 * @param arg, arg, arg {...}   to be passed to |func|
 * @returns {Function}
 */
function cbParams(func) {
  var args = Array.prototype.slice.call(arguments, 1); // remove first arg |func|, keep rest
  return function() {
    func.apply(null, args);
  };
}

/**
 * Creates a callback that calls a method on a given object
 *
 * E.g. addEventListener("click", makeCallbackOnObj(e, setTitle, title), true);
 * will call: e.setTitle(title);
 * makeCallbackParams(obj, func, foo, bar, baz)
 * will call: obj.func(foo, bar, baz);
 *
 * @param obj {Object}   on which instance to call |func|. obj.func() must exist.
 * @param func {Function}   to be called
 * @param arg, arg, arg {...}   to be passed to |func|
 * @returns {Function}
 */
function cbObj(obj, method) {
  var args = Array.prototype.slice.call(arguments, 2); // remove args |obj| and |func|, keep rest
  return function() {
    method.apply(obj, args);
  };
}


/**
 * Removes |element| from |array|.
 * @param array {Array} to be modified. Will be modified in-place.
 * @param element {Object} If |array| has a member that equals |element|,
 *    the array member will be removed.
 * @param all {boolean}
 *     if true: remove all occurences of |element| in |array.
 *     if false: remove only the first hit
 * @returns {Integer} number of hits removed (0, 1 or more)
 */
function arrayRemove(array, element, all)
{
  var found = 0;
  var pos = 0;
  while ((pos = array.indexOf(element, pos)) != -1)
  {
    array.splice(pos, 1);
    found++
    if ( ! all)
      return found;
  }
  return found;
}

/**
 * Check whether |element| is in |array|
 * @param array {Array}
 * @param element {Object}
 * @returns {boolean} true, if |array| has a member that equals |element|
 */
function arrayContains(array, element)
{
  return array.indexOf(element) != -1;
}


function shortenText(text, maxLen) {
  return text.length <= maxLen ? text : text.substr(0, maxLen) + "…";
}

/**
 * Adds leading characters until a certain length is filled.
 * This can be used to add leading 0s, so that a number always has
 * at least n digits.
 * @param str {String or Integer}  the number, e.g. "9"
 * @param pad {String}   what you want to put in front, e.g "0"
 * @param minlen {Integer}   minimum length
 *     determines the number of 0s in front, e.g. 3
 * @returns e.g. "009"
 */
function padLeading(str, pad, minlen) {
    str = str + ""; // convert to String
    while (str.length < minlen) {
      str = pad + str;
    }
    return str;
}

function trim(str) {
  return str.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
}

/* jQuery plugin to handle the enter key in <input> fields */
$.fn.returnKey = function(fn) {
  return this.each(function() {
    $(this).bind("enterPressed", fn);
    $(this).keyup(function(e) {
      if(e.keyCode == 13) {
        $(this).trigger("enterPressed");
      }
    });
  });
};

/**
 * Translate string
 * @param id {String}   key in "appui.properties" file
 * @param args {String or Array of String}   replacement parameters
 */
function tr(id, args) {
  if (typeof(args) == "string") {
    args = [ args ];
  }
  if ( !tr._sb) {
    tr._sb = new StringBundle("appui.properties")
  }
  return tr._sb.get(id, args);
}

function dataURL(relPath) {
  assert(relPath && typeof(relPath) == "string");
  // gCurrentLang from app.js
  return "data/" + gCurrentLang + "/" + relPath;
}

function mediaURL(relPath) {
  return "data/media/" + relPath;
}

/**
 * @param url {String}   http[s]:// or file:///
 * @dataType {String-enum}  Expected type of file contents
 *    "text", "json", "xml" or "html"
 * @param successCallback {Function(result)}
 *    result {String or Object or DOMDocument}
 * @param errorCallback {Function(e {Exception or Error})}
 */
function loadURL(paramsOrURL, dataType, successCallback, errorCallback) {
  var params = {};
  var url;
  if (typeof(paramsOrURL) == "string") {
    url = paramsOrURL;
  } else {
    params = paramsOrURL;
    var url = params.url;
  }
  assert(typeof(url) == "string" && url, "need URL");
  for (var name in params.urlArgs) {
    url += (url.indexOf("?") == -1 ? "?" : "&") +
            name + "=" + encodeURIComponent(params.urlArgs[name]);
  }
  dataType = dataType || params.dataType;
  assert(typeof(dataType) == "string" && dataType, "need type");

  var mimetype = null;
  //if (url.substr(0, 7) == "file://") {
    if (dataType == "text") {
      mimetype = "text/plain";
    } else if (dataType == "xml") {
      mimetype = "text/xml";
    } else if (dataType == "html") {
      mimetype = "text/html";
    } else if (dataType == "json") {
      //mimetype = "text/plain";
      mimetype = "text/javascript";
    } else {
      assert(false, "unknown dataType");
    }
    mimetype += "; charset=UTF-8";
  //}

  // <copied from="FetchHTTP">
  console.log("trying to open " + url);
  var callStack = Error().stack;

  function statusToException(req) {
    try {
      var errorCode = req.status;
      var errorStr = req.statusText;
      if (errorCode == 0 && errorStr == "" || errorStr == "Failure") {
        errorCode = -2;
        var sb = new StringBundle("appui.properties");
        errorStr = sb.get("cannot_contact_server.error");
      }
      var ex = new ServerException(errorStr, errorCode, url);
      ex.stack = callStack;
      return ex;
    } catch (e) {
      return e;
    }
  }

  function response(req) {
    try {
      var result = null;
      var ex = null;

      // HTTP level success
      var isHTTP = window.location.href.substr(0, 4) == "http";
      if ( !isHTTP ||
          (req.status >= 200 && req.status < 300)) {
        try {
          result = req.responseText;
        } catch (e) {
          var sb = new StringBundle("appui.properties");
          var errorStr = sb.get("bad_response_content.error") + ": " + e;
          ex = new ServerException(errorStr, -4, url);
          ex.stack = callStack;
        }
      } else {
        ex = statusToException(req);
      }

      // Callbacks
      if ( !ex) {
        return result;
      } else {
        errorCallback(ex);
      }
    } catch (e) { errorCallback(e); }
  }
  // </copied>

  var req = new XMLHttpRequest();
  req.onerror = function() {
    errorCallback(statusToException(req));
  };
  req.onload = function() {
    try {
      var data = response(req);
      if ( !data) { // errorCallback called
        return;
      }
      if (dataType == "xml") {
        data = req.responseXML;
      } else if (dataType == "html") {
        data = new DOMParser().parseFromString(data, "text/html");
      } else if (dataType == "json") {
        if (data.substr(0, 5) == "load(") {
          data = data.substr(5, data.length - 6);
        }
        data = JSON.parse(data);
      }
      successCallback(data);
    } catch (e) { errorCallback(e); }
  };
  //req.overrideMimeType("text/plain; charset=UTF-8");
  try {
    req.open("GET", url, true); // async
    req.send();
  } catch (e) { // send() throws (!) when file:// URL and file not found
    errorCallback(e);
  }
}


/*
 * Gives a download URL for file contents in a JS string
 * This allows to download a file that you have constructed in a JS variable.
 *
 * @param contents {String}   the file contents
 * @param mimetype {String}   the file type
 * @returns {String}   download URL
 */
function downloadFromVariable(contents, mimetype) {
  assert(contents && typeof(contents) == "string", "need file contents");
  assert(mimetype && typeof(mimetype) == "string", "need mimetype");
  assert(mimetype.indexOf("/") > 0 && mimetype.indexOf(" ") == -1, "mimetype is malformed");
  /* using data: URL. Downside: 1) can't represent many UTF8 chars 2) "" in JSON goes wrong
  var replace = {
    "”" : '"',
    "“" : '"',
    "ʼ" : "'",
    "′" : "'",
    "’" : "'",
    "ʽ" : "'",
    "‘" : "'",
    "—" : "--",
    "–" : "-",
    "ר" : "(greek r)",
    "ד" : "(greek d)",
    "נ" : "(greek nun)",
    "כ" : "(greek kaph)",
    "1⁄2" : "1/2",
  };
  for (var badChar in replace) {
    contents = contents.replace(new RegExp(badChar, "g"), replace[badChar]);
  }
  var rest = contents;
  while (rest.length > 0) {
    var snipplet = rest.substr(0, 40);
    rest = rest.substr(40);
    try {
      window.btoa(snipplet)
    } catch (e) {
      alert(e + "\n" + snipplet);
      return;
    }
  }
  var url = "data:" + mimetype + ";charset=utf-8;base64," + window.btoa(contents);
  window.location = url;
  */

  /* again as data: URL, but created using FileReader() */
  var file = new Blob([ contents ], { type : mimetype });
  var reader = new FileReader();
  reader.onload = function(e) { window.location = e.target.result; };
  reader.readAsDataURL(file);

  /* using window.open(). Downside: 1) user must copy&paste to text editor 2) popup blocker
  var win = window.open("about:blank");
  if ( !win) {
    alert("Please allow popup windows, then try again");
    return;
  }
  win.document.write("<pre>");
  win.document.write(contents);
  win.document.write("\n</pre>");
  win.document.close();
  */
}



/**
 * Parses a URL query string into an object.
 *
 * @param queryString {String} query ("?foo=bar&baz=3") part of the URL,
 *     with or without the leading question mark
 * @returns {Object} JS map { name1 : "value", name2: "othervalue" }
 */
function parseURLQueryString(queryString)
{
  var queryParams = {};
  if (queryString.charAt(0) == "?")
    queryString = queryString.substr(1); // remove leading "?", if it exists
  var queries = queryString.split("&");
  for (var i = 0; i < queries.length; i++) {
    try {
      if ( !queries[i]) {
        continue;
      }
      var querySplit = queries[i].split("=");
      var value = querySplit[1].replace(/\+/g, " "); // "+" is space, before decoding
      queryParams[querySplit[0]] = decodeURIComponent(value);
    } catch (e) {
      // Errors parsing the query string are not fatal, we should just continue
      errorNonCritical(e);
    }
  }
  return queryParams;
}

function runAsync(func, errorCallback) {
  assert(typeof(func) == "function");
  assert(typeof(errorCallback) == "function");
  setTimeout(function() {
    try {
      func();
    } catch (e) { errorCallback(e); }
  }, 0);
}

function runLater(millisec, func, errorCallback) {
  assert(typeof(func) == "function");
  assert(typeof(errorCallback) == "function");
  setTimeout(function() {
    try {
      func();
    } catch (e) { errorCallback(e); }
  }, millisec);
}

function Exception(msg)
{
  this._message = msg;

  // get stack
  try {
    not.found.here += 1; // force a native exception ...
  } catch (e) {
    this.stack = e.stack; // ... to get the current stack
  }
  //debug("ERROR (exception): " + msg + "\nStack:\n" + this.stack);
}
Exception.prototype =
{
  get message()
  {
    return this._message;
  },
  set message(msg)
  {
    this._message = msg;
  },
  toString : function()
  {
    return this._message;
  }
}

function NotReached(msg)
{
  Exception.call(this, msg);
}
extend(NotReached, Exception);


function UserCancelledException(msg)
{
  // The user knows they cancelled so I don't see a need
  // for a message to that effect.
  if (!msg)
    msg = "";
  Exception.call(this, msg);
}
UserCancelledException.prototype =
{
}
extend(UserCancelledException, Exception);

function ServerException(serverMsg, code, uri)
{
  Exception.call(this, serverMsg);
  this.rootErrorMsg = serverMsg;
  this.code = code;
  this.uri = uri;
}
ServerException.prototype =
{
}
extend(ServerException, Exception);


/**
 * Return the contents of an object as multi-line string, for debugging.
 * @param obj {Object} What you want to show
 * @param name {String} What this object is. Used as prefix in output.
 * @param maxDepth {Integer} How many levels of properties to access.
 *    1 = just the properties directly on |obj|
 * @param curDepth {Integer} internal, ignore
 */
function dumpObject(obj, name, maxDepth, curDepth)
{
  if (curDepth == undefined)
    curDepth = 1;
  if (maxDepth != undefined && curDepth > maxDepth)
    return "";

  var result = "";
  var i = 0;
  for (var prop in obj)
  {
    i++;
    if (typeof(obj[prop]) == "xml")
    {
      result += name + "." + prop + "=[object]" + "\n";
      result += dumpObject(obj[prop], name + "." + prop, maxDepth, curDepth+1);
    }
    else if (typeof(obj[prop]) == "object")
    {
      if (obj[prop] && typeof(obj[prop].length) != "undefined")
        result += name + "." + prop + "=[probably array, length " + obj[prop].length + "]" + "\n";
      else
        result += name + "." + prop + "=[object]" + "\n";
      result += dumpObject(obj[prop], name + "." + prop, maxDepth, curDepth+1);
    }
    else if (typeof(obj[prop]) == "function")
      result += name + "." + prop + "=[function]" + "\n";
    else
      result += name + "." + prop + "=" + obj[prop] + "\n";
  }
  if ( ! i)
    result += name + " is empty\n";
  return result;
}
