/*
 * Base class for all protocol handlers
 */
function ProtocolHandler(url) {

  /**
   * Connects this handler to a source
   */
  this.connect = function() {};

  /**
   * Disconnects this handler from a source
   */
  this.disconnect = function() {};

  /**
   * Performs a request to a source
   */
  this.send = function(request) {};
  
  this.toString = function() { return "[object ProtocolHandler]" };
}

/*
 * A protocol handler for erroneous protocols
 */
function UnknownProtocolHandler(url) {

  var $this = $(this);

  /**
   * Directly triggers an error event
   */
  this.connect = this.disconnect = this.send = function(request) {
  
    $this.trigger(new MediaRequestError(request, MediaRequestError.UNKNOWN_PROTOCOL));
  };
  
  this.toString = function() { return "[object UnknownProtocolHandler]" };
}
// Intentionally a regular provider to allow for overrides
providers.addProvider("protocolHandlers", "__unknown__", UnknownProtocolHandler);

/*
 * A protocol handler for Web Sockets
 *
 * Possible improvements of Web Sockets:
 * http://blogs.webtide.com/gregw/entry/how_to_improve_websocket
 *
 * Possible requirement in Firefox: option "network.websocket.enabled" set to true,
 * See: http://www.0xdeadbeef.com/weblog/2010/12/disabling-websockets-for-firefox-4/
 * Possible requirement in Opera: option opera:config#UserPrefs|EnableWebSockets enabled
 * See: http://my.opera.com/desktopteam/blog/2010/12/10/friday-morning-improvements
 *
 * No transmission of binary data as of yet:
 * http://www.w3.org/TR/2009/WD-websockets-20091222/
 */
function WebSocketProtocolHandler(url) {

  var $this = $(this);
  var socket = null;
  var fileOperations = [];
  
  /**
   * Tries to create a socket for communication
   * using the following techniques:
   * - Web Sockets (http://www.w3.org/TR/websockets/)
   */
  this.connect = function() {
  
    try {
    
      socket = new WebSocket(url);
      $(socket).one({open: onOpen});
    } catch (e) {

      $this.trigger(new MediaRequestError(null, MediaRequestError.CONNECTION_FAILED));
    }
  }

  /**
   * Stops the Web Socket connection
   */
  this.disconnect = function() {

    if (socket) {

      socket.close();
      $this.trigger("disconnected");
    }
  };

  /**
   * Performs a request via Web Sockets
   */
  this.send = function(request) {

    if (!socket) {
    
      //$this.trigger(new MediaRequestError(null, "Web Socket konnte nicht erstellt werden"))

      $this.trigger(new MediaRequestError(request, MediaRequestError.CONNECTION_FAILED));
      return;
    }
    
    socket.onmessage = function(e) {
    
      var response = e.data;
      
      try {
      
        response = JSON.parse(response);
      } catch (e) {
      
        $this.trigger(new MediaRequestError(request, MediaRequestError.MALFORMED_RESPONSE));
        return;
      }

      var responseEvent = new jQuery.Event("response");
      responseEvent.request = request;
      responseEvent.response = response;
      
      $this.trigger(responseEvent);
    };
    
    socket.onerror = function(e) {
    
      $this.trigger(new MediaRequestError(request, MediaRequestError.CONNECTION_FAILED));
    };
    
    socket.onclose = function(e) {
    
      $this.trigger(new MediaRequestError(request, MediaRequestError.DISCONNECTED));
    };
    
    if ((request.type ==  MediaRequest.ADD) && request.files) {
      
      // Prepare file operations
      request.files.forEach(function(file) {
      
        var reader = new FileReader();
        var operation = {
          file: file,
          reader: reader
        };
        
        // Circular reference for removal
        reader.operation = operation;
      
        fileOperations.push(operation);
      });
    }
    
    // Still ongoing operations, wait until they finish
    if (fileOperations.length > 0) {
        
      request.files = [];
    
      fileOperations.forEach(function(operation) {
      
        operation.reader.onload = function() {
        
          var operation = this.operation;
          delete this.operation; // Don't leak circular references
          fileOperations.splice(fileOperations.indexOf(operation), 1);
        
          request.files.push({
            name: operation.file.name,
            type: operation.file.type,
            size: operation.file.size,
            data: this.result.split(",")[1] // Drop "data:MIME/TYPE;base64"
          });
          
          // Finally submit data after the last operation has finished
          if (fileOperations.length == 0) {
          
            socket.send(JSON.stringify({mediaRequest: request}));
          }
        };
        
        operation.reader.onabort = function(e) {
        
          var operation = this.operation;
          delete this.operation; // Don't leak circular references
          fileOperations.splice(fileOperations.indexOf(operation), 1);
        };
        
        operation.reader.onerror = function(e) {
        
          // Something failed, abort all operations
          fileOperations.forEach(function(operation) {
          
            operation.reader.abort();
          });
          
          // Notify about the culprit
          $this.trigger(request, new MediaRequestError(e));
        };
        
        operation.reader.onprogress = function(e) {

          if (e.lengthComputable) {
    
            try { request.progress.set(e.loaded / e.total); }
            catch (ex) { request.progress = e.loaded / e.total; }
          }
        };
        
        operation.reader.readAsDataURL(operation.file);
      });
    } else {
    
      socket.send(JSON.stringify({mediaRequest: request}));
    }
  };
  
  this.toString = function() { return "[object WebSocketProtocolHandler]" };
  
  function onOpen(e) {
  
    $this.trigger("connected");
  }
}
WebSocketProtocolHandler.prototype = new ProtocolHandler;
providers.addProvider("protocolHandlers", "ws", WebSocketProtocolHandler);

/*
 * A protocol handler for HTTP requests
 */
function HTTPProtocolHandler(url) {

  var $this = $(this);
  // List of all current request objects
  var requests = [];

  /**
   * Immediately signifies successfully opened connections
   */
  this.connect = function() {

    $this.trigger("connected");
  };

  /**
   * Cancels all current requests
   */
  this.disconnect = function() {
  
    requests.forEach(function(request) {
    
      $(request.upload).unbind();
      $(request).unbind();
      request.abort();
    });
    requests = [];
    
    $this.trigger("disconnected");
  };

  /**
   * Performs a cross origin HTTP request
   */
  this.send = function(request) {
  
    var requestObject = getRequestObject();
    
    if (!requestObject) {
    
      /*
      $this.trigger(new MediaRequestError(request, "Objekt für domainübergreifende Anfragen" +
                                          " konnte nicht erstellt werden"));
      */
      $this.trigger(new MediaRequestError(request, MediaRequestError.CONNECTION_FAILED));

      return;
    }
    
    // Allow for retrieval of binary large objects (BLOB)
    // Currently not supported in any browser, see reports:
    // http://code.google.com/p/chromium/issues/detail?id=52486
    //requestObject.asBlob = false;
    // Get via requestObject.responseBlob
    
    $(requestObject).bind({
      load: function(e) {
      
        // Remove this object from the list of requests
        requests.splice(requests.indexOf(this), 1);
        $(this.upload).unbind();
        $(this).unbind();

        var response = this.responseText;
        
        try {
        
          response = JSON.parse(response);
        } catch (e) {
        
          $this.trigger(new MediaRequestError(request, MediaRequestError.MALFORMED_RESPONSE));
          return;
        }

        var responseEvent = new jQuery.Event("response");
        responseEvent.request = request;
        responseEvent.response = response;
        
        $this.trigger(responseEvent);
      },
      progress: function(e) {
      
        var e = e.originalEvent;

        if (e.lengthComputable) {

          try { request.progress.set(e.loaded / e.total); }
          catch (ex) { request.progress = e.loaded / e.total; }
        }
      },
      error: function(e) {
      
        // Remove this object from the list of requests
        requests.splice(requests.indexOf(this), 1);
        $(this.upload).unbind();
        $(this).unbind();
      
        $this.trigger(new MediaRequestError(request, MediaRequestError.CONNECTION_FAILED));
      },
      timeout: function(e) {
      
        // Remove this object from the list of requests
        requests.splice(requests.indexOf(this), 1);
        $(this.upload).unbind();
        $(this).unbind();
      
        $this.trigger(new MediaRequestError(request, MediaRequestError.DISCONNECTED));
      }
    });
    
    $(requestObject.upload).bind({
      progress: function(e) {
      
        var e = e.originalEvent;
      
        if (e.lengthComputable) {

          try { request.progress.set(e.loaded / e.total); }
          catch (ex) { request.progress = e.loaded / e.total; }
        }
      }
    });
    
    var formData = new FormData();
    
    formData.append("mediaRequest[version]", request.version);
    formData.append("mediaRequest[type]", request.type);
    
    if (request.credentials) {
    
      formData.append("mediaRequest[credentials]",
        JSON.stringify(request.credentials));
    }
    
    switch (request.type) {
        
      case MediaRequest.ADD:
      case MediaRequest.REMOVE:
      
        if (request.resources) {
        
          formData.append("mediaRequest[resources]",
            JSON.stringify(request.resources));
        }
        // Intentionally not breaking here
        
      case MediaRequest.ADD:
      
        if (request.files) {
        
          request.files.forEach(function(file) {
          
            formData.append("mediaRequest[files][]", file);
          });
        }
        break;
    }
    
    requestObject.open("POST", url, true);
    
    // Add this object to the list of requests
    requests.push(requestObject);
    
    // Pass request cancel to the internal object
    $(request).one({cancelled: function(e) {
    
      requestObject.abort();
    }});
    
    // Taking HTML5 FormData shim into account
    // See: https://github.com/francois2metz/html5-formdata
    if (formData.fake) {
    
      requestObject.setRequestHeader("Content-Type",
        "multipart/form-data; boundary=" + formData.boundary);
      requestObject.sendAsBinary(formData.toString());
    } else {
    
      requestObject.send(formData);
    }
  };

  
  /*
   * Tries to create an object suitable for cross origin requests
   * using the following techniques:
   * - XMLHttpRequest Level 2 (http://www.w3.org/TR/XMLHttpRequest2/)
   * - Cross-Origin Resource Sharing (http://www.w3.org/TR/cors/)
   * - XDomainRequest (http://msdn.microsoft.com/en-us/library/cc288060%28VS.85%29.aspx)
   */
  function getRequestObject() {
  
    var request = new XMLHttpRequest();
    
    if (!("withCredentials" in request)) { // Not XHR Level 2
    
      if (typeof XDomainRequest != "undefined") {
      
        request = new XDomainRequest();
      } else { // No CORS-compatible request object available
      
        request = null;
      }
    }
  
    return request;
  }
  
  this.toString = function() { return "[object HTTPProtocolHandler]" };
}
HTTPProtocolHandler.prototype = new ProtocolHandler;
providers.addProvider("protocolHandlers", "http", HTTPProtocolHandler);

/**
 * Represents a connection to an URL, no matter the
 * underlying implementation
 * Handles requests to and responses from URLs
 */
function Connection(url) {

  var $this = $(this);
  var handler = null;
  this.url = url;
  this.state = Connection.IDLE;

  /**
   * Opens the connection to the URL
   */
  this.open = function() {
    
    if (this.state == Connection.CLOSING || this.state == Connection.OPENING) {
    
      return;
    }
    
    this.state = Connection.OPENING;
  
    var protocol = this.url.substring(0, this.url.indexOf("://"));

    handler = new (providers.getProvider("protocolHandlers", protocol) ||
                   providers.getProvider("protocolHandlers", "__unknown__"))(this.url);

    $(handler).bind({
      connected: onHandlerConnected.bind(this),
      disconnected: onHandlerDisconnected.bind(this),
      response: onHandlerResponse.bind(this),
      error: onHandlerError.bind(this),
    });

    handler.connect();
  };

  /**
   * Sends requests to the URL
   */
  this.send = function(request) {
  
    if (this.state == Connection.CLOSED) {
    
      onHandlerError(new MediaRequestError(request, MediaRequestError.DISCONNECTED));
      return;
    }
    
    if (this.state == Connection.CLOSING) {
    
      return;
    }

    if (handler) {

      handler.send(request);
    } else {

      onHandlerError(new MediaRequestError(request, MediaRequestError.DISCONNECTED));
    }
  };

  /**
   * Closes the connection to the URL,
   * no further requests will be possible
   */
  this.close = function() {
    
    if (this.state == Connection.CLOSING) {
    
      return;
    }
  
    if (this.state == Connection.CLOSED) {
    
      onError(new MediaRequestError(request, MediaRequestError.DISCONNECTED));
      return;
    }
  
    this.state = Connection.CLOSING;

    handler.disconnect();
  };
  
  this.toString = function() {
  
    return "[object Connection]";
  };

  /**
   * Notifies about successfully opened connections
   */
  function onHandlerConnected() {
  
    this.state = Connection.OPEN;

    var openEvent = new jQuery.Event("opened");
    openEvent.connection = this;

    $this.trigger(openEvent);
  }

  /**
   * Notifies about closed connections
   */
  function onHandlerDisconnected() {
  
    this.state = Connection.CLOSED;

    var closeEvent = new jQuery.Event("closed");
    closeEvent.connection = this;

    $this.trigger(closeEvent)
  }

  /**
   * Notifies about the response from the source
   */
  function onHandlerResponse(e) {
  
    try { e.request.progress.set(1); }
    catch (ex) { e.request.progress = 1; }

    if (e.response.version != MediaRequest.VERSION) {

      onError(new MediaRequestError(e.request, MediaRequestError.UNKNOWN_PROTOCOL_VERSION));
      return;
    }

    if (e.response.result == "error") {
    
      onHandlerError(new MediaRequestError(e.request, e.response.error));
      return;
    }

    e.connection = this;

    $this.trigger(e);
  }

  /**
   * Notifies about errors of a request
   */
  function onHandlerError(e) {

    e.connection = this;

    $this.trigger(e);
  }
}
Connection.IDLE = "idle";
Connection.OPENING = "opening"
Connection.OPEN = "open";
Connection.CLOSING = "closing";
Connection.CLOSED = "closed"

/*
 * General interface for all media requests
 */
function MediaRequest(type, data, unique) {

  var $this = $(this);
  var _progress = 0; // [0..1]
  
  this.version = MediaRequest.VERSION;
  this.type = type;
  this.id = null;
  
  (function() {
  
    var properties = {
      progress: {
        get: function() {
        
          return _progress;
        },
        set: function(progress) {
        
          if (progress < 0) {
          
            _progress = -1;
            $this.trigger("cancelled");
            
            return;
          }
          
          progress *= 1.0; // Ensure float
          progress = Math.max(0, progress);
          progress = Math.min(progress, 1);
          
          if (progress != _progress) {
          
            _progress = progress;

            var progressEvent = new jQuery.Event("progress");
            progressEvent.progress = progress;

            $this.trigger(progressEvent);
            
            if (progress == 1) {
            
              $this.trigger("done");
            }
          }
        }
      }
    };
    
    try {
    
      Object.defineProperties(this, properties);
    } catch (e) {
    
      for (var property in properties) {
      
        this[property] = properties[property];
      }
    }

    if (data) {

      this.addData(data);
    }
    
    if (unique) {
    
      this.id = SHA1((new Date().getTime()).toString());
    }
  }).call(this);

  /**
   * Allows for adding data to this request
   */
  this.addData = function(data) {

    if ("resources" in data) {

      this.resources = data.resources;
    }

    if ("files" in data) {

      this.files = data.files;
    }

    if ("credentials" in data) {

      this.credentials = credentials;
    }
  };
  
  /**
   * Cancels this request
   */
  this.cancel = function() {
  
    try { this.progress.set(-1); }
    catch (e) { this.progress = -1; }
  };
  
  this.toString = function() { return "[object MediaRequest]"; };
}
MediaRequest.VERSION = 1.0;
// Constant definition of media request types
MediaRequest.INFO = "info"; // Information about the source
MediaRequest.LIST = "list"; // List of resources
MediaRequest.ADD = "add"; // Add resources/files to source
MediaRequest.REMOVE = "remove"; // Remove resources from source

/**
 * General class for all media request errors
 */
function MediaRequestError(request, error) {

  this.request = request;
  
  if (typeof(error.code) !== "undefined") { // Existing error object
  
    this.code = error.code;
  
    if (error.code in MediaRequestError.messages) {
    
      this.message = MediaRequestError.messages[error.code];
    
      if (error.code == MediaRequestError.unknown && error.message) {
      
        this.message += ": " + error.message;
      }
    } else { // Untranslatable error
    
      this.message = error.message;
    }
  } else {
  
    if (error in MediaRequestError.messages) { // Literal error code
    
      this.code = error;
      this.message = MediaRequestError.messages[error];
    } else { // Completely unknown
    
      this.code = MediaRequestError.unknown;
      this.message = error;
    }
  }

  this.toString = function() {
    
    return "[object MediaRequestError]";
  };
}
MediaRequestError.prototype = new jQuery.Event("error");
// Constant definition of media request errors
MediaRequestError.UNKNOWN = "unknown";
MediaRequestError.UNKNOWN_PROTOCOL = "unknownProtocol";
MediaRequestError.UNKNOWN_PROTOCOL_VERSION = "unknownProtocolVersion";
MediaRequestError.UNKNOWN_REQUEST_TYPE = "unknownRequestType";
MediaRequestError.CONNECTION_FAILED = "connectionFailed";
MediaRequestError.DISCONNECTED = "disconnected";
MediaRequestError.AUTHENTICATION_REQUIRED = "authenticationRequired";
MediaRequestError.AUTHENTICATION_FAILED = "authenticationFailed";
MediaRequestError.MALFORMED_REQUEST = "malformedRequest";
MediaRequestError.MALFORMED_RESPONSE = "malformedResponse";

MediaRequestError.messages = {}
MediaRequestError.messages[MediaRequestError.UNKNOWN] = "Unbekannter Fehler";
MediaRequestError.messages[MediaRequestError.UNKNOWN_PROTOCOL] = "Unbekanntes Protokoll";
MediaRequestError.messages[MediaRequestError.UNKNOWN_PROTOCOL_VERSION] = "Unbekannte Protokoll-Version";
MediaRequestError.messages[MediaRequestError.UNKNOWN_REQUEST_TYPE] = "Unbekannte Anfrage";
MediaRequestError.messages[MediaRequestError.CONNECTION_FAILED] = "Verbindungsaufbau fehlgeschlagen";
MediaRequestError.messages[MediaRequestError.DISCONNECTED] = "Verbindung getrennt";
MediaRequestError.messages[MediaRequestError.AUTHENTICATION_REQUIRED] = "Authentifizierung erforderlich";
MediaRequestError.messages[MediaRequestError.AUTHENTICATION_FAILED] = "Authentifizierung fehlgeschlagen";
MediaRequestError.messages[MediaRequestError.MALFORMED_REQUEST] = "Verarbeitung der Anfrage an die Quelle fehlgeschlagen";
MediaRequestError.messages[MediaRequestError.MALFORMED_RESPONSE] = "Verarbeitung der Antwort der Quelle fehlgeschlagen";
