/*
 * 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.
 */

/*global ActiveXObject, DOMParser */
/*global shindig */

/**
 * @fileoverview Provides remote content retrieval facilities.
 *     Available to every gadget.
 */

/**
 * @class Provides remote content retrieval functions.
 */

gadgets.io = function() {
  // Ever incrementing Ajax transaction id
  var ioTransactionId = 0;

  // Object to store ids for the ajax poll to avoid IE memory leak
  var ajaxPollQ = {};

  /**
   * Holds configuration-related data such as proxy urls.
   */
  var config = {};

  /**
   * Holds state for OAuth.
   */
  var oauthState;

  /**
   * Internal facility to create an xhr request.
   * @return {XMLHttpRequest}
   */
  function makeXhr() {
    var x;
    if (typeof shindig != 'undefined' &&
        shindig.xhrwrapper &&
        shindig.xhrwrapper.createXHR) {
      return shindig.xhrwrapper.createXHR();
    } else if (typeof ActiveXObject != 'undefined') {
      try {
        x = new ActiveXObject('Msxml2.XMLHTTP');
        if (!x) {
          x = new ActiveXObject('Microsoft.XMLHTTP');
        }
        return x;
      } catch (e) {} // An exception will be thrown if ActiveX is disabled
    }

    // The second construct is for the benefit of jsunit...
    if (typeof XMLHttpRequest != 'undefined' || window.XMLHttpRequest) {
      return new window.XMLHttpRequest();
    }
    else throw ('no xhr available');
  }

  /**
   * Checks the xobj for errors, may call the callback with an error response
   * if the error is fatal.
   *
   * @param {Object} xobj The XHR object to check.
   * @param {function(Object)} callback The callback to call if the error is fatal.
   * @return {boolean} true if the xobj is not ready to be processed.
   */
  function hadError(xobj, callback) {
    if (xobj['readyState'] !== 4) {
      return true;
    }
    try {
      if (xobj['status'] !== 200) {
        var error = ('' + xobj['status']);
        if (xobj['responseText']) {
          error = error + ' ' + xobj['responseText'];
        }
        callback({
          'errors': [error],
          'rc': xobj['status'],
          'text': xobj['responseText']
        });
        return true;
      }
    } catch (e) {
      callback({
        'errors': [e['number'] + ' Error not specified'],
        'rc': e['number'],
        'text': e['description']
      });
      return true;
    }
    return false;
  }

  /**
   * Handles non-proxied XHR callback processing.
   *
   * @param {string} url
   * @param {function(Object)} callback
   * @param {Object} params
   * @param {Object} xobj
   */
  function processNonProxiedResponse(url, callback, params, xobj) {
    if (hadError(xobj, callback)) {
      return;
    }
    var data = {
      'body': xobj['responseText']
    };
    callback(transformResponseData(params, data));
  }

  /**
   * Handles XHR callback processing.
   *
   * @param {string} url
   * @param {function(Object)} callback
   * @param {Object} params
   * @param {Object} xobj
   */
  function processResponse(url, callback, params, xobj) {
    if (hadError(xobj, callback)) {
      return;
    }
    var txt = xobj['responseText'];

    var UNPARSEABLE_CRUFT = config['unparseableCruft'];
    // remove unparseable cruft used to prevent cross-site script inclusion
    var offset = txt.indexOf(UNPARSEABLE_CRUFT) + UNPARSEABLE_CRUFT.length;

    // If no cruft then just return without a callback - avoid JS errors
    // TODO craft an error response?
    if (offset < UNPARSEABLE_CRUFT.length) return;
    txt = txt.substr(offset);

    // We are using eval directly here  because the outer response comes from a
    // trusted source, and json parsing is slow in IE.
    var data = eval('(' + txt + ')');
    data = data[url];
    // Save off any transient OAuth state the server wants back later.
    if (data['oauthState']) {
      oauthState = data['oauthState'];
    }
    // Update the security token if the server sent us a new one
    if (data['st']) {
      shindig.auth.updateSecurityToken(data['st']);
    }
    callback(transformResponseData(params, data));
  }

  /**
   * @param {Object} params
   * @param {Object} data
   * @return {Object}
   */

  function transformResponseData(params, data) {
    // Sometimes rc is not present, generally when used
    // by jsonrpccontainer, so assume 200 in its absence.
    var resp = {
      'text': data['body'],
      'rc': data['rc'] || 200,
      'headers': data['headers'],
      'oauthApprovalUrl': data['oauthApprovalUrl'],
      'oauthError': data['oauthError'],
      'oauthErrorText': data['oauthErrorText'],
      'oauthErrorTrace': data['oauthErrorTrace'],
      'oauthErrorUri': data['oauthErrorUri'],
      'oauthErrorExplanation': data['oauthErrorExplanation'],
      'errors': []
    };

    if (resp['rc'] < 200 || resp['rc'] >= 400) {
      resp['errors'] = [resp['rc'] + ' Error'];
    } else if (resp['text']) {
      if (resp['rc'] >= 300 && resp['rc'] < 400) {
        // Redirect pages will usually contain arbitrary
        // HTML which will fail during parsing, inadvertently
        // causing a 500 response. Thus we treat as text.
        params['CONTENT_TYPE'] = 'TEXT';
      }
      switch (params['CONTENT_TYPE']) {
        case 'JSON':
        case 'FEED':
          resp['data'] = gadgets.json.parse(resp.text);
          if (!resp['data']) {
            resp['errors'].push('500 Failed to parse JSON');
            resp['rc'] = 500;
            resp['data'] = null;
          }
          break;
        case 'DOM':
          var dom;
          if (typeof DOMParser != 'undefined') {
            var parser = new DOMParser();
            dom = parser.parseFromString(resp['text'], 'text/xml');
            if ('parsererror' === dom.documentElement.nodeName) {
              resp['errors'].push('500 Failed to parse XML');
              resp['rc'] = 500;
            } else {
              resp['data'] = dom;
            }
          } else if (typeof ActiveXObject != 'undefined') {
            dom = new ActiveXObject('Microsoft.XMLDOM');
            dom.async = false;
            dom.validateOnParse = false;
            dom.resolveExternals = false;
            if (!dom.loadXML(resp['text'])) {
              resp['errors'].push('500 Failed to parse XML');
              resp['rc'] = 500;
            } else {
              resp['data'] = dom;
            }
          } else {
            resp['errors'].push('500 Failed to parse XML because no DOM parser was available');
            resp['rc'] = 500;
          }
          break;
        default:
          resp['data'] = resp['text'];
          break;
      }
    }
    return resp;
  }

  /**
   * Sends an XHR post or get request
   *
   * @param {string} realUrl The url to fetch data from that was requested by the gadget.
   * @param {string} proxyUrl The url to proxy through.
   * @param {function()} callback The function to call once the data is fetched.
   * @param {Object} paramData The params to use when processing the response.
   * @param {string} method
   * @param {function(string,function(Object),Object,Object)}
   *     processResponseFunction The function that should process the
   *     response from the sever before calling the callback.
   * @param {Object=} opt_headers - Optional headers including a Content-Type that defaults to
   *     'application/x-www-form-urlencoded'.
   */
  function makeXhrRequest(realUrl, proxyUrl, callback, paramData, method,
      params, processResponseFunction, opt_headers) {
    var xhr = makeXhr();

    if (proxyUrl.indexOf('//') == 0) {
      proxyUrl = document.location.protocol + proxyUrl;
    }

    xhr.open(method, proxyUrl, true);
    if (callback) {
      var closureCallback = gadgets.util.makeClosure(null, processResponseFunction, realUrl,
        callback, params, xhr);

      // check for alternate ajax for onreadystatechange event handler
      var shouldPoll = gadgets.util.shouldPollXhrReadyStateChange();
      if(shouldPoll) {
        handleReadyState(xhr, closureCallback);
      }
      else {
        xhr.onreadystatechange = closureCallback;
      }
    }

    if (typeof opt_headers === 'string') {
      // This turned out to come directly from a public API, so we need to
      // keep compatibility...
      contentType = opt_headers;
      opt_headers = {};
    }
    var headers = opt_headers || {};

    if (paramData !== null) {
      var contentTypeHeader = 'Content-Type';
      var contentType = 'application/x-www-form-urlencoded';
      if (!headers[contentTypeHeader]) headers[contentTypeHeader] = contentType;
    }

    for (var headerName in headers) {
      xhr.setRequestHeader(headerName, headers[headerName]);
    }

    xhr.send(paramData);
  }

  /**
    * Helper function to use poll setInterval to call the callback for Ajax to avoid
    * memory leak in certain browsers (eg: IE7) due to circular linking.
    *
    * The function  will create  interval polling to poll the XHR object's readyState
    * property instead of binding a callback to the onreadystatechange event.
    *
    * @param {xhr} The Ajax object
    * @param {function} The callback function for the Ajax call
    * @return void
    */
    function handleReadyState(xhr, callback) {
      var tempTid = ioTransactionId;
      var pollInterval = config['xhrPollIntervalMs'] || 50;
      ajaxPollQ[tempTid] = window.setInterval(
        function() {
          if(xhr && xhr.readyState === 4) {
            // Clear the polling interval for the transaction and remove
            // the reference from ajaxPollQ
            window.clearInterval(ajaxPollQ[tempTid]);
            delete ajaxPollQ[tempTid];

            // call the callback
            if(callback) {
              callback();
            }
          }
        }, pollInterval);

      ioTransactionId++;
    }

  /**
   * Satisfy a request with data that is prefetched as per the gadget Preload
   * directive. The preloader will only satisfy a request for a specific piece
   * of content once.
   *
   * @param {Object} postData The definition of the request to be executed by the proxy.
   * @param {Object} params The params to use when processing the response.
   * @param {function(Object)} callback The function to call once the data is fetched.
   * @return {boolean} true if the request can be satisfied by the preloaded
   *         content false otherwise.
   */
  function respondWithPreload(postData, params, callback) {
    if (gadgets.io.preloaded_ && postData.httpMethod === 'GET') {
      for (var i = 0; i < gadgets.io.preloaded_.length; i++) {
        var preload = gadgets.io.preloaded_[i];
        if (preload && (preload.id === postData.url)) {
          // Only satisfy once
          delete gadgets.io.preloaded_[i];

          if (preload['rc'] !== 200) {
            callback({'rc': preload['rc'], 'errors': [preload['rc'] + ' Error']});
          } else {
            if (preload['oauthState']) {
              oauthState = preload['oauthState'];
            }
            var resp = {
              'body': preload['body'],
              'rc': preload['rc'],
              'headers': preload['headers'],
              'oauthApprovalUrl': preload['oauthApprovalUrl'],
              'oauthError': preload['oauthError'],
              'oauthErrorText': preload['oauthErrorText'],
              'oauthErrorTrace': preload['oauthErrorTrace'],
              'oauthErrorUri': preload['oauthErrorUri'],
              'oauthErrorExplanation': preload['oauthErrorExplanation'],
              'errors': []
            };
            callback(transformResponseData(params, resp));
          }
          return true;
        }
      }
    }
    return false;
  }

  /**
   * @param {Object} configuration Configuration settings.
   * @private
   */
  function init(configuration) {
    config = configuration['core.io'] || {};
  }

  gadgets.config.register('core.io', null, init);

  return /** @scope gadgets.io */ {
    /**
     * Fetches content from the provided URL and feeds that content into the
     * callback function.
     *
     * Example:
     * <pre>
     * gadgets.io.makeRequest(url, fn,
     *    {contentType: gadgets.io.ContentType.FEED});
     * </pre>
     *
     * @param {string} url The URL where the content is located.
     * @param {function(Object)} callback The function to call with the data from
     *     the URL once it is fetched.
     * @param {Object.<gadgets.io.RequestParameters, Object>=} opt_params
     *     Additional
     *     <a href="gadgets.io.RequestParameters.html">parameters</a>
     *     to pass to the request.
     *
     * @member gadgets.io
     */
    makeRequest: function(url, callback, opt_params) {
      // TODO: This method also needs to respect all members of
      // gadgets.io.RequestParameters, and validate them.

      var params = opt_params || {};

      var httpMethod = params['METHOD'] || 'GET';
      var refreshInterval = params['REFRESH_INTERVAL'];

      // Check if authorization is requested
      var auth, st;
      if (params['AUTHORIZATION'] && params['AUTHORIZATION'] !== 'NONE') {
        auth = params['AUTHORIZATION'].toLowerCase();
        st = shindig.auth.getSecurityToken();
      }

      // Include owner information?
      var signOwner = true;
      if (typeof params['SIGN_OWNER'] !== 'undefined') {
        signOwner = params['SIGN_OWNER'];
      }

      // Include viewer information?
      var signViewer = true;
      if (typeof params['SIGN_VIEWER'] !== 'undefined') {
        signViewer = params['SIGN_VIEWER'];
      }

      var headers = params['HEADERS'] || {};
      if (httpMethod === 'POST' && !headers['Content-Type']) {
        headers['Content-Type'] = 'application/x-www-form-urlencoded';
      }

      var urlParams = gadgets.util.getUrlParameters();

      var paramData = {
        'url': url,
        'httpMethod': httpMethod,
        'headers': gadgets.io.encodeValues(headers, false),
        'postData': params['POST_DATA'] || '',
        'authz': auth || '',
        'st': st || '',
        'contentType': params['CONTENT_TYPE'] || 'TEXT',
        'numEntries': params['NUM_ENTRIES'] || '3',
        'getSummaries': !!params['GET_SUMMARIES'],
        'signOwner': signOwner,
        'signViewer': signViewer,
        'gadget': urlParams['url'],
        'container': urlParams['container'] || urlParams['synd'] || 'default',
        // should we bypass gadget spec cache (e.g. to read OAuth provider URLs)
        'bypassSpecCache': gadgets.util.getUrlParameters()['nocache'] || '',
        'getFullHeaders': !!params['GET_FULL_HEADERS']
      };

      // add the nocache parameter if necessary
      // request param NO_CACHE takes precedence over osapi.container.RenderParam.NO_CACHE

      if (params.hasOwnProperty('NO_CACHE')) {
        paramData['nocache'] = params['NO_CACHE'];
      } else if (urlParams.hasOwnProperty('nocache')) {
        paramData['nocache'] = urlParams['nocache'];
      }

      // OAuth goodies
      if (auth === 'oauth' || auth === 'signed' || auth === 'oauth2') {
        if (gadgets.io.oauthReceivedCallbackUrl_) {
          paramData['OAUTH_RECEIVED_CALLBACK'] = gadgets.io.oauthReceivedCallbackUrl_;
          gadgets.io.oauthReceivedCallbackUrl_ = null;
        }
        paramData['oauthState'] = oauthState || '';
        // Just copy the OAuth parameters into the req to the server
        for (var opt in params) {
          if (params.hasOwnProperty(opt)) {
            if (opt.indexOf('OAUTH_') === 0 || opt === 'code') {
              paramData[opt] = params[opt];
            }
          }
        }
      }

      // Security token may have been set above
      st = st || shindig.auth.getSecurityToken();
      var opt_headers = st ? { 'X-Shindig-ST' : st } : {};

      var proxyUrl = config['jsonProxyUrl'].replace('%host%', document.location.host);

      // FIXME -- processResponse is not used in call
      if (!respondWithPreload(paramData, params, callback)) {
        if (httpMethod == 'GET' && typeof(refreshInterval) != 'undefined') {
            paramData['refresh'] = refreshInterval; // gadget requested cache override.
        }

        if (httpMethod === 'GET' && !paramData['authz']) {
          var extraparams = '?' + gadgets.io.encodeValues(paramData);
          makeXhrRequest(url, proxyUrl + extraparams, callback,
              null, 'GET', params, processResponse, opt_headers);
        } else {
          var extraparams = gadgets.io.encodeValues(paramData);
          makeXhrRequest(url, proxyUrl, callback,
              extraparams, 'POST', params,
              processResponse, opt_headers);
        }
      }
    },

    /**
     * @param {string} relativeUrl url to fetch via xhr.
     * @param callback callback to call when response is received or for error.
     * @param {Object=} opt_params
     * @param {Object=} opt_headers
     *
     */
    makeNonProxiedRequest: function(relativeUrl, callback, opt_params, opt_headers) {
      var params = opt_params || {};
      makeXhrRequest(relativeUrl, relativeUrl, callback, params['POST_DATA'],
          params['METHOD'], params, processNonProxiedResponse, opt_headers);
    },

    /**
     * Used to clear out the oauthState, for testing only.
     *
     * @private
     */
    clearOAuthState: function() {
      oauthState = undefined;
    },

    /**
     * Converts an input object into a URL-encoded data string.
     * (key=value&amp;...)
     *
     * @param {Object} fields The post fields you wish to encode.
     * @param {boolean=} opt_noEscaping An optional parameter specifying whether
     *     to turn off escaping of the parameters. Defaults to false.
     * @return {string} The processed post data in www-form-urlencoded format.
     *
     * @member gadgets.io
     */
    encodeValues: function(fields, opt_noEscaping) {
      var escape = !opt_noEscaping;

      var buf = [];
      var first = false;
      for (var i in fields) {
        if (fields.hasOwnProperty(i) && !/___$/.test(i)) {
          if (!first) {
            first = true;
          } else {
            buf.push('&');
          }
          buf.push(escape ? encodeURIComponent(i) : i);
          buf.push('=');
          buf.push(escape ? encodeURIComponent(fields[i]) : fields[i]);
        }
      }
      return buf.join('');
    },

    /**
     * Gets the proxy version of the passed-in URL.
     *
     * @param {string} url The URL to get the proxy URL for.
     * @param {Object.<gadgets.io.RequestParameters, Object>=} opt_params Optional Parameter Object.
     *     The following properties are supported:
     *       .REFRESH_INTERVAL The number of seconds that this
     *           content should be cached.  Defaults to 3600.
     *
     * @return {string} The proxied version of the URL.
     * @member gadgets.io
     */
    getProxyUrl: function(url, opt_params) {
      var proxyUrl = config['proxyUrl'];
      if (!proxyUrl) {
        return proxyUrl;
      }
      var params = opt_params || {};
      var refresh = params['REFRESH_INTERVAL'];
      if (typeof refresh == 'undefined') {
        refresh = '3600';
      }

      var urlParams = gadgets.util.getUrlParameters();
      var st = shindig.auth.getSecurityToken();
      var authz = params[gadgets.io.RequestParameters.AUTHORIZATION];
      var serviceName = params[gadgets.io.RequestParameters.OAUTH_SERVICE_NAME];

      var rewriteMimeParam =
          params['rewriteMime'] ? '&rewriteMime=' + encodeURIComponent(params['rewriteMime']) : '';
      var authParam = '';
      if(authz) {
        if(authz == gadgets.io.AuthorizationType.OAUTH || authz == gadgets.io.AuthorizationType.OAUTH2) {
          authParam = '&authz=' + authz.toLowerCase() + '&st=' + encodeURIComponent(st)
            + '&OAUTH_SERVICE_NAME=' + encodeURIComponent(serviceName);
        } else {
          authParam = '&authz=' + authz.toLowerCase();
        }
      }

      var uri = shindig.uri(url);
      var path = uri.getPath();
      var fileName = "";
      var lSlash = path.lastIndexOf('/');
      if (lSlash !== -1) {
        fileName = path.substring(lSlash); // include the slash
      }

      var ret = proxyUrl.replace('%url%', encodeURIComponent(url)).
          replace('%host%', document.location.host).
          replace('%rawurl%', url).
          replace('%filename%', fileName).
          replace('%refresh%', encodeURIComponent(refresh)).
          replace('%gadget%', encodeURIComponent(urlParams['url'])).
          replace('%container%', encodeURIComponent(urlParams['container'] || urlParams['synd'] || 'default')).
          replace('%authz%', authParam).
          replace('%rewriteMime%', rewriteMimeParam);
      if (ret.indexOf('//') == 0) {
        ret = window.location.protocol + ret;
      }
      return ret;
    },

    /**
     * @private
     */
    processResponse_: processResponse
  };
}();

/**
 * @const
 **/
gadgets.io.RequestParameters = gadgets.util.makeEnum([
  'ALIAS',
  'METHOD',
  'CONTENT_TYPE',
  'POST_DATA',
  'HEADERS',
  'AUTHORIZATION',
  'NUM_ENTRIES',
  'GET_SUMMARIES',
  'GET_FULL_HEADERS',
  'REFRESH_INTERVAL',
  'SIGN_OWNER',
  'SIGN_VIEWER',
  'OAUTH_SERVICE_NAME',
  'OAUTH_USE_TOKEN',
  'OAUTH_TOKEN_NAME',
  'OAUTH_REQUEST_TOKEN',
  'OAUTH_REQUEST_TOKEN_SECRET',
  'OAUTH_RECEIVED_CALLBACK',
  'NO_CACHE'
]);

/**
 * @const
 */
gadgets.io.MethodType = gadgets.util.makeEnum([
  'GET', 'POST', 'PUT', 'DELETE', 'HEAD'
]);

/**
 * @const
 */
gadgets.io.ContentType = gadgets.util.makeEnum([
  'TEXT', 'DOM', 'JSON', 'FEED'
]);

/**
 * @const
 */
gadgets.io.AuthorizationType = gadgets.util.makeEnum([
  'NONE', 'SIGNED', 'OAUTH', "OAUTH2"
]);
