/**
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * LEGAL bits go here: we modified this
 */

var gadgets = gadgets || {};

/**
 * IFrame pool
 */
gadgets.IFramePool_ = function() {
  this.pool_ = [];
};

/**
 * Returns a newly created IFRAME with the locked state as specified
 * @param {Boolean} locked whether the created IFRAME is locked by default
 * @returns {HTMLElement} the created IFRAME element
 * @private
 */
gadgets.IFramePool_.prototype.createIFrame_ = function(locked) {
  var div = document.createElement("DIV");

  // MSIE will reliably trigger an IFRAME onload event if the onload is defined
  // inlined but not if it is defined via JS with element.onload = func;
  // We create it within a DIV but eventually is moved directly into doc.body.
  div.innerHTML = "<iframe onload='this.pool_locked=false'></iframe>";

  var iframe = div.getElementsByTagName("IFRAME")[0];
  iframe.style.visibility = 'hidden';
  iframe.style.width = iframe.style.height = '0px';
  iframe.style.border = '0px';
  iframe.style.position = 'absolute';

  iframe.pool_locked = locked;
  this.pool_[this.pool_.length] = iframe;

  // The div was only used to create the iframe. Now we disown and remove it.
  div.removeChild(iframe);
  div = null;
  return iframe;
};

/**
 * Retrieves an available IFrame and sets the URL to 'url'
 * @param {String} url The URL the IFrame is pointed to
 */
gadgets.IFramePool_.prototype.iframe = function(url) {
    
  // Reject weird urls
  if (!url.match(/^http[s]?:\/\//)) {
    return;
  }
  
  // We wrap this code in a setTimeout call to avoid tying the UI up too much
  // with a series of repeated IFRAME creation calls.

  var ifp = this;
  window.setTimeout(function() {
    var iframe = null;

    // For MSIE, delete any iframes that are no longer being used. MSIE cannnot
    // re-use the IFRAME because it will 'click' when we set the SRC.
    // Other browsers scan the pool for a free iframe to re-use.
    for (var i = ifp.pool_.length - 1; i >= 0; i--) {
      var ifr = ifp.pool_[i];
      if (ifr && !ifr.pool_locked) {
        ifr.parentNode.removeChild(ifr);
        if (window.ActiveXObject) {  // MSIE
          ifr = null;
          ifp.pool_[i] = null;
          ifp.pool_.splice(i,1);  // Remove it from the array
        } else {
          ifr.pool_locked = true;
          iframe = ifr;
          break;
        }
      }
    }

    // If no iframe was found to re-use we create a new one
    iframe = iframe ? iframe : ifp.createIFrame_(true);
    iframe.src = url;
    // We append to the body after setting the src otherwise MSIE will 'click'
    document.body.appendChild(iframe);
  }, 0);
  
};

/**
 * Clears the pool and re-initializes it to empty
 */
gadgets.IFramePool_.prototype.clear = function() {
  for (var i = 0; i < this.pool_.length; i++) {
    this.pool_[i].onload = null;
    this.pool_[i] = null;
  }
  this.pool_.length = 0;
  this.pool_ = new Array();
};

/**
 * Inter-frame procedure call
 */
gadgets.IFPC_ = function() {

var CALLBACK_ID_PREFIX_ = "cbid";
var CALLBACK_SERVICE_NAME_ = "ifpc_callback";
var iframe_pool_ = new gadgets.IFramePool_();
var packet_store_ = {};
var services_ = {};
var callbacks_ = {};
var callback_counter_ = 0;
var call_counter_ = 0;

/**
 * Registers a new service and associates it with 'handler'
 * @param {String} name the id to used to identify this service when calling
 * @param {Function} handler function to handle incoming requests
 */
function registerService(name, handler) {
  services_[name] = handler;
}

/**
 * Unregisters a registered service
 * @param {String} name the id used to identify the service when calling
 */
function unregisterService(name) {
  delete services_[name];
}

/**
 * dispatches the call
 * @param {String} iframe_id iframe ID to use for this request
 * @param {String} service_name service name
 * @param {Array} args_list array of arguments expected by this service
 * @param {String} remote_relay_url remote relay URL of the relay HTML page
 * @param {Function} callback callback function if a response is expected
 *        (can be null if no callback expected)
 * @param {String} local_relay_url local relay URL of the relay HTML page
 *        (can be null if callback is also null)
 */
function call(iframe_id,
              service_name,
              args_list,
              remote_relay_url,
              callback,
              local_relay_url,
              opt_shouldThrowError) {
  // We prepend some other arguments that the processRequest
  // method is expecting and will shift off in reverse order
  // once all the packets have been received
  // First make a local copy of args_list
  
  args_list = args_list.slice(0);
  args_list.unshift(registerCallback_(callback));
  args_list.unshift(local_relay_url);
  args_list.unshift(service_name);
  args_list.unshift(iframe_id);

  // Figure out how much URL space is available for actual data.
  // MSIE puts a limit of 4095 total chars including the # data.
  // Other browsers have limits at least as large as 4095.
  var max_data_len = 4095 - remote_relay_url.length;
  // Because we encodeArgs twice we need to leave room for escape chars
  max_data_len = parseInt(max_data_len / 3, 10);

  if (typeof opt_shouldThrowError == "undefined") {
    opt_shouldThrowError = true;
  }

  // Format of each packet is:
  // #iframe_id&callId&num_packets&packet_num&block_of_data
  var data = encodeArgs_(args_list);
  var num_packets = parseInt(data.length / max_data_len, 10);
  if (data.length % max_data_len > 0) {
    num_packets += 1;
  }
  
  for (var i = 0; i < num_packets; i++) {
    var data_slice = data.substr(i*max_data_len, max_data_len);
    var packet = [iframe_id, call_counter_, num_packets, i,
                  data_slice, opt_shouldThrowError];
    iframe_pool_.iframe(
      remote_relay_url + "#" + encodeArgs_(packet));
  }
  call_counter_++;
  
}

/**
 * Clears internal state.
 * Should be called from an unload handler to avoid memory leaks.
 */
function clear() {
  services_ = {};
  callbacks_ = {};
  iframe_pool_.clear();
}

/**
 * Relays a request either from container to gadget, from gadget to container,
 * or from gadget to gadget.
 * @param {String} argsString encoded parameters
 */
function relayRequest(argsString) {
    
  // Extract the iframe-id.
  var iframeId = decodeArgs_(argsString)[0];
  
  // Need to find the destination window to pass the request on to.
  // We are in an IFPC relay iframe within the source window.
  var win = null;
  // If container-to-gadget communication, the window corresponding to
  // 'iframeId' will be our sibling, ie. a child of the container page,
  // and this child is the window we need.
  try {
    win = window.parent.frames[iframeId];
  } catch (e) {
    // Doesn't look like container-to-gadget communication.
    // Just leave 'win' unset.
  }
  // If gadget-to-gadget communication, the window corresponding to
  // 'iframeId' will be a sibling of our outer page, and this is the
  // window we need.
  try {
    if (!win && window.parent.parent.frames[iframeId] != window.parent) {
      win = window.parent.parent.frames[iframeId];
    }
  } catch (e) {
    // Doesn't look like gadget-to-gadget communication.
    // Just leave 'win' unset.
  }
  if (!win) {
    // Wasn't container-to-gadget nor gadget-to-gadget communication.
    // If gadget-to-container communication, 'iframeId' will be our grandparent.
    win = window.parent.parent;
  }
  // Now that 'win' is set appropriately, pass on the request.
  // Obscure Firefox bug sometimes causes an exception when xmlhttp is
  // utilized in an IFPC handler. Wrapping our handleRequest calls
  // with a setTimeout in the target window's scope prevents this
  // exception.
  // See this Mozilla bug for more info:
  // https://bugzilla.mozilla.org/show_bug.cgi?id=249843
  // Also see this blogged account of the bug:
  // http://the-stickman.com/web-development/javascript/iframes-xmlhttprequest-bug-in-firefox
  var fn = function() {
             win.gadgets.IFPC_.handleRequest(argsString);
           };

  if (window.ActiveXObject) { // MSIE
    // call the relay synchronously in IE
    // this is required because the iframe (and its relay closure)
    // may otherwise be deleted/invalidated before this call is made
    fn();
  } else {
    // all other browsers call with timeout, particularly FF. See
    // above comment regarding FF bug for why it's done this way
    
    //win.setTimeout(fn, 0);
    //BUGBUG: workaround the workaround since execution doesn't return to the timeout function
    //once the callstack "crosses back" to the x.myspace.com subdomain
    fn();
  }
}

/**
 * Internal function that processes the request
 * @param {String} packet encoded parameters
 */
function handleRequest(packet) {

  var packet = decodeArgs_(packet);

  var iframeId = packet.shift();
  var callId = packet.shift();
  var numPackets = packet.shift();
  var packetNum = packet.shift();
  var data = packet.shift();
  var shouldThrowError = packet.shift();
  // If you see fit to add a parameter here, don't.
  // If you must, be sure to add it to the END of the list!
  // If you don't, lots of problems will occur in situations where
  // IFPC versions mismatch, because ordered arguments will no longer
  // match up, causing all manner of breakages and odd behavior.

  // We store incoming packets in the packet_store object.
  // The key is the iframeId + the unique callId.
  // The value is an array to hold all the packets for the request.
  // The elements in the array are a 2-element array: packetNum and data.
  // When all packets are received, we sort based on the packetNum and then
  // re-create the original data block before passing to the Service Handler.
  var key = iframeId + "_" + callId;
  if (!packet_store_[key]) packet_store_[key] = [];
  packet_store_[key].push([packetNum, data]);

  if (packet_store_[key].length == numPackets) {
    // All packets have been received
    packet_store_[key].sort(function(a,b){
      return parseInt(a[0], 10) - parseInt(b[0], 10);
    });

    data = "";
    for (var i = 0; i < numPackets; i++) {
      data += packet_store_[key][i][1];
    }
    // Clear this entry from the packet_store
    packet_store_[key] = null;

    var args = decodeArgs_(data);

    var iframeId = args.shift();
    var serviceName = args.shift();
    var remote_relay_url = args.shift();
    var callbackId = args.shift();

    var handler = getServiceHandler_(serviceName);
     
    var opt_callback = null;
    if(isCallbackIdWellFormed_(callbackId)) {
        opt_callback = function() { 
            var argsArray = [].splice.call(arguments, 0);
            handleCallback_(callbackId, iframeId, remote_relay_url, argsArray);
        }
        args.push(opt_callback);
    }
    
    if (handler) {
      var args_list_result = handler.apply(null, args);
      handleCallback_(callbackId, iframeId, remote_relay_url, args_list_result);      
    } else if (shouldThrowError) {
      throw new Error("Service " + serviceName + " not registered.");
    }
  }
}

/**
 * Handles a callback
 */
function handleCallback_(callbackId, iframeId, remote_relay_url, args_list_result) {
    if (args_list_result instanceof Array && isCallbackIdWellFormed_(callbackId)) {
        args_list_result.unshift(callbackId);
        call(iframeId,
                   CALLBACK_SERVICE_NAME_,
                   args_list_result,
                   remote_relay_url,
                   null,   // no callback from the callback
                   "");    // no callback, no relay needed
     }
}

/**
 * Returns the service handler given a specific service name
 * @param {String} name service name
 * @returns {Function} service
 * @private
 */
function getServiceHandler_(name) {
  if(services_.hasOwnProperty(name)) {
    return services_[name];
  } else {
    return null;
  }
}

/**
 * Registers a new callback
 * @param {Function} callback callback function
 * @returns {String} a callback ID to use with call()
 * @private
 */
function registerCallback_(callback) {
  var callbackId = "";
  if (callback && typeof callback == "function") {
    callbackId = getNewCallbackId_();
    callbacks_[callbackId] = callback;
  }
  return callbackId;
}

/**
 * Unregisters an existing callback
 * @param {String} callback_id callback ID
 */
function unregisterCallback_(callback_id) {
  if (callbacks_.hasOwnProperty(callback_id)) {
    callbacks_[callback_id] = null;
  }
}

/**
 * Returns the callback given a specific callback id
 * @param {String} callback_id callback ID
 * @returns {Function|null} callback function
 * @private
 */
function getCallback_(callback_id) {
  if (callback_id &&
      callbacks_.hasOwnProperty(callback_id)) {
    return callbacks_[callback_id];
  }
  return null;
}

/**
 * Gets a new callback ID
 * @returns {String} a callback ID string
 * @private
 */
function getNewCallbackId_() {
  return CALLBACK_ID_PREFIX_ + (callback_counter_++);
}

/**
 * Return the decoded arguments a a list. First element is the service name.
 * @param {String} argsString Encoded argument string
 * @returns {Array} decoded argument list
 * @private
 */
function decodeArgs_(argsString) {
  var args = argsString.split('&');
  for(var i = 0; i < args.length; i++) {
    var arg = decodeURIComponent(args[i]);
    try {
      arg = gadgets.JSON.parse(arg);
    } catch (e) {
      // unexpected, but ok - treat as a string
    }
    args[i] = arg;
  }
  return args;
}

/**
 * Determines whether a callbackId is well-formed.
 * @param {String} callbackId callback ID
 * @returns {Boolean} whether the callbackId is well-formed
 * @private
 */
function isCallbackIdWellFormed_(callbackId) {
  return (callbackId+"").indexOf(CALLBACK_ID_PREFIX_) == 0;
}

/**
 * Private handler for the built-in callback service
 * @param {String} callbackId callback ID
 * @private
 */
function callbackServiceHandler_(callbackId) {
  var callback = getCallback_(callbackId);
  if (callback) {
    var args = [];
    for (var i = 1; i < arguments.length; i++) {
      args[args.length] = arguments[i];  // append the extra arguments
    }
    callback.apply(null, args);

    // Once the callback is triggered, we remove it.
    unregisterCallback_(callbackId);
  } else {
    throw new Error("Invalid callbackId");
  }
}

/**
 * Return the encoded argument string.
 * @param {Array} args list of arguments to encode
 * @returns {String} encoded argument string
 * @private
 */
function encodeArgs_(args) {
  var argsEscaped = [];
  for(var i = 0; i < args.length; i++) {
    var arg = gadgets.JSON.stringify(args[i]);
    argsEscaped.push(encodeURIComponent(arg));
  }
  return argsEscaped.join('&');
}

// Register the built-in callback handler
registerService(CALLBACK_SERVICE_NAME_, callbackServiceHandler_);

// Public methods
return {
  registerService: registerService,
  unregisterService: unregisterService,
  call: call,
  clear: clear,
  relayRequest: relayRequest,
  processRequest: relayRequest,
  handleRequest: handleRequest
};

}();

// Alias for legacy code
var _IFPC = gadgets.IFPC_;