/**
 * Modified the existing pages so that the user can
 * - edit the values of |Detail|s
 * - add new objects, usually in relation to existing objects
 * - save the changes to a file
 */

var gEditorOn = true;

$(document).ready(function() {
  try {
    var query = parseURLQueryString(window.location.search);
    if (query.editor == "true") {
      editorStartup();
    }
  } catch (e) { showError(e) }
});

function editorStartup() {
  console.log("editor started");

  var headE = $("head").get(0);
  // Can't use JQuery for this, because that stupid thing tries to parse the JS itself :-(
//       var script = document.createElement("script");
//       script.setAttribute("type", "application/javascript");
//       script.setAttribute("src", "editor.js");
//       headE.appendChild(script);
  $("<link rel='stylesheet' type='text/css' href='editor.css'/>").appendTo($("head"));
  // OpenLayers
  var script = document.createElement("script");
  script.setAttribute("type", "application/javascript");
  script.setAttribute("src", "lib/OpenLayers.js");
  headE.appendChild(script);
  $("<link rel='stylesheet' type='text/css' href='lib/OpenLayers.css'/>").appendTo($("head"));

  // Watch for <title> changes, to know when a new object is being displayed
  document.querySelector("body").addEventListener("page-change", function(e) {
    try {
      // get the object from a special body element property
      var obj = document.querySelector("body").detailObj;
      if (obj) {
        editorDetail(obj);
      } else {
        editorMainPage();
      }
    } catch (e) { showError(e) }
  }, false);

  editorMenu();
}

/**
 * Called when a new object page shows
 */
function editorDetail(obj) {
  assert(obj instanceof Detail, "obj must be a Detail");
  if ( !gEditorOn) {
    return;
  }
  console.log("editing " + obj.name + " of type " + obj.typename);

  // Name textfield
  var firstnameE = $("<input type='text' class='title editor'/>");
  $(".obj-title").after(firstnameE);
  firstnameE.val(obj.name);
  firstnameE.change(function() {
    try {
      console.log("Name of " + obj.name + " changed to " + firstnameE.val());
      assert(firstnameE.val(), "Name cannot be empty");
      obj.name = firstnameE.val().trim();
    } catch (e) { showError(e); }
  });

  // ID display
  var detailsBox = $("<div class='details-box editor'/>");
  firstnameE.after(detailsBox);
  var idE = $("<span class='id editor'/>");
  detailsBox.append(idE);
  idE.text("ID " + obj.id);
  var idExtE = $("<span class='id editor'/>");
  detailsBox.append(idExtE);
  idExtE.text("IDExt " + (obj.idExt || "(none)"));


  // Description textfield
  var descrBox = $(".descr-widget").parent().empty();
  var descrE = $("<textarea class='descr editor'/>");
  descrBox.after(descrE);
  descrE.val(obj.descr);
  descrE.change(function() {
    try {
      console.log("Descr of " + obj.name + " changed to " + descrE.val());
      obj.descr = descrE.val();
    } catch (e) { showError(e); }
  });

  // Other names textfield
  //var otherNamesBox = $(".other-names");
  //otherNamesBox.empty();
  $(".other-names").empty();
  var otherNamesBox = $("<div class='other-names'/>").appendTo(detailsBox);
  otherNamesBox.append($("<label class='other-names'/>").text("Other names:"));
  var otherNamesE = $("<input type='text' class='other-names editor'/>");
  otherNamesBox.append(otherNamesE);
  otherNamesE.val(obj.otherNames && obj.otherNames.length > 0 ? obj.otherNames.join(", ") : "");
  otherNamesE.change(function() {
    try {
      obj.otherNames = [];
      var str = otherNamesE.val().trim();
      if (!str) {
        return;
      }
      var names = str.split(",");
      names.forEach(function(name) {
        name = trim(name);
        obj.otherNames.push(name);
      });
    } catch (e) { showError(e); }
  });

  // Role textfield
  var roleE = $("<input type='text' class='role editor'/>");
  otherNamesBox.append($("<label class='role'/>").text("Important role (King, Apostle etc.):"));
  otherNamesBox.append(roleE);
  roleE.val(obj.role);
  roleE.change(function() {
    try {
      obj.role = roleE.val();
    } catch (e) { showError(e); }
  });

  var buttonsE = $("<div class='add button-list editor'/>");
  $(".obj-title").after(buttonsE);
  var objTypes = [ Event, Source, Place ];
  if ( !(obj instanceof Person)) {
    objTypes.push(Person);
  }
  objTypes.forEach(function(type) {
    var typename = type.prototype.typename;
    var buttonE = $("<span class='add button editor'/>");
    buttonsE.append(buttonE);
    buttonE.addClass(typename);
    buttonE.text("Add " + typename + "...");
    buttonE.click(function() {
      try {
        var editObj = editorEditObject(obj);
        editorAddDialog(type, function(newObj, isNew) {
          if ( !newObj) {
            return;
          }
          var rel = editObj.addRelation(newObj, typename);
          var relOpp = rel.opposite();
          gStorage.add(editObj); // May already exist
          relOpp.type = editObj.typename;
          relOpp.add();
          gStorage.add(newObj);
          editorRefresh();
        }, showError);
      } catch (e) { showError(e); }
    });
  });
  var buttonE = $("<span class='remove button editor'/>");
  buttonsE.prepend(buttonE);
  buttonE.text("Delete");
  buttonE.click(function() {
    try {
      // TODO confirm
      obj.remove();
      gStorage.remove(obj);
      gBrowsingHistory.goBack();
    } catch (e) { showError(e); }
  });

  var $relations = $("<div class='remove-relations editor'/>").appendTo($("#detail"));
  uiSectionTitle({
    container: $relations,
    text : "All relations",
  });
  uiList({
    container : $relations,
    data : obj.relations,
    columns : [
      { label : "Object type", displayFunc : function(rel) { return rel.obj.typename; } },
      { label : "Relation type", displayFunc : function(rel) { return rel.typeLabel; } },
      { label : "Object name", displayFunc : function(rel) { return rel.obj.name; } },
      { label : "Remove", displayFunc : function(rel) { return "Remove"; },
          clickFunc : function(rel, $td) {
            rel.remove();
            //$td.parent().remove(); // remove row in list, to give some visual feedback
            editorRefresh(); // update other lists
          }, },
    ],
  });

  dbpediaIDShow(obj);

  // Insight book source select
  var $insightBox = $("<div class='source-select editor'/>").appendTo($("div.detail"));
  uiSectionTitle({
    container: $insightBox,
    text : "Insight book",
  });
  var $insightLoadButton = $("<div class='button''/>")
      .text("Select insight book article")
      .appendTo($insightBox)
  $insightLoadButton.click(function() {
    $insightLoadButton.attr("disabled", "true").text("Loading...");
    editorLoadInsightBook(function(insightBookStorage) {
      var name = obj.name.split(" ")[0];
      insightBookStorage.searchName(name, function(sources) {
        sources = sources.filter(function(source) {
          return source instanceof Source && source.idExt;
        });
        $insightLoadButton.remove();
        uiList({
          container : $insightBox,
          data : sources,
          columns : [
            { label : "Article number", displayFunc : function(s) { return s.idExt; } },
            { label : "Name", displayFunc : function(s) { return s.name; } },
            { label : "First paragraph", displayFunc : function(s) { return s.descr /*? s.descr.split("\n")[0] : "";*/ } },
            { label : "Action", displayFunc : function(s) { return "Select"; },
                clickFunc : function(s) {
                  obj.idExt = s.idExt;
                  //obj.addRelation(s, "description");
                  //gStorage.add(s);
                  editorRefresh(); // update other lists
                }, },
          ],
        });
      }, showError);
    }, showError);
  });

  if (obj instanceof Person) {
    editorPerson(obj);
  } else if (obj instanceof Event) {
    editorEvent(obj);
  } else if (obj instanceof Place) {
    editorPlace(obj);
  } else if (obj instanceof BibleText) {
    editorBibleReader(obj);
  }
}

/**
 * Called as part of editorDetail()
 */
function editorPerson(obj) {
  assert(obj instanceof Person);
  var detailsBox = $(".details-box.editor");

  // Gender (Male/Female) checkbox
  var genderBox = $("<span class='gender-box editor'/>");
  var genderE = $("<input type='checkbox' id='gender-editor' class='gender editor'/>")
  genderBox.append(genderE);
  genderBox.append($("<label for='gender-editor'/>").text("Male"));
  detailsBox.append(genderBox);
  genderE.prop("checked", obj.male);
  genderE.change(function() {
    try {
      console.log("Gender of " + obj.name + " changed to " + !!genderE.prop("checked"));
      obj.male = !!genderE.prop("checked");
    } catch (e) { showError(e); }
  });

  // Add relative
  var buttonsE = $("<div class='person button-list editor'/>");
  var relList = $(".related-person-list");
  if (relList.get(0)) {
    relList.after(buttonsE);
  } else {
    $(".obj-title").after(buttonsE);
  }
  [ "father", "mother", "child", "married", "friend", "disciple" ].forEach(function(relType) {
    var buttonE = $("<span class='add person button editor'/>");
    buttonsE.append(buttonE);
    buttonE.addClass(relType);
    buttonE.text("Add " + relType + "...");
    buttonE.click(function() {
      try {
        var editObj = editorEditObject(obj);
        var male = relType != "mother";
        var type = relType;
        if (relType == "son" || relType == "daughter") {
          type = "child";
        }
        editorAddDialog(Person, function(newObj, isNew) {
          if ( !newObj) {
            return;
          }
          newObj.male = male;
          gStorage.add(editObj); // May already exist
          var rel = editObj.addRelation(newObj, type);
          rel.opposite().add();
          gStorage.add(newObj);
          editorRefresh();
        }, showError);
      } catch (e) { showError(e); }
    });
  });

  // Add birth/death event
  var eventbuttonsE = $("div.add");
  ["birth", "death"].forEach(function(typename) {
    var buttonE = $("<span class='add button editor'/>");
    eventbuttonsE.append(buttonE);
    buttonE.addClass(typename);
    buttonE.text("Add " + typename + "...");
    buttonE.click(function() {
      try {
        var person = editorEditObject(obj);
        var event = new Event();
        event.name = person.name + " " + (typename == "birth" ? "born" : "died");
        var rel = person.addRelation(event, typename);
        var relOpp = rel.opposite();
        relOpp.type = typename;
        relOpp.add();
        gStorage.add(person); // May already exist
        gStorage.add(event);
        showDetail(event);
      } catch (e) { showError(e); }
    });
  });

}

/**
 * Called as part of editorDetail()
 */
function editorEvent(obj) {
  var detailsBox = $(".details-box.editor");
  var timeBox = $("<div class='time-box event editor'/>");
  detailsBox.append(timeBox);

  // Year textfield
  var yearE = $("<input type='number' size='5' class='year event editor'/>");
  timeBox.append($("<label class='year'/>").text("Year"));
  timeBox.append(yearE);
  if (obj.time) {
    yearE.val(obj.time.getUTCFullYear());
  }
  yearE.change(function() {
    try {
      if (yearE.val()) {
        obj.time = new Date(yearE.val(), 1);
        obj.timeGenerated = false;
        timeE.val(obj.time.toISOString()); // update other field
      } else {
        obj.time = null;
        obj.timeGenerated = false;
        timeE.val("");
      }
    } catch (e) { showError(e); }
  });

  // Calendar textfield
  var timeE = $("<input type='datetime' class='calendar event editor'/>");
  timeBox.append($("<label class='calendar'/>").text("Time"));
  timeBox.append(timeE);
  if (obj.time) {
    timeE.val(obj.time.toISOString());
  }
  var changing = false; // prevent loop
  timeE.change(function() {
    try {
      if (changing) {
        return;
      }
      changing = true;
      if (timeE.val()) {
        obj.time = timeE.get(0).valueAsDate || new Date(timeE.val());
        obj.timeGenerated = false;
        yearE.val(obj.time.getUTCFullYear()); // update other field
      } else {
        obj.time = null;
        obj.timeGenerated = false;
        yearE.val("");
      }
      changing = false;
    } catch (e) { showError(e); }
  });

  // "After event..." button
  var afterButtonE = $("<span class='after-event button editor'/>");
  timeBox.append(afterButtonE);
  afterButtonE.text("After event...");
  afterButtonE.click(function() {
    try {
      editorAddDialog(Event, function(newObj, isNew) {
        if ( !newObj) {
          return;
        }
        gStorage.add(newObj);
        obj.wasAfterEvent(newObj);
        editorRefresh();
      }, showError);
    } catch (e) { showError(e); }
  });

  // "After event (with relative time)..." button
  var afterWithTimeButtonE = $("<span class='after-event button editor'/>");
  timeBox.append(afterWithTimeButtonE);
  afterWithTimeButtonE.text("After event (with relative time)...");
  afterWithTimeButtonE.click(function() {
    try {
      editorAddDialog(Event, function(newObj, isNew) {
        if ( !newObj) {
          return;
        }
        gStorage.add(newObj);
        var relativeTime = parseRelativeTime(prompt("How long after '" + newObj.name + "' was this?\ny = years, d = days, h = hours"));
        obj.setAfterEventWithRelativeTime(newObj, relativeTime);
        editorRefresh();
      }, showError);
    } catch (e) { showError(e); }
  });

  var afterEvents = obj.relationsOfType("afterEvent", Event);
  var beforeEvents = obj.relationsOfType("beforeEvent", Event);
  if (afterEvents.length > 0) {
    uiList({
      container : timeBox,
      columns : [
        { label : "This happened after", displayFunc : function(event) { return event.name; } },
      ],
      data : afterEvents,
    });
  }
  if (beforeEvents.length > 0) {
    uiList({
      container : timeBox,
      columns : [
        { label : "This happened before", displayFunc : function(event) { return event.name; } },
      ],
      data : beforeEvents,
    });
  }


  // Show full quote of sources, and add source obj,
  // so that editorSelectedBibleSource() can process it
  if (obj.sources.length > 0) {
    var $container = $("div.source-list");
    obj.sources.forEach(function(source) {
      source.fetch(function() {
        $("div.source-list table.list").empty();
        var $source = $("<div class='source editor'/>").appendTo($container);
        $source.prop("obj", source);
        $("<div class='source-name editor'/>").text(source.name).appendTo($source);
        source.quote.split("\n").forEach(function(line) {
          $("<div class='descr-p'/>").text(line).appendTo($source);
        });

        var $buttons = $("<div class='text-obj button-list editor'/>").appendTo($source);
        editorAddObjectButtonsFromTextSearch(source.quote, obj, [], $buttons);
      }, showError);
    });
  }
}

/**
 * @param timeStr {String} e.g. "1y" or "1 y" or "2d"
 * @returns {Integer} Unixtime difference, in milliseconds
 */
function parseRelativeTime(timeStr) {
  var num = parseInt(timeStr); // truncates all letters
  if (isNaN(num)) throw new Exception("Number needed");
  var unit = trim(timeStr.substr((num + "").length));
  var relTime = new Date(0);
  if (unit == "y" || unit == "year" || unit == "years") {
    relTime.setFullYear(relTime.getFullYear() + num);
  } else if (unit == "d" || unit == "day" || unit == "days") {
    relTime.setDate(relTime.getDate() + num);
  } else if (unit == "h" || unit == "hour" || unit == "hours") {
    relTime.setHour(relTime.getHour() + num);
  } else {
    throw new Exception("Unknown unit " + unit);
  }
  return relTime.getTime();
  /*
  var kHour = 3600 * 1000;
  if (unit == "y" || unit == "year" || unit == "years") {
    return num * 356.25 * 24 * kHour;
  } else if (unit == "d" || unit == "day" || unit == "days") {
    return num * 24 * kHour;
  } else if (unit == "h" || unit == "hour" || unit == "hours") {
    return num * kHour;
  } else {
    throw new Exception("Unknown unit " + unit);
  }
  */
}

/**
 * Called as part of editorDetail()
 */
function editorBibleReader(obj) {
  assert(obj instanceof BibleText);

  // Can't delete parts of the Bible :)
  $(".editor.button.remove").remove();

  // Add buttons for objs found in text
  /*
  var $buttons = $("div.editor.button-list.add");
  var $button = $("<span class='find-objs button editor'/>");
  $buttons.prepend($button);
  $button.text("Find unmarked objects in text");
  $button.click(function() {
    try {
  */
  obj.fetch(function() {
      var $verses = $("div.bible-reader a.verse");
      $verses.each(function() {
        var $verse = $(this);
        var bt = $verse.prop("obj");
        assert(bt instanceof BibleText);
        var text = $verse.text();
        var $buttonsVerse = $("<div class='button-list editor'/>").appendTo($verse);
        obj.findAllReferencingObjects(gStorage, function(verseObjs) {
          editorAddObjectButtonsFromTextSearch(text, bt, verseObjs, $buttonsVerse);
        }, showError);
      });
  }, showError);
  /*
    } catch (e) { showError(e); }
  });
  */
}

/**
 * Called as part of editorDetail()
 */
function editorPlace(obj) {
  var mode = 0; // 0 = no edit, 1 = point, 2 = area
  function addedCallback(geocoords) {
    console.log(dumpObject(geocoords, "gotcoord", 2));
    assert(mode, "We shouldn't be adding edit coord mode");
    if ( !geocoords.length) {
      showError("Got no coordinates");
      return;
    }
    assert(geocoords[0] instanceof GeoCoordinate);
    if (mode == 1) { // point
      obj.point = geocoords[0];
      obj.area = null;
    } else if (mode == 2) { // area
      obj.area = geocoords;
      obj.point = null;
    }
    console.log(dumpObject(geocoords, "place", 2));
  }
  // Show a map, with edit enabled
  $("#map").empty();
  var $container = $("<div class='map-edit'/>");
  $("div.editor.details-box").after($container);
  //var mapControl = uiMapOpenLayers({ // TODO throws errors, fix it
  var mapControl = uiMap({
    container : $container,
    places : [ obj ],
    //editMode : true,
    //addedCallback : addedCallback,
    navFunc : function() {},
    errorCallback : showError,
  });
  // Show 2 buttons that allow to create either a point or an area
  var buttonsE = $("<div class='place button-list editor'/>").prependTo($container);
  var buttonE = $("<span class='geo-point-manual button editor'/>");
  buttonsE.append(buttonE);
  buttonE.text("Set geo point using coordinates...");
  buttonE.click(function() {
    try {
      var str = prompt("Enter coordinates lat, lon. E.g.: 31.703°, 35.1944° or 31° 51′ 19.6″ N, 35° 27′ 43.85″ E", ""); // blocks
      var parsed = parseLatLonString(str); // throws
      var geo = new GeoCoordinate(parsed.lon, parsed.lat);
      obj.point = geo;
      obj.area = null;
      editorRefresh();
    } catch (e) { showError(e); }
  });
  buttonE = $("<span class='geo-point button editor'/>");
  buttonsE.append(buttonE);
  buttonE.text("Set geo point on map...");
  buttonE.click(function() {
    try {
      mode = 1; // point
      mapControl.editAddStart();
    } catch (e) { showError(e); }
  });
  buttonE = $("<span class='geo-area button editor'/>");
  buttonsE.append(buttonE);
  buttonE.text("Set geo area on map...");
  buttonE.click(function() {
    try {
      mode = 2; // area
      mapControl.editAddStart();
    } catch (e) { showError(e); }
  });
}

/**
 * @param str {String}   coordinates in string form
 *     E.g.: 31.703°, 35.1944°
 *     or 31° 51′ 19.6″ N, 35° 27′ 43.85″ E
 * @returns { lat {Float}, lon {Float}}
 * @throws Exception from assert()
 */
function parseLatLonString(str) {
  var sp = str.split(",");
  assert(sp.length == 2, "Need exactly 2 coordinates, separated by comma");
  var latStr = trim(sp[0]);
  var lonStr = trim(sp[1]);
  var lat, lon;
  if (latStr.indexOf("N") != -1 || lonStr.indexOf("S") != -1) {
    // degree, minutes, seconds
    ["°", "'", '"', "′", "″"].forEach(function(removeChar) {
      latStr = latStr.replace(removeChar, "");
      lonStr = lonStr.replace(removeChar, "");
    });
    latSp = latStr.split(" ");
    lonSp = lonStr.split(" ");
    assert(latSp.length == 4 && lonSp.length == 4, "Need degree, minutes, seconds, N/S/W/E, separated by space");
    var latDir = latSp[3];
    var lonDir = lonSp[3];
    lonDir = lonDir.replace("O", "E"); // HACK Allow German -- translate?
    assert(["N", "S"].indexOf(latDir) != -1 && ["W", "E"].indexOf(lonDir) != -1, "N/S/W/E as last component");
    lat = parseInt(latSp[0]) + parseInt(latSp[1]) / 60 + parseFloat(latSp[2]) / 3600
        * (latDir == "N" ? 1 : -1);
    lon = parseInt(lonSp[0]) + parseInt(lonSp[1]) / 60 + parseFloat(lonSp[2]) / 3600
        * (lonDir == "E" ? 1 : -1);
    console.log("Converted " + str + " to lat " + lat + " lon " + lon);
  } else {
    // decimal degree
    lat = parseFloat(latStr.replace("°", ""));
    lon = parseFloat(lonStr.replace("°", ""));
  }
  assert( !isNaN(lat), "lat is not a number");
  assert( !isNaN(lon), "lon is not a number");
  return { lat : lat, lon : lon };
}

/**
 * Opens a dialog that allows the user to enter the name of an
 * object. If an object with that name already exist, allows the user
 * to select that, and that will be the result.
 * Otherwise (if not existing, or the user doesn't select one),
 * creates a new object with that name, and returns that.
 * If the user aborts, returns null.
 * @param type {a type of Detail}  E.g. |Person| or |Event| or |Source|
 * @param successCallback {Function(obj {Detail}, isNew {Boolean})}
 */
function editorAddDialog(type, successCallback, errorCallback) {
  var prefillText = "";
  var selectedText = editorSelectedText();
  if (selectedText && selectedText.length < 40) {
    prefillText = selectedText;
  }

  var dialog;
  function add() {
    var obj;
    if (type == Person) {
      obj = new Person();
    } else if (type == Event) {
      obj = new Event();
    } else if (type == Source) {
      obj = new Source();
    } else if (type == Place) {
      obj = new Place();
    }
    obj.name = nameE.val();
    console.log("Created " + obj.name);
    dialog.dialog("close");
    successCallback(obj, true);
  }
  function onItemSelect(e, row) {
    var obj = row.item.obj;
    assert(obj instanceof Detail, "Autocomplete widget gave me an unexpected result");
    console.log(dumpObject(obj, "selected", 1));
    $(e.target).autocomplete("close");
    dialog.dialog("close");
    successCallback(obj, false);
  }
  function cancel() {
    successCallback(null, false);
    dialog.dialog("close");
  }
  function autocompleteSearch(req, showSuggestions) {
    //console.log("autocomplete search for " + req.term);
    gStorage.searchName(req.term, function(results) {
      var suggestions = [];
      results.forEach(function(obj) {
        if ( !(obj instanceof type)) {
          return;
        }
        suggestions.push({
          label : obj.descriptiveName ? obj.descriptiveName : obj.name,
          value : obj.name,
          obj : obj,
        });
      });
      //console.log(dumpObject(suggestions, "sug", 1));
      showSuggestions(suggestions);
    }, function(e) {
      showSuggestions([]); // per API spec, must always call this
      showError(e);
    });
  }
  dialog = $("<div class='add dialog editor'/>");
  $("<label/>").text("Name").appendTo(dialog);
  var nameE = $("<input type='text' class='name' />").appendTo(dialog);
  nameE.autocomplete({
    source : autocompleteSearch,
    select : onItemSelect,
  });
  nameE.returnKey(add);
  dialog.dialog({
    appendTo : "body",
    modal : true,
    draggable : false,
    resizable : false,
    title : "Add " + type.prototype.typename,
    buttons : [
      { text : "OK", click : add, },
      { text : "Cancel", click : cancel, },
    ],
  });
  if (prefillText) {
    nameE.val(prefillText);
    nameE.autocomplete("search", prefillText);
  }
}

/**
 * If some text anywhere on the current page is selected (highlighted),
 * return that.
 * @returns {String}
 */
function editorSelectedText() {
  if ( !window.getSelection().isCollapsed) {
    return window.getSelection().toString();
  }
  //var e = document.activeElement; -- once the button is clicked, the textarea focus is already lost
  var e = $("textarea.descr").get(0);
  if (e && e.selectionStart && typeof(e.value) == "string") {
    return e.value.substring(e.selectionStart, e.selectionEnd);
  }
  return null;
}

/**
 * If we're in Bible reader, and the user selected
 * one or more specific verses, then return those.
 *
 * If only a part of a verse is selected, the whole verse is returned.
 *
 * @param chapter {BibleText}
 * @returns {BibleText}
 *     May be null, if no verse selected
 */
function editorSelectedBibleVerses(chapter) {
  try {
    if (window.getSelection().isCollapsed) {
      return null;
    }
    var sel = window.getSelection();
    var verseFrom = null;
    var verseTo = null;
    for (var e = sel.anchorNode; e && !verseFrom; e = e.parentNode) {
      if (e.nodeName.toLowerCase() == "a") {
        verseFrom = parseInt(e.getAttribute("verse"));
      }
    }
    for (var e = sel.focusNode; e && !verseTo; e = e.parentNode) {
      if (e.nodeName.toLowerCase() == "a") {
        verseTo = parseInt(e.getAttribute("verse"));
      }
    }
    if ( !verseFrom || !verseTo) {
      return null;
    }
    assert(chapter instanceof BibleText);
    var b = chapter.clone();
    if (verseFrom == verseTo) {
      b.verse = verseFrom;
      b.verseTo = 0;
    } else if (verseFrom < verseTo) {
      b.verse = verseFrom;
      b.verseTo = verseTo;
    } else { // swapped
      b.verse = verseTo;
      b.verseTo = verseFrom;
    }
    b.init();
    b._fetchFromCache();
    console.log(dumpObject(b, "b", 1));
    return b;
  } catch (e) { showError(e); }
}

/**
 * The object that we currently edit
 * where we will add new relations to.
 * @param obj {Detail}   What you think the edited object is
 * @returns {Detail}   What I know (unless I know better, I'll return |obj|)
 */
function editorEditObject(obj) {
  return editorSelectedBibleVerses(obj) || obj;
}

/**
 * Reloads the current page and object,
 * to update any changes that were just made by the user.
 */
function editorRefresh() {
  $("body").attr("detail-id", ""); // HACK to make DOMAttrModified above trigger
  //gBrowsingHistory.refreshCurrent();
}

/**
 * Called when the main page shows
 */
function editorMainPage() {
}

/**
 * Called onLoad
 * Sets up the editor buttons on the top right, to
 * - toggle edit mode
 * - save to file
 */
function editorMenu() {
  /*
  var modeButtonE = $("#menu-box").before($("<span id='editor-mode' class='mode button editor'/>"));
  modeButtonE.text("Edit on/off");
  modeButtonE.click(function() {
    gEditorOn = !gEditorOn;
    var obj = document.querySelector("body").detailObj;
    if (obj) {
      showDetail(obj); // TODO from app.js
      editorDetail(detail);
    }
  });
  */
  var saveButtonE = $("<span id='editor-save' class='save button editor'/>");
  $("#menu-box").before(saveButtonE);
  saveButtonE.text("Save to file");
  saveButtonE.click(function() {
    console.log("saving");
    new SaveToNativeJSON(gStorage).save(function(jsonStr) {
      downloadFromVariable(jsonStr, "text/json");
    }, showError);
  });
  var save2ButtonE = $("<span id='editor-save2' class='save button editor'/>");
  $("#menu-box").before(save2ButtonE);
  save2ButtonE.text("Save without descr");
  save2ButtonE.click(function() {
    console.log("saving");
    new SaveToNativeJSON(gStorage, false, false).save(function(jsonStr) {
      downloadFromVariable(jsonStr, "text/json");
    }, showError);
  });
  var saveDescrButtonE = $("<span id='editor-save-descr' class='save button editor'/>");
  $("#menu-box").before(saveDescrButtonE);
  saveDescrButtonE.text("Save descrs");
  saveDescrButtonE.click(function() {
    exportDescrToJSON(gStorage, function(jsonStr) {
      downloadFromVariable(jsonStr, "text/json");
    }, showError);
  });
}

/**
 * Searches |text| for mentioned names of objects in |gStorage|.
 * Adds buttons to |$container| that offer to add them to |obj|.
 *
 * @param text {String}   where to search for obj names
 * @param obj {Detail}   where to add found objs
 * @param knownObjs {Detail}   additional objects *not* to add (Optional)
 * @param $container {JQuery element}   where to add the buttons
 */
function editorAddObjectButtonsFromTextSearch(text, obj, knownObjs, $container) {
  if ( !text) {
    return;
  }
  assert(typeof(text) == "string");
  assert(obj instanceof Detail);
  assert(typeof(knownObjs.length) == "number");
  knownObjs = knownObjs.concat(obj.allRelatedObjs);

  gStorage.getAll(null, function(allObjs) {
    var found = findObjectsInText(text, allObjs, true);  // widgets.js
    var foundObjs = [];
    found.forEach(function(foundInfo) {
      if (foundObjs.indexOf(foundInfo.obj) != -1) { // duplicate
        return;
      }
      if (knownObjs.some(function(k) { return k.name == foundInfo.obj.name; })) {
        return;
      }
      foundObjs.push(foundInfo.obj);
    });

    foundObjs.forEach(function(foundObj) {
      var buttonE = $("<span class='add text-obj button editor'/>");
      $container.append(buttonE);
      buttonE.addClass(foundObj.typename);
      buttonE.text("Add " + foundObj.descriptiveName);
      buttonE.click(function() {
        try {
          gStorage.add(obj); // May already exist. Just for BibleText
          var rel = obj.addRelation(foundObj, foundObj.typename);
          var relOpp = rel.opposite();
          relOpp.type = obj.typename;
          relOpp.add();
          editorRefresh();
        } catch (e) { showError(e); }
      });
    });
  }, showError);
}

var gInsightBookStorageCache;

function editorLoadInsightBook(successCallback, errorCallback) {
  if (gInsightBookStorageCache) {
    successCallback(gInsightBookStorageCache);
    return;
  }
  loadURL(dataURL("insight-book.ids.json"), "json", function(names) {
    loadURL(dataURL("insight-book.split.json"), "json", function(articles) {
      gInsightBookStorageCache = new RAMStorage();
      for (var id in articles) {
        var article = articles[id];
        article = article.split("\n")[0];
        var idSp = id.split("-");
        var idAll = idSp[0] + "-" + idSp[1] + "-A";
        var name;
        for (var n in names) {
          if (names[n] == idAll) {
            name = n;
            break;
          }
        }
        var s = new Source();
        s.name = name;
        s.idExt = id;
        s.descr = article;
        gInsightBookStorageCache.add(s);
      }
      successCallback(gInsightBookStorageCache);
    }, errorCallback);
  }, errorCallback);
}



function dbpediaIDShow(obj) {

  // ID display
  var detailsBox = $("div.details-box");
  var dbpediaBox = $("<div class='dbpedia-box editor'/>").appendTo(detailsBox);

  // Name textfield
  $("<label class='editor'/>").text("Wikipedia ID").appendTo(dbpediaBox);
  var idE = $("<input type='text' class='dbpediaID editor'/>")
    .appendTo(dbpediaBox)
    .val(obj.dbpediaID)
    .attr("verified", obj.dbpediaID ? "" : "false");
  idE.change(function() {
    try {
      obj.dbpediaID = idE.val().trim();
    } catch (e) { showError(e); }
  });

  var dbpediaLoadButton = $("<div class='dbpedia-load button''/>")
      .text("Load dbpedia article")
      .appendTo(dbpediaBox).click(function() {
        var dbpediaID = obj.dbpediaID || dbpediaIDFromTitle(obj.name);
        dbpediaSelect(obj, dbpediaID);
      });
}

function dbpediaSelect(obj, dbpediaID) {
  var dbpediaBox = $("div.dbpedia-box");
  var idE = $("input.dbpediaID");
  $("div.dbpedia-select-box").remove();
  var box = $("<div class='dbpedia-select-box editor'/>").appendTo(dbpediaBox);
  uiSectionTitle({
    container: box,
    text : "Confirm matching dbpedia Article",
  });

  // Check redirect
  var query = "SELECT * FROM <http://dbpedia.org> WHERE {" +
    "dbpedia:" + esc(dbpediaID) + " dbpedia-owl:wikiPageRedirects ?other . " +
  "}";
  sparqlSelect1(query, {}, function(result) {
    if (result && result.other) {
      var redirectedToID = result.other.replace("http://dbpedia.org/resource/", "");
      dbpediaSelect(obj, redirectedToID);
    }
  }, function(e) {
    if (e.rootErrorMsg != "Nothing found") {
      showError(e);
    }
  });

  // check other objs for overlap
  if (dbpediaID) {
    gStorage.iterate(function(other) {
      if (other.dbpediaID == dbpediaID && other != obj) {
        showError("Other object " + obj.name + " (" + obj.typename + ") already has this same dbpedia ID");
      }
    }, function() {}, showError);
  }

  var confirmBox = $("<div class='dbpedia-confirm-box editor'/>").appendTo(box);
  var loadingE = $("<div class='dbpedia-loading'>")
      .text("Loading...")
      .appendTo(confirmBox);

  var query = "SELECT * FROM <http://dbpedia.org> WHERE {" +
    "dbpedia:" + esc(dbpediaID) + " rdfs:comment ?descr . " +
    "filter(langMatches(lang(?descr), 'en')) " + // one lang
  "}";
  sparqlSelect1(query, {}, function(result) {
    var descr = result.descr;
    loadingE.remove();

    var articleE = $("<div class='dbpedia-article'>")
        .text(descr)
        .appendTo(confirmBox);

    var confirmButton = $("<div class='dbpedia-confirm button''/>")
        .text("Confirm match")
        .appendTo(confirmBox).click(function() {
          obj.dbpediaID = dbpediaID;
          idE.val(dbpediaID);
          idE.attr("verified", "true");
          dbpediaMatch(obj);
          confirmBox.remove();
        });
    var wrongButton = $("<div class='dbpedia-reject button''/>")
        .text("Not correct")
        .appendTo(confirmBox).click(function() {
          obj.dbpediaID = null;
          idE.val("");
          confirmBox.remove();
        });
  }, function(e) {
    loadingE.text(e);
  });

  var type = "";
  if (obj instanceof Place) {
    type += "?other a dbpedia-owl:Place . ";
  } else if (obj instanceof Person) {
    type += "?other a dbpedia-owl:Person . ";
  }
  query = "SELECT ?other ?name ?descr FROM <http://dbpedia.org> WHERE {" +
    "{ { " +
      "dbpedia:" + esc(dbpediaID) + " dbpedia-owl:wikiPageDisambiguates ?other . " +
    "} UNION { " +
      "?disamb dbpedia-owl:wikiPageDisambiguates dbpedia:" + esc(dbpediaID) + " . " +
      "?disamb dbpedia-owl:wikiPageDisambiguates ?other . " +
    "} } " +
    type +
    "OPTIONAL { ?other rdfs:label ?name } " +
    "OPTIONAL { ?other rdfs:comment ?descr } " +
    "filter(langMatches(lang(?name), 'en')) " + // one lang
    "filter(langMatches(lang(?descr), 'en')) " +
  "} GROUP BY ?other LIMIT 50";
  sparqlSelect(query, {}, function(results) {
    uiSectionTitle({
      container: box,
      text : "Select matching dbpedia Article",
    });
    uiList({
      container : box,
      data : results,
      columns : [
        { label : "Name", displayFunc : function(o) { return o.name; } },
        { label : "Description", displayFunc : function(o) { return o.descr; } },
        { label : "Action", displayFunc : function(s) { return "Select"; },
            clickFunc : function(s) {
              obj.dbpediaID = s.other.replace("http://dbpedia.org/resource/", "");
              idE.val(obj.dbpediaID);
              idE.attr("verified", "true");
              dbpediaMatch(obj);
            }, },
      ],
    });
  }, errorNonCritical);
}

/**
 * Called when the user confirmed that the dbpedia ID matches.
 * Allows to fetch certain data from dbpedia and store it, e.g.
 * geo location.
 */
function dbpediaMatch(obj) {
  // Get location
  if ( !obj.point && obj.area.length == 0) {
    getLocation(obj.dbpediaID, function(lat, long) {
      obj.point = new GeoCoordinate(long, lat);
      editorRefresh();
    }, showError);
    return;
  }
}




/**** Lib: Linked Open Data lookups ****/

function dbpediaIDFromTitle(title) {
  title = title
      .replace(/ \(.*/g, "") // remove (Mount)
      .replace(/ /g, "_"); // Spaces -> _
  title = title[0] + title.substr(1).toLowerCase(); // Double words in lowercase
  return title;
}

function getLocation(dbpediaID, resultCallback, errorCallback) {
  var query = "SELECT ?lat ?long FROM <http://dbpedia.org> WHERE { " +
    "dbpedia:" + esc(dbpediaID) + " geo:lat ?lat ; " +
    " geo:long ?long . " +
  "}";
  sparqlSelect1(query, {}, function(result) {
    resultCallback(parseFloat(result.lat), parseFloat(result.long));
  }, errorCallback);
}

function esc(str) {
  // TODO
  return str.replace(/\&/g, "and")
    .replace(/\"/g, "'")
    .replace(/ /g, "_")
    .replace(/\(/g, "%28") // TODO doesn't work, neither does \\u28
    .replace(/\)/g, "%29");
}

/**
 * @param serverEx {ServerException}
 * @param query {String} the SPARQL query string, readable
 */
function SPARQLException(serverEx, query)
{
  var msg = serverEx.rootErrorMsg;
  ddebug(msg + "\n" + query);
  ServerException.call(this, serverEx.rootErrorMsg, serverEx.code, serverEx.uri);
  this.query = query;
  this._message = msg;
}
SPARQLException.prototype =
{
}
extend(SPARQLException, ServerException);

var cRDFPrefixes = {
  rdfs: "http://www.w3.org/2000/01/rdf-schema#",
  rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
  dc: "http://purl.org/dc/elements/1.1/",
  foaf: "http://xmlns.com/foaf/0.1/",
  dbpedia: "http://dbpedia.org/resource/",
  dbpediaprop: "http://dbpedia.org/property/",
  dbpediaowl: "http://dbpedia.org/ontology/",
  "dbpedia-prop": "http://dbpedia.org/property/",
  "dbpedia-owl": "http://dbpedia.org/ontology/",
  geo: "http://www.w3.org/2003/01/geo/wgs84_pos#",
  geonames: "http://www.geonames.org/ontology#",
  freebase: "http://rdf.freebase.com/ns/",
  owl: "http://www.w3.org/2002/07/owl#",
  skos: "http://www.w3.org/2004/02/skos/core#",
};

function sparqlSelect(query, params, resultCallback, errorCallback) {
  assert(params && typeof(params) == "object", "Need params");
  var url;
  if (params.url) {
    url = params.url;
  } else if (params.endpoint) {
    url = "/sparql/" + params.endpoint + "/";
  } else {
    url = "/sparql/dbpedia/";
  }
  params.prefixes = params.prefixes || cRDFPrefixes;
  if (params.prefixes) {
    for (var prefix in params.prefixes) {
      if (query.indexOf(prefix + ":") != -1) {
        query = "PREFIX " + prefix + ": <" + params.prefixes[prefix] + "> " + query;
      }
    }
  }
  ddebug("Running SPARQL query: " + query);
  loadURL({
    url : url,
    urlArgs : {
      query : query,
      format : "application/sparql-results+json",
      output : "json",
      //callback : "load",
    },
    dataType : "json",
  }, null, function(json) {
    try {
      if (json.results.bindings.length == 0) {
        errorCallback(new SPARQLException(new ServerException("Nothing found", 0, url), query));
        return;
      }
      // drop the .value, and make it a real Array
      var results = [];
      var bindings = json.results.bindings;
      for (var i = 0, l = bindings.length; i < l; i++) {
        var cur = bindings[i];
        var result = {};
        for (var name in cur) {
          result[name] = cur[name].value;
        }
        results.push(result);
      }
      resultCallback(results);
    } catch (e) { errorCallback(e); }
  }, function(e) {
    errorCallback(new SPARQLException(e, query));
  });
}

function sparqlSelect1(query, params, resultCallback, errorCallback) {
  var myResultCallback = function(results) {
    resultCallback(results[0]);
  };
  query += " LIMIT 1";
  sparqlSelect(query, params, myResultCallback, errorCallback);
}

