/**
 * The central instance responsible
 * for handling all interaction
 */
function MediaManager() {

  var $this = $(this);
  
  // List of managed sources
  this.sources = [];
  // The currently selected source
  var _selectedSource = null;
  // The currently selected resource
  var _selectedResource = null;
  
  // Prepare getters/setters
  (function() {
  
    var properties = {
      selectedSource: {
        get: function() { return _selectedSource; },
        set: function(source) {
        
          if (source !== _selectedSource) {
          
            _selectedSource = source;

            var sourceSelectedEvent = new jQuery.Event("sourceSelected");
            sourceSelectedEvent.source = source;
            
            $this.trigger(sourceSelectedEvent);
            settings.set("selectedSourceURL", (source ? source.url : ""));
          }
        }
      },
      selectedResource: {
        get: function() { return _selectedResource; },
        set: function(resource) {
        
          if (resource != _selectedResource) {
          
            _selectedResource = resource;

            var resourceSelectedEvent = new jQuery.Event("resourceSelected");
            resourceSelectedEvent.resource = resource;
            
            $this.trigger(resourceSelectedEvent);
          }
        }
      }
    };
    
    try {
    
      // Internet Explorer 8 only supports this for DOM objects
      Object.defineProperties(this, properties);
    } catch (e) {
    
      // Internet Explorer < 9 does not support proper getters
      // and setters for arbitrary objects, thus fallback
      for (var property in properties) {
      
        this[property] = properties[property];
      }
    }
    
    $this.bind({
      sourceAdded: onSourceAdded.bind(this),
      sourceRemoved: onSourceRemoved.bind(this),
      sourceSelected: onSourceSelected.bind(this)
    });
    
    // Stop handling of connection events
    // before refreshing or leaving the page
    $(window).bind({
      beforeunload: (function() {
    
        this.sources.forEach(function(source) {
        
          $(source.connection).unbind();
        });
      }).bind(this)
    });
  }).call(this);
  
  /**
   * Adds a source from an URL
   */
  this.addSource = function(url) {

    if (!url) {

      return;
    }
  
    var caller = this.addSource.caller;
    
    function sameURL(source) {
    
      return url == source.url;
    }
    
    if (this.sources.some(sameURL)) {
        
      messages.addWarning(
        "Quelle nicht hinzugefügt",
        "Die Quelle <strong>" + url + "</strong> " +
        "wurde bereits hinzugefügt."
      );
      return;
    }

    var connection = new Connection(url);

    $(connection).bind({
      opened: onConnectionOpened.bind(this),
      closed: onConnectionClosed.bind(this),
      response: onConnectionResponse.bind(this),
      error: onConnectionError.bind(this)
    });

    connection.open();
  };
  
  /*
   * Removes either a literal source or from an URL
   */
  this.removeSource = function(source) {
    
    var transaction = new Transaction();
    var caller = this.removeSource.caller;
    var source = this.findSource(source) || source;
    
    if (source === null) {

      messages.addWarning(
        "Quelle nicht entfernt",
        "Die Quelle <strong>" + source + "</strong> " +
        "ist nicht vorhanden.",
        caller
      );
      
      try { transaction.status.set(Transaction.FAILED); }
      catch (e) { transaction.status = Transaction.FAILED; };

      return;
    }
    
    $(source.connection).unbind();
    source.connection.close();
    
    this.sources.splice(this.sources.indexOf(source), 1);

    var sourceRemovedEvent = new jQuery.Event("sourceRemoved");
    sourceRemovedEvent.source = source;
    
    $this.trigger(sourceRemovedEvent);
    
    messages.addInfo(
      "Quelle entfernt",
      "Die Quelle <strong>" + source + "</strong> wurde entfernt.",
      caller
    );
    
    try { transaction.status.set(Transaction.DONE); }
    catch (e) { transaction.status = Transaction.DONE; }

    try { var isCurrentSource = (source == this.selectedSource.get()); }
    catch (e) { var isCurrentSource = (source == this.selectedSource); }

    if (isCurrentSource) {

      // Try to get the next source
      source = this.sources[this.sources.length - 1] || null;

      try { this.selectedSource.set(source); }
      catch (e) { this.selectedSource = source; }
    }
    
    return transaction;
  };
  
  /**
   * Adds a resource or file to a given source
   */
  this.addResource = function(resource, source) {
  
    return this.addResources([resource], source);
  };  
  
  /**
   * Adds resources or files to a given source
   */
  this.addResources = function(resources, source) {
  
    var transaction = new Transaction();
    var caller = this.addResources.caller;
    
    if (!(MediaRequest.ADD in source.allowed)) {
      
      messages.addError(
        "Hinzufügen fehlgeschlagen",
        "Das Hinzufügen von Ressourcen zu <strong>" +
        source +
        "</strong> ist nicht möglich.",
        caller
      );
      
      try { transaction.status.set(Transaction.FAILED); }
      catch (e) { transaction.status = Transaction.FAILED; }
      
      return transaction;
    }

    var request = new MediaRequest(MediaRequest.ADD);
    
    function isFile(resource) { return (resource instanceof File); }
    function isResource(resource) { return (resource instanceof Resource); }
    
    // Not directly invoking resources.filter() since resources can be of type FileList
    var files = Array.prototype.filter.call(resources, isFile);
    var resources = Array.prototype.filter.call(resources, isResource);
    
    if (files.length > 0) {
    
      request.files = files;
    }
    
    if (resources.length > 0) {
    
      request.resources = {};
      
      // Map [{url1: properties}, {url2: properties}]
      // to {url1: properties, url2: properties}
      resources.forEach(function(resource) {
      
        request.resources[resource.fullURL] = resource.properties;
      });
    }

    var progressMessage = new Message(
      "Ressourcen hinzufügen",
      "Die Resourcen werden zu <strong>" + source + "</strong> hinzugefügt",
      Message.PROGRESS,
      caller
    );
    
    $(progressMessage).one({response: function(e) {
    
      if (e.response == Response.CANCEL) {
      
        request.cancel();
      }
    }});
    
    $(request).bind({progress: function(e) {
    
      try { progressMessage.progress.set(e.progress); }
      catch (ex) { progressMessage.progress = e.progress; }
    }});
    
    messages.addMessage(progressMessage);

    if (source.requiresCredentials(MediaRequest.ADD)) {

      var credentialsRequestEvent = new jQuery.Event("credentialsRequest");
      credentialsRequestEvent.request = request;
      credentialsRequestEvent.source = source;
      credentialsRequestEvent.callback = function(credentials) {

        if (credentials) {

          request.credentials = credentials;
        }

        source.connection.send(request);
      };

      $this.bind("credentialsRequest", onCredentialsRequest)
           .trigger(credentialsRequestEvent)
           .unbind("credentialsRequest", onCredentialsRequest);
    } else {
    
      if (request.type in source.credentials) {

        request.credentials = source.credentials[request.type];
      }
      
      source.connection.send(request);
    }
    
    return transaction;
  };
  
  /*
   * Removes a resource from its source
   */
  this.removeResource = function(resource) {
  
    return this.removeResources([resource]);
  };
  
  /*
   * Removes resources from their sources
   */
  this.removeResources = function(resources) {
  
    var transaction = new Transaction();
    var caller = this.removeResource.caller;
    var removals = {};
    
    // Build the mapping from sources and the resources to remove from them
    resources.forEach(function(resource) {

      var source = resource.source;
    
      if (!(source in removals)) {

        removals[source] = [];
      }

      removals[source].push(resource);
    });
    
    function removalForbidden(source) {
    
      return !(MediaRequest.REMOVE in source.allowed);
    }
    
    // Check whether at least one source does not allow removal
    var sourcesWithForbiddenRemoval = removals.filter(removalForbidden);
    
    if (sourcesWithForbiddenRemoval.length > 0) {
    
      var lastSource = sourcesWithForbiddenRemoval.pop();
      var sourcesText = sourcesWithForbiddenRemoval.join(", ") +
                        (lastSource ? "und " + lastSource : "");
    
      messages.addError(
        "Entfernen fehlgeschlagen",
        "Das Entfernen von Ressourcen auf <strong>" +
        sourcesText +
        "</strong> ist nicht möglich.",
        caller
      );
      
      try { transaction.status.set(Transaction.FAILED); }
      catch (e) { transaction.status = Transaction.FAILED; }
      
      return transaction;
    }

    // Now remove the resources from their sources
    Object.keys(removals).forEach(function(source) {

      var request = new MediaRequest(MediaRequest.REMOVE);
      request.resources = removals[source];

      if (source.requiresCredentials(MediaRequest.REMOVE)) {

        var credentialsRequestEvent = new jQuery.Event("credentialsRequest");
        credentialsRequestEvent.request = request;
        credentialsRequestEvent.source = source;
        credentialsRequestEvent.callback = function(credentials) {

          if (credentials) {

            request.credentials = credentials;
          }

          source.connection.send(request);
        };

        $this.bind("credentialsRequest", onCredentialsRequest)
             .trigger(credentialsRequestEvent)
             .unbind("credentialsRequest", onCredentialsRequest);
      } else {
    
        if (request.type in source.credentials) {

          request.credentials = source.credentials[request.type];
        }

        source.connection.send(request);
      }
    });
    
    return transaction;
  };
  
  /*
   * Moves a resource from its source to another source
   * and ensures the success of the operation
   */
  this.moveResource = function(resource, targetSource) {
  
    return this.moveResources([resource], targetSource);
  };
  
  /*
   * Moves resources from their sources to another
   * source and ensures the success of the operations
   */
  this.moveResources = function(resources, targetSource) {
  
    var transaction = new Transaction();
    var addTransaction = this.addResources(resources, targetSource);
    
    $(addTransaction).one("statusChanged", (function(e, status) {
    
      if (status == Transaction.DONE) {
      
        this.removeResources(resources);
      }
      
      try { transaction.status.set(status); }
      catch (e) { transaction.status = status; }
    }).bind(this));
    
    return transaction;
  };

  /**
   * Tries to find a source based on a given URL
   */
  this.findSource = function(url) {

    var sources = this.sources.filter(function(source) {

      return source.url == url;
    });

    return (sources.length == 1 ? sources[0] : null);
  };
  
  /**
   * Tries to find a resource based on a given URL
   */
  this.findResource = function(url) {
  
    var resources = this.findResources([url]);
    
    return (resources.length == 1 ? resources[0] : null);
  };
  
  /**
   * Tries to find all resources based on given URLs
   */
  this.findResources = function(urls) {
  
    var resources = [];
    var urls = "\0" + urls.join("\0");
    
    this.sources.forEach(function(source) {
    
      source.resources.forEach(function(resource) {
      
        if (urls.indexOf("\0" + resource.fullURL) > -1) {
        
          resources.push(resource);
        }
      });
    });
    
    return resources;
  };
  
  /**
   * Retrieves the resources for a source and selects
   * the newly added source if none is selected yet
   */
  function onSourceAdded(e) {
    
    var caller = onSourceAdded.caller;
      
    if (!(MediaRequest.LIST in e.source.allowed)) {
      
      messages.addError(
        "Ressourcen-Auflistung nicht möglich",
        "Die Ressourcen von <strong>" +
        e.source +
        "</strong> können nicht aufgelistet werden.",
        caller
      );
      
      return;
    }
    
    // Prevent multiple requests
    if (e.source.resources.length != 0) {
    
      return;
    }

    var source = e.source;
    var request = new MediaRequest(MediaRequest.LIST);

    if (source.requiresCredentials(MediaRequest.LIST)) {

      var credentialsRequestEvent = new jQuery.Event("credentialsRequest");
      credentialsRequestEvent.request = request;
      credentialsRequestEvent.source = source;
      credentialsRequestEvent.callback = function(credentials) {

        if (credentials) {

          request.credentials = credentials;
        }

        source.connection.send(request);
      };

      $this.bind("credentialsRequest", onCredentialsRequest)
           .trigger(credentialsRequestEvent)
           .unbind("credentialsRequest", onCredentialsRequest);

    } else {
    
      if (request.type in source.credentials) {

        request.credentials = source.credentials[request.type];
      }

      source.connection.send(request);
    }
  }
  
  /**
   * Stores source list changes
   */
  function onSourceRemoved(e) {
  
    settings.set("sourceURLs", this.sources);
  }
  
  /**
   * Updates the window title
   */
  function onSourceSelected(e) {
  
    var title = "Medienverwaltung";
    
    if (e.source !== null) {
  
      title += ": " + e.source.properties["title"];
    }
    
    document.title = title;
  }

  /**
   * Notifies about opened connections
   */
  function onConnectionOpened(e) {

    var source = this.findSource(e.connection.url) || e.connection.url;

    messages.addInfo(
      "Verbindung geöffnet",
      "Die Verbindung zu <strong>" + source + "</strong> wurde geöffnet."
    );

    // Request source info after the connection was opened successfully
    e.connection.send(new MediaRequest(MediaRequest.INFO));
  }

  /**
   * Notifies about closed connections
   */
  function onConnectionClosed(e) {

    var source = this.findSource(e.connection.url) || e.connection.url;
      
    messages.addWarning(
      "Verbindung geschlossen",
      "Die Verbindung zu <strong>" + source + "</strong> wurde geschlossen."
    );
  }

  /**
   * Takes different actions based on
   * messages received from sources
   */
  function onConnectionResponse(e) {

    if (e.response.type == MediaRequest.INFO) {

      var source = new Source(e.connection.url, e.response.info);
      source.connection = e.connection;
      
      this.sources.push(source);

      var sourceAddedEvent = new jQuery.Event("sourceAdded");
      sourceAddedEvent.source = source;
      
      $this.trigger(sourceAddedEvent);

      settings.set("sourceURLs", this.sources);
      
      messages.addInfo(
        "Quelle hinzugefügt",
        "Die Quelle <strong>" + source + "</strong> wurde hinzugefügt."
      );
    } else {
      
      var source = this.findSource(e.connection.url);

      switch (e.response.type) {

        case MediaRequest.LIST:
    
          source.resources = [];
          // Intentionally not breaking here

        case MediaRequest.LIST:
        case MediaRequest.ADD:

          Object.keys(e.response.resources).forEach(function(url) {
          
            var resource = new Resource(source, url, e.response.resources[url]);
            
            source.resources.push(resource);

            var resourceAddedEvent = new jQuery.Event("resourceAdded");
            resourceAddedEvent.resource = resource;

            $this.trigger(resourceAddedEvent);
          });

          var resourcesAddedEvent = new jQuery.Event("resourcesAdded");
          resourcesAddedEvent.source = source;
          
          $this.trigger(resourcesAddedEvent);
          break;

        case MediaRequest.REMOVE:
      
          var urls = response.resources.map(function(url) {
          
            // Replicate resource creation
            return (new Resource(source, url).fullURL);
          });
          var resources = this.findResources(urls);
          
          resources.forEach(function(resource) {
          
            source.resources.splice(source.resources.indexOf(resource), 1);

            var resourceRemovedEvent = new jQuery.Event("resourceRemoved");
            resourceRemovedEvent.resource = resource;

            $this.trigger(resourceRemovedEvent);
          
            messages.addInfo(
              "Entfernen erfolgreich",
              "Das Entfernen von <strong>" +
              resource.properties.title +
              "</strong> von <strong>" +
              source +
              "</strong> war erfolgreich."
            );
          });

          var resourceRemovedEvent = new jQuery.Event("resourcesRemoved");
          resourceRemovedEvent.source = source;
          
          $this.trigger(resourceRemovedEvent);
          break;
      }

      if (e.request && e.request.credentials &&
          !(e.request.type in source.credentials)) {

        source.credentials[e.request.type] = e.request.credentials;
        
        var credentialsStoredEvent = new jQuery.Event("credentialsStored");
        credentialsStoredEvent.source = source;
        credentialsStoredEvent.requestType = e.request.type;

        $this.trigger(credentialsStoredEvent);
      }
    }
  }

  /**
   * Notifies about connection errors
   */
  function onConnectionError(e) {

    var connection = e.connection;
    var source = this.findSource(connection.url) || connection.url;
    
    if (e.request) {
    
      var request = e.request;
      
      var actions = {};
      actions[Action.CLOSE] = Response.CLOSE;
      actions[Action.RETRY] = Response.RETRY;
      var retryMessage = new Message("", "", Message.WARNING, null, actions);
          
      $(retryMessage).bind({response: function(e) {
      
        if (e.response == Response.retry) {
        
          connection.send(request);
        }
      }});

      switch (e.request.type) {

        case MediaRequest.INFO:
          retryMessage.title = "Informationsabfrage fehlgeschlagen";
          retryMessage.text = "Fehler beim Abfragen der Informationen über " +
                              "<strong>" + source + "</strong>: " + e.message;
          break;

        case MediaRequest.LIST:
          retryMessage.title = "Ressourcen-Auflistung fehlgeschlagen";
          retryMessage.text = "Fehler beim Auflisten der Ressourcen von "+
                              "<strong>" + source + "</strong>: " + e.message;
          break;

        case MediaRequest.ADD:
          retryMessage.title = "Hinzufügen fehlgeschlagen";
          retryMessage.text = "Fehler beim Hinzufügen von Ressourcen zu " +
                              "<strong>" + source + "</strong>: " + e.message;
          break;

        case MediaRequest.REMOVE:
          retryMessage.title = "Entfernen fehlgeschlagen";
          retryMessage.text = "Fehler beim Entfernen von Ressourcen von "
                              "<strong>" + source + "</strong>: " + e.message;
          break;
      }
      
      messages.addMessage(retryMessage);
    } else {
    
      messages.addError(
        "Verbindung fehlgeschlagen",
        "Die Verbindung zu <strong>" + source + "</strong> " +
        "ist fehlgeschlagen: " + e.message
      );
    }
  }

  /**
   * Default handler for credential requests,
   * intended to signify prompt failure
   **/
  function onCredentialsRequest(e) {

    e.callback(null); // No credentials available
  }
  
  // Placed here to ensure all methods are available
  (function() {
  
    settings.setDefault("sourceURLs", []);
    
    var sourceURLs = settings.get("sourceURLs").slice(0);

    sourceURLs.forEach(function(sourceURL) {
    
      // Restore source and optionally selection
      this.addSource(sourceURL, (sourceURL == selectedSourceURL));
    }, this);
    
    var selectedSourceURL = settings.get("selectedSourceURL");
    
    // Wait for sources to be added before restoring the last selection
    $this.bind({
      sourceAdded: function(e) {
      
        if (e.source.url == selectedSourceURL) {
        
          try { this.selectedSource.set(e.source); }
          catch (ex) { this.selectedSource = e.source; }
          
          $this.unbind(e);
        }
      }
    });
    
    settings.setDefault("view.visibleProperties", ["title", "type", "size"]);
    settings.setDefault("view.sortingProperty", "title");
    settings.setDefault("view.sortingOrder", Sorting.ASCENDING);
  }).call(this);
  
  this.toString = function() { return "[object MediaManager]"; };
}
