/**
 * Window for cb-shop in a layer.
 */

// closure for jQuery shorthand notation
var ShopWindow = (function ($) {
   "use strict";

   return $.CbWidget.window.extend({
      constructor : function(url) {
         var self = this;

         this.extra_window = $.browser.safari || $.browser.msie;

         this.base({}, {
            height:     self.extra_window ? 0 : 630,
            width:      self.extra_window ? 0 : 650,
            modal:      !self.extra_window,
            showShadow: false
         });

         this.autocenterX();
         this.moveToY(20);
         this.url = url;
      },

      handleLoad : function() {
         var self = this;

         if (self.extra_window) {
            /* only firefox, and maybe chrome (TODO) are stupid enough to accept cookies from sites in iframes by default. */
            window.open(self.url, 'shop', 'dependent=no,height=630,hotkeys=no,location=no,menubar=no,scrollbars=yes,status=no,toolbar=no,width=650');
         } else {
            self.element().append($(document.createElement('img'))
                  .attr('src', '/module/lib/framework/pics/delete.gif')
                  .addClass('__CbUiCloseButton')
                  .click(function() {
                     self.close();
                  }));
            self.element().append($(document.createElement('iframe'))
                  .attr('src', self.url).width('100%').height('100%'));
         }
      }
   });
}(jQuery));



/**
 * Preconfigured CbPlayerWindow-widget for use with the onlinefilm website.
 */

// closure for jQuery shorthand notation
var OfiPlayerWindow = (function ($) {
   "use strict";

   return CbSimplePlayerWindow.extend({
      constructor: function (element, options) {
         this.base(element, $.extend({
            id_type:           'td',
            config:            'ofi',
            layerColor:        '#223A6E',
            width:             960,
            height:            540,
            na_icon:           DOCUMENT_ROOT+'/media/png/icon_na.png',
            no_icon:           DOCUMENT_ROOT+'/media/png/icon_no.png',
            play_icon:         (options.player=='none'?null:$(document.createElement('a')).attr('href', '#').addClass('play-button icon-play'))
         }, options));
      }
   });
}(jQuery));



// DOM-ready event
jQuery(function ($) {
   "use strict";

   var
      /**
       * Determines the entry point URL for standard ajax requests.
       *
       * @global DOCUMENT_ROOT (relative) root directory of the project
       * @param method (optional) request action
       * @return Entry point URL
       */
      getAjaxEntryPoint = function (method) {
         var entryPoint = DOCUMENT_ROOT+'/ajax.php';

         if (method) {
            entryPoint += '?method='+encodeURIComponent(method);
         }

         return entryPoint;
      },

      /**
       * Determines the format of the incoming ajax data.
       *
       * @return Format
       */
      getAjaxFormat = function () {
         return 'json';
      },

      /**
       * Creates a function that can be passed as a jQuery ajax callback to
       * perform an implicit error-check on the response data and handle the
       * data encapsulation. This should be used for every standard ajax
       * request.
       *
       * @param successCallback Function that is invoked if the ajax request
       *    succeeded. It gets passed the ajax response data.
       * @param errorCallback Function that is invoked if the ajax request
       *    failed. It gets passed the ajax response data (error message) if
       *    existant, null otherwise.
       * @return Function that is able to handle the ajax communication format
       *    and check for errors beforehand.
       */
      getAjaxResponseHandler = function (successCallback, errorCallback) {
         successCallback = successCallback || $.noop;
         errorCallback = errorCallback || $.noop;

         return function (data) {
            if (data !== null) {
               if (data.status === 'success') {
                  successCallback.call(this, data.data || null);
               } else {
                  errorCallback.call(this, data.data || null);
               }
            } else {
               errorCallback.call(this, null);
            }
         };
      },

      /**
       * Wrapper for basic ajax requests that automatically caches everything in
       * JavaScript, so that all of the data are "loaded" in a blink after the
       * first request.
       *
       * @param <string> method entry point method (see getAjaxEntryPoint())
       * @param <object> params parameters for the request; gets replaced with
       *    an empty object if it is considered a falsy value
       * @param <function> callback function that is invoked after a successful
       *    request; gets passed the request result
       * @param <object> options (optional) list of options with less often used
       *    configuration; possible options are:
       *
       *       cache             boolean value that indicates wether the request
       *                         should be auto-cached; discards any existing
       *                         cache if set to false; defaults to true
       *       httpMethod        jQuery ajax method name; possible values are
       *                         'get' and 'post'; defaults to 'get'
       *       failureCallback   function that is invoked if a request fails for
       *                         any reason; gets passed the re
       */
      fetch = (function () {
         var cache = {};

         return function (method, params, callback, options) {
            var serializedParams,

               // Ensures that the callback is always invoked in the same
               // manner.
               invokeCallback = function () {
                  callback.call(this, cache[method][serializedParams]);
               };

            // Merge with defaults.
            options = $.extend({
               cache:           true,
               httpMethod:      'get',
               failureCallback: $.noop
            }, options || {});

            // Initialize defaults.
            params = params || {};
            cache[method] = cache[method] || {};
            serializedParams = $.param(params);
            cache[method][serializedParams] = !options.cache || !cache[method][serializedParams] ? undefined : cache[method][serializedParams];

            // Do we need to fetch anything?
            if (cache[method][serializedParams] !== undefined) {
               // We already have what we need. Pass the result back instantly.
               invokeCallback();
            } else {
               // Nothing cached. We need to reach the server for these data.
               if ($.isFunction($[options.httpMethod])) {
                  $[options.httpMethod](getAjaxEntryPoint(method), params, getAjaxResponseHandler(function (response) {
                     // Success! Store result in cache and invoke callback.
                     cache[method][serializedParams] = response;
                     invokeCallback();
                  }, function (response) {
                     // Failure! Discard result and notify the user.
                     cache[method][serializedParams] = undefined;
                     options.failureCallback.call(this, response);
                  }), getAjaxFormat());
               } else {
                  throw "The HTTP request method needs to be supported by jQuery. `"+options.httpMethod+"' is not supported. Try `post' or `get'.";
               }
            }
         };
      }()),

      /**
       * Invokes a function that can be used with #!<command>/<arg1>/<arg2>,
       * etc. manually. The first parameter is the command name. All following
       * ones are passed as arguments to the called function.
       */
      invokeHashCommand = (function () {
         var
            // Stores all commands that can be used with #!.
            commands = {
               /**
                * Toggles the network box on top of the page. This function
                * assumes that the box is hidden by default and uses a negative
                * top margin to move itself out of the way.
                *
                * @param duration (optional) time that the animation takes (in
                *    milliseconds); defaults to one second
                * @param open (optional) wether to open or to close the box
                */
               explore: (function () {
                  var element = $('#network'),
                     collapsedOffset = element.css('margin-top'),
                     isOpen = false,
                     exploreButton = $('#main .head .explore-the-network');

                  return function (duration, open) {
                     duration = duration !== undefined && duration !== null ? duration : 1000;
                     isOpen = open = open !== undefined && open !== null ? !!open : !isOpen;

                     exploreButton.toggleClass('active', open);

                     if (open) {
                        // expand
                        element.stop().animate({
                           marginTop: '0px'
                        }, duration);
                     } else {
                        // collapse
                        element.stop().animate({
                           marginTop: collapsedOffset
                        }, duration);
                     }
                  };
               }()),

               /**
                * Opens the shop window for the specified film.
                *
                * @param filmId cb-film ID
                */
               buy: function (filmId) {
                  new ShopWindow('https://shop.culturebase.org/' + CURRENT_LANGUAGE + '/1/movie/' + filmId + '/ofi').open();
               },

               /**
                * Opens the shop window for the film list.
                */
               shop: function () {
                  new ShopWindow('https://shop.culturebase.org/' + CURRENT_LANGUAGE + '/1/view/0/ofi').open();
               },

               /**
                * Opens the detail information page for the specified film.
                *
                * @param filmId cb-film ID
                */
               film: function (filmId) {
                  // Push this on the UI queue (workaround for history being
                  // overwritten).
                  setTimeout(function () {
                     window.location.assign(LINK_ROOT+'/film/'+filmId);
                  }, 0);
               },

               /**
                * Opens a partner website.
                *
                * @param url URL of the partner website
                */
               partner: function (url) {
                  if (url.length > 0) {
                     window.open(url);
                  }

                  // Close network box.
                  invokeHashCommand('explore');
               },

               /**
                * Opens a layer for displaying a big version of a film.
                *
                * @param filmId cb-film ID
                * @param previewImage URL to the preview image
                * @param player
                */
               zoom: function (filmId, previewImage, player) {
                  var cbWidget = $('#main .viewport').CbWidget();
                  if(cbWidget)
                     cbWidget.reset();

                  var params = {
                     config: 'ofi_layer',
                     id:     filmId,
                     image:  previewImage
                  };

                  if(player)
                     params.player = player;

                  new OfiPlayerWindow(null, params).open();
               },

               /**
                * Reloads the current page in another language by telling cb-ml
                * about the change. This function takes action only if the
                * target language and the current language differ.
                *
                * @global DOCUMENT_ROOT (relative) root directory of the project
                * @global LINK_ROOT (relative) root directory of the project,
                *    including the currently active language
                * @global CURRENT_LANGUAGE cb-ml locale that is currently active
                * @param locale cb-ml locale
                */
               language: function (locale) {
                  var url = window.location.href,
                     linkRoot = LINK_ROOT+'/',
                     linkRootPos = url.indexOf(linkRoot),
                     suffix = 'index';

                  if (locale != CURRENT_LANGUAGE) {
                     // Separate everything after the link root and store it for
                     // later use.
                     if (linkRootPos !== -1) {
                        suffix = url.substr(linkRootPos + linkRoot.length);
                     }

                     // Build new URL.
                     url = DOCUMENT_ROOT+'/'+locale+'/'+suffix;

                     // Load the new URL.
                     window.location.assign(url);
                  }
               },

               /**
                * Easy way to open a popup.
                *
                * @param url target
                * @param width popup window width in pixels
                * @param height popup window height in pixels
                * @param title (optional) headline; defaults to an empty string
                */
               popup: function (url, width, height, title) {
                  // Preprocess options.
                  url = url || '';
                  width = parseInt(width, 10);
                  height = parseInt(height, 10);
                  title = title || '';

                  // Fire up popup.
                  window.open(url, title, 'width='+width+',height='+height);
               },

               /**
                * Ensures that the service information at the bottom of the page
                * are visible.
                */
               service: (function () {
                  var element = $('#main > .content .service');

                  return function () {
                     toggleCollapsible(element, null, true);
                  };
               }()),

               /**
                * Starts the search process and adheres to the currently active
                * search mode automatically. It ensures that the query is sent
                * to the correct destination.
                *
                * @param query what to search for; a search is only performed if
                *    the query is not empty
                * @param mode what mode to search in (throws if mode is
                *    unsupported)
                */
               search: function (query, mode) {
                  var url,
                     params;

                  if (query.length > 0) {
                     params = $.param({q: query, m: mode});

                     url = {
                        td: LINK_ROOT+'/discover'+(document.URL.match(/tiling/) ? '/tiling' : '')+'?'+params,
                        fc: LINK_ROOT+'/discover'+(document.URL.match(/tiling/) ? '/tiling' : '')+'?'+params,
                        id: LINK_ROOT+'/searchresults?'+params
                     }[mode] || null;

                     if (url !== null) {
                        window.location.assign(url);
                     } else {
                        throw "search mode `"+mode+"' not implemented (yet)";
                     }
                  }
               },

               /**
                * Makes the section with the given name visible.
                *
                * @param name section identifier
                * @param fragment sub-identifier
                */
               section: (function () {
                  var sectionSelectorElements = $('#main > .content .section-selectors .section-selector'),
                     sectionElements = $('#main > .content .sections .section'),
                     subSectionElement = $('#main > .content .section ul'),
                     subSectionTitleElements = subSectionElement.find('li > p'),
                     subSectionTextElements = subSectionElement.find('li blockquote').hide();

                  return function (name, fragment, jumpTo) {
                     var element,
                        thisTextElement;

                     sectionSelectorElements.removeClass('active').filter('.section-selector-'+name).addClass('active');
                     sectionElements.hide().filter('.section-'+name).show();

                     if (fragment) {
                        element = $(document.getElementById(fragment)).closest('p');
                        thisTextElement = element.closest('li').find('blockquote');

                        subSectionTitleElements.not(element).removeClass('active');
                        element.toggleClass('active');
                        subSectionTextElements.not(thisTextElement).hide();
                        thisTextElement.toggle();
                     }

                     if (jumpTo) {
                        $(window).scrollTop(element.offset().top);
                     }

                     // Explicitly do not prevent the default action (so that
                     // the user gets to see a url that matches his query).
                     return false;
                  };
               }())
            };

         return function () {
            var args = Array.prototype.slice.call(arguments),
               command = args.shift();

            if (commands[command] !== undefined) {
               return commands[command].apply(this, args) !== false;
            }
         };
      }()),

      /**
       * Creates the string representation of a hash command and its arguments.
       * The first argument to this function is considered the command name,
       * while all others are added as his arguments.
       */
      createHashCommand = function () {
         var args = Array.prototype.slice.call(arguments),
            command = args.shift(),
            count = args.length,
            i;

         for (i = 0; i < count; i++) {
            args[i] = encodeURIComponent(args[i]);
         }

         return '#!'+command+'/'+args.join('/');
      },

      /**
       * Toggles the state (i.e. visibility) of a collabsible box. A collapsible
       * element needs to have two direct children with classes "topbar" and
       * "content". The "content"-element is the one that gets toggled in
       * visibility.
       *
       * @param element DOM node with a class of collapsible
       * @param duration (optional) how long the animation takes (in
       *    milliseconds); defaults to one second
       * @param open (optional) wether to open or to close the "content"-element
       */
      toggleCollapsible = function (element, duration, open) {
         var button = element.children('.topbar').find('.toggle-button'),
            content = element.children('.content'),
            data = element.data('collapsible'),
            isOpen;

         if (data === undefined) {
            data = {};
            element.data('collapsible', data);
         }

         duration = duration !== null && duration !== undefined ? duration : 1000;
         isOpen = data.isOpen !== null && data.isOpen !== undefined ? data.isOpen : true;
         open = open !== null && open !== undefined ? !!open : !isOpen;

         data.isOpen = open;
         button.toggleClass('icon-minus', open);
         button.toggleClass('icon-plus', !open);

         if (open) {
            // Expand.
            if (data.height) {
               content.stop().animate({
                  height: data.height+'px'
               }, duration);
            }
         } else {
            // Save content height for expanding later.
            data.height = data.height || content.children('div').height();

            // Collapse.
            content.stop().animate({
               height: '0px'
            }, duration);
         }
      },

      /**
       * Inserts all necessary data in the info box of a film box.
       *
       * @param element DOM node or jQuery selection that contains the infobox
       * @param itemData data used to populate the infobox
       * @param viewport root node for the player window
       */
      populateFilmBoxInfo = function(infobox, itemData, viewport) {
         infobox = $(infobox);
         infobox.find('.bottom .buy-button').removeClass('no-offer');

         if (!itemData.active) {
            infobox.find('.bottom .buy-button').addClass('no-offer');
         }

         infobox.find('.bottom .buy-button-text').text(itemData.buy_text);
         infobox.find('.bottom .buy-button').attr('href', createHashCommand('buy', itemData.movie_id));
         infobox.find('.top .expand-button').attr('href', createHashCommand('zoom', itemData.id, itemData.src));
         infobox.find('.top .details .director').html(itemData.director);
         infobox.find('.top .details .subtitle').html(itemData.subtitle);
         infobox.find('.bottom .details').html(itemData.genres);

         new OfiPlayerWindow(viewport, {
            image:  itemData.src,
            width:  636,
            height: 358,
            modal:  false,
            player: itemData.player,
            id:     itemData.id
         }).open();
      },

      /**
       * Factory function for creating a fancy selectbox-like element for
       * choosing one out of multiple values.
       *
       * @param data value/label pairs
       * @param callback (optional) handler function that gets called when the
       *    selection changes; it gets passed the selected value as an argument;
       *    it gets invoked once while initialization if the defaultValue
       *    parameter is set to a valid value
       * @param defaultValue (optional) one of the values contained in the data
       *    parameter that should be active in the beginning
       * @param separator (optional) anything that fits into jQuery.append()
       *    gets inserted between the selectors
       * @return a span element containing all of the selectors with the
       *    separator in between
       */
      createFancySelect = function (data, callback, defaultValue, separator) {
         var parent = $(document.createElement('span')).addClass('fancy-select'),
            selectors = {},
            currentValue = null,
            internalCallback = function (selectedValue) {
               // We only need to to anything if something has changed.
               if (selectedValue !== currentValue) {
                  // Update visual status.
                  $.each(selectors, function (value, item) {
                     item.toggleClass('active', value === selectedValue);
                  });

                  // Report the change.
                  callback(selectedValue);
               }
            };

         // Set default values if needed.
         callback = callback || $.noop;
         defaultValue = data[defaultValue] !== undefined && data[defaultValue] !== null ? defaultValue : null;
         separator = separator || null;

         // Create all elements.
         $.each(data, function (value, label) {
            selectors[value] = $(document.createElement('a')).attr('href', '#').text(label).click(function (e) {
               e.preventDefault();
               internalCallback(value);
            });
         });

         // Set default selection.
         if (defaultValue !== null) {
            internalCallback(defaultValue);
         }

         // Add all items to the parent element and separate them if needed.
         $.each(selectors, (function () {
            var isFirst = true;

            return function (value, item) {
               if (!isFirst && separator !== null) {
                  parent.append(separator);
               }

               isFirst = false;
               parent.append(item);
            };
         }()));

         return parent;
      },

      /**
       * Returns a list of category IDs that are defined as alias for a ID.
       *
       * @param id cb-film category ID
       * @return list of cb-film category IDs
       */
      getFilmCategoryIdsWithAliases = (function (aliases) {
         return function (masterId) {
            return [masterId].concat(aliases[masterId] || []);
         };
      }(FILM_CATEGORY_ALIASES)),

      /**
       * Binds a category callback. Implementation follows later on.
       *
       * @param callback function that should be called when the category
       *    selection changes.
       */
      bindCategorySelectionChange,

      /**
       * Reports category changes to all bound callbacks. Implementation
       * follows later on.
       *
       * @param categorySelection list of category IDs
       */
      reportCategorySelectionChanged;

   (function () {
      var bindings = [];

      bindCategorySelectionChange = function (callback) {
         bindings.push(callback);
      };

      reportCategorySelectionChanged = function (categorySelection) {
         $.each(bindings, function (index, callback) {
            callback(categorySelection);
         })
      };
   }());

   // geomap -------------------------------------------------------------------
   (function () {
      var markers = {
            // Add new markers here (please use alphabetical order to simplify
            // the process for future edits). The item structure is as follows:
            //
            // <country name (for css class and info request; unique)>: {
            //    url:      <external URL for partner; empty if not needed>,
            //    coords:   <coordinates list (relative): x1, y1, x2, y2, ...>,
            //    position: [
            //       <x (topleft corner)>,
            //       <y (topleft corner)>
            //    ]
            // }
            //
            // Make sure to add the item in the style sheets as well (look at
            // the others for examples).
            austria:   {url: 'http://austria.onlinefilm.org/',   position: [183, 281], coords: [1, 17, 26, 15, 27, 9, 46, 3, 56, 6, 56, 15, 48, 27, 34, 29]},
            germany:   {url: '',                                 position: [160, 208], coords: [24, 0, 35, 4, 58, 8, 66, 50, 46, 57, 65, 76, 48, 86, 8, 82, 6, 33]},
            greece:    {url: 'http://greece.onlinefilm.org/',    position: [255, 355], coords: [0, 25, 9, 25, 14, 14, 41, 4, 57, 1, 61, 1, 61, 12, 55, 15, 65, 32, 61, 42, 64, 47, 83, 58, 83, 62, 67, 80, 40, 80, 23, 61]},
            ireland:   {url: 'http://ireland.onlinefilm.org/',   position: [ 55, 180], coords: [12, 6, 26, 1, 34, 1, 40, 3, 41, 16, 34, 18, 33, 30, 27, 36, 16, 37, 5, 37, 0, 30, 1, 26, 9, 22]},
            italy:     {url: 'http://italy.onlinefilm.org/',     position: [155, 298], coords: [0, 8, 45, 0, 55, 17, 44, 20, 93, 81, 62, 120, 4, 88, 4, 66, 17, 65, 15, 88, 38, 101, 72, 96, 68, 84, 16, 33, 0, 34]},
            latvia:    {url: 'http://latvia.onlinefilm.org/',    position: [263, 174], coords: [1, 18, 10, 8, 19, 16, 20, 6, 42, 10, 47, 22, 36, 29, 27, 24, 7, 24, 2, 27]},
            lithuania: {url: 'http://lithuania.onlinefilm.org/', position: [266, 198], coords: [0, 6, 6, 3, 30, 4, 36, 12, 30, 25, 19, 30, 2, 14]},
            slovenia:  {url: 'http://slovenia.onlinefilm.org/',  position: [212, 304], coords: [2, 6, 24, 5, 14, 18, 3, 17]}
         },
         activeCountry = 'germany',
         parent = $('#network'),
         mapWrapElement = parent.find('.map-wrap'),
         infoWrapElement = parent.find('.info-wrap'),
         infoContentElement = infoWrapElement.find('.content'),
         titleElement = infoWrapElement.find('.title'),
         descriptionElement = infoWrapElement.find('.description'),
         visitPartnerButton = infoWrapElement.find('.visit-partner-button'),
         markersElement = mapWrapElement.find('.markers'),
         mapElement = mapWrapElement.find('map'),
         areaElementPrototype = mapElement.find('area').detach(),

         // functions
         fetchCountryInfo,
         pupulateInfo;

      if (parent.length > 0) {
         fetchCountryInfo = (function () {
            var cache = {};

            return function (country, callback) {
               if (cache[country] !== undefined) {
                  callback(cache[country]);
               } else {
                  $.get(getAjaxEntryPoint('getCountryInfo'), {
                     country: country
                  }, getAjaxResponseHandler(function (info) {
                     cache[country] = info;
                     callback(cache[country]);
                  }), getAjaxFormat());
               }
            };
         }());

         pupulateInfo = function (country) {
            infoContentElement.stop().animate({
               opacity: 0.0
            }, 250);

            fetchCountryInfo(country, function (info) {
               titleElement.html(info.title);
               descriptionElement.html(info.text);
               visitPartnerButton.html(info.link).attr('href', createHashCommand('partner', markers[country].url));

               infoContentElement.stop().animate({
                  opacity: 1.0
               }, 250);
            });
         };

         // Add markers to the map
         $.each(markers, function (name, config) {
            var absoluteCoords = [],
               coordsCound = config.coords.length,
               i,
               markerElement,
               areaElement;

            // Calculate absolute position in map.
            for (i = 0; i < coordsCound; i += 2) {
               if (config.coords[i] && config.coords[i + 1]) {
                  absoluteCoords.push(config.coords[i] + config.position[0]); // x
                  absoluteCoords.push(config.coords[i + 1] + config.position[1]); // y
               }
            }

            // Create and insert area element.
            areaElement = areaElementPrototype.clone();
            areaElement.attr('coords', absoluteCoords.join(','));
            mapElement.append(areaElement);

            // Create marker element.
            markerElement = $(document.createElement('div'))
               .addClass('marker marker-'+name)
               .css({
                  top:  config.position[1]+'px',
                  left: config.position[0]+'px'
               });

            // Bind marker.
            areaElement.bind('mouseenter mouseleave', function () {
               markerElement.toggleClass('highlighted');
            }).mousedown(function () {
               activeCountry = name;
               markerElement.addClass('active')
                  .siblings('.marker')
                  .removeClass('active');
            }).mouseenter(function () {
               // Load country info.
               pupulateInfo(name);
            }).mouseleave(function () {
               if (name != activeCountry) {
                  // Unload info and go back to the default.
                  pupulateInfo(activeCountry);
               }
            });

            // Insert marker element.
            markersElement.append(markerElement);

            // Apply defaults on startup by simulating regular usage.
            if (name == activeCountry) {
               areaElement.mouseenter().mousedown().mouseleave();
            }
         });

         // Fix map image dimensions.
         (function () {
            this.attr('height', this.height()).attr('width', this.width());
         }.call(parent.find('.map-image')));
      }
   }());

   // search -------------------------------------------------------------------
   (function (searchModes, searchModeDefaultValues, query, searchMode) {
      var element = $('#main .head .search'),
         inputElement = element.find('.input'),
         input = inputElement.find('input[name="query"]'),
         select = inputElement.find('select[name="query"]'),
         paramsElement = element.find('.params'),
         optionsButton = paramsElement.find('.options-button'),
         optionsElement = paramsElement.find('.options'),
         open = false,
         searchButton = element.find('.search-button'),
         inputMode = 'input',
         lastUsedSearchModeCookie = 'LAST_USED_SEARCH_MODE',
         timerEnabled = true,
         hoverTimer = null,

         // functions
         getInputMode = function (mode) {
            if (mode == 'fc') {
               return 'select';
            }

            return 'input';
         },

         fillCategorySelect = function (selectedValue) {
            selectedValue = selectedValue || null;
            select.children('option[value!="0"]').remove();

            fetch('getFilmCategories', {}, function (data) {
               $.each(data, function (id, label) {
                  $(document.createElement('option')).attr('value', id).text(label).appendTo(select);
               });

               if (selectedValue !== null) {
                  select.val(selectedValue);
               }
            });
         };

      // Restore the users settings if possible.
      searchMode = searchMode !== null ? searchMode : ($.cookie(lastUsedSearchModeCookie) || 'td');

      if (element.length > 0) {
         // Apply default values.
         switch (getInputMode(searchMode)) {
            case 'input':
               input.val(query !== null ? query : null);
               break;
            case 'select':
               fillCategorySelect(query);
               break;
         }

         // Add search mode selector.
         optionsElement.children('div').append(createFancySelect(searchModes, function (value) {
            var targetInputMode;

            searchMode = value;

            // Store the search mode to be able to restore the users settings.
            $.cookie(lastUsedSearchModeCookie, searchMode, {
               path: '/'
            });

            targetInputMode = getInputMode(searchMode);

            switch (searchMode) {
               case 'fc':
                  fillCategorySelect();
                  break;
            }

            if (targetInputMode !== inputMode) {
               element.removeClass('mode-'+inputMode).addClass('mode-'+targetInputMode);
               inputMode = targetInputMode;
               input.val('');
               select.val(0);
            }

            //paramsElement.click();

            switch (inputMode) {
               case 'input':
                  input.defaultValuedInput(searchModeDefaultValues[searchMode], 'active');
                  break;
               case 'select':
                  select.children('option[value="0"]').text(searchModeDefaultValues[searchMode]);
                  break;
            }
         }, searchMode, '<span> // </span>'));

         (function () {
            this.width(this.width() + 5); // FF on Mac fails to calculate the with correctly.
         }.call(optionsElement.children('div')));

         optionsElement.width(0);

         // Bind everything.
         paramsElement.click(function () {
            open = !open;

            optionsButton.toggleClass('icon-minus', open);
            optionsButton.toggleClass('icon-plus', !open);

            if (open) {
               // expand
               optionsElement.stop().animate({
                  width: optionsElement.children('div').width()+'px'
               }, 500);
            } else {
               // collapse
               optionsElement.stop().animate({
                  width: '0px'
               }, 500);
            }
         });

         input.keypress(function (e) {
            if (e.which == 13) { // enter
               invokeHashCommand('search', input.val(), searchMode);
            }
         });

         select.change(function () {
            if (select.val() > 0) {
               invokeHashCommand('search', select.val(), searchMode);
            }
         });

         searchButton.click(function () {
            if (input.val() != '') {
               invokeHashCommand('search', input.val(), searchMode);
            }
         });

         $().add(inputElement).add(paramsElement).mouseenter(function () {
            if (timerEnabled && !open) {
               hoverTimer = setTimeout(function () {
                  hoverTimer = null;
                  paramsElement.click();
               }, 1000);
            }
         })

         $().add(input).add(select).focus(function () {
            timerEnabled = false;

            if (hoverTimer !== null) {
               clearTimeout(hoverTimer);
               hoverTimer = null;
            }
         });

         element.click(function () {
            timerEnabled = false;

            if (hoverTimer !== null) {
               clearTimeout(hoverTimer);
               hoverTimer = null;
            }
         }).mouseleave(function () {
            if (timerEnabled) {
               if (hoverTimer !== null) {
                  clearTimeout(hoverTimer);
                  hoverTimer = null;
               }

               if (open) {
                  paramsElement.click();
               }
            }
         });
      }
   }(SEARCH_MODES, SEARCH_MODE_DEFAULT_VALUES, SEARCH_QUERY, SEARCH_MODE));

   // film detail --------------------------------------------------------------
   if (FILM_FILM_ID !== null) {
      // film box
      (function (filmId) {
         var element = $('#main > .content .film-detail'),
            viewport = element.find('.viewport'),
            sidebar = element.find('.sidebar'),
            infobox = sidebar.find('.infobox');

         if (element.length > 0) {
            fetch('getFilm', {
               id:     filmId,
               limit:  1,
               offset: 0,
               supported_players: $.CbWidget.player.detect()
            }, function (itemData) {
               // There is only one item.
               populateFilmBoxInfo(infobox, itemData[0], viewport);

               // Insert film information.
               infobox.find('.top .title').removeClass('long').html(itemData[0].title);

               if (itemData[0].title.length > 32) {
                  infobox.find('.top .title').addClass('long');
               }
            }, {
               failureCallback: function () {
                  // Add dummy image.
                  viewport.html('<div class="placeholder"></div>');
               }
            });
         }
      }(FILM_FILM_ID));

      // related films
      (function (filmId) {
         var element = $('#main > .content .related-films'),
            topbar = element.children('.topbar'),
            content = element.children('.content'),
            wrap = element.find('.items'),
            itemPrototype = element.find('.item').detach(),
            position = 0,
            widthPerItem,
            itemsPerSlide = 6,
            loadedPositions = [true, true, true, true, true, true],
            doneLoading = false,
            isLoading = false,
            isSliding = false,

            loadFilms = function (offset, successCallback, failureCallback) {
               fetch('getRelatedFilms', {
                  id:     filmId,
                  limit:  itemsPerSlide,
                  offset: offset,
                  supported_players: $.CbWidget.player.detect()
               }, successCallback, {
                  failureCallback: failureCallback
               });
            },

            createItems = function (itemsData) {
               var items = [],
                  item,
                  i;

               $.each(itemsData, function (i, itemData) {
                  item = itemPrototype.clone();
                  item.css('background-image', 'url("http://data.heimat.de/transform.php?file='+encodeURIComponent(itemData.thumb)+'&width=151&height=100&do=cropOut")');
                  item.find('.label').html(itemData.title).attr('href', createHashCommand('film', itemData.movie_id));
                  item.find('.icons > .expand-button').attr('href', createHashCommand('zoom', itemData.id, itemData.src));
                  items.push(item);
               });

               for (i = 0; i < itemsPerSlide - itemsData.length; i++) {
                  item = itemPrototype.clone();
                  item.children().remove();
                  items.push(item);
               }

               return items;
            },

            addInitialItems = function (itemsData) {
               $.each(createItems(itemsData), function (i, element) {
                  wrap.append(element);
                  widthPerItem = element.outerWidth(true);
               });

               wrap.width(widthPerItem * itemsPerSlide);
               content.scrollLeft(0);
            };

         if (element.length > 0) {
            content.scrollLeft(0);

            topbar.find('.prev-button').click(function (e) {
               e.preventDefault();
               position -= itemsPerSlide;

               if (position >= 0) {
                  content.animate({
                     scrollLeft: widthPerItem * position
                  }, 500);
               } else {
                  position = 0;
               }
            });

            topbar.find('.next-button').click(function (e) {
               var slideToItems = function (pos) {
                     isSliding = true;

                     content.stop().animate({
                        scrollLeft: widthPerItem * pos
                     }, 500, function () {
                        isSliding = false;
                     });
                  };

               e.preventDefault();

               if (!isSliding) {
                  position += itemsPerSlide;

                  if (!loadedPositions[position]) {
                     if (!doneLoading && !isLoading) {
                        isLoading = true;
                        loadFilms(position, function (itemsData) {
                           var items = createItems(itemsData),
                              i,
                              item;

                           $.each(items, function (i, item) {
                              loadedPositions[position + i] = true;
                              wrap.append(item);
                           });

                           if (itemsData.length < itemsPerSlide) {
                              doneLoading = true;
                           }

                           if (itemsData.length > 0) {
                              wrap.width(wrap.width() + widthPerItem * itemsPerSlide);
                              slideToItems(position);
                           } else {
                              position -= itemsPerSlide;
                           }

                           isLoading = false;
                        });
                     } else {
                        position -= itemsPerSlide;
                     }
                  } else {
                     slideToItems(position);
                  }
               }
            });

            // Add touch bindings.
//FIXME  this bugged massive
/*            content.wipe({
               preventDefaults: true,
               threshold: 50,

               left: function () {
                  topbar.find('.next-button').click();
               },

               right: function () {
                  topbar.find('.prev-button').click();
               }
            });
*/
            // Load initial content.
            loadFilms(position, function (itemsData) {
               addInitialItems(itemsData);
            }, function () {
               addInitialItems([]);
            });
         }
      }(FILM_FILM_ID));
   }

   // films of the week --------------------------------------------------------
   if (FILM_OF_THE_WEEK_CATEGORIES !== null) {
      (function (categories) {
         var element = $('#main > .content .film-of-the-week'),
            selection = element.find('.topbar .label-wrap .category'),
            viewport = element.find('.viewport'),
            sidebar = element.find('.sidebar'),
            infobox = sidebar.find('.infobox'),
            position,
            maxKnownPosition,
            maxPosition,
            ofiPlayerWindow = null,
            optionsButton = element.find('.options-button'),
            optionsElement = element.find('.options'),
            open = false,
            selectedCategories = [],

            resetPosition = function () {
               position = 0;
               maxKnownPosition = 0;
               maxPosition = null;
            },

            displayCurrentItem = function () {
               maxKnownPosition = Math.max(position, maxKnownPosition);

               fetch('getFilmsOfTheWeek', {
                  limit:        1,
                  offset:       position,
                  category_ids: selectedCategories,
                  supported_players: $.CbWidget.player.detect()
               }, function (itemData) {
                  if (itemData.length > 0) {
                     itemData = itemData[0];

                     if (itemData) {
                        infobox.find('.top .title').removeClass('long');

                        if (itemData.title.length > 32) {
                           infobox.find('.top .title').addClass('long');
                        }

                        infobox.find('.top .title a').html(itemData.title).attr('href', createHashCommand('film', itemData.movie_id));

                        if (ofiPlayerWindow !== null) {
                           ofiPlayerWindow.close();
                        }

                        populateFilmBoxInfo(infobox, itemData, viewport);
                     } else {
                        maxPosition = position - 1;
                        next();
                        displayCurrentItem();
                     }
                  }
               }, {
                  failureCallback: function () {
                     // Add dummy image.
                     viewport.html('<div class="placeholder"></div>');
                  }
               });
            },

            prev = function () {
               position -= 1;
               position = position < 0 ? maxKnownPosition + position + 1 : position;
            },

            next = function () {
               position += 1;
               position = maxPosition ? position % maxPosition : position;
            };

         if (element.length > 0) {
            resetPosition();

            optionsElement.children('div').append(createFancySelect(categories, function (value) {
               selection.text(categories[value]);
               optionsButton.click();
               reportCategorySelectionChanged(value > 0 ? getFilmCategoryIdsWithAliases(value) : []);
            }, "0", '<span> // </span>'));

            bindCategorySelectionChange(function (selection) {
               selectedCategories = selection;
               resetPosition();
               displayCurrentItem();
            });

            (function () {
               this.width(this.width() + 5); // FF on Mac fails to calculate the with correctly.
            }.call(optionsElement.children('div')));

            optionsElement.width(0);

            sidebar.find('.prev-button').click(function (e) {
               e.preventDefault();
               prev();
               displayCurrentItem();
            });

            sidebar.find('.next-button').click(function (e) {
               e.preventDefault();
               next();
               displayCurrentItem();
            });

            optionsButton.click(function () {
               open = !open;

               optionsButton.toggleClass('icon-minus', open);
               optionsButton.toggleClass('icon-plus', !open);

               if (open) {
                  // expand
                  optionsElement.stop().animate({
                     width: optionsElement.children('div').width()+'px'
                  }, 500);
               } else {
                  // collapse
                  optionsElement.stop().animate({
                     width: '0px'
                  }, 500);
               }
            });

            displayCurrentItem();
         }
      }(FILM_OF_THE_WEEK_CATEGORIES));
   }

   // featured films -----------------------------------------------------------
   (function () {
      var element = $('#main > .content .featured-films'),
         topbar = element.children('.topbar'),
         content = element.children('.content'),
         wrap = element.find('.items'),
         itemPrototype = element.find('.item').detach(),
         widthPerItem,
         itemsPerSlide = 3,
         position,
         loadedPositions,
         doneLoading = false,
         isLoading = false,
         isSliding = false,
         optionsButton = element.find('.options-button'),
         optionsElement = element.find('.options'),
         open = false,
         selectedCategories = [],

         resetPosition = function () {
            position = 0;
            loadedPositions = [true, true, true];
         },

         loadFilms = function (offset, successCallback, failureCallback) {
            fetch('getFeaturedFilms', {
               limit:        itemsPerSlide,
               offset:       offset,
               category_ids: selectedCategories,
               supported_players: $.CbWidget.player.detect()
            }, successCallback, {
               failureCallback: failureCallback
            });
         },

         addInitialItems = function (itemsData) {
            wrap.width(0).children().remove();

            $.each(createItems(itemsData), function (i, element) {
               wrap.append(element);
               widthPerItem = element.outerWidth(true);
            });

            wrap.width(widthPerItem * itemsPerSlide);
            content.scrollLeft(0);
         },

         loadInitialContent = function () {
            loadFilms(position, function (itemsData) {
               addInitialItems(itemsData);
            }, function () {
               addInitialItems([]);
            });
         },

         createItems = function (itemsData) {
            var items = [],
               item,
               i;

            $.each(itemsData, function (i, itemData) {
               item = itemPrototype.clone();
               item.css('background-image', 'url("http://data.heimat.de/transform.php?file='+encodeURIComponent(itemData.thumb)+'&width=312&height=177&do=cropOut")');
               item.find('.label > .main').html(itemData.title).attr('href', createHashCommand('film', itemData.movie_id));
               item.find('.icons > .expand-button').attr('href', createHashCommand('zoom', itemData.id, itemData.src, itemData.player));

               if (itemData.subtitle) {
                  item.find('.label > .sub').html(itemData.subtitle.replace(/\n/g, '\n<br />'));
               }

               items.push(item);
            });

            for (i = 0; i < itemsPerSlide - itemsData.length; i++) {
               item = itemPrototype.clone();
               item.children().remove();
               items.push(item);
            }

            return items;
         };

      if (element.length > 0) {
         resetPosition();

         (function () {
            this.width(this.width() + 5); // FF on Mac fails to calculate the with correctly.
         }.call(optionsElement.children('div')));

         optionsElement.width(0);
         content.scrollLeft(0);

         bindCategorySelectionChange(function (selection) {
            selectedCategories = selection;
            resetPosition();
            loadInitialContent();
         });

         topbar.find('.prev-button').click(function (e) {
            e.preventDefault();
            position -= itemsPerSlide;

            if (position >= 0) {
               content.animate({
                  scrollLeft: widthPerItem * position
               }, 500);
            } else {
               position = 0;
            }
         });

         topbar.find('.next-button').click(function (e) {
            var slideToItems = function (pos) {
                  isSliding = true;

                  content.stop().animate({
                     scrollLeft: widthPerItem * pos
                  }, 500, function () {
                     isSliding = false;
                  });
               };

            e.preventDefault();

            if (!isSliding) {
               position += itemsPerSlide;

               if (!loadedPositions[position]) {
                  if (!doneLoading && !isLoading) {
                     isLoading = true;
                     loadFilms(position, function (itemsData) {
                        var items = createItems(itemsData),
                           i,
                           item;

                        $.each(items, function (i, item) {
                           loadedPositions[position + i] = true;
                           wrap.append(item);
                        });

                        if (itemsData.length < itemsPerSlide) {
                           doneLoading = true;
                        }

                        if (itemsData.length > 0) {
                           wrap.width(wrap.width() + widthPerItem * itemsPerSlide);
                           slideToItems(position);
                        } else {
                           position -= itemsPerSlide;
                        }

                        isLoading = false;
                     });
                  } else {
                     position -= itemsPerSlide;
                  }
               } else {
                  slideToItems(position);
               }
            }
         });

         optionsButton.click(function () {
            open = !open;

            optionsButton.toggleClass('icon-minus', open);
            optionsButton.toggleClass('icon-plus', !open);

            if (open) {
               // expand
               optionsElement.stop().animate({
                  width: optionsElement.children('div').width()+'px'
               }, 500);
            } else {
               // collapse
               optionsElement.stop().animate({
                  width: '0px'
               }, 500);
            }
         });

         // Add touch bindings.
//FIXME  this bugged massive
/*         content.wipe({
            preventDefaults: true,
            threshold: 50,

            left: function () {
               topbar.find('.next-button').click();
            },

            right: function () {
               topbar.find('.prev-button').click();
            }
         });
*/
         loadInitialContent();
      }
   }());

   // random films -------------------------------------------------------------
   (function () {
      var element = $('#main > .content .random-films'),
         topbar = element.children('.topbar'),
         content = element.children('.content'),
         wrap = element.find('.items'),
         itemPrototype = element.find('.item').detach(),
         widthPerItem,
         itemsPerSlide = 6,
         position,
         loadedPositions,
         doneLoading = false,
         isLoading = false,
         isSliding = false,
         selectedCategories = [],

         resetPosition = function () {
            position = 0;
            loadedPositions = [true, true, true];
         },

         loadFilms = function (offset, successCallback, failureCallback) {
            fetch('getRandomFilms', {
               limit:        itemsPerSlide,
               offset:       offset,
               category_ids: selectedCategories,
               supported_players: $.CbWidget.player.detect()
            }, successCallback, {
               failureCallback: failureCallback
            });
         },

         addInitialItems = function (itemsData) {
            wrap.width(0).children().remove();

            $.each(createItems(itemsData), function (i, element) {
               wrap.append(element);
               widthPerItem = element.outerWidth(true);
            });

            wrap.width(widthPerItem * itemsPerSlide);
            content.scrollLeft(0);
         },

         loadInitialContent = function () {
            loadFilms(position, function (itemsData) {
               addInitialItems(itemsData);
            }, function () {
               addInitialItems([]);
            });
         },

         createItems = function (itemsData) {
            var items = [],
               item,
               i;

            $.each(itemsData, function (i, itemData) {
               item = itemPrototype.clone();
               item.css('background-image', 'url("http://data.heimat.de/transform.php?file='+encodeURIComponent(itemData.thumb)+'&width=151&height=100&do=cropOut")');
               item.find('.label').html(itemData.title).attr('href', createHashCommand('film', itemData.movie_id));
               item.find('.icons > .expand-button').attr('href', createHashCommand('zoom', itemData.id, itemData.src, itemData.player));
               items.push(item);
            });

            for (i = 0; i < itemsPerSlide - itemsData.length; i++) {
               item = itemPrototype.clone();
               item.children().remove();
               items.push(item);
            }

            return items;
         };

      if (element.length > 0) {
         resetPosition();

         content.scrollLeft(0);

         bindCategorySelectionChange(function (selection) {
            selectedCategories = selection;
            resetPosition();
            loadInitialContent();
         });

         topbar.find('.prev-button').click(function (e) {
            e.preventDefault();
            position -= itemsPerSlide;

            if (position >= 0) {
               content.animate({
                  scrollLeft: widthPerItem * position
               }, 500);
            } else {
               position = 0;
            }
         });

         topbar.find('.next-button').click(function (e) {
            var slideToItems = function (pos) {
                  isSliding = true;

                  content.stop().animate({
                     scrollLeft: widthPerItem * pos
                  }, 500, function () {
                     isSliding = false;
                  });
               };

            e.preventDefault();

            if (!isSliding) {
               position += itemsPerSlide;

               if (!loadedPositions[position]) {
                  if (!doneLoading && !isLoading) {
                     isLoading = true;
                     loadFilms(position, function (itemsData) {
                        var items = createItems(itemsData),
                           i,
                           item;

                        $.each(items, function (i, item) {
                           loadedPositions[position + i] = true;
                           wrap.append(item);
                        });

                        if (itemsData.length < itemsPerSlide) {
                           doneLoading = true;
                        }

                        if (itemsData.length > 0) {
                           wrap.width(wrap.width() + widthPerItem * itemsPerSlide);
                           slideToItems(position);
                        } else {
                           position -= itemsPerSlide;
                        }

                        isLoading = false;
                     });
                  } else {
                     position -= itemsPerSlide;
                  }
               } else {
                  slideToItems(position);
               }
            }
         });

         // Add touch bindings.
//FIXME  this bugged massive
/*         content.wipe({
            preventDefaults: true,
            threshold: 50,

            left: function () {
               topbar.find('.next-button').click();
            },

            right: function () {
               topbar.find('.prev-button').click();
            }
         });
*/
         loadInitialContent();
      }
   }());

   // user films ---------------------------------------------------------------
   if (USER_ID !== null) {
      (function (userId) {
         var element = $('#main > .content .user-films'),
            topbar = element.children('.topbar'),
            content = element.children('.content'),
            wrap = element.find('.items'),
            itemPrototype = element.find('.item').detach(),
            position = 0,
            widthPerItem,
            itemsPerSlide = 6,
            loadedPositions = [true, true, true],
            doneLoading = false,
            isLoading = false,
            isSliding = false,

            loadFilms = function (offset, successCallback, failureCallback) {
               fetch('getUserFilms', {
                  id:     userId,
                  limit:  itemsPerSlide,
                  offset: offset,
                  supported_players: $.CbWidget.player.detect()
               }, successCallback, {
                  failureCallback: failureCallback
               });
            },

            createItems = function (itemsData) {
               var items = [],
                  item,
                  i;

               $.each(itemsData, function (i, itemData) {
                  item = itemPrototype.clone();
                  item.css('background-image', 'url("http://data.heimat.de/transform.php?file='+encodeURIComponent(itemData.thumb)+'&width=151&height=100&do=cropOut")');
                  item.find('.label').html(itemData.title).attr('href', createHashCommand('film', itemData.movie_id));
                  item.find('.icons > .expand-button').attr('href', createHashCommand('zoom', itemData.id, itemData.src));
                  items.push(item);
               });

               for (i = 0; i < itemsPerSlide - itemsData.length; i++) {
                  item = itemPrototype.clone();
                  item.children().remove();
                  items.push(item);
               }

               return items;
            },

            addInitialItems = function (itemsData) {
               $.each(createItems(itemsData), function (i, element) {
                  wrap.append(element);
                  widthPerItem = element.outerWidth(true);
               });

               wrap.width(widthPerItem * itemsPerSlide);
               content.scrollLeft(0);
            };

         if (element.length > 0) {
            content.scrollLeft(0);

            topbar.find('.prev-button').click(function (e) {
               e.preventDefault();
               position -= itemsPerSlide;

               if (position >= 0) {
                  content.animate({
                     scrollLeft: widthPerItem * position
                  }, 500);
               } else {
                  position = 0;
               }
            });

            topbar.find('.next-button').click(function (e) {
               var slideToItems = function (pos) {
                     isSliding = true;

                     content.stop().animate({
                        scrollLeft: widthPerItem * pos
                     }, 500, function () {
                        isSliding = false;
                     });
                  };

               e.preventDefault();

               if (!isSliding) {
                  position += itemsPerSlide;

                  if (!loadedPositions[position]) {
                     if (!doneLoading && !isLoading) {
                        isLoading = true;
                        loadFilms(position, function (itemsData) {
                           var items = createItems(itemsData),
                              i,
                              item;

                           $.each(items, function (i, item) {
                              loadedPositions[position + i] = true;
                              wrap.append(item);
                           });

                           if (itemsData.length < itemsPerSlide) {
                              doneLoading = true;
                           }

                           if (itemsData.length > 0) {
                              wrap.width(wrap.width() + widthPerItem * itemsPerSlide);
                              slideToItems(position);
                           } else {
                              position -= itemsPerSlide;
                           }

                           isLoading = false;
                        });
                     } else {
                        position -= itemsPerSlide;
                     }
                  } else {
                     slideToItems(position);
                  }
               }
            });

            // Add touch bindings.
//FIXME  this bugged massive
/*            content.wipe({
               preventDefaults: true,
               threshold: 50,

               left: function () {
                  topbar.find('.next-button').click();
               },

               right: function () {
                  topbar.find('.prev-button').click();
               }
            });
*/
            // Load initial content.
            loadFilms(position, function (itemsData) {
               addInitialItems(itemsData);
            }, function () {
               addInitialItems([]);
            });
         }
      }(USER_ID));
   }

   // support ------------------------------------------------------------------
   (function () {
      var faqTitleElements = $('#main > .content .support .section ul li > p'),

         // TODO: Add documentation.
         makeFragment = function (text) {
            // Handle some special cases.
            $.each({
               'ü': 'ue', 'Ü': 'Ue',
               'ö': 'oe', 'Ö': 'Oe',
               'ä': 'ae', 'Ä': 'Ae',
               'ß': 'ss'
            }, function (pattern, replacement) {
               text = text.replace(new RegExp(pattern, 'g'), replacement);
            });

            return text.replace(/[^a-z0-9.-]/gi, '_').replace(/__+/g, '_').replace(/^_+/, '').replace(/_+$/, '');
         };

      // Make all expandable headlines an a-tag to make hotlinking easy.
      faqTitleElements.each(function () {
         var self = $(this),
            section = self.closest('.section').attr('class').replace(/^.*\bsection\-([^\b\s]+)\b.*$/, '$1'),
            text = self.text(),
            fragment = makeFragment(text),
            link = $(document.createElement('a')).attr('href', createHashCommand('section', section, fragment)).attr('id', fragment).text(text);

         self.empty().append(link);
      });
   }());

   // style fixes --------------------------------------------------------------
   (function () {
      this.find('.separator').height(this.children('div').height());
   }).call($('#main > .content .service .content'));

   (function () {
      this.prepend('<span class="bullet icon-bull-gray"></span>');
   }).call($('#main > .content .sections h1'));

   // bindings -----------------------------------------------------------------

   // Activate the first section selector that is available.
   $('#main > .content .section-selectors .section-selector:first').click().addClass('active');

   // Bind existing #! commands.
   (function () {
      var parseHashCommand = function (str) {
            var segments = [];

            $.each(str.replace(/^.*#!/g, '').split('/'), function (i, segment) {
               segments.push(decodeURIComponent(segment));
            });

            return segments;
         };

      // Fire this on page load.
      if (window.location.hash.substr(0, 2) === '#!') {
         var hashCommand = parseHashCommand(window.location.hash);
         if (hashCommand[0] == 'section' && hashCommand.length > 2) {
            hashCommand.push(true);
         }
         invokeHashCommand.apply(this, hashCommand);
      }

      // Fire this on link click.
      $('body').delegate('a[href^="#!"]', 'click', function (e) {
         if (invokeHashCommand.apply(this, parseHashCommand(this.href))) {
            e.preventDefault();
         }
      });
   }());

   // Handle collapsible boxes.
   $('#main').delegate('#main > .content .box.collapsible .topbar .toggle-button', 'click', function (e) {
      e.preventDefault();
      toggleCollapsible($(this).closest('.box.collapsible'));
   });

   // Disable empty anchor links. Seriously. Who needs those?
   $('body').delegate('a[href="#"]', 'click', function (e) {
      e.preventDefault();
   });

   // initialization -----------------------------------------------------------

   // Close service box if we are not on the index page.
   if (CURRENT_PAGE != 'index') {
      toggleCollapsible($('#main > .content .service'), 0, false);
   }

   // Print the page if needed.
   if (PRINT) {
      window.print();
   }

   // partnerlogos slider in footer
   $('#main > .foot .slider-content').mediaSlider({
      leftArea:   '.sponsors-slider-area-left',
      rightArea:  '.sponsors-slider-area-right',
      entry:      '.entry',
      continuous: true,
      autoScroll: true,
      buttonSpeed: 2,
      areaSpeed:   1
   });
});

