/*  Prototype-UI, version trunk
 *
 *  Prototype-UI is freely distributable under the terms of an MIT-style license.
 *  For details, see the PrototypeUI web site: http://www.prototype-ui.com/
 *
 *--------------------------------------------------------------------------*/

if(typeof Prototype == 'undefined' || !Prototype.Version.match("1.6"))
  throw("Prototype-UI library require Prototype library >= 1.6.0");

(function(p) {
  var b = p.Browser, n = navigator;

  if (b.WebKit) {
    b.WebKitVersion = parseFloat(n.userAgent.match(/AppleWebKit\/([\d\.\+]*)/)[1]);
    b.Safari2 = (b.WebKitVersion < 420);
  }

  if (b.IE) {
    b.IEVersion = parseFloat(n.appVersion.split(';')[1].strip().split(' ')[1]);
    b.IE6 = b.IEVersion == 6;
    b.IE7 = b.IEVersion == 7;
  }

  p.falseFunction = function() { return false };
  p.trueFunction  = function() { return true  };
})(Prototype);

/*
Namespace: UI

  Introduction:
    Prototype-UI is a library of user interface components based on the Prototype framework.
    Its aim is to easilly improve user experience in web applications.

    It also provides utilities to help developers.

  Guideline:
    - Prototype conventions are followed
    - Everything should be unobstrusive
    - All components are themable with CSS stylesheets, various themes are provided

  Warning:
    Prototype-UI is still under deep development, this release is targeted to developers only.
    All interfaces are subjects to changes, suggestions are welcome.

    DO NOT use it in production for now.

  Authors:
    - Sébastien Gruhier, <http://www.xilinus.com>
    - Samuel Lebeau, <http://gotfresh.info>
*/

var UI = {
  Abstract: { },
  Ajax: { }
};
Object.extend(Class.Methods, {
  extend: Object.extend.methodize(),

  addMethods: Class.Methods.addMethods.wrap(function(proceed, source) {
    // ensure we are not trying to add null or undefined
    if (!source) return this;

    // no callback, vanilla way
    if (!source.hasOwnProperty('methodsAdded'))
      return proceed(source);

    var callback = source.methodsAdded;
    delete source.methodsAdded;
    proceed(source);
    callback.call(source, this);
    source.methodsAdded = callback;

    return this;
  }),

  addMethod: function(name, lambda) {
    var methods = {};
    methods[name] = lambda;
    return this.addMethods(methods);
  },

  method: function(name) {
    return this.prototype[name].valueOf();
  },

  classMethod: function() {
    $A(arguments).flatten().each(function(method) {
      this[method] = (function() {
        return this[method].apply(this, arguments);
      }).bind(this.prototype);
    }, this);
    return this;
  },

  // prevent any call to this method
  undefMethod: function(name) {
    this.prototype[name] = undefined;
    return this;
  },

  // remove the class' own implementation of this method
  removeMethod: function(name) {
    delete this.prototype[name];
    return this;
  },

  aliasMethod: function(newName, name) {
    this.prototype[newName] = this.prototype[name];
    return this;
  },

  aliasMethodChain: function(target, feature) {
    feature = feature.camelcase();

    this.aliasMethod(target+"Without"+feature, target);
    this.aliasMethod(target, target+"With"+feature);

    return this;
  }
});
Object.extend(Number.prototype, {
  // Snap a number to a grid
  snap: function(round) {
    return parseInt(round == 1 ? this : (this / round).floor() * round);
  }
});
/*
Interface: String

*/

Object.extend(String.prototype, {
  camelcase: function() {
    var string = this.dasherize().camelize();
    return string.charAt(0).toUpperCase() + string.slice(1);
  },

  /*
    Method: makeElement
      toElement is unfortunately already taken :/

      Transforms html string into an extended element or null (when failed)

      > '<li><a href="#">some text</a></li>'.makeElement(); // => LI href#
      > '<img src="foo" id="bar" /><img src="bar" id="bar" />'.makeElement(); // => IMG#foo (first one)

    Returns:
      Extended element

  */
  makeElement: function() {
    var wrapper = new Element('div'); wrapper.innerHTML = this;
    return wrapper.down();
  }
});
Object.extend(Array.prototype, {
  /**
   * Array#isEmpty() -> Boolean
   * Convenient method to check wether or not array is empty
   * returns: true if array is empty, false otherwise
   **/
  isEmpty: function() {
    return !this.length;
  },

  /**
   * Array#at(index) -> Object
   * Returns the element at the given index or undefined if index is out of range.
   * A negative index counts from the end.
   **/
  at: function(index) {
    return this[index < 0 ? this.length + index : index];
  },

  /**
   * Array#removeAt(index) -> Object | undefined
   * Deletes item at the given index, which may be negative
   * returns: deleted item or undefined if index is out of range
   **/
  removeAt: function(index) {
    if (-index > this.length) return;
    return this.splice(index, 1)[0];
  },

  /**
   * Array#removeIf(iterator[, context]) -> Array
   * Deletes items for which iterator returns a truthy value, bound to optional context
   * returns: array of items deleted
   **/
  removeIf: function(iterator, context) {
    for (var i = this.length - 1, objects = [ ]; i >= 0; i--)
      if (iterator.call(context, this[i], i))
        objects.push(this.removeAt(i));
    return objects.reverse();
  },

  /**
   * Array#remove(object) -> Number
   * Deletes items that are identical to given object
   * returns: number of items deleted
   **/
  remove: function(object) {
    return this.removeIf(function(member) { return member === object }).length;
  },

  /**
   * Array#insert(index, object[, ...])
   * Inserts the given objects before the element with the given index (which may be negative)
   * returns: this
   **/
  insert: function(index) {
    if (index > this.length)
      this.length = index;
    else if (index < 0)
      index = this.length + index + 1;

    this.splice.apply(this, [ index, 0 ].concat($A(arguments).slice(1)));
    return this;
  }
});

// backward compatibility
Array.prototype.empty = Array.prototype.isEmpty;
Element.addMethods({
  getScrollDimensions: function(element) {
    element = $(element);
    return {
      width:  element.scrollWidth,
      height: element.scrollHeight
    }
  },

  getScrollOffset: function(element) {
    element = $(element);
    return Element._returnOffset(element.scrollLeft, element.scrollTop);
  },

  setScrollOffset: function(element, offset) {
    element = $(element);
    if (arguments.length == 3)
      offset = { left: offset, top: arguments[2] };
    element.scrollLeft = offset.left;
    element.scrollTop  = offset.top;
    return element;
  },

  // returns "clean" numerical style (without "px") or null if style can not be resolved
  // or is not numeric
  getNumStyle: function(element, style) {
    var value = parseFloat($(element).getStyle(style));
    return isNaN(value) ? null : value;
  },

  // with courtesy of Tobie Langel
  //   (http://tobielangel.com/2007/5/22/prototype-quick-tip)
  appendText: function(element, text) {
    element = $(element);
    element.appendChild(document.createTextNode(String.interpret(text)));
    return element;
  }
});

document.whenReady = (function() {
  var queue = [ ];

  document.observe('dom:loaded', function() {
    queue.invoke('call', document);
    queue.clear();
    document.whenReady = function(callback) { callback.bind(document).defer() };
  });

  return function(callback) { queue.push(callback) };
})();

Object.extend(document.viewport, {
  // Alias this method for consistency
  getScrollOffset: document.viewport.getScrollOffsets,

  setScrollOffset: function(offset) {
    Element.setScrollOffset(Prototype.Browser.WebKit ? document.body : document.documentElement, offset);
  },

  getScrollDimensions: function() {
    return Element.getScrollDimensions(Prototype.Browser.WebKit ? document.body : document.documentElement);
  }
});

document.whenReady(function() {
  window.$head = $(document.getElementsByTagName('head')[0]);
  window.$body = $(document.body);
});
/*
Interface: UI.Options
  Mixin to handle *options* argument in initializer pattern.

  TODO: find a better example than Circle that use an imaginary Point function,
        this example should be used in tests too.

  It assumes class defines a property called *options*, containing
  default options values.

  Instances hold their own *options* property after a first call to <setOptions>.

  Example:
    > var Circle = Class.create(UI.Options, {
    >
    >   // default options
    >   options: {
    >     radius: 1,
    >     origin: Point(0, 0)
    >   },
    >
    >   // common usage is to call setOptions in initializer
    >   initialize: function(options) {
    >     this.setOptions(options);
    >   }
    > });
    >
    > var circle = new Circle({ origin: Point(1, 4) });
    >
    > circle.options
    > // => { radius: 1, origin: Point(1,4) }

  Accessors:
    There are builtin methods to automatically write options accessors. All those
    methods can take either an array of option names nor option names as arguments.
    Notice that those methods won't override an accessor method if already present.

     * <optionsGetter> creates getters
     * <optionsSetter> creates setters
     * <optionsAccessor> creates both getters and setters

    Common usage is to invoke them on a class to create accessors for all instances
    of this class.
    Invoking those methods on a class has the same effect as invoking them on the class prototype.
    See <classMethod> for more details.

    Example:
    > // Creates getter and setter for the "radius" options of circles
    > Circle.optionsAccessor('radius');
    >
    > circle.setRadius(4);
    > // 4
    >
    > circle.getRadius();
    > // => 4 (circle.options.radius)

  Inheritance support:
    Subclasses can refine default *options* values, after a first instance call on setOptions,
    *options* attribute will hold all default options values coming from the inheritance hierarchy.
*/

(function() {
  UI.Options = {
    methodsAdded: function(klass) {
      klass.classMethod($w(' setOptions allOptions optionsGetter optionsSetter optionsAccessor '));
    },

    // Group: Methods

    /*
      Method: setOptions
        Extends object's *options* property with the given object
    */
    setOptions: function(options) {
      if (!this.hasOwnProperty('options'))
        this.options = this.allOptions();

      this.options = Object.extend(this.options, options || {});
    },

    /*
      Method: allOptions
        Computes the complete default options hash made by reverse extending all superclasses
        default options.

        > Widget.prototype.allOptions();
    */
    allOptions: function() {
      var superclass = this.constructor.superclass, ancestor = superclass && superclass.prototype;
      return (ancestor && ancestor.allOptions) ?
          Object.extend(ancestor.allOptions(), this.options) :
          Object.clone(this.options);
    },

    /*
      Method: optionsGetter
        Creates default getters for option names given as arguments.
        With no argument, creates getters for all option names.
    */
    optionsGetter: function() {
      addOptionsAccessors(this, arguments, false);
    },

    /*
      Method: optionsSetter
        Creates default setters for option names given as arguments.
        With no argument, creates setters for all option names.
    */
    optionsSetter: function() {
      addOptionsAccessors(this, arguments, true);
    },

    /*
      Method: optionsAccessor
        Creates default getters/setters for option names given as arguments.
        With no argument, creates accessors for all option names.
    */
    optionsAccessor: function() {
      this.optionsGetter.apply(this, arguments);
      this.optionsSetter.apply(this, arguments);
    }
  };

  // Internal
  function addOptionsAccessors(receiver, names, areSetters) {
    names = $A(names).flatten();

    if (names.empty())
      names = Object.keys(receiver.allOptions());

    names.each(function(name) {
      var accessorName = (areSetters ? 'set' : 'get') + name.camelcase();

      receiver[accessorName] = receiver[accessorName] || (areSetters ?
        // Setter
        function(value) { return this.options[name] = value } :
        // Getter
        function()      { return this.options[name]         });
    });
  }
})();
/*
  Class: UI.IframeShim
    Handles IE6 bug when <select> elements overlap other elements with higher z-index
  
  Example:
    > // creates iframe and positions it under "contextMenu" element
    > this.iefix = new UI.IframeShim().positionUnder('contextMenu');
    > ...
    > document.observe('click', function(e) {
    >   if (e.isLeftClick()) {
    >     this.contextMenu.hide();
    >     
    >     // hides iframe when left click is fired on a document
    >     this.iefix.hide();
    >   }
    > }.bind(this))
    > ...
*/

// TODO:
//  
// Maybe it makes sense to bind iframe to an element 
// so that it automatically calls positionUnder method 
// when the element it's binded to is moved or resized
// Not sure how this might affect overall perfomance...

UI.IframeShim = Class.create(UI.Options, {
  
  options: {
    parent: document.body
  },

  /*
    Method: initialize
    Constructor
      
      Creates iframe shim and appends it to the body.
      Note that this method does not perform proper positioning and resizing of an iframe.
      To do that use positionUnder method
      
    Returns:
      this
  */
  initialize: function(options) {
    this.setOptions(options);
    this.element = new Element('iframe', {
      style: 'position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=1);',
      src: 'javascript:false;',
      frameborder: 0
    });
    $(this.options.parent || document.body).insert(this.element);
  },
  
  /*
    Method: hide
      Hides iframe shim leaving its position and dimensions intact
      
    Returns:
      this
  */
  hide: function() {
    this.element.hide();
    return this;
  },
  
  /*
    Method: show
      Show iframe shim leaving its position and dimensions intact
      
    Returns:
      this
  */
  show: function() {
    this.element.show();
    return this;
  },
  
  /*
    Method: positionUnder
      Positions iframe shim under the specified element 
      Sets proper dimensions, offset, zIndex and shows it
      Note that the element should have explicitly specified zIndex
      
    Returns:
      this
  */
  positionUnder: function(element) {
    var element = $(element),
        offset = element.cumulativeOffset(),
        dimensions = element.getDimensions(),
        style = { 
          left: offset[0] + 'px', 
          top: offset[1] + 'px',
          width: dimensions.width + 'px',
          height: dimensions.height + 'px',
          zIndex: element.getStyle('zIndex') - 1
        };
    this.element.setStyle(style).show();
    
    return this;
  },
  
  /*
    Method: setBounds
      Sets element's width, height, top and left css properties using 'px' as units
    
    Returns:
      this
  */
  setBounds: function(bounds) {
    for (prop in bounds) {
      bounds[prop] = parseInt(bounds[prop]) + 'px';
    }
    this.element.setStyle(bounds);
    return this;
  },
  
  /*
    Method: setSize
      Sets element's width, height
    
    Returns:
      this
  */
  setSize: function(width, height) {   
    this.element.style.width  = parseInt(width) + "px";
    this.element.style.height = parseInt(height) + "px"; 
    return this;
  },
  
  /*
    Method: setPosition
      Sets element's top and left 
    
    Returns:
      this
  */
  setPosition: function(top, left) {
    this.element.style.top  = parseInt(top) + "px";
    this.element.style.left = parseInt(left) + "px";
    return this;
  },
  
  /*
    Method: destroy
      Completely removes the iframe shim from the document
      
    Returns:
      this
  */
  destroy: function() {
    if (this.element)
      this.element.remove();
    
    return this;
  }
});
/*
  Group: Drag
    UI provides Element#enableDrag method that allow elements to fire drag-related events.
    
    Events fired:
      - drag:started : fired when a drag is started (mousedown then mousemove)
      - drag:updated : fired when a drag is updated (mousemove)
      - drag:ended   : fired when a drag is ended (mouseup)
      
    Notice it doesn't actually move anything, drag behavior has to be implemented
    by attaching handlers to drag events.
    
    Drag-related informations:
      event.memo contains useful information about the drag occuring:
        - dx         : difference between pointer x position when drag started 
                       and actual x position
        - dy         : difference between pointer y position when drag started
                       and actual y position
        - mouseEvent : the original mouse event, useful to know pointer absolute position,
                       or if key were pressed.
    
    Example, with event handling for a specific element:
    
    > // Now "resizable" will fire drag-related events
    > $('resizable').enableDrag();
    >
    > // Let's observe them
    > $('resizable').observe('drag:started', function(event) { 
    >   this._dimensions = this.getDimensions();
    > }).observe('drag:updated', function(event) {
    >   var drag = event.memo;
    >
    >   this.setStyle({
    >     width:  this._dimensions.width  + drag.dx + 'px',
    >     height: this._dimensions.height + drag.dy + 'px'
    >   });
    > });
    
    Example, with event delegating on the whole document:
    
    > // All elements in the having the "draggable" class name will fire drag events.
    > $$('.draggable').invoke('enableDrag');
    >
    > document.observe('drag:started', function(event) {
    >   UI.logger.info('trying to drag ' + event.element().id);
    > }):
*/

Element.addMethods({
  enableDrag: function(element) {
    return $(element).writeAttribute('draggable');
  },

  disableDrag: function(element){
    return $(element).writeAttribute('draggable', null);
  },

  isDraggable: function(element) {
    return $(element).hasAttribute('draggable');
  }
});

(function() {
  var initPointer, draggedElement;
  
  document.observe('mousedown', function(event) {
    if (draggedElement = findDraggable(event.element())) {
      // prevent default browser action to avoid selecting text for instance
      event.preventDefault();
      initPointer = event.pointer();
      
      document.observe('mousemove', startDrag);
      document.observe('mouseup',   cancelDrag);
    }
  });
  
  function findDraggable(element) {
    while (element && element !== document) {
      if (element.hasAttribute('draggable')) return element;
      element = $(element.parentNode);
    }
  };
  
  function startDrag(event) {
    document.stopObserving('mousemove', startDrag)
            .stopObserving('mouseup',   cancelDrag)
            .observe('mousemove', drag)
            .observe('mouseup',   endDrag);
    
    fire('drag:started', event);
  };
  
  function cancelDrag(event) {
    document.stopObserving('mousemove', startDrag)
            .stopObserving('mouseup',   cancelDrag);
  };
  
  function drag(event) {
    fire('drag:updated', event);
  };
  
  function endDrag(event) {
    document.stopObserving('mousemove', drag)
            .stopObserving('mouseup',   endDrag);
    
    fire('drag:ended', event);
  };
  
  function fire(eventName, event) {
    var pointer = event.pointer();
    
    draggedElement.fire(eventName, {
      dx: pointer.x - initPointer.x,
      dy: pointer.y - initPointer.y,
      mouseEvent: event
    });
  };
})();

/*
Namespace: CSS

  Utility functions for CSS/StyleSheet files access

  Authors:
    - Sébastien Gruhier, <http://www.xilinus.com>
    - Samuel Lebeau, <http://gotfresh.info>
*/

var CSS = (function() {
  // Code based on:
  //   - IE5.5+ PNG Alpha Fix v1.0RC4 (c) 2004-2005 Angus Turnbull http://www.twinhelix.com
  //   - Whatever:hover - V2.02.060206 - hover, active & focus (c) 2005 - Peter Nederlof * Peterned - http://www.xs4all.nl/~peterned/
  function fixPNG() {
   parseStylesheet.apply(this, $A(arguments).concat(fixRule));
  };

  function parseStylesheet() {
    var patterns = $A(arguments);
    var method = patterns.pop();

    // To avoid flicking background
    // if (Prototype.Browser.IE)
    //   document.execCommand("BackgroundImageCache", false, true);
    // Parse all document stylesheets
    var styleSheets = $A(document.styleSheets);
    if (patterns.length > 0) {
      styleSheets = styleSheets.select(function(css) {
        return patterns.any(function(pattern) {
          return css.href && css.href.match(pattern)
          });
      });
    }
    styleSheets.each(function(styleSheet) {fixStylesheet.call(this, styleSheet, method)});
  };

  // Fixes a stylesheet
  function fixStylesheet(stylesheet, method) {
    // Parse import files
    if (stylesheet.imports)
      $A(stylesheet.imports).each(fixStylesheet);

    var href = stylesheet.href || document.location.href;
    var docPath = href.substr(0, href.lastIndexOf('/'));
	  // Parse all CSS Rules
    $A(stylesheet.rules || stylesheet.cssRules).each(function(rule) { method.call(this, rule, docPath) });
  };

  var filterPattern = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(src="#{src}",sizingMethod="#{method}")';

  // Fixes a rule if it has a PNG background
  function fixRule(rule, docPath) {
    var bgImg = rule.style.backgroundImage;
    // Rule with PNG background image
    if (bgImg && bgImg != 'none' && bgImg.match(/^url[("']+(.*\.png)[)"']+$/i)) {
      var src = RegExp.$1;
      var bgRepeat = rule.style.backgroundRepeat;
      // Relative path
      if (src[0] != '/')
        src = docPath + "/" + src;
      // Apply filter
      rule.style.filter = filterPattern.interpolate({
        src:    src,
        method: bgRepeat == "no-repeat" ? "crop" : "scale" });
      rule.style.backgroundImage = "none";
    }
  };

  var preloadedImages = new Hash();

  function preloadRule(rule, docPath) {
    var bgImg = rule.style.backgroundImage;
    if (bgImg && bgImg != 'none'  && bgImg != 'initial' ) {
      if (!preloadedImages.get(bgImg)) {
        bgImg.match(/^url[("']+(.*)[)"']+$/i);
        var src = RegExp.$1;
        // Relative path
        if (!(src[0] == '/' || src.match(/^file:/) || src.match(/^https?:/)))
          src = docPath + "/" + src;
        preloadedImages.set(bgImg, true);
        var image = new Image();
        image.src = src;
      }
    }
  }

  return {
    /*
       Method: fixPNG
         Fix transparency of PNG background of document stylesheets.
         (only on IE version<7, otherwise does nothing)

         Warning: All png background will not work as IE filter use for handling transparency in PNG
         is not compatible with all background. It does not support top/left position (so no CSS sprite)

         I recommend to create a special CSS file with png that needs to be fixed and call CSS.fixPNG on this CSS

         Examples:
          > CSS.fixPNG() // To fix all css
          >
          > CSS.fixPNG("mac_shadow.css") // to fix all css files with mac_shadow.css so mainly only on file
          >
          > CSS.fixPNG("shadow", "vista"); // To fix all css files with shadow or vista in their names

       Parameters
         patterns: (optional) list of pattern to filter css files
    */
    fixPNG: (Prototype.Browser.IE && Prototype.Browser.IEVersion < 7) ? fixPNG : Prototype.emptyFunction,

    // By Tobie Langel (http://tobielangel.com)
    //   inspired by http://yuiblog.com/blog/2007/06/07/style/
    addRule: function(css) {
      var style = new Element('style', { type: 'text/css', media: 'screen' });
      $head.insert(style);
      if (style.styleSheet) style.styleSheet.cssText = css;
      else style.appendText(css);
      return style;
    },

    preloadImages: function() {
      parseStylesheet.apply(this, $A(arguments).concat(preloadRule));
    }
  };
})();
/*
  Interface: Date
  author: Minho Kim
*/
Object.extend(Date.prototype, {
  addDays: function(days) {
    return new Date(this.getFullYear(), this.getMonth(), this.getDate() + days, this.getHours(), this.getMinutes(), this.getSeconds(), this.getMilliseconds());
  },

  succ: function() {
    return this.addDays(1);
  },

  firstOfMonth: function() {
    return new Date(this.getFullYear(), this.getMonth(), 1);
  },

  endOfMonth: function() {
    return new Date(this.getFullYear(), this.getMonth() + 1, 0);
  },

  getDayOfYear: function() {
    return Math.ceil((this - new Date(this.getFullYear(), 0, 1)) / 86400000);
  },

  strftime: function(grammar) {
    var parts = { }, i18n = Date.default_i18n;
    var lambda = function(date, part) {
      switch (part) {
      // date
        case 'a': return i18n.WEEKDAYS_MEDIUM[date.getDay()];
        case 'A': return i18n.WEEKDAYS[date.getDay()];
        case 'b':
        case 'h': return i18n.MONTHS_SHORT[date.getMonth()];
        case 'B': return i18n.MONTHS[date.getMonth()];
        case 'C': return Math.floor(date.getFullYear() / 100);
        case 'd': return date.getDate().toPaddedString(2);
        case 'e': return date.getDate();
        case 'j': return date.getDayOfYear();
        case 'm': return (date.getMonth()+1).toPaddedString(2);
        case 'u': return date.getDay() || 7;
        case 'w': return date.getDay();
        case 'y': return date.getFullYear().toString().substring(2);
        case 'Y': return date.getFullYear();

        // time
        case 'H': return date.getHours().toPaddedString(2);
        case 'I': return (date.getHours() % 12).toPaddedString(2);
        case 'M': return date.getMinutes().toPaddedString(2);
        case 'p': return date.getHours() < 12 ? 'am' : 'pm';
        case 'S': return date.getSeconds().toPaddedString(2);

        // static
        case 'n': return '\n';
        case 't': return '\t';

        // combined
        case 'D': return date.strftime('%m/%d/%y');
        case 'r': return date.strftime('%I:%M:%S %p'); // time in a.m. and p.m. notation
        case 'R': return date.strftime('%H:%M:%S'); // time in 24 hour notation
        case 'T': return date.strftime('%H:%M:%S'); // current time, equal to %H:%M:%S

        // locale
        case 'c': return date.strftime(i18n.FORMAT_DATETIME);
        case 'x': return date.strftime(i18n.FORMAT_DATE);
        case 'X': return date.strftime(i18n.FORMAT_TIME);
      }
    };
    grammar.scan(/\w+/, function(e){
      var part = e.first();
      parts[part] = lambda(this, part);
    }.bind(this));
    return grammar.interpolate(parts, Date.STRFT_GRAMMER);
  },

  equalsDate: function(other) {
    return (this.getMonth() == other.getMonth() && this.getDate() == other.getDate() && this.getFullYear() == other.getFullYear());
  }
});

Object.extend(Date, {
  STRFT_GRAMMER : /(^|.|\r|\n)(\%(\w+))/,

  default_i18n: {
    MONTHS_SHORT: $w('Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'),
    MONTHS: $w('January February March April May June July August September October November December'),
    WEEKDAYS_MEDIUM: $w('Sun Mon Tue Wed Thu Fri Sat'),
    WEEKDAYS: $w('Sunday Monday Tuesday Wednesday Thursday Friday Saturday'),
    FORMAT_DATE: '%m/%d/%Y',
    FORMAT_TIME: '%H:%M:%S',
    FORMAT_DATETIME: '%x %X'
  },

  parseString: function(dateString, format) {
    var date = new Date(), i18n = Date.default_i18n;

    format=format.replace('%D','%m/%d/%y');
    format=format.replace('%T','%H:%M:%S').replace('%r','%I:%M:%S %p').replace('%R','%H:%M:%S');
    format=format.replace('%c',i18n.FORMAT_DATETIME).replace('%x',i18n.FORMAT_DATE).replace('%X',i18n.FORMAT_TIME);

    var tokens = format.match(/%./g);

    // the regex /\W+/ does not work for utf8 chars
    dateString.split(/[^A-Za-z0-9\u00A1-\uFFFF]+/).each(function(e, i) {
      switch (tokens[i]) {
        case '%a':
        case '%A':
        case '%u':
        case '%w': break;

        case '%b':
        case '%h': date.setMonth(i18n.MONTHS_SHORT.indexOf(e)); break;
        case '%B': date.setMonth(i18n.MONTHS.indexOf(e)); break;
        case '%C': break; //century
        case '%d':
        case '%e': date.setDate(parseInt(e,10)); break;
        case '%j': break; // day of year
        case '%m': date.setMonth(parseInt(e,10)-1); break;
        case '%w': date.setDay(parseInt(e,10)); break;
        case '%y':
          var year = parseInt(e,10);
          if (year<50)  year+=2000;
          if (year<100) year+=1900;
          date.setYear(year);
          break;
        case '%Y': date.setFullYear(parseInt(e,10)); break;

        // time
        case '%H': date.setHours(parseInt(e,10)); break;
        case '%I': date.setHours(parseInt(e,10)); break;
        case '%M': date.setMinutes(parseInt(e,10)); break;
        case '%p': if(e=='pm') date.setHours(date.getHours()+12); break;
        case '%S': date.setSeconds(parseInt(e,10)); break;
      }
    });
    return date;
  }
});
UI.Benchmark = {
  benchmark: function(lambda, iterations) {
    var date = new Date();
    (iterations || 1).times(lambda);
    return (new Date() - date) / 1000;
  }
};
/*
  Group: Drag
    UI provides Element#enableDrag method that allow elements to fire drag-related events.

    Events fired:
      - drag:started : fired when a drag is started (mousedown then mousemove)
      - drag:updated : fired when a drag is updated (mousemove)
      - drag:ended   : fired when a drag is ended (mouseup)

    Notice it doesn't actually move anything, drag behavior has to be implemented
    by attaching handlers to drag events.

    Drag-related informations:
      event.memo contains useful information about the drag occuring:
        - dx         : difference between pointer x position when drag started
                       and actual x position
        - dy         : difference between pointer y position when drag started
                       and actual y position
        - mouseEvent : the original mouse event, useful to know pointer absolute position,
                       or if key were pressed.

    Example, with event handling for a specific element:

    > // Now "resizable" will fire drag-related events
    > $('resizable').enableDrag();
    >
    > // Let's observe them
    > $('resizable').observe('drag:started', function(event) {
    >   this._dimensions = this.getDimensions();
    > }).observe('drag:updated', function(event) {
    >   var drag = event.memo;
    >
    >   this.setStyle({
    >     width:  this._dimensions.width  + drag.dx + 'px',
    >     height: this._dimensions.height + drag.dy + 'px'
    >   });
    > });

    Example, with event delegating on the whole document:

    > // All elements in the having the "draggable" class name will fire drag events.
    > $$('.draggable').invoke('enableDrag');
    >
    > document.observe('drag:started', function(event) {
    >   UI.logger.info('trying to drag ' + event.element().id);
    > }):
*/

Element.addMethods({
  enableDrag: function(element) {
    return $(element).writeAttribute('draggable');
  },

  disableDrag: function(element){
    return $(element).writeAttribute('draggable', null);
  },

  isDraggable: function(element) {
    return $(element).hasAttribute('draggable');
  }
});

(function() {
  var initPointer, draggedElement;

  document.observe('mousedown', function(event) {
    if (draggedElement = findDraggable(event.element())) {
      // prevent default browser action to avoid selecting text for instance
      event.preventDefault();
      initPointer = event.pointer();

      document.observe('mousemove', startDrag);
      document.observe('mouseup',   cancelDrag);
    }
  });

  function findDraggable(element) {
    while (element && element !== document) {
      if (element.hasAttribute('draggable')) return element;
      element = $(element.parentNode);
    }
  };

  function startDrag(event) {
    document.stopObserving('mousemove', startDrag)
            .stopObserving('mouseup',   cancelDrag)
            .observe('mousemove', drag)
            .observe('mouseup',   endDrag);

    fire('drag:started', event);
  };

  function cancelDrag(event) {
    document.stopObserving('mousemove', startDrag)
            .stopObserving('mouseup',   cancelDrag);
  };

  function drag(event) {
    fire('drag:updated', event);
  };

  function endDrag(event) {
    document.stopObserving('mousemove', drag)
            .stopObserving('mouseup',   endDrag);

    fire('drag:ended', event);
  };

  function fire(eventName, event) {
    var pointer = event.pointer();

    draggedElement.fire(eventName, {
      dx: pointer.x - initPointer.x,
      dy: pointer.y - initPointer.y,
      mouseEvent: event
    });
  };
})();
/*
  UI.State
    This helper allows to use back/forward browser buttons to switch between states and
    to bookmark an application in a given state. It uses fragment part of URL to store state name.
    A state is just a simple string, basic examples are "previewing", "editing" for a composition
    application.

    Use UI.State.change('previewing') to switch to the 'previewing' state.
    Use document.observe('state:changed', callback) to detect state changes.
    Inside the callback, event.memo.value gives the state name.

    On page load, state:changed is fired if the fragment part of the URL is not blank.
    If users gets back to the initial state, state:changed is fired with empty string as value.

    This is a simplified (and still buggy) version of YUI Browser History Manager.
    (http://yuiblog.com/blog/2007/02/21/browser-history-manager/)

    Seems to work in Safari 2/3, IE6, Firefox 1.5/2
    Opera <9.5 isn't supported for now
    Try UI.State.isSupported() before any attempt...

    Warning: You should need to wait for document to be loaded before using this helper.
    Using document.whenReady is a good way to ensure this.
    Classic anchors with hash (<a href="#top"> for instance) triggers state:changed on all
    browsers but IE, therefore using those anchors is NOT the good way to switch between states.

    Example:
      A simple REST catalog that works without Javascript.

      <div id="products_menu">
        <a href="/products/14-mug">Mug</a>
        <a href="/products/15-chair">Chair</a>
      </div>

      <div id="content">
        <!-- selected product -->
      </div>


      Server side: at route "/products[/id]"
        if javascript is wanted?
          render javascript instructions to replace $('content') with
          current product (if id is present) or index
        else
          render product (if id is present) or index in html within catalog layout
        end

      Javascript side:
        document.observe('state:changed', function(event) {
          var fragment = event.memo.value;
          // Back to initial (index) state
          if (fragment.empty()) new Ajax.Request('/products');
          // If fragment matches a product, request this product
          if (fragment.match(/\d+-\w+/)) new Ajax.Request('/products/'+fragment);
        });

        document.observe('mousedown', function(event) {
          var link = event.findElement('#products_menu a');
          if (link) {
            // prevent browser from following link
            event.stop();
            UI.State.change(link.readAttribute('href').sub('/products/', ''));
          }
        });


*/
UI.State = (function(browser) {
  var watch, change, changed, fragment;

  function getFragment() {
    return top.location.hash.substr(1);
  }

  function fragmentChanged(newFragment) {
    document.fire('state:changed', { value: newFragment, previousValue: fragment });
    fragment = newFragment;
  }

  document.whenReady(function() {
    var initialFragment = getFragment();
    if (initialFragment) fragmentChanged(initialFragment);

    if (browser.IE) {
      var iframe = new Element('iframe', { style: 'display: none' });
      $body.appendChild(iframe);

      var contentWindow = iframe.contentWindow;

      function writeFragmentToIFrame(fragment) {
        var doc = contentWindow.document;
        doc.open();
        doc.write(fragment || '');
        doc.close();
      }

      function readFragmentFromIFrame() {
        return contentWindow.document.body.innerText;
      }

      writeFragmentToIFrame(fragment);

      watch = function() {
        var newFragment = readFragmentFromIFrame();
        if (newFragment != fragment) {
          top.location.hash = newFragment || '';
          fragmentChanged(newFragment);
        }
      };

      change = function(newFragment) {
        if (newFragment != fragment) writeFragmentToIFrame(newFragment);
      };

    } else {
      // watching history.length is only useful for WebKit
      var counter = history.length, fragments = [ ];

      watch = function() {
        var newFragment = getFragment(), newCounter = history.length;
        if (newFragment != fragment) fragmentChanged(newFragment);
        else if (newCounter != counter) {
          fragmentChanged(fragments[newCounter - 1]);
          fragment = newFragment;
        }
        counter = newCounter;
      };

      change = function(newFragment) {
        top.location.hash = newFragment;
        if (browser.WebKit) fragments[history.length] = newFragment;
      };
    }

    setInterval(function() {
      if (changed) watch()
    }, 50);
  });

  return {
    change: function(value) {
      change(value);
      changed = true;
    },

    isSupported: function() {
      return !browser.Opera || navigator.appVersion.match("9.5");
    }
  }
})(Prototype.Browser);
/*
  Class: UI.IframeShim
    Handles IE6 bug when <select> elements overlap other elements with higher z-index

  Example:
    > // creates iframe and positions it under "contextMenu" element
    > this.iefix = new UI.IframeShim().positionUnder('contextMenu');
    > ...
    > document.observe('click', function(e) {
    >   if (e.isLeftClick()) {
    >     this.contextMenu.hide();
    >
    >     // hides iframe when left click is fired on a document
    >     this.iefix.hide();
    >   }
    > }.bind(this))
    > ...
*/

// TODO:
//
// Maybe it makes sense to bind iframe to an element
// so that it automatically calls positionUnder method
// when the element it's binded to is moved or resized
// Not sure how this might affect overall perfomance...

UI.IframeShim = Class.create(UI.Options, {

  options: {
    parent: document.body
  },

  /*
    Method: initialize
    Constructor

      Creates iframe shim and appends it to the body.
      Note that this method does not perform proper positioning and resizing of an iframe.
      To do that use positionUnder method

    Returns:
      this
  */
  initialize: function(options) {
    this.setOptions(options);
    this.element = new Element('iframe', {
      style: 'position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=1);',
      src: 'javascript:false;',
      frameborder: 0
    });
    $(this.options.parent || document.body).insert(this.element);
  },

  /*
    Method: hide
      Hides iframe shim leaving its position and dimensions intact

    Returns:
      this
  */
  hide: function() {
    this.element.hide();
    return this;
  },

  /*
    Method: show
      Show iframe shim leaving its position and dimensions intact

    Returns:
      this
  */
  show: function() {
    this.element.show();
    return this;
  },

  /*
    Method: positionUnder
      Positions iframe shim under the specified element
      Sets proper dimensions, offset, zIndex and shows it
      Note that the element should have explicitly specified zIndex

    Returns:
      this
  */
  positionUnder: function(element) {
    var element = $(element),
        offset = element.cumulativeOffset(),
        dimensions = element.getDimensions(),
        style = {
          left: offset[0] + 'px',
          top: offset[1] + 'px',
          width: dimensions.width + 'px',
          height: dimensions.height + 'px',
          zIndex: element.getStyle('zIndex') - 1
        };
    this.element.setStyle(style).show();

    return this;
  },

  /*
    Method: setBounds
      Sets element's width, height, top and left css properties using 'px' as units

    Returns:
      this
  */
  setBounds: function(bounds) {
    for (prop in bounds) {
      bounds[prop] = parseInt(bounds[prop]) + 'px';
    }
    this.element.setStyle(bounds);
    return this;
  },

  /*
    Method: setSize
      Sets element's width, height

    Returns:
      this
  */
  setSize: function(width, height) {
    this.element.style.width  = parseInt(width) + "px";
    this.element.style.height = parseInt(height) + "px";
    return this;
  },

  /*
    Method: setPosition
      Sets element's top and left

    Returns:
      this
  */
  setPosition: function(top, left) {
    this.element.style.top  = parseInt(top) + "px";
    this.element.style.left = parseInt(left) + "px";
    return this;
  },

  /*
    Method: destroy
      Completely removes the iframe shim from the document

    Returns:
      this
  */
  destroy: function() {
    if (this.element)
      this.element.remove();

    return this;
  }
});
/*
Class: UI.Logger
*/

/*
  Group: Logging Facilities
    Prototype UI provides a facility to log message with levels.
    Levels are in order "debug", "info", "warn" and "error".

    As soon as the DOM is loaded, a default logger is present in UI.logger.

    This logger is :
    * an <ElementLogger> if $('log') is present
    * a <ConsoleLogger> if window.console is defined
    * a <MemLogger> otherwise

    See <AbstractLogger> to learn how to use it.

    Example:

    > UI.logger.warn('something bad happenned !');
*/

// Class: AbstractLogger

UI.Abstract.Logger = Class.create({
  /*
    Property: level
      The log level, default value is debug  <br/>
  */
  level: 'debug'
});

(function() {
  /*
    Method: debug
      Logs with "debug" level

    Method: info
      Logs with "info" level

    Method: warn
      Logs with "warn" level

    Method: error
      Logs with "error" level
  */
  var levels = $w(" debug info warn error ");

  levels.each(function(level, index) {
    UI.Abstract.Logger.addMethod(level, function(message) {
      // filter lower level messages
      if (index >= levels.indexOf(this.level))
        this._log({ level: level, message: message, date: new Date() });
    });
  });
})();

/*
  Class: NullLogger
    Does nothing
*/
UI.NullLogger = Class.create(UI.Abstract.Logger, {
  _log: Prototype.emptyFunction
});

/*
  Class: MemLogger
    Logs in memory

    Property: logs
      An array of logs, objects with "date", "level", and "message" properties
*/
UI.MemLogger = Class.create(UI.Abstract.Logger, {
  initialize: function() {
    this.logs = [ ];
  },

  _log: function(log) {
    this.logs.push(log);
  }
});

/*
  Class: ConsoleLogger
    Logs using window.console
*/
UI.ConsoleLogger = Class.create(UI.Abstract.Logger, {
  _log: function(log) {
    console[log.level](log.message);
  }
});

/*
  Class: ElementLogger
    Logs in a DOM element
*/
UI.ElementLogger = Class.create(UI.Abstract.Logger, {
  /*
    Method: initialize
      Constructor, takes a DOM element to log into as argument
  */
  initialize: function(element) {
    this.element = $(element);
  },

  /*
    Property: format
      A format string, will be interpolated with "date", "level" and "message"

      Example:
        > "<p>(#{date}) #{level}: #{message}</p>"
  */
  format: '<p>(<span class="date">#{date}</span>) ' +
              '<span class="level">#{level}</span> : ' +
              '<span class="message">#{message}</span></p>',

  _log: function(log) {
    var entry = this.format.interpolate({
      level:   log.level.toUpperCase(),
      message: log.message.escapeHTML(),
      date:    log.date.toLocaleTimeString()
    });
    this.element.insert({ top: entry });
  }
});

document.whenReady(function() {
  if ($('log'))             UI.logger = new UI.ElementLogger('log');
  else if (window.console)  UI.logger = new UI.ConsoleLogger();
  else                      UI.logger = new UI.MemLogger();
});
UI.PullDown = Class.create(UI.Options, {
  options: {
    className:   '',
    shadow:      false,
    position:    'over',
    cloneWidth:   false,
    beforeShow:   null,
    afterShow:    null,
    beforeUpdate: null,
    afterUpdate:  null,
    afterCreate:  null
  },

  initialize: function(container, options){
    this.setOptions(options);
		this.container = $(container);

    this.element = new Element('div', {
      className: 'UI-widget-dropdown ' + this.options.className,
      style: 'z-index:999999;position:absolute;'
    }).hide();

    if (this.options.shadow)
      this.shadow = new UI.Shadow(this.element, {theme: this.options.shadow}).hide();
    else
      this.iframe = Prototype.Browser.IE ? new UI.IframeShim() : null;

    this.outsideClickHandler = this.outsideClick.bind(this);
    this.placeHandler        = this.place.bind(this);
    this.hideHandler         = this.hide.bind(this);
  },

  destroy: function(){
 		if (this.active)
      this.element.remove();
    this.element = null;
    this.stopObserving();
  },

  /*
    Method: insert
      Inserts a new Element to the PullDown

    Parameters:
      elem  - an DOM element

    Returns:
      this
   */
   insert: function(elem){
     return this.element.insert(elem);
   },

   /*
    Method: place
      Place the PullDown

    Parameters:
      none

    Returns:
     this
  */
  place: function(){
    this.element.clonePosition(this.container, {
      setHeight: false,
      setWidth:  this.options.cloneWidth,
      offsetTop: this.options.position == 'below' ? this.container.offsetHeight : 0
    });

    var w = this.element.getWidth();
    var h = this.element.getHeight();
    var t = parseInt(this.element.style.top);
    var l = parseInt(this.element.style.left);

    if (this.shadow)
      this.shadow.setBounds({top: t, left: l, width: w, height: h});
    if (this.iframe)
      this.iframe.setPosition(t, l).setSize(w, h);
    return this;
  },

  /*
    Method: show
    Show the PullDown

    Parameters:
      event  - (optional) Event fired the show

    Returns:
     this
  */
  show: function(event){
    if (this.active)
        return this;

    this.active = true;

    if (this.options.beforeShow)
        this.options.beforeShow(this);

    this.element.hide();

    if (this.iframe)
			this.iframe.show();

    document.body.insert(this.element);

    if (this.shadow)
      this.shadow.show();
    this.element.show();

    if (this.options.afterShow)
      this.options.afterShow(this);

    document.observe('mousedown',  this.outsideClickHandler);
		Event.observe(window,'scroll', this.placeHandler);
		Event.observe(window,'resize', this.hideHandler);

		return this;
  },

  outsideClick: function(event) {
    if (event.findElement('.UI-widget-dropdown'))
			return;
    //this.hide();
  },

  /*
    Method: hide
      Hide the PullDown

    Returns:
      this
  */
  hide: function(){
		if (this.active) {
      this.active = false;
      if (this.shadow)
        this.shadow.hide();

  		if(this.iframe)
  			this.iframe.hide();

      this.element.remove();

    }
    this.stopObserving();
		return this;
  },

  stopObserving: function() {
		Event.stopObserving(window,'resize', this.hideHandler);
		Event.stopObserving(window,'scroll', this.placeHandler);
    document.stopObserving('click', this.outsideClickHandler);
  }
});
/*
Class: UI.Shadow
  Add shadow around a DOM element. The element MUST BE in ABSOLUTE position.

  Shadow can be skinned by CSS (see mac_shadow.css or drop_shadow.css).
  CSS must be included to see shadow.

  A shadow can have two states: focused and blur.
  Shadow shifts are set in CSS file as margin and padding of shadow_container to add visual information.

  Example:
    > new UI.Shadow("element_id");
*/
UI.Shadow = Class.create(UI.Options, {
  options: {
    theme: "mac_shadow",
    focus: false,
    zIndex: 100,
    withIFrameShim: true
  },

  /*
    Method: initialize
      Constructor, adds shadow elements to the DOM if element is in the DOM.
      Element MUST BE in ABSOLUTE position.

    Parameters:
      element - DOM element
      options - Hashmap of options
        - theme (default: mac_shadow)
        - focus (default: true)
        - zIndex (default: 100)

    Returns:
      this
  */
  initialize: function(element, options) {
    this.setOptions(options);

    this.element = $(element);
    this.create();
    this.iframe = Prototype.Browser.IE && this.options.withIFrameShim ? new UI.IframeShim() : null;

    if (Object.isElement(this.element.parentNode))
      this.render();
  },

  /*
    Method: destroy
      Destructor, removes elements from the DOM
  */
  destroy: function() {
    if (this.shadow.parentNode)
      this.remove();
  },

  // Group: Size and Position
  /*
    Method: setPosition
      Sets top/left shadow position in pixels

    Parameters:
      top -  top position in pixel
      left - left position in pixel

    Returns:
      this
  */
  setPosition: function(top, left) {
    if (this.shadowSize) {
      var shadowStyle = this.shadow.style;
      top =  parseInt(top)  - this.shadowSize.top  + this.shadowShift.top;
      left = parseInt(left) - this.shadowSize.left + this.shadowShift.left;
      shadowStyle.top  = top + 'px';
      shadowStyle.left = left + 'px';
      if (this.iframe)
        this.iframe.setPosition(top, left);
    }
    return this;
  },

  /*
    Method: setSize
      Sets width/height shadow in pixels

    Parameters:
      width  - width in pixel
      height - height in pixel

    Returns:
      this
  */
  setSize: function(width, height) {
    if (this.shadowSize) {
      try {
        var w = Math.max(0, parseInt(width) + this.shadowSize.width - this.shadowShift.width) + "px";
        this.shadow.style.width = w;
        var h = Math.max(0, parseInt(height) - this.shadowShift.height) + "px";

        // this.shadowContents[1].style.height = h;
        this.shadowContents[1].childElements().each(function(e) {e.style.height = h});
        this.shadowContents.each(function(item){ item.style.width = w});
        if (this.iframe)
          this.iframe.setSize(width + this.shadowSize.width - this.shadowShift.width, height + this.shadowSize.height - this.shadowShift.height);
      }
      catch(e) {
        // IE could throw an exception if called to early
      }
    }
    return this;
  },

  /*
    Method: setBounds
      Sets shadow bounds in pixels

    Parameters:
      bounds - an Hash {top:, left:, width:, height:}

    Returns:
      this
  */
  setBounds: function(bounds) {
    return this.setPosition(bounds.top, bounds.left).setSize(bounds.width, bounds.height);
  },

  /*
    Method: setZIndex
      Sets shadow z-index

    Parameters:
      zIndex - zIndex value

    Returns:
      this
  */
  setZIndex: function(zIndex) {
    this.shadow.style.zIndex = zIndex;
    return this;
  },

   // Group: Render
  /*
    Method: show
      Displays shadow

    Returns:
      this
  */
  show: function() {
    this.render();
    this.shadow.show();
    if (this.iframe)
      this.iframe.show();
    return this;
  },

  /*
    Method: hide
      Hides shadow

    Returns:
      this
  */
  hide: function() {
    this.shadow.hide();
    if (this.iframe)
      this.iframe.hide();
    return this;
  },

  /*
    Method: remove
      Removes shadow from the DOM

    Returns:
      this
  */
  remove: function() {
    this.shadow.remove();
    return this;
  },

  // Group: Status
  /*
    Method: focus
      Focus shadow.

      Change shadow shift. Shift values are set in CSS file as margin and padding of shadow_container
      to add visual information of shadow status.

    Returns:
      this
  */
  focus: function() {
    this.options.focus = true;
    this.updateShadow();
    return this;
  },

  /*
    Method: blur
      Blurs shadow.

      Change shadow shift. Shift values are set in CSS file as margin and padding of shadow_container
      to add visual information of shadow status.

    Returns:
      this
  */
  blur: function() {
    this.options.focus = false;
    this.updateShadow();
    return this;
  },

  // Private Functions
  // Adds shadow elements to DOM, computes shadow size and displays it
  render: function() {
    if (this.element.parentNode && !Object.isElement(this.shadow.parentNode)) {
      this.element.parentNode.appendChild(this.shadow);
      this.computeSize();
      this.setBounds(Object.extend(this.element.getDimensions(), this.getElementPosition()));
      this.shadow.show();
    }
    return this;
  },

  // Creates HTML elements without inserting them into the DOM
  create: function() {
    var zIndex = this.element.getStyle('zIndex');
    if (!zIndex)
      this.element.setStyle({zIndex: this.options.zIndex});
    zIndex = (zIndex || this.options.zIndex) - 1;

    this.shadowContents = new Array(3);
    this.shadowContents[0] = new Element("div")
      .insert(new Element("div", {className: "shadow_center_wrapper"}).insert(new Element("div", {className: "n_shadow"})))
      .insert(new Element("div", {className: "shadow_right ne_shadow"}))
      .insert(new Element("div", {className: "shadow_left nw_shadow"}));

    this.shadowContents[1] = new Element("div")
      .insert(new Element("div", {className: "shadow_center_wrapper c_shadow"}))
      .insert(new Element("div", {className: "shadow_right e_shadow"}))
      .insert(new Element("div", {className: "shadow_left w_shadow"}));
    this.centerElements = this.shadowContents[1].childElements();

    this.shadowContents[2] = new Element("div")
      .insert(new Element("div", {className: "shadow_center_wrapper"}).insert(new Element("div", {className: "s_shadow"})))
      .insert(new Element("div", {className: "shadow_right se_shadow"}))
      .insert(new Element("div", {className: "shadow_left sw_shadow"}));

    this.shadow = new Element("div", {className: "shadow_container " + this.options.theme,
                                      style: "position:absolute; top:-10000px; left:-10000px; display:none; z-index:" + zIndex })
      .insert(this.shadowContents[0])
      .insert(this.shadowContents[1])
      .insert(this.shadowContents[2]);
  },

  // Compute shadow size
  computeSize: function() {
    if (this.focusedShadowShift)
      return;
    this.shadow.show();

    // Trick to get shadow shift designed in CSS as padding
    var content = this.shadowContents[1].select("div.c_shadow").first();
    this.unfocusedShadowShift = {};
    this.focusedShadowShift = {};

    $w("top left bottom right").each(function(pos) {this.unfocusedShadowShift[pos] = content.getNumStyle("padding-" + pos) || 0}.bind(this));
    this.unfocusedShadowShift.width  = this.unfocusedShadowShift.left + this.unfocusedShadowShift.right;
    this.unfocusedShadowShift.height = this.unfocusedShadowShift.top + this.unfocusedShadowShift.bottom;

    $w("top left bottom right").each(function(pos) {this.focusedShadowShift[pos] = content.getNumStyle("margin-" + pos) || 0}.bind(this));
    this.focusedShadowShift.width  = this.focusedShadowShift.left + this.focusedShadowShift.right;
    this.focusedShadowShift.height = this.focusedShadowShift.top + this.focusedShadowShift.bottom;

    this.shadowShift = this.options.focus ? this.focusedShadowShift : this.unfocusedShadowShift;

    // Get shadow size
    this.shadowSize  = {top:    this.shadowContents[0].childElements()[1].getNumStyle("height"),
                        left:   this.shadowContents[0].childElements()[1].getNumStyle("width"),
                        bottom: this.shadowContents[2].childElements()[1].getNumStyle("height"),
                        right:  this.shadowContents[0].childElements()[2].getNumStyle("width")};

    this.shadowSize.width  = this.shadowSize.left + this.shadowSize.right;
    this.shadowSize.height = this.shadowSize.top + this.shadowSize.bottom;

    // Remove padding
    content.setStyle("padding:0; margin:0");
    this.shadow.hide();
  },

  // Update shadow size (called when it changes from focused to blur and vice-versa)
  updateShadow: function() {
    this.shadowShift = this.options.focus ? this.focusedShadowShift : this.unfocusedShadowShift;
    var shadowStyle = this.shadow.style, pos  = this.getElementPosition(), size = this.element.getDimensions();

    shadowStyle.top  =  pos.top    - this.shadowSize.top   + this.shadowShift.top   + 'px';
    shadowStyle.left  = pos.left   - this.shadowSize.left  + this.shadowShift.left  + 'px';
    shadowStyle.width = size.width + this.shadowSize.width - this.shadowShift.width + "px";
    var h = size.height - this.shadowShift.height + "px";
    this.centerElements.each(function(e) {e.style.height = h});

    var w = size.width + this.shadowSize.width - this.shadowShift.width+ "px";
    this.shadowContents.each(function(item) { item.style.width = w });
  },

  // Get element position in integer values
  getElementPosition: function() {
    return {top: this.element.getNumStyle("top"), left: this.element.getNumStyle("left")}
  }
});

// Set theme and focus as read/write accessor
document.whenReady(function() { CSS.fixPNG("shadow") });
/*
Class: UI.Window
  Main class to handle windows inside a web page.

  Example:
    > new UI.Window({ theme: 'bluglighting' }).show()
*/


/*
<div class="STitle">Options</div>
*/

UI.Window = Class.create(UI.Options, {
  // Group: Options
  options: {

    // Property: theme
    //   window theme, uses the window manager theme as default
    theme:         null,

    // Property: shadowTheme
    //   window shadow theme, uses the window manager one as default
    //   Only useful if <shadow> options is true, see <UI.Shadow> for details
    shadowTheme:   null,

    // Property: id
    //   id ot the window, generated by default
    id:            null,

    // Property: windowManager
    //   window manager that manages this window,
    //   uses UI.defaultWM as default
    windowManager: null,

    top:           null,
    left:          null,
    width:         200,
    height:        300,
    minHeight:     100,
    minWidth:      200,
    maxHeight:     null,
    maxWidth:      null,
    altitude:      "front",

    // Property: resizable
    //   true by default
    resizable:     true,

    // Property: draggable
    //   true by default
    draggable:     true,

    // Property: wired
    //   draw wires around window when dragged, false by default
    wired:         false,

    // Property: show
    //   Function used to show the window, default is Element.show
    show: Element.show,

    // Property: hide
    //   Function used to hide the window, default is Element.hide.
    hide: Element.hide,

    // Property: superflousEffects
    //   uses superflous effects when resizing or moving window.
    //   it's true if Scriptaculous' Effect is defined, false otherwise
    superflousEffects: !Object.isUndefined(window.Effect),

    // Property: shadow
    //   draw shadow around the window, default is false
    shadow:            false,

    // Property: activeOnClick
    //   When set to true, a click on an blurred window content activates it,
    //   default is true
    activeOnClick:     true,

    // Grid
    gridX:  1,
    gridY:  1,

    // Buttons and actions (false to disable)

    // Property: close
    //   Window method name as string, or false to disable close button
    //   Default is 'destroy'
    close:    'destroy',

    // Property: minimize
    //   Window method name as string, or false to disable minimize button
    //   Default is 'toggleFold'
    minimize: 'toggleFold',

    // Property: maximize
    //   Window method name as string, or false to disable maximize button
    //   Default is 'toggleMaximize'
    maximize: 'toggleMaximize'
  },

  // Group: Attributes

  /*
    Property: id
      DOM id of the window's element

    Property: element
      DOM element containing the window

    Property: windowManager
      Window manager that manages the window

    Property: content
      Window content element

    Property: header
      Window header element

    Property: footer
      Window footer element

    Property: visible
      true if window is visible

    Property: focused
      true if window is focused

    Property: folded
      true if window is folded

    Property: maximized
      true if window is maximized
  */

  /*
    Group: Events
    List of events fired by a window
  */

  /*
    Property: created
      Fired after creating the window

    Property: destroyed
      Fired after destroying the window

    Property: showing
      Fired when showing a window

    Property: shown
      Fired after showing effect

    Property: hiding
      Fired when hiding a window

    Property: hidden
      Fired after hiding effect

    Property: focused
      Fired after focusing the window

    Property: blurred
      Fired after bluring the window

    Property: maximized
      Fired after maximizing the window

    Property: restored
      Fired after restoring the window from its maximized state

    Property: fold
      Fired after unfolding the window

    Property: unfold
      Fired after folding the window

    Property: altitude:changed
      Fired when window altitude has changed (z-index)

    Property: size:changed
      Fired when window size has changed

    Property: position:changed
      Fired when window position has changed

    Property: move:started
      Fired when user has started a moving a window, position:changed are then fired continously

    Property: move:ended
      Fired when user has finished moving a window

    Property: resize:started
      Fired when user has started resizing window, size:changed are then fired continuously

    Property: resize:ended
      Fired when user has finished resizing window

  */

  // Group: Contructor

  /*
    Method: initialize
      Constructor, should not be called directly, it's called by new operator (new Window())
      The window is not open and nothing has been added to the DOM yet

    Parameters:
      options - (Hash) list of optional parameters

    Returns:
      this
  */
  initialize: function(options) {
    this.setOptions(options);
    this.windowManager = this.options.windowManager || UI.defaultWM;
    this.create();
    this.id = this.element.id;
    this.windowManager.register(this);
    this.render();
    if (this.options.activeOnClick)
      this.overlay.setStyle({ zIndex: this.lastZIndex + 1 }).show();
  },

  /*
    Method: destroy
      Destructor, closes window, cleans up DOM and memory
  */
  destroy: function($super) {
    this.hide();
    if (this.centerOptions)
      Event.stopObserving(this.windowManager.scrollContainer, "scroll", this.centerOptions.handler);
    this.windowManager.unregister(this);
    this.fire('destroyed');
  },

  // Group: Event handling

  /*
    Method: fire
      Fires a window custom event automatically namespaced in "window:" (see Prototype custom events).
      The memo object contains a "window" property referring to the window.

    Example:
      > UI.Window.addMethods({
      >   iconify: function() {
      >     // ... your iconifying code here ...
      >     this.fire('iconified');
      >     // chain friendly
      >     return this;
      >   }
      > });
      >
      > document.observe('window:iconified', function(event) {
      >   alert("Window with id " + event.memo.window.id + " has just been iconified");
      > });

    Parameters:
      eventName - an event name
      memo - a memo object

    Returns:
      fired event
  */
  fire: function(eventName, memo) {
    memo = memo || { };
    memo.window = this;
    return (this.savedElement || this.element).fire('window:' + eventName, memo);
  },

   /*
     Method: observe
       Observe a window event with a handler function automatically bound to the window

     Parameters:
       eventName - an event name
       handler - a handler function

     Returns:
       this
  */
  observe: function(eventName, handler) {
    this.element.observe('window:' + eventName, handler.bind(this));
    return this;
  },


  // Group: Actions

  /*
    Method: show
      Opens the window (appends it to the DOM)

    Parameters:
      modal - open the window in a modal mode (default false)

    Returns:
      this
 */
  show: function(modal) {
    if (this.visible) return this;

    this.fire('showing');
    this.effect('show');

    if (modal) {
      this.windowManager.startModalSession(this);
      this.modalSession = true;
    }

    this.addElements();
    this.visible = true;

    new PeriodicalExecuter(function(executer) {
      if (!this.element.visible()) return;
      this.fire('shown');
      executer.stop();
    }.bind(this), 0.1);

    return this;
  },

  /*
    Method: hide
       Hides the window, (removes it from the DOM)

     Returns:
       this
  */
  hide: function() {
    if (!this.visible) return this;

    this.fire('hiding');
    this.effect('hide');

    if (this.modalSession) {
      this.windowManager.endModalSession(this);
      this.modalSession = false;
    }

    this.windowManager.hide(this);

    new PeriodicalExecuter(function(executer) {
      if (this.element.visible()) return;
      this.visible = false;
      this.element.remove();
      this.fire('hidden');
      executer.stop();
    }.bind(this), 0.1);

    return this;
  },

  close: function() {
    return this.action('close');
  },

  /*
    Method: activate
      Brings window to the front and sets focus on it

     Returns:
       this
  */
  activate: function() {
    return this.bringToFront().focus();
  },

  /*
    Method: bringToFront
      Brings window to the front (but does not set focus on it)

     Returns:
       this
  */
  bringToFront: function() {
    return this.setAltitude('front');
  },

  /*
    Method: sendToBack
      Sends window to the back (without changing its focus)

     Returns:
       this
  */
  sendToBack: function() {
    return this.setAltitude('back');
  },

  /*
    Method: focus
      Focuses the window (without bringing window to the front)

     Returns:
       this
  */
  focus: function() {
    if (this.focused) return this;

    this.windowManager.focus(this);
    // Hide the overlay that catch events
    this.overlay.hide();
    // Add focused class name
    this.element.addClassName(this.options.theme + '_focused');

    this.focused = true;
    this.fire('focused');
    return this;
  },

  /*
    Method: blur
      Blurs the window (without changing windows order)

     Returns:
       this
  */
  blur: function() {
    if (!this.focused) return this;

    this.windowManager.blur(this);
    this.element.removeClassName(this.options.theme + '_focused');

    // Show the overlay to catch events
    if (this.options.activeOnClick)
      this.overlay.setStyle({ zIndex: this.lastZIndex + 1 }).show();

    this.focused = false;
    this.fire('blurred');
    return this;
  },

  /*
    Method: maximize
      Maximizes window inside its viewport (managed by WindowManager)
      Makes window take full size of its viewport

     Returns:
       this
  */
  maximize: function() {
    if (this.maximized) return this;

    // Get bounds has to be before  this.windowManager.maximize for IE!! this.windowManager.maximize remove overflow
    // and it breaks this.getBounds()
    var bounds = this.getBounds();
    if (this.windowManager.maximize(this)) {
      this.disableButton('minimize').setResizable(false).setDraggable(false);

      this.activate();
      this.maximized = true;
      this.savedArea = bounds;
      var newBounds = Object.extend(this.windowManager.viewport.getDimensions(), { top: 0, left: 0 });
      this[this.options.superflousEffects && !Prototype.Browser.IE ? "morph" : "setBounds"](newBounds);
      this.fire('maximized');
      return this;
    }
  },

  /*
    Function: restore
      Restores a maximized window to its initial size

     Returns:
       this
  */
  restore: function() {
    if (!this.maximized) return this;

    if (this.windowManager.restore(this)) {
      this[this.options.superflousEffects  && !Prototype.Browser.IE ? "morph" : "setBounds"](this.savedArea);
      this.enableButton("minimize").setResizable(true).setDraggable(true);

      this.maximized = false;
      this.fire('restored');
      return this;
    }
  },

  /*
    Function: toggleMaximize
      Maximizes/Restores window inside it's viewport (managed by WindowManager)

     Returns:
       this
  */
  toggleMaximize: function() {
    return this.maximized ? this.restore() : this.maximize();
  },

  /*
    Function: adapt
      Adapts window size to fit its content

     Returns:
       this
  */
  adapt: function() {
    var dimensions = this.content.getScrollDimensions();
    if (this.options.superflousEffects)
      this.morph(dimensions, true);
    else
      this.setSize(dimensions.width, dimensions.height, true);
    return this;
  },

  /*
    Method: fold
      Folds window content

     Returns:
       this
  */
  fold: function() {
    if (!this.folded) {
      var size = this.getSize(true);
      this.folded = true;
      this.savedInnerHeight = size.height;

      if (this.options.superflousEffects)
        this.morph({ width: size.width, height: 0 }, true);
      else
        this.setSize(size.width, 0, true);

      this.setResizable(false);
      this.fire("fold");
    }
    return this;
  },

  /*
    Method: unfold
      Unfolds window content

     Returns:
       this
  */
  unfold: function() {
    if (this.folded) {
      var size = this.getSize(true);
      this.folded = false;

      if (this.options.superflousEffects)
        this.morph({ width: size.width, height: this.savedInnerHeight }, true);
      else
        this.setSize(size.width, this.savedInnerHeight, true);

      this.setResizable(true);
      this.fire("unfold");
    }
    return this;
  },

  /*
    Method: toggleFold
      Folds/Unfolds window content

     Returns:
       this
  */
  toggleFold: function() {
    return this.folded ? this.unfold() : this.fold();
  },

  /*
    Method: setHeader
      Sets window header, equivalent to this.header.update(...) but allows chaining

     Returns:
       this
  */
  setHeader: function(header) {
    this.header.update(header);
    return this;
  },

  /*
    Method: setContent
      Sets window content, equivalent to this.content.update(...) but allows chaining

     Returns:
       this
  */
  setContent: function(content) {
    this.content.update(content);
    return this;
  },

  /*
    Method: setFooter
      Sets window footer, equivalent to this.footer.update(...) but allows chaining

     Returns:
       this
  */
  setFooter: function(footer) {
    this.footer.update(footer);
    return this;
  },

  /*
    Method: setAjaxContent
      Sets window content using Ajax request

     Parameters:
        url - Ajax URL
        options - Ajax Updater options (see http://prototypejs.org/api/ajax/options and
          http://prototypejs.org/api/ajax/updater)

     Returns:
       this
  */
  setAjaxContent: function(url, options) {
    if (!options)
      options = {};

    // bind all callbacks to the window
    Object.keys(options).each(function(name) {
      if (Object.isFunction(options[name]))
        options[name] = options[name].bind(this);
    }, this);

    var onComplete = options.onComplete;
    options.onComplete = (function(response, json) {
      this.setContent(response.responseText);
      if (Object.isFunction(onComplete)) onComplete(response, json);
    }).bind(this);

    new Ajax.Request(url, options);
    return this;
  },

  // Group: Size and Position

  /*
    Method: getPosition
      Returns top/left position of a window (in pixels)

     Returns:
       an Hash {top:, left:}
  */
  getPosition: function() {
    return { left: this.options.left, top: this.options.top };
  },

  /*
    Method: setPosition
      Sets top/left position of a window (in pixels)

    Parameters
      top:  top position in pixel
      left: left position in pixel

    Returns:
      this
  */
  setPosition: function(top, left) {
    var pos = this.computePosition(top, left);
    this.options.top  = pos.top;
    this.options.left = pos.left;

    var elementStyle  = this.element.style;
    elementStyle.top  = pos.top + 'px';
    elementStyle.left = pos.left + 'px';

    this.fire('position:changed');
    return this;
  },

  /*
    Method: center
      Centers the window within its viewport

    Returns:
      this
  */
  center: function(options) {
    var size          = this.getSize(),
        windowManager = this.windowManager,
        viewport      = windowManager.viewport;
        viewportArea  = viewport.getDimensions(),
        offset        = viewport.getScrollOffset();

    if (options && options.auto) {
      this.centerOptions = Object.extend({ handler: this.recenter.bind(this) }, options);
      Event.observe(this.windowManager.scrollContainer,"scroll", this.centerOptions.handler);
      Event.observe(window,"resize", this.centerOptions.handler);
    }

    options = Object.extend({
      top:  (viewportArea.height - size.height) / 2,
      left: (viewportArea.width  - size.width)  / 2
    }, options || {});

    return this.setPosition(options.top + offset.top, options.left + offset.left);
  },

  /*
    Method: getSize
      Returns window width/height dimensions (in pixels)

    Parameters
      innerSize: returns content size if true, window size if false (defaults to false)

    Returns:
      Hash {width:, height:}
  */
  getSize: function(innerSize) {
    if (innerSize)
      return { width:  this.options.width  - this.borderSize.width,
               height: this.options.height - this.borderSize.height };
    else
      return { width: this.options.width, height: this.options.height };
  },

  /*
    Method: setSize
      Sets window width/height dimensions (in pixels), fires size:changed

    Parameters
      width:  width (in pixels)
      height: height (in pixels)
      innerSize: if true change set content size, else set window size (defaults to false)

    Returns:
      this
  */
  setSize: function(width, height, innerSize) {
    var size = this.computeSize(width, height, innerSize);
    var elementStyle = this.element.style, contentStyle = this.content.style;

    this.options.width  = size.outerWidth;
    this.options.height = size.outerHeight;

    elementStyle.width = size.outerWidth + "px", elementStyle.height = size.outerHeight + "px";
    contentStyle.width = size.innerWidth + "px", contentStyle.height = size.innerHeight + "px";
    this.overlay.style.height = size.innerHeight + "px";

    this.fire('size:changed');
 	  return this;
  },

  /*
    Method: getBounds
      Returns window bounds (in pixels)

    Parameters
      innerSize: returns content size if true, window size otherwise

    Returns:
      an Hash {top:, left:, width:, height:}
  */
  getBounds: function(innerSize) {
    return Object.extend(this.getPosition(), this.getSize(innerSize));
  },

  /*
    Method: setBounds
      Sets window bounds (in pixels), fires position:changed and size:changed

    Parameters
      bounds: Hash {top:, left:, width:, height:} where all values are optional
      innerSize: sets content size if true, window size otherwise

    Returns:
      Hash {top:, left:, width:, height:}
  */
  setBounds: function(bounds, innerSize) {
    return this.setPosition(bounds.top, bounds.left)
               .setSize(bounds.width, bounds.height, innerSize);
  },

  morph: function(bounds, innerSize) {
    bounds = Object.extend(this.getBounds(innerSize), bounds || {});

    if (this.centerOptions && this.centerOptions.auto)
       bounds = Object.extend(bounds, this.computeRecenter(bounds));

    if (innerSize) {
      bounds.width  += this.borderSize.width;
      bounds.height += this.borderSize.height;
    }

    this.animating = true;

    new UI.Window.Effects.Morph(this, bounds, {
      duration: 0.5,
      afterFinish: function() { this.animating = false }.bind(this)
    });

    Object.extend(this.options, bounds);

    return this;
  },

  /*
    Method: getAltitude
      Returns window altitude, an integer between 0 and the number of windows,
      the higher the altitude number - the higher the window position.
  */
  getAltitude: function() {
    return this.windowManager.getAltitude(this);
  },

  /*
    Method: setAltitude
      Sets window altitude, fires 'altitude:changed' if altitude was changed
  */
  setAltitude: function(altitude) {
    if (this.windowManager.setAltitude(this, altitude))
      this.fire('altitude:changed');
    return this;
  },

  /*
    Method: setResizable
      TODO
  */
  setResizable: function(resizable) {
    this.options.resizable = resizable;

    var toggleClassName = (resizable ? 'add' : 'remove') + 'ClassName';

    this.element[toggleClassName]('resizable')
      .select('div:[class*=_sizer]').invoke(resizable ? 'show' : 'hide');
    if (resizable)
      this.createResizeHandles();

    this.element.select('div.se').first()[toggleClassName]('se_resize_handle');

    return this;
  },

  /*
    Method: setDraggable
      TODO
  */
  setDraggable: function(draggable) {
    this.options.draggable = draggable;
    this.element[(draggable ? 'add' : 'remove') + 'ClassName']('draggable');
    return this;
  },

  // Group: Theme
  /*
    Method: getTheme
      Returns window theme name
  */
  getTheme: function() {
    return this.options.theme || this.windowManager.getTheme();
  },

  /*
    Method: setTheme
      Sets window theme
  */
  setTheme: function(theme, windowManagerTheme) {
    this.element.removeClassName(this.getTheme()).addClassName(theme);
    // window has it's own theme
    if (!windowManagerTheme)
      this.options.theme = theme;

    return this;
  },

  /*
    Method: getShadowTheme
      Returns shadow theme name
  */
  getShadowTheme: function() {
    return this.options.shadowTheme || this.windowManager.getShadowTheme();
  }
});

UI.Window.addMethods(UI.Window.Buttons);
UI.Window.addMethods(UI.Window.Shadow);
UI.Window.optionsAccessor($w(" minWidth minHeight maxWidth maxHeight gridX gridY altitude "));
// Private functions for window.js
UI.Window.addMethods({
  style: "position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0;",

  action: function(name) {
    var action = this.options[name];
    if (action)
      Object.isString(action) ? this[action]() : action.call(this, this);
  },

  create: function() {
    function createDiv(className, options) {
      return new Element('div', Object.extend({ className: className }, options));
    };

    // Main div
    this.element = createDiv("ui-window " + this.getTheme(), {
      id: this.options.id,
      style: "top:-10000px; left:-10000px"
    });

    // Create HTML window code
    this.header  = createDiv('n move_handle').enableDrag();
    this.content = createDiv('content').appendText(' ');
    this.footer  = createDiv('s move_handle').enableDrag();

    var header   = createDiv('nw').insert(createDiv('ne').insert(this.header));
    var content  = createDiv('w').insert(createDiv('e', {style: "position:relative"}).insert(this.content));
    var footer   = createDiv('sw').insert(createDiv('se' + (this.options.resizable ?  " se_resize_handle" : "")).insert(this.footer));

    this.element.insert(header).insert(content).insert(footer).identify('ui-window');
    this.header.observe('mousedown', this.activate.bind(this));

    this.setDraggable(this.options.draggable);
    this.setResizable(this.options.resizable);

    this.overlay = new Element('div', { style: this.style + "display: none" })
        .observe('mousedown', this.activate.bind(this));

    if (this.options.activeOnClick)
      this.content.insert({ before: this.overlay });
  },

  createWiredElement: function() {
    this.wiredElement = this.wiredElement || new Element("div", {
      className: this.getTheme() + "_wired",
      style:    "display: none; position: absolute; top: 0; left: 0"
    });
  },

  createResizeHandles: function() {
    $w(" n  w  e  s  nw  ne  sw  se ").each(function(id) {
      this.insert(new Element("div", {
        className:   id + "_sizer resize_handle",
        drag_prefix: id }).enableDrag());
    }, this.element);
    this.createResizeHandles = Prototype.emptyFunction;
  },

  // First rendering, pre-compute window border size
  render: function() {
    this.addElements();

    this.computeBorderSize();
    this.updateButtonsOrder();
    this.element.hide().remove();

    // this.options contains top, left, width and height keys
    return this.setBounds(this.options);
  },

  // Adds window elements to the DOM
  addElements: function() {
    this.windowManager.container.appendChild(this.element);
  },

  // Set z-index to all window elements
  setZIndex: function(zIndex) {
    if (this.zIndex != zIndex) {
      this.zIndex = zIndex;
      [ this.element ].concat(this.element.childElements()).each(function(element) {
        element.style.zIndex = zIndex++;
      });
      this.lastZIndex = zIndex;
    }
    return this;
  },

  effect: function(name, element, options) {
    var effect = this.options[name] || Prototype.emptyFunction;
    effect(element || this.element, options || {});
  },

  // re-compute window border size
  computeBorderSize: function() {
    if (this.element) {
      if (Prototype.Browser.IEVersion >= 7)
        this.content.style.width = "100%";
      var dim = this.element.getDimensions(), pos = this.content.positionedOffset();
      this.borderSize = {  top:    pos[1],
                           bottom: dim.height - pos[1] - this.content.getHeight(),
                           left:   pos[0],
                           right:  dim.width - pos[0] - this.content.getWidth() };
      this.borderSize.width  = this.borderSize.left + this.borderSize.right;
      this.borderSize.height = this.borderSize.top  + this.borderSize.bottom;
      if (Prototype.Browser.IEVersion >= 7)
        this.content.style.width = "auto";
    }
  },

  computeSize: function(width, height, innerSize) {
    var innerWidth, innerHeight, outerWidth, outerHeight;
	  if (innerSize) {
	    outerWidth  =  width  + this.borderSize.width;
	    outerHeight =  height + this.borderSize.height;
    } else {
	    outerWidth  =  width;
	    outerHeight =  height;
    }
    // Check grid value
    if (!this.animating) {
      outerWidth = outerWidth.snap(this.options.gridX);
      outerHeight = outerHeight.snap(this.options.gridY);

      // Check min size
      if (!this.folded) {
        if (outerWidth < this.options.minWidth)
          outerWidth = this.options.minWidth;

        if (outerHeight < this.options.minHeight)
          outerHeight = this.options.minHeight;
      }

      // Check max size
      if (this.options.maxWidth && outerWidth > this.options.maxWidth)
        outerWidth = this.options.maxWidth;

      if (this.options.maxHeight && outerHeight > this.options.maxHeight)
        outerHeight = this.options.maxHeight;
    }

    if (this.centerOptions && this.centerOptions.auto)
      this.recenter();

    innerWidth  = outerWidth - this.borderSize.width;
    innerHeight = outerHeight - this.borderSize.height;
    return {
      innerWidth: innerWidth, innerHeight: innerHeight,
      outerWidth: outerWidth, outerHeight: outerHeight
    };
  },

  computePosition: function(top, left) {
    if (this.centerOptions && this.centerOptions.auto)
      return this.computeRecenter(this.getSize());                                                                                                            ;

    return {
      top:  this.animating ? top  : top.snap(this.options.gridY),
      left: this.animating ? left : left.snap(this.options.gridX)
    };
  },

  computeRecenter: function(size) {
    var viewport   = this.windowManager.viewport,
        area       = viewport.getDimensions(),
        offset     = viewport.getScrollOffset(),
        center     = {
          top:  Object.isUndefined(this.centerOptions.top)  ? (area.height - size.height) / 2 : this.centerOptions.top,
          left: Object.isUndefined(this.centerOptions.left) ? (area.width  - size.width)  / 2 : this.centerOptions.left
        };

    return {
      top:  parseInt(center.top + offset.top),
      left: parseInt(center.left + offset.left)
    };
  },

  recenter: function(event) {
    var pos = this.computeRecenter(this.getSize());
    this.setPosition(pos.top, pos.left);
  }
});
UI.URLWindow = Class.create(UI.Window, {
  options: {
    url: 'about:blank'
  },

  afterClassCreate: function() {
    this.undefMethod('setAjaxContent');
  },

  initialize: function($super, options) {
    $super(options);
    this.createIFrame();
  },

  destroy: function($super){
    this.iframe.src = null;
    $super();
  },

  getUrl: function() {
    return this.iframe.src;
  },

  setUrl: function(url, options) {
    this.iframe.src = url;
    return this;
  },

  createIFrame: function($super) {
    this.iframe = new Element('iframe', {
      style: this.style,
      frameborder: 0,
      src: this.options.url,
      name: this.element.id + "_frame",
      id:  this.element.id + "_frame"
    });

    this.content.insert(this.iframe);
  }
});
if (!Object.isUndefined(window.Effect)) {
  UI.Window.Effects = UI.Window.Effects || {};
  UI.Window.Effects.Morph = Class.create(Effect.Base, {
    initialize: function(window, bounds) {
      this.window = window;
      var options = Object.extend({
        fromBounds: this.window.getBounds(),
        toBounds:   bounds,
        from:       0,
        to:         1
      }, arguments[2] || { });
      this.start(options);
    },

    update: function(position) {
      var t = this.options.fromBounds.top + (this.options.toBounds.top   - this.options.fromBounds.top) * position;
      var l = this.options.fromBounds.left + (this.options.toBounds.left - this.options.fromBounds.left) * position;

      var ow = this.options.fromBounds.width + (this.options.toBounds.width - this.options.fromBounds.width) * position;
      var oh = this.options.fromBounds.height + (this.options.toBounds.height - this.options.fromBounds.height) * position;

      this.window.setBounds({top: t,  left: l, width: ow, height: oh})
    }
  });
}
UI.Window.addMethods({
  startDrag: function(handle) {
    this.initBounds = this.getBounds();
    this.activate();

    if (this.options.wired) {
      this.createWiredElement();
      this.wiredElement.style.cssText = this.element.style.cssText;
      this.element.hide();
      this.saveElement = this.element;
      this.windowManager.container.appendChild(this.wiredElement);
      this.element = this.wiredElement;
    }

    handle.hasClassName('resize_handle') ? this.startResize(handle) : this.startMove();
  },

  endDrag: function() {
    this.element.hasClassName('resized') ? this.endResize() : this.endMove();

    if (this.options.wired) {
      this.saveElement.style.cssText = this.wiredElement.style.cssText;
      this.wiredElement.remove();
      this.element = this.saveElement;
      this.saveElement = false;
    }
  },

  startMove: function() {
    // method used to drag
    this.drag = this.moveDrag;
    this.element.addClassName('moved');
    this.fire('move:started');
  },

  endMove: function() {
    this.element.removeClassName('moved');
    this.fire('move:ended');
  },

  startResize: function(handle) {
    this.drag = this[handle.readAttribute('drag_prefix')+'Drag'];
    this.element.addClassName('resized');
    this.fire('resize:started');
  },

  endResize: function() {
    this.element.removeClassName('resized');
    this.fire('resize:ended');
  },

  moveDrag: function(dx, dy) {
    this.setPosition(this.initBounds.top + dy, this.initBounds.left + dx);
  },

  swDrag: function(dx, dy) {
    var initBounds = this.initBounds;
    this.setSize(initBounds.width - dx, initBounds.height + dy)
        .setPosition(initBounds.top,
                     initBounds.left + (initBounds.width - this.getSize().width));
  },

  seDrag: function(dx, dy) {
    this.setSize(this.initBounds.width + dx, this.initBounds.height + dy);
  },

  nwDrag: function(dx, dy) {
    var initBounds = this.initBounds;
    this.setSize(initBounds.width - dx, initBounds.height - dy)
        .setPosition(initBounds.top + (initBounds.height - this.getSize().height),
                     initBounds.left + (initBounds.width - this.getSize().width));
  },

  neDrag: function(dx, dy) {
    var initBounds = this.initBounds;
    this.setSize(initBounds.width + dx, initBounds.height - dy)
        .setPosition(initBounds.top + (initBounds.height - this.getSize().height),
                     initBounds.left);
  },

  wDrag: function(dx, dy) {
    var initBounds = this.initBounds;
    this.setSize(initBounds.width - dx, initBounds.height)
        .setPosition(initBounds.top,
                     initBounds.left + (initBounds.width - this.getSize().width));
  },

  eDrag: function(dx, dy) {
    this.setSize(this.initBounds.width + dx, this.initBounds.height);
  },

  nDrag: function(dx, dy) {
    var initBounds = this.initBounds;
    this.setSize(initBounds.width, initBounds.height - dy)
        .setPosition(initBounds.top + (initBounds.height - this.getSize().height),
                     initBounds.left);
  },

  sDrag: function(dx, dy) {
    this.setSize(this.initBounds.width, this.initBounds.height + dy);
  }
});
UI.Window.addMethods({
  methodsAdded: function(base) {
    base.aliasMethodChain('create',  'buttons');
    base.aliasMethodChain('destroy', 'buttons');
  },

  createWithButtons: function() {
    this.createWithoutButtons();

    if (!this.options.resizable) {
      this.options.minimize = false;
      this.options.maximize = false;
    }

    this.buttons = new Element("div", { className: "buttons" })
      .observe('click',     this.onButtonsClick.bind(this))
      .observe('mouseover', this.onButtonsHover.bind(this))
      .observe('mouseout',  this.onButtonsOut.bind(this));

    this.element.insert(this.buttons);

    this.defaultButtons.each(function(button) {
      if (this.options[button] !== false)
        this.addButton(button);
    }, this);
  },

  destroyWithButtons: function() {
    this.buttons.stopObserving();
    this.destroyWithoutButtons();
  },

  defaultButtons: $w(' minimize maximize close '),

  getButtonElement: function(buttonName) {
    return this.buttons.down("." + buttonName);
  },

  // Controls close, minimize, maximize, etc.
  // action can be either a string or a function
  // if action is a string, it is the method name that will be called
  // else the function will take the window as first parameter.
  // if not given action will be taken in window's options
  addButton: function(buttonName, action) {
    this.buttons.insert(new Element("a", { className: buttonName, href: "#"}));

    if (action)
      this.options[buttonName] = action;

    return this;
  },

  removeButton: function(buttonName) {
    this.getButtonElement(buttonName).remove();
    return this;
  },

  disableButton: function(buttonName) {
    this.getButtonElement(buttonName).addClassName("disabled");
    return this;
  },

  enableButton: function(buttonName) {
    this.getButtonElement(buttonName).removeClassName("disabled");
    return this;
  },

  onButtonsClick: function(event) {
    var element = event.findElement('a:not(.disabled)');

    if (element) this.action(element.className);
    event.stop();
  },

  onButtonsHover: function(event) {
    this.buttons.addClassName("over");
  },

  onButtonsOut: function(event) {
    this.buttons.removeClassName("over");
  },

  updateButtonsOrder: function() {
    var buttons = this.buttons.childElements();

    buttons.inject(new Array(buttons.length), function(array, button) {
      array[parseInt(button.getStyle("padding-top"))] = button.setStyle("padding: 0");
      return array;
    }).each(function(button) { this.buttons.insert(button) }, this);
  }
});
UI.Window.addMethods({
  methodsAdded: function(base) {
    (function(methods) {
      $w(methods).each(function(m) { base.aliasMethodChain(m, 'shadow') });
    })(' create addElements setZIndex setPosition setSize setBounds ');
  },

  showShadow: function() {
    if (this.shadow) {
      this.shadow.hide();
      this.effect('show', this.shadow.shadow);
    }
    if (this.iframe)
      this.iframe.show();
  },

  hideShadow: function() {
    if (this.shadow)
      this.effect('hide', this.shadow.shadow);
    if (this.iframe)
      this.iframe.hide();
  },

  removeShadow: function() {
    if (this.shadow)
      this.shadow.remove();
  },

  focusShadow: function() {
    if (this.shadow)
      this.shadow.focus();
  },

  blurShadow: function() {
    if (this.shadow)
      this.shadow.blur();
  },

  // Private Functions
  createWithShadow: function() {
    this.createWithoutShadow();

    this.observe('showing', this.showShadow)
        .observe('hiding',  this.hideShadow)
        .observe('hidden',  this.removeShadow)
        .observe('focused', this.focusShadow)
        .observe('blurred', this.blurShadow);

    if (this.options.shadow)
      this.shadow = new UI.Shadow(this.element, {theme: this.getShadowTheme(), withIFrameShim: false});
    this.iframe = Prototype.Browser.IE ? new UI.IframeShim({parent: this.windowManager.container}) : null;
  },

  addElementsWithShadow: function() {
    this.addElementsWithoutShadow();
    if (this.shadow) {
      this.shadow.setBounds(this.options).render();
    }
  },

  setZIndexWithShadow: function(zIndex) {
    if (this.zIndex != zIndex) {
      if (this.shadow)
        this.shadow.setZIndex(zIndex - 1);
      this.setZIndexWithoutShadow(zIndex);
      this.zIndex = zIndex;
    }
    return this;
  },

  setPositionWithShadow: function(top, left) {
    this.setPositionWithoutShadow(top, left);
    if (this.shadow) {
      var pos = this.getPosition();
      this.shadow.setPosition(pos.top, pos.left);
    }
    if (this.iframe) {
      var pos = this.getPosition();
      if(!this.iframe.setPosition) {
			this.iframe.style.top  = parseInt(pos.top) + "px";
			this.iframe.style.left = parseInt(pos.left) + "px";
		} else {
			this.iframe.setPosition(pos.top, pos.left);
		}
    }
    return this;
  },

  setSizeWithShadow: function(width, height, innerSize) {
    this.setSizeWithoutShadow(width, height, innerSize);
    if (this.shadow) {
      var size = this.getSize();
      this.shadow.setSize(size.width, size.height);
    }
    if (this.iframe) {
      var size = this.getSize();
      if(!this.iframe.setSize) {
			this.iframe.style.width  = parseInt(width) + "px";
			this.iframe.style.height = parseInt(height) + "px"; 
      } else {
			this.iframe.setSize(size.width, size.height);
		}
    }
    return this;
  },

  setBoundsWithShadow: function(bounds, innerSize) {
    this.setBoundsWithoutShadow(bounds, innerSize);
    if (this.shadow)
      this.shadow.setBounds(this.getBounds());
    if (this.iframe)
		if(!this.iframe.setBounds) {
			for (prop in this.getBounds()) {
				bounds[prop] = parseInt(bounds[prop]) + 'px';
			}
			this.iframe.setStyle(bounds);
		} else {
			this.iframe.setBounds(this.getBounds());
		}
  }
});
/*
Class: UI.Dialog
  Main class to handle dialog. Dialog are Windows with buttons like Ok/Cancel for having nicer alert/confirm javascript dialog.

  Buttons are fully configurable and designed by CSS

  Alert and confirm are already designed

  To open a alert dialog just do:
  > new UI.Dialog({buttons: UI.Dialog.okButton}).center().setHeader("Open a new sesssion").setContent("your content").show(true);
  >

  To open a cofirm dialog just do:
  > new UI.Dialog({buttons: UI.Dialog.okCancelButton}).center().setHeader("Open a new sesssion").setContent("your content").show(true);
  >
  > // Or without options as it's by default
  > new UI.Dialog().center().setHeader("Open a new sesssion").setContent("your content").show(true);

*/

UI.Dialog = Class.create(UI.Window, {
  options: {
    buttons:       null, // default = ok/cancel button
    beforeSelect:  Prototype.trueFunction,
    close:         false,
    resizable:     false,
    activeOnClick: false
  },

  // Group: Attributes

  /*
    Property: buttons
      Array of buttons attributes. A button is defined with an hash with
      - title
      - className
      - callback (optional) called when a user click on a button (not disabled)

      Example for alert Dialog
      > [{title: 'Ok',     className: 'ok',     callback: function(win) { win.destroy(); }}]
      >
      >

    Property: beforeSelect
      function calls when a user click on a button (not disabled). If beforeSelect returns false, button callbacks is not executed
  */


  // Group: Instance Methods

  /*
    Method: disableButton
      Disables a button (add disabled css class name)
      The button will not be clickable anymore

    Parameters:
      index - Button index (start from 0)

    Returns:
      this
  */
  disableDialogButton: function(index) {
    var button = this.getDialogButton(index);
    if (button)
      button.addClassName("disabled");
    return this;
  },

  /*
    Method: enableButton
      Enables a button (remove disabled css class name)
      The button is now clickable

    Parameters:
      index - Button index (start from 0)

    Returns:
      this
  */
  enableDialogButton: function(index) {
    var button = this.getDialogButton(index);
    if (button)
      button.removeClassName("disabled");
    return this;
  },

  /*
    Method: getButton
      Gets button element

    Parameters:
      index - Button index (start from 0)

    Returns:
      button element or null if bad index
  */
  getDialogButton: function(index) {
    var buttons = this.buttonContainer.childElements();
    if (index >= 0 && index < buttons.length)
      return buttons[index];
    else
      return null;
  },

  // Override create to add dialog buttons
  create: function($super) {
    $super();

    // Create buttons
    this.buttonContainer = this.createButtons();
    // Add a new content for dialog content
    this.dialogContent   = new Element('div', {className:'ui-dialog-content'});

    this.content.update(this.dialogContent);
    this.content.insert(this.buttonContainer);
  },

  addElements: function($super) {
    $super();
    // Pre compute buttons height
    this.buttonsHeight = this.buttonContainer.getHeight() || this.buttonsHeight;
    this.setDialogSize();
  },

  setContent: function(content, withoutButton) {
    this.dialogContent.update(content);

    // Remove button if need be
    if (withoutButton && this.buttonContainer.parentNode)
      this.buttonContainer.remove();
    else
      this.content.insert(this.buttonContainer);

    // Update dialog size
    this.setDialogSize();
    return this;
  },

  onSelect: function(e) {
    var element = e.element();
    if (element.callback && !element.hasClassName('disabled')) {
      if (this.options.beforeSelect(element))
        element.callback(this);
    }
  },

  createButtons: function(dialogButtons) {
    var buttonContainer = new Element('div', { className: 'ui-dialog-buttons'});
    (this.options.buttons || UI.Dialog.okCancelButtons).each(function(item){
      var button;
      if (item.separator)
        button = new Element('span', {className: 'separator'});
      else
        button = new Element('button', {title: item.title,
                                        className: (item.className || '') + (item.disabled ? ' disabled' : '')})
                   .observe('click', this.onSelect.bind(this))
                   .update(item.title);

       buttonContainer.insert(button);
       button.callback = item.callback;
    }.bind(this));
    return buttonContainer;
  },

  setDialogSize: function () {
    // Do not compute dialog size if window is not completly ready
    if (!this.borderSize)
      return;

    this.buttonsHeight = this.buttonContainer.getHeight() || this.buttonsHeight;
    var style = this.dialogContent.style, size  = this.getSize(true);
    style.width = size.width + "px", style.height = size.height - this.buttonsHeight + "px";
  },

  setSize: function($super, width, height, innerSize) {
  	$super(width, height, innerSize);
    this.setDialogSize();
    return this;
  }
});


UI.Dialog =  Object.extend(UI.Dialog, {
  okButton        : [{title: 'Ok',     className: 'ok',     callback: function(win) { win.destroy(); }}],
  okCancelButtons : [{title: 'Ok',     className: 'ok',     callback: function(win) { win.destroy(); }},
                     {title: 'Cancel', className: 'cancel', callback: function(win) { win.destroy(); }}]
});

/*
Class: UI.WindowManager
  Window Manager.
  A default instance of this class is created in UI.defaultWM.

  Example:
    > new UI.WindowManger({
    >   container: 'desktop',
    >   theme: 'mac_os_x'
    > });
*/

UI.WindowManager = Class.create(UI.Options, {
  options: {
    container:   null, // will default to document.body
    zIndex:      0,
    theme:       "alphacube",
    shadowTheme: "mac_shadow",
    showOverlay: Element.show,
    hideOverlay: Element.hide,
    positionningStrategy: function(win, area) {
      UI.WindowManager.DumbPositionningStrategy(win, area);
    }
  },

  initialize: function(options) {
    this.setOptions(options);

    this.container = $(this.options.container || document.body);

    if (this.container === $(document.body)) {
      this.viewport = document.viewport;
      this.scrollContainer = window;
    } else {
      this.viewport = this.scrollContainer = this.container;
    }

    this.container.observe('drag:started', this.onStartDrag.bind(this))
                  .observe('drag:updated', this.onDrag.bind(this))
                  .observe('drag:ended',   this.onEndDrag.bind(this));

    this.stack = new UI.WindowManager.Stack();
    this.modalSessions = 0;

    this.createOverlays();
    this.resizeEvent = this.resize.bind(this);

    Event.observe(window, "resize", this.resizeEvent);
  },

  destroy: function() {
    this.windows().invoke('destroy');
    this.stack.destroy();
    Event.stopObserving(window, "resize", this.resizeEvent);
  },

  /*
    Method: setTheme
      Changes window manager's theme, all windows that don't have a own theme
      will have this new theme.

    Parameters:
      theme - theme name

    Example:
      > UI.defaultWM.setTheme('bluelighting');
  */
  setTheme: function(theme) {
    this.stack.windows.select(function(w) {
      return !w.options.theme;
    }).invoke('setTheme', theme, true);
    this.options.theme = theme;
    return this;
  },

  register: function(win) {
    if (this.getWindow(win.id)) return;

    this.handlePosition(win);
    this.stack.add(win);
    this.restartZIndexes();
  },

  unregister: function(win) {
    this.stack.remove(win);

    if (win == this.focusedWindow)
      this.focusedWindow = null;
  },

  /*
    Method: getWindow
      Find the window containing a given element.

    Example:
      > $$('.ui-window a.close').invoke('observe', 'click', function() {
      >   UI.defaultWM.getWindow(this).close();
      > });

    Parameters:
      element - element or element identifier

    Returns:
      containing window or null
  */
  getWindow: function(element) {
    element = $(element);

    if (!element) return;

    if (!element.hasClassName('ui-window'))
      element = element.up('.ui-window');

    var id = element.id;
    return this.stack.windows.find(function(win) { return win.id == id });
  },

  /*
    Method: windows
      Returns an array of all windows handled by this window manager.
      First one is the back window, last one is the front window.

    Example:
      > UI.defaultWM.windows().invoke('destroy');
  */
  windows: function() {
    return this.stack.windows.clone();
  },

  /*
    Method: getFocusedWindow
      Returns the focused window
  */
  getFocusedWindow: function() {
    return this.focusedWindow;
  },

  // INTERNAL

  // Modal mode
  startModalSession: function(win) {
    if (!this.modalSessions) {
      this.removeOverflow();
      this.modalOverlay.className = win.getTheme() + "_overlay";
      this.container.appendChild(this.modalOverlay);

      if (!this.modalOverlay.opacity)
        this.modalOverlay.opacity = this.modalOverlay.getOpacity();
      this.modalOverlay.setStyle("height: " + this.viewport.getHeight() + "px");

      this.options.showOverlay(this.modalOverlay, {from: 0, to: this.modalOverlay.opacity});
      if (this.iframe) {
        this.iframe.setBounds({top: 0, left: 0, width: this.viewport.getWidth(), height: this.viewport.getHeight()});
        this.iframe.show();
      }
    }
    this.modalOverlay.setStyle({ zIndex: win.zIndex - 1 });
    this.modalSessions++;
  },

  endModalSession: function(win) {
    this.modalSessions--;
    if (this.modalSessions) {
      this.modalOverlay.setStyle({ zIndex: this.stack.getPreviousWindow(win).zIndex - 1 });
    } else {
      this.resetOverflow();
      this.options.hideOverlay(this.modalOverlay, { from: this.modalOverlay.opacity, to: 0 });
      if (this.iframe)
        this.iframe.hide();
    }
  },

  moveHandleSelector:   '.ui-window.draggable .move_handle',
  resizeHandleSelector: '.ui-window.resizable .resize_handle',

  onStartDrag: function(event) {
    var handle = event.element(),
        isMoveHandle   = handle.match(this.moveHandleSelector),
        isResizeHandle = handle.match(this.resizeHandleSelector);

    // ensure dragged element is a window handle !
    if (isResizeHandle || isMoveHandle) {
      event.stop();

      // find the corresponding window
      var win = this.getWindow(event.findElement('.ui-window'));

      // render drag overlay
      this.container.insert(this.dragOverlay.setStyle({ zIndex: this.getLastZIndex() }));

      win.startDrag(handle);
      this.draggedWindow = win;
    }
  },

  onDrag: function(event) {
    if (this.draggedWindow) {
      event.stop();
      this.draggedWindow.drag(event.memo.dx, event.memo.dy);
    }
  },

  onEndDrag: function(event) {
    if (this.draggedWindow) {
      event.stop();
      this.dragOverlay.remove();
      this.draggedWindow.endDrag();
      this.draggedWindow = null;
    }
  },

  maximize: function(win) {
    this.removeOverflow();
    this.maximizedWindow = win;
    return true;
  },

  restore: function(win) {
    if (this.maximizedWindow) {
      this.resetOverflow();
      this.maximizedWindow = false;
    }
    return true;
  },

  removeOverflow: function() {
    var container = this.container;
    // Remove overflow, save overflow and scrolloffset values to restore them when restore window
    container.savedOverflow = container.style.overflow || "auto";
    container.savedOffset = this.viewport.getScrollOffset();
    container.style.overflow = "hidden";

    this.viewport.setScrollOffset({ top:0, left:0 });

    if (this.container == document.body && Prototype.Browser.IE)
      this.cssRule = CSS.addRule("html { overflow: hidden }");
  },

  resetOverflow: function() {
    var container = this.container;
    // Restore overflow ans scrolloffset
    if (container.savedOverflow) {
      if (this.container == document.body && Prototype.Browser.IE)
        this.cssRule.remove();

      container.style.overflow = container.savedOverflow;
      this.viewport.setScrollOffset(container.savedOffset);

      container.savedOffset = container.savedOverflow = null;
    }
  },

  hide: function(win) {
    var previous = this.stack.getPreviousWindow(win);
    if (previous) previous.focus();
  },

  restartZIndexes: function(){
    // Reset zIndex
    var zIndex = this.getZIndex() + 1; // keep a zIndex free for overlay divs
    this.stack.windows.each(function(w) {
      w.setZIndex(zIndex);
      zIndex = w.lastZIndex + 1;
    });
  },

  getLastZIndex: function() {
    return this.stack.getFrontWindow().lastZIndex + 1;
  },

  overlayStyle: "position: absolute; top: 0; left: 0; display: none; width: 100%;",

  createOverlays: function() {
    this.modalOverlay = new Element("div", { style: this.overlayStyle });
    this.dragOverlay  = new Element("div", { style: this.overlayStyle+"height: 100%" });
    this.iframe       = Prototype.Browser.IE ? new UI.IframeShim() : null;
  },

  focus: function(win) {
    // Blur the previous focused window
    if (this.focusedWindow)
      this.focusedWindow.blur();
    this.focusedWindow = win;
  },

  blur: function(win) {
    if (win == this.focusedWindow)
      this.focusedWindow = null;
  },

  setAltitude: function(win, altitude) {
    var stack = this.stack;

    if (altitude === "front") {
      if (stack.getFrontWindow() === win) return;
      stack.bringToFront(win);
    } else if (altitude === "back") {
      if (stack.getBackWindow() === win) return;
      stack.sendToBack(win);
    } else {
      if (stack.getPosition(win) == altitude) return;
      stack.setPosition(win, altitude);
    }

    this.restartZIndexes();
    return true;
  },

  getAltitude: function(win) {
    return this.stack.getPosition(win);
  },

  resize: function(event) {
    var area = this.viewport.getDimensions();

    if (this.maximizedWindow)
      this.maximizedWindow.setSize(area.width, area.height);

    if (this.modalOverlay.visible())
      this.modalOverlay.setStyle("height:" + area.height + "px");
  },

  handlePosition: function(win) {
    // window has its own position, nothing needs to be done
    if (Object.isNumber(win.options.top) && Object.isNumber(win.options.left))
      return;

    var strategy = this.options.positionningStrategy,
        area     = this.viewport.getDimensions();

    Object.isFunction(strategy) ? strategy(win, area) : strategy.position(win, area);
  }
});

UI.WindowManager.DumbPositionningStrategy = function(win, area) {
  size = win.getSize();

  var top  = area.height - size.height,
      left = area.width  - size.width;

  top  = top  < 0 ? 0 : Math.random() * top;
  left = left < 0 ? 0 : Math.random() * left;

  win.setPosition(top, left);
};

UI.WindowManager.optionsAccessor('zIndex', 'theme', 'shadowTheme');

UI.WindowManager.Stack = Class.create(Enumerable, {
  initialize: function() {
    this.windows = [ ];
  },

  each: function(iterator) {
    this.windows.each(iterator);
  },

  add: function(win, position) {
    this.windows.splice(position || this.windows.length, 0, win);
  },

  remove: function(win) {
    this.windows = this.windows.without(win);
  },

  sendToBack: function(win) {
    this.remove(win);
    this.windows.unshift(win);
  },

  bringToFront: function(win) {
    this.remove(win);
    this.windows.push(win);
  },

  getPosition: function(win) {
    return this.windows.indexOf(win);
  },

  setPosition: function(win, position) {
    this.remove(win);
    this.windows.splice(position, 0, win);
  },

  getFrontWindow: function() {
    return this.windows.last();
  },

  getBackWindow: function() {
    return this.windows.first();
  },

  getPreviousWindow: function(win) {
    return (win == this.windows.first()) ? null : this.windows[this.windows.indexOf(win) - 1];
  }
});

document.whenReady(function() {
  UI.defaultWM = new UI.WindowManager();
});
