package de.dtele.net.protocol {
  
  import com.adobe.serialization.json.JSON;
  import com.adobe.serialization.json.JSONParseError;
  import com.hurlant.util.Base64;
  
  import de.dtele.data.MimeTypes;
  import de.dtele.net.MediaRequest;
  import de.dtele.net.MediaRequestError;
  import de.dtele.net.MediaResponse;
  import de.dtele.net.events.ProtocolHandlerErrorEvent;
  import de.dtele.net.events.ProtocolHandlerEvent;
  
  import flash.events.Event;
  import flash.events.EventDispatcher;
  import flash.events.IOErrorEvent;
  import flash.events.ProgressEvent;
  import flash.net.FileReference;
  
  import mx.managers.BrowserManager;
  import mx.managers.IBrowserManager;
  import mx.utils.URLUtil;
  
  import net.gimite.WebSocket;
  import net.gimite.WebSocketEvent;
  
  /**
   * Dispatched when the connection to a source has been established
   */
  [Event("connected", type="de.dtele.net.events.ProtocolHandlerEvent")]
  /**
   * Dispatched when the connection to a source has been closed
   */
  [Event("disconnected", type="de.dtele.net.events.ProtocolHandlerEvent")]
  /**
   * Dispatched when a message from the source was received
   */
  [Event("response", type="de.dtele.net.events.ProtocolHandlerEvent")]
  /**
   * Dispatched when an error has occurred
   */
  [Event("error", type="de.dtele.net.events.ProtocolHandlerErrorEvent")]
  
  /**
   * Implements the WebSocket protocol for MediaRequest
   * 
   * <p>Uses the <em>HTML5 Web Socket implementation powered by Flash</em>
   * for implementing WebSocket communication.</p>
   * 
   * @see https://github.com/guille/web-socket-js
   * 
   * @author Mathias Brodala
   */
  public class WebSocketProtocolHandler extends EventDispatcher implements IProtocolHandler {
    
    /* Properties */
    /**
     * The internal WebSocket object
     */
    protected var socket:IWebSocket;
    
    /**
     * The browser manager, required for the document base URL
     */
    protected var browserManager:IBrowserManager = BrowserManager.getInstance();
    
    /* Methods */
    /**
     * Connects this handler to a source
     * 
     * @param url The url of a source
     */
    public function connect(url:String):void {
      
      var handler:WebSocketProtocolHandler = this;
      
      try {
        
        this.socket = new WebSocket(
          new Date().time,
          url,
          null,
          (URLUtil.getProtocol(this.browserManager.base) + "://" + URLUtil.getServerNameWithPort(this.browserManager.base)).toLowerCase(),
          null,
          0,
          "",
          null,
          new DummyWebSocketLogger()
        );

        this.socket.addEventListener(WebSocketEvent.OPEN, this.onOpen);
        this.socket.addEventListener(WebSocketEvent.MESSAGE, handler.onMessage);
        this.socket.addEventListener(WebSocketEvent.ERROR, this.onError);
        this.socket.addEventListener(WebSocketEvent.CLOSE, this.onClose);
      } catch (e:Error) {
        
        this.dispatchEvent(new ProtocolHandlerErrorEvent(
          ProtocolHandlerErrorEvent.ERROR,
          new MediaRequestError(MediaRequestError.CONNECTION_FAILED, e.message)
        ));
        return;
      }
    }
    
    /**
     * Disconnects this handler from a source
     */
    public function disconnect():void {
      
      if (this.socket) {
        
        this.socket.close();
        this.dispatchEvent(new ProtocolHandlerEvent(ProtocolHandlerEvent.DISCONNECTED));
      }
    }
    
    /**
     * Performs a request to a source
     * 
     * @param request The request to submit
     */
    public function send(request:MediaRequest):void {
      
      var handler:WebSocketProtocolHandler = this;
      var requestCancelled:Boolean = false;
      
      if (!this.socket) {
        
        this.dispatchEvent(new ProtocolHandlerErrorEvent(
          ProtocolHandlerErrorEvent.ERROR,
          new MediaRequestError(MediaRequestError.CONNECTION_FAILED),
          request
        ));
        return;
      }
      
      function onResponse(e:WebSocketEvent):void {
        
        // Manually call the general message listener
        handler.onMessage(e, request);
        // Only listening to this one request, thus remove this listener
        handler.socket.removeEventListener(WebSocketEvent.MESSAGE, onResponse);
        // Prevent the general message listener from being called
        e.stopImmediatePropagation();
      }
      
      // Set higher priority than default (0) to run before the general message listener
      this.socket.addEventListener(WebSocketEvent.MESSAGE, onResponse, false, 1);
      
      var mediaRequest:Object = {
        version: request.version,
        type: request.type
      };
      
      if (request.credentials) {
        
        mediaRequest.credentials = request.credentials;
      }
      
      // Allow for cancelling the request
      request.addEventListener(Event.CANCEL, function(e:Event):void {
        
        request.files.forEach(function(file:FileReference, ...a):void {
          
          file.cancel();
        });
        
        requestCancelled = true;
      });
      
      if (request.type == MediaRequest.ADD) {
        
        // Encode ressources and add them to the request
        mediaRequest.resources = request.resources;
      
        // Add files to the request
        if (request.files.length > 0) {
          
          mediaRequest.files = [];
          
          function isLoaded(file:FileReference, ...a):Boolean {
            
            return !!file.data;
          }
          
          request.files.forEach(function(file:FileReference, ...a):void {
            
            file.addEventListener(Event.COMPLETE, function(e:Event):void {
              
              var file:FileReference = e.target as FileReference;
              var type:String = file.type;
              var dotPosition:int = type.indexOf(".");
              
              if (dotPosition > -1) {
                
                type = MimeTypes.getMimeType(type.substr(dotPosition + 1));
              }
              
              mediaRequest.files.push({
                name: file.name,
                type: type,
                size: file.size,
                data: Base64.encodeByteArray(file.data)
              });
              
              if (request.files.every(isLoaded) && !requestCancelled) {
                
                handler.socket.send(JSON.encode({mediaRequest: mediaRequest}));
              }
            });
            
            file.addEventListener(IOErrorEvent.IO_ERROR, function(e:IOErrorEvent):void {
              
              handler.dispatchEvent(new ProtocolHandlerErrorEvent(
                ProtocolHandlerErrorEvent.ERROR,
                new MediaRequestError(MediaRequestError.UNKNOWN, e.text),
                request
              ));
            });
            
            file.addEventListener(ProgressEvent.PROGRESS, function(e:ProgressEvent):void {
              
              request.bytesLoaded = e.bytesLoaded;
              request.bytesTotal = e.bytesTotal;
            });
            
            // Start the file loading
            file.load();
          });
        } else if (!requestCancelled) {
          
          // If there are no files to load directly send the request
          this.socket.send(JSON.encode({mediaRequest: mediaRequest}));
        }
      } else if (!requestCancelled) {
        
        // If there are neither resources nor files to add directly send the request
        this.socket.send(JSON.encode({mediaRequest: mediaRequest}));
      }
    }
    
    /**
     * Notifies about opened WebSocket connections
     * 
     * @param e The WebSocket event
     */
    protected function onOpen(e:WebSocketEvent):void {
      
      this.dispatchEvent(new ProtocolHandlerEvent(ProtocolHandlerEvent.CONNECTED));
      this.socket.removeEventListener(WebSocketEvent.OPEN, onOpen);
    }
    
    /**
     * Notifies about closed WebSocket connections
     * 
     * @param e The WebSocket event
     */
    protected function onClose(e:WebSocketEvent):void {
      
      this.dispatchEvent(new ProtocolHandlerEvent(ProtocolHandlerEvent.DISCONNECTED));
    }
    
    /**
     * Handles messages from the WebSocket
     * 
     * @param e The Websocket event
     * @param request The optional MediaRequest preceding this message
     */
    protected function onMessage(e:WebSocketEvent, request:MediaRequest = null):void {
      
      var data:Object;
      
      try {
        
        trace("[WebSocketProtocolHandler] Got response: " + e.message);
        data = JSON.decode(e.message);
      } catch (e:JSONParseError) {
        
        this.dispatchEvent(new ProtocolHandlerErrorEvent(
          ProtocolHandlerErrorEvent.ERROR,
          new MediaRequestError(MediaRequestError.MALFORMED_RESPONSE),
          request
        ));
        return;
      }
      
      this.dispatchEvent(new ProtocolHandlerEvent(
        ProtocolHandlerEvent.RESPONSE,
        request,
        new MediaResponse(data)
      ));
    }
    
    /**
     * Notifies about connection errors
     * 
     * @param e The WebSocket event
     */
    protected function onError(e:WebSocketEvent):void {
      
      this.dispatchEvent(new ProtocolHandlerErrorEvent(
        ProtocolHandlerErrorEvent.ERROR,
        new MediaRequestError(MediaRequestError.CONNECTION_FAILED)
      ));
    }
  }
}