/*------------------------------------------*\
InsertNote plugin for Xinha
___________________________
     
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 3 of the License, or (at
your option) any later version.

This program is distributed in the hope that it will be useful, 
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License (at http://www.gnu.org/licenses/lgpl.html) 
for more details.

InsertNote
----------
Allows for the insertion of footnotes/endnotes. Supports:
  * Automatic numbering
  * Automatic linking to anchors
  * Links back to each specific citation
  * Saves previously referenced notes for easy reuse
  * Cleanup of unreferenced notes (or references to non-existent notes)
\*------------------------------------------*/

function InsertNote(editor)
{
  this.editor = editor;
  var cfg = editor.config;
  var self = this;

  cfg.registerButton({
    id       : "insert-note",
    tooltip  : this._lc("Insert footnote"),
    image    : editor.imgURL("insert-note.gif", "InsertNote"),
    textMode : false,
    action   : function() {
      self.show();
    }
  });
  cfg.addToolbarElement("insert-note", "createlink", 1);
};

InsertNote._pluginInfo = {
  name          : "InsertNote",
  version       : "0.4",
  developer     : "Nicholas Bergson-Shilcock",
  developer_url : "http://www.nicholasbs.com",
  c_owner       : "Nicholas Bergson-Shilcock",
  sponsor       : "The Open Planning Project",
  sponsor_url   : "http://topp.openplans.org",
  license       : "LGPL"
};

InsertNote.prototype.onGenerateOnce = function ()
{
  this.notes = {};
  this.noteNums = {};
  this.ID_PREFIX = "InsertNoteID_"; // This should suffice to be unique...
  this.MARKER_SUFFIX = "_marker";
  this.MARKER_CLASS = "InsertNoteMarker";
  this.LINK_BACK_SUFFIX = "_LinkBacks";
  this.NOTE_LIST_ID = "InsertNote_NoteList";

  this._prepareDialog();
};

InsertNote.prototype._prepareDialog = function()
{
  var self = this;
  var editor = this.editor;

  if (!this.html)
  {
    Xinha._getback(Xinha.getPluginDir("InsertNote") + "/dialog.html",
                   function(getback) { 
                     self.html = getback;
                     self._prepareDialog();
                   });
    return;
  }

  this.dialog = new Xinha.Dialog(editor, this.html, "InsertNote", 
                                 {width: 400,
                                  closeOnEscape: true,
                                  resizable: false,
                                  centered: true,
                                  modal: true
                                 });

  this.dialog.getElementById("ok").onclick = function() {self.apply();};
  this.dialog.getElementById("cancel").onclick = function() {self.dialog.hide();};
  this.dialog.getElementById("noteMenu").onchange = function() {
    var noteId = this.options[this.selectedIndex].value;
    var text = self.notes[noteId] || "";
    var textArea = self.dialog.getElementById("noteContent")
    textArea.value = text;
    if (text) {
      textArea.disabled = true;
    } else {
      textArea.disabled = false;
      textArea.focus();
    }
  };

  this.ready = true;
};

InsertNote.prototype.show = function()
{
  if (!this.ready)
  {
    var self = this;
    window.setTimeout(function() {self.show();}, 80);
    return;
  }
  
  var editor = this.editor;

  // All of the Xinha dialogs are part of the main document, not the editor
  // document, so we have to use the correct document reference to modify them.
  var doc = document;

  this.repairNotes();
  
  // Can't insert footnotes inside of other footnotes...
  if (this.cursorInFootnote()) 
  {
    alert("Footnotes cannot be inserted inside of other footnotes.");
    return
  }

  this.dialog.show();

  var popupMenu = this.dialog.getElementById("noteMenu");
  while (popupMenu.lastChild.value != "new")
  {
    // Remove all menu options except "New note"
    popupMenu.removeChild(popupMenu.lastChild);
  }
  var temp, displayName, n;
  var orderedIds = this._getOrderedNoteIds();
  for (var i=0; i<orderedIds.length; i++)
  {
    n = orderedIds[i];
    temp = doc.createElement("option");
    temp.setAttribute("value", n);
    displayName = this.notes[n];
    displayName = displayName.replace(/<[^>]*>/gi, ""); // XXX TODO: Strip HTML less naively.
    if (displayName.length > 50) // Truncate preview text to 50 chars
    {
      displayName = displayName.substr(0,50) + "...";
    }
    displayName = this._getNumFromNoteId(n) + ". " + displayName;
    temp.appendChild(doc.createTextNode(displayName));
    popupMenu.appendChild(temp);
  }

  var textArea = this.dialog.getElementById("noteContent");
  textArea.disabled = false;
  textArea.focus();
  textArea.value = "";
};

// Checks to see whether or not the cursor is placed inside of a footnote
InsertNote.prototype.cursorInFootnote = function(ignoreMarkers)
{
  var ancestors = this.editor.getAllAncestors();
  for (var i=0; i<ancestors.length; i++) 
  {
    if (ancestors[i].id == this.NOTE_LIST_ID)
      return true;
    if (ancestors[i].className == this.MARKER_CLASS && !ignoreMarkers)
      return true;
  }
  
  return false;
}

/*  This function makes sure all of the notes are consistent.
    Specifically:
       * If a note exists but is never referred to, that note is removed
       * Conversely, if a note is referred to but does not exist, the 
         references are removed
       * If the markup for a note marker exists but contains no visible
         text, the markup is removed
       * All numbering is updated as follows:
          - the first note is numbered 1
          - each note thereafter is numbered sequentially, unless it has
            already been referenced, in which case it's given the same
            number as it was the first time it was referenced.
       * The note text stored in the this.notes hash is updated to reflect
         any changes made by the user (so if the user changes the 
         citation body in Xinha we don't overwrite these changes next
         time we update things)
       * The noteNums array is regenerated so as to be up to date

*/
InsertNote.prototype.repairNotes = function() 
{
  var self = this;
  var note;
  var markers;
  var marker;
  var isEmpty;
  var temp;
  var doc = this.editor._doc;

  this.repairScheduled = false;

  // First, we remove any markers if they reference a note that doesn't exist.
  // If the note does exist, we make sure we have a reference to it by
  // calling this._saveNote()
  markers = this._getMarkers();
  for (var i=0; i<markers.length; i++)
  {
    marker = markers[i];

    // Remove empty markers
    isEmpty = false;
    if (marker) // check if anchor is empty
      isEmpty = marker.innerHTML.match(/^(<span[^>]*>)?(<sup>)?(<a[^>]*>)?[\s]*(<\/a>)?(<\/sup>)?(<\/span>)?$/m);
    if (isEmpty)
      marker.parentNode.removeChild(marker);


    var noteId = this._getIdFromMarkerId(marker.id);
    if (!this.notes[noteId]) 
    {
      // We don't have this note stored, let's see if 
      // it actually exists...
      note = doc.getElementById(noteId);
      if (note)
      {
        this._saveNote(note);
      } else
      {
        if (marker)
          marker.parentNode.removeChild(marker);
      }
    }
  }

  // Now we iterate through all the note ids we have stored...
  for (var id in this.notes)
  {
    // Get reference to note
    note = doc.getElementById(id);
    
    markers = this._getMarkers(id);
    if (note)
    {
      if (markers.length == 0) // Remove note if it's not referenced anywhere
      {
        // remove the note
        note.parentNode.removeChild(note);
        delete this.notes[id];
      } else 
      {
        // The note exists and *is* referenced, so we save its contents
        this._saveNote(note);
      }
    } else
    {
      // Note no longer exists. Remove any references to it.
      for (var i=0; i<markers.length; i++)
      {
        markers[i].parentNode.removeChild(markers[i]);
      }
      delete this.notes[id];
    }
  }

  // Correct marker numbering
  this._updateRefNums();

  // At this point we now have all of the note markers correctly
  // numbered, an up-to-date hash (this.noteNums) of note numbers 
  // keyed to note ids, and no stray markers or unreferenced notes.

  // Final step: Correct the numbering/order of the actual notes
  var noteList = doc.getElementById(this.NOTE_LIST_ID);
  var newNoteList = this._createNoteList();
  if (newNoteList)
  {
    noteList.parentNode.replaceChild(newNoteList, noteList);
  } else
  {
    if (noteList)
      noteList.parentNode.removeChild(noteList);
  }
};

InsertNote.prototype._saveNote = function(note)
{
  var doc = this.editor._doc;
  // Before saving a note's contents, we do two things:
  // 1) we remove the markup we inserted so we don't end up with duplicate links
  // 2) check to see if the note is empty, and if so, we remove it
  var temp = doc.getElementById(this._getLinkBackSpanId(note.id));
  if (temp)
    temp.parentNode.removeChild(temp);
  if (note.innerHTML)
  {
    this.notes[note.id] = note.innerHTML;
  } else
  {
    // If we're here then the note is empty,
    // so we delete it and any markers for it.
    delete this.notes[note.id];
    markers = this._getMarkers(note.id);
    for (var i=0; i<markers.length; i++)
      markers[i].parentNode.removeChild(markers[i]);
  }
};

InsertNote.prototype._updateRefNums = function()
{
  var runningCount = 1; // first reference is 1
  var num;
  var marker;
  var noteId;

  // Reset all note numbering...
  this.noteNums = {};
  
  var markers = this._getMarkers();

  for (var i=0; i<markers.length; i++)
  {
    marker = markers[i];
    noteId = this._getIdFromMarkerId(marker.id);
    if (this.noteNums[noteId])
    {
      num = this.noteNums[noteId];
    } else
    {
      this.noteNums[noteId] = runningCount;
      num = runningCount++;
    }
    // span -> sup -> anchor
    marker.firstChild.firstChild.innerHTML = num;
    // XXX TODO: don't use firstChild.firstChild?
  }
};

InsertNote.prototype.apply = function()
{
  var editor = this.editor;
  var param = this.dialog.hide();
  var noteId = param['noteMenu'].value;
  var newNote = (noteId == "new");
  var noteContent = param['noteContent'];
  var doc = editor._doc;

  if (newNote) // Inserting a new note
    noteId = this._getNextId(this.ID_PREFIX);

  this.notes[noteId] = noteContent;

  var markerTemplate = this._createNoteMarker(noteId)
  editor.insertNodeAtSelection(markerTemplate);
  var marker = doc.getElementById(markerTemplate.id);
  
  var currentNoteList = doc.getElementById(this.NOTE_LIST_ID);
  var newNoteList = this._createNoteList();
  var body = doc.body;
  if (currentNoteList)
  {
    body.replaceChild(newNoteList, currentNoteList);
  } else
  {
    body.appendChild(newNoteList);
  }

  var newel = doc.createTextNode('\u00a0');
  if (marker.nextSibling)
  {
    newel = marker.parentNode.insertBefore(newel, marker.nextSibling);
  } else
  {
    newel = marker.parentNode.appendChild(newel);
  }
  this.repairNotes();
  editor.selectNodeContents(newel,false);
};

InsertNote.prototype._createNoteList = function()
{
  var doc = this.editor._doc;
  var noteList = doc.createElement("ol");
  noteList.id = this.NOTE_LIST_ID;
  var orderedIds = this._getOrderedNoteIds();
  if (orderedIds.length == 0)
    return null;
  var note, id;
  for (var i=0; i<orderedIds.length; i++)
  {
    id = orderedIds[i];
    note = this._createNoteNode(id, this.notes[id]);
    noteList.appendChild(note);
  }
  return noteList;
};

InsertNote.prototype._createNoteMarker = function(noteId)
{
  // Create the note marker, i.e., the # superscript
  var doc = this.editor._doc;
  var noteNum = this._getNumFromNoteId(noteId);

  var link = doc.createElement("a");
  link.href = "#" + noteId;
  link.innerHTML = noteNum;
  
  var superscript = doc.createElement("sup");
  superscript.appendChild(link);

  var span = doc.createElement("span");
  span.id = this._getNextMarkerId(noteId);
  span.className = this.MARKER_CLASS;
  span.appendChild(superscript);

  return span
};

InsertNote.prototype._createNoteNode = function(noteId, noteContent)
{
  var doc = this.editor._doc;

  var anchor;
  var superscript = doc.createElement("sup");
  var markers = this._getMarkers(noteId);
  var temp;
  for (var i=0; i<markers.length; i++)
  {
    anchor = doc.createElement("a");    
    if (markers.length == 1)
    {
      anchor.innerHTML = "^";
    }
    else
    {
      anchor.innerHTML = this._letterNum(i);
    }
    anchor.href = "#" + markers[i].id;
    superscript.appendChild(anchor);
    if (i < markers.length-1)
    {
      temp = doc.createTextNode(", ");
      superscript.appendChild(temp);
    }
  }

  var span = doc.createElement("span");
  span.id = this._getLinkBackSpanId(noteId);
  span.appendChild(superscript);

  var li = doc.createElement("li");
  li.id = noteId;
  li.innerHTML = noteContent;
  li.appendChild(span);

  return li;
};

InsertNote.prototype._getNextId = function(prefix)
{
  // We can't just use Xinha.uniq here because it doesn't
  // work across editing sessions. E.g., it will break if 
  // the user copies the html from one editor to another.
  var id = Xinha.uniq(prefix);
  while (this.editor._doc.getElementById(id))
    id = Xinha.uniq(prefix);
  return id;
};

InsertNote.prototype._getOrderedNoteIds = function()
{
  var self = this;
  var orderedIds = new Array();
  for (var id in this.notes)
    orderedIds.push(id);
  
  orderedIds.sort(function(a,b) {
    var n1 = self._getNumFromNoteId(a);
    var n2 = self._getNumFromNoteId(b);

    return (n1 - n2);
  });

  return orderedIds;
};

InsertNote.prototype._getNumFromNoteId = function(noteId)
{
  if (!this.noteNums[noteId])
    return 0;
  return this.noteNums[noteId];
};


// Return array of all markers that reference the specified note.
// If noteId is not supplied, *all* markers are returned.
InsertNote.prototype._getMarkers = function(noteId)
{
  var doc = this.editor._doc;
  var markers = Xinha.getElementsByClassName(doc, this.MARKER_CLASS);
  
  if (!noteId)
    return markers;
  
  var els = new Array();
  for (var i=0; i<markers.length; i++)
  {
    if (this._getIdFromMarkerId(markers[i].id) == noteId)
    {
      els.push(markers[i]);
    }
  }
  return els;
};

InsertNote.prototype._getNextMarkerId = function(noteId)
{
  return this._getNextId(noteId + this.MARKER_SUFFIX);
};

InsertNote.prototype._getIdFromMarkerId = function(markerId)
{
  return markerId.substr(0, markerId.search(this.MARKER_SUFFIX));
};

InsertNote.prototype._getLinkBackSpanId = function(noteId)
{
  return noteId + this.LINK_BACK_SUFFIX;
};

InsertNote.prototype._lc = function(string)
{
  return Xinha._lc(string, "InsertNote");
};

// Probably overkill, but it does the job
InsertNote.prototype._letterNum = function(num)
{
  var letters = "abcdefghijklmnopqrstuvwxyz";
  var len = Math.floor(num/letters.length) + 1;
  var s = "";
  for (var i=0; i<len; i++)
    s += letters.substr(num % letters.length,1);
  return s;
};

InsertNote.prototype.inwardHtml = function(html)
{
  return html;
};

InsertNote.prototype.outwardHtml = function(html)
{
  this.repairNotes();
  return this.editor.getHTML();
};

InsertNote.prototype.onKeyPress = function (event)
{
  // This seems a bit hacky, but I don't presently see
  // a better way.
  if (event.keyCode == 8 || event.keyCode == 46)
  {
    var self = this;
    if (!this.repairScheduled && !this.cursorInFootnote(true)) // ignore if in a footnote
    {
      this.repairScheduled = true;
      window.setTimeout(function() { self.repairNotes(); }, 1000);
    }
  }
  return false;
};

/**
 * 
 * @param {String} mode either 'textmode' or 'wysiwyg'
 */
InsertNote.prototype.onMode = function (mode)
{
  return false;
};

/**
 * 
 * @param {String} mode either 'textmode' or 'wysiwyg'
 */
InsertNote.prototype.onBeforeMode = function (mode)
{
  return false;
};
