/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.
 */

gadgets.rpctx = gadgets.rpctx || {};

/*
 * For older WebKit-based browsers, the security model does not allow for any
 * known "native" hacks for conducting cross browser communication. However,
 * a variation of the IFPC (see below) can be used, entitled "RMR". RMR is
 * a technique that uses the resize event of the iframe to indicate that a
 * message was sent (instead of the much slower/performance heavy polling
 * technique used when a defined relay page is not avaliable). Simply put,
 * RMR uses the same "pass the message by the URL hash" trick that IFPC
 * uses to send a message, but instead of having an active relay page that
 * runs a piece of code when it is loaded, RMR merely changes the URL
 * of the relay page (which does not even have to exist on the domain)
 * and then notifies the other party by resizing the relay iframe. RMR
 * exploits the fact that iframes in the dom of page A can be resized
 * by page A while the onresize event will be fired in the DOM of page B,
 * thus providing a single bit channel indicating "message sent to you".
 * This method has the added benefit that the relay need not be active,
 * nor even exist: a 404 suffices just as well. Note that the technique
 * doesn't actually strictly require WebKit; it just so happens that these
 * browsers have no known alternatives (but are very ill-used right now).
 * The technique's implementation accounts for timing issues through
 * a packet-ack'ing protocol, so should work on just about any browser.
 * This may be of value in scenarios where neither wpm nor Flash are
 * available for some reason.
 *
 *   rmr: Resizing trick, works particularly well on WebKit.
 *      - Safari 2+
 *      - Chrome 1
 */
if (!gadgets.rpctx.rmr) {  // make lib resilient to double-inclusion

  gadgets.rpctx.rmr = function() {
    // Consts for RMR, including time in ms RMR uses to poll for
    // its relay frame to be created, and the max # of polls it does.
    var RMR_SEARCH_TIMEOUT = 500;
    var RMR_MAX_POLLS = 10;

    // JavaScript references to the channel objects used by RMR.
    // Gadgets will have but a single channel under
    // rmr_channels['..'] while containers will have a channel
    // per gadget stored under the gadget's ID.
    var rmr_channels = {};

    var parentParam = gadgets.util.getUrlParameters()['parent'];

    var process;
    var ready;

    /**
   * Append an RMR relay frame to the document. This allows the receiver
   * to start receiving messages.
   *
   * @param {Node} channelFrame Relay frame to add to the DOM body.
   * @param {string} relayUri Base URI for the frame.
   * @param {string} data to pass along to the frame.
   * @param {string=} opt_frameId ID of frame for which relay is being appended (optional).
   */
    function appendRmrFrame(channelFrame, relayUri, data, opt_frameId) {
      var appendFn = function() {
        // Append the iframe.
        document.body.appendChild(channelFrame);

        // Set the src of the iframe to 'about:blank' first and then set it
        // to the relay URI. This prevents the iframe from maintaining a src
        // to the 'old' relay URI if the page is returned to from another.
        // In other words, this fixes the bfcache issue that causes the iframe's
        // src property to not be updated despite us assigning it a new value here.
        channelFrame.src = 'about:blank';
        if (opt_frameId) {
          // Process the initial sent payload (typically sent by container to
          // child/gadget) only when the relay frame has finished loading. We
          // do this to ensure that, in processRmrData(...), the ACK sent due
          // to processing can actually be sent. Before this time, the frame's
          // contentWindow is null, making it impossible to do so.
          channelFrame.onload = function() {
            processRmrData(opt_frameId);
          };
        }
        channelFrame.src = relayUri + '#' + data;
      };

      if (document.body) {
        appendFn();
      } else {
        // Common gadget case: attaching header during in-gadget handshake,
        // when we may still be in script in head. Attach onload.
        gadgets.util.registerOnLoadHandler(function() { appendFn(); });
      }
    }

    /**
   * Sets up the RMR transport frame for the given frameId. For gadgets
   * calling containers, the frameId should be '..'.
   *
   * @param {string} frameId The ID of the frame.
   */
    function setupRmr(frameId) {
      if (typeof rmr_channels[frameId] === 'object') {
        // Sanity check. Already done.
        return;
      }

      var channelFrame = document.createElement('iframe');
      var frameStyle = channelFrame.style;
      frameStyle.position = 'absolute';
      frameStyle.top = '0px';
      frameStyle.border = '0';
      frameStyle.opacity = '0';

      // The width here is important as RMR
      // makes use of the resize handler for the frame.
      // Do not modify unless you test thoroughly!
      frameStyle.width = '10px';
      frameStyle.height = '1px';
      channelFrame.id = 'rmrtransport-' + frameId;
      channelFrame.name = channelFrame.id;

      // Use the explicitly set relay, if one exists. Otherwise,
      // Construct one using the parent parameter plus robots.txt
      // as a synthetic relay. This works since browsers using RMR
      // treat 404s as legitimate for the purposes of cross domain
      // communication.
      var relayUri = gadgets.rpc.getRelayUrl(frameId);
      var relayOrigin = gadgets.rpc.getOrigin(parentParam);
      if (!relayUri) {
        relayUri = relayOrigin + '/robots.txt';
      }

      rmr_channels[frameId] = {
        frame: channelFrame,
        receiveWindow: null,
        relayUri: relayUri,
        relayOrigin: relayOrigin,
        searchCounter: 0,
        width: 10,

        // Waiting means "waiting for acknowledgement to be received."
        // Acknowledgement always comes as a special ACK
        // message having been received. This message is received
        // during handshake in different ways by the container and
        // gadget, and by normal RMR message passing once the handshake
        // is complete.
        waiting: true,
        queue: [],

        // Number of non-ACK messages that have been sent to the recipient
        // and have been acknowledged.
        sendId: 0,

        // Number of messages received and processed from the sender.
        // This is the number that accompanies every ACK to tell the
        // sender to clear its queue.
        recvId: 0,

        // Token sent to target to verify domain.
        // TODO: switch to shindig.random()
        verifySendToken: String(Math.random()),

        // Token received from target during handshake. Stored in
        // order to send back to the caller for verification.
        verifyRecvToken: null,
        originVerified: false
      };

      if (frameId !== '..') {
        // Container always appends a relay to the gadget, before
        // the gadget appends its own relay back to container. The
        // gadget, in the meantime, refuses to attach the container
        // relay until it finds this one. Thus, the container knows
        // for certain that gadget to container communication is set
        // up by the time it finds its own relay. In addition to
        // establishing a reliable handshake protocol, this also
        // makes it possible for the gadget to send an initial batch
        // of messages to the container ASAP.
        appendRmrFrame(channelFrame, relayUri, getRmrData(frameId));
      }

      // Start searching for our own frame on the other page.
      conductRmrSearch(frameId);
    }

    /**
   * Searches for a relay frame, created by the sender referenced by
   * frameId, with which this context receives messages. Once
   * found with proper permissions, attaches a resize handler which
   * signals messages to be sent.
   *
   * @param {string} frameId Frame ID of the prospective sender.
   */
    function conductRmrSearch(frameId) {
      var channelWindow = null;

      // Increment the search counter.
      rmr_channels[frameId].searchCounter++;

      try {
        var targetWin = gadgets.rpc._getTargetWin(frameId);
        if (frameId === '..') {
          // We are a gadget.
          channelWindow = targetWin.frames['rmrtransport-' + gadgets.rpc.RPC_ID];
        } else {
          // We are a container.
          channelWindow = targetWin.frames['rmrtransport-..'];
        }
      } catch (e) {
        // Just in case; may happen when relay is set to about:blank or unset.
        // Catching exceptions here ensures that the timeout to continue the
        // search below continues to work.
      }

      var status = false;

      if (channelWindow) {
        // We have a valid reference to "our" RMR transport frame.
        // Register the proper event handlers.
        status = registerRmrChannel(frameId, channelWindow);
      }

      if (!status) {
        // Not found yet. Continue searching, but only if the counter
        // has not reached the threshold.
        if (rmr_channels[frameId].searchCounter > RMR_MAX_POLLS) {
          // If we reach this point, then RMR has failed and we
          // fall back to IFPC.
          return;
        }

        window.setTimeout(function() {
          conductRmrSearch(frameId);
        }, RMR_SEARCH_TIMEOUT);
      }
    }

    /**
   * Attempts to conduct an RPC call to the specified
   * target with the specified data via the RMR
   * method. If this method fails, the system attempts again
   * using the known default of IFPC.
   *
   * @param {string} targetId Module Id of the RPC service provider.
   * @param {string} serviceName Name of the service to call.
   * @param {string} from Module Id of the calling provider.
   * @param {Object} rpc The RPC data for this call.
   */
    function callRmr(targetId, serviceName, from, rpc) {
      var handler = null;

      if (from !== '..') {
        // Call from gadget to the container.
        handler = rmr_channels['..'];
      } else {
        // Call from container to the gadget.
        handler = rmr_channels[targetId];
      }

      if (handler) {
        // Queue the current message if not ACK.
        // ACK is always sent through getRmrData(...).
        if (serviceName !== gadgets.rpc.ACK) {
          handler.queue.push(rpc);
        }

        if (handler.waiting ||
            (handler.queue.length === 0 &&
            !(serviceName === gadgets.rpc.ACK && rpc && rpc['ackAlone'] === true))) {
          // If we are awaiting a response from any previously-sent messages,
          // or if we don't have anything new to send, just return.
          // Note that we don't short-return if we're ACKing just-received
          // messages.
          return true;
        }

        if (handler.queue.length > 0) {
          handler.waiting = true;
        }

        var url = handler.relayUri + '#' + getRmrData(targetId);

        try {
          // Update the URL with the message.
          handler.frame.contentWindow.location = url;

          // Resize the frame.
          var newWidth = handler.width == 10 ? 20 : 10;
          handler.frame.style.width = newWidth + 'px';
          handler.width = newWidth;

          // Done!
        } catch (e) {
          // Something about location-setting or resizing failed.
          // This should never happen, but if it does, fall back to
          // the default transport.
          return false;
        }
      }

      return true;
    }

    /**
   * Returns as a string the data to be appended to an RMR relay frame,
   * constructed from the current request queue plus an ACK message indicating
   * the currently latest-processed message ID.
   *
   * @param {string} toFrameId Frame whose sendable queued data to retrieve.
   */
    function getRmrData(toFrameId) {
      var channel = rmr_channels[toFrameId];
      var rmrData = {id: channel.sendId};
      if (channel) {
        rmrData['d'] = Array.prototype.slice.call(channel.queue, 0);
        var ackPacket = { 's': gadgets.rpc.ACK, 'id': channel.recvId };
        if (!channel.originVerified) {
          ackPacket['sendToken'] = channel.verifySendToken;
        }
        if (channel.verifyRecvToken) {
          ackPacket['recvToken'] = channel.verifyRecvToken;
        }
        rmrData['d'].push(ackPacket);
      }
      return gadgets.json.stringify(rmrData);
    }

    /**
   * Retrieve data from the channel keyed by the given frameId,
   * processing it as a batch. All processed data is assumed to have been
   * generated by getRmrData(...), pairing that method with this.
   *
   * @param {string} fromFrameId Frame from which data is being retrieved.
   */
    function processRmrData(fromFrameId) {
      var channel = rmr_channels[fromFrameId];
      var data = channel.receiveWindow.location.hash.substring(1);

      // Decode the RPC object array.
      var rpcObj = gadgets.json.parse(decodeURIComponent(data)) || {};
      var rpcArray = rpcObj['d'] || [];

      var nonAckReceived = false;
      var noLongerWaiting = false;

      var numBypassed = 0;
      var numToBypass = (channel.recvId - rpcObj['id']);
      for (var i = 0; i < rpcArray.length; ++i) {
        var rpc = rpcArray[i];

        // If we receive an ACK message, then mark the current
        // handler as no longer waiting and send out the next
        // queued message.
        if (rpc['s'] === gadgets.rpc.ACK) {
          // ACK received - whether this came from a handshake or
          // an active call, in either case it indicates readiness to
          // send messages to the from frame.
          ready(fromFrameId, true);

          // Store sendToken if challenge was passed.
          // This will cause the token to be sent back to the sender
          // to prove origin verification.
          channel.verifyRecvToken = rpc['sendToken'];

          // If a recvToken came back, check to see if it matches the
          // sendToken originally sent as a challenge. If so, mark
          // origin as having been verified.
          if (!channel.originVerified && rpc['recvToken'] &&
              String(rpc['recvToken']) == String(channel.verifySendToken)) {
            channel.originVerified = true;
          }

          if (channel.waiting) {
            noLongerWaiting = true;
          }

          channel.waiting = false;
          var newlyAcked = Math.max(0, rpc['id'] - channel.sendId);
          channel.queue.splice(0, newlyAcked);
          channel.sendId = Math.max(channel.sendId, rpc['id'] || 0);
          continue;
        }

        // If we get here, we've received > 0 non-ACK messages to
        // process. Indicate this bit for later.
        nonAckReceived = true;

        // Bypass any messages already received.
        if (++numBypassed <= numToBypass) {
          continue;
        }

        ++channel.recvId;

        // Send along the origin if it's been verified during handshake.
        // In either case, dispatch the message.
        process(rpc, channel.originVerified ? channel.relayOrigin : undefined);
      }

      // Send an ACK indicating that we got/processed the message(s).
      // Do so if we've received a message to process or if we were waiting
      // before but a received ACK has cleared our waiting bit, and we have
      // more messages to send. Performing this operation causes additional
      // messages to be sent.
      if (nonAckReceived ||
          (noLongerWaiting && channel.queue.length > 0)) {
        var from = (fromFrameId === '..') ? gadgets.rpc.RPC_ID : '..';
        callRmr(fromFrameId, gadgets.rpc.ACK, from, {'ackAlone': nonAckReceived});
      }
    }

    /**
   * Registers the RMR channel handler for the given frameId and associated
   * channel window.
   *
   * @param {string} frameId The ID of the frame for which this channel is being
   *   registered.
   * @param {Object} channelWindow The window of the receive frame for this
   *   channel, if any.
   *
   * @return {boolean} True if the frame was setup successfully, false
   *   otherwise.
   */
    function registerRmrChannel(frameId, channelWindow) {
      var channel = rmr_channels[frameId];

      // Verify that the channel is ready for receiving.
      try {
        var canAccess = false;

        // Check to see if the document is in the window. For Chrome, this
        // will return 'false' if the channelWindow is inaccessible by this
        // piece of JavaScript code, meaning that the URL of the channelWindow's
        // parent iframe has not yet changed from 'about:blank'. We do this
        // check this way because any true *access* on the channelWindow object
        // will raise a security exception, which, despite the try-catch, still
        // gets reported to the debugger (it does not break execution, the try
        // handles that problem, but it is still reported, which is bad form).
        // This check always succeeds in Safari 3.1 regardless of the state of
        // the window.
        canAccess = 'document' in channelWindow;

        if (!canAccess) {
          return false;
        }

        // Check to see if the document is an object. For Safari 3.1, this will
        // return undefined if the page is still inaccessible. Unfortunately, this
        // *will* raise a security issue in the debugger.
        // TODO Find a way around this problem.
        canAccess = typeof channelWindow['document'] == 'object';

        if (!canAccess) {
          return false;
        }

        // Once we get here, we know we can access the document (and anything else)
        // on the window object. Therefore, we check to see if the location is
        // still about:blank (this takes care of the Safari 3.2 case).
        var loc = channelWindow.location.href;

        // Check if this is about:blank for Safari.
        if (loc === 'about:blank') {
          return false;
        }
      } catch (ex) {
        // For some reason, the iframe still points to about:blank. We try
        // again in a bit.
        return false;
      }

      // Save a reference to the receive window.
      channel.receiveWindow = channelWindow;

      // Register the onresize handler.
      function onresize() {
        processRmrData(frameId);
      };

      if (typeof channelWindow.attachEvent === 'undefined') {
        channelWindow.onresize = onresize;
      } else {
        channelWindow.attachEvent('onresize', onresize);
      }

      if (frameId === '..') {
        // Gadget to container. Signal to the container that the gadget
        // is ready to receive messages by attaching the g -> c relay.
        // As a nice optimization, pass along any gadget to container
        // queued messages that have backed up since then. ACK is enqueued in
        // getRmrData to ensure that the container's waiting flag is set to false
        // (this happens in the below code run on the container side).
        appendRmrFrame(channel.frame, channel.relayUri, getRmrData(frameId), frameId);
      } else {
        // Process messages that the gadget sent in its initial relay payload.
        // We can do this immediately because the container has already appended
        // and loaded a relay frame that can be used to ACK the messages the gadget
        // sent. In the preceding if-block, however, the processRmrData(...) call
        // must wait. That's because appendRmrFrame may not actually append the
        // frame - in the context of a gadget, this code may be running in the
        // head element, so it cannot be appended to body. As a result, the
        // gadget cannot ACK the container for messages it received.
        processRmrData(frameId);
      }

      return true;
    }

    return {
      getCode: function() {
        return 'rmr';
      },

      isParentVerifiable: function() {
        return true;
      },

      init: function(processFn, readyFn) {
        // No global setup.
        process = processFn;
        ready = readyFn;
        return true;
      },

      setup: function(receiverId, token) {
        try {
          setupRmr(receiverId);
        } catch (e) {
          gadgets.warn('Caught exception setting up RMR: ' + e);
          return false;
        }
        return true;
      },

      call: function(targetId, from, rpc) {
        return callRmr(targetId, rpc['s'], from, rpc);
      }
    };
  }();

} // !end of double-inclusion guard
