/* Project:   HereComesTheCard 
 * File:      admin.js
 * Author:    Manuel Holtgrewe <purestorm at ggnore dot net>
 * Copyright: (c) 2006 by TuringStudio
 *
 * This JavaScript file defines the common JavaScript code that is necessary
 * to support the admin templates.
 *
 * DEPENDENCY
 *
 *   - This depends on the Prototype and Scriptaculous library.
 */

/**
 * The BrowserDetector class provides information about the user's browser.
 *
 * It follows the Singleton pattern. You can retrieve the Singleton instance
 * with the "getInstance()" method of the BrowserDetector class.
 *
 * You can access the following object properties:
 *
 *  - browser  = The name of the browser. One of "Safari", "OmniWeb", "Opera",
 *               "WebTV", "iCab", "Internet Explorer", "Netscape Navigator", 
 *               "an unknown browser".
 *  - version  = The version of the browser.
 *  - os       = The Operating System. Can be one of "Linux", "Unix", "Mac",
 *               "Windows", "an unknown operating system".
 *
 * Based on the work of Peter Paul Koch at http://www.quirksmode.org/js/detect.html
 *
 * Note that this class is very rough in terms of OOP since it has been extracted
 * from procedural JS code. It's also very rough in terms of the elegance
 * that JS offers.
 *
 * TODO: Move all this so we have a global BrowserDetector variable that
 * provides the os, version and browser information.
 */
// Instance Methods
var BrowserDetector = Class.create();
BrowserDetector.prototype = {
  
  os: null,
  detect : null,
  browser : null,
  version : null,
  thestring : null,
  
  /**
   * The constructor automatically runs the browser detection.
   */
  initialize : function() {
    this.detect = navigator.userAgent.toLowerCase();
    this.runDetection();
  },
  
  /**
   * Must be registered as a handler for the "load" event of the <body>.
   *
   * Sets the object's properties.
   */
  runDetection : function() {
    if (this.checkIt('konqueror')) {
      this.browser = "Konqueror";
      this.os = "Linux";
    }
    else if (this.checkIt('safari')) this.browser   = "Safari"
    else if (this.checkIt('omniweb')) this.browser   = "OmniWeb"
    else if (this.checkIt('opera')) this.browser     = "Opera"
    else if (this.checkIt('webtv')) this.browser     = "WebTV";
    else if (this.checkIt('icab')) this.browser     = "iCab"
    else if (this.checkIt('msie')) this.browser     = "Internet Explorer"
    else if (!this.checkIt('compatible')) {
      this.browser = "Netscape Navigator"
      this.version = this.detect.charAt(8);
    }
    else this.browser = "an unknown browser";

    if (!this.version) this.version = this.detect.charAt(this.place + this.thestring.length);

    if (!this.os) {
      if (this.checkIt('linux')) this.os     = "Linux";
      else if (this.checkIt('x11')) this.os   = "Unix";
      else if (this.checkIt('mac')) this.os   = "Mac"
      else if (this.checkIt('win')) this.os   = "Windows"
      else this.os                 = "an unknown operating system";
    }
  },

  checkIt : function(string) {
    this.place = this.detect.indexOf(string) + 1;
    this.thestring = string;
    return this.place;
  }
}

// Instance Methods

/**
 * @return Returns the Singleton BrowserDetector instance.
 */
BrowserDetector.getInstance = function() {
  if (this.instance == null)
    this.instance = new BrowserDetector();
  
  return this.instance;
}


/**
 * Groups several <div>'s logically like a RadioGroup.
 *
 * This means that when you use the class' toggle(String) method then you can
 * be sure that only one of the <div>'s in this group is shown at any time.
 *
 * UsageExample:
 *
 * <pre>
 * theRadioDiv = new RadioDiv("id1", "id2", "id3");
 *
 * // later in your code
 * <a href="javascript:theRadioDiv.toggle('id1');">toggle!</a>
 * </pre>
 */
var RadioDiv = Class.create();
RadioDiv.prototype = {
  /**
   * Expects an arbitrary number of arguments, each being the id of a div (or 
   * in fact any other element).
   */
  initialize: function() {
    this.divs = new Array();
    
    args = this.initialize.arguments;
    
    for (var i = 0; i < args.length; i++) {
      this.divs.push(args[i]);
    }
  },
  
  /**
   * Toggles the visibility of the dive with the given id.
   *
   * @param id The id of the <div> to toggle.
   */
  toggle: function(id) {
    this.divs.each(function(div) {
      if (div != id) {
        Element.hide(div);
      }
    });

    Element.toggle(id);
  }
}

/**
 * Checks all checkbox elements whose "name" attribute begins with prefix
 * in the form with id "form".
 */
function toggleAllBoxes(form, prefix) {
  Form.getElements(form).each(function(element){
    if (element.type == "checkbox")
      if (element.name.indexOf(prefix) == 0)
        element.checked = !element.checked;
  })
}

/**
 * SearchBox objects manage text input fields so they can work as search boxes.
 */
var SearchBox = Class.create();
SearchBox.prototype = {
  /**
   *
   * @param element_id
   *          The id of the text input box in the search form with the search terms.
   * @param result_div_id
   *          The id of the result div.
   * @param go_button_id
   *          The id of the "GO" button.
   * @param initial_text
   *          The intial search term that is cleared when the cursor enters the input.
   * @param activityIndicator
   *          The ActivityIndicator object to use for notifying the user about activity.
   */
  initialize: function(element_id, go_button_id, result_div_id, initial_text, activityIndicator) {
    this.elementId = element_id;
    this.goButtonId = go_button_id;
    this.resultDivId = result_div_id;
    this.initialText = initial_text;
    $(this.elementId).value = initial_text;
    this.activityIndicator = activityIndicator;
    
    this.setupEvents();
    this.setupStyles();
  },
  
  hideResults: function(tryNew) {
    new Effect.Fade(this.resultDivId);
    if (tryNew) {
      $(this.elementId).focus();
    }
  },
  
  /**
   * Executes the search with the terms in the configured element.
   *
   * Will fade the search result box before starting the search.
   */
  performSearch: function() {
    this.activityIndicator.activityStarted();
    
    var url = '/admin/search/livesearch';
    var pars = 'term=' + $(this.elementId).value;
    
    var theAjax = new Ajax.Updater(
      { success: this.resultDivId },
      url,
      {
        method: 'post',
        parameters: pars,
        onSuccess: this.displaySearchResults.bind(this),
        onFailure: this.handleSearchError.bind(this)
      }
      );
  },
  
  handleSearchError: function(request) {
    var url = '/admin/search/failure';
    
    var theAjax = new Ajax.Updater(
      { success: this.resultDivId },
      url,
      {
        method: 'get',
        onFailure: function() { 
          alert('There was an unrecoverable error. Please contact an admin if this error persists.'); 
          this.activityIndicator.activityStopped(); 
        },
        onSuccess: this.displaySearchResults.bind(this)
      }
      );
  },
  
  /**
   * Displays the search result <div>.
   *
   * @param request
   *          The AJAX request object.
   */
  displaySearchResults: function(request) {
    new Effect.Appear(this.resultDivId);

    this.activityIndicator.activityStopped(); 
  },
  
  /**
   * This method will make the search box look nice.
   *
   * Currently, this will only have an effect in Safari. We will set the
   * input's "type" attribute to "search" there and add the "safari" class
   * to all search widgets.
   */ 
  setupStyles : function() {
    if (BrowserDetector.getInstance().browser == "Safari") {
      $(this.elementId).setAttribute('type', 'search');
      $(this.elementId).setAttribute('results', '5');
      
      Element.addClassName(this.activityIndicator.activityImgId, 'safari');
      Element.addClassName(this.elementId, 'safari');
      Element.addClassName(this.goButtonId, 'safari');
    }
  },
  
  /**
   * Registers this object as the handler for the focus, blur and keydown
   * event. This makes specifying these as attributes in HTML superflous.
   */
  setupEvents : function() {
    Event.observe(this.elementId, 'focus', this.onFocus.bindAsEventListener(this), false);
    Event.observe(this.elementId, 'blur', this.onBlur.bindAsEventListener(this), false);
    Event.observe(this.elementId, 'keydown', this.onKeyDown.bindAsEventListener(this), false);
    
    // select the field's content on key up
    Event.observe(this.elementId, 'keyup', this.onKeyUp.bindAsEventListener(this), false);
  },
  
  /**
   * Submit the search when [enter] was pressed.
   *
   * @params e
   *           The event object for this event.
   */
  onKeyDown : function(e) {
    var kc = e.keyCode;
    if (e.keyCode == Event.KEY_RETURN) {
      this.performSearch();
    }
  },
  
  /**
   * Selects the text in the search field.
   *
   * @params e
   *           The event object for this event.
   */
  onKeyUp : function(e) {
    if (e.keyCode == Event.KEY_RETURN)
      $(this.elementId).select() 
  },
  
  /**
   * Clears the text input if the inpUt's value is the initial one.
   *
   * @params e
   *           The event object for this event.
   */
  onFocus: function(e) {
    if ($(this.elementId).value == this.initialText) {
      $(this.elementId).value = '';
    }
  },
  
  /**
   * Sets the text input's value to the initial one on leaving if the text input's value is "".
   *
   * @params e
   *           The event object for this event.
   */
  onBlur: function(e) {
    if ($(this.elementId).value == '') {
      $(this.elementId).value = this.initialText;
    }
  }
}

/**
 * The ActivityIndicator class allows displaying and hiding a status indicator.
 * You can increment and decrement the count of currently running action with
 * the activityStarted() and activityStopped() methods. The count is 0 initially.
 * When it is bigger than 0 then a status indicator object will hide the activity
 * indicating div.
 */
var ActivityIndicator = Class.create();
ActivityIndicator.prototype = {
  /**
   * @param activityImgId
   *          The id of the <div> displaying the activity indicator.
   */
  initialize: function(activityImgId) {
    this.activityImgId = activityImgId;
    this.activityCount = 0;
  },
  
  /**
   * Increments the counter of currently active activities.
   */
  activityStarted: function() {
    this.activityCount += 1;
    this.updateIndicator();
  },
  
  /**
   * Decrements the counter of currently active activities.
   */
  activityStopped: function() {
    this.activityCount -= 1;
    this.updateIndicator();
  },
  
  /**
   * Displays/hides the activity indicator div depending on the current
   * activity count.
   */
  updateIndicator: function() {
    if (this.activityCount == 0) {
      new Effect.Fade(this.activityImgId);
    } else {
      new Effect.Appear(this.activityImgId);
    }
  }
}

/**
 * Sends an XmlHttpRequest to the delete action of the given controller.
 *
 * The result will be displayed in a LightBox.
 *
 * @param formId
 *          The id of the form to delete the selected items from.
 * @param controller
 *          The name of the controller we want to post to.
 */
function deleteSelected(formId, controller) {
  params = Form.serialize(formId);
  
  var sendRequest = function(handler) {
    var myAjax = new Ajax.Request(
      '/admin/' + controller + '/delete',
      {
        method: 'get',
        parameters: params,
        onComplete: handler
      }
      );
  };
  
  LightBox.getInstance().displayAjaxResult(sendRequest);
}

/* The following lines force the displayed image to fit into
 * the browser window (+ "border" px margin on each side).
 *
 * CURRENTLY NOT USED
 */
function updateImageSize(image, border) {
  // Only resize if the image has been loaded successfully and is available at all.
  if (typeof $(image) == "undefined" || typeof $(image).naturalWidth == "undefined")
    return;
  
  var windowDimension = { width : window.innerWidth, height : window.innerHeight };
  var imageDimension = { width: $(image).naturalWidth, 
                         height: $(image).naturalHeight };

  // determine which required scaling is more constraining
  var factorX = 1;
  var factorY = 1;

  if (imageDimension.width > windowDimension.width - 2 * border)
    factorX = (windowDimension.width - 2 * border) / imageDimension.width;
  if (imageDimension.height > windowDimension.height - 2 * border)
    factorY = (windowDimension.height - 2 * border) / imageDimension.height;

  var factor = Math.min(factorX, factorY);
//  alert(imageDimension.width);
//  alert(factor);

  $(image).style.width = (imageDimension.width * factor) + "px";
  $(image).style.height = (imageDimension.height * factor) + "px";
  
  // do not check whether we should resize the image periodically any more
//  theImageResizer.stopExecuting();
}

/**
 * Safari does not feature clickeable <label>s. This method fixes the problem.
 *
 * Code taken from http://www.chriscassell.net/log/2004/12/19/add_label_click.html
 */
function addLabelFocus() {
  var item = document.getElementById(this.getAttribute("for"));
  item.focus();
  if (item.getAttribute("type") == "checkbox") {
    if (!item["checked"]) {
      item["checked"] = true;
    } else {
      item["checked"] = false;
    }
  } else if (item.getAttribute("type") == "radio") {
    var allRadios = document.getElementsByTagName("input");
    var radios = new Array();
    for (i = 0; i < allRadios.length; i++) {
      if (allRadios[i].getAttribute("name") == item.getAttribute("name")) {
        radios.push(allRadios[i]);
      }
    }
    for (i = 0; i < radios.length; i++) {
      if (radios[i]["checked"] && radios[i].getAttribute("id") != item.getAttribute("id")) {
        radios[i]["checked"] = false;
      }
    }
    item["checked"] = true;
  }
}
function fixSafariLabels() {
  if (navigator.userAgent.indexOf("Safari") > 0) {
    var labels = document.getElementsByTagName("label");
    for (i = 0; i < labels.length; i++) {
      labels[i].addEventListener("click", addLabelFocus, false);
    }
  }
} 
Event.observe(window, 'load', fixSafariLabels, false);

var ColorEditor = Class.create();
ColorEditor.prototype = {
  initialize: function(prefix) {
    this.prefix = prefix;
    this.formId = prefix + "-form";
    this.ulId = prefix + "-list";
    this.localColorCount = 1;
    this.colorSelectors = new Array();
  },
  
  /**
   * Adds a form line for color editor detail view.
   *
   * @param offset
   *          The number of colors whose lines have been created by the action
   *          on the server. Required since we do not want any duplicate entries.
   */
  addColor: function(offset) {
    this.localColorCount += 1;
    $(this.ulId).appendChild(this.buildLine(offset + this.localColorCount));
  },
  
  /**
   * The detail view from the server may contain color editor entries. Their
   * onclick attribute refers removeRemoteColor(). This method will remove the
   * line with the inputs for the given color and add a hidden input field
   * in the form which will contain the color's id as its values.
   *
   * When the form is sumitted then the colors that are contained in this 
   * hidden fields will be removed.
   *
   * @param colorId
   *          The id pkey of the color to be deleted.
   * @param lineNo
   *          The number of the line which is to be deleted.
   */
  removeRemoteColor: function(colorId, lineNo) {
    Element.remove(this.prefix + '-' + lineNo + '-line');
    
    var hiddenInput = document.createElement('input');
    hiddenInput.type = "hidden";
    hiddenInput.name = "colors_to_delete[]";
    hiddenInput.value = colorId;
    
    $(this.formId).appendChild(hiddenInput);
  },
  
  /**
   * The form lines that are created with the "Add" button contain a "Remove"
   * button which calls this method "onclick". This method will then remove
   * the the added line again.
   */
  removeLocalColor: function(lineNo) {
    Element.remove(this.prefix + '-' + lineNo + '-line');
  },
  
  /** 
   * Creates the <li> item with content that is used for a line in the
   * form display.
   */
  buildLine: function(lineNo) {
    var li = null;
    
    li = document.createElement('li');
    li.id = this.prefix + '-' + lineNo + '-line';
  
    var hexInput = document.createElement('input');
    hexInput.id = "colors-" + lineNo + "-hex-value";
    hexInput.name = "colors[" + lineNo + "][hex_value]";
    hexInput.type = 'text';
    hexInput.value = 'ffffff';
    hexInput.size = 6;
    Event.observe(hexInput, 'keyup', (function(event) { this.colorChanged(lineNo, true); }).bind(this), false);
  
    var colorSelectorToggleInput = document.createElement('input');
    colorSelectorToggleInput.type = 'button';
    colorSelectorToggleInput.value = 'chooser';
    colorSelectorToggleInput.onclick = (function() { theColorEditor.toggleSelectorFor(lineNo); }).bind(this);
    
    var nameInput = document.createElement('input');
    nameInput.name = "colors[" + lineNo + "][name]";
    nameInput.type = 'text';
    nameInput.value = 'white';
    nameInput.size = 10;
    
    var removeInput = document.createElement('input');
    removeInput.type = 'button';
    removeInput.value = 'remove';
    Event.observe(removeInput, 'click', (function() { Element.remove(this.prefix + '-' + lineNo + '-line'); }).bind(this), false);

    li.appendChild(document.createTextNode('#'));
    li.appendChild(hexInput);
    li.appendChild(document.createTextNode(' ')); // empty space for visual layout
    li.appendChild(colorSelectorToggleInput);
    li.appendChild(document.createTextNode(' ')); // empty space for visual layout
    li.appendChild(nameInput);
    li.appendChild(document.createTextNode(' ')); // empty space for visual layout
    li.appendChild(removeInput);
    
    return li;
  },
  
  /**
   * Called when the color input's value changed by typing. This way we can
   * update the <li>'s background color and the color chooser's value.
   *
   * @param lineNo The number of the line that changed.
   * @param updateColorSelector Boolean. True if the color selector should be 
   *          notified. False otherwise.
   */
  colorChanged : function(lineNo, updateColorSelector) {
    var hexvalue = $('colors-' + lineNo + '-hex-value').value;

    if (/^[a-fA-F0-9]{6,6}$/.exec(hexvalue)) {
      $('colors-' + lineNo + '-line').style.backgroundColor = '#' + hexvalue;

      if (updateColorSelector) {
        this.getColorSelector(lineNo).setHex(hexvalue);
      }
    }
  },
  
  /**
   * Displays a color selector element for the given form line.
   *
   * @param lineNo The line number of the form.
   */
  toggleSelectorFor : function(lineNo) {
    this.getColorSelector(lineNo).toggle();
  },
  
  /**
   * Returns the color selector for the given line.
   */
  getColorSelector : function(lineNo) {
    if (this.colorSelectors[lineNo] == null) {
      this.colorSelectors[lineNo] = new ColorSelector($('colors-' + lineNo + '-hex-value').value);
      colorChangeHandler = function(chooser) {
        $('colors-' + lineNo + '-hex-value').value = chooser.getHex();
        this.colorChanged(lineNo, false);
      }
      this.colorSelectors[lineNo].onColorChange = colorChangeHandler.bind(this);
      this.colorSelectors[lineNo].placeBelow('colors-' + lineNo + '-hex-value');
    }
    
    return this.colorSelectors[lineNo];
  },
  
  /**
   * Validates the things the user entered and displays errors in a <ul>.
   */
  validate : function() {
    var valid = true;
    var emptyNameOccured = false;

    var errorList = $(this.prefix + '-errors');
    while (errorList.firstChild != null) errorList.removeChild(errorList.firstChild);
    
    var elements = $(this.prefix + '-form').elements;
    for (var i = 0; i < elements.length; i++) {
      var element = elements[i];
      
      element.value = element.value.replace(/ /, '')
      
      if (/hex_value/.exec(element.name)) {
        if (!(/^[a-fA-F0-9]{6,6}$/.exec(element.value))) {
          var errorNode = document.createElement('li');
          errorNode.innerHTML = '&quot;' + element.value + '&quot; is not a valid color.';
          errorList.appendChild(errorNode);
          
          valid = false;
        }
      } else if (/name/.exec(element.name)) {
        if (element.value == '' && !emptyNameOccured) {
          var errorNode = document.createElement('li');
          errorNode.innerHTML = 'Color names must not be empty.';
          errorList.appendChild(errorNode);
          
          emptyNameOccured = true;
          valid = false;
        }
      }
    }
    
    return valid;
  }
}
