var Overlay = new Class({
  Implements: [Events, Options],
  
  options: {
    'autoFit'           : true, // boolean - whether or not the overlay pane will automatically resize to fit in the window (adds scroll bars)
    'autoFitPadding'    : '25px', // pixels - how much padding you want for autoFit (between overlay and window border)
    'backgroundClass'   : 'overlayBackground', // css class - to be used for the overlay background
    'backgroundOpacity' : 0.35, // decimal - the end opacity of the background
    'clickEvent'        : null, // click event - if passed the overlay will pop out from the mouse click (comes from center of screen if left empty)
    'closeLinkClass'    : 'overlayCloseLink', // css class - to be used for the overlay close button
    'closeLinks'        : null, // css selector - any element on the page with this selector will close the overlay when clicked
    'closeText'         : 'Close X', // text - the text that appears as the default close link
    'hasCloseLink'      : true, // boolean - whether or not to show the default close link (upper right)
    'isEscapable'       : true, // boolean - whether or not pressing escape key will close the overlay
    'onChange'          : $empty, // function - executed when the overlay changes elements
    'onClose'           : $empty, // function - executed when the overlay closes
    'onRender'          : $empty, // function - executed when the overlay renders from not being open
    'openDuration'      : 600, // milliseconds - the time in takes to finish each transition
    'paneClass'         : 'overlayPane', // css class - to be used for the overlay pane
    'panePosition'      : { left: null, top: null }, // object - cooridnates for the overlay pane (centers on screen if left empty)
    'startHeight'       : '50px', // pixels - how tall the pane spawns at before it gets increased to its proper size
    'startWidth'        : '50px', // pixels - how wide the pane spawns at before it gets increased to its proper size
    'transition'        : 'circ:out' // mootools transition - the algorithm to use when making any transition (find them at mootools.net)
  },
  
  states: {
    'isAnimating': false,
    'isAutoFit': false,
    'isRendered': false
  },
    
  initialize: function (options) {
    // store a copy of the options we instantiated with
    this.instanceOptions = new Hash(this.options).extend(options || {});
    
    // set config to be our options
    this.config = this.instanceOptions;
    
    // set our options according to passed in options
    this.setOptions(this.config.getClean());
    
    // re-position after window resize
    window.addEvent('resize', this._position.bind(this));
    
    // re-position after window scroll (IE6 only)
    if (Browser.Engine.trident4) {
      window.addEvent('scroll', this._position.bind(this));
    }
  },
  
  // change the pane content to a new element
  _change: function (el) {
    this._empty();
    
    // move the element offscreen for measurement
    el.setStyles({
      'left'      : '-9999px',
      'position'  : 'absolute',
      'top'       : '-9999px'
    });
    el.removeClass('hide');
    
    // measurement fix
    el.inject(document.body);
    
    // calculate the new pane dimensions for transition
    var newWidth = el.getSize().x;
    var newHeight = el.getSize().y;
    
    // calculate the new pane position for transition (IE6 does it differently)
    var newLeft = (window.getSize().x / 2) - (newWidth / 2);
    var newTop = (window.getSize().y / 2) - (newHeight / 2);
    var newTopFirstHalf = (window.getSize().y / 2) - (this.pane.getSize().y / 2);
    
    if (this.config.panePosition.left) newLeft = this.config.panePosition.left;
    if (this.config.panePosition.top) {
      newTopFirstHalf = this.config.panePosition.top.toInt();
      newTop = this.config.panePosition.top.toInt();
    }
    
    if (Browser.Engine.trident4) {
      newLeft += window.getScroll().x;
      newTop += window.getScroll().y;
      newTopFirstHalf += window.getScroll().y;
    }
    
    if (this.config.transition) {
      // start transition
      this.states.isAnimating = true;
            
      // prepare pane transition
      this.pane.set('morph', {
        'duration'        : this.config.openDuration / 2,
        'link'            : 'chain',
        'onChainComplete' : (function () { // end animation state and insert the element when finished
          this.states.isAnimating = false;
          this._insert(el);
        }).bind(this),
        'transition'      : this.config.transition
      });
      
      // begin pane transition
      this.pane.morph({ // width expand
        'left'  : newLeft,
        'top'   : newTopFirstHalf,
        'width' : newWidth
      }).morph({ // height expand
        'height'  : newHeight,
        'top'     : newTop
      });
    } else {
      // show the pane with no transition
      this.pane.setStyles({
        'height'  : newHeight,
        'left'    : newLeft,
        'top'     : newTop,
        'width'   : newWidth
      });
      
      this._insert(el);
    }
  },
  
  // closes the overlay
  close: function () {
    // return if the overlay pane is in the middle of an animation
    if (this.states.isAnimating) return;
  
    // mark as not rendered
    this.states.isRendered = false;
    
    // run the onClose function
    this.fireEvent('close');
    
    // put the element back in the body before we remove the overlay components
    this._empty();
    
    // remove the overlay components from view before destroying them (Opera fix)
    this.pane.setStyle('display', 'none');
    this.background.setStyle('display', 'none');
    
    return false;
  },
  
  // close function for passed in close links
  _closeLinksClose: function () {
    Overlay.instance.close();
  },
  
  // dumps the content of the pane to the body
  _empty: function () {
    // return if there is nothing to empty
    if (!this.el) return;
    
    this.el.addClass('hide');
    this.el.inject(document.body);
    this.el = null;
    
    this.pane.set('html', '');
  },
  
  // press escape to close function
  _escapeClose: function () {
    var ev = arguments[0];
    if (ev.key == 'esc') Overlay.instance.close();
  },
  
  // inserts the element into the pane
  _insert: function (el) {
    // if this is our first insert, fire the onRender function
    if (!this.states.isRendered) {
      // mark as rendered
      this.states.isRendered = true;
  
      // run the onRender function
      this.fireEvent('render');
    }
  
    // cache the element to be overlayed
    this.el = el;
    
    // build stock close link
    if (this.config.hasCloseLink) {
      var closeLink = new Element('a', {
        'class'   : this.config.closeLinkClass,
        'href'    : '#',
        'onclick' : 'return false;',
        'text'    : this.config.closeText
      });
      closeLink.addEvent('click', this.close.bind(this));
      closeLink.inject(this.pane);
    }

    // wire up the close links from passed in close class
    if (this.config.closeLinks) {
      $$(this.config.closeLinks).removeEvent('click', this._closeLinksClose);
      $$(this.config.closeLinks).addEvent('click', this._closeLinksClose);
    }
    
    this.el.setStyles({
      'left'      : 'auto',
      'position'  : 'relative',
      'top'       : 'auto'
    });
    
    this.el.inject(this.pane);
    
    // cache the pane height (used for pane resizing later)
    this.paneHeight = this.pane.getSize().y.toInt();
        
    // position everything again now that dimensions have changed
    this._position();
    
    // run the onChange function
    this.fireEvent('change');
  },
  
  // positions the overlay components properly
  _position: function (component) {
    if (!this.background || !this.pane) return;
    
    // ensure the overlay pane isn't too tall for the window. if it is, shrink it down and add a scrollbar (autoFit)
    if (this.config.autoFit) {
      
      // is there any heightOffset
      var heightOffset = (this.config.panePosition.top) ? this.config.panePosition.top.toInt() : 0;
      
      // engage autoFit when the pane is halfway through the autoFitPadding (this gives a more noticeable autoFit change)
      var heightThreshold = window.getSize().y - this.config.autoFitPadding.toInt();
      
      if (this.paneHeight > heightThreshold) {
        this.states.isAutoFit = true;
        
        // give padding for autoFit
        var newHeight = window.getSize().y - (this.config.autoFitPadding.toInt() * 2);
        
        this.pane.setStyles({
          'height'    : newHeight,
          'overflowX' : 'hidden',
          'overflowY' : 'scroll'
        });
      } else {
        this.states.isAutoFit = false;
        
        // if panePosition.top forces the overlay to clip then center overlay and trigger the autoFit flag
        // this case doesn't need scrollbars but it is still autoFit because it ignores panePosition.top to stay visible
        if ((this.paneHeight + heightOffset) > heightThreshold) {
          this.states.isAutoFit = true;
        }
        
        // in IE if you are trying to put an iframe in an overlay the iframe doesnt show
        // for some CRAZY reason this only happens when the paneHeight is exactly equal to the overlay content height
        // so we add 1 to the paneHeight to get rid of the IE bug
        this.pane.setStyles({
          'height'    : this.paneHeight + 1,
          'overflowX' : 'hidden',
          'overflowY' : 'hidden'
        });
      }
    }
    
    // ensure the background covers the whole document and center the pane unless a position has been specified (IE6 does it differently)
    if (Browser.Engine.trident4) {
      this.background.setStyles({
        'height'    : window.getSize().y,
        'left'      : window.getScroll().x,
        'position'  : 'absolute',
        'top'       : window.getScroll().y,
        'width'     : window.getSize().x
      });
      
      var newLeft = ((window.getSize().x / 2) - (this.pane.getSize().x / 2)) + window.getScroll().x;
      var newTop = ((window.getSize().y / 2) - (this.pane.getSize().y / 2)) + window.getScroll().y;
      
      if (!this.states.isAutoFit && this.config.panePosition.left)
        newLeft = this.config.panePosition.left.toInt() + window.getScroll().x;
      if (!this.states.isAutoFit && this.config.panePosition.top)
        newTop = this.config.panePosition.top.toInt() + window.getScroll().y;
      
      this.pane.setStyles({
        'left'      : newLeft,
        'position'  : 'absolute',
        'top'       : newTop
      });
    } else {
      this.background.setStyles({
        'height'    : window.getSize().y,
        'left'      : 0,
        'top'       : 0,
        'width'     : window.getSize().x
      });
      
      if (component != 'background') {
        var newLeft = (window.getSize().x / 2) - (this.pane.getSize().x / 2);
        var newTop = (window.getSize().y / 2) - (this.pane.getSize().y / 2);
        
        if (!this.states.isAutoFit && this.config.panePosition.left)
          newLeft = this.config.panePosition.left;
        if (!this.states.isAutoFit && this.config.panePosition.top)
          newTop = this.config.panePosition.top;
        
        this.pane.setStyles({
          'left'      : newLeft,
          'top'       : newTop
        });
      }
    }
  },
  
  // function that renders an element in an overlay
  // el - element to be placed inside the overlay
  // options - bag of options touse for this render only (reverts back to instantiated options on next render)
  render: function (el, options) {
    // return if a different overlay is already rendered
    if (Overlay.instance && Overlay.instance != this && Overlay.instance.states.isRendered) return;
    
    // cache the latest instance
    Overlay.instance = this;
  
    // cast the element into a mootools element
    el = $(el);
    
    // make sure to remove any events as they will be re-added when we recreate our config
    this.removeEvents();
    
    // copy in the render options for this render only
    this.config = new Hash(this.instanceOptions).extend(options || {});
    this.setOptions(this.config.getClean());
    
    // wire up escape key if options says escape key is ok
    if (this.config.isEscapable) {
      document.addEvent('keydown', this._escapeClose);
    } else {
      document.removeEvent('keydown', this._escapeClose);
    }
    
    if (!this.states.isRendered) {
      // cast the click event object into a mootools object
      if (this.config.clickEvent) this.config.clickEvent = new Event(this.config.clickEvent);
      
      // calculate where to position the pane for the first time
      var newLeft = (window.getSize().x / 2) + window.getScroll().x;
      var newTop = (window.getSize().y / 2) + window.getScroll().y;
      
      // calculate where to position the pane if an event is passed in
      if (this.config.clickEvent) {
        newLeft = this.config.clickEvent.client.x;
        newTop = this.config.clickEvent.client.y;
      }
      
      // deduct half the height and width of the start dimensions from the pane position to keep it centered
      newLeft -= this.config.startWidth.toInt() / 2;
      newTop -= this.config.startHeight.toInt() / 2;
      
      // remove old overlay components before we create (Opera fix)
      if (this.pane) this.pane.destroy();
      if (this.background) this.background.destroy();
      
      // create the overlay background
      this.background = new Element('div', {
        'class'   : this.config.backgroundClass,
        'styles'  : {
          'backgroundColor' : '#000',
          'opacity'   : 0,
          'position'  : 'fixed'
        }
      });
            
      // create the overlay pane and show it on the screen
      this.pane = new Element('div', {
        'class'   : this.config.paneClass,
        'styles'  : {
          'left'      : newLeft,
          'height'    : this.config.startHeight,
          'position'  : 'fixed',
          'top'       : newTop,
          'width'     : this.config.startWidth
        }
      });
      
      // set our autoFit paneHeight variable to the config startHeight on each new render call
      this.paneHeight = this.config.startHeight.toInt();
     
      // insert the pane and background into the body
      this.background.inject(document.body);
      this.pane.inject(document.body);
            
      // position the background for the first time
      this._position('background');
      
      // show the background
      if (this.config.transition) { // fade the overlay background in
        this.background.set('morph', {
          'duration'    : this.config.openDuration,
          'transition'  : this.config.transition
        });
        this.background.morph({ 'opacity': this.config.backgroundOpacity });
      } else {
        // show the overlay background with no transition
        this.background.setStyle('opacity', this.config.backgroundOpacity);
      }
    }
    
    // change to the element
    this._change(el);
  }
});