// copyright (c) 2006-2008 Franz Inc, Oakland, CA - All rights reserved.
//
// The software, data and information contained herein are proprietary
// to, and comprise valuable trade secrets of, Franz, Inc.  They are
// given in confidence by Franz, Inc. pursuant to a written license
// agreement, and may be stored and used only in accordance with the terms
// of such license.
//
// Restricted Rights Legend
// ------------------------
// Use, duplication, and disclosure of the software, data and information
// contained herein by any agency, department or entity of the U.S.
// Government are subject to restrictions of Restricted Rights for
// Commercial Software developed at private expense as specified in
// DOD FAR Supplement 52.227-7013 (c) (1) (ii), as applicable.

// Functionality related to the query interface.

// The query store (queries and queryMap variables) keep track of
// recent queries in order to make back/forward work. Uses the
// historyStorage from RSH, so needs to be initialised after that.
function initQueryStore() {
  window.queries = historyStorage.get("queries") || [];
  window.queryMap = {};
  for (var i = 0; i < queries.length; i++) {
    var query = queries[i];
    queryMap[query.type + query.text] = i;
  }
}

// Add or lookup a query in the query store. Returns a numeric
// identifying this query.
function storeQuery(type, query, title) {
  var known = queryMap[type + query];
  if (known != null) return known;

  if (queries.length >= 100) {
    var del = queries.shift();
    delete queryMap[del.type + del.text];
  }

  var id = (queries.length ? queries[queries.length - 1].id + 1 : 0);
  queries.push({text: query, type: type, title: title, id: id});
  historyStorage.put("queries", queries);
  queryMap[type + query] = id;
  return id;
}

// Display function for #query/NUM
function showQuery(num) {
  var found = queries[num];
  if (num == null)
    doShowQuery("SPARQL", "", null, user && user.name);
  else if (!found)
    goTo("query");
  else
    doShowQuery(found.type, found.text, found.title, user && user.name);
}

function refreshQueryPage() {
  var result = $("query-results");
  if (result) {
    if (result.showing) result.showing();
    else result.innerHTML = "";
  }
}

// Helper for the various things that need to display a query.
function doShowQuery(type, query, title, userName, namespaces) {
  Mold.cast(getPage(), template("query/input"), {
    query: query,
    type: type,
    title: title,
    user: userName,
    namespaces: namespaces
  });
  customRefresh = refreshQueryPage;

  if (query) {
    storeQuery(type, query, title);
    loadQueryResults(type, query, userName);
  }
}

// Load and show the results of a query in the DIV with id
// query-results.
function loadQueryResults(type, query, user) {
  var results = $("query-results");
  results.showing = partial(loadQueryResults, type, query, user);
  Mold.cast(results, template("instruction/loading"));
  fetchJSON("query/" + type.toLowerCase(), [query, 0, 500, user, useReasoning], loaded, failed);
  function loaded(data) {
    Mold.cast(results, template("query/results"));
    var templ = data.verb == "ask" ? "query/bool" : "query/table";
    Mold.cast($("query-result-space"), template(templ), {query: query, type: type, user: user, result: data});
  }
  function failed(message) {
    results.innerHTML = "";
    showMessage(message, true, true);
  }
}

function guessLanguage(type, query) {
  if (!siteInfo.prolog) return type;
  var guessed = /^\s*[\(\;]/.test(query) ? "Prolog" : "SPARQL";
  var select = $("querylanguage");
  if (guessed != type && select) select.selectedIndex = (guessed == "SPARQL" ? 0 : 1);
  return guessed;
}

// Fires when the user presses the execute button. Refreshes the query
// results, updates the URL fragment.
function executeQuery(type, query) {
  if (!/^\s*$/.test(query)) {
    type = guessLanguage(type, query);
    dhtmlHistory.add("query/" + storeQuery(type, query));
    loadQueryResults(type, query, user && user.name);
  }
  else {
    showMessage("You did not enter a query.", 2, false);
  }
}

// Get another chunk of query results from the server.
function fetchMoreQueryResults(query, ok, fail) {
  fetchJSON("query/" + query.type.toLowerCase(), [query.query, query.result.more, 500, query.user, useReasoning], loaded, failed);
  function loaded(data) {
    query.result.result.concat(data.result);
    query.result.more = data.more;
    ok && ok(data);
  }
  function failed(message) {
    failMessage("Loading more results", message);
    fail && fail(message);
  }
}

// Display more query results in the table result view.
function loadMoreQueryResults(linkCell, query) {
  Mold.cast(linkCell, template("query/loadingmore"));
  fetchMoreQueryResults(query, loaded, failed);

  function loaded(data) {
    var tbody = linkCell.parentNode.parentNode;
    Mold.cast(addTBodyBefore(tbody), template("query/tablerows"), data.result);
    if (data.more)
      Mold.cast(linkCell, template("query/more"), query);
    else
      removeNode(tbody);
  }
  function failed() {
    Mold.cast(linkCell, template("query/more"), query);
  }
}

// Display function for #query/db/NAME
function showDBQuery(name) {
  var found = name && findIf(function(q) {return q.title == name;}, dbInfo.queries);
  if (!name)
    Mold.cast(getPage(), template("query/stored"), dbInfo.queries);
  else if (found)
    doShowQuery(found.type, found.text, found.title, null, dbInfo.namespaces);
  else {
    goTo("query/db");
    showMessage("No query '" + name + "' found.", true, true);
  }
}

// User queries at #query/s/USER/NAME
function showUserQuery(userName, queryName) {
  if (user && userName == user.name) {
    if (!queryName) listMyQueries();
    else showMyQuery(queryName);
  }
  else {
    if (!queryName) listSavedQueries(userName);
    else showSavedQuery(userName, queryName);
  }
}

function listSavedQueries(userName) {
  fetchJSON("query/list", [userName], success, failure);
  Mold.cast(getPage(), template("message/loading"));
  function success(queryNames) {
    Mold.cast(getPage(), template("query/saved"), {user: userName, queries: queryNames});
  }
  function failure(message) {
    clearPage();
    showMessage(message, true, true);
  }
}

function listMyQueries() {
  Mold.cast(getPage(), template("query/mysaved"), user.queries);
}

function showMyQuery(queryName) {
  var query = findIf(function(q){return q.title == queryName;}, user.queries);
  if (query)
    doShowQuery(query.type, query.text, queryName, user.name);
  else
    showMessage("No query '" + queryName + "' found.", true, true);
}

function showSavedQuery(userName, queryName) {
  fetchJSON("query/lookup", [userName, queryName], success, failure);
  Mold.cast(getPage(), template("message/loading"));
  function success(data) {
    doShowQuery(data.type, data.text, queryName + " (" + userName + ")", userName, data.namespaces);
  }
  function failure(message) {
    clearPage();
    showMessage(message, true, true);
  }
}

function showRecent() {
  var recent = [], lowest = Math.max(0, queries.length - 20);
  for (var i = queries.length - 1; i >= lowest; i--)
    recent.push(queries[i]);
  Mold.cast(getPage(), template("query/recent"), recent);
}

// The editor is resizeable, preferred size is stored in cookie when
// adjusted.
var inputFieldHeight = Number(getCookie("inputFieldHeight")) || 200;

function initQueryInput(type, query, replace) {
  return new CodeMirror(CodeMirror.replace(replace), {
    height: inputFieldHeight + "px",
    content: query,
    initCallback: function(ed){
      if (!query) ed.focus();
      ed.setParser(type == "SPARQL" ? "SparqlParser" : "LispParser");
    }
  });
}

// Crummy drag-drop code using a 1px-high div as a drag indicator.
function resizeInput(editor, handle, event) {
  event.stop();
  var initY = event.clientY;
  var indicator = document.createElement("DIV");
  indicator.className = "dragger";
  handle.parentNode.appendChild(indicator);
  editor.frame.style.visibility = "hidden";
  handle.style.display = "none";
  var move = connect(document, "mousemove", function(event) {
    indicator.style.bottom = (initY - event.clientY) + "px";
  });
  var release = connect(document, "mouseup", function(event) {
    disconnect(move);
    disconnect(release);
    removeNode(indicator);
    editor.frame.style.visibility = handle.style.display = "";
    inputFieldHeight += event.clientY - initY;
    setCookie("inputFieldHeight", String(inputFieldHeight));
    editor.frame.style.height = inputFieldHeight + "px";
  });
}

// Saving a query means storing it in the server, updating the local
// query list, and going to that query's URL.
function saveQuery(type, query, name, shared) {
  if (/^\s*$/.test(query)) return;
  type = guessLanguage(type, query);
  var saving = $("saving"), shared = !!shared, title = $("query-title");
  saving.style.display = "";
  postJSON("query/store", [type, name || null, query, shared], success, failure);
  function success(data) {
    saving.style.display = "none";
    user.queries = filter(function(q) {return q.title != data;}, user.queries);
    user.queries.push({title: data, text: query, type: type, shared: shared});
    if (title.parentNode) {
      title.innerHTML = Mold.escapeHTML(data);
      dhtmlHistory.add("query/s/" + user.name + "/" + data);
    }
  }
  function failure(message) {
    saving.style.display = "none";
    failMessage("Saving query", message);
  }
}

function shareQuery(query, shared) {
  postJSON("query/store", [query.type, query.title, query.text, shared], success, failure);
  function success() {
    query.shared = shared;
    refresh();
  };
  function failure(message) {
    failMessage(capitalise((shared ? "" : "un") + "sharing query"), message);
  }
}

function deleteQuery(query) {
  postJSON("query/delete", [query.title], success, failure);
  function success() {
    user.queries = filter(function(q) {return q.title != query.title;}, user.queries);
    refresh();
  }
  function failure(message) {
    failMessage("Deleting query", message);
  }
}

// Display a Google map with the geospatial literals in query result
// set.
function showQueryMap(query, node) {
  // Rows with geospatial data.
  var rows = query.result.result, points = [];
  // Need to know the area in which the points occur in order to show
  // the relevant piece of earth. (Assumes the area isn't around the
  // -180 longitude.)
  var minlong = Infinity, minlat = Infinity, maxlong = -Infinity, maxlat = -Infinity;
  for (var i = 0; i < rows.length; i++) {
    var coords = null, row = rows[i];
    for (var j = 0; j < row.length; j++) {
      coords = part(row[j]).geo();
      if (coords) {
        minlong = Math.min(minlong, coords.longitude); minlat = Math.min(minlat, coords.latitude);
        maxlong = Math.max(maxlong, coords.longitude); maxlat = Math.max(maxlat, coords.latitude);
        points.push({point: coords, row: row});
        break;
      }
    }
  }
  if (!points.length) {
    showMessage("No geospatial data found.", true, true);
    return;
  }

  // The zoom factor uses some kind of logarithmic system going from 1
  // to 16, the formula for which isn't documented. But log2(360/size)
  // + 1 seems to work well. Have to take the floor because the map
  // errors out when the zoom factor isn't an integer.
  var size = Math.max(maxlat - minlat, maxlong - minlong);
  var scale = size == 0 ? 13 : Math.min(Math.log(360 / size) / Math.LN2 + 1, 16);
  var map = showGMap(node, new GLatLng((minlat + maxlat) / 2, (minlong + maxlong) / 2), Math.floor(scale));
  for (var i = 0; i < points.length; i++) {
    var label = [], row = points[i].row, cs = points[i].point;
    for (var j = 0; j < row.length; j++)
      label.push(query.result.names[j] + ": " + part(row[j]).shortName);
    map.addOverlay(new GMarker(new GLatLng(cs.latitude, cs.longitude), {title: label.join(" | ")}));
  }
}
