
/* START web_app_mode.js */
function setupWebAppMode() {
  if (!$('deadlines_container')) {
    return
  }
  
  if (!Object.isUndefined(window.fluid)) {
    return new FluidAppMode
  }
  
  if (typeof(SSB) != 'undefined') {
    return new BubblesAppMode
  }
}

var WebAppMode = Class.create({
  logMessageDisplay: function(message_id) {
    // Log message to cookie
    var deadline_notifier_messages = Azuki.Storage.Cookie.find('deadline_notifier_messages')
    
    if (Object.isUndefined(deadline_notifier_messages)) {
      deadline_notifier_messages = message_id
    } else {
      deadline_notifier_messages = deadline_notifier_messages + ',' + message_id
    }
    
    Azuki.Storage.Cookie.create('deadline_notifier_messages', deadline_notifier_messages, 365)
    
    this.log(deadline_notifier_messages)
  },
  
  messagesMarkedWithNow: function() {
    var messages = new Array
    $$('table.deadlines .deadline').each(function(element) {
      if (element.down('.date_container').innerHTML.match(/Now/)) {
        var message_id = parseInt(element.id.match(/_(\d+)$/)[1])
        messages.push([message_id, element.down('.description').innerHTML])
      }
    })
    
    return messages
  },
  
  displayedMessage: function(message_id) {
    var deadline_notifier_messages = Azuki.Storage.Cookie.find('deadline_notifier_messages')
    message_id = parseInt(message_id)
    
    if (Object.isUndefined(deadline_notifier_messages)) {
      return false
    }
    
    var message_ids = $A(deadline_notifier_messages.split(','))
    
    return message_ids.find(function(logged_id) {
      if (parseInt(logged_id) == message_id) {
        return true
      }
    })
  },
  
  showNotifierMessages: function() {
    this.messagesMarkedWithNow().each(function(message) {
      this.showNotifierMessage(message[1], message[0])
    }.bind(this))
  },
  
  runRefresher: function() {
    this.refresher = new PeriodicalExecuter(this.refreshDeadlines.bind(this), this.refreshPeriod);
  },
  
  refreshDeadlines: function() {
    this.showNotifierMessages()
    
    if ($$('.inplaceeditor-form').size() == 0) {
      new Ajax.Updater($('deadlines_container'), '/deadlines', { method: 'get', onComplete: function() { DeadlinesController.deadline_list() }.bind(this)})
    }
  }
})

/* Fluid: Mac Site Specific Browsing */

var FluidAppMode = Class.create(WebAppMode, {
  initialize: function() {
    this.refreshPeriod = 60
    this.debug = false
    
    // Count the number of Deadlines with a time set to 'Now'
    this.setDockBadge()
    
    // Run refresh
    this.runRefresher()
    
    // Show growl messages
    this.showNotifierMessages()
  },
  
  log: function(message) { 
    if (this.debug) {
      console.log(message)
    }
  },
  
  showNotifierMessage: function(message, message_id) {
    if (this.displayedMessage(message_id)) {
      this.log("Already displayed message ID: " + message_id)
      return
    }
    
    this.log("Showing message ID: " + message_id)
    
    window.fluid.showGrowlNotification({
        title: "Deadline Reminder", 
        description: message, 
        priority: 1, 
        sticky: false,
        identifier: message_id
    })
    
    this.logMessageDisplay(message_id)
  },
  
  setDockBadge: function() {
    var count = this.countExpiredDeadlines()
    
    if (count > 0) {
      this.log("Dock badge count: " + count)
      window.fluid.dockBadge = count
    }
  },
  
  countExpiredDeadlines: function() {
    return $$('.date_container').inject(0, function(sum, element) {
      if (element.innerHTML.match(/Now/)) {
        sum = sum + 1
      }
      
      return sum
    })
  },
  
  refreshDeadlines: function() {
    this.showNotifierMessages()
    
    if ($$('.inplaceeditor-form').size() == 0) {
      new Ajax.Updater($('deadlines_container'), '/deadlines', { method: 'get', onComplete: function() { this.setDockBadge(); DeadlinesController.deadline_list() }.bind(this)})
    }
  }
})

/* Bubbles: Windows Site Specific Browsing */

var BubblesAppMode = Class.create(WebAppMode, {
  initialize: function() {
    this.refreshPeriod = 60
    this.debug = false
    
    // Run refresh
    this.runRefresher()
    
    // Show notifier messages
    this.showNotifierMessages()
    
    if (this.debug) {
      SSB.console.init('debug')
    }
    
    var favicon = window.location.protocol + '//' + window.location.host + '/favicon.ico'
    SSB.setIcon(favicon)
  },

  log: function(message) { 
    if (this.debug) {
      SSB.console.debug(message)
    }
  },
  
  showNotifierMessage: function(message, message_id) {
    if (this.displayedMessage(message_id)) {
      return
    }
    
    this.log("Showing message ID: " + message_id)
    
    SSB.simpleNotify(message)
    this.logMessageDisplay(message_id)
  }
})
/* END web_app_mode.js */

/* START users_controller.js */
var TabManager = Class.create()
TabManager.prototype = {
  initialize: function() {
    this.tabsClassName = 'tabs'
    this.bodyClassName = 'tab_content'
    
    Event.observe(document, 'click', this.tab_click.bind(this))
    this.select_from_cookie()
    this.last_setting_tab_name = 'last_setting_tab'
  },
  
  tab_click: function(event) {
    var element = Event.element(event)
    
    if (element.nodeName == 'A' && element.up('ul.tabs')) {
      this.show_tab(element)
      Event.stop(event)
      return false
    }
  },
  
  select_from_cookie: function() {
    var cookie_value = Azuki.Storage.Cookie.find('last_setting_tab')
    if (cookie_value) {
      var link = $$('a.' + cookie_value).first()
      
      if (link != $$('ul.tabs li a.selected').first()) {
        this.show_tab(link)
      }
    }
  },
  
  show_tab: function(link) {
    try {
      var tab_id = $w(link.className).find(function(element) { return element.match(/tab_/) })
      var active_tab = link.up().up().next('.tab_body').down('.selected')
      var active_link = link.up('.tabs').down('li a.selected')
    
      if (link == $$('ul.tabs li a.selected').first()) {
        return
      }
    
      Azuki.Storage.Cookie.create(this.last_setting_tab_name, tab_id)
    
      tab_id = tab_id.replace(/tab_/, '')
    
      $(tab_id).show()
      $(tab_id).addClassName('selected')
    
      active_tab.hide()
      active_tab.removeClassName('selected')
    
      link.addClassName('selected')
      active_link.removeClassName('selected')
    } catch (exception) {
    }
  }
}

var UsersController = new Object()
UsersController.Methods = {
  run: function()
  {
    new TabManager()
    
    DeadlinesController.footer_hover()
    Event.observe('user_wants_reminders', 'change', this.reminder_checkbox_hider)
    Event.observe('additional_reminder', 'change', this.additional_reminder_checkbox_hider)
    this.reminder_checkbox_hider()

    if ($('user_reminder_frequency').value == '0') {
      $('additional_reminders').hide()
    }
    
    /* Colours */
    new ColourBlockEditorController
  },
  
  reminder_checkbox_hider: function()
  {
    $('user_wants_reminders').checked ? $('reminder_settings').show() : $('reminder_settings').hide()
  },
  
  additional_reminder_checkbox_hider: function()
  {
    if ($('additional_reminder').checked) {
      $('user_reminder_frequency').value = 1
      $('additional_reminders').show()
    } else {
      $('user_reminder_frequency').value = 0
      $('additional_reminders').hide()
    }
  }
}

Object.extend(UsersController, UsersController.Methods)
/* END users_controller.js */

/* START stripes.js */
var Stripes = new Object()

Stripes.Methods = {
  apply_to: function(selector)
  {
    var even = false
    
    $$(selector).each(function(item)
    {
      $(item).removeClassName(!even ? 'even' : 'odd')
      $(item).addClassName(even ? 'even' : 'odd')
      
      even = !even
    })
  }
}

Object.extend(Stripes, Stripes.Methods)
/* END stripes.js */

/* START fade_text.js */
var FadeText = new Object()

FadeText.Methods = {
  colourDarkerThanBackground: function(colour) {
    try {
      var backgroundValue = $$('body').first().getStyle('backgroundColor')

      if (backgroundValue.match(/rgb/)) {
        var backgroundColour = new Azuki.Style.Color.RGB(backgroundValue)
      } else {
        var backgroundColour = new Azuki.Style.Color.Hex(backgroundValue).to_rgb()
      }
    
      if ($A(colour).inject(0, function(acc, n) { return acc + n; })
          < $A(backgroundColour.colors).inject(0, function(acc, n) { return acc + n; })) {
        return true
      }
    } catch(exception) {

    }
    
    return false
  },
  
  apply_to: function(selector, start_colour)
  {
    var items = $$(selector)
    var count = items.length
    
    var direction = this.colourDarkerThanBackground(start_colour) ? 1 : -1
    var threshold = 80
    var lower_threshold = 0 + threshold
    var upper_threshold = 255 - threshold

    threshold = direction == -1 ? lower_threshold : upper_threshold

    if (direction == 1) {
      items = items.reverse()
    }

    items.each(function(item, i)
    {
      var offset = 0
      var item_colour = new Azuki.Style.Color.RGB(Object.clone(start_colour))

      if (i == 0) {
        item.style.color = item_colour.to_s()
        return
      }
      
      if (direction == -1) {
        offset = Math.round(((upper_threshold) / count) * i)
      } else if (direction == 1) {
        offset = Math.round(((upper_threshold) / count) * (count - i))
      }
      
      if (direction == -1) {
        var red =   item_colour.red()   - offset < threshold ? threshold : item_colour.red()   - offset
        var green = item_colour.green() - offset < threshold ? threshold : item_colour.green() - offset
        var blue =  item_colour.blue()  - offset < threshold ? threshold : item_colour.blue()  - offset
      } else {
        var red =   item_colour.red()   + offset > threshold ? threshold : item_colour.red()   + offset
        var green = item_colour.green() + offset > threshold ? threshold : item_colour.green() + offset
        var blue =  item_colour.blue()  + offset > threshold ? threshold : item_colour.blue()  + offset
      }

      item_colour.set_red(red)
      item_colour.set_green(green)
      item_colour.set_blue(blue)

      item.style.color = item_colour.to_s()
    })
  }
}

Object.extend(FadeText, FadeText.Methods)

/* END fade_text.js */

/* START editor.js */
var Editor = new Object()

Editor.Methods = {
  apply_to: function(selector, field_name)
  {
    $$(selector).each(function(item)
    {
      var item_id = item.up().up().id.replace(/.*_(\d+)$/, '$1')
      var save_clicked = false
      var highlightendcolor = item.up().up('tr').getStyle('backgroundColor')
      var clickToEditText = 'Click to edit'
      
      if (highlightendcolor == 'transparent') {
        highlightendcolor = $$('body').first().getStyle('backgroundColor')
      }

      if (highlightendcolor && highlightendcolor.match(/^rgb/)) {
        highlightendcolor = '#' + (new Azuki.Style.Color.RGB(highlightendcolor).to_hex())
      } else { 
        highlightendcolor = '#000000'
      }
      
      if (field_name == 'due') {
        clickToEditText = ''
      }
      
      var editor_options = {
        highlightendcolor: highlightendcolor,
        highlightcolor: editor['highlightcolor'],
        okText: 'Save',
        cancelText: 'Cancel',
        clickToEditText: clickToEditText,
        onEnterEditMode: DeadlinesController.hide_all_date_details,
        onComplete: function()
        {
          if (save_clicked)
          {
            new Ajax.Updater('Deadline_' + item_id, '/deadlines/' + item_id, {
              method: 'get',
              onComplete: function()
              {
                DeadlinesController.deadline_list_for_item(item_id)
                DeadlinesController.sort_list()
                DeadlinesController.decorate_deadline_list()
              }
            })
          }
          
          save_clicked = false
        },
        callback: function(form)
        {
          var value = Form.serialize(form, { getHash: true })['value']
          save_clicked = true
          return 'deadline[' + field_name + ']=' + value
        }
      }
      
      if (!editor['use_hover_fader']) {
        editor_options['onEnterHover'] = function(ipe) { ipe.element.style.backgroundColor = ipe.options.highlightColor; }
        editor_options['onLeaveHover'] = function(ipe) { 
          ipe._effect = new Effect.Highlight(ipe.element, {
            startcolor: ipe.options.highlightColor, endcolor: ipe.options.highlightEndColor,
            restorecolor: ipe._originalBackground, keepBackgroundImage: true,
            duration: 0
          });
        }
      }
      
      new Ajax.InPlaceEditor(item, '/deadlines/update/' + item_id, editor_options)
    })
  }
}

Object.extend(Editor, Editor.Methods)

/* END editor.js */

/* START deadlines_controller.js */
var FieldWithHelp = Class.create()
FieldWithHelp.prototype = {
  initialize: function(element) {
    this.default_value = $(element).value
    $(element).observe('focus', this.activate_field.bind(this))
    $(element).observe('blur', this.deactivate_field.bind(this))
  },
  
  activate_field: function(event)
  {
    var element = Event.element(event)
    if (element.addClassName)
    {
      if (element.value == this.default_value) element.value = ''
      element.addClassName('active')
    }
  },
  
  deactivate_field: function(event)
  {
    var element = Event.element(event)
    if (element.addClassName)
    {
      if (element.value.length == 0) element.value = this.default_value
      element.removeClassName('active')
    }
  }
}

var DeadlineDescriptionField = Class.create()
DeadlineDescriptionField.prototype = {
  initialize: function() {
    this.field_with_help = new FieldWithHelp('deadline_description')
  }
}

var SearchField = Class.create()
SearchField.prototype = {
  initialize: function() {
    this.form = $('search_form')
    this.field_with_help = new FieldWithHelp('deadline_search')
    this.observer = new Form.Element.Observer('deadline_search', 0.5, this.perform_search.bind(this))
  },
  
  perform_search: function(event)
  {
    var searching = $('deadline_search').hasClassName('active') && $('deadline_search').value.length > 0 ? true : false
    var url = searching ? '/deadlines/search' : '/deadlines'
    var method = searching ? 'post' : 'get'
    new Ajax.Updater('deadlines_container', url, { parameters: this.form.serialize(true), onComplete: DeadlinesController.deadline_list, method: method })
  }
}


var DeadlinesController = new Object()
DeadlinesController.Methods = {
  run: function()
  {
    if ($('controls'))
    {
      this.deadline_list()
      this.add_control_events('mouseout', Element.hide)
      this.add_control_events('mouseover', Element.show)
      this.data_entry_balloon()

      this.control_width()
      Event.observe(window, 'resize', this.control_width)

      new DeadlineDescriptionField
      new SearchField
    }
    
    this.date_fields()
    this.footer_hover()
    this.last_error = ''
    
    Event.observe(document, 'mouseover', function(e) {
      var element = Event.element(e)
      
      if (!element) return
      
      if (element.hasClassName('description') || element.hasClassName('date')) {
        element.addClassName('hover')
      }
    })
    
    Event.observe(document, 'mouseout', function(e) {
      var element = Event.element(e)
      
      if (!element) return
      
      if ((element.hasClassName('description') || element.hasClassName('date'))) {
        element.removeClassName('hover')
      }
    })

    if ($('HelpPage')) {
      var tab_manager = new TabManager('last_setting_tab_help')
    }
    
    new DateHoverDetails()
    setupWebAppMode()
  },
  
  hide_all_date_details: function() {
    //onEnterEditMode
    $$('.date_details').each(function(date_element) {
      if (date_element.visible()) {
        new Effect.Fade(date_element, { duration: 0.3 })
      }
    })
  },
  
  control_width: function() {
    try {
      var header_width = $('header').getWidth()
      var page_width = $('container').getWidth()
      var deadline_input_width = $$('#controls .deadline').first().getWidth()
      var controls_width = page_width - header_width - 80
      var search_width = controls_width / 4
    
      deadline_input_width = controls_width - search_width - 4

      $('controls').setStyle({ width: controls_width + 'px' })
      $$('#controls #search').first().setStyle({ width: search_width + 'px' })
      $$('#controls .deadline').first().setStyle({ width: deadline_input_width + 'px' })
    } catch (excpetion) {
      //alert(excpetion)
    }
  },
  
  footer_hover: function()
  {
    Event.observe('header', 'mouseover', function() {
      $('footer').removeClassName('dark')
      $('footer').addClassName('light')
    })
    
    Event.observe('header', 'mouseout', function() {
      $('footer').removeClassName('light')
      $('footer').addClassName('dark')
    })
  },
  
  deadline_response: function(request)
  {
    if (request.status == 422 && !$('DeadlineForm'))
    {
      var balloon = new Balloon($('deadline_description'), 'SearchMessage', '<span id="DeadlineLength">140</span> characters remaining', 'Error: ' + request.responseText)
      this.last_error = eval(request.responseText)
      balloon.destroy()
      // balloon.show()
      var remote_form = new Azuki.Forms.Remote({form_id: 'DeadlineForm', controller_name: 'deadlines', item_name: 'deadline', add_close_button: false, form_display_callback: DeadlinesController.form_display_callback })
      remote_form.show_form('DeadlineForm', remote_form.options.new_url, remote_form.options.create_url, remote_form.options.form_display_callback)

      // DeadlinesController.show_deadline_length()
    }
    else if (request.status.toString().match(/^20/))
    {
      $('deadline_description').value = ''
      new Ajax.Updater('deadlines_container', '/deadlines', { onComplete: DeadlinesController.deadline_list, method: 'get' })
      var balloon = new Balloon($('deadline_description'), 'SearchMessage', '<span id="DeadlineLength">140</span> characters remaining', 'Deadline added')
      balloon.destroy()
      balloon.show()
      balloon.fade_after(3)
    }
    else
    {
      alert(request.status)
    }
  },
  
  form_display_callback: function()
  {
    var last_error = DeadlinesController.last_error
    
    DeadlinesController.date_fields()
    $('deadline_description_form').value = $('deadline_description').value
    
    if (last_error && last_error[0])
    {
      $('deadline_errors').innerHTML = 'Error: ' + last_error[0][1]
      DeadlinesController.last_error = null
    }
  },
  
  data_entry_balloon: function()
  {
    var balloon = new Balloon($('deadline_description'), 'SearchMessage', '<span id="DeadlineLength">140</span> characters remaining', 'Enter a deadline with a date then press return to save it.  Dates can be written in English, like "today", "tuesday" or "tuesday evening".')
    DeadlinesController.show_deadline_length()
    Event.observe('deadline_description', 'blur', function(e)
    {
      balloon.destroy()
    })
    
    Event.observe('deadline_description', 'focus', function(e)
    {
      balloon.show()
      DeadlinesController.show_deadline_length()
    })
    
    new Form.Element.Observer(
      'deadline_description',
       0.2,  // 200 milliseconds
      function(element, value){
        DeadlinesController.show_deadline_length()
      }
    )
    
    Event.observe('deadline_description', 'keypress', function(e)
    {
      DeadlinesController.show_deadline_length()
    })
  },
  
  show_deadline_length: function()
  {
    if ($('DeadlineLength'))
      $('DeadlineLength').innerHTML = 140 - $('deadline_description').value.length
  },
  
  make_start_colour: function()
  {
    var hex_colour = new Azuki.Style.Color.Hex(editor['highlightcolor'])
    var rgb_colour = hex_colour.to_rgb()
    
    var red =   rgb_colour.red()
    var green = rgb_colour.green()
    var blue =  rgb_colour.blue()

    return (new Array(red, green, blue))
  },
  
  decorate_deadline_list: function()
  {
    var start_colour = this.make_start_colour()
    Stripes.apply_to('.deadlines .deadline')
    
    if (editor['use_colour_fader']) {
      FadeText.apply_to('.deadlines .date', start_colour)
      FadeText.apply_to('.deadlines .description', start_colour)
    }
  },
  
  deadline_list: function()
  {
    DeadlinesController.decorate_deadline_list()
    Editor.apply_to('.deadlines .description', 'description')
    Editor.apply_to('.deadlines .date', 'due')
  },
  
  deadline_list_for_item: function(item_id)
  {
    /* Add the editor to specific elements */
    Editor.apply_to('.deadlines #Deadline_' + item_id + ' .description', 'description')
    Editor.apply_to('.deadlines #Deadline_' + item_id + ' .date', 'due')
  },
  
  add_control_events: function(event_name, method)
  {
    Event.observe(document, event_name, function(e)
    {
      var element = Event.element(e)
      var deadline_block = element.hasClassName('deadline') ? element : element.up('.deadline')

      if (deadline_block)
      {
        try
        {
          method(deadline_block.down('.controls'))
        }
        catch (exception)
        {
        }
      }
    })
  },
  
  sort_list: function()
  {
    /* Internet explorer fails */
    if (Prototype.Browser.IE) return;

    var list = $A()
    var table = $$('table.deadlines').first()

    $$('td.date_container').each(function(element)
    {
      var date = element.down('.date_value').innerHTML
      var date_container_id = element.up('tr.deadline').id
      
      list.push([date, date_container_id])
    })
    
    list = list.sortBy(function(s) { return s[0] })
    
    list.each(function(value) {
      var date_container_id = value[1]
      var table_cell = table.down('#' + date_container_id)
      table_cell.remove()
      new Insertion.Bottom(table, table_cell)
    })
  },
  
  date_fields: function()
  {
    if ($('DateTimeSelectLink'))
    {
      Event.observe('DateTimeSelectLink', 'click', function(e)
      {
        if ($('DateTimeSelect').visible())
        {
          new Effect.BlindUp('DateTimeSelect', { duration: 0.1 })
          $('QuickDate').value = $('QuickDate').options[0]
        }
        else
        {
          new Effect.BlindDown('DateTimeSelect', { duration: 0.1 })
          $('QuickDate').value = '0'
        }
        Event.stop(e)
        return false
      })
      
      Event.observe('QuickDate', 'change', function()
      {
        if ($('QuickDate').value == '0' && !$('DateTimeSelect').visible())
        {
          new Effect.BlindDown('DateTimeSelect', { duration: 0.1 })
        }
        else if ($('DateTimeSelect').visible())
        {
          new Effect.BlindUp('DateTimeSelect', { duration: 0.1 })
        }
      })
    }
  }
}

Object.extend(DeadlinesController, DeadlinesController.Methods)

/* END deadlines_controller.js */

/* START date_hover.js */
var DateHoverDetails = Class.create({
  initialize: function() {
    this.blocked = false
    Event.observe(document, 'mouseover', this.mouseover.bindAsEventListener(this))
    Event.observe(document, 'mouseout', this.mouseout.bindAsEventListener(this))
  },
  
  mouseover: function(e) {
    var element = Event.element(e)
    
    if (!element || this.displaying_details) return
    
    if (element.hasClassName('date')) {
      if (this.blocked) {
        Event.stop(e)
        return false
      }
      
      var date_element = element.next('.date_details')
      
      if (date_element && !date_element.visible()) {
        this.blocked = true
        new Effect.Appear(date_element, { duration: 0.3 })
        Event.stop(e)
        return false
      }
    }
  },
  
  mouseout: function(e) {
    var element = Event.element(e)
    
    if (!element) return
    
    if (element.hasClassName('date')) {
      var date_element = element.next('.date_details')
      this.blocked = false
      new Effect.Fade(date_element, { duration: 0.3 })
      Event.stop(e)
      return false
    }
  },
  
  hide_all_date_details: function() {
    $$('.date_details').each(function(date_element) {
      if (date_element && date_element.visible()) {
        date_element.hide()
      }
    })
  }
})
/* END date_hover.js */

/* START colour_slider.js */
var ColourSlider = Class.create()
ColourSlider.prototype = {
  initialize: function(slider, colour_name, block, save_callback)
  {
    this.colour_block = block
    this.slider = slider
    this.colour_name = colour_name
    this.save_callback = save_callback
    
    this.track = slider
    this.handle = slider.down('div.handle')
    
    this.create_slider()
  },

  create_slider: function()
  {
    if (this.control) return this.control
    
    this.control = new Control.Slider(this.handle, this.track,
    {
      range: $R(0, 255),
      sliderValue: this.colour_block.get(this.colour_name),
      disabled: false,
      onChange: this.updateColourAndSave.bind(this),
      onSlide: this.updateColour.bind(this)
    })
  },
  
  updateColourAndSave: function(value)
  {
    this.updateColour(value)
    if (this.save_callback) this.save_callback()
  },
  
  updateColour: function(value)
  {
    this.colour_block.set(this.colour_name, Math.round(value))
    this.colour_block
    this.colour_block.update()
  }
}

/* END colour_slider.js */

/* START colour_block_helper.js */
if (!window.ColourBlockHelper) var ColourBlockHelper = new Object()

ColourBlockHelper.apply_colour_slider_to = function(slider, value_field, save_callback)
{
  var red = slider.down('div.slider')
  var green = red.next('div.slider')
  var blue = green.next('div.slider')
  var block = slider.next('div.colour_block')
  
  var colour_block = new ColourBlock(block, value_field)
  new ColourSlider(red, 'red', colour_block, save_callback)
  new ColourSlider(green, 'green', colour_block, save_callback)
  new ColourSlider(blue, 'blue', colour_block, save_callback)
}
/* END colour_block_helper.js */

/* START colour_block_controller.js */
var ColourBlockEditorController = Class.create();
ColourBlockEditorController.prototype = {
  initialize: function(selector)
  {
    if (!selector) selector = '.colour_block_small'
    
    /* This should be an option */
    this.save_url = "/users/save_colour/#{type}"
    
    /* Add dynamic block editors to the small blocks */
    $$(selector).each(function(block)
    {
      Event.observe(block, 'click', this.show_editor.bindAsEventListener(this))
    }.bind(this))
    
    /* Add the normal editors when they're visible */
    $$('div.colour_sliders').each(function(slider)
    {
      ColourBlockHelper.apply_colour_slider_to(slider, $('ColourValueField_' + slider.id.match(/(\d+)$/)[0]))
    })
  },
  
  show_editor: function(event)
  {
    element = Event.element(event)
    
    var colour = element.getStyle('backgroundColor').parseColor()
    var html = this.html()
    // Delete the sliders if they're already being displayed
    if ($('DynamicSliderContainer')) $('DynamicSliderContainer').remove()
    // Insert the slider HTML
    new Insertion.Top(document.body, html)
    Azuki.Helpers.Window.center('DynamicSliderContainer')
    Azuki.Windowing.Fader.fade()
    $('DynamicSliderContainer').show()
    
    // Change the colour of the colour swatch
    $('DynamicSliderValue').value = colour
    $('ColourBlock_Dynamic').setStyle({ backgroundColor: colour})

    // Ingore form submission
    Event.observe('DynamicSliderContainer', 'submit', function(e) { Event.stop(e); return false })

    // Save
    Event.observe('SaveDynamicColourSelection', 'click', function(e)
    {
      this.save(element)
      element.setStyle({backgroundColor: $('DynamicSliderValue').value})
      
      $('DynamicSliderContainer').remove()
      Azuki.Windowing.Fader.remove()
      
      Event.stop(e); return false 
    }.bind(this))

    // Cancel
    Event.observe('CancelDynamicColourSelection', 'click', function(e)
    {
      $('DynamicSliderContainer').remove()
      Azuki.Windowing.Fader.remove()
      
      Event.stop(e)
      return false
    }.bind(this))

    ColourBlockHelper.apply_colour_slider_to($('DynamicSlider'), $('DynamicSliderValue'))
  },
  
  html: function()
  {
    var image_format = Azuki.Helpers.Compatibility.suitable_image_format()
    var html = ''

    html += '<form class="floating_sliders standard" id="DynamicSliderContainer" style="display: none; background-color: #000000">'
    html += '    <p>Drag the sliders to change the selected colour.</p>'
    html += '    <div style="margin-left: 15px" class="colour_sliders" id="DynamicSlider">'
    html += '      Red<div class="slider" id="colour_dynamic_track1" style="width: 255px"><div class="handle" id="colour_dynamic_handle1"> </div></div> '
    html += '      Green<div class="slider" id="colour_dynamic_track2" style="width: 255px"><div class="handle" id="colour_dynamic_handle2"> </div></div>'
    html += '      Blue<div class="slider" id="colour_dynamic_track3" style="width: 255px; clear: right"><div class="handle" id="colour__dynamichandle3"> </div></div>'
    html += '    </div>'
    html += '  <div class="colour_block" id="ColourBlock_Dynamic"> </div>'
    html += '  <div class="action"><div class="buttons"><input type="text" name="slider_value" value="" id="DynamicSliderValue" style="display: none" /><input type="text" name="slider_value" value="" id="DynamicSliderValue" style="display: none" /><button id="SaveDynamicColourSelection" class="yes" id="submit" type="submit"><img alt="" src="/images/azuki/yes.gif"/> Save</button> <a id="CancelDynamicColourSelection" class="no" href="#"><img alt="" src="/images/azuki/no.gif"/> Cancel </a></div></div>'
    html += '</form>'
    
    return html
  },
  
  save: function(element)
  {
    if (!element.className.match(/_Colour_Block_/)) return
    
    var id   = element.className.match(/\d+$/)[0]
    var type = element.className.replace(/[^ ]* (.*)_Colour_Block_\d+$/, '$1')
    var url  = '/' + type.toLowerCase() + '/' + id + '/save_colour'
    var colour_class = type + '_Colour_Block_' + id
    
    /* TODO: This needs to be generic */
    url = "/users/save_colour/#{type}".interpolate({ type: type.toLowerCase(), id: id });
    
    /* Reload page on reports (to get new graphs) */
    new Ajax.Request(url, {method: 'post', postBody: 'colour=' + $('DynamicSliderValue').value, onComplete: function() { if ($('Preview')) { $('Preview').src = $('Preview').src + '?reload' } }})

    /* Update any other colour blocks with this class */
    $$('.' + colour_class).each(function(colour_block)
    {
      colour_block.setStyle({backgroundColor: $('DynamicSliderValue').value})
    })
  }
}

/* END colour_block_controller.js */

/* START colour_block.js */
var ColourBlock = Class.create()
ColourBlock.prototype = {
  initialize: function(block, value_field)
  {
    this.colours = Array(0, 0, 0)
    this.block = block
    this.value_field = value_field
    
    this.set_preset_colours()
  },

  /* The following methods return and expect RGB values */
  red: function() { return this.colours[0] },
  green: function() { return this.colours[1] },
  blue: function() { return this.colours[2] },
  
  set_red: function(red) { this.colours[0] = red },
  set_green: function(green) { this.colours[1] = green },
  set_blue: function(blue) { this.colours[2] = blue },
  
  update: function()
  {
    var colour = 'rgb(' + this.colours[0] + ',' + this.colours[1] + ',' + this.colours[2] + ')'
    this.value_field.value = '#' + this.to_hex()
    $(this.block).setStyle({ backgroundColor: colour })
  },
  
  set_preset_colours: function()
  {
    try
    {
      var red   = $(this.value_field).value.charAt(1) + $(this.value_field).value.charAt(2)
      var green = $(this.value_field).value.charAt(3) + $(this.value_field).value.charAt(4)
      var blue  = $(this.value_field).value.charAt(5) + $(this.value_field).value.charAt(6)
      
      this.block.style.backgroundColor = this.value_field.value

      this.set_red(this.rgb(red))
      this.set_green(this.rgb(green))
      this.set_blue(this.rgb(blue))
      
      this.update()
    }
    catch (exception)
    {
      this.set_red(0)
      this.set_green(0)
      this.set_blue(0)
    }
  },
  
  to_hex: function()
  {
    var hex = ''
    
    $A(this.colours).each(function(colour)
    {
      var value = this.hex(colour)
      
      if (value.length == 1) { value = '0' + value }
      
      hex = hex + value
    }.bind(this))
    
    return hex
  },
  
  hex: function(d)
  {
    var hex_map = '0123456789ABCDEF'
    var h = hex_map.substr(d&15, 1)
    
    while (d > 15) { d >>= 4; h = hex_map.substr(d&15, 1) + h }
    return h
  },
  
  rgb: function(hex)
  {
    return parseInt(hex, 16) || 0
  },
  
  /* Consider these private */
  get: function(colour_name)
  {
    switch(colour_name)
    {
      case 'red':
        return this.colours[0]
      break;
      
      case 'green':
        return this.colours[1]
      break;

      case 'blue':
        return this.colours[2]
      break;
    }
  },

  set: function(colour_name, value)
  {
    switch(colour_name)
    {
      case 'red':
        return this.set_red(value)
      break;
      
      case 'green':
        return this.set_green(value)
      break;

      case 'blue':
        return this.set_blue(value)
      break;
    }
  }
}

/* END colour_block.js */

/* START balloon.js */
Balloon = Class.create()
Balloon.prototype = {
  initialize: function(parent_element, id, title, message)
  {
    this.id = id
    this.title = title
    this.message = message
    this.parent_element = $(parent_element)
  },
  
  show: function()
  {
    if ($(this.id)) return
    
    var html = ''
    html = html + '<div class="balloon_container" id="#{id}">'.interpolate({ id: this.id })
    html = html + '  <div class="triangle_top">&nbsp;</div>'
    html = html + '  <div class="balloon"><div class="balloon_content"><div class="top"></div>'
    html = html + '    <h2>#{title}</h2>'.interpolate({ title: this.title })
    html = html + '    <p>#{message}</p>'.interpolate({ message: this.message })
    html = html + '  </div><div class="bottom"><div></div></div></div>'
    html = html + '</div>'

    Element.insert($('header'), { after: html })
    Element.clonePosition(this.id, this.parent_element,
    {
      setTop: true, setLeft: true, setWidth: false, setHeight: false,
      offsetTop: this.parent_element.getHeight() + 6,
      offsetLeft: this.parent_element.getWidth() - ($(this.id).getWidth() / 2) - 4
    } ) 

  },
  
  destroy: function()
  {
    if ($(this.id))
      $(this.id).remove()
  },
  
  fade_after: function(seconds)
  {
    if (!$(this.id)) return
    Effect.DropOut(this.id, { delay: seconds })
  }
}

/* END balloon.js */

/* START azukilib.js */
var Azuki = {
  Version: '0.0.1',
  Forms: { },
  Helpers: { },
  Storage: { },
  Style: { },
  Windowing: { }
}

/* START popinfo.js */

Azuki.Windowing.PopInfo = {
  display: function(element, info, callback)
  {
    var delay = 2000
    var popup_element_id = element.id + '_popup'

    this.remove(popup_element_id)
    new Insertion.Before(element, '<div style="display: none" id="' + popup_element_id + '" class="popinfo_frame"><div class="popinfo_inner">' + info + '</div><div class="popinfo_image"></div></div>')
    
    if (callback) callback(element)
    
    new Effect.Appear(popup_element_id, { duration: 0.2 })
    
    setTimeout((function() { this.fade_popup(popup_element_id) }.bind(this)), delay)
  },
  
  fade_popup: function(popup_element_id)
  {
    try
    {
      new Effect.Fade(popup_element_id)
      setTimeout((function() { this.remove(popup_element_id) }).bind(this), 1000)
    }
    catch (exception)
    {
    }
  },
  
  remove: function(popup_element_id)
  {
    try
    {
      if ($(popup_element_id)) Element.remove(popup_element_id)
    }
    catch (exception)
    {
      
    }
  }
}
/* END popinfo.js */

/* START lightbox.js */
Azuki.Windowing.Lightbox = Class.create()
Azuki.Windowing.Lightbox.prototype = {
  initialize: function(class_name)
  {
    this.class_name = typeof class_name == 'undefined' ? 'lightbox' : class_name
    
    this.container_id = 'LightboxContainer'
    this.close_id = 'LightboxClose'
    this.remote_type = 'image'
    
    this.remove_event = this.remove.bindAsEventListener(this)
    Event.observe(document, 'click', this.events.bind(this))
  },
  
  events: function(e)
  {
    var element = Event.element(e)
    this.image = null
    this.link = null
    
    if (element.nodeName == 'A' && element.down('img') && element.down('img').hasClassName(this.class_name))
    {
      this.image = element.down('img')
      this.link = element
    }
    
    if (element.nodeName == 'IMG' && element.hasClassName(this.class_name))
    {
      this.link = element.up('a')
      this.image = element
    }
    
    if (this.link && this.image)
    {
      this.display(e)
    }
  },
  
  remove: function(e)
  {
    Event.stopObserving($(this.close_id), 'click', this.remove_event)
    $(this.container_id).remove()
    Azuki.Windowing.Fader.remove()
    Event.stop(e)
    return false
  },
  
  display: function(e)
  {
    var image_format = Azuki.Helpers.Compatibility.suitable_image_format()
    var title_text = this.image.title ? this.image.title : ''
    var title = title_text ? '<div class="title">' + title_text + '</div>' : ''
    
    if (Azuki.Windowing.Busybox.active())
    {
      Event.stop(e)
      return false
    }
    
    if ($(this.container_id)) $(this.container_id).remove()
    Azuki.Windowing.Busybox.busy('loading')
    
    new Insertion.Top(document.body, '<div id="' + this.container_id + '" class="popinfo_container" style="display: none"><div id="' + this.close_id + '" style="position: absolute; left: -15px; top: -15px; cursor: pointer"><img width="36" height="36" border="0" src="/images/azuki/close.' + image_format + '"/></div></div>')

    Event.observe($(this.close_id), 'click', this.remove_event)

    switch(this.remote_type)
    {
      case 'image':
        var image = document.createElement('img')
        
        image.onload = function()
        {
          $(this.container_id).appendChild(image)

          if (title)
          {
            var title_element = $(this.container_id).insert(title)
            
            if (image.width > 0 )
              title_element.setStyle({ width: image.width + 'px' })
          }

          Azuki.Windowing.Busybox.done()
          Azuki.Helpers.Window.center(this.container_id)
          Azuki.Windowing.Fader.fade()
          new Effect.Appear(this.container_id, {duration: 0.3})

          return false
        }.bind(this)

        image.src = this.link.href
      break
      
      case 'html':
        new Ajax.Updater(this.container_id, this.link.href, { method: 'get', evalScripts: true, onComplete: function()
        {
          Azuki.Windowing.Busybox.done()
          Azuki.Windowing.Fader.fade()
          Azuki.Helpers.Window.center($(this.container_id))
          
          $(this.container_id).show()
        }.bind(this)})
      break
    }
    
    if (e) Event.stop(e)
    return false
  }
}

/* END lightbox.js */

/* START fader.js */
Azuki.Windowing.Fader = {
  active: function()
  {
    return $('Fader') ? true : false
  },
  
  set_height: function()
  {
    $('Fader').setStyle({height: Azuki.Helpers.Window.page_size().height + 'px'})
  },
  
  fade: function()
  {
    new Insertion.Top(document.body, '<div id="Fader" style="display: none; position: absolute; z-index: 120; width: 100%; height: 100%; top: 0; left: 0; background-color: #000000"></div>')
    Position.prepare()
    Azuki.Windowing.Fader.set_height()
    $('Fader').setOpacity(0)
    $('Fader').show(0)
    Azuki.Windowing.Fader.alter_selects('hidden')
    Azuki.Windowing.Fader.alter_overflows('hidden')
    new Effect.Opacity('Fader', {duration: 0, from: 0.7, to: 0.7})
  },
  
  alter_selects: function(visible, selector)
  {
    if (!(/MSIE/.test(navigator.userAgent) && !window.opera)) return
    if (!selector) selector = 'select'
    
    $$(selector).each(function(element)
    {
      element.setStyle({ visibility: visible })
    })
  },
  
  alter_overflows: function(overflow_setting)
  {
    $$('.overflow').each(function(element)
    {
      element.setStyle({ overflow: overflow_setting })
    })
  },
  
  remove: function()
  {
    if (!$('Fader')) return
    
    new Effect.Opacity('Fader', {duration: 0.2, from: 0.7, to: 0.0, afterFinish: function()
    {
      $('Fader').remove()
      Azuki.Windowing.Fader.alter_selects('visible')
      Azuki.Windowing.Fader.alter_overflows('auto')
    }})
  }
}

Event.observe(window, 'resize', function()
{
  if (!$('Fader')) return
  
  Azuki.Windowing.Fader.set_height()
})

/* END fader.js */

/* START contextual_help.js */
Azuki.Windowing.ContextualHelp = Class.create()
Azuki.Windowing.ContextualHelp.prototype = {
  initialize: function(help_class)
  {
    this.help_class = help_class ? help_class : 'help'
    
    Event.observe(document, 'click', function(e)
    {
      var element = $(Event.element(e))
      if (!element.hasClassName(this.help_class)) return
      if (element.nodeName != 'A') return
      
      this.display_help(element.href)
      
      Event.stop(e)
      return false
    }.bind(this))
  },
  
  display_help: function(url)
  {
    var image_format = Azuki.Helpers.Compatibility.suitable_image_format()
    var faded = Azuki.Windowing.Fader.active()

    if (Azuki.Windowing.Busybox.active()) return

    if ($('HelpContainer')) $('HelpContainer').remove()
    
    Azuki.Windowing.Busybox.busy('loading')
    new Insertion.Top(document.body, '<div id="HelpContainer" class="popinfo_container" style="display: none"><div id="HelpClose" style="position: absolute; left: -15px; top: -15px; cursor: pointer"><img width="36" height="36" border="0" src="/images/azuki/close.' + image_format + '"/></div><div id="HelpContent"></div></div>')
    
    Event.observe($('HelpClose'), 'click', function() { $('HelpContainer').remove(); if (!faded) { Azuki.Windowing.Fader.remove() } })
    
    new Ajax.Updater('HelpContent', url, { insertion: Insertion.Bottom, onComplete: function()
    {
      Azuki.Windowing.Busybox.done()
      Azuki.Helpers.Window.center('HelpContainer')
      if (!faded) Azuki.Windowing.Fader.fade()
      new Effect.Appear($('HelpContainer'), {duration: 0.3})
    }})
  }
}
/* END contextual_help.js */

/* START busybox.js */
Azuki.Windowing.Busybox = {
  /* operation can be 'loading', 'saving', etc. depending on the images you've got. */
  busy: function(operation)
  {
    this.remove()
    var image_format = Azuki.Helpers.Compatibility.suitable_image_format()

    new Insertion.Top(document.body, '<div id="Busybox" style="display: none"><img width="138" height="81" src="/images/azuki/' + operation + '.' + image_format + '" /></div>')
    Azuki.Helpers.Window.center('Busybox')
    $('Busybox').show()
  },
  
  remove: function()
  {
    if ($('Busybox')) $('Busybox').remove()
  },
  
  done: function()
  {
    new Effect.Fade($('Busybox'), { afterFinish: function() { Azuki.Windowing.Busybox.remove() }})
  },
  
  active: function()
  {
    return $('Busybox') ? true : false
  }
}

/* END busybox.js */

/* START table_ruler.js */
/* Not implemented yet */
/* END table_ruler.js */

/* START color.js */
Azuki.Style.Color = {}
Azuki.Style.Color.RGB = Class.create()
Azuki.Style.Color.RGB.prototype = {
  initialize: function(value)
  {
    this.colors = Array(0, 0, 0)
    
    switch (typeof(value))
    {
      case 'string':
        this.set_from_rgb_string(value)
      break
      
      case 'object':
        this.colors = value
      break
    }
  },
  
  red: function() { return this.colors[0] },
  green: function() { return this.colors[1] },
  blue: function() { return this.colors[2] },

  set_red: function(red) { this.colors[0] = red },
  set_green: function(green) { this.colors[1] = green },
  set_blue: function(blue) { this.colors[2] = blue },

  /* Assumes rgb(1, 2, 3) */
  set_from_rgb_string: function(value)
  {
    this.colors = $A(value.replace(/rgb\(/, '').replace(/\)/, '').split(',')).collect(function(value)
    {
      return parseInt(value)
    })
  },

  to_s: function()
  {
    return 'rgb(' + this.red() + ',' + this.green() + ',' + this.blue() + ')'
  },

  to_hex: function()
  {
    var hex = ''
  
    $A(this.colors).each(function(colour)
    {
      var value = this._hex_value(colour)
    
      if (value.length == 1) { value = '0' + value }
    
      hex = hex + value
    }.bind(this))
  
    return hex
  },

  _hex_value: function(d)
  {
    var hex_map = '0123456789ABCDEF'
    var h = hex_map.substr(d&15, 1)
  
    while (d > 15) { d >>= 4; h = hex_map.substr(d&15, 1) + h }
    return h
  }
}

Azuki.Style.Color.Hex = Class.create()
Azuki.Style.Color.Hex.prototype = {
  /* Create with values like this: '#000000' */
  initialize: function(value)
  {
    this.value = value
  },
  
  to_rgb: function()
  {
    this._set_rgb_values()
    var rgb_array = $A([this.red, this.green, this.blue]).collect(function(color)
    {
      return this._rgb_value(color)
    }.bind(this))
    
    return new Azuki.Style.Color.RGB(rgb_array)
  },
  
  _rgb_value: function(hex)
  {
    return parseInt(hex, 16) || 0
  },
  
  _set_rgb_values: function()
  {
    this.red   = this.value.charAt(1) + this.value.charAt(2)
    this.green = this.value.charAt(3) + this.value.charAt(4)
    this.blue  = this.value.charAt(5) + this.value.charAt(6)
  }
}

Azuki.Style.Color.Methods = {
  invert: function(value)
  {
    var color = new Azuki.Style.Color.RGB(value)
    color.set_red(255 - parseInt(color.red()))
    color.set_green(255 - parseInt(color.green()))
    color.set_blue(255 - parseInt(color.blue()))
    return color.to_s()
  },
  
  random: function()
  {
    var color = new Azuki.Style.Color.RGB(Array(Math.round((Math.random() * 255)), Math.round((Math.random() * 255)), Math.round((Math.random() * 255))))
    return color.to_s()
  }
}

Azuki.Style.Color.ElementMethods = {
  invertColor: function(element, property)
  {
    try
    {
      element.style[property] = Azuki.Style.Color.invert(element.getStyle(property))
      return true
    }
    catch (exception)
    {
      return false
    }
  },
  
  randomColor: function(element, property)
  {
    try
    {
      element.style[property] = Azuki.Style.Color.random()
      return true
    }
    catch (exception)
    {
      return false
    }
  }
}


Object.extend(Azuki.Style.Color, Azuki.Style.Color.Methods)
Element.addMethods(Azuki.Style.Color.ElementMethods)
/* END color.js */

/* START cookie.js */
Azuki.Storage.Cookie = {
  create: function(name, value, days, path)
  {
    var expires = ''
    
    path = typeof path == 'undefined' ? '/' : path
    
    if (days)
    {
      var date = new Date()
      date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000))
      expires = "; expires=" + date.toGMTString()
    }

    if (name && value)
    {
      document.cookie = name + '=' + escape(value) + expires + '; path=' + path
    }
  },
  
  find: function(name)
  {
    var matches = document.cookie.match(name + '=([^;]*)')

    if (matches && matches.length == 2)
    {
      return unescape(matches[1])
    }
  },
  
  destroy: function(name)
  {
    this.create(name, ' ', -1)
  }
}
/* END cookie.js */

/* START window_helper.js */
Azuki.Helpers.Window = {
  size: function()
  {
    var width, height
    
    if (self.innerHeight)
    {
      width  = self.innerWidth
      height = self.innerHeight
    }
    else if (document.documentElement && document.documentElement.clientHeight)
    {
      // IE 6 Strict Mode
      width  = document.documentElement.clientWidth
      height = document.documentElement.clientHeight
    }
    else if (document.body)
    {
      // IE
      width  = document.body.clientWidth
      height = document.body.clientHeight
    }
    
    return {width: width, height: height}
  },
  
  page_size: function()
  {
    var x_scroll, y_scroll
    var width, height
    var window_size = Azuki.Helpers.Window.size()

    if (window.innerHeight && window.scrollMaxY)
    {
      x_scroll = document.body.scrollWidth
      y_scroll = window.innerHeight + window.scrollMaxY
    }
    else if (document.body.scrollHeight > document.body.offsetHeight)
    {
      x_scroll = document.body.scrollWidth
      y_scroll = document.body.scrollHeight
    }
    else
    {
      x_scroll = document.body.offsetWidth
      y_scroll = document.body.offsetHeight
    }

    width  = x_scroll < window_size.width  ? window_size.width  : x_scroll
    height = y_scroll < window_size.height ? window_size.height : y_scroll

    return {width: width, height: height}
  },
  
  /* Centres absolute positions elements */
  center: function(element)
  {
    var options = Object.extend({update: false}, arguments[1] || {})
    element = $(element)

    Position.prepare()

    var offset_x = (Position.deltaX + Math.floor((Azuki.Helpers.Window.size().width - element.getDimensions().width) / 2))   || '0'
    var offset_y = (Position.deltaY + Math.floor((Azuki.Helpers.Window.size().height - element.getDimensions().height) / 2)) || '0'

    element.setStyle({ left: offset_x + 'px' })
    element.setStyle({ top:  offset_y + 'px' })

    if (options.update)
    {
      Event.observe(window, 'resize', function() { Position.center(element) })
      Event.observe(window, 'scroll', function() { Position.center(element) })
    }
  },
  
  center_with_margin: function(element)
  {
    var element = $(element)
    var container = element.up()
    var margin = (container.getWidth() - element.getWidth()) / 2
    element.setStyle({ marginLeft: margin + 'px', marginRight: margin + 'px'})
  }
}

/* END window_helper.js */

/* START keyboard_helper.js */
Azuki.Helpers.Keyboard = {
  which_meta_key: function()
  {
    var key = ''
  
    if (navigator.platform.match(/win/i))
    {
      key = 'alt'
    }
    else if (navigator.platform.match(/mac/i))
    {
      key = 'ctrl'
    }
    else
    {
      key = 'ctrl or alt'
    }
  
    return key
  },

  underline_accesskey: function(element)
  {
    var element = $(element)
    var key = this.which_meta_key()
    var regex = new RegExp(element.accessKey, 'i')
    var match = ''

    if (element.accessKey.length == 0 || element.hasClassName('noaccesskey') > 0) return

    if (element.tagName == 'A' && !element.innerHTML.match(/class=.?accesskey/))
    {
      match = element.innerHTML.match(regex)
      element.innerHTML = element.innerHTML.replace(regex, '<span class="accesskey" title="Press ' + key + '-' + String(match).toLowerCase() + ' to access this link using the keyboard">' + match + '</span>')
    }
    else if (element.tagName == 'INPUT')
    {
      element.value = element.value + ' [' + key + '+' + String(element.accessKey).toLowerCase() + ']'
    }
    else if (element.tagName == 'BUTTON')
    {
      var text = ''
      
      /* Remove text nodes from the button, this assumes the button has a format like: image text */
      $A(element.childNodes).each(function(child)
      {
        if (child.nodeName == '#text')
        {
          text = text + child.data
          element.removeChild(child)
        }
      })
      
      match = text.match(regex)
      new Insertion.Bottom(element, text.replace(regex, '<span class="accesskey" title="Press ' + key + '-' + String(match).toLowerCase() + ' to access this link using the keyboard">' + match + '</span>'))
    }
  },

  underline_accesskeys: function()
  {
    this.underline_tag_accesskeys('a')
    this.underline_tag_accesskeys('button')
    this.underline_tag_accesskeys('input')
  },

  underline_tag_accesskeys: function(tag_name)
  {
    var key = this.which_meta_key()
    $A(document.getElementsByTagName(tag_name)).each(function(element)
    {
      this.underline_accesskey(element)
    }.bind(this))
  }
}

/* END keyboard_helper.js */

/* START forms_helper.js */
Azuki.Helpers.Forms = {
  /* Text helpers */
  set_caret_position: function(element, pos)
  {
    if (element.setSelectionRange)
    {
      element.focus()
      element.setSelectionRange(pos, pos)
    }
    else if (element.createTextRange)
    {
      var range = element.createTextRange()

      range.collapse(true)
      range.moveEnd('character', pos)
      range.moveStart('character', pos)
      range.select()
    }
  },
  
  get_caret_position: function(element)
  {
    if (element.setSelectionRange)
    {
      return element.selectionStart
    }
    else if (element.createTextRange)
    {
      // The current selection
      var range = document.selection.createRange()
      // We'll use this as a 'dummy'
      var stored_range = range.duplicate()
      // Select all text
      stored_range.moveToElementText(element)
      // Now move 'dummy' end point to end point of original range
      stored_range.setEndPoint('EndToEnd', range)

      return stored_range.text.length - range.text.length
    }
  },

  activate_controls: function(names)
  {
    $A(document.getElementsByTagName('input')).each(function(element)
    {
      $A(names).each(function(name)
      {
        if (element.name == name)
        {
          element.disabled = false
        }
      })
    })
  },
  
  disable_on_submit: function()
  {
    $$('form').each(function(form)
    {
      var submit = $A(form.getElementsByTagName('input')).find(function(input) { return input.type == 'submit' } )
      
      if (!submit) return
      
      Event.observe(form, 'submit', function(e)
      {
        submit = $(submit)
        if (submit && !submit.hasClassName('nodisable')) submit.disable()
      })
    })
  }
}

/* END forms_helper.js */

/* START compatibility_helper.js */
Azuki.Helpers.Compatibility = {
  /* Rather than hacking IE's png transparency, we currently use this to switch between gif and png. */
  suitable_image_format: function()
  {
    if (!navigator.appVersion.match(/MSIE/)) return 'png'
    
    try
    {
      var version = parseFloat(navigator.appVersion.split('MSIE')[1])
      return (version < 7.0) ? 'gif' : 'png'
    }
    catch (exception)
    {
      return 'png'
    }
  }
}

/* END compatibility_helper.js */

/* START textarea_extensions.js */
Azuki.Forms.TextAreaExtensions = Class.create()
Azuki.Forms.TextAreaExtensions.prototype = {
  initialize: function()
  {
    this._add_images()
    
    this.bigger = $$('.bigger')
    this.smaller = $$('.smaller')
  
    // Add event observer to all more buttons
    this.bigger.each(function(item)
    {
      Event.observe(item, 'click', this.increase_rows.bindAsEventListener(this))
    }.bind(this))
  
    // Add event observer to all less buttons
    this.smaller.each(function(item)
    {
      Event.observe(item, 'click', this.decrease_rows.bindAsEventListener(this))
    }.bind(this))
  },

  increase_rows: function(e)
  {
    var element = $(Event.element(e))
    var textarea = this._get_next_textarea(element)
    textarea.rows += 5
  },

  decrease_rows: function(e)
  {
    var element = $(Event.element(e))
    var textarea = this._get_next_textarea(element)
    
    if (textarea.rows >= 5)
    {
      textarea.rows -= 4
    }
  },
  
  /** Private methods **/
  _get_next_textarea: function(element)
  {
    var children = $A(element.parentNode.parentNode.childNodes)

    return children.find(function(child) { return child.nodeName == 'TEXTAREA' })
  },
  
  _add_images: function()
  {
    $$('textarea').each(function(textarea)
    {
      new Insertion.Before(textarea, '<span class="resizer"><img class="bigger" alt="Increase the size of this text box" src="/images/azuki/open.png"/><img class="smaller" alt="Decrease the size of this text box" src="/images/azuki/close_small.png"/></span><br/>')
    })
  }
}

/* END textarea_extensions.js */

/* START select_search.js */
Azuki.Forms.SelectSearch = Class.create()
Azuki.Forms.SelectSearch.prototype = {
  initialize: function(search_input, search_results)
  {
    this.search_input = $(search_input)
    this.search_results = $(search_results)
    this.items = this.read_values()
    
    new Event.observe(this.search_input, 'click', this.clear_search_field.bindAsEventListener(this))
    new Event.observe(this.search_input, 'keydown', this.search.bindAsEventListener(this))
  },
  
  clear_search_field: function()
  {
    this.search_input.value = ''
    this.items.each(function(option, i)
    {
      this.search_results.options[i] = new Option(option.text, option.value)
    }.bind(this))
  },
  
  read_values: function()
  {
    return $A(this.search_results.options).collect(function(option)
    {
      return {value: option.value, text: option.innerHTML}
    })
  },
  
  search: function()
  {
    var query = this.search_input.value
    var results = this.items.findAll(function(value)
    {
      return value.text.toLowerCase().match(query.toLowerCase())
    })
    
    this.search_results.options.length = 0
    results.each(function(option, i)
    {
      this.search_results.options[i] = new Option(option.text, option.value)
    }.bind(this))
  }
}

/* END select_search.js */

/* START remote.js */

Azuki.Forms.Remote = Class.create()
Azuki.Forms.Remote.prototype = {
  initialize: function(options)
  {
    this.options = {
      form_id:                'RemoteForm',
      controller_name:        '',
      item_name:              '',
      method:                 'post',
      enctype:                null,
      ajax:                   false,
      add_close_button:       true,
      form_visible:           false,
      form_display_callback:  false,
      add_completed_callback: null
    }
    Object.extend(this.options, options || { })
    
    this.options.edit_url    = '/' + this.options.controller_name + '/edit/'
    this.options.new_url     = '/' + this.options.controller_name + '/new'
    this.options.create_url  = '/' + this.options.controller_name + '/create'
    this.options.update_url  = '/' + this.options.controller_name + '/update/'
    this.options.destroy_url = '/' + this.options.controller_name + '/destroy/'
  },
  
  edit: function(e)
  {
    var element = Event.element(e)
    
    if (element.nodeName == 'IMG')
    {
      element = element.up('.edit_' + this.options.item_name)
    }
    
    if (!element) return
    
    if ((element.nodeName == 'A' || element.nodeName == 'BUTTON') && element.hasClassName('edit_' + this.options.item_name))
    {
      var item_id = element.id.match(/\d+$/)
      
      this.show_form(this.options.form_id, this.options.edit_url + item_id, this.options.update_url + item_id, this.options.form_display_callback)
      Event.stop(e)
      return false
    }
  },
  
  add: function(e)
  {
    var element = Event.element(e)

    if (element.nodeName == 'IMG')
    {
      element = element.up('.add_' + this.options.item_name)
    }

    if (!element) return

    if ((element.nodeName == 'A' || element.nodeName == 'BUTTON') && element.hasClassName('add_' + this.options.item_name))
    {
      if (this.options.add_callback) this.options.add_callback()
      this.show_form(this.options.form_id, this.options.new_url, this.options.create_url, this.options.form_display_callback)
      Event.stop(e)
      return false
    }
  },
  
  destroy: function(e)
  {
    var element = Event.element(e)
    if ((element.nodeName == 'A' || element.nodeName == 'IMG') && element.hasClassName('destroy_' + this.options.item_name))
    {
      var item_id = element.id.match(/\d+$/)
      
      if (confirm('Are you sure you want to delete that item?'))
      {
        window.location = this.options.destroy_url + item_id
      }

      Event.stop(e)
      return false
    }
  },

  show_form: function(id, url, save_url, callback)
  {
    if (this.options.form_visible)
    {
      return
    }
    else
    {
      this.options.form_visible = true
    }
    
    Azuki.Windowing.Busybox.busy('loading')
    
    function close_form()
    {
      if ($(id))
      {
        $(id).remove()
        Azuki.Windowing.Fader.remove()
      }
    }
    
    function focus_field()
    {
      try
      {
        if ($(id)) $(id).focusFirstElement()
      }
      catch (exception)
      {
        
      }
    }

    var fields_id = id + 'FormFields'
    var cancel_id = 'Cancel' + id
    var editor_html = ''
    var image_format = Azuki.Helpers.Compatibility.suitable_image_format()
    var enctype = typeof this.options.enctype == 'undefined' ? '' : 'enctype="multipart/form-data"'
    
    close_form()

    editor_html += '<form style="display: none"' + this.ajax_form_html(save_url, id) + ' method="' + this.options.method + '" ' + enctype + ' action="' + save_url + '" class="edit standard" id="' + id + '">'
    if (this.options.add_close_button) editor_html += '  <div id="' + cancel_id + '" style="position: absolute; left: -15px; top: -25px; cursor: pointer"><img alt="Cancel without saving" style="cursor: pointer" width="36" height="36" border="0" src="/images/close.' + image_format + '"/></div>'
    editor_html += '  <div id="' + fields_id + '"></div>'
    editor_html += '</form>'
    
    // Insert the editor HTML
    new Insertion.Top(document.body, editor_html)
    
    // Get the form fields from the server
    new Ajax.Updater($(fields_id), url, {
      evalScripts: true,
      method: 'get',
      onComplete: function()
      {
        Azuki.Helpers.Window.center(id)
        new Effect.Appear($(id), { duration: 0.3, afterFinish: function() { Azuki.Windowing.Fader.alter_selects('visible'); focus_field() } })
        Azuki.Windowing.Fader.fade()
        Azuki.Windowing.Busybox.done()
        
        if (this.options.ajax)
        {
          try
          {
            Event.observe(id, 'submit', this.ajax_submit.bindAsEventListener(this))
          }
          catch(e)
          {
          }
        }
        
        Event.observe(cancel_id, 'click', function(e) { close_form(id); this.options.form_visible = false; Event.stop(e); return false }.bind(this))

        if (callback) callback()
      }.bind(this)
    })
  },
  
  ajax_form_html: function()
  {
    if (this.options.ajax == false) return ''
    
    return ' onsubmit="return false" '
  },
  
  ajax_submit: function()
  {
    this.options.form_visible = false
    new Ajax.Request(this.options.create_url, { onSuccess: this.options.ajax_on_succes, postBody: Form.serialize(this.options.form_id), onFailure: this.options.ajax_on_failure })
    $(this.options.form_id).remove()
    Azuki.Windowing.Fader.remove()
    return false
  }
}
/* END remote.js */

/* START controller.js */
Azuki.Controller = {
  run: function()
  {
    if (document.body.id) Azuki.Controller.run_controller(document.body.id)
    Azuki.Controller.run_controller('Application')
  },
  
  run_controller: function(id)
  {
    var controller_class = id + 'Controller'
    
    if (!window[controller_class]) return

    if (eval(controller_class + '.run'))
    {
      eval(controller_class + '.run()')
    }
    else
    {
      controller = new window[controller_class]
    }
  }
}

function init()
{
  // quit if this function has already been called
  if (arguments.callee.done) return

  // flag this function so we don't do the same thing twice
  arguments.callee.done = true

  // kill the timer
  if (_timer) clearInterval(_timer)

  // do stuff
  Azuki.Controller.run()
}

/* for Mozilla/Opera9 */
if (document.addEventListener)
{
  document.addEventListener("DOMContentLoaded", init, false)
}

/* for Internet Explorer */
/*@cc_on @*/
/*@if (@_win32)
  var void_src_url = location.protocol == "https:" ?  "https://javascript:void(0)" : "javascript:void(0)";
  var script = document.createElement("script");
  document.write("<script id=__ie_onload defer src='" + void_src_url + "'><\/script>")
  
  script.onreadystatechange = function()
  {
    if (this.readyState == "complete")
    {
      init() // call the onload handler
    }
  }
/*@end @*/

/* for Safari */
if (/WebKit/i.test(navigator.userAgent))
{
  var _timer = setInterval(function()
  {
    if (/loaded|complete/.test(document.readyState))
    {
      init() // call the onload handler
    }
  }, 10)
}

/* for other browsers */
window.onload = init


/* END controller.js */

/* END azukilib.js */

/* START application_controller.js */
var ApplicationController = new Object()

ApplicationController.Methods = {
  run: function()
  {
    Event.observe(document, 'mouseover', function(e) {
      var element = Event.element(e)
      if (element && element.id == 'SettingsLink') {
        var settings_help = $('SettingsHelp')
        if (!settings_help.visible()) {
          new Effect.Appear(settings_help, { duration: 0.3 })
        }
      }
    })
    
    Event.observe(document, 'mouseout', function(e) {
      var element = Event.element(e)
      if (element && element.id == 'SettingsLink') {
        var settings_help = $('SettingsHelp')
        new Effect.Fade(settings_help, { duration: 0.3 })
      }
    })
  }
}

Object.extend(ApplicationController, ApplicationController.Methods)
/* END application_controller.js */
