/**
 * A push button that executes a function or goes to another page.
 *
 * Creates:
 * <span class="button">Click me</span>
 *
 * @param $container {jquery Element}
 * @param classes {String}   will be added to class=""
 * @param title {String}   text on the button
 * @param icon {String URL relative path}   icon image for the button.
 *     E.g. "static/foo.png"
 * @param clickCallback {Function}   Called when the user clicks on the button
 * @param errorCallback {Function(e)}   Called when the click function fails
 * @returns the button {JQuery element}
 */
function uiButton(p) {
  assert(p.$container, "div: need element");
  assert(typeof(p.title) == "string" && p.title, "need title");
  assert( !p.icon || typeof(p.icon) == "string", "icon: need URL string");
  assert( !p.classes || typeof(p.classes) == "string", "classes: need string");
  assert(typeof(p.clickCallback) == "function", "clickCallback: need function");
  assert(typeof(p.errorCallback) == "function", "errorCallback: need function");

  var $button = $("<span class='button'/>").appendTo(p.$container);
  if (p.icon) {
    $("<img class='icon' width='16' height='16' />")
        .attr("src", p.icon)
        .appendTo($button);
  }
  if (p.classes) {
    p.classes.split(" ").forEach(function(cl) {
      $button.addClass(cl);
    });
  }
  $("<span class='text'/>").text(p.title).appendTo($button);
  $button.click(function() {
    try {
      p.clickCallback();
    } catch (e) { p.errorCallback(e); }
  });
  return $button;
}

/**
 * Creates a push button that links to a |Detail| page.
 *
 * @param $container {jquery Element}
 * @param obj {Detail}   The object that this button stands for
 * @param navFunc {Function}   Called when the user
 *     clicks on the button and the detail should be shown.
 * @param classes {String}   will be added to class=""
 * @returns the button {JQuery element}
 */
function uiObjButton(p) {
  assert(p.$container, "div: need element");
  assert(p.obj instanceof Detail, "obj: need Detail");
  assert(typeof(p.navFunc) == "function", "navFunc: need function");

  var icon;
  if (p.obj instanceof Person) {
    if (p.obj.male) {
      icon = "static/person.png";
    } else {
      icon = "static/woman.png";
    }
  } else if (p.obj instanceof Event) {
    icon = "static/event.png";
  } else if (p.obj instanceof Place) {
    icon = "static/place.png";
  } else if (p.obj instanceof BibleText) {
    // no icon
  } else if (p.obj instanceof Source) {
    icon = "static/source.png";
  }
  return uiButton({
    $container : p.$container,
    title : p.obj.name,
    icon : icon,
    classes : p.classes + " object",
    clickCallback : function() {
      p.navFunc(p.obj);
    },
    errorCallback : function(e) {},
  });
}

/**
 * Creates a field for the name of the object, in big font
 *
 * Creates:
 * <div class="obj-title">Abraham</div>
 *
 * @param container {jquery DOM Element}   <div> where to add the UI
 * @param text {String}   The name to show to the user
 */
function uiObjTitle(p) {
  assert(p.container, "div: need element");
  assert(typeof(p.text) == "string", "text: need name");

  return $("<div class='obj-title'/>").appendTo(p.container).text(p.text);
}



/**
 * Creates a header for a part of the page
 *
 * Creates:
 * <div class="section-title">Abraham</div>
 *
 * @param container {jquery DOM Element}   <div> where to add the UI
 * @param text {String}   The title to show to the user
 */
function uiSectionTitle(p) {
  assert(p.container, "div: need element");
  assert(typeof(p.text) == "string", "text: need title");

  return $("<div class='section-title'/>").appendTo(p.container).text(p.text);
}


/**
 * Creates a field for text with several paragraphs.
 * This is the main description of an object.
 *
 * Creates:
 * <div class="descr descr-widget">
 *    <div class="descr-p">bl bla</div>
 *    <div class="descr-p">blo bla</div>
 * </div>
 *
 * @param container {jquery DOM Element}   <div> where to add the UI
 * @param text {String}   The description to show to the user
 *    Any \n in the text will be converted into a paragraph.
 */
function uiDescr(p) {
  assert(p.container, "div: need element");
  assert(typeof(p.text) == "string", "text: need description");

  var descrE = $("<div class='descr descr-widget'/>").appendTo(p.container);
  $.each(p.text.split("\n"), function(i, line) {
    var para = $("<div class='descr-p'/>").appendTo(descrE);
    para.text(line);
  });
  return descrE;
}

/**
 * Takes an existing <div> that already contains text,
 * iterates over all text nodes within it,
 * then recognizes links and linkable objects in it,
 * then creates corresponding HTML link DOM nodes for each link.
 *
 * TODO this function is slow. On a low-end phone, for a large article,
 * it takes 5 seconds, during which the browser is blocked.
 *
 * Takes
 * <div>
 *    bl bla
 *    <span>blub bla</span>
 * </div>
 * Creates:
 * <div>
 *    bl <span class="link recognized" onclick="...">bla</span>
 *    <span>blub <span class="link recognized" onclick="...">bla</span></span>
 * </div>
 *
 * @param container {jquery DOM Element}   <div> to convert
 * @param objs {Array of Detail}   objects to look for
 * @param navFunc {Function(obj {Detail})}   what to do
 *     when the user clicked on a link with an object
 * @param errorCallback {Function(e)}
 */
function enhanceDOMTextWithLinks(p) {
  assert(p.container, "need element");
  assert(typeof(p.objs.length) == "number", "objs: need array");
  assert(typeof(p.navFunc) == "function", "need navFunc");
  assert(typeof(p.errorCallback) == "function", "need errorCallback");

  runLater(500, function() {

  /*
  // remove multiple objects with same name
  var objs = p.objs.slice(0);
  objs = objs.filter(function(a) {
    return objs.filter(function(b) {
      return a != b && a.name == b.name; // different obj with same name
    }).length == 0; // return only those where this is *not* true
  });
  console.log("objs: " + objs.map(function(obj) { return obj.name; }).join(", "));
  */

  //var textBefore = p.container.text();
  p.container.find("*").each(function() {
    var $e = $(this);
    var text = $e.text();

    // TODO factor out
    var links = findObjectsInText(text, p.objs, true);
    links = links.concat(findBibleTexts(text));

    if ( !links.length) {
      return;
    }
    links.sort(function(a, b) { // must be in order of appearance in text
      return a.index - b.index;
    });
    // make sure links are not overlapping
    var lastEndIndex = 0;
    links = links.filter(function(l) {
      var ok = lastEndIndex < l.index;
      if (ok) {
        lastEndIndex = l.index + l.length;
      }
      return ok;
    });

    $e.text("");
    var lastIndex = 0;
    links.forEach(function(link) {
      var obj = link.obj;
      $e.append(document.createTextNode(text.substring(lastIndex, link.index)));
      var $link = $("<span class='link recognized'/>")
      $link.addClass(obj.typename);
      $link.text(text.substr(link.index, link.length));
      $link.mouseover(function() {
        if (obj instanceof Source) {
          obj.load(function() {
            $link.attr("title", obj.name + "\n" + obj.descr);
          });
        } else {
          $link.attr("title", obj.descriptiveName);
        }
      });
      $link.click(cbParams(p.navFunc, obj));
      $e.append($link);
      lastIndex = link.index + link.length;
    });
    $e.append(document.createTextNode(text.substr(lastIndex)));
    /*
    var after = $e.text();
    if (text != after) {
      console.log("before " + text);
      console.log("after " + $e.text());
      for (var i = 0; i < text.length; i++) {
        if (text[i] != after[i]) {
          console.log("diff at index " + i);
          console.log(text.substr(i - 30, 30) + "---" + text.substr(i, 30));
          console.log(after.substr(i - 30, 30) + "---" + after.substr(i, 30));
          break;
        }
      }
    }
    */
  });
  //assert(p.container.text() == textBefore, "Text changed after links");

  }, p.errorCallback);
}


/**
 * Search for the names of the given objects in the text.
 *
 * @param text {String}   a paragraph or more of human-language text
 *     that may contain e.g. "Jonah"
 * @param objs {Array of Detail}   objects to look for
 * @param capsOnly {Boolean}   Only check words where the first char is upper case.
 *    This is a lot faster.
 * @returns {Array of {
 *    index {Integer}   Position in |text| where obj name starts.
 *        This is the link text.
 *    length {Integer}   Length (in characters) of bible ref in |text|
 *    obj {Detail}  Referenced object.
 *        This is the link target.
 * }}
 */
function findObjectsInText(text, objs, capsOnly) {
  assert(typeof(text) == "string");
  assert(typeof(objs.length) == "number", "objs: need array");
  text = text.replace(/\n/g, " "); // treat newline like space
  objs = objs.filter(function(obj) { return obj.name && obj.name.length >= 2; });
  var result = [];
  // find space - can't use split(" "), because name may contain space
  for (var iSpace = 0, i = 0;
       i < text.length && iSpace != -1;
       iSpace = text.indexOf(" ", i), i = iSpace + 1) {
    if (capsOnly && text[i] != text[i].toUpperCase()) {
      continue;
    }
    objs.forEach(function(obj) {
      if (obj.name &&
          // matches name, case sensitive
          text.substr(i, obj.name.length) == obj.name &&
          // following char is not alphanum
          ! /\w/.test(text[i + obj.name.length])) {
        result.push({
          index : i,
          length : obj.name.length,
          obj : obj,
        });
      }
    });
  }
  return result;
}

/**
 * Creates a scrollable listbox/table with potentially many entries.
 * It can have multiple columns.
 * Rows can be clickable.
 * It is not editable, and individual cells cannot be selected.
 *
 * Creates:
 * <table class="list">
 *   <thead><tr>
 *       <th>Name</th>
 *       <th>Age</th>
 *   </tr></thead>
 *   <tbody>
 *     <tr>
 *       <td>Abraham</td>
 *       <td>99</td>
 *     </tr>
 *     <tr>
 *       <td>Sarah</td>
 *       <td>92</td>
 *     </tr>
 *   </tbody>
 * <table>
 *
 * @param container {jquery DOM Element}   <div> where to add the UI
 * @param columns {Array of {
 *    label {String}   The user-visible column header label.
 *    displayFunc {Function}  This function will be called with each object
 *       in |elements| and will return the value to show to the user in this cell.
 * }   The columns and how to fill them.
 * @param data {Array of Object}  The rows. One object per row.
 *     Will be passed to each column's displayFunc() to determine the
 *     values of each cell.
 * @param clickFunc {Function({Object}}  Will be called when the user
 *     clicks on a row.
 *     The first parameter is the object from |data|.
 */
function uiList(p) {
  assert(p.container, "div: need element");
  assert(p.columns.length > 0, "columns: needed");
  assert(p.data.length >= 0, "data: array needed");
  assert(!p.clickFunc || typeof(p.clickFunc) == "function", "clickFunc: need function");

  var tableE = $("<table class='list'/>").appendTo(p.container);
  var headE = $("<thead/>").appendTo(tableE);
  var bodyE = $("<tbody/>").appendTo(tableE);
  // header
  var trE = $("<tr/>").appendTo(headE);
  p.columns.forEach(function(column) {
    var thE = $("<th/>").appendTo(trE).text(column.label);
  });
  // rows
  p.data.forEach(function(obj) {
    var trE = $("<tr/>").appendTo(bodyE);
    var tdEs = [];
    p.columns.forEach(function(column) {
      var tdE = $("<td/>").appendTo(trE);
      tdE.text(column.displayFunc(obj, tdE));
      if (column.classes) {
        tdE.attr("class", column.classes);
      }
      if (column.clickFunc) {
        assert(typeof(column.clickFunc) == "function", "clickFunc is not a function");
        tdE.addClass("clickable");
        tdE.click(function() {
          column.clickFunc(obj, tdE);
        });
      }
      tdEs.push(tdE);
    });
    if (p.clickFunc) {
      assert(typeof(p.clickFunc) == "function", "clickFunc is not a function");
      trE.click(function() {
        p.clickFunc(obj, trE);
      });
      tdEs.forEach(function(tdE) {
        tdE.addClass("clickable");
      });
    }
  });
  return tableE;
}

/**
 * Shows an image, video or audio file.
 *
 * Image:
 * First shows a small version of the image.
 * When the user clicks on it, enlarges to full size.
 * Video:
 * First shows a static preview image of the video.
 * When the user clicks on it, enlarges to full size
 * and starts playing.
 * Audio:
 * First shows a Play icon and a slider.
 * When the user clicks on it, starts playing.
 *
 * Creates:
 * <div class="image media">
 *    <img width="300" src="..."/>
 * </div>
 *
 * @param container {jquery DOM Element}   <div> where to add the UI
 * @param url {String-URL}   the image URL
 */
function uiMedia(p) {
  assert(p.container, "div: need element");
  assert(p.media instanceof Media, "need media object");
  assert(p.media.url, "need media URL");

  if (p.media instanceof Image) {
    assert(typeof(p.media.url) == "string", "need url");
    p.container.addClass("media");
    p.container.addClass("image");
    var $img = $("<img/>").appendTo(p.container);
    $img.attr("src", mediaURL(p.media.url));
    var small = false;
    $img.click(function() {
      small = !small;
      $img.attr("width", small ? "300px" : $("body").width() + "px");
    });
    $img.click();
    return $img;
  }
}


/**
 * Shows a hint to the end user to teach him how to use the program.
 *
 * @param msgID {String}   help text to show to the end user
 *     This is the translation string ID, i.e. the parameter for tr()
 * @param onlyOnce {Boolean}   if true, shows the msg exactly once,
 *     and then never again
 * @param container
 * @returns {  Object with a few functions to control the hint
 *   close() {Function()}   remove the hint now
 *   noMore() {Function()}   never show this hint again
 * }
 */
function uiHint(p) {
  assert(typeof(p.msgID) == "string");
  var $hint;

  // Save and load. Hacky, string-based. Replace with proper objects.
  if ( !gSettings.hintDisabled) {
    gSettings.hintDisabled = ",";
  }
  function isEnabled() {
    return gSettings.hintDisabled.indexOf("," + p.msgID + ",") == -1;
  }
  function disable() {
    if (isEnabled()) {
      gSettings.hintDisabled += p.msgID + ",";
    }
  }


  function show() {
    $hint = $("<div class='hint box'/>").text(tr(p.msgID));
    p.container.prepend($hint);
  }
  function close() {
    if ($hint) {
      $hint.remove();
    }
  }

  if (isEnabled()) {
    show();
    if (p.onlyOnce) {
      disable();
    }
  }
  //setTimeout(close, 5000); // 5 seconds

  return {
    close : close,
    noMore : disable,
  };
}
