/**
 * The data format "GEDCOM" is a common format for genealogy data.
 *
 * Call load()
 *
 * Data format:
 * It is a line-based text file describing objects.
 * The number at the beginning is the hierarchy level. I.e. 0 is a top level entry.
 * 1 is a detail of the top level entry. 2 is a detail of 1. and so on.
 * E.g.:
 * ...
 * 0 @I0001@ INDI (entry of type Person with ID "I0001" starts)
 * 1 NAME Eve// (full name)
 * 2 GIVN Eve (first name)
 * 1 NAME Eva// (full name, in another language)
 * 2 GIVN Eva (first name, in another language)
 * 1 SEX F (gender)
 * 1 FAMS @F0044@ (family, referenced by ID, look this up)
 * 1 SOUR @S0013@ (source, referenced by ID)
 * 1 SOUR @S0014@ (another source, referenced by ID)
 * 1 CHAN (changed, last modified)
 * 2 DATE 7 JUL 2008 (time this entry was modified)
 * 3 TIME 23:38:05 (time this entry was modified)
 * 0 @I0000@ INDI (new entry of type Person)
 * 1 NAME Adam// (full name)
 * 1 SEX M (gender)
 * 1 BIRT (birth)
 * 2 TYPE A+0 (type of date is: A (=Adam born, I defined 'A' like that) plus 0 years)
 * 2 DATE 0 (year 0)
 * 1 DEAT (death)
 * 2 TYPE A+930 (930 years after Adam was born)
 * 2 DATE @#DHEBREW@ 930 (Again, I re-defined "hebrew" to be 0 = Adam born)
 * ...
 *
 * Implementation:
 * 1. We parse the objects
 * 2. When we see a higher number, we drill into the object.
 *     When we see a lower number, them we return and unwind.
 * 3. For each foreign ID we encounter, we create a Relationship object,
 *    hook it up to the subject, but not the obj, because we don't necessarily
 *    have parsed the obj yet. Thus, we store the reference and ID in
 *    |needReference|, and look them up at the end after parsing.
 */
function GEDCOMParser(text) {
  this._needReference = [];
  this._storage = new RAMStorage();
}
GEDCOMParser.prototype = {
  // file contents, as object tree
  // GEDCOM object {
  //    type {String}
  //    data {String}
  //    details { Array of GEDCOM objects }
  // }
  _ged : null,
  _needReference : null, // { Array of { rel {Relation}, id {String} (id of obj, e.g. "S0032") } Stores a list of IDs that we need to look up after parsing everything.
  _storage : null, // the result

  /**
   * Convenience function to load the file from a URL,
   * parse it and return the DB.
   * @param url {String}
   * @param successCallback {Function(db {Storage})}
   * @param errorCallback
   */
  loadFromURL : function(url, successCallback, errorCallback) {
    var self = this;
    loadURL(url, "text", function(text) {
      successCallback(self.load(text));
    }, errorCallback);
  },

  /**
   * Entry function to parse
   * @param text {String}   GEDOM file contents
   * @returns {Storage}   DB with the objects
   */
  load : function(text) {
    this.parseText(text);
    return this.parseAllObjects();
  },

  /**
   * parse lines into a hierarchy of GEDCOM objects
   * @param text {String}   GEDOM file contents
   */
  parseText : function(text) {
    this._ged = {
      type : "WHOLEFILE",
      data : null,
      details : [],
    }
    // stores currently open object on a given level
    // index = level, value = GEDCOM object
    var levelObjs = [];
    levelObjs[-1] = this._ged;

    var linesText = text.split("\n");
    for (var i = 0; i < linesText.length; i++) {
      var line = linesText[i].trim();
      if ( !line) {
        continue;
      }
      var fields = line.split(" ", 2); // first 2 fields
      fields[2] = line.substr(fields[0].length + fields[1].length + 2); // rest of line
      if ( !fields[0] || !fields[1]) {
        throw "Invalid line in GEDCOM file: " + line;
      }
      var level = parseInt(fields[0]);
      var ged = {
        type : fields[1], // e.g. NAME
        data : fields[2], // e.g. "Fred//Flintstone"
        details : [], // e.g. GIVN
      };
      assert(level >= 0, "Invalid level number in line: " + line);
      levelObjs[level] = ged;
      levelObjs[level - 1].details.push(ged);
    }
  },

  /**
   * This drives the actual parsing
   * @returns {Storage}
   */
  parseAllObjects : function() {
    var ged = this._ged;
    //console.log(dumpObject(ged, "ged"));
    assert(ged.type == "WHOLEFILE");
    for (var i = 0; i < ged.details.length; i++) {
      //console.log("parsing detail " + ged.details[i].type + " " + ged.details[i].data);
      this.parseObject(ged.details[i]);
    }
    this.lookupRelations();
    return this._storage;
  },

  /**
   * parses top-level object
   */
  parseObject : function(ged) {
    if (ged.type == "HEAD") {
      this.parseHead(ged);
      return;
    }
    switch (ged.data) {
    case "INDI":
      this.parsePerson(ged);
      break;
    case "SOUR":
      this.parseSource(ged);
      break;
    case "FAM":
      this.parseFamily(ged);
      break;
    case "NOTE":
      this.parseNote(ged);
      break;
    case "SUBM":
      // author - part of header
      // skip
      break;
    }
  },

  /**
   * parses Person object
   */
  parsePerson : function(ged) {
    assert(ged.data == "INDI");
    var person = new Person();
    person.id = ged.type;
    this.ensureID(person);
    for (var i = 0; i < ged.details.length; i++) {
      var gedSub = ged.details[i];
      switch (gedSub.type) {
      case "NAME": // change = last modified
        this.parsePersonName(gedSub, person);
        break;
      case "SEX": // gender
        person.male = gedSub.data == "M";
        break;
      case "FAMS": // parent of family
        //this.relationFamily(gedSub.data, person, person.male ? "father" : "mother");
        // will be parsed as part of family object
        break;
      case "FAMC": // child of family
        //this.relationFamily(gedSub.data, person, "child");
        // will be parsed as part of family object
        break;
      case "BIRT": // birth event
      case "DEAT": // death event
        var birth = gedSub.type == "BIRT";
        var event = new Event();
        event.id = "E" + (birth ? "birth" : "death") + person.id;
        this.ensureID(event);
        event.name = person.name + (birth ? " born" : " died");
        this.parseEventDetails(gedSub, event);
        this.makeRelation(person, event, birth ? "birth" : "death");
        this._storage.add(event);
        break;
      case "SOUR": // reference to source
        var sourceID = gedSub.data;
        this.makeRelationByID(person, sourceID, "source");
        break;
      }
    }
    this._storage.add(person);
    return person;
  },

  parseEventDetails : function(ged, event) {
    for (var i = 0; i < ged.details.length; i++) {
      var gedSub = ged.details[i];
      switch (gedSub.type) {
      case "DATE":
        event.time = this.parseDate(gedSub);
        break;
      case "SOUR": // reference to source
        var sourceID = gedSub.data;
        this.makeRelationByID(event, sourceID, "source");
        break;
      }
    }
  },

  parseDate : function(ged) {
    assert(ged.type == "DATE");
    var dateSplit = ged.data.split(" ");
    baseYear = 0;
    var calendar = dateSplit.filter(function(a) { return a[0] == "@"; })[0];
    if (calendar == "@#DHEBREW@") {
      baseYear = -4026; // Adam born
    }
    var year = parseInt(dateSplit[dateSplit.length - 1]); // last part
    if (isNaN(year)) {
      //console.log("parsing date " + ged.data);
      return null;
    }
    year += baseYear;
    var date = new Date(year, 1, 1);
    return date;
  },

  /**
   * parses ged FAM object
   */
  parseFamily : function(ged) {
    assert(ged.data == "FAM");
    var rel = new Relation();
    rel.id = ged.type;
    this.ensureID(rel);
    var father;
    var mother;
    var children = [];
    for (var i = 0; i < ged.details.length; i++) {
      var gedSub = ged.details[i];
      switch (gedSub.type) {
      case "HUSB": // husband = father
        father = this._storage.getID(gedSub.data);
        break;
      case "WIFE": // mother
        mother = this._storage.getID(gedSub.data);
        break;
      case "CHIL": // child
        children.push(this._storage.getID(gedSub.data));
        break;
      case "CHAN": // last modified
        // skip
        break;
      }
    }
    if (mother && father) {
      this.makeRelation(father, mother, "married");
      this.makeRelation(mother, father, "married");
    }
    for (var i = 0; i < children.length; i++) {
      var child = children[i];
      if (father) {
        this.makeRelation(child, father, "father");
        this.makeRelation(father, child, "child");
      }
      if (mother) {
        this.makeRelation(mother, child, "child");
        this.makeRelation(child, mother, "mother");
      }
    }
  },

  /**
   * parses Source object at the current line and the following lines
   */
  parseSource : function(ged) {
    assert(ged.data == "SOUR");
    var source = new Source();
    source.id = ged.type;
    this.ensureID(source);
    for (var i = 0; i < ged.details.length; i++) {
      var gedSub = ged.details[i];
      switch (gedSub.type) {
      case "TITL": // title = name
        source.name = gedSub.data;
        break;
      case "CHAN": // change = last modified
        // skip
        break;
      }
    }

    // Check whether this is a bible text - TODO move into bibletext.js ?
    try {
      var bibletext = new BibleText(source.name);
      bibletext.descr = source.descr;
      bibletext.id = source.id;
      source = bibletext;
    } catch (e) {} // console.log("Could not parse source as bible text: " + e); }

    this._storage.add(source);
    return source;
  },

  /**
   * parses Person object at the current line and the following lines
   */
  parsePersonName : function(ged, person) {
    assert(ged.type == "NAME");
    // assuming that we'll get GIVN etc, we ignore ged.data here
    for (var i = 0; i < ged.details.length; i++) {
      var gedSub = ged.details[i];
      switch (gedSub.type) {
      case "GIVN": // given name = first name
        person.firstName = gedSub.data;
        break;
      case "SURN": // surname = last name
        person.lastName = gedSub.data;
        break;
      }
    }
    var name = person.firstName +
        (person.lastName ? " " + person.lastName : "");
    if (person.name) {
      person.otherNames.push(name);
    } else {
      person.name = name;
    }
  },

  /**
   */
  parseNote : function(ged) {
  },

  /**
   * parses the file header with meta-info
   */
  parseHead : function(ged) {
  },

  _lastID : 0,
  getAutoID : function() {
    return ++this._lastID + "";
  },
  ensureID : function(detail) {
    if ( !detail.id) {
      detail.id = "autoID-" + this.getAutoID();
    }
  },

  /**
   * Create a relation object and hook it up
   */
  makeRelation : function(subj, obj, type) {
    var rel = new Relation();
    rel.subj = subj;
    rel.obj = obj;
    rel.type = type;
    rel.add();
    rel.opposite().add();
  },

  /**
   * Store a relation of which only the ID of the obj is known
   * @param subj {Detail}
   * @param objID {String} ID
   * @param type {String} role
   */
  makeRelationByID : function(subj, objID, type) {
    var rel = new Relation();
    rel.subj = subj;
    rel.type = type;
    this._needReference.push({ rel: rel, objID : objID});
  },

  lookupRelations : function() {
    for (var i = 0; i < this._needReference.length; i++) {
      var rel = this._needReference[i].rel;
      var objID = this._needReference[i].objID;
      var obj = this._storage.getID(objID);
      if ( !obj) {
        console.log("referenced " + objID + " not found");
        continue;
      }
      rel.obj = obj;
      rel.add();
      rel.opposite().add();
    }
  },
}
