/*
 * Copyright (c) 2011 TWIMPACT UG (haftungsbeschraenkt). All rights reserved.
 */

(function($, undefined) {

  var // Stream object instances
      instances = {},

    // Streaming agents
      agents = {},

    // HTTP Streaming transports
      transports = {},

    // Does the throbber of doom exist?
      throbber = $.browser.webkit && !$.isReady;

  // Once the window is fully loaded, the throbber of doom will not be appearing
  if (throbber) {
    $(window).load(function() {
      throbber = false;
    });
  }

  // Stream is based on The WebSocket API
  // W3C Working Draft 19 April 2011 - http://www.w3.org/TR/2011/WD-websockets-20110419/
  $.stream = function(url, options) {

    // Returns the first Stream in the document
    if (!arguments.length) {
      for (var i in instances) {
        return instances[i];
      }

      return null;
    }

    // Stream to which the specified url or alias is mapped
    var instance = instances[url];

    if (!options) {
      return instance || null;
    } else if (instance && instance.readyState < 3) {
      return instance;
    }

    var // Stream object
        stream = {

          // URL to which to connect
          url: url,

          // Merges options
          options: $.stream.setup({}, options),

          // The state of stream
          // 0: CONNECTING, 1: OPEN, 2: CLOSING, 3: CLOSED
          readyState: 0,

          // Fake send
          send: function() {},

          // Fake close
          close: function() {}

        },
        match = /^(http|ws)s?:/.exec(stream.url),
        open = function() {
          // Delegates open process
          agents[stream.options.type](stream);
        };

    // Stream type
    if (match) {
      stream.options.type = match[1];
    }

    // Makes arrays of event handlers
    for (var i in {open: 1, message: 1, error: 1, close: 1}) {
      stream.options[i] = $.makeArray(stream.options[i]);
    }

    // The url and alias are a identifier of this instance within the document
    instances[stream.url] = stream;
    if (stream.options.alias) {
      instances[stream.options.alias] = stream;
    }

    // Deals with the throbber of doom
    if (stream.options.type === "ws" || !throbber) {
      open();
    } else {
      switch (stream.options.throbber.type || stream.options.throbber) {
        case "lazy":
          $(window).load(function() {
            setTimeout(open, stream.options.throbber.delay || 50);
          });
          break;
        case "reconnect":
          open();
          $(window).load(function() {
            if (stream.readyState === 0) {
              stream.options.open.push(function() {
                stream.options.open.pop();
                setTimeout(reconnect, 10);
              });
            } else {
              reconnect();
            }

            function reconnect() {
              stream.options.close.push(function() {
                stream.options.close.pop();
                setTimeout(function() {
                  $.stream(stream.url, stream.options);
                }, stream.options.throbber.delay || 50);
              });

              var reconn = stream.options.reconnect;
              stream.close();
              stream.options.reconnect = reconn;
            }
          });
          break;
      }
    }

    return stream;
  };

  $.extend($.stream, {

    version: "1.2",

    // Logic borrowed from jQuery.ajaxSetup
    setup: function(target, options) {
      if (!options) {
        options = target;
        target = $.extend(true, $.stream.options, options);
      } else {
        $.extend(true, target, $.stream.options, options);
      }

      for (var field in {context: 1, url: 1}) {
        if (field in options) {
          target[field] = options[field];
        } else if (field in $.stream.options) {
          target[field] = $.stream.options[field];
        }
      }

      return target;
    },

    options: {
      // Stream type
      type: window.WebSocket ? "ws" : "http",
      // Whether to automatically reconnect when stream closed
      reconnect: true,
      // Whether to trigger global stream event handlers
      global: true,
      // Only for WebKit
      throbber: "lazy",
      // Message data type
      dataType: "text",
      // Message data converters
      converters: {
        text: window.String,
        json: $.parseJSON,
        xml: $.parseXML
      }
      // Additional parameters for GET request
      // openData: null,
      // WebSocket constructor argument
      // protocols: null,
      // XDomainRequest transport
      // enableXDR: false,
      // rewriteURL: null
      // Polling interval
      // operaInterval: 0
      // iframeInterval: 0
    }

  });

  $.extend(agents, {

    // WebSocket wrapper
    ws: function(stream) {
      if (!window.WebSocket) {
        return;
      }

      var // Absolute WebSocket URL
          url = prepareURL(getAbsoluteURL(stream.url).replace(/^http/, "ws"), stream.options.openData),
        // WebSocket instance
          ws = stream.options.protocols ? new window.WebSocket(url, stream.options.protocols) : new window
              .WebSocket(url);

      // WebSocket event handlers
      $.extend(ws, {
        onopen: function(event) {
          stream.readyState = 1;
          trigger(stream, event);
        },
        onmessage: function(event) {
          trigger(stream, $.extend({}, event, {data: stream.options.converters[stream.options.dataType](event.data)}));
        },
        onerror: function(event) {
          stream.options.reconnect = false;
          trigger(stream, event);
        },
        onclose: function(event) {
          var readyState = stream.readyState;

          stream.readyState = 3;
          trigger(stream, event);

          // Reconnect?
          if (stream.options.reconnect && readyState !== 0) {
            $.stream(stream.url, stream.options);
          }
        }
      });

      // Overrides send and close
      $.extend(stream, {
        send: function(data) {
          if (stream.readyState === 0) {
            $.error("INVALID_STATE_ERR: Stream not open");
          }

          ws.send(typeof data === "string" ? data : param(data));
        },
        close: function() {
          if (stream.readyState < 2) {
            stream.readyState = 2;
            stream.options.reconnect = false;
            ws.close();
          }
        }
      });
    },

    // HTTP Streaming
    http: function(stream) {
      var // Transport
          transportFn,
          transport,
        // Low-level request and response handler
          handleOpen,
          handleMessage,
          handleSend,
        // Latch for AJAX
          sending,
        // Data queue
          dataQueue = [],
        // Helper object for parsing response
          message = {
            // The index from which to start parsing
            index: 0,
            // The temporary data
            data: ""
          };

      // Chooses a proper transport
      transportFn = transports[
        // xdr
          stream.options.enableXDR && window.XDomainRequest ? "xdr" :
            // iframe
              window.ActiveXObject ? "iframe" :
                // xhr
                  window.XMLHttpRequest ? "xhr" : null];

      if (!transportFn) {
        return;
      }

      // Default response handler
      handleOpen = stream.options.handleOpen || function(text, message, stream) {
        // The top of the response is made up of the id and padding
        // optional identifier within the server
        stream.id = text.substring(0, text.indexOf(";"));
        // message.index = text.indexOf(";", stream.id.length + ";".length) + ";".length;
        message.index = text.indexOf(";", stream.id.length + 1) + 1;
      };
      handleMessage = stream.options.handleMessage || function(text, message) {
        // Response could contain a single message, multiple messages or a fragment of a message
        // default message format is message-size ; message-data ;
        if (message.size == null) {
          // Checks a semicolon of size part
          var sizeEnd = text.indexOf(";", message.index);
          if (sizeEnd < 0) {
            return false;
          }

          message.size = +text.substring(message.index, sizeEnd);
          // index: sizeEnd + ";".length,
          message.index = sizeEnd + 1;
        }

        var data = text.substr(message.index, message.size - message.data.length);
        message.data += data;
        message.index += data.length;

        // Has stream message been completed?
        if (message.size !== message.data.length) {
          return false;
        }

        // Checks a semicolon of data part
        var dataEnd = text.indexOf(";", message.index);
        if (dataEnd < 0) {
          return false;
        }

        // message.index = dataEnd + ";".length;
        message.index = dataEnd + 1;

        // Completes parsing
        delete message.size;
      };

      // Default request handler
      handleSend = stream.options.handleSend || function(type, options, stream) {
        var metadata = {"metadata.id": stream.id, "metadata.type": type};

        options.data =
          // Close
            type === "close" ? param(metadata) :
              // Send
              // converts data if not already a string
                ((typeof options.data === "string" ? options.data : param(options.data)) + "&" + param(metadata));
      };

      transport = transportFn(stream, {
        response: function(text) {
          if (stream.readyState === 0) {
            if (handleOpen(text, message, stream) === false) {
              return;
            }

            stream.readyState = 1;
            trigger(stream, "open");
          }

          for (; ;) {
            if (handleMessage(text, message, stream) === false) {
              return;
            }

            if (stream.readyState < 3) {
              // Pseudo MessageEvent
              trigger(stream, "message", {
                // Converts the data type
                data: stream.options.converters[stream.options.dataType](message.data),
                origin: "",
                lastEventId: "",
                source: null,
                ports: null
              });
            }

            // Resets the data
            message.data = "";
          }
        },
        close: function(isError) {
          var readyState = stream.readyState;
          stream.readyState = 3;

          if (isError) {
            // Prevents reconnecting
            stream.options.reconnect = false;

            // If establishing a connection fails, fires the close event instead of the error event
            if (readyState === 0) {
              // Pseudo CloseEvent
              trigger(stream, "close", {
                wasClean: false,
                code: null,
                reason: ""
              });
            } else {
              trigger(stream, "error");
            }
          } else {
            // Pseudo CloseEvent
            trigger(stream, "close", {
              // Presumes that the stream closed cleanly
              wasClean: true,
              code: null,
              reason: ""
            });

            // Reconnect?
            if (stream.options.reconnect) {
              $.stream(stream.url, stream.options);
            }
          }
        }
      }, message);

      transport.open();

      // Overrides send and close
      $.extend(stream, {
        send: function(data) {
          if (stream.readyState === 0) {
            $.error("INVALID_STATE_ERR: Stream not open");
          }

          // Pushes the data into the queue
          dataQueue.push(data);

          if (!sending) {
            sending = true;

            // Performs an Ajax iterating through the data queue
            (function post() {
              if (stream.readyState === 1 && dataQueue.length) {
                var options = {url: stream.url, type: "POST", data: dataQueue.shift()};

                if (handleSend("send", options, stream) !== false) {
                  $.ajax(options).complete(post);
                } else {
                  post();
                }
              } else {
                sending = false;
              }
            })();
          }
        },
        close: function() {
          // Do nothing if the readyState is in the CLOSING or CLOSED
          if (stream.readyState < 2) {
            stream.readyState = 2;

            var options = {url: stream.url, type: "POST"};

            if (handleSend("close", options, stream) !== false) {
              // Notifies the server
              $.ajax(options);
            }

            // Prevents reconnecting
            stream.options.reconnect = false;
            transport.close();
          }
        }
      });
    }

  });

  $.extend(transports, {

    // XMLHttpRequest: Modern browsers except Internet Explorer
    xhr: function(stream, handler, message) {
      var stop,
          polling,
          preStatus,
          xhr = new window.XMLHttpRequest();

      xhr.onreadystatechange = function() {
        switch (xhr.readyState) {
          // Handles open and message event
          case 3:
            if (xhr.status !== 200) {
              return;
            }

            handler.response(xhr.responseText);

            // For Opera
            if ($.browser.opera && !polling) {
              polling = true;

              stop = iterate(function() {
                if (xhr.readyState === 4) {
                  return false;
                }

                if (xhr.responseText.length > message.index) {
                  handler.response(xhr.responseText);
                }
              }, stream.options.operaInterval);
            }
            break;
          // Handles error or close event
          case 4:
            // HTTP status 0 could mean that the request is terminated by abort method
            // but it's not error in Stream object
            handler.close(xhr.status !== 200 && preStatus !== 200);
            break;
        }
      };

      return {
        open: function() {
          xhr.open("GET", prepareURL(stream.url, stream.options.openData));
          xhr.send();
        },
        close: function() {
          if (stop) {
            stop();
          }

          // Saves status
          try {
            preStatus = xhr.status;
          } catch (e) {}
          xhr.abort();
        }
      };
    },

    // Hidden iframe: Internet Explorer
    iframe: function(stream, handler, message) {
      var stop,
          closed,
          onload = function() {
            if (!closed) {
              closed = true;
              handler.close();
            }
          },
          doc = new window.ActiveXObject("htmlfile");

      doc.open();
      doc.close();

      return {
        open: function() {
          var iframe = doc.createElement("iframe");
          iframe.src = prepareURL(stream.url, stream.options.openData);

          doc.body.appendChild(iframe);

          // For the server to respond in a consistent format regardless of user agent, we polls response text
          var cdoc = iframe.contentDocument || iframe.contentWindow.document;

          stop = iterate(function() {
            if (!cdoc.documentElement) {
              return;
            }

            // Detects connection failure
            if (cdoc.readyState === "complete") {
              try {
                $.noop(cdoc.fileSize);
              } catch(e) {
                handler.close(true);
                return false;
              }
            }

            var response = cdoc.body.lastChild,
                readResponse = function() {
                  // Clones the element not to disturb the original one
                  var clone = response.cloneNode(true);

                  // If the last character is a carriage return or a line feed, IE ignores it in the innerText property
                  // therefore, we add another non-newline character to preserve it
                  clone.appendChild(cdoc.createTextNode("."));

                  var text = clone.innerText;
                  return text.substring(0, text.length - 1);
                };

            // To support text/html content type
            if (!$.nodeName(response, "pre")) {
              // Injects a plaintext element which renders text without interpreting the HTML and cannot be stopped
              // it is deprecated in HTML5, but still works
              var head = cdoc.head || cdoc.getElementsByTagName("head")[0] || cdoc.documentElement,
                  script = cdoc.createElement("script");

              script.text = "document.write('<plaintext>')";

              head.insertBefore(script, head.firstChild);
              head.removeChild(script);

              // The plaintext element will be the response container
              response = cdoc.body.lastChild;
            }

            // Handles open event
            handler.response(readResponse());

            // Handles message and close event
            stop = iterate(function() {
              var text = readResponse();
              if (text.length > message.index) {
                handler.response(text);

                // Empties response every time that it is handled
                response.innerText = "";
                message.index = 0;
              }

              if (cdoc.readyState === "complete") {
                onload();
                return false;
              }
            }, stream.options.iframeInterval);

            return false;
          });
        },
        close: function() {
          if (stop) {
            stop();
          }

          doc.execCommand("Stop");
          onload();
        }
      };
    },

    // XDomainRequest: Optionally Internet Explorer 8+
    xdr: function(stream, handler) {
      var xdr = new window.XDomainRequest(),
          rewriteURL = stream.options.rewriteURL || function(url) {
            // Maintaining session by rewriting URL
            // http://stackoverflow.com/questions/6453779/maintaining-session-by-rewriting-url
            var rewriters = {
              JSESSIONID: function(sid) {
                return url.replace(/;jsessionid=[^\?]*|(\?)|$/, ";jsessionid=" + sid + "$1");
              },
              PHPSESSID: function(sid) {
                return url.replace(/\?PHPSESSID=[^&]*&?|\?|$/, "?PHPSESSID=" + sid + "&").replace(/&$/, "");
              }
            };

            for (var name in rewriters) {
              // Finds session id from cookie
              var matcher = new RegExp("(?:^|;\\s*)" + encodeURIComponent(name) + "=([^;]*)").exec(document.cookie);
              if (matcher) {
                return rewriters[name](matcher[1]);
              }
            }

            return url;
          };

      // Handles open and message event
      xdr.onprogress = function() {
        handler.response(xdr.responseText);
      };
      // Handles error event
      xdr.onerror = function() {
        handler.close(true);
      };
      // Handles close event
      var onload = xdr.onload = function() {
        handler.close();
      };

      return {
        open: function() {
          xdr.open("GET", prepareURL(rewriteURL(stream.url), stream.options.openData));
          xdr.send();
        },
        close: function() {
          xdr.abort();
          onload();
        }
      };
    }

  });

  // Closes all stream when the document is unloaded
  // this works right only in IE
  $(window).bind("unload.stream", function() {
    for (var url in instances) {
      instances[url].close();
      delete instances[url];
    }
  });

  $.each("streamOpen streamMessage streamError streamClose".split(" "), function(i, o) {
    $.fn[o] = function(f) {
      return this.bind(o, f);
    };
  });

  // Works even in IE6
  function getAbsoluteURL(url) {
    var div = document.createElement("div");
    div.innerHTML = "<a href='" + url + "'/>";

    return div.firstChild.href;
  }

  function trigger(stream, event, props) {
    event = event.type ?
        event :
        $.extend($.Event(event), {bubbles: false, cancelable: false}, props);

    var handlers = stream.options[event.type],
        applyArgs = [event, stream];

    // Triggers local event handlers
    for (var i = 0, length = handlers.length; i < length; i++) {
      handlers[i].apply(stream.options.context, applyArgs);
    }

    if (stream.options.global) {
      // Triggers global event handlers
      $.event.trigger("stream" + event.type.substring(0, 1).toUpperCase() + event.type.substring(1), applyArgs);
    }
  }

  function prepareURL(url, data) {
    // Converts data into a query string
    if (data && typeof data !== "string") {
      data = param(data);
    }

    // Attaches a time stamp to prevent caching
    var ts = $.now(),
        ret = url.replace(/([?&])_=[^&]*/, "$1_=" + ts);

    return ret + (ret === url ? (/\?/.test(url) ? "&" : "?") + "_=" + ts : "") + (data ? ("&" + data) : "");
  }

  function param(data) {
    return $.param(data, $.ajaxSettings.traditional);
  }

  function iterate(fn, interval) {
    var timeoutId;

    // Though the interval is 0 for real-time application, there is a delay between setTimeout calls
    // For detail, see https://developer.mozilla.org/en/window.setTimeout#Minimum_delay_and_timeout_nesting
    interval = interval || 0;

    (function loop() {
      timeoutId = setTimeout(function() {
        if (fn() === false) {
          return;
        }

        loop();
      }, interval);
    })();

    return function() {
      clearTimeout(timeoutId);
    };
  }

})(jQuery);
