/* All classes from MooTools are combined in kw_mootools-{version}.js.
 * For better performance you can load this file from google:
 * http://ajax.googleapis.com/ajax/libs/mootools/{version}/mootools.js
 * 
 * Some extensions require MooTools More classes. These required and
 * other useful classes from MooTools More are combined in
 * kw_mootools_more-{version}.js:
 *  - More
 *  - Class.Refactor
 *  - Class.Binds
 *  - Array.Extras
 *  - Hash.Extras
 *  - String.Extras
 *  - String.QueryString
 *  - URI
 *  - Elements.From
 *  - Element.Delegation (make sure you have read the documentation -
 *    you can dramatically increase your sites efficiency!)
 *  - Element.Measure
 *  - Element.Shortcuts
 *  - Request.JSONP
 *  - Keyboard
 * 
 * Take care that these two scripts are loaded before you load
 * kw_mootools_extensions-{version}.js
 */



/* Class: Request
 * ==============
 */

/* Refactoring the Request Class to allow custom status codes
 * Requires: Class.Refactor
 */
Request = Class.refactor(Request, {
  
  /* Description of success status codes:
   * 409: Validation errors
   */
  additional_success_status_codes: [409],
  
  /* Check if the returned success code is included in our custom ones
   * or call the original function which handles all the default stuff
   */
  isSuccess: function() {
    return (this.additional_success_status_codes.contains(this.status) || this.previous());
  },
  
  /* Fire custom status event if returning status code is included in
   * our custom status codes or fire the default success event
   */
  onSuccess: function() {
    if(this.additional_success_status_codes.contains(this.status))
      this.fireEvent('complete', arguments).fireEvent('status' + this.status, arguments).callChain();
    else
      this.fireEvent('complete', arguments).fireEvent('success', arguments).callChain();
  }

});

/* Uncached Ajax-Request (adds timestamp to request)
 */
Request.Uncached = new Class({
  
  Extends: Request,
  
  options: {
    noCache: true
  },
  
  initialize: function(options) {
    this.parent(options);
  }
  
});

/* Uncached Ajax-Request which executes JS in responseText AT THE END
 * (due to errors if you reference html elements in executed JS and
 * the DOM is not yet up to date)
 */
Request.UncachedHTML = new Class({
  
  Extends: Request.HTML,
  
  options: {
    noCache: true,
    evalScripts: false
  },
  
  initialize: function(options) {
    this.parent(options);
  },
  
  onSuccess: function(responseTree, responseElements, responseHTML, responseJavaScript) {
    this.parent(responseTree, responseElements, responseHTML, responseJavaScript);
    $exec(responseJavaScript);
  }
  
});



/* Class: Selectors.Pseudo
 * =======================
 * 
 * Requires: Element.Shortcuts
 */

// for recognizing html5 data-* attributes
Selectors.RegExps.combined = (/\.([\w-]+)|\[([\w-]+)(?:([!*^$~|]?=)(["']?)([^\4]*?)\4)?\]|:([\w-]+)(?:\( ["']?(.*?)?["']?\)|$)/g);

Selectors.Pseudo.visible = function() {
  return $(this).isDisplayed();
};

Selectors.Pseudo.hidden = function() {
  return !$(this).isDisplayed();
};

Selectors.Pseudo.unchecked = function() {
  return !$(this).checked;
};

Selectors.Pseudo.writeable = function() {
  return !$(this).get('readonly');
};

/* Enhanced enabled pseudo selector which now matches also elements
 * with class 'disabled'
 */
Selectors.Pseudo.enabled = function() {
  return (!$(this).hasClass('disabled') && !$(this).get('disabled'));
};

/* Some selectors for new html5 input attributes
 */
 
Selectors.Pseudo.required = function() {
  return !!$(this).get('required');
};

Selectors.Pseudo.optional = function() {
  return !$(this).get('required');
};

Selectors.Pseudo.placeholder = function() {
  return !!$(this).get('placeholder');
};



/* Class: RegExp
 * =============
 */

RegExp.escape = function(string) {
  // .*+?|()[]{}\
  var specials = new RegExp('[.*+?|()\\[\\]{}\\\\]', 'g');
  return string.replace(specials, '\\$&');
};



/* Class: String
 * =============
 */
 
String.implement({
  
  isEmpty: function() {
    return this.length === 0;
  },
  
  /* Calls String.trim before emptiness check
   */
  isBlank: function() {
    return this.trim().isEmpty();
  },
  
  /* As of version 1.2.4 MooTools provides an own method to create
   * elements from an string (Elements.From)
   */
  toElements: function() {
    var div = document.createElement('div');
    div.innerHTML = this;
    return $$(div.childNodes);
  },
  
  /* Returns a ruby-like class formated string
   */
  classify: function(first_to_uppercase) {
    var stirng = this;
    
    if(first_to_uppercase)
      string = string.firstToUppercase();
    
    return string.replace(/_\D/g, function(match){
      return match.charAt(1).toUpperCase();
    });
  },
  
  firstToUppercase: function() {
    return this.replace(/^\D/g, function(match){
      return match.charAt(0).toUpperCase();
    });
  },
  
  chickenize: function(options) {
    options = $merge({
      delimiter: '_',
      regex: /[^\wüöä]+/g
    }, options || {});
    
    return this.replace(options.regex, options.delimiter).replace(/^_+|_+$/g, '');
  },
  
  fried_chickenize: function() {
    return this.chickenize().toLowerCase();
  },
  
  createUUID: function(options) {
	  options = options || {};
	  
    var uuid = [];
    var hex = '0123456789ABCDEF';

    for(var i = 0; i < 36; i++)
      uuid[i] = Math.floor(Math.random() * 0x10);

    // Conform to RFC-4122, section 4.4
    uuid[14] = 4;  // Set 4 high bits of time_high field to version
    uuid[19] = (uuid[19] & 0x3) | 0x8;  // Specify 2 high bits of clock sequence

    // Convert to hex chars
    for(i = 0; i <36; i++)
      uuid[i] = hex[uuid[i]];

    uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';

    return [options.prefix, uuid.join(''), options.sufix].clean().join('-');
  }
  
});
String.alias('fried_chickenize', 'kfc');



/* Class: Element
 * ==============
 */
 
var oldToggleClass = Element.prototype.toggleClass;
Element.implement({
  
  getFormElementHash: function() {
    var hash = {};
    
    this.getElements('input, select, textarea').each(function(element) {
      if(!element || ['submit', 'reset', 'file'].contains(element.get('type')) || !element.get('name') || element.get('disabled') || element.hasClass('default'))
        return;
        
      var value = (element.tagName.toLowerCase() == 'select') ? Element.getSelected(element).map(function(option) {
        return option.value;
      }) : ((element.type == 'radio' || element.type == 'checkbox') && !element.checked) ? null : element.value;
      
      $splat(value).each(function(value) {
        if(typeof value != 'undefined')
          hash[element.name] = value;
      });
    });
    
    return hash;
  },
  
  unenabled: function() {
    return ((this.hasClass('button') && this.hasClass('button_disabled')) || this.hasClass('disabled'));
  },
  
  enabled: function() {
    return !this.unenabled();
  },
  
  /* Clones the element and all its events
   * Shortcut if you work on cached elements for performance reasons
   * TODO:
   * Benchmark event deep cloning
   */
  cacheClone: function(options) {
    options = $extend({
      contents: true,
      ids: true,
      events: true
    }, (options || {}));

    var clone = this.clone(options.contents, options.ids);
    if(options.events) clone = clone.cloneEvents(this);
    if(options.events && options.contents) {
      var original_elements = this.getElements('*');
      var cloned_elements = clone.getElements('*');
      var length = original_elements.length;
      for(var i = 0; i < length; i++) {
        cloned_elements[i] = cloned_elements[i].cloneEvents(original_elements[i]);
      }
    }
    
    return clone;
  },
  
  createUUID: function(options) {
	  if(this.get('id') === null || this.get('id').isBlank())
	    this.set('id', this.createUUID(options));
	    
	  return this;
  },
  
  toggleClass: function(class_1, class_2) {
    if(!class_2) return oldToggleClass.apply(this, class_1);
    
    if(this.hasClass(class_1))
      this.removeClass(class_1).addClass(class_2);
    else if(this.hasClass(class_2))
      this.removeClass(class_2).addClass(class_1);
    else if(!this.hasClass(class_1))
      this.addClass(class_1);
    else if(!this.hasClass(class_2))
      this.addClass(class_2);
      
    return this;
  },
  
  disableInputs: function() {
    this.getElements('input, textarea, select').each(function(element) {
      element.set('disabled', true);
    });
    
    return this;
  },
  
  enableInputs: function() {
    this.getElements('input, textarea, select').each(function(element) {
      element.set('disabled', false);
    });
    
    return this;
  },
  
  addEventIfExists: function(selector, event, callback) {
    var element = this.getElement(selector);
    if(element) element.addEvent(event, callback);
    
    return this;
  },
  
  fadeAndBlind: function(direction, options) {
    options = options || {};
    direction = direction || 'in';
    
    if(direction == 'in') {
      var height = this.show().getComputedSize().height;
      this.set('morph', $merge({
        onStart: function() {
          this.setStyles({
            opacity: 0,
            height: 0
          }).show();
        }.bind(this)
      }), options).morph({
        opacity: 1,
        height: height
      });
    } else {
      this.set('morph',  $merge({
        onStart: function() {
          this.show();
        }.bind(this),
        onComplete: function() {
          this.hide();
        }.bind(this)
      }), options).morph({
        opacity: 0,
        height: 0
      });
    }
    
    return this;
  }
  
});



/* Class: Number
 * =============
 */

if($defined(MooTools.lang)) {
  MooTools.lang.set('en-US', 'Number', {
  
    format: {
      precision: 2,
      separator: '.',
      delimiter: ','
    },
    currency: {
      format: {
        unit: '$',
        format: '%u%n'
      }
    },
    percentage: {
      format: {
        unit: '%'
      }
    },
    precision: {

    },
    human: {
      format: {
        precision: 1
      },
      storage_units: {
        // Storage units output formatting.
        // %u is the storage unit, %n is the number (default: 2 MB)
        format: '%n %u',
      
        units: {
          'byte': {
            one: 'Byte',
            other: 'Bytes'
          },
          kb: 'KB',
          mb: 'MB',
          gb: 'GB',
          tb: 'TB'
        }
      }
    }

  });
}

Number.implement({

  minimum: function(min) {
    return this.toInt() < min ? min : this.toInt();
  },
  
  maximum: function(max) {
    return this.toInt() > max ? max : this.toInt();
  },
  
  numberToCurrency: function(options) {
    var defaults = MooTools.lang.get('Number', 'format');
    var current = MooTools.lang.get('Number', 'currency');
    options = $merge(defaults, current.format, options || {});
    
    try {
      return options.format.replace(/%n/g, this.numberWithPrecision(options)).replace(/%u/g, options.unit);
    } catch(error) {
      return this;
    }
  },
  
  numberToHumanSize: function(options) {
    var defaults = MooTools.lang.get('Number', 'format');
    var current = MooTools.lang.get('Number', 'human');
    options = $merge(defaults, current.format, options || {});
    
    if(this < 1024) {
      var unit = this == 1 ? current.storage_units.units['byte'].one : current.storage_units.units['byte'].other;
      return current.storage_units.format.replace('%n', this).replace('%u', unit);
    } else {
      var unit_keys = $H(current.storage_units.units).getKeys();
      var max_exp = unit_keys.length - 1;
      var exponent = (Math.log(this) / Math.log(1024)).floor();
      if(exponent > max_exp) exponent = max_exp;
      var number = this / Math.pow(1024, exponent);
      
      try {
        var escaped_separator = RegExp.escape(options.separator);
        var regex_1 = new RegExp('(\d)({escaped_separator}[1-9]*)?0+\z'.substitute({
          escaped_separator: escaped_separator
        }));
        var regex_2 = new RegExp('{escaped_separator}\z'.substitute({
          escaped_separator: escaped_separator
        }));
        
        return current.storage_units.format.replace(/%n/g, number.numberWithPrecision(options).toString().replace(regex_1, '$1$2').replace(regex_2, '')).replace(/%u/g, current.storage_units.units[unit_keys[exponent]]);
      } catch(error) {
        return this;
      }
    }
  },
  
  numberToPercentage: function(options) {
    var defaults = MooTools.lang.get('Number', 'format');
    var current = MooTools.lang.get('Number', 'percentage');
    options = $merge(defaults, current.format, options || {});
    
    try {
      return options.format.replace(/%n/g, this.numberWithPrecision(options)).replace(/%u/g, options.unit);
    } catch(error) {
      return this;
    }
  },
  
  numberWithDelimiter: function(options) {
    var defaults = MooTools.lang.get('Number', 'format');
    options = $merge(defaults, options || {});
    
    try {
      var parts = this.toString().split('.');
      parts[0] = parts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1{delimiter}'.substitute({
        delimiter: options.delimiter
      }));
      return parts.join(options.separator);
    } catch(error) {
      return this;
    }
  },
  
  numberWithPrecision: function(options) {
    var defaults = MooTools.lang.get('Number', 'format');
    var current = MooTools.lang.get('Number', 'precision');
    options = $merge(defaults, current.format, options || {});

    try {
      var rounded_number = (Math.round((this * Math.pow(10, options.precision))) / Math.pow(10, options.precision)).toString();
      numbers = rounded_number.split('.');
      return '{number}{separator}{precision}'.substitute({
        number: parseInt(numbers.first(), 10).numberWithDelimiter(options),
        separator: options.precision > 1 ? options.separator : '',
        precision: numbers.length > 1 ? numbers.last() : (function() {
          var precision = [];
          options.precision.times(function() {
            precision.push('0');
          });
          return precision.join('');
        })()
      });
    } catch(error) {
      return this;
    }
  }

});



/* Class: Date
 * ===========
 */

Date.implement({

  beginningOfDay: function() {
    return this.set('hours', 0).set('minutes', 0).set('seconds', 0).set('milliseconds', 0);
  },
    
  beginningOfWeek: function() {
    var week_day = this.get('day');
    return this.decrement('day', week_day != 0 ? week_day - 1 : 6).beginningOfDay();
  },
  
  beginningOfMonth: function() {
    return this.set('date', 1).beginningOfDay();
  },
  
  beginningOfQuarter: function() {
    return this.beginningOfMonth().set('month', [10, 7, 4, 1].detect(function(month) {
      return month <= this.get('month');
    }));
  },
  
  beginningOfYear: function() {
    return this.set('month', 1).beginningOfMonth();
  },
  
  endOfDay: function() {
    return this.set('hours', 23).set('minutes', 59).set('seconds', 59).set('milliseconds', 0);
  },
    
  endOfWeek: function() {
    var week_day = this.get('day');
    return this.increment('day', week_day != 0 ? 7 - week_day : 0).endOfDay();
  },
  
  endOfMonth: function() {
    return this.set('date', this.getLastDayOfMonth()).endOfDay();
  },
  
  endOfQuarter: function() {
    return this.beginningOfMonth().set('month', [3, 6, 9, 12].detect(function(month) {
      return month >= this.get('month');
    })).endOfMonth();
  },
  
  endOfYear: function() {
    return this.set('month', 12).endOfMonth();
  },
  
  midnight: function() {
    return this.beginningOfDay();
  }
  
});



/* Class: Array
 * ============
 */
 
Array.implement({

  first: function() {
    return this[0];
  },
  
  second: function() {
    return this[1];
  },
  
  findFirst: function(fn, bind) {
		for (var i = 0, l = this.length; i < l; i++){
			if (fn.call(bind, this[i], i, this)) return this[i];
		}
		return null;
  }
  
});
Array.alias('getLast', 'last');



/* Class: Chain
 * ============
 */
 
Chain.implement({

  chainTop: function() {
    this.$chain = Array.flatten(arguments).extend(this.$chain);
  }

});



/* Class: Form.Utilities
 * =====================
 * 
 * Requires MooTools Form Class or creates its own Namespace
 */
 
if(!$defined(Form)) var Form = {};
Form.Utilities = {
  
  /* Calling this method will cause all checkboxes to stay in its checked
   * state. If scope is undefined all checkboxes in document are affected
   */
  inputStaysChecked: function(scope) {
    var elements;

    switch($type(scope)) {
      case 'array': elements = $$(scope); break;
      case 'element': elements = scope; break;
      default: elements = ($(scope) || $(document.body)).getElements('input[type=checkbox]:stay_checked'); break;
    }
    
    elements.addEvent('click', function(event) {
      event.stop();
      
      this.set('checked', true);
    });
  }
  
};



/* Class: K
 * ====================
 */
 
if(!$defined(K)) var K = {};



/* Class: K.Placeholder
 * ====================
 * 
 * Add the behaviour of the new html5 input attribute 'placeholder'
 * for all inputs with this attribute.
 * Unless kw_modernizr-{version}.js is loaded every browser is
 * affected so you have to take care by yourself that browsers which
 * currently support this new attribute are not messed up with this
 * (When this was created only WebKit browsers are supporting this attribute).
 */
 
K.Placeholder = new Class({
  
  Implements: [Options, Events],
  
  options: {
    scope: undefined,
    placeholder_class: 'k-placeholder',
    hide_on_submit: false,
    hide_labels: false,
    skip_check: false
  },
  
	initialize: function(options) {
	  this.setOptions(options);
	  this.options.scope = $(this.options.scope || document.body);
	  
    // Catch all input elements with placeholder attribute
	  this.options.scope.getElements('input:placeholder, textarea:placeholder').each(function(input) {
	    // Return and do nothing if browser supports placeholder attribute
  	  if(!this.options.skip_check && input.tagName.toLowerCase() == 'input' && ($type(window.Modernizr) && Modernizr.input['placeholder']) && !input.get('data-k-placeholder-skip-check')) return;
  	  
      // Add focus and blur event
      input.addEvents({
        focus: this.unsetInputDefaultValue.bind(this, input),
        blur: this.setInputDefaultValue.bind(this, input)
      }).set('data-k-placeholder-default-name', input.get('name'));
      
      var id = input.get('id');
      if((this.options.hide_labels || input.get('data-k-placeholder-hide-label')) && !id.isBlank())
        this.options.scope.getElements('label[for={id}]'.substitute({
          id: id
        })).hide();
      
      this.setInputDefaultValue(input);
	  }.bind(this));

    return this;
	},

	setInputDefaultValue: function(input) {
	  var placeholder_class = input.get('data-k-placeholder-class') || this.options.placeholder_class;
	  if(input.get('value').isBlank() || input.hasClass(placeholder_class)) {
	    this.fireEvent('show_placeholder', input);
      input.addClass(placeholder_class).set('value', input.get('placeholder').replace(/\\n/g, "\n"));
      var name = input.get('name');
      if((this.options.hide_on_submit || input.get('data-k-placeholder-hide-on-submit')) && name && !name.isBlank())
        input.set('data-k-placeholder-default-name', name).set('name', '');
    }
	  return input;
	},

	unsetInputDefaultValue: function(input) {
	  var placeholder_class = input.get('data-k-placeholder-class') || this.options.placeholder_class;
	  if(input.hasClass(placeholder_class)) {
	    this.fireEvent('hide_placeholder', input);
	    input.removeClass(placeholder_class).set('value', '');
      if(this.options.hide_on_submit || input.get('data-k-placeholder-hide-on-submit')) {
        var default_name = input.get('data-k-placeholder-default-name');
        if(default_name && !default_name.isBlank()) input.set('name', default_name);
      }
    }

	  return input;
	}
  
});



/* Class: K.RequestOverlay
 * =======================
 * 
 * Another attemp to create an abstract overlay class -
 * seems to be very difficult ;)
 * Only tested in Ruflotse but i thought i let it stay in here
 */
 
K.RequestOverlay = new Class({
  
  Implements: [Options, Events],
  
  options: {
    container: 'overlay_container'
  },
  
  initialize: function(url, options) {
    this.setOptions(options);
    
    this.overlay_container = $(this.options.container);
    this.overlay = this.overlay_container.getElement('.overlay');
    
    if(this.overlay_container.getStyle('height') == '100%') {
      var height, parent = this.overlay_container.getParent();
      if(parent.get('tag') == 'body')
        height = window.getScrollSize().y;
      else
        height = this.overlay_container.getParent().getSize().y;
      this.overlay_container.setStyle('height', height);
    }
    
    if(!url || url.isBlank())
      return false;
      
    var request = new Request.UncachedHTML({
      method: 'get',
      onRequest: function() {
        this.fireEvent('request');
      }.bind(this),
      onComplete: function() {
        this.fireEvent('complete');
      }.bind(this),
      onSuccess: function(responseTree, responseElements, responseHTML, responseJavaScript) {
        this.updateOverlay(responseHTML);
        this.positionOverlay();
        this.initializeOverlay();
        this.showOverlay();
        new __defaultEvents(this.overlay_container);
        this.fireEvent('success', [this.overlay, responseTree, responseElements, responseHTML, responseJavaScript]);
      }.bind(this),
      onFailure: function() {
        this.hideOverlay();
        this.fireEvent('failure');
      }.bind(this)
    });
    
    this.overlay.removeEvents('update').addEvent('update', function() {
      this.overlay.store('request', request.send({
        url: url,
        data: this.options.data
      }));
    }.bind(this));
      
    this.showRequestOverlay();
    
    return this;
  },

  updateOverlay: function(content) {
    this.overlay.getElement('.overlay_inner_container').set('html', content);
    
    return this;
  },
  
  positionOverlay: function() {
    // this.overlay.position({
    //   position: 'centertop',
    //   edge: 'centertop'
    // });
    
    return this;
  },
  
  initializeOverlay: function(request) {
    if(!request)
      this.fireEvent('show', this.overlay);
    else
      this.fireEvent('requestShow', this.overlay);
    
    this.overlay.getElements('.cancel').addEvent('click', function(event) {
      event.stop();
      
      this.overlay.retrieve('request').cancel();
      
      if(!request)
        this.fireEvent('hide', this.overlay);
      else
        this.fireEvent('requestHide', this.overlay);
        
      this.hideOverlay();
    }.bind(this));
    
    return this;
  },
  
  showRequestOverlay: function() {
    if(!this.overlay.isDisplayed()) {
      try {
        this.overlay.getElement('h2').set('html', this.overlay.get('loading_headline'));
        this.overlay.getElements('.overlay_section').erase(this.overlay.getElement('.overlay_section').set('html', this.overlay.get('loading_message'))).hide();
        this.overlay.getElements('.button_container a.button').hide();
      } catch(error) {};
      
      this.positionOverlay();
      this.initializeOverlay(true);
      this.showOverlay();
    }
    
    this.overlay.fireEvent('update');

    return this;
  },
    
  showOverlay: function() {
    $try(function() {
      this.overlay_container.show();
    }.bind(this));
    
    // In IE6 select elements are always on top of the window
    // Lets hide them when we will show an overlay
    if(Browser.Engine.name == 'trident' && Browser.Engine.version == 4)
      $(document.body).getElements('select').hide();
    
    this.overlay.show();
    
    return this;
  },
  
  hideOverlay: function() {
    // In IE6 select elements are always on top of the window
    // We hid them when showing an overlay, no we have to show them
    if(Browser.Engine.name == 'trident' && Browser.Engine.version == 4)
      $(document.body).getElements('select').show();
      
    this.overlay.hide();
    $try(function() {
      this.overlay_container.hide();
    }.bind(this));
    
    return this;
  }
  
});



/* Class: K.PostRequest
 * ====================
 * 
 * Post data by injecting a hidden form with inputs
 * filled with the provided data
 */
 
K.PostRequest = new Class({
  
  Implements: Options,
  
  options: {
    method: 'post',
    enctype: 'multipart/form-data',
    'accept-charset': 'utf-8'
  },
  
  initialize: function(action, options) {
    this.setOptions(options);
    
    form = new Element('form', {
      action: action,
      method: this.options.method,
      enctype: this.options.enctype,
      'accept-charset': this.options['accept-charset']
    });
    
    switch($type(this.options.data)) {
      case 'array':
        this.options.data.each(function(hash) {
          if($type(object) != 'object')
            return false;
            
          return new Hash(object).each(function(value, key) {
            new Element('input', {
              type: 'hidden',
              name: key,
              value: value
            }).inject(form);
          });
        });
        break;
      case 'object':
        new Hash(this.options.data).each(function(value, key) {
          return new Element('input', {
            type: 'hidden',
            name: key,
            value: value
          }).inject(form);
        });
        break;
    };
    
    return form.inject($(document.body)).submit();
  }
  
});



/* Class: K.Slideshow
 * ==================
 * 
 * Add Slideshow for an ul
 * 
 * Options:
 * name, values, default value
 * 
 * - teaser_container: id|element, 'teaser'
 *     Add slideshow behaviour for this uls.
 *     If teaser_container is undefined, behaviour is added to all .slideshow
 * - autoplay: true|false, true
 * - view: 'stack|list|fade', 'stack'
 * - stack_offset: number in px, 0
 * - pause_delay: number in ms, 30000
 * - slide_delay: number in ms, 2000
 *     Minimum is 1000
 * - duration: number in ms, 1000
 * - stack_duration_delay: number in ms, 250
 *     Delay between item collection slides
 * - transition_in: transition|function, expo:out
 * - transition_out: transition|function, expo:out
 * - onInitializeTeaser: function(item, position)
 * - onKeyboardLeft: function(event)
 * - onKeyboardRight: function(event)
 * - onSlideComplete: function(item)
 * - getTeaserCatch: function(item), first a inside an item
 *     returns the catch for item
 */

K.Slideshow = new Class({
  
  Implements: [Options, Events],
  
  options: {
    teaser_container: 'teaser',
    autoplay: true,
    view: 'stack',
    stack_offset: 0,
    pause_delay: 30000,
    slide_delay: 5000,
    duration: 1000,
    stack_duration_delay: 250,
    transition_in: 'expo:out',
    transition_out: 'expo:out',
    onInitializeTeaser: $empty,
    onKeyboardLeft: $empty,
    onKeyboardRight: $empty,
    onSlideComplete: $empty,
    
    getTeaserCatch: function(teaser_item) {
      return teaser_item.getElement('a');
    }
  },
  
  teaser_catches: [],
  teaser_items: [],
  teaser_items_length: 0,
  teaser_item_width: 0,
  teaser_container_width: 0,
  current_teaser: 0,
  pause: 0,
  fx_stack: [],
  
  /* Internal events
   * 
   * item: event name, parameter
   * 
   * teaser_item: 'slide', teaser_item, position
   * teaser_catch: 'click', teaser_item, position
   * teaser_item: 'slide', teaser_item, position
   */
  
  initialize: function(options) {
    this.setOptions(options);
    this.options.teaser_container = $(this.options.teaser_container);
    
    if(!this.options.teaser_container) return this;
    
    this.teaser_container_width = this.options.teaser_container.getWidth();
    this.teaser_items = this.options.teaser_container.getElements('li');
    this.teaser_item_width = this.teaser_items.first().getWidth();
    this.teaser_items_length = this.teaser_items.length;
    
    // Bind events to teaser
    for(var i = 0; i < this.teaser_items_length; i++) {
      var teaser_item = this.teaser_items[i];
      if(this.options.view == 'stack') teaser_item.show();
      var teaser_catch = $lambda(this.options.getTeaserCatch)(teaser_item);
      
      teaser_item.addEvent('slide', this.slide.bindWithEvent(this, [teaser_item, i]));

      if(teaser_catch) {
        teaser_item.store('catch', teaser_catch);
        this.teaser_catches.push(teaser_catch);
      
        teaser_catch.addEvent('click', function(event, teaser_item, i) {
          event.stop();

          this.pause = this.options.pause_delay;
          this.current_teaser = i;
          teaser_item.fireEvent('slide', teaser_item, i);
        }.bindWithEvent(this, [teaser_item, i]));
      }
      
      this.fireEvent('initializeTeaser', [teaser_item.toggleClass('open', 'closed'), i]);
    };
    
    // Auto sliding
    if(this.options.autoplay) this.autoSlide.periodical([this.options.slide_delay, 1000].max(), this);
    
    // Keyboard Events
    new Keyboard({
      events: {
        'left': function(event) {
          this.pause = this.options.pause_delay;
          this.fireEvent('keyboardLeft', event);
          this.autoSlide({
            direction: -1,
            force: true
          });
        }.bind(this),
        'right': function(event) {
          this.pause = this.options.pause_delay;
          this.fireEvent('keyboardRight', event);
          this.autoSlide({
            force: true
          });
        }.bind(this)
      }
    }).activate();
    
    return this;
  },
  
  autoSlide: function(options) {
    options = $merge({
      direction: 1,
      force: false
    }, (options || {}));
    
    if(!options.force && this.pause > 0) {
      this.pause = this.pause - this.options.slide_delay;
      
      return false;
    }
    
    this.current_teaser = this.current_teaser + options.direction;
    var next_teaser = this.teaser_items[this.current_teaser];
    
    if(!next_teaser) {
      if(options.direction > 0) {
        this.current_teaser = 0;
        next_teaser = this.teaser_items.first();
      } else if(options.direction < 0) {
        this.current_teaser = this.teaser_items_length;
        next_teaser = this.teaser_items.last();
      }
    }
    
    next_teaser.fireEvent('slide', next_teaser, this.current_teaser);
    
    return this;
  },
  
  slide: function(event, teaser_item, position) {
    if(event) this.fx_stack = [];
    
    this['slide' + this.options.view.capitalize()](event, teaser_item, position);
    
    if(event) this.fireEvent('slideComplete', teaser_item);
    
    return this;
  },
  
  slideStack: function(event, teaser_item, position) {
    var tween;
    var tween_options = {
      unit: 'px',
      property: 'left',
      transition: this.options.transition_out,
      duration: this.options.duration
    };
    
    if(teaser_item.hasClass('open')) {
      var next = teaser_item.getNext('.open');
      if(next) this.slide(undefined, next, position + 1);
      if(!event) {
        this.fx_stack.push([teaser_item.set('tween', $merge(tween_options, {
          onStart: function(item) {
            item.removeClass('open');
          },
          onComplete: function(item) {
            item.addClass('closed');
          }
        })), position]);
      } else {
        this.fx_stack.each(function(array, i) {
          array[0].tween.delay(i * this.options.stack_duration_delay, array[0], this.teaser_container_width - (this.options.stack_offset * (this.teaser_items_length - array[1])));
        }.bind(this));
      }
    } else if(teaser_item.hasClass('closed')) {
      var previous = teaser_item.getPrevious('.closed');
      if(previous) this.slide(undefined, previous, position - 1);
      
      this.fx_stack.push([teaser_item.set('tween', $merge(tween_options, {
        onStart: function(item) {
          item.removeClass('closed');
        },
        onComplete: function(item) {
          item.addClass('open');
        }
      })), position]);
      
      if(event) {
        this.fx_stack.each(function(array, i) {
          array[0].tween.delay(i * this.options.stack_duration_delay, array[0], this.options.stack_offset * array[1]);
        }.bind(this));
      }
    }
    
    return this;
  },
  
  slideList: function(event, teaser_item, position) {
    this.options.teaser_container.set('tween', {
      unit: 'px',
      property: 'left',
      transition: this.options.transition_in,
      duration: this.options.duration,
      onStart: function(item) {
        this.teaser_items.removeClass('open');
      }.bind(this),
      onComplete: function(item) {
        item.addClass('open');
      }.bind(this)
    }).tween(teaser_item.getAllPrevious('li').length * -this.teaser_item_width);
    
    return this;
  },
  
  slideFade: function(event, teaser_item, position) {
    if(teaser_item.hasClass('open')) return;
    
    this.teaser_items.setStyle('z-index', 1);
    teaser_item.setStyles({
      opacity: 0,
      'z-index': 2
    }).set('tween', {
      transition: this.options.transition_in,
      duration: this.options.duration,
      onStart: function(item) {
        this.teaser_items.removeClass('open');
        item.removeClass('closed').addClass('open');
      }.bind(this)
    }).tween('opacity', 0, 1);
    
    return this;
  }
  
});



/* Class: K.TemplateRenderer
 * =========================
 * 
 * Simple class to fill HTML Templates with substitutions (compare to mootools string.substitute()). 
 * Supports some nice features like conditional subtemplates. You need and should only initialize one
 * TemplateRenderer per page (there is a caching mechanism to speed up template load).
 *
 * The Following Substitutions are recognized:
 *
 *   {var}: Will be replaced with the value from the substitution object
 *   {obj.var}: Same as before, works for subobjects of the substitution object
 *   {template:name}: Renders the named subtemplate
 *   {if:condition:template1:template2}: If the replacement {condition} is not null, template1 will be rendered, otherwise template2
 *   {if:condition:"string":"otherstring"}: If the replacement {condition} is not null, string1 will be rendered, otherwise otherstring
 *   {unless:condition:template1}: If the replacement {condition} is null, template1 will be rendered
 *   {each:var:template}: Renders template for each entry in the array var
 *   {document:obj.var_or_function}: Reads a property or calls a function relative to the document object
 *
 * Usage Example:
 *
 *   In your HTML:
 *    <div id="template-sub-template-1">
 *      sub1: {sub1.text}
 *    </div>
 *    
 *    <div id="template-sub-template-2">
 *      sub2: {sub2.text}
 *    </div>
 *    
 *    <ul id="template-main">
 *      <li>
 *        {template:sub-template-1}
 *        {if:sub2:sub-template-2}
 *        {text}
 *      </li>
 *    </ul>
 * 
 *  In your JS
 * 
 *    var renderer = new TemplateRenderer();
 *    alert(renderer.render('main', {text: 'hello', sub1: {text: 'hello from sub1'}, sub2: {text: 'hello from sub2'}}}))
 * 
 */
 
K.TemplateRenderer = new Class({
  
  Implements: Options,
  
  options: {
    return_element: false
  },
  
  initialize: function(options) {
    this.setOptions(options);
    if(!document.__k_tempplate_renderer) document.__k_tempplate_renderer = new Hash();
    
    return this;
  },
  
  templateSubstitute: function(template, object){
		return unescape(template).replace((/\\?\{([^{}]+)\}/g), function(match, name) {
			if (match.charAt(0) == '\\') return match.slice(1);
			
			var fparts = name.split(':');
			if(fparts.length == 1) {
			  return this.computeTemplateSubstitution(name, object);
			} else {
			  var fcall = fparts.shift();
			  switch(fcall) {
			    case 'template':
			      return this.render(fparts[0], object);
			    case 'if':
			      return this.computeTemplateSubstitution(fparts[0], object) ? this.render(fparts[1], object) : this.render(fparts[2], object);
			    case 'unless':
			      return this.computeTemplateSubstitution(fparts[0], object) ? '' : this.render(fparts[1], object);
			    case 'each':
			      var arr = this.computeTemplateSubstitution(fparts[0], object);
			      var retval = '';
			      if(arr) {
			        for(var i = 0; i < arr.length; i++) {
			          retval += this.render(fparts[1], arr[i]);
			        }
			      }
			      return retval;
			    case 'apply':
			      var thisobj = object;
			      if(fparts[1] != undefined) {
			         thisobj = this.computeTemplateSubstitution(fparts[1], object);
		        }
			      return this.computeTemplateSubstitution(fparts[0], window, thisobj);
			    case 'document':
		      case 'document-var':
		      case 'document-call':
		        var dobj = this.computeTemplateSubstitution(fparts[0], document);
		        if((typeof dobj) == 'function') {
              return dobj.apply(object);
            } else {
              return dobj;
            }
			    default:
			      return 'unknown fcall: ' + fcall;
			  }
			  
			}
		}.bind(this));
	},

  computeTemplateSubstitution: function(name, object, apply_object) {
    var cObject = object;
    
    if(cObject != undefined) {
      var parts = name.split('.');
      var length = parts.length;
      
      for(i = 0; i < length; i++) {
        var cObjectPart;
        if(parts[i] == 'this') {
          cObjectPart = cObject;
        } else {
          cObjectPart = cObject[parts[i]];
        }
                
        if((typeof cObjectPart) == 'function') {
          if(apply_object === undefined || apply_object === null || apply_object === false) {
            cObject = cObjectPart.apply(object);
          } else {
            cObject = cObjectPart.apply(apply_object);
          }
        } else if(cObjectPart != undefined) {        
          cObject = cObjectPart;
        } else {
          cObject = '';
          break;
        }
      }
      
      return cObject;
    } else {
      return '';
    }
  },
  
  render: function(name, object) {
    if(name) {
      if((typeof name) == 'function') {
        name = name.apply(object);
      } 
      
      var matchobj;
      if(matchobj = name.match(/"([^"]*)"|'([^']*)'/)) {
        return (matchobj[1] || '').isBlank() ? matchobj[2] : matchobj[1];
      } else if(matchobj = name.match(/@([^@]*)/)) {
        return this.render(this.computeTemplateSubstitution(matchobj[1], object), object);
      } else {
        var idname = 'template-' + name.hyphenate().replace(/::/g, '-');

        if(!document.__k_tempplate_renderer.has(idname)) {
          var templateElement = $(idname);
          if(templateElement) {
            document.__k_tempplate_renderer.set(idname, templateElement.innerHTML);
            templateElement.destroy();
          } else {
            return '';
          }
        }
      
        var string = this.templateSubstitute(document.__k_tempplate_renderer.get(idname), object);

        return this.options.return_element ? Elements.from(string) : string;
      }
    } else {
      return '';
    }
  }
  
});




/* Class: K.DatePeriodSelector
 * ====================
 * 
 * Shows a date period selector (google analytics style) for input
 * fields with type "date_period"
 */

K.DatePeriodSelector = new Class({
  
  Implements: [Options, Events],
  
  options: {
    start_month: false,
    start_date: false,
    end_date: false,
    template_layer: '',
    template_day: '',
    glue: ' – ',
    format: '%x',
    onShow: function(layer) {
      layer.show();
    },
    onHide: function(layer) {
      layer.hide();
    },
    onSubmit: function(form) {
      form.submit();
    }
  },
  
  initialize: function(options) {
    this.setOptions(options);
    this.options.template_layer = Elements.from(this.options.template_layer)[0];

    $$('input[data-special_type=date_period]').each(function(input) {
      var layer = this.options.template_layer.clone();
      input.store('k-date-period-selector-layer', layer);
      layer.addClass('date_period_layer').addEvents({
        'click:relay(a.back)': function(event) {
          event.stop();
          
          this.initLayerWithDate(input, -1);
        }.bind(this),
        'click:relay(a.forward)': function(event) {
          event.stop();
          
          this.initLayerWithDate(input, 1);
        }.bind(this),
        'click:relay(a.today)': function(event) {
          event.stop();
          
          this.initLayerWithDate(input);
        }.bind(this),
        'click:relay(li.month a)': function(event) {
          event.stop();
          
          this.setDateFrame($(event.target), layer);
        }.bind(this),
        'click:relay(a.button)': function(event) {
          event.stop();

          this.fireEvent('submit', $(event.target).getParent('form'));
        }.bind(this),
        'click:relay(a.close)': function(event) {
          event.stop();
          
          this.fireEvent('hide', layer);
        }.bind(this)
      }).inject(input, 'after');
    }.bind(this));

    $$('input[data-special_type=date_period]').addEvents({
      'click': function(event) {
        event.stop();

        this.fireEvent('show', this.initLayerWithDate($(event.target)));
      }.bind(this)
    });

    return this;
  },
  
  initLayerWithDate: function(input, start_offset) {
    var layer = input.getNext('.date_period_layer');
    var value = input.get('value');
    var dates = value.split(this.options.glue);
    var date = new Date();
    this.options.start_date = dates[0] ? date.clone().parse(dates[0]) : date.clone();
    this.options.end_date = dates[1] ? date.clone().parse(dates[1]) : date.clone();

    switch ($type(start_offset)){
      case 'date':
        this.options.start_month = start_offset.clone().beginningOfMonth();
        break;
      case 'string':
        this.options.start_month = date.clone().parse(start_offset);
        break;
      case 'number':
        this.options.start_month = (this.options.start_month || this.options.start_date.beginningOfMonth()).increment('month', start_offset);
        break;
      default:
        this.options.start_month = new Date().beginningOfMonth();
    }
    
    layer.getElements('ul').each(function(list, i) {
      var current_month = this.options.start_month.clone().increment('month', i);
      var start_week_day;
      var days = 1;
      
      list.getPrevious('strong').set('text', current_month.format('%B %Y'));
      var new_list = document.createDocumentFragment();
      // Last days of previous month
      var start_week = current_month.clone().beginningOfWeek();
      var current_day = start_week.clone().decrement('day', 1);
      start_week_day = start_week.get('date');
      if(start_week_day > 1) {
        var previous_month_days = current_month.clone().decrement('month', 1).getLastDayOfMonth();
        while(start_week_day <= previous_month_days) {
          this.addDayToList(new_list, current_day.increment('day', 1), start_week_day, 'previous_month');
          
          start_week_day++;
          days++;
        }
      }
      
      // Days of current month
      start_week_day = 1;
      var sum_of_days = current_month.getLastDayOfMonth();
      while(start_week_day <= sum_of_days) {
        this.addDayToList(new_list, current_day.increment('day', 1), start_week_day, 'current_month');
        
        start_week_day++;
        days++;
      }
      
      // First days of next month
      var end_week = current_month.clone().endOfMonth().endOfWeek();
      var end_week_day = end_week.get('date');
      start_week_day = 1;
      if(end_week_day < 7) {
        while(start_week_day <= end_week_day) {
          this.addDayToList(new_list, current_day.increment('day', 1), start_week_day, 'next_month');
          
          start_week_day++;
          days++;
        }
      }
      
      while(days <= 42) {
        this.addDayToList(new_list, current_day.increment('day', 1), start_week_day, 'next_month');
        
        start_week_day++;
        days++;
      }
      
      list.getElements('li.month').destroy();
      list.appendChild(new_list);
    }.bind(this));
    
    return layer;
  },
  
  addDayToList: function(list, date, day, classes) {
    if(date.diff(this.options.start_date) <= 0 && date.diff(this.options.end_date) >= 0)
      classes = [classes, 'active'];
    
    return list.appendChild(Elements.from(this.options.template_day.substitute({
      klass: 'month ' + ($type(classes) == 'array' ? classes.flatten().join(' ') : classes),
      day: day
    }))[0].set('date', date.format('%x')));
  },
  
  setDateFrame: function(day, layer) {
    day = day.getParent('li');
    var date = new Date().parse(day.get('date'));
    var input = day.getParent('.date_period_layer').getPrevious('input[data-special_type=date_period]');

    if(this.options.start_date && (this.options.end_date && !this.skip_end_date)) {
      layer.getElements('li.active').removeClass('active');
      this.options.end_date = false;
      this.options.start_date = date;
      this.skip_end_date = true;
      input.set('value', [date.format(this.options.format), date.format(this.options.format)].join(this.options.glue));
      day.addClass('active');
    } else {
      if(this.options.start_date.diff(date) >= 0) {
        this.options.end_date = date;
      } else {
        this.options.end_date = this.options.start_date;
        this.options.start_date = date;
      }
      this.skip_end_date = false;
      input.set('value', [this.options.start_date.format(this.options.format), this.options.end_date.format(this.options.format)].join(this.options.glue));
      this.initLayerWithDate(input, this.options.start_date);
    }

    return this;
  }
  
});



/* Class: K.InfoBubble
 * ===================
 */
 
Selectors.Pseudo.k_infobubble_headline = function() {
  return !!$(this).get('data-k-infobubble-headline');
};

Selectors.Pseudo.k_infobubble_content = function() {
  return !!$(this).get('data-k-infobubble-content');
};

K.InfoBubble = new Class({
  
  Implements: Options,
  
  renderer: undefined,
  
  options: {
    scope: undefined
  },
  
  initialize: function(options) {
    this.setOptions(options);
    this.options.scope = $(this.options.scope || document.body);
    
    this.initializeRenderer();
    
    this.options.scope.getElements('*:k_infobubble_content, *:k_infobubble_headline').addEvents({
      'mouseenter': function(event) {
        this.showInfoBubble.delay(250, this, event);
      }.bind(this),
      'mouseleave': function(event) {
        this.hideInfoBubble.delay(300, this, event);
      }.bind(this)
    });
    
    return this;
  },
  
  initializeRenderer: function() {
    this.renderer = this.renderer || new K.TemplateRenderer({
      return_element: true
    });
    
    return this.renderer;
  },
  
  initializeInfoBubble: function(element, x, y) {
    var bubble = element.retrieve('k-infobubble');
    
    if(!bubble) {
      bubble = this.renderer.render('k-infobubble', {
        headline: element.get('data-k-infobubble-headline'),
        content: element.get('data-k-infobubble-content')
      })[0];
      
      element.store('k-infobubble', bubble.inject(document.body, 'bottom'));
      
      var offset = parseInt((element.get('data-k-infobubble-offset') || 0), 10);
      var element_width = element.getWidth();
      var bubble_width = bubble.show().getWidth();
      var left = y + element_width + offset;
      
      bubble.hide();
      
      if(left + bubble_width >= $(document.body).getWidth())
        left = y - (bubble_width + offset);
      
      bubble.setStyles({
        top: x,
        left: left
      });
      
      return bubble;
    } else {
      return bubble;
    }
  },
  
  showInfoBubble: function(event) {
    var element = $(event.target);
    if(element.get('data-k-infobubble-headline') || element.get('data-k-infobubble-content')) {
      var coordinates = element.getCoordinates();
      var info_bubble = this.initializeInfoBubble(element, coordinates.top, coordinates.left);
    }
    
    if(info_bubble) info_bubble.show();
    
    return this;
  },
  
  hideInfoBubble: function(event) {
    var element = $(event.target).retrieve('k-infobubble');
    if(element) element.hide();
    
    return this;
  }
  
});



/* Class: K.UIDCreator
 * ===================
 */
K.UIDCreator = new Class({
  
  getNextUuid: function() {
    document._uid_creator_current_id = (document._uid_creator_current_id || 0) + 1;
    return document._uid_creator_current_id;
  },
  
  getNextUid: function() {
    this._uid_creator_current_id = (this._uid_creator_current_id || 0) + 1;
    return this._uid_creator_current_id;
  }
  
});



/* Class: K.RestfulLinks
 * =====================
 * 
 * Elements with data-rest-method and href or data-rest-url set
 * will be submitting an +data-rest-method+ request when clicking
 * 
 * Options:
 * name: values, default value
 * 
 * - scope: id|element, undefined
 * - methods: array, ['post', 'put', 'delete']
 * - token: string, ''
 */
 
K.RestfulLinks = new Class({
  
  Implements: [Options],
  
  options: {
    scope: undefined,
    methods: ['post', 'put', 'delete'],
    token: ''
  },
  
  initialize: function(options) {
    this.setOptions(options);
    this.scope = $(this.scope || document.body);
    
    this.scope.addEvents({
      'click:relay(*:data-rest-method)': function(event, element) {
        event.stop();
        
        var url = element.get('data-rest-url') || element.get('href');
        var method = element.get('data-rest-method');

        if(!url || !this.options.methods.contains(method)) return;

        var form = new Element('form', {
          action: url,
          method: 'post',
          styles: {
            display: 'none'
          }
        });
        
        new Element('input', {
          type: 'hidden',
          name: 'authenticity_token',
          value: this.options.token
        }).inject(form);
        
        if(method != 'post') 
          new Element('input', {
            type: 'hidden',
            name: '_method',
            value: method
          }).inject(form);
          
        form.inject(element, 'before').submit();
      }.bind(this)
    });
    
    return this;
  }
  
});



/* Class: K.ElementMessage
 * =======================
 * 
 * Shows a message for an element.
 * 
 * Options:
 * name: values, default value
 * 
 * - message_container: string|element, label
 * - attributes: object, {}
 * - position: 'top|bottom|before|after', 'before'
 * - class_name: string, ''
 * - destroy_in_scope: id|element, undefined
 */
 
K.ElementMessage = new Class({
  
  Implements: [Options],
  
  options: {
    message_container: undefined,
    attributes: {},
    position: 'before',
    class_name: '',
    destroy_in_scope: undefined
  },
  
  initialize: function(message, element, options) {
    this.setOptions(options);
    
    if(this.options.message_container === undefined)
      this.options.message_container = new Element('label', {
        'for': element.get('id'),
        text: '{message}'
      });
    
    if($type(this.options.message_container) == 'string')
      this.options.message_container = Elements.from(this.options.message_container).first();
    
    var current_message = element.retrieve('k-element-message');
    if(current_message)
      current_message.destroy();
      
    element.store('k-element-message', this.options.message_container);
    
    if(this.options.destroy_in_scope) $(this.options.destroy_in_scope).getElements('.k-element-message').destroy();
    
    this.options.message_container.set($merge({
      text: this.options.message_container.get('text').substitute({
        message: message
      })
    }, this.options.attributes)).addClass('k-element-message').addClass(this.options.class_name).addClass(this.options.position).hide().inject(element, this.options.position);
    
    return this.options.message_container;
  }
  
});





