// The following are instance methods and variables
var Note = Class.create({
  initialize: function(id, is_new, raw_body) {
    if (Note.debug) {
      console.debug("Note#initialize (id=%d)", id)
    }

    this.id = id
    this.is_new = is_new
    this.document_observers = [];
    
    // Cache the elements
    this.elements = {
      box: $('note-box-' + this.id),
      corner: $('note-corner-' + this.id),
      body: $('note-body-' + this.id),
      image: $('image')
    }

    // Cache the dimensions
    this.fullsize = {
      left: this.elements.box.offsetLeft,
      top: this.elements.box.offsetTop,
      width: this.elements.box.clientWidth,
      height: this.elements.box.clientHeight
    }

    // Store the original values (in case the user clicks Cancel)
    this.old = {
      raw_body: raw_body,
      formatted_body: this.elements.body.innerHTML
    }
    for (p in this.fullsize) {
      this.old[p] = this.fullsize[p]
    }

    // Make the note translucent
    if (is_new) {
      this.elements.box.setOpacity(0.2)
    } else {
      this.elements.box.setOpacity(0.5)
    }

    if (is_new && raw_body == '') {
      this.bodyfit = true
      this.elements.body.style.height = "100px"
    }

    // Attach the event listeners
    this.elements.box.observe("mousedown", this.dragStart.bindAsEventListener(this))
    this.elements.box.observe("mouseout", this.bodyHideTimer.bindAsEventListener(this))
    this.elements.box.observe("mouseover", this.bodyShow.bindAsEventListener(this))
    
    this.elements.corner.observe("mousedown", this.resizeStart.bindAsEventListener(this))
    this.elements.body.observe("mouseover", this.bodyShow.bindAsEventListener(this))
    this.elements.body.observe("mouseout", this.bodyHideTimer.bindAsEventListener(this))
    this.elements.body.observe("click", this.showEditBox.bindAsEventListener(this))

    this.adjustScale()
    
    if (Note.drag_created) {
      var t = this,
          C,
          called = false
          B = function(e) {
            if (called) {
              C()
            } else {
              called = true
              t._resize(e.pageX, e.pageY)
            }
          }
      C = function() {
        jQuery(document).unbind('mousemove', B)
      }
      
      jQuery(document).bind('mousemove', B)
    }
  },
  
  // Returns the raw text value of this note
  textValue: function() {
    if (Note.debug) {
      console.debug("Note#textValue (id=%d)", this.id)
    }

    return this.old.raw_body.strip()
  },

  // Removes the edit box
  hideEditBox: function(e) {
    if (Note.debug) {
      console.debug("Note#hideEditBox (id=%d)", this.id)
    }

    var editBox = $('edit-box')

    if (editBox != null) {
      var boxid = editBox.noteid

      $("edit-box").stopObserving()
      $("note-save-" + boxid).stopObserving()
      $("note-cancel-" + boxid).stopObserving()
      $("note-remove-" + boxid).stopObserving()
      $("note-history-" + boxid).stopObserving()
      $("edit-box").remove()
    }
  },

  // Shows the edit box
  showEditBox: function(e) {
    if (Note.debug) {
      console.debug("Note#showEditBox (id=%d)", this.id)
    }

    this.hideEditBox(e)

    var insertionPosition = Note.getInsertionPosition()
    var top = insertionPosition[0]
    var left = insertionPosition[1]
    var html = ""

    html += '<div id="edit-box" style="top: '+top+'px; left: '+left+'px; position: absolute; visibility: visible; z-index: 100; background: white; border: 1px solid black; padding: 12px;">'
    html += '<form onsubmit="return false;" style="padding: 0; margin: 0;">'
    html += '<textarea rows="7" id="edit-box-text" style="width: 350px; margin: 2px 2px 12px 2px;">' + this.textValue() + '</textarea>'
    html += '<input type="submit" value="Save" name="save" id="note-save-' + this.id + '">'
    html += '<input type="submit" value="Cancel" name="cancel" id="note-cancel-' + this.id + '">'
    html += '<input type="submit" value="Remove" name="remove" id="note-remove-' + this.id + '">'
    html += '<input type="submit" value="History" name="history" id="note-history-' + this.id + '">'
    // MI: Add Markdown notice.
    html += '<br>'
    html += '<span style="color:gray;font-size:80%;">Markdown is used for formatting. <a href="/help/notes#markdown">More info</a>'
    html += '</form>'
    html += '</div>'

    $("note-container").insert({bottom: html})
    $('edit-box').noteid = this.id
    $("edit-box").observe("mousedown", this.editDragStart.bindAsEventListener(this))
    $("note-save-" + this.id).observe("click", this.save.bindAsEventListener(this))
    $("note-cancel-" + this.id).observe("click", this.cancel.bindAsEventListener(this))
    $("note-remove-" + this.id).observe("click", this.remove.bindAsEventListener(this))
    $("note-history-" + this.id).observe("click", this.history.bindAsEventListener(this))
    $("edit-box-text").focus()
    
    Note.active_note = this
    Note.bindEditShortcuts()
  },

  // Shows the body text for the note
  bodyShow: function(e) {
    if (Note.debug) {
      console.debug("Note#bodyShow (id=%d)", this.id)
    }

    if (this.dragging) {
      return
    }

    if (this.hideTimer) {
      clearTimeout(this.hideTimer)
      this.hideTimer = null
    }

    if (Note.noteShowingBody == this) {
      return
    }

    if (Note.noteShowingBody) {
      Note.noteShowingBody.bodyHide()
    }

    Note.noteShowingBody = this

    if (Note.zindex >= 9) {
      /* don't use more than 10 layers (+1 for the body, which will always be above all notes) */
      Note.zindex = 0
      for (var i=0; i< Note.all.length; ++i) {
        Note.all[i].elements.box.style.zIndex = 0
      }
    }

    this.elements.box.style.zIndex = ++Note.zindex
    this.elements.body.style.zIndex = 10
    this.elements.body.style.top = 0 + "px"
    this.elements.body.style.left = 0 + "px"

    var dw = document.documentElement.scrollWidth
    this.elements.body.style.visibility = "hidden"
    this.elements.body.style.display = "block"
    if (!this.bodyfit) {
      this.elements.body.style.height = "auto"
      this.elements.body.style.minWidth = "140px"
      var w = null, h = null, lo = null, hi = null, x = null, last = null
      w = this.elements.body.offsetWidth
      h = this.elements.body.offsetHeight
      if (w/h < 1.6180339887) {
        /* for tall notes (lots of text), find more pleasant proportions */
        lo = 140, hi = 400
        do {
          last = w
          x = (lo+hi)/2
          this.elements.body.style.minWidth = x + "px"
          w = this.elements.body.offsetWidth
          h = this.elements.body.offsetHeight
          if (w/h < 1.6180339887) lo = x
          else hi = x
        } while ((lo < hi) && (w > last))
      } else if (this.elements.body.scrollWidth <= this.elements.body.clientWidth) {
        /* for short notes (often a single line), make the box no wider than necessary */
        // scroll test necessary for Firefox
        lo = 20, hi = w

        do {
          x = (lo+hi)/2
          this.elements.body.style.minWidth = x + "px"
          if (this.elements.body.offsetHeight > h) lo = x
          else hi = x
        } while ((hi - lo) > 4)
        if (this.elements.body.offsetHeight > h)
          this.elements.body.style.minWidth = hi + "px"
      }

      if (Prototype.Browser.IE) {
        // IE7 adds scrollbars if the box is too small, obscuring the text
        if (this.elements.body.offsetHeight < 35) {
          this.elements.body.style.minHeight = "35px"
        }

        if (this.elements.body.offsetWidth < 47) {
          this.elements.body.style.minWidth = "47px"
        }
      }
      this.bodyfit = true
    }
    this.elements.body.style.top = (this.elements.box.offsetTop + this.elements.box.clientHeight + 5) + "px"
    // keep the box within the document's width
    var l = 0, e = this.elements.box
    do { l += e.offsetLeft } while (e = e.offsetParent)
    l += this.elements.body.offsetWidth + 10 - dw
    if (l > 0)
      this.elements.body.style.left = this.elements.box.offsetLeft - l + "px"
    else
      this.elements.body.style.left = this.elements.box.offsetLeft + "px"
    this.elements.body.style.visibility = "visible"
  },

  // Creates a timer that will hide the body text for the note
  bodyHideTimer: function(e) {
    if (Note.debug) {
      console.debug("Note#bodyHideTimer (id=%d)", this.id)
    }
    this.hideTimer = setTimeout(this.bodyHide.bindAsEventListener(this), 250)
  },

  // Hides the body text for the note
  bodyHide: function(e) {
    if (Note.debug) {
      console.debug("Note#bodyHide (id=%d)", this.id)
    }

    this.elements.body.hide()
    if (Note.noteShowingBody == this) {
      Note.noteShowingBody = null
    }
  },

  addDocumentObserver: function(name, func)
  {
    document.observe(name, func);
    this.document_observers.push([name, func]);
  },

  clearDocumentObservers: function(name, handler)
  {
    for(var i = 0; i < this.document_observers.length; ++i)
    {
      var observer = this.document_observers[i];
      document.stopObserving(observer[0], observer[1]);
    }

    this.document_observers = [];
  },

  // Start dragging the note
  dragStart: function(e) {
    if (Note.debug) {
      console.debug("Note#dragStart (id=%d)", this.id)
    }

    this.addDocumentObserver("mousemove", this.drag.bindAsEventListener(this))
    this.addDocumentObserver("mouseup", this.dragStop.bindAsEventListener(this))
    this.addDocumentObserver("selectstart", function() {return false})

    this.cursorStartX = e.pointerX()
    this.cursorStartY = e.pointerY()
    this.boxStartX = this.elements.box.offsetLeft
    this.boxStartY = this.elements.box.offsetTop
    this.boundsX = new ClipRange(5, this.elements.image.clientWidth - this.elements.box.clientWidth - 5)
    this.boundsY = new ClipRange(5, this.elements.image.clientHeight - this.elements.box.clientHeight - 5)
    this.dragging = true
    this.bodyHide()
  },

  // Stop dragging the note
  dragStop: function(e) {
    if (Note.debug) {
      console.debug("Note#dragStop (id=%d)", this.id)
    }

    this.clearDocumentObservers()

    this.cursorStartX = null
    this.cursorStartY = null
    this.boxStartX = null
    this.boxStartY = null
    this.boundsX = null
    this.boundsY = null
    this.dragging = false

    this.bodyShow()
  },

  ratio: function() {
    return this.elements.image.width / this.elements.image.getAttribute("large_width")
    // var ratio = this.elements.image.width / this.elements.image.getAttribute("large_width")
    // if (this.elements.image.scale_factor != null)
      // ratio *= this.elements.image.scale_factor;
    // return ratio
  },

  // Scale the notes for when the image gets resized
  adjustScale: function() {
    if (Note.debug) {
      console.debug("Note#adjustScale (id=%d)", this.id)
    }

    var ratio = this.ratio()
    for (p in this.fullsize) {
      this.elements.box.style[p] = this.fullsize[p] * ratio + 'px'
    }
  },

  // Update the note's position as it gets dragged
  drag: function(e) {
    var left = this.boxStartX + e.pointerX() - this.cursorStartX
    var top = this.boxStartY + e.pointerY() - this.cursorStartY
    left = this.boundsX.clip(left)
    top = this.boundsY.clip(top)

    this.elements.box.style.left = left + 'px'
    this.elements.box.style.top = top + 'px'
    var ratio = this.ratio()
    this.fullsize.left = left / ratio
    this.fullsize.top = top / ratio

    e.stop()
  },

  // Start dragging the edit box
  editDragStart: function(e) {
    if (Note.debug) {
      console.debug("Note#editDragStart (id=%d)", this.id)
    }

    var node = e.element().nodeName
    if (node != 'FORM' && node != 'DIV') {
      return
    }

    this.addDocumentObserver("mousemove", this.editDrag.bindAsEventListener(this))
    this.addDocumentObserver("mouseup", this.editDragStop.bindAsEventListener(this))
    this.addDocumentObserver("selectstart", function() {return false})

    this.elements.editBox = $('edit-box');
    this.cursorStartX = e.pointerX()
    this.cursorStartY = e.pointerY()
    this.editStartX = this.elements.editBox.offsetLeft
    this.editStartY = this.elements.editBox.offsetTop
    this.dragging = true
  },

  // Stop dragging the edit box
  editDragStop: function(e) {
    if (Note.debug) {
      console.debug("Note#editDragStop (id=%d)", this.id)
    }
    this.clearDocumentObservers()

    this.cursorStartX = null
    this.cursorStartY = null
    this.editStartX = null
    this.editStartY = null
    this.dragging = false
  },

  // Update the edit box's position as it gets dragged
  editDrag: function(e) {
    var left = this.editStartX + e.pointerX() - this.cursorStartX
    var top = this.editStartY + e.pointerY() - this.cursorStartY

    this.elements.editBox.style.left = left + 'px'
    this.elements.editBox.style.top = top + 'px'

    e.stop()
  },

  _resize: function(cursorX, cursorY) {
    if (Note.debug) {
      console.debug("Note#_resize (id=%d)", this.id)
    }

    this.cursorStartX = cursorX
    this.cursorStartY = cursorY
    this.boxStartWidth = this.elements.box.clientWidth
    this.boxStartHeight = this.elements.box.clientHeight
    this.boxStartX = this.elements.box.offsetLeft
    this.boxStartY = this.elements.box.offsetTop
    this.boundsX = new ClipRange(10, this.elements.image.clientWidth - this.boxStartX - 5)
    this.boundsY = new ClipRange(10, this.elements.image.clientHeight - this.boxStartY - 5)
    this.dragging = true

    this.clearDocumentObservers()
    this.addDocumentObserver("mousemove", this.resize.bindAsEventListener(this))
    this.addDocumentObserver("mouseup", this.resizeStop.bindAsEventListener(this))
    
    this.bodyHide()
  },
  
  // Start resizing the note
  resizeStart: function(e) {
    e.stop()
    this._resize(e.pointerX(), e.pointerY())
  },

  // Stop resizing teh note
  resizeStop: function(e) {
    if (Note.debug) {
      console.debug("Note#resizeStop (id=%d)", this.id)
    }

    this.clearDocumentObservers()

    this.boxCursorStartX = null
    this.boxCursorStartY = null
    this.boxStartWidth = null
    this.boxStartHeight = null
    this.boxStartX = null
    this.boxStartY = null
    this.boundsX = null
    this.boundsY = null
    this.dragging = false
    
    e.stop()
    
    if (Note.drag_created) {
      this.old.width = this.fullsize.width
      this.old.height = this.fullsize.height
      // Note.toggled_create = false
      Note.drag_created = false
      this.showEditBox({})
    }
  },

  // Update the note's dimensions as it gets resized
  resize: function(e) {
    if (Note.debug) {
      console.debug("Note#resize (id=%d)", this.id)
    }
    var width = this.boxStartWidth + e.pointerX() - this.cursorStartX
    var height = this.boxStartHeight + e.pointerY() - this.cursorStartY
    width = this.boundsX.clip(width)
    height = this.boundsY.clip(height)
    
    this.elements.box.style.width = width + "px"
    this.elements.box.style.height = height + "px"
    var ratio = this.ratio()
    this.fullsize.width = width / ratio
    this.fullsize.height = height / ratio
    
    e.stop()
  },

  // Save the note to the database
  save: function(e) {
    if (Note.debug) {
      console.debug("Note#save (id=%d)", this.id)
    }
    
    var note = this
    for (p in this.fullsize) {
      this.old[p] = this.fullsize[p]
    }
    this.old.raw_body = $('edit-box-text').value
    this.old.formatted_body = this.textValue()
    // FIXME: this is not quite how the note will look (filtered elems, <tn>...). the user won't input a <script> that only damages him, but it might be nice to "preview" the <tn> here
    this.elements.body.update(this.textValue())

    this.hideEditBox(e)
    this.bodyHide()
    this.bodyfit = false

    var params = {
      "id": this.id,
      "note[x]": this.old.left,
      "note[y]": this.old.top,
      "note[width]": this.old.width,
      "note[height]": this.old.height,
      "note[body]": this.old.raw_body
    }

    if (this.is_new) {
      params["note[post_id]"] = Note.post_id
    }

    notice("Saving note...")

    new Ajax.Request('/note/update.json', {
      parameters: params,

      onComplete: function(resp) {
        var resp = resp.responseJSON

        if (resp.success) {
          notice("Note saved")
          var note = Note.find(resp.old_id)

          if (resp.old_id < 0) {
            note.is_new = false
            note.id = resp.new_id
            note.elements.box.id = 'note-box-' + note.id
            note.elements.body.id = 'note-body-' + note.id
            note.elements.corner.id = 'note-corner-' + note.id
          }
          note.elements.body.innerHTML = resp.formatted_body
          note.elements.box.setOpacity(0.5)
          note.elements.box.removeClassName('unsaved')
        } else {
          notice("Error: " + resp.reason)
          note.elements.box.addClassName('unsaved')
        }
      }
    })

    if (e.stop) e.stop()
    Note.unbindEditShortcuts()
  },

  // Revert the note to the last saved state
  cancel: function(e) {
    if (Note.debug) {
      console.debug("Note#cancel (id=%d)", this.id)
    }

    this.hideEditBox(e)
    this.bodyHide()

    var ratio = this.ratio()
    for (p in this.fullsize) {
      this.fullsize[p] = this.old[p]
      this.elements.box.style[p] = this.fullsize[p] * ratio + 'px'
    }
    this.elements.body.innerHTML = this.old.formatted_body

    if (e.stop) e.stop()
    Note.unbindEditShortcuts()
  },

  // Remove all references to the note from the page
  removeCleanup: function() {
    if (Note.debug) {
      console.debug("Note#removeCleanup (id=%d)", this.id)
    }

    this.elements.box.remove()
    this.elements.body.remove()

    var allTemp = []
    for (i=0; i<Note.all.length; ++i) {
      if (Note.all[i].id != this.id) {
        allTemp.push(Note.all[i])
      }
    }

    Note.all = allTemp
    Note.updateNoteCount()
  },

  // Removes a note from the database
  remove: function(e) {
    if (Note.debug) {
      console.debug("Note#remove (id=%d)", this.id)
    }

    this.hideEditBox(e)
    this.bodyHide()
    this_note = this

    if (this.is_new) {
      this.removeCleanup()
      notice("Note removed")

    } else {
      notice("Removing note...")

      new Ajax.Request('/note/update.json', {
        parameters: {
          "id": this.id,
          "note[is_active]": "0"
        },
        onComplete: function(resp) {
          var resp = resp.responseJSON

          if (resp.success) {
            notice("Note removed")
            this_note.removeCleanup()
          } else {
            notice("Error: " + resp.reason)
          }
        }
      })
    }

    e.stop()
  },

  // Redirect to the note's history
  history: function(e) {
    if (Note.debug) {
      console.debug("Note#history (id=%d)", this.id)
    }

    this.hideEditBox(e)

    if (this.is_new) {
      notice("This note has no history")
    } else {
      location.href = '/history?search=notes:' + this.id
    }

    e.stop()
  }
})

// The following are class methods and variables
Object.extend(Note, {
  zindex: 0,
  counter: -1,
  all: [],
  display: true,
  debug: false,
  drag_created: false,
  active_note:null,
  create_click_x: 0,
  create_click_y: 0,
  
  ctrlEnterSave: function(ev) {
    if (ev.keyCode == 13 && ev.ctrlKey)
      Note.active_note.save({})
  },
  
  escapeCancel: function(ev) {
    if (ev.keyCode == 27)
      Note.active_note.cancel({})
  },
  
  bindEditShortcuts: function() {
    jQuery(document).bind('keydown', Note.ctrlEnterSave)
    jQuery(document).bind('keydown', Note.escapeCancel)
  },

  unbindEditShortcuts: function() {
    jQuery(document).unbind('keydown', Note.ctrlEnterSave)
    jQuery(document).unbind('keydown', Note.escapeCancel)
  },
  
  // Show all notes
  show: function() {
    if (Note.debug) {
      console.debug("Note.show")
    }

    $("note-container").show()
  },

  // Hide all notes
  hide: function() {
    if (Note.debug) {
      console.debug("Note.hide")
    }

    $("note-container").hide()
  },

  // Find a note instance based on the id number
  find: function(id) {
    if (Note.debug) {
      console.debug("Note.find")
    }

    for (var i=0; i<Note.all.size(); ++i) {
      if (Note.all[i].id == id) {
        return Note.all[i]
      }
    }

    return null
  },

  // Toggle the display of all notes
  toggle: function() {
    if (Note.debug) {
      console.debug("Note.toggle")
    }

    if (Note.display) {
      Note.hide()
      Note.display = false
    } else {
      Note.show()
      Note.display = true
    }
  },

  // Update the text displaying the number of notes a post has
  updateNoteCount: function() {
    if (Note.debug) {
      console.debug("Note.updateNoteCount")
    }

    if (Note.all.length > 0) {
      var label = ""

      if (Note.all.length == 1)
        label = "note"
      else
        label = "notes"

      $('note-count').innerHTML = "This post has <a href=\"/note/history?post_id=" + Note.post_id + "\">" + Note.all.length + " " + label + "</a>"
    } else {
      $('note-count').innerHTML = ""
    }
  },
  
  dragCreate: function(e) {
    if (Note.debug)
      console.debug("Note.toggleCreateEv")
    Note.drag_created = true
    // Note.toggled_create = true
    Note.create_click_x = e.pageX
    Note.create_click_y = e.pageY
    Note.create()
  },

  // Create a new note
  create: function() {
    if (Note.debug) {
      console.debug("Note.create")
    }

		Note.show()

    var insertion_position = Note.getInsertionPosition()
    var top = insertion_position[0]
    var left = insertion_position[1]
    var html = '',
        size = this.drag_created ? '10' : '150',
        id = 'note-box-' + Note.counter;
    html += '<div class="note-box unsaved" style="width: '+size+'px; height: '+size+'px; '
    html += 'top: ' + top + 'px; '
    html += 'left: ' + left + 'px;" '
    html += 'id="'+id+'">'
    html += '<div class="note-corner" id="note-corner-' + Note.counter + '"></div>'
    html += '</div>'
    html += '<div class="note-body" title="Click to edit" id="note-body-' + Note.counter + '"></div>'
    $("note-container").insert({bottom: html})
    var note = new Note(Note.counter, true, "")
    Note.all.push(note)
    Note.counter -= 1
  },

  // Find a suitable position to insert new notes
  getInsertionPosition: function() {
    if (Note.debug) {
      console.debug("Note.getInsertionPosition")
    }
    
    if (Note.drag_created) {
      var img_width = $("image").width;
      var ratio = parseInt(jQuery("#image").attr('large_width')) / img_width;
      var offset = jQuery("#image").offset();
      var x = Note.create_click_x - offset.left;
      var y = Note.create_click_y - offset.top;
      var min = Math.ceil(20 / ratio)
      x = (x * ratio) - 10
      y = (y * ratio) - 10
      
      if (x < min)
        x = min
      else if (x > (img_width * ratio))
        x = (img_width * ratio) - min
      
      var lh = parseInt(jQuery("#image").attr('large_height'))
      if (y < min)
        y = min
      else if (y > (lh * ratio))
        y = (lh * ratio) - min
      return [y, x]
    }
    
    // We want to show the edit box somewhere on the screen, but not outside the image.
    var scroll_x = $("image").cumulativeScrollOffset()[0]
    var scroll_y = $("image").cumulativeScrollOffset()[1]
    var image_left = $("image").positionedOffset()[0]
    var image_top = $("image").positionedOffset()[1]
    var image_right = image_left + $("image").width
    var image_bottom = image_top + $("image").height
    var left = 0
    var top = 0

    if (scroll_x > image_left) {
      left = scroll_x
    } else {
      left = image_left
    }

    if (scroll_y > image_top) {
      top = scroll_y
    } else {
      top = image_top + 20
    }

    if (top > image_bottom) {
      top = image_top + 20
    }

    return [top, left]
  },
  
  toggleCreateNotice: function() {
    var e = jQuery('#note_create_notice');
    e.is(':visible') ? e.hide() : e.fadeIn(200);
  }
})
