package de.dtele.control {
  
  import de.dtele.control.events.*;
  import de.dtele.data.*;
  import de.dtele.messages.Message;
  import de.dtele.messages.MessageManager;
  import de.dtele.messages.MessageResponse;
  import de.dtele.messages.events.MessageResponseEvent;
  import de.dtele.net.Connection;
  import de.dtele.net.MediaRequest;
  import de.dtele.net.events.ConnectionErrorEvent;
  import de.dtele.net.events.ConnectionEvent;
  import de.dtele.net.protocol.HTTPProtocolHandler;
  import de.dtele.net.protocol.UnknownProtocolHandler;
  import de.dtele.net.protocol.WebSocketProtocolHandler;
  import de.dtele.providers.ProviderManager;
  import de.dtele.settings.SettingsManager;
  
  import flash.errors.IllegalOperationError;
  import flash.events.Event;
  import flash.events.EventDispatcher;
  import flash.events.ProgressEvent;
  import flash.net.FileReference;
  import flash.utils.Dictionary;
  import flash.utils.getQualifiedClassName;
  
  import mx.collections.ArrayCollection;
  import mx.collections.ListCollectionView;
  
  /**
   * Dispatched when the <code>selectedSource</code> property changes
   * 
   * @eventType de.dtele.control.events.SourceEvent.SELECTED
   */
  [Event("sourceSelected", type="de.dtele.control.events.SourceEvent")]
  
  /**
   * Dispatched when a source has been added
   * 
   * @eventType de.dtele.control.events.SourceEvent.ADDED
   */
  [Event("sourceAdded", type="de.dtele.control.events.SourceEvent")]
  
  /**
   * Dispatched when a source has been removed
   * 
   * @eventType de.dtele.control.events.SourceEvent.REMOVED
   */
  [Event("sourceRemoved", type="de.dtele.control.events.SourceEvent")]
  
  /**
   * Dispatched when the <code>selectedResource</code> property changes
   * 
   * @eventType de.dtele.control.events.ResourceEvent.SELECTED
   */
  [Event("resourceSelected", type="de.dtele.control.events.ResourceEvent")]
  
  /**
   * Dispatched when a resource has been added
   * 
   * @eventType de.dtele.control.events.ResourceEvent.ADDED
   */
  [Event("resourceAdded", type="de.dtele.control.events.ResourceEvent")]
  
  /**
   * Dispatched when a resource has been removed
   * 
   * @eventType de.dtele.control.events.ResourceEvent.REMOVED
   */
  [Event("resourceRemoved", type="de.dtele.control.events.ResourceEvent")]
  
  /**
   * Dispatched when a credentials for a request to a source are required
   * 
   * @eventType de.dtele.control.events.CredentialsEvent.REQUEST
   */
  [Event("credentialsRequest", type="de.dtele.control.events.CredentialsEvent")]

  /**
   * The central instance responsible
   * for handling all interaction
   * 
   * @author Mathias Brodala
   */
  public final class MediaManager extends EventDispatcher {
    
    /* Properties */
    private static var _instance:MediaManager;
    /**
     * The singleton instance of the media manager
     */
    public static function get instance():MediaManager {
      
      if (_instance == null) {
        
        _instance = new MediaManager();
      }
      
      return _instance;
    }
    
    private var _sources:ListCollectionView = new ArrayCollection();
    /**
     * The list of managed sources
     */
    [Bindable]public function get sources():ListCollectionView { return this._sources; };
    protected function set sources(sources:ListCollectionView):void { this._sources = sources; }
    
    private var _selectedSource:ISource;
    /**
     * The currently selected source 
     */
    [Bindable]public function get selectedSource():ISource { return this._selectedSource; }
    public function set selectedSource(source:ISource):void {
      
      if (source != this._selectedSource) {
        
        this._selectedSource = source;
        this.dispatchEvent(new SourceEvent(SourceEvent.SELECTED, source));
        
        SettingsManager.instance.set("selectedSourceURL", source.url);
      }
    }

    private var _selectedResource:IResource;    
    /**
     * The currently selected resource 
     */
    [Bindable]public function get selectedResource():IResource { return this._selectedResource; }
    public function set selectedResource(resource:IResource):void {
      
      if (resource != this._selectedResource) {
        
        this._selectedResource = resource;
        this.dispatchEvent(new ResourceEvent(ResourceEvent.SELECTED, resource));
      }
    }
    
    /**
     * URLs of sources currently being added
     */
    private var pending:Array = [];

    /* Methods */
    /**
     * Sets up the media manager
     */
    public function MediaManager() {
      
      if (_instance != null) {
        
        throw new IllegalOperationError("Cannot instantiate " + getQualifiedClassName(this) + ". " +
                                        "Use the singleton instance instead.");
      }
      
      ProviderManager.instance.addProvider("protocolHandlers", "__unknown__", UnknownProtocolHandler);
      ProviderManager.instance.addProvider("protocolHandlers", "http", HTTPProtocolHandler);
      ProviderManager.instance.addProvider("protocolHandlers", "ws", WebSocketProtocolHandler);
    
      this.addEventListener(SourceEvent.ADDED, this.onSourceAdded);
      this.addEventListener(SourceEvent.REMOVED, this.onSourceRemoved);
    }
    
    /**
     * Restores the state from stored settings
     */
    public function restoreState():void {
      
      // Restore selected source after sources have been added
      var selectedSourceURL:String = SettingsManager.instance.get("selectedSourceURL") as String;
      
      function restoreSelectedSource(e:SourceEvent):void {
        
        if (e.source.url == selectedSourceURL) {
          
          MediaManager.instance.selectedSource = e.source;
          MediaManager.instance.removeEventListener(SourceEvent.ADDED, restoreSelectedSource);
        }
      }
      
      this.addEventListener(SourceEvent.ADDED, restoreSelectedSource);
      
      // Restore the sources
      SettingsManager.instance.setDefault("sourceURLs", this.sources);
      
      for each (var url:String in SettingsManager.instance.get("sourceURLs")) {
        
        this.addSource(url);
      }
    }
    
    /**
     * Collects the URLs of all currently available sources
     * 
     * @return An array of URL strings
     */
    private function getSourceURLs():Array {
      
      return this.sources.toArray().map(function(source:ISource, ...a):String {
        
        return source.url;
      });
    }
    
    /**
     * Performs a MediaRequest to a source,
     * asking for credentials if required
     * 
     * @param request The MediaRequest
     * @param source The source to send the request to
     */
    private function sendAuthenticated(request:MediaRequest, source:ISource):void {
      
      if (source.requiresCredentials(request.type)) {
        
        this.dispatchEvent(new CredentialsEvent(
          CredentialsEvent.REQUEST,
          source,
          request,
          function(credentials:Credentials):void {
            
            if (credentials) {
              
              request.credentials = credentials;
            }
            
            source.connection.send(request);
          }
        ));
      } else {
        
        if (request.type in source.credentials) {
          
          request.credentials = source.credentials[request.type];
        }
        
        source.connection.send(request);
      }
    }
    
    /**
     * Adds a source from an URL
     * 
     * @param url The URL to add a source from
     */
    public function addSource(url:String):void {
      
      if (!url) {
        
        return;
      }
      
      if (this.pending.indexOf(url) > -1) {
        
        MessageManager.instance.addWarning(
          "Quelle wird hinzugefügt",
          "Die Quelle " + url + " wird gerade hinzugefügt."
        );
        
        return;
      }
      
      this.pending.push(url);
      
      function sameURL(source:Source, ...a):Boolean {
        
        return url == source.url;
      }
      
      trace("[MediaManager] Adding source from " + url);
      
      if (this.sources.toArray().some(sameURL)) {
        
        MessageManager.instance.addWarning(
          "Quelle nicht hinzugefügt",
          "Die Quelle " + url + " wurde bereits hinzugefügt."
        );
        
        return;
      }
      
      var connection:Connection = new Connection(url);
      
      connection.addEventListener(ConnectionEvent.OPENED, this.onConnectionOpen);
      connection.addEventListener(ConnectionEvent.CLOSED, this.onConnectionClose);
      connection.addEventListener(ConnectionEvent.RESPONSE, this.onConnectionResponse);
      connection.addEventListener(ConnectionErrorEvent.ERROR, this.onConnectionError);
      
      connection.open();
    }
    
    /**
     * Removes a source
     * 
     * @param source The source to remove
     */
    public function removeSource(source:ISource):void {
      
      trace("[MediaManager] Removing source " + source);
      
      source.connection.close();
    }
    
    /**
     * Adds a resource to a source
     * 
     * @param resource The resource to add
     * @param source The source to add the resource to
     */
    public function addResource(resource:Object, source:ISource):void {
      
      this.addResources([resource], source);
    }
    
    /**
     * Adds resources to a source
     * 
     * @param resources The resources to add
     * @param source The source to add the resources to
     */
    public function addResources(resources:Array, source:ISource):void {
      
      if (!(MediaRequest.ADD in source.allowed)) {
        
        MessageManager.instance.addError(
          "Hinzufügen fehlgeschlagen",
          "Das Hinzufügen von Ressourcen zu " + source + " ist nicht möglich."
        );
        
        return;
      }
      
      var request:MediaRequest = new MediaRequest(MediaRequest.ADD);
      
      resources.forEach(function(resource:Object, ...a):void {
        
        if (resource is FileReference) {
          
          request.files.push(resource as FileReference);
        } else if (resource is IResource) {
          
          request.resources[resource.url] = resource.properties;
        }
      });
      
      var progressMessage:Message = new Message(
        "Ressourcen hinzufügen",
        "Die Resourcen werden zu " + source + " hinzugefügt",
        Message.PROGRESS
      );
      
      // Set up listener for cancelling the request
      progressMessage.addEventListener(MessageResponseEvent.RESPONSE,
        function(e:MessageResponseEvent):void {
          
          if (e.response.type == MessageResponse.CANCEL) {
            
            request.cancel();
          }
        }
      );
      
      MessageManager.instance.addMessage(progressMessage);
      
      request.addEventListener(ProgressEvent.PROGRESS, function(e:ProgressEvent):void {
        
        progressMessage.dispatchEvent(e);
      });
      
      request.addEventListener(Event.COMPLETE, function(e:Event):void {
        
        progressMessage.dispatchEvent(e);
      });
      
      request.addEventListener(Event.CANCEL, function(e:Event):void {
        
        progressMessage.dispatchEvent(e);
      });
      
      this.sendAuthenticated(request, source);
    }
    
    /**
     * Removes a resource from its source
     * 
     * @param resource The resource to remove
     */
    public function removeResource(resource:IResource):void {
      
      this.removeResources(Vector.<IResource>([resource]));
    }
    
    /**
     * Removes resources from their sources
     * 
     * @param resources The resources to remove
     */
    public function removeResources(resources:Vector.<IResource>):void {
      
      var removals:Dictionary = new Dictionary();
      
      // Map [resource1, resource2, ...] to {source1: [resource1, ...], source2: [...], ...}
      for each (var resource:IResource in resources) {
        
        if (!(MediaRequest.REMOVE in resource.source)) {
          
          MessageManager.instance.addError(
            "Entfernen fehlgeschlagen",
            "Das Entfernen von Ressourcen von einer der Quellen ist nicht möglich."
          );
          
          return;
        }
        
        if (!(resource.source in removals)) {
          
          removals[resource.source] = [];
        }
        
        removals[resource.source].push(resource);
      };
      
      for (var source:Object in removals) {
        
        var request:MediaRequest = new MediaRequest(MediaRequest.REMOVE);
        request.resources = removals[source];
        
        this.sendAuthenticated(request, source as ISource);
      }
    }
    
    /**
     * Moves a resource from its source to another source
     * 
     * @param resource The resource to move
     * @param targetSource The source to move the resource to
     */
    public function moveResource(resource:IResource, targetSource:ISource):void {}
    
    /**
     * Moves resources from their sources to another source
     * 
     * @param resources The resources to move
     * @param targetSource The source to move the resources to
     */
    public function moveResources(resources:Vector.<IResource>, targetSource:ISource):void {}
    
    /**
     * Tries to find a source based on a given URL
     * 
     * @param url The URL to find a source for
     * @return A source or null
     */
    public function findSource(url:String):ISource {
      
      for each (var source:ISource in this.sources) {
        
        if (source.url == url) {
          
          return source;
        }
      }
      
      return null;
    }
    
    /**
     * Tries to find a resource based on a given URL
     * 
     * @param url The URL to find a resource for
     * @return A resource or null
     */ 
    public function findResource(url:String):IResource {
      
      var resources:Vector.<IResource> = this.findResources(Vector.<String>([url]));
      
      return (resources.length == 1 ? resources[0] : null);
    }
    
    /**
     * Tries to find a resource based on given URLs
     * 
     * @param urls The URLs to find resources for
     * @return A list of resources, empty if none where found
     */ 
    public function findResources(urls:Vector.<String>):Vector.<IResource> {
      
      var resources:Vector.<IResource> = new Vector.<IResource>();
      var resourceURLs:String = "\0" + urls.join("\0");
      
      for each (var source:ISource in this.sources) {
        
        for each (var resource:IResource in source.resources) {
          
          if (resourceURLs.indexOf("\0" + resource.url) > -1) {
            
            resources.push(resource);
          }
        }
      };
      
      return resources;
    }
    
    /**
     * Requests the list of resources of a source,
     * selects the newly added source if possible
     * and persists the sources
     */
    private function onSourceAdded(e:SourceEvent):void {
      
      this.pending.splice(this.pending.indexOf(e.source.url), 1);
      
      if (!this.selectedSource) {
        
        this.selectedSource = e.source;
      }
      
      if (!(MediaRequest.LIST in e.source.allowed)) {
        
        MessageManager.instance.addError(
          "Ressourcen-Auflistung nicht möglich",
          "Die Ressourcen von " +
          e.source.properties.title +
          "können nicht aufgelistet werden."
        );
        return;
      }
      
      var request:MediaRequest = new MediaRequest(MediaRequest.LIST);
      
      this.sendAuthenticated(request, e.source);
      
      SettingsManager.instance.set("sourceURLs", this.getSourceURLs());
    }
    
    /**
     * Persists the source URLs
     */
    private function onSourceRemoved(e:SourceEvent):void {
      
      SettingsManager.instance.set("sourceURLs", this.getSourceURLs());
    }
    
    /**
     * Requests the info data of a newly connected source
     */
    private function onConnectionOpen(e:ConnectionEvent):void {
      
      var connection:Connection = e.target as Connection;
      var source:String = this.findSource(connection.url) as String || connection.url;
      
      trace("[MediaManager] Connection to " +  source + " opened");
      
      connection.send(new MediaRequest(MediaRequest.INFO));
    }
    
    /**
     * Removes a disconnected source and selects the
     * last added source if possible
     */
    private function onConnectionClose(e:ConnectionEvent):void {
      
      var connection:Connection = e.target as Connection;
      var source:ISource = this.findSource(connection.url);
      
      trace("[MediaManager] Connection to " +  source + " closed");
      
      MessageManager.instance.addWarning(
        "Verbindung geschlossen",
        "Die Verbindung zu " + (source as String || connection.url) + " wurde geschlossen."
      );
      
      if (source) {
      
        this.sources.removeItemAt(this.sources.getItemIndex(source));
        
        this.dispatchEvent(new SourceEvent(SourceEvent.REMOVED, source));
        
        // If this was the current source, try to select the next one
        if ((source == this.selectedSource) && this.sources.length > 0) {
          
          this.selectedSource = this.sources.getItemAt(this.sources.length - 1) as ISource;
        } else {
          
          this.selectedSource = null;
        }
      }
    }
    
    private function onConnectionResponse(e:ConnectionEvent):void {
      
      trace("[MediaManager] Got response with result " + e.response.result);
      var connection:Connection = e.target as Connection;
      
      if (e.response.type == MediaRequest.INFO) {
        
        var newSource:ISource = new Source(connection.url, e.response.info);
        newSource.connection = connection;
        
        this.sources.addItem(newSource);
        this.dispatchEvent(new SourceEvent(SourceEvent.ADDED, newSource));
        
        MessageManager.instance.addInfo(
          "Quelle hinzugefügt",
          "Die Quelle " + newSource.properties.title + " wurde hinzugefügt."
        );
      } else {
        
        var source:ISource = this.findSource(connection.url);
        
        switch (e.response.type) {
          
          case MediaRequest.LIST:
            
            source.resources.removeAll();
            // Intentionally not breaking here
          
          case MediaRequest.LIST:
          case MediaRequest.ADD:
            
            for (var addedURL:String in e.response.resources) {
              
              var resource:IResource = new Resource(source, addedURL, e.response.resources[addedURL]);
              
              source.resources.addItem(resource);
              
              this.dispatchEvent(new ResourceEvent(ResourceEvent.ADDED, resource));
            }
            break;
          
          case MediaRequest.REMOVE:
            
            var urls:Vector.<String> = Vector.<String>(e.response.resources);
            var resources:Vector.<IResource> = this.findResources(urls);
            
            resources.forEach(function(resource:IResource):void {
              
              source.resources.removeItemAt(source.resources.getItemIndex(resource));
              
              this.dispatchEvent(new ResourceEvent(ResourceEvent.REMOVED, resource));
              
              MessageManager.instance.addInfo(
                "Entfernen erfolgreich",
                "Das Entfernen von " + resource +
                " von " +source.properties.title +
                " war erfolgreich."
              );
            });
            break;
        }
        
        if (e.request && e.request.credentials && !(e.request.type in source.credentials)) {
          
          source.credentials[e.request.type] = e.request.credentials;
          
          // TODO: Dispatch credentialsStored event
        }
      }
    }
    
    private function onConnectionError(e:ConnectionErrorEvent):void {
      
      var connection:Connection = e.target as Connection;
      var source:String = this.findSource(connection.url) as String || connection.url;
      
      try { this.pending.splice(this.pending.indexOf(connection.url), 1); } catch (e:Error) {}
      
      trace("[MediaManager] Got connection error: " + e.error.code);
      
      if (e.request) {
        
        var request:MediaRequest = e.request;
        var message:String = (e.error.message ? ": " + e.error.message : "");
        
        switch (e.request.type) {
          
          case MediaRequest.INFO:
            MessageManager.instance.addError(
              "Informationsabfrage fehlgeschlagen",
              "Fehler beim Abfragen der Informationen über " +
              source + message
            );
            break;
          
          case MediaRequest.LIST:
            MessageManager.instance.addError(
              "Ressourcen-Auflistung fehlgeschlagen",
              "Fehler beim Auflisten der Ressourcen von " +
              source + message
            );
            break;
          
          case MediaRequest.ADD:
            MessageManager.instance.addError(
              "Hinzufügen fehlgeschlagen",
              "Fehler beim Hinzufügen von Ressourcen zu " +
              source + message
            );
            break;
          
          case MediaRequest.REMOVE:
            MessageManager.instance.addError(
              "Entfernen fehlgeschlagen",
              "Fehler beim Entfernen von Ressourcen von " +
              source + message
            );
            break;
        }
      } else {
        
        MessageManager.instance.addError(
          "Verbindung fehlgeschlagen",
          "Die Verbindung zu " + source + " ist fehlgeschlagen: " +
          e.error.message
        );
      }
    }
    
    /**
     * Default handler for credentials request, indented to signify failure
     * 
     * @param e The credentials event
     */
    private function onCredentialsRequest(e:CredentialsEvent):void {
      
      if (e.isDefaultPrevented()) {
        
        trace("[MediaManager] prevented");
        return;
      }
      
      e.callback(null);
    }
  }
}