/*
 * Generic class implementing drop
 * handling of resources and files
 */
function DropTarget(manager, $container) {

  $container.bind("drop", onDrop);

  /*
   * Handles data drops
   */
  function onDrop(e) {
  
    var $target = $(e.target).removeClass("dragover");
    var targetSource = $target.data("source");
    
    if (!(MediaRequest.ADD in targetSource.allowed)) {
    
      return false;
    }
    
    var dataTransfer = e.originalEvent.dataTransfer;
    var files = dataTransfer.files;
    
    // Handle file upload requests
    // (No such thing in IE yet)
    if (files && files.length > 0) {
    
      manager.addResources(files, targetSource);
    } else { // Handle internal drops
    
      var urls = dataTransfer.getData("URL");
      
      if (urls) {
      
        manager.moveResources(manager.findResources(urls.split(",")), targetSource);
      }
    }
    
    return false;
  }
}

/**
 * An element which puts itself above all elements, thus
 * preventing any interaction with the application
 *
 * It automatically listens to some events to trigger itself
 */
function Overlay() {

  var $overlay = $("<div/>", {"class": "overlay hidden"});
  var questionMessages = [];
  
  $(messages).bind({
    messageAdded: onMessageAdded,
    messageRemoved: onMessageRemoved
  });
  
  /**
   * Marks the overlay as visible upon question messages
   */
  function onMessageAdded(e) {
    
    if (e.message.type == Message.QUESTION) {
    
      questionMessages.push(e.message);
      
      $overlay.removeClass("hidden");
    }
  }
  
  /**
   * Marks the overlay as hidden if the last
   * question message has been removed
   */
  function onMessageRemoved(e) {
    
    if (e.message.type == Message.QUESTION) {
    
      var index = questionMessages.indexOf(e.message);
      
      if (index > -1) {
      
        questionMessages.splice(index, 1);
      }
      
      // Mark as hidden if this was the last question
      if (questionMessages.length == 0) {
      
        $overlay.addClass("hidden");
      }
    }
  }
  
  return $overlay
}

/*
 * A user dialog for changing settings
 * of a resource list view
 */
function SettingsDialog(manager) {

  var $dialog = $("<section/>");
  var dialog = $dialog[0];
  var $titleText = $("<h1/>");
  var $closeButton = $("<button/>", {"class": "action close"});
  var $sorting = $("<p/>");
  var $ascendingButton = $("<button/>");
  var $descendingButton = $("<button/>");
  var $properties = $("<section/>");
  
  (function() {
    
    $closeButton.text("Schließen")
                .attr("title", "Schließen")
                .click(function() { $dialog.fadeOut("fast") });
    $titleText.text("Einstellungen")
              .append($closeButton);
    
    $ascendingButton.append($("<span>Auf</span>"))
                    .attr("title", "Aufsteigend sortieren")
                    .addClass("ascending")
                    .data("order", Sorting.ASCENDING);
    $descendingButton.append($("<span>Ab</span>"))
                     .attr("title", "Absteigend sortieren")
                     .addClass("descending")
                     .data("order", Sorting.DESCENDING);
    $sorting.attr("id", "sorting")
            .append($("<label/>").text("Sortierung:"))
            .append($ascendingButton)
            .append($descendingButton)
            .delegate("button", "click", onSortingButtonClick);
    
    switch (settings.get("view.sortingOrder")) {
    
      case Sorting.ASCENDING:
      
        $ascendingButton.addClass("active");
        break;
        
      case Sorting.DESCENDING:
      
        $descendingButton.addClass("active");
        break;
    }
    
    var visibleProperties = settings.get("view.visibleProperties");
    var $propertyList = $("<ul/>");
    var provider = providers.getProviders("resourceProperties");
    
    // TODO: Sort by title
    for (var name in provider) {
    
      $propertyList.append(createItem(
        name,
        provider[name],
        visibleProperties.indexOf(name) > -1
      ).slideDown());
    }
    
    $(providers).bind({
      providerAdded: onProviderAdded.bind(this),
      providerRemoved: onProviderRemoved.bind(this)
    });
    
    /*
     * 1) Input -> uncheck [done]
     * 2) Input known tag -> Check -> Clear & Uncheck -> Check known tag
     * 3) Check -> Move to regular label -> Recreate
     */
    var $otherCheckbox = $("<input/>");
    var $otherField = $("<input/>");
    
    $otherCheckbox[0].setAttribute("type", "checkbox");
    $otherCheckbox.click(function(e) {
    
      var $this = $(this);
      var name = $this.attr("name");
      
      if (!name) {
      
        return false;
      }
      
      var $checkbox = $("input[name=" + name + "]", $propertyList).not($this);
      
      // If a known tag was entered, select that one
      if ($checkbox.length > 0) {
        
        if (!$checkbox.attr("checked")) {
        
          $checkbox.click();
        }
      } else {
      
        var $label = $("<label/>");
        var $checkbox = $("<input/>");
        
        $checkbox[0].setAttribute("type", "checkbox");
        $checkbox.attr({ name: name, checked: true });
        
        $label.append($checkbox);
        $label.append(name);
        
        $("<li/>").append($label).insertBefore($this.parent());
      }
      
      // Reset values
      $this.attr("name", "");
      $otherField.val("");
    
      return false;
    });
    $otherField.attr({ placeholder: "Andere", size: 8 })
               .keyup(function() {
                
                 $otherCheckbox.attr({
                   name: $(this).val(),
                   checked: false
                 });
               });
    
    $propertyList.append($("<li/>").append($otherCheckbox)
                                   .append($otherField))
                 .delegate("input[type=checkbox]", "click",
                   onPropertyCheckboxClick)
    
    $properties.attr("id", "properties")
               .append($("<label/>").text("Eigenschaften:"))
               .append($propertyList);
    
    $dialog.addClass("dialog settings")
           .append($titleText)
           .append($sorting)
           .append($properties)
           .hide();
    
    $(settings).bind("optionSet", onOptionSet);
  }).call(this);
  
  /*
   * Creates an item for property selection
   */
  function createItem(name, property, selected) {
  
    var $label = $("<label/>");
    var $checkbox = $("<input/>", {
      name: name,
      checked: selected
    });
    $checkbox[0].setAttribute("type", "checkbox");
    
    $label.append($checkbox);
    $label.append(property.title);
    
    return $("<li/>").append($label).hide();
  }
  
  /*
   * Stores the requested sorting order
   */
  function onSortingButtonClick(e) {
  
    settings.set("view.sortingOrder", $(e.target).data("order"));
  }
  
  /*
   * Stores the requested visible properties
   */
  function onPropertyCheckboxClick(e) {
  
    var $checkbox = $(e.target);
    var property = $checkbox.attr("name");
    
    if (!property) {
    
      return;
    }
    
    var propertyVisible = $checkbox.attr("checked");
    var visibleProperties = settings.get("view.visibleProperties");
    var index = visibleProperties.indexOf(property);
    
    if (propertyVisible && index < 0) {
    
      // Add property if not yet selected
      visibleProperties.push(property);
    } else if (index > -1) {
    
      // Remove property if not selected
      visibleProperties.splice(index, 1);
    }
    
    settings.set("view.visibleProperties", visibleProperties);
  }
  
  /**
   * Visually reflects option changes
   */
  function onOptionSet(e) {
  
    if (e.option == "view.sortingOrder") {
    
      $("button", $dialog).removeClass("active")
                          .filter(function() {
                            return $(this).data("order") == e.value;
                          })
                          .addClass("active");
    }
  }
  
  /**
   * Adds new lines to the property selection area
   */
  function onProviderAdded(e) {
  
    var visibleProperties = settings.get("view.visibleProperties");
    var $item = createItem(
      e.providerName, e.provider,
      visibleProperties.indexOf(e.providerName) > -1
    );
  
    $("ul li:last", $properties).before($item.slideDown());
  }
  
  /**
   * Removes lines from the property selection area
   */
  function onProviderRemoved(e) {
  
    $("input[name=" + e.providerName + "]", $properties) // input
      .parent() // label
      .parent() // li
      .slideUp(function() {
      
        $(this).remove()
      });
  }
  
  /*
   * Overridden for easier identification
   */
  dialog.toString = function() {
  
    return "[object SettingsDialog]";
  };
  
  return dialog;
}

/**
 * A message to prompt for credentials
 */
function CredentialsPrompt(manager) {

  var $this = $(this);
  var $description = $("<p/>", {"class": "description"});
  var $userField = $("<input/>", {"class": "user"});
  var $passwordField = $("<input/>", {"class": "password"});
  
  (function() {
  
    $userField[0].setAttribute("type", "text");
    $userField.attr({
                placeholder: "Nutzername",
                required: true,
                autofocus: true
              });
    $passwordField[0].setAttribute("type", "password");
    $passwordField.attr({
                    placeholder: "Passwort",
                    required: true
                  });
  
    $(manager).bind({
      credentialsRequest: onCredentialsRequest.bind(this)
    });
  }).call(this);
  
  /**
   * Triggers the default message response
   */
  function onFieldKeyDown(e) {
  
    if (e.which == KeyEvent.DOM_VK_RETURN) {
    
      var responseEvent = new jQuery.Event("response");
      responseEvent.response = Response.OK;
      
      $(e.data.promptMessage).trigger(responseEvent);
    
      $(this).unbind(e);
    }
  }
  
  /*
   * Prompts the user for credentials
   */
  function onCredentialsRequest(e) {
  
    var description = "Für den Zugriff auf <strong>" + e.source +
                      "</strong> ist eine Identifikation erforderlich.";

    switch (e.request.type) {

      case MediaRequest.LIST:
        description = "Zum Auflisten der Ressourcen von <strong>" + e.source +
                      "</strong> ist eine Identifikation erforderlich.";
        break;

      case MediaRequest.ADD:
        description = "Zum Hinzufügen von Ressourcen zu <strong>" + e.source +
                      "</strong> ist eine Identifikation erforderlich.";
        break;

      case MediaRequest.REMOVE:
        description = "Zum Entfernen von Ressourcen von <strong>" + e.source +
                      "</strong> ist eine Identifikation erforderlich.";
        break;
    }
    
    var $description = $("<p/>", {"class": "description", html: description})
                .after($("<p/>").append($userField))
                .after($("<p/>").append($passwordField));
    
    var promptMessage = new Message(
      "Identifikation erforderlich",
      $description,
      Message.QUESTION
    );
    
    $userField.keydown({promptMessage: promptMessage}, onFieldKeyDown);
    $passwordField.keydown({promptMessage: promptMessage}, onFieldKeyDown);
    $(e.request).one("cancelled", function() {
    
      var responseEvent = new jQuery.Event("response");
      responseEvent.response = Response.CANCEL;
      
      $(promptMessage).trigger(responseEvent);
    });
    
    $(promptMessage).one("response", function(re) {
    
      if (re.response == Response.OK) {
      
        var username = $userField.val();
        var password = $passwordField.val();
        
        if (password) {
        
          password = SHA1(password);
        }
        
        e.callback(new Credentials(username, password));
        
        username = password = null;
      }
      
      $userField.unbind().val("");
      $passwordField.unbind().val("");
      
      $(promptMessage).unbind();
      messages.removeMessage(promptMessage);
    });
    
    messages.addMessage(promptMessage);
    
    e.stopImmediatePropagation();
  }
}

/*
 * A graphical interface for notifications and actions
 */
function MessageView($list) {

  // Types of messages to filter
  this.filter = ["debug"];

  // Change execution context to this class
  $(messages).bind({
    messageAdded: (function(e) {
  
      // Ignore filtered message types
      if (this.filter.join(",").indexOf(e.message.type) > -1) {
      
        return;
      }

      this.addMessage(e.message);
    }).bind(this),
    messageRemoved: (function(e) {
    
      this.removeMessage(e.message);
    }).bind(this)
  });

  /*
   * Creates a list item from a item description
   */
  function createItem(message) {
  
    var $item = $("<li/>");
    var $text = $("<section/>");
    var $actions = $("<section/>");
    var $title = $("<h1/>", {html: message.title});
    var $description = message.text;

    if ($description.html && ($description.html() == null)) {

      $description = $("<p/>", {text: message.text});
    }
    
    $item.data("message", message)
         .addClass("message").addClass(message.type);
    $text.append($title, $description)
         .addClass("text");
    $actions.addClass("actions");
    
    for (var action in message.actions) {
    
      var $action = $("<button/>", {"class": "action"});
      var text = "";
      
      if (action.toUpperCase() in Action) {
      
        $action.addClass(action);
      }
      
      $action.bind("click", {"response": message.actions[action]}, function(e) {
        
        var responseEvent = new jQuery.Event("response");
        responseEvent.response = e.data.response;

        $(message).trigger(responseEvent);
        
        $("button", $actions).attr("disabled", true);
        
        messages.removeMessage(message);
      });
      
      switch (action) {
      
        case Action.CLOSE:
        
          text = "Schließen";
          break;
      
        case Action.OK:
        
          text = "OK";
          break;
      
        case Action.CANCEL:
        
          text = "Abbrechen";
          break;
      
        case Action.APPLY:
        
          text = "Anwenden";
          break;
          
        case Action.RETRY:
        
          text = "Wiederholen";
          break;
          
        default:
        
          text = action;
          break;
      }
      
      if (action == Action.CLOSE) {
      
        $action.attr("title", "Schließen");
      }
      
      $action.text(text);
      $actions.append($action);
    }
    
    $item.append($text);
    
    if (message.type == Message.PROGRESS) {
    
      var $progress = $("<progress/>");
      var $progressText = $("<label/>");
      var $fallbackProgress = $("<span/>");
      var $fallbackProgressBar = $("<span/>");
      
      try { var progress = message.progress.get() * 100; }
      catch (e) { var progress = message.progress * 100; }
      
      $progressText.addClass("progress")
                   .text("wird durchgeführt …");
      $fallbackProgress.addClass("fallback")
                       .append($fallbackProgressBar);
      $fallbackProgressBar.addClass("bar")
                          .css("width", progress + "%");
      
      $progress.attr("max", 100)
               .attr("value", progress)
               .addClass("unknown") // Initially unknown length
               .append($fallbackProgress);
      
      $item.append($progress)
           .append($progressText);
      
      $(message).bind({
        progress: function(e) {
        
          var progress = e.progress * 100;
        
          // Length now is most likely computable
          $progress.removeClass("unknown");
          $progress.attr("value", progress);
          $fallbackProgressBar.css("width", progress + "%");
          $progressText.text(parseInt(progress) + "%");
        },
        "done cancelled": function(e) {
        
          $fallbackProgressBar.css("width", "100%");
          $progressText.text("100%");
          $("button", $actions).attr("disabled", true);
          // Remove after 2 seconds
          $item.delay(2000).slideUp("slow", function() {
          
            $item.remove();
          });
        }
      });
    }
    
    $item.append($actions);
    $item.hide();
    
    return $item;
  }

  /*
   * Adds a new item to the list of messages
   * and prepares automatic hiding if appropriate
   */
  this.addMessage = function(message) {
  
    var $item = createItem(message);
    
    if (message.type == Message.QUESTION) {
    
      $list.prepend($item);
    } else {
    
      $list.append($item);
    }
    
    $item.slideDown("fast")
  };
  
  this.removeMessage = function(message) {
  
    $("li", $list).filter(function() {
                  
                    return $(this).data("message") == message;
                  })
                  .slideUp("slow", function() {
                  
                    $(this).remove();
                  });
  };
}

/*
 * A graphical viewer for resources
 */
function Viewer(manager, resourceView) {

  var $this = $(this);
  var _fullScreen = false;
  
  var genericResourceFormatter = new ResourceFormatter();
  
  var $overlay = $("<div/>");
  var $viewer = $("<div/>");
  var $controls = $("<section/>");
  var $previousButton = $("<button/>"),
      $nextButton = $("<button/>"),
      $fullScreenButton = $("<button/>");
  
  (function() {
  
    var properties = {
      fullScreen: {
        get: function() { return _fullScreen; },
        set: function(fullScreen) {

          if (fullScreen != _fullScreen) {

            _fullScreen = fullScreen;

            var fullScreenToggledEvent = new jQuery.Event("fullScreenToggled");
            fullScreenToggledEvent.fullScreen = fullScreen;

            $this.trigger(fullScreenToggledEvent);
          }
        }
      }
    };
    
    try {
    
      Object.defineProperties(this, properties);
    } catch (e) {
    
      for (var property in properties) {
      
        this[property] = properties[property];
      }
    }
  
    $previousButton.append($("<span/>").text("Vorheriges"))
                   .addClass("action previous")
                   .attr("title", "Vorheriges (p)")
                   .click(onPreviousButtonClick);
    $nextButton.append($("<span/>").text("Nächstes"))
               .addClass("action next")
               .attr("title", "Nächstes (n)")
               .click(onNextButtonClick);
    $fullScreenButton.append($("<span/>").text("Vollbild"))
                     .addClass("action fullscreen")
                     .attr("title", "Vollbild (f)")
                     .click(onFullScreenButtonClick);
    $controls.addClass("controls")
             .append($previousButton)
             .append($nextButton)
             .append($fullScreenButton)
             .appendTo($viewer);
    
    // Dummy resource container
    $viewer.append($("<span/>").addClass("resource"))
           .click(onViewerClick)
           .attr("id", "viewer");
           
    $overlay.append($viewer)
            .addClass("overlay")
            .hide();
    $(document.body).append($overlay);

    $(manager).bind({
      sourceSelected: onSourceSelected,
      resourceSelected: onResourceSelected
    });
    $(document).keydown(onKeyDown);
    
    $this.bind({
      fullScreenToggled: onFullScreenToggled
    });
  }).call(this);
  
  /*
   * Selects the previous resource
   */
  function onPreviousButtonClick(e) {
  
    $viewer.addClass("switching previous");
  
    try {
    
      var resources = resourceView.resources.get();
      var i = resources.indexOf(manager.selectedResource.get()) - 1;
    } catch (e) {
    
      var resources = resourceView.resources;
      var i = resources.indexOf(manager.selectedResource) - 1;
    }
    
    if (i < 0) {
    
      i = resources.length + i;
    }
    
    try { manager.selectedResource.set(resources[i]); }
    catch (e) { manager.selectedResource = resources[i]; }
    
    return false;
  }
  
  /*
   * Selects the next resource
   */
  function onNextButtonClick(e) {
  
    $viewer.addClass("switching next");
  
    try {
    
      var resources = resourceView.resources.get();
      var i = resources.indexOf(manager.selectedResource.get()) + 1;
    } catch (e) {
    
      var resources = resourceView.resources;
      var i = resources.indexOf(manager.selectedResource) + 1;
    }
    
    
    i %= resources.length;
    
    try { manager.selectedResource.set(resources[i]); }
    catch (e) { manager.selectedResource = resources[i]; }
    
    return false;
  }
  
  /*
   * Toggles between fullscreen and regular view
   */
  function onFullScreenButtonClick(e) {

    try { $this[0].fullScreen.set(!$this[0].fullScreen.get()); }
    catch (e) { $this[0].fullScreen = !$this[0].fullScreen; }
    
    return false;
  }
  
  /*
   * Hides the viewer
   */
  function onViewerClick(e) {
  
    try { manager.selectedResource.set(null); }
    catch (e) { manager.selectedResource = null; }
  }
  
  /*
   * Reverts state to default
   */
  function onSourceSelected(e) {
  
    $viewer.removeClass("fullscreen");
  }
  
  /*
   * Shows or hides the viewer according
   * to the selection of resources
   */
  function onResourceSelected(e) {
  
    if (e.resource) {
    
      var type = e.resource.properties.type;
      var formatter = providers.getProvider("resourceFormatters", type) ||
                      // Try fallback to general type
                      providers.getProvider("resourceFormatters", type.split("/")[0]) ||
                      // Fall back to the generic formatter
                      genericResourceFormatter;
      
      var $item = formatter.format(e.resource);
      // Take fixed sizes into account
      var enlarge = $item.data("enlarge");
      
      if (!enlarge) {
      
        try { $this[0].fullScreen.set(false); }
        catch (e) { $this[0].fullScreen = false; }
      }
      $fullScreenButton.attr("disabled", !enlarge);
      
      // Remove possible previous controls
      $("*", $controls).not($previousButton)
                       .not($nextButton)
                       .not($fullScreenButton)
                       .remove();
      
      // Add additional controls
      var controls = $item.data("controls") || [];
      
      controls.forEach(function(control) {
      
        $controls.append(control);
      });
      
      $item.addClass("resource");
      // Prevent closing the viewer upon click on the resource
      $item.bind("click", function(e) { e.stopPropagation(); });

      $overlay.fadeIn("fast");
      $item.hide().appendTo($viewer).fadeIn("fast", function() {
      
        $viewer.addClass("active");
        $item.trigger("added");
         
        $(".resource:first", $viewer).fadeOut("fast", function() {
        
          $(this).remove()
                 .trigger("removed");
          $viewer.removeClass("switching previous next");
        });
      });
    } else {
    
      $(".resource", $viewer).trigger("removed");
      $viewer.removeClass("active");
      $overlay.fadeOut("fast");
    }
  }
  
  /*
   * Handles keyboard interaction
   */
  function onKeyDown(e) {
  
    try { var selectedResource = manager.selectedResource.get(); }
    catch (e) { var selectedResource = manager.selectedResource; }

    if (!selectedResource) {
    
      return;
    }
  
    switch (e.which) {
        
      case KeyEvent.DOM_VK_LEFT:
      case KeyEvent.DOM_VK_P:
      
        $previousButton.click();
        break;
    
      case KeyEvent.DOM_VK_RIGHT:
      case KeyEvent.DOM_VK_N:
      
        $nextButton.click();
        break;
        
      case KeyEvent.DOM_VK_F:
      
        try { $this[0].fullScreen.set(!$this[0].fullScreen.get()); }
        catch (e) { $this[0].fullScreen = !$this[0].fullScreen; }
        break;
    
      case KeyEvent.DOM_VK_ESCAPE:
      case KeyEvent.DOM_VK_Q:
      case KeyEvent.DOM_VK_X:
      
        try { manager.selectedResource.set(null); }
        catch (e) { manager.selectedResource = null; }
        break;
    }
  }
  
  /*
   * Shows the current resource in fullscreen or regular view
   */
  function onFullScreenToggled(e) {
  
    $viewer.toggleClass("fullscreen");
    $fullScreenButton.toggleClass("fullscreen")
                     .toggleClass("restore");
  }
}

/*
 * A management wrapper responsible for the
 * graphical addition of sources
 */
function SourceInput(manager, $container) {

  var $field = $("<input/>");
  var $button = $("<button/>", {"class": "action add"});
  
  (function() {
  
    // Make sure to get the value, not the reference
    var location = (window.location + "").replace("/html5/", "");
  
    $field[0].setAttribute("type", "url");
    $field.attr("placeholder", "Adresse einer Quelle eingeben");
    $field.autocomplete([
      location + "/ringo/",
      location + "/ichigo/",
      "http://www.apps-entwickler.de/_brodala/mikan/",
      "ws://dtm:1111/suika",
    ]);
    $button.attr("title", "Quelle hinzufügen");
    $button.html("&nbsp;");
    
    $field.keypress(onKeyPress);
    $button.click(onClick);
    
    $container.append($field);
    $container.append($button);
    
    $(manager).bind({
      sourceAdded: onSourceAdded
    });
    
    $(messages).bind({
      messageAdded: onMessageAdded,
      messageRemoved: onMessageRemoved
    });
  })();
  
  /**
   * Displays messages in place
   */
  function displayMessage(message) {
  
    $field.addClass(message.type);
    $button.attr("title", $("<b/>").html(message.text).text());
    $button.addClass(message.type);
  }
  
  /**
   * Clears the displayed message
   */
  function clearMessage() {
  
    $field.val("");
    $field.removeClass();
    $button.removeClass().addClass("action add");
  }
  
  /*
   * Triggers the default action
   */
  function onKeyPress(e) {
  
    $field.removeClass();
    $button.removeClass().addClass("action add");
    $button.attr("title", "Quelle hinzufügen");
  
    if (e.which == KeyEvent.DOM_VK_RETURN) {
    
      $button.click();
      
      return false;
    }
  }
  
  /*
   * Requests the addition of the entered source
   */
  function onClick(e) {
  
    if ($field.val()) {
    
      manager.addSource($field.val());
    }
  }
  
  /*
   * Clears the input field
   */
  function onSourceAdded(e) {
  
    clearMessage();
  }
  
  /*
   * Displays messages in place
   */
  function onMessageAdded(e) {
  
    if (e.message.context == onClick) {
    
      displayMessage(e.message);
    }
  }
  
  /*
   * Hides previously displayed messages
   */
  function onMessageRemoved(e) {
  
    if (e.message.context == onClick) {
    
      clearMessage();
    }
  }
}

/*
 * A management wrapper responsible for the
 * graphical interaction with the source list
 */
function SourceListView(manager, $list) {

  // Human readable descriptions of the request types
  var restrictedTypeLabels = {};
  restrictedTypeLabels[MediaRequest.LIST] = "Auflistung beschränkt";
  restrictedTypeLabels[MediaRequest.ADD] = "Hinzufügen beschränkt";
  restrictedTypeLabels[MediaRequest.REMOVE] = "Entfernen beschränkt";
  
  (function() {
  
    DropTarget.call(this, manager, $list);
  
    $(manager).bind({
      sourceAdded: onSourceAdded,
      sourceRemoved: onSourceRemoved,
      sourceSelected: onSourceSelected,
      resourceAdded: onResourceAddedOrRemoved,
      resourceRemoved: onResourceAddedOrRemoved,
      credentialsStored: onCredentialsStored
    });
         
    // Initialize from possibly existing sources
    manager.sources.forEach(function(source) {
    
      var sourceAddedEvent = new jQuery.Event("sourceAdded");
      sourceAddedEvent.source = source;

      onSourceAdded(sourceAddedEvent);
    });
  })();
  
  /*
   * Creates a list item from a source
   */
  function createItem(source) {

    var $item = $("<li/>", {"class": "source"});
    var $restrictions = $("<ul/>");
    var $closeButton = $("<button/>", {
      "class": "action close",
      click: onCloseButtonClicked
    });
    
    $item.bind({
           dragover: onDragOver,
           dragenter: onDragEnter,
           dragleave: onDragLeave,
           click: onItemClicked
         })
         .append($("<strong/>", {"class": "title", text: source.properties.title}))
         .append($("<ul/>", {"class": "data"})
                  .append($("<li/>", {"class": "url", text: source.url}))
                  .append($("<li/>", {"class": "count"})
                          .append($("<span/>", {"class": "number", text: 0}))
                          .append(" Ressourcen")
                         )
                )
         .attr("title", source.url)
         .data("source", source);
    
    $restrictions.addClass("restrictions");
    $restrictions.attr("title", "Beschränkungen");
    
    for (var type in source.restricted) {
    
      if (source.credentials[type]) {
      
        continue;
      }
      
      var $restriction = $("<li/>", {"class": "type"});
      
      $restriction.addClass(type)
                  .attr("data-type", type);
      
      if (type in restrictedTypeLabels) {
      
        type = restrictedTypeLabels[type];
      }
      
      $restriction.text(type)
                  .attr("title", type)
                  .appendTo($restrictions);
    }
    
    if ($restrictions.children().length > 0) {
    
      $item.append($restrictions);
    }
    
    $closeButton.attr("title", "Schließen");
    $item.append($closeButton);
    $item.hide();
    
    try { var sourceSelected = (source == manager.selectedSource.get()); }
    catch (e) { var sourceSelected = (source == manager.selectedSource); }
    
    if (sourceSelected) {
    
      $item.addClass("selected");
    }
    
    return $item;
  }
  
  /*
   * Changes the selected source
   */
  function onItemClicked(e) {
  
    var $item = $(this);
    
    try { manager.selectedSource.set($item.data("source")); }
    catch (e) { manager.selectedSource = $item.data("source"); }
  }
  
  /*
   * Removes a source
   */
  function onCloseButtonClicked(e) {
  
    var $item = $(this).parents(".source");
    
    manager.removeSource($item.data("source"));
    
    return false;
  }
  
  /*
   * Indicates possible drop
   */
  function onDragOver(e) {

    var $item = $(this);
    var targetSource = $item.data("source");
    
    // Indicate impossible action
    if (!targetSource || !(MediaRequest.ADD in targetSource.allowed)) {
    
      return true;
    }
    
    var urls = e.originalEvent.dataTransfer.getData("URL");
    
    if (urls) {
      
      urls = urls.split(",");
      
      for (var i = 0; i < urls.length; ++i) {
      
        var resource = manager.findResource(urls[i]);
        
        // Indicate impossible upload to the same source
        if (resource && (resource.source == targetSource)) {
        
          return true;
        }
      }
    }
  
    $item.addClass("dragover");
    
    return false;
  }
  
  /*
   * Changes the selected source during drag operations
   */
  function onDragEnter(e) {
  
    var $item = $(this);
  
    // Switch to hovered source after 1 second
    $item.delay(1000).queue(function() {
    
      try { manager.selectedSource.set($item.data("source")); }
      catch (e) { manager.selectedSource = $item.data("source"); }
      
      $item.dequeue();
    });
  }
  
  /*
   * Cancels source selection changes
   * and undos drop indication
   */
  function onDragLeave(e) {
  
    var $item = $(this);
    
    $item.clearQueue();
    $item.removeClass("dragover");
    
    return false;
  }
  
  /**
   * Adds new items for newly added sources
   */
  function onSourceAdded(e) {
  
    var $existingItems = $(".source", $list).filter(function() {
    
      if ($(this).data("source") == e.source) {
        
        return true;
      }
    });
    
    if ($existingItems.length == 0) {
    
      var $item = createItem(e.source);
      // Add item to the collection for sorting
      var $items = $(".source", $list).add($item).sort(function(a, b) {
      
        return $(a).text().localeCompare($(b).text());
      });
      
      // Try to find the predecessor
      var previousIndex = $items.index($item) - 1;
      
      if (previousIndex < 0) { // No predecessor: empty or beginning
      
        $list.prepend($item);
      } else { // Predecessor: middle or end
      
        $items.eq(previousIndex).after($item);
      }
      
      $item.slideDown("slow");
    }
  }
  
  /**
   * Removes items after source removal
   */
  function onSourceRemoved(e) {
  
    $(".source", $list).filter(function() {
    
      if ($(this).data("source") == e.source) {
        
        return true;
      }
    }).slideUp("fast", function() {
    
      $(this).remove();
    });
  }
  
  /**
   * Updates the selection marker
   */
  function onSourceSelected(e) {
  
    var $items = $(".source", $list);
    
    $items.removeClass("selected")
          .filter(function(i, item) {
          
            return $(item).data("source") == e.source;
          })
          .addClass("selected");
  }
  
  /**
   * Updates the resource count for sources
   */
  function onResourceAddedOrRemoved(e) {
  
    var source = e.resource.source;
    var $item = $(".source", $list).filter(function() {
    
      return $(this).data("source") == source;
    });
    
    if ($item.length == 1) {
    
      $(".count .number", $item)
        .text(source.resources.length)
        .addClass("changed")
        .addClass((e.type == "resourceAdded" ? "added" : "removed"))
        .stop(true, true)
        .delay(3000).queue(function() {
        
          $(this).removeClass("changed added removed")
                 .dequeue();
        });
    }
  }
  
  /**
   * Updates the restriction indicators
   */
  function onCredentialsStored(e) {
  
    var $item = $(".source", $list).filter(function() {
    
      return $(this).data("source") == e.source;
    });
    
    var $restriction = $("*[data-type=" + e.requestType + "]", $item);
    var $restrictions = $restriction.parent();
    
    $restriction.fadeOut("slow", function() {
    
      $restriction.remove();
    
      if ($restrictions.children().length == 0) {
      
        $restrictions.fadeOut("slow", function() {
        
          $restrictions.remove();
        });
      }
    });
  }
}

/*
 * A management wrapper responsible for
 * the interaction with the resource list
 *
 * TODO: Keyboard navigation
 */
function ResourceListView(manager, $list) {

  /*************
   * Properties
   *************/
  var $this = $(this);
  var _resources = [];
  var thumbnailPropertyFormatter = new ThumbnailPropertyFormatter();
  var genericPropertyFormatter = new PropertyFormatter();
  
  (function() {
  
    var properties = {
      resources: { get: function() { return _resources; } }
    };
    
    try {
    
      Object.defineProperties(this, properties);
    } catch (e) {
    
      for (var property in properties) {
      
        this[property] = properties[property];
      }
    }
  }).call(this);

  var $selectionCanvas = $("<canvas/>", {"class": "selection"});
  /*@cc_on
  if (!$selectionCanvas[0].getContext) {
  
    G_vmlCanvasManager.initElement($selectionCanvas[0]);
  }
  @*/
  var context = $selectionCanvas[0].getContext("2d");
  
  /*****************
   * Initialization
   *****************/
  (function() {
  
    DropTarget.call(this, manager, $list);
  
    // For rubberband selection
    $selectionCanvas.css({
                      position: "absolute"/*@cc_on @if (@_jscript_version < 9),
                      // IE 8 and less does not handle mousemove on unfilled elements
                      background: "white",
                      filter: "Alpha(opacity=0)" @end @*/
                    })
                    .hide();
    $(document.body).append($selectionCanvas);
    
    /***********
     * Bindings
     ***********/
    $(manager).bind({
      sourceSelected: onSourceSelected.bind(this),
      resourceAdded: onResourceAdded.bind(this),
      resourceRemoved: onResourceRemoved.bind(this)
    });
    
    $(settings).bind({
      optionSet: onOptionSet.bind(this)
    });
    
    $selectionCanvas.bind({
      mousemove: onCanvasMouseMove,
      mouseleave: onCanvasMouseLeave,
      mouseup: onCanvasMouseUp
    });
    
    // Initialize from possibly selected source
    //onSourceSelected(manager.selectedSource);
    
    $list.bind({
      mousedown: onMouseDown,
      dragstart: onDragStart,
      /*@cc_on
      dragend: onDragEnd,
      @*/
      dragover: onDragOver,
      dragleave: onDragLeave
    });
    
    $list.delegate("li", "click", onItemClick);
    $list.delegate("li", "dblclick", onItemDoubleClick);
  }).call(this);
  
  /**
   * Adds a resource to the view
   */
  this.addResource = function(resource) {
  
    try { var isSelectedSource = (resource.source == manager.selectedSource.get()); }
    catch (ex) { var isSelectedSource = (resource.source == manager.selectedSource); }

    if (!isSelectedSource) {
    
      return;
    }
  
    var $item = createItem(resource);
    var $items = $("li", $list);
    
    // Add item to the collection for sorting
    $items = $items.add($item.hide());
    
    $items.sort(function(a, b) {
    
      var sortingProperty = settings.get("view.sortingProperty");
      var sortingOrder = settings.get("view.sortingOrder");
      var $propertyA = $("span[data-property=" + sortingProperty + "]", a);
      var $propertyB = $("span[data-property=" + sortingProperty + "]", b);
      var textA = $propertyA.text();
      var textB = $propertyB.text();
      var valueA = $propertyA.data("value");
      var valueB = $propertyB.data("value");
      var formatter = providers.getProvider("propertyFormatters", sortingProperty) ||
                      genericPropertyFormatter;
      
      return formatter.sort($propertyA, $propertyB, sortingOrder);
    });
    
    // Try to find the predecessor
    var previousIndex = $items.index($item) - 1;
    
    if (previousIndex < 0) { // No predecessor: empty or beginning
    
      $list.prepend($item);
    } else { // Predecessor: middle or end
    
      $items.eq(previousIndex).after($item);
    }
    
    _resources = [];
    
    $items.each(function() {
    
      _resources.push($(this).data("resource"));
    });
    
    return $item;
  };
  
  /**
   * Removes a resource from the view
   */
  this.removeResource = function(resource) {
  
    var $item = $("li", $list).filter(function() {
    
      return $(this).data("resource") == e.resource;
    }).addClass("changed removed")
      .fadeOut("fast", function() {
      
        $(this).remove();
      });
      
    return $item;
  };
  
  /*
   * Creates a list item from a resource
   */
  function createItem(resource) {
  
    var $item = $("<li/>");
    var $figure = $("<figure/>");
    var $caption = $("<figcaption/>");
    var $thumb = thumbnailPropertyFormatter.format(
      resource, resource.properties.thumbnail);
    
    $item.data("resource", resource);
    $item.attr("draggable", true);
    $item.attr("data-downloadurl",
      resource.properties.type + ":" +
      resource.fullURL.substring(resource.fullURL.lastIndexOf('/') + 1) + ":" +
      resource.fullURL
    );
    
    $figure.append($thumb);
    
    var visibleProperties = settings.get("view.visibleProperties");
    
    for (var i = 0; i < visibleProperties.length; ++i) {

      var property = visibleProperties[i];
      var value = resource.properties[property];
      var $propertyLine = $("<span/>");
      var formatter = providers.getProvider("propertyFormatters", property) ||
                      genericPropertyFormatter;
      
      $propertyLine.attr("data-property", property);
      $propertyLine.attr("data-value", value);
      
      $propertyLine.append(formatter.format(resource, value));
      $caption.append($propertyLine);
    }
    
    $figure.append($caption);
    /*@cc_on
    // Only links and images can be dragged in IE
    var $a = $("<a/>", {
      click: onItemClick,
      focus: function() { this.blur(); },
      href: "#",
      css: {
        color: "inherit",
        cursor: "default",
        display: "block",
        textDecoration: "none"
      }
    });
    
    $figure = $a.append($figure);
    
    // Suppress border of linked images added by IE
    $thumb.css("border", "none");
    @*/
    $item.append($figure);
    
    return $item;
  }
  
  /*
   * Starts the rubberband selection
   */
  function onMouseDown(e) {
  
    // Only handle clicks on the list itself
    if ($(e.target).filter($list).length > 0) {

      var listPosition = $list.offset();

      // Update canvas data
      $selectionCanvas.attr({
                         width: $selectionCanvas.width(),
                         height: $selectionCanvas.height()
                       })
                       // Store the starting location
                       .data("startLocation", {x: e.pageX, y: e.pageY})
                       .show();

      return false;
    }
  }
  
  /*
   * Selects or unselects items
   */
  function onItemClick(e) {
  
    // Without meta key allow only
    // one item to be selected
    if (!e.metaKey) {
    
      $("li", $list).removeClass("selected");
    }
  
    // Select the clicked item
    $("li", $list).has(e.target).toggleClass("selected");
    
    return false;
  }
  
  /*
   * Starts the viewer with the selected item
   */
  function onItemDoubleClick(e) {
  
    // Do nothing in selection mode
    if (!e.metaKey) {
    
      var $item = $("li", $list).has(e.target);
      
      try { manager.selectedResource.set($item.data("resource")); }
      catch (e) { manager.selectedResource = $item.data("resource"); }
    }
  }
  
  /*
   * Draws the rubberband selection and
   * updates the selection state of items
   */
  function onCanvasMouseMove(e) {
  
    var $items = $("li", $list);
    
    // Keep selection if meta key is being held
    if (!e.metaKey) {
    
      $items.removeClass("selected");
    }
    
    var listOffset = $list.offset();
    var startLocation = $selectionCanvas.data("startLocation");
    var selectionRectangle = new Rectangle(
      startLocation.x + 0.5,
      startLocation.y + 0.5,
      e.pageX - startLocation.x,
      e.pageY - startLocation.y
    );
    var fillStyle = context.createLinearGradient(
      selectionRectangle.left,
      selectionRectangle.top,
      selectionRectangle.left,
      $selectionCanvas.height()
    );
    fillStyle.addColorStop(0, "rgba(255, 255, 255, 0.2)");
    fillStyle.addColorStop(1, "rgba(255, 255, 255, 1)");
    
    context.clearRect(0, 0, $selectionCanvas.width(),
      $selectionCanvas.height());
    context.fillStyle = fillStyle;//"rgba(255, 255, 255, 0.5)";
    context.fillRect(
      selectionRectangle.left - 0.5,
      selectionRectangle.top,
      selectionRectangle.width,
      selectionRectangle.height
    );
    context.strokeStyle = "rgba(0, 0, 0, 0.8)";
    context.strokeRect(
      selectionRectangle.left - 0.5,
      selectionRectangle.top,
      selectionRectangle.width,
      selectionRectangle.height
    );
    
    // Select the intersected items
    $items.filter(function() {
      
      var $item = $(this);
      var itemOffset = $item.offset();
      var itemRectangle = new Rectangle(
        itemOffset.left, itemOffset.top,
        $item.outerWidth(), $item.outerHeight()
      );
      
      return itemRectangle.intersects(selectionRectangle);
    }).addClass("selected");
  }
  
  /*
   * Triggers selection clearing
   */
  function onCanvasMouseLeave(e) {
  
    $selectionCanvas.mouseup();
  }
  
  /*
   * Clears the selection and hides the rubber band
   */
  function onCanvasMouseUp(e) {
  
    var startLocation = $selectionCanvas.data("startLocation");
    
    // Clear selection only on simple clicks
    // and without meta key being held
    if (e.pageX == startLocation.x &&
        e.pageY == startLocation.y &&
        !e.metaKey) {
        
      $("li", $list).removeClass("selected");
    }
    
    $selectionCanvas.hide();
  }
  
  /*
   * Prepares drag data for internal drops
   */
  function onDragStart(e) {
  
    var $item = $(e.target)/*@cc_on .parent() @*/;
    var urls = [];
    var titles = [];
    
    // Keep selection if meta key is being held
    if (!e.metaKey) {
    
      var $selectedItems = $("li.selected", $list).filter(function(i, item) {
      
        return item == e.target;
      });
    
      if ($selectedItems.length == 0) {
    
        $("li", $list).removeClass("selected");
      }
    }
    
    $item.addClass("selected");
    
    var $selectedItems = $("li.selected", $list);
    
    /*@cc_on
    if ($selectedItems.length == 0) {
    
      // Adding the "selected" class is not immediately reflected
      // in IE, thus manually pick the currently dragged element
      $selectedItems = $item.parent(); // $item is the link, thus get parent
    }
    @*/
    
    $selectedItems.each(function() {
    
      var resource = $(this).data("resource");
      
      urls.push(resource.fullURL);
      titles.push(resource.properties.title);
    });
    
    var dataTransfer = e.originalEvent.dataTransfer;

    if (dataTransfer.addElement) {
    
      $selectedItems.each(function() {
      
        dataTransfer.addElement(this);
      });
    } else if (dataTransfer.setDragImage) { // Not available in IE
    
      dataTransfer.setDragImage(e.target, 0, 0);
    }/*@cc_on else {
    
      $selectedItems.each(function() {
      
        // Offset of the original item since the drag happened on this one
        var offset = $(this).offset();
        // Fully re-create item, avoids hierarchy ($.clone()) and
        // namespace (node.cloneNode()) messup
        var $dragItem = createItem($(this).data("resource"));
        
        // Calculate offset for proper drag image movement
        offset.left = e.pageX - offset.left;
        offset.top = e.pageY - offset.top;
        
        // Store offset for later movement
        $dragItem.data("offset", offset);
        $dragItem.addClass("dragimage");
        $dragItem.css({
          position: "absolute",
          visibility: "hidden"
        });
        $dragItem.appendTo($list);
      });
    }@*/
    
    dataTransfer.effectAllowed = "copyMove";

    // Format "URL" instead of "text/uri-list" due to IE, also
    // data can only be a string, not an arbitrary object
    dataTransfer.setData("URL", urls + "");
    dataTransfer.setData("Text", titles + "");
    
    /*if ($selectedItems.length > 0) {
    
      // Store download url
      try {
      
        dataTransfer.setData("DownloadURL",
          $selectedItems.first().data("downloadurl"));
      } catch(e) {}
    }*/
    
    return true;
  }
  
  /*
   * Removes the drag image indicators
   */
  /*@cc_on
  function onDragEnd(e) {
  
    $(".dragimage", $list).remove();
  }
  @*/
  
  /*
   * Indicates possible drop
   */
  function onDragOver(e) {
    
    var targetSource = $list.data("source");
    
    // Indicate impossible action
    if (!(MediaRequest.ADD in targetSource.allowed)) {
    
      return true;
    }
    
    var urls = e.originalEvent.dataTransfer.getData("URL");
    
    if (urls) {
    
      urls = urls.split(",");
      
      for (var i = 0; i < urls.length; ++i) {
      
        var resource = manager.findResource(urls[i]);
        
        // Indicate impossible upload to the same source
        if (resource && (resource.source == targetSource)) {
        
          return true;
        }
      }
    }
  
    /*@cc_on
    $(".dragimage", $list).each(function() {
    
      var offset = $(this).data("offset");
    
      // Adjust position by original offset
      $(this).css({
        left: e.pageX - offset.left,
        top: e.pageY - offset.top,
        visibility: "visible"
      });
    });
    @*/
    
    // FIXME: What is this?
    /*if ($(".dragimage", $list).has(e.target)) {
    
      return true;
    }*/
  
    $list.addClass("dragover");
    
    return false;
  }
  
  /*
   * Undos drop indication
   */
  function onDragLeave(e) {
  
    $list.removeClass("dragover");
    
    return false;
  }
  
  /*
   * Updates the list of displayed resources
   */
  function onSourceSelected(e) {
  
    $list.empty();
    $list.data("source", e.source);
  
    if (e.source) {

      e.source.resources.forEach((function(resource) {

        this.addResource(resource).fadeIn("fast");
      }).bind(this));
    }
  }
  
  /**
   * Adds a new item to the list of displayed resources
   */
  function onResourceAdded(e) {
  
    var $item = this.addResource(e.resource);
    
    if ($item) {
    
      $item.addClass("changed added")
           .fadeIn("slow", function() {
            
              $item.delay(1500).queue(function() {
              
                 $item.removeClass("changed added")
                      .dequeue();
               });
            });
    }
  }
  
  /**
   * Removes an item from the list of displayed resources
   */
  function onResourceRemoved(e) {
  
    this.removeResource(e.resource);
  }
  
  /**
   * Updates appearance after option changes
   */
  function onOptionSet(e) {
  
    switch (e.option) {
    
      case "view.visibleProperties":
      case "view.sortingProperty":
      case "view.sortingOrder":
        
        $list.empty();
        
        try { var source = manager.selectedSource.get(); }
        catch (e) { var source = manager.selectedSource; }

        source.resources.forEach((function(resource) {

          this.addResource(resource).fadeIn("fast");
        }).bind(this));
        break;
    }
  }
}

/*
 * A widget which draws a row of images
 * and allows for selecting a rating
 */
function RatingWidget(rating, maximum) {

  var $this = $("<canvas/>");
  var canvas = $this[0];
  /*@cc_on if (!canvas.getContext) { G_vmlCanvasManager.initElement(canvas); } @*/
  var context = canvas.getContext("2d");
  var _rating = rating || 0, // [0..1]
      _maximum = maximum || 5;
  var ratingImage = new Image();
      ratingImage.onload = function() {
      
        canvas.width = this.width * _maximum;
        canvas.height = this.height;
        
        inactiveImageData = getInactiveImageData();
        
        drawRatingImages();
      };
      ratingImage.src = "images/star.png";
  var inactiveImageData = null;
  var _highlighted = false;
      
  (function() {
  
    var properties = {
      rating: {
        get: function() { return _rating; },
        set: function(rating) {
        
          // Allow for toggling the rating
          if ((rating * _maximum).toFixed() == (_rating * _maximum).toFixed()) {
          
            rating = 0;
          }
        
          if (rating != _rating) {
          
            _rating = rating;
            $this.text((_rating * _maximum) + "/" + _maximum);
            
            var ratingChangedEvent = new jQuery.Event("ratingChanged");
            ratingChangedEvent.rating = rating;

            $this.trigger(ratingChangedEvent);
          }
        }
      },
      maximum: {
        get: function() { return _maximum; },
        set: function(maximum) {
        
          if (maximum != _maximum) {
          
            _maximum = maximum;
            $this.text((_rating * _maximum) + "/" + _maximum);
            
            var maximumChangedEvent = new jQuery.Event("maximumChanged");
            maximumChangedEvent.maximum = maximum;

            $this.trigger(maximumChangedEvent);
          }
        }
      },
      ratingImageURL: {
        get: function() { return ratingImage.src; },
        set: function(url) {
        
          if (url != ratingImage.src) {
          
            ratingImage.src = url;
            
            $this.trigger("ratingImageChanged");
          }
        }
      }
    };
    
    try {
    
      Object.defineProperties(this, properties);
    } catch (e) {
    
      for (var property in properties) {
      
        this[property] = properties[property];
      }
    }
    
    $(this).bind({
      "mouseover": function(e) {
      
        _highlighted = true;
      },
      "mousemove click": function(e) {
        
        var offset = $(e.target).offset();
        var threshold = (ratingImage.width / _maximum);// / 2;
        var x = e.pageX - offset.left;
        var newRating = (x + threshold) / canvas.width;
        
        if (e.type == "click") {
        
          try { this.rating.set(newRating); }
          catch (e) { this.rating = newRating; }
        } else {
        
          drawRatingImages(newRating);
        }
      },
      "mouseout": function() {
      
        _highlighted = false;
        drawRatingImages();
      },
      "ratingChanged maximumChanged ratingImageChanged": function(e) {
      
        drawRatingImages();
      }
    });
  }).call(canvas);
  
  /*
   * Generates the image data required
   * to draw the inactive rating images
   */
  function getInactiveImageData() {
  
    // No support in e.g. Explorer Canvas
    if (!context.getImageData) {
    
      return null;
    }
  
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.globalAlpha = 0.3;
    context.drawImage(ratingImage, 0, 0);
    
    var imageData = context.getImageData(0, 0,
      ratingImage.width, ratingImage.height);
    
    context.clearRect(0, 0, ratingImage.width, ratingImage.height);
      
    for (var i = 0; i < imageData.data.length; i += 4) {
    
      // Luminance formula by Marco Lisci
      // (http://spyrestudios.com/html5-canvas-image-effects-black-white/)
      var average = imageData.data[i + 0] * 0.30 + // R
                    imageData.data[i + 1] * 0.59 + // G
                    imageData.data[i + 2] * 0.11;  // B
                    
      imageData.data[i + 0] = average;
      imageData.data[i + 1] = average;
      imageData.data[i + 2] = average;
    }
    
    return imageData;
  }
      
  /*
   * Draws the rating images based on the current rating
   */
  function drawRatingImages(rating) {
  
    var rating = ((rating || _rating) * _maximum).toFixed();
  
    context.clearRect(0, 0, canvas.width, canvas.height);
    
    if (_highlighted) {
    
      context.shadowOffsetX = 1;
      context.shadowOffsetY = 1;
      context.shadowBlur = 1;
      context.shadowColor = "black";
    }
    
    // Draw the active rating images
    context.globalAlpha = 1;
    
    for (var i = 0; i < rating; ++i) {
    
      context.drawImage(ratingImage, i * ratingImage.width, 0);
    }
    
    if (_highlighted) {
    
      context.shadowOffsetX = 0;
      context.shadowOffsetY = 0;
      context.shadowBlur = 0;
      context.shadowColor = null;
    }
    
    // Draw the inactive rating images
    if (inactiveImageData) {
    
      for (var i = rating; i < _maximum; ++i) {
      
        context.putImageData(inactiveImageData, i * ratingImage.width, 0);
      }
    } else {
    
      context.globalAlpha = 0.3;
      
      for (var i = rating; i < _maximum; ++i) {
    
        context.drawImage(ratingImage, i * ratingImage.width, 0);
      }
    }
  }
  
  /*
   * Overridden for easier identification
   */
  canvas.toString = function() {
  
    return "[object RatingWidget]";
  };
  
  return canvas;
}

/**
 * A widget which indicates progress and allows for seeking
 */
function ProgressSeekWidget(player) {

  var $seekProgress = $("<span/>", {"class": "progress unknown"});
  var $seekProgressBar = $("<span/>", {"class": "bar"});
  var $seeker = $("<span/>", {"class": "seeker"});
  var $seekerText = $("<span/>", {"class": "text"}).appendTo($seeker);
  var seekProgress = $seekProgress[0];
  var _progress = 0;
  var _seeking = false;
  var durationFormatter = new DurationFormatter();
  
  (function() {
  
    var properties = {
      progress: {
        get: function() { return _progress; },
        set: function(progress) {
        
          if (progress != _progress) {
          
            _progress = progress;
            
            var progressChangedEvent = new jQuery.Event("progressChanged");
            progressChangedEvent.progress = progress;

            $seekProgress.trigger(progressChangedEvent);
          }
        }
      },
      seeking: {
        get: function() { return _seeking; },
        set: function(seeking) {
        
          if (seeking != _seeking) {
          
            _seeking = seeking;
            
            if (seeking) {
            
              $seekProgress.trigger("seekingStarted");
            } else {
            
              $seekProgress.trigger("seekingEnded");
            }
          }
        }
      }
    };
    
    try {
    
      Object.defineProperties(this, properties);
    } catch (e) {
    
      for (var property in properties) {
      
        this[property] = properties[property];
      }
    }
    
    update();
                    
    $seekProgress.append($seekProgressBar)
                 .append($seeker)
                 .mousedown(function(e) {
                 
                   try { this.seeking.set(true); }
                   catch (e) { this.seeking = true; }
                   
                   return false;
                 });
                 
    $seekProgress.bind({
      progressChanged: function() { update(); },
      seekingStarted: function() { $seekProgress.addClass("seeking"); },
      seekingEnded: function() { $seekProgress.removeClass("seeking"); }
    });

    $(player).bind({
      timeupdate: function() {
      
        $seekProgress.removeClass("unknown");
        
        try { var seeking = seekProgress.seeking.get(); }
        catch (e) { var seeking = seekProgress.seeking; }
        
        if (!seeking) {
        
          _progress = this.currentTime / this.duration;
          update();
        }
      },
      ended: function() {
      
        _progress = 0;
        update();
      }
    });
    
    $(document.body).bind({
      mousemove: function(e) {
        
        try { var seeking = seekProgress.seeking.get(); }
        catch (e) { var seeking = seekProgress.seeking; }
     
        if (seeking) {
        
          var position = e.pageX - $seekProgress.offset().left;
        
          // Clamp to minimum and maximum values
          position = Math.max(0, position);
          position = Math.min(position, $seekProgress.width());
        
          if (isNaN(position)) { // Just in case
          
            position = 0;
          }
        
          $seekProgressBar.css("width", position + "px");
          $seekerText.text(durationFormatter.format(player.duration * (position / $seekProgress.width())));
          
          return false;
        }
      },
      mouseup: function(e) {
        
        try { var seeking = seekProgress.seeking.get(); }
        catch (e) { var seeking = seekProgress.seeking; }

        if (seeking) {
        
          try { seekProgress.seeking.set(false); }
          catch (e) { seekProgress.seeking = false; }
       
          var position = e.pageX - $seekProgress.offset().left;
         
          // Clamp to minimum and maximum values
          position = Math.max(0, position);
          position = Math.min(position, $seekProgress.width());
         
          if (isNaN(position)) { // Just in case
         
            position = 0;
          }
      
          var progress = position / $seekProgress.width();
        
          try { seekProgress.progress.set(progress); }
          catch (e) { seekProgress.progress = progress; }
         
          return false;
        }
      }
    });
  }).call(seekProgress);
  
  /**
   * Updates the appearance of this widget
   */
  function update() {
  
    $seekProgressBar.css("width", (_progress * 100) + "%");
  }
  
  /*
   * Overridden for easier identification
   */
  seekProgress.toString = function() {
  
    return "[object ProgressSeekWidget]";
  };
  
  return seekProgress;
}

/**
 * A widget which draws a volume indicator
 */
function VolumeWidget(volume) {

  var $this = $("<canvas/>");
  var canvas = $this[0];
  canvas.height = 20;
  canvas.width = 60;
  /*@cc_on if (!canvas.getContext) { G_vmlCanvasManager.initElement(canvas); } @*/
  var context = canvas.getContext("2d");
  var gradient = context.createLinearGradient(0, 0, canvas.width, 0);
  gradient.addColorStop(0, "rgba(255, 255, 255, 1)");
  gradient.addColorStop(1, "rgba(255, 255, 255, 0)");
  var _volume = volume || 1; // [0..1]
  
  (function() {
  
    update(volume);
  
    var properties = {
      volume: {
        get: function() { return _volume; },
        set: function(volume) {
        
          if (volume != _volume) {
          
            _volume = volume;
            
            var volumeChangedEvent = new jQuery.Event("volumeChanged");
            volumeChangedEvent.volume = volume;

            $this.trigger(volumeChangedEvent);
          }
        }
      }
    };
    
    try {
    
      Object.defineProperties(this, properties);
    } catch (e) {
    
      for (var property in properties) {
      
        this[property] = properties[property];
      }
    }
    
    $this.bind({
      mousedown: function() {
      
        $this.addClass("changing");
      },
      volumeChanged: function(e) {
      
        update(e.volume);
      }
    });
    
    $(document.body).bind({
      mousemove: function(e) {
          
        if ($this.hasClass("changing")) {
        
          var offset = $this.offset();
          var position = (e.pageX - offset.left);
          position = Math.max(0, position);
          position = Math.min(position, $this.width());
          var volume = position / $this.width();
          
          update(volume);
        }
      },
      mouseup: function(e) {
          
        if ($this.hasClass("changing")) {
        
          $this.removeClass("changing");
        
          var offset = $this.offset();
          var position = (e.pageX - offset.left);
          position = Math.max(0, position);
          position = Math.min(position, $this.width());
          var volume = position / $this.width();
          
          try { canvas.volume.set(volume); }
          catch (e) { canvas.volume = volume; }
        }
      }
    });
  }).call(canvas);
  
  /**
   * Updates the widget appearance based on the given volume
   */
  function update(volume) {
  
    $this.attr("title", "Lautstärke: " + parseInt(volume * 100) + "%");
    drawVolume(volume);
  }
  
  /**
   * Draws the indicator based on the given volume
   */
  function drawVolume(volume) {
        
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.fillStyle = gradient;
    context.beginPath();
    context.moveTo(0, canvas.height);
    // y = mx [skip +n since it's 0]
    var offset = (canvas.height / canvas.width) * (canvas.width * volume);
    context.lineTo(canvas.width * volume, canvas.height);
    context.lineTo(canvas.width * volume, canvas.height - offset);
    context.closePath();
    context.fill();
  }
  
  /*
   * Overridden for easier identification
   */
  canvas.toString = function() {
  
    return "[object VolumeWidget]";
  };
  
  return canvas;
}
