// Dissecting part/node strings, expanding and collapsing namespaces.

// Currently active namespaces are kept as global state. This is
// changed when a user logs in or out, or modifies his/her namespaces.
var NameSpaces = {
  known: [],

  // Namespaces sets are arrays of {abbr, uri} objects.
  set: function(namespaces) {
    this.known = namespaces;
    clearPartCache();
  },
  get: function() {
    return this.known;
  },

  // Finds a matching namespace for a url, or false if none found.
  match: function(url) {
    var longest = -1, found = false;
    forEach(this.known, function(ns) {
      if (ns.uri.length > longest && url.indexOf(ns.uri) == 0) {
        longest = ns.uri.length;
        found = ns;
      }
    });
    return found;
  },
  // Abbreviate a URL, returns null if no namespace found.
  abbreviate: function(url) {
    var ns = this.match(url);
    return ns ? ns.abbrev + ":" + url.slice(ns.uri.length) : null;
  },
  // Take a part string that might contain an abbreviated URI or type,
  // and restore the full URI. Return null if the string could not be
  // parsed, or the string itself if it contains no abbreviations.
  restore: function(string) {
    if (string.charAt(0) == "<") return string;
    else if (string.charAt(0) == "\"") {
      var match = string.match(/^(\"(?:.|\n|\r)*[^\\]\"|"")(?:\^\^([\d\w_-]*):(.*)|@.+|\^\^<.+>)?$/);
      if (!match) return null;
      if (!match[2] || match[2] == "_") return string;
      var ns = findIf(function(ns){return ns.abbrev == match[2];}, this.known);
      return ns ? match[1] + "^^<" + ns.uri + match[3] + ">" : null;
    }
    else {
      var match = string.match(/^(.*):(.+)$/);
      if (!match) return null;
      if (match[1] == "_") return string;
      var ns = findIf(function(ns){return ns.abbrev == match[1];}, this.known);
      return ns ? "<" + ns.uri + match[2] + ">" : null;
    }
  }
};

function abbrevURL(url) {
  var lastSep = Math.max(url.lastIndexOf("/"), url.lastIndexOf("#"));
  if (lastSep == url.length - 1)
    lastSep = Math.max(url.lastIndexOf("/", lastSep - 1), url.lastIndexOf("#", lastSep - 1));
  return url.slice(lastSep + 1);
}

// Objects used to represent parts. Decomposes the part string (url,
// lit, type, lang properties) and produces a shortName property.
var Part = Prototype.extend({
  construct: function(string) {
    this.string = this.identity = string;
    var match;
    if (match = string.match(/^<(.+)>$/)) {
      this.url = match[1];
      var abbrev = NameSpaces.abbreviate(this.url);
      if (showLongParts)
        this.shortName = this.string;
      else if (abbrev == null)
        this.shortName = abbrevURL(this.url);
      else
        this.shortName = this.identity = abbrev;
    }
    else if (match = string.match(/^("(?:.|\n|\r)*[^\\]"|"")(?:\^\^<(.+)>|@(.+))?$/)) {
      this.lit = match[1];
      this.type = match[2];
      this.lang = match[3];
      this.shortName = showLongParts ? this.string : this.lit;
      var abbrev = this.type && NameSpaces.abbreviate(this.type);
      if (abbrev)
        this.identity = "\"" + this.lit + "\"^^" + abbrev;
    }
    else if (match = string.match(/^_:(.+)$/)) {
      this.anon = match[1];
      this.shortName = string;
    }
    else {
      window.failed = string;
      throw new Error("Unrecognised part: '" + string + "'");
    }
  },

  // Check whether this part is an AllegroGraph-style geospatial
  // coordinate pair, and return a {latitude, longitude} object if it
  // is.
  geo: function() {
    if (this.geoData) return this.geoData;
    if (!this.lit || !this.type || !/^http:\/\/franz.com\/ns\/allegrograph\/3\.0\/geospatial\/spherical\//.test(this.type)) return null;

    function parse(str, isLongitude) {
      // Longitude gets an extra digit, but regexps aren't flexible enough to express that sanely in one regexp.
      var match = str.match(isLongitude ? /^(\+|-)(\d\d\d)(\d\d)?(\d\d)?(\.\d+)?$/ : /^(\+|-)(\d\d)(\d\d)?(\d\d)?(\.\d+)?$/);
      if (!match) return null;
      if (match[5]) {
        if (match[4]) match[4] += match[5];
        else if (match[3]) match[3] += match[5];
        else match[2] += match[5];
      }
      var degrees = Number(match[2]);
      if (match[3]) degrees += Number(match[3]) / 60;
      if (match[4]) degrees += Number(match[4]) / 3600;
      return match[1] == "+" ? degrees : -degrees;
    }

    var match = this.lit.match(/^"((?:\+|-)[\d.]+)((?:\+|-)[\d.]+)"$/), latitude, longitude;
    if (match && (latitude = parse(match[1])) != null && (longitude = parse(match[2], true)) != null) {
      this.geoData = {longitude: longitude, latitude: latitude};
      return this.geoData;
    }
    else {
      return null;
    }
  },

  toString: function() {
    return this.string;
  }
});

// A cache of string -> part object mappings is kept to prevent
// parsing the same thing a hundred times in a row. Use part() instead
// of new Part() to when creating part objects.
var part = (function() {
  var cache = Dictionary.create(), count = 0,
      genSize = 2000, halfGen = Math.round(genSize / 2);
  window.clearPartCache = function() {cache.clear();};

  return function(string) {
    if (cache.contains(string)) {
      var found = cache.lookup(string);
      if (found.cacheNr < count - halfGen)
        found.cacheNr = count++;
    }
    else {
      var found = Part.create(string);
      cache.store(string, found);
      found.cacheNr = count++;
    }

    if (count % halfGen == 0) {
      var oldCache = cache;
      cache = Dictionary.create();
      oldCache.each(function(string, part) {
        if (part.cacheNr > count - genSize)
          cache.store(string, part);
      });
    }

    return found;
  };
})();

// Try to get a part object from a string that might contain
// abbreviated uris and might be invalid. Return null on invalid
// input.
function partFromUntrusted(string) {
  string = NameSpaces.restore(string);
  if (string == null) return null;
  try {return part(string);}
  catch(e) {return null;}
}
