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

/**
 * @fileoverview This manages rendering of gadgets in a place holder, within an
 * HTML element in the container. The API for this is low-level. Use the
 * container APIs to work with gadget sites.
 */

/**
 * @param {osapi.container.Container} container The container that hosts this gadget site.
 * @param {osapi.container.Service} service The container's service.
 * @param {Object} args containing:
 *          {osapi.container.Service} service to fetch gadgets metadata, token.
 *          {string} navigateCallback name of callback function on navigateTo().
 *          {Element} gadgetEl Element into which to render the gadget.
 *          {Element} bufferEl Optional element for double buffering.
 * @constructor
 * @extends {osapi.container.Site}
 */
osapi.container.GadgetSite = function(container, service, args) {
  var undef;

  osapi.container.Site.call(this, container, service, args['gadgetEl']); // call super

  /**
   * @type {string}
   * @private
   */
  this.navigateCallback_ = args['navigateCallback'];

  /**
   * @type {Element}
   * @private
   */
  this.loadingGadgetEl_ = args['bufferEl'];

  /**
   * @type {string}
   * @private
   */
  this.gadgetOnLoad_ = args['gadgetOnLoad'];

  /**
   * Unique numeric module ID for this gadget instance.  A module id is used to
   * identify persisted instances of gadgets.
   *
   * @type {number}
   * @private
   */
  this.moduleId_ = 0;

  /**
   * Information about the currently visible gadget.
   * @type {osapi.container.GadgetHolder?}
   * @private
   */
  this.currentGadgetHolder_ = undef;

  /**
   * Information about the currently loading gadget.
   * @type {osapi.container.GadgetHolder?}
   * @private
   */
  this.loadingGadgetHolder_ = undef;

  this.onConstructed();
};

osapi.container.GadgetSite.prototype = new osapi.container.Site;

/**
 * @return {undefined|null|number} The numerical moduleId of this gadget, if
 *   set.  May return null or undefined if not set.
 */
osapi.container.GadgetSite.prototype.getModuleId = function() {
  return this.moduleId_;
};

/**
 * If you want to change the moduleId after a gadget has rendered, re-navigate the site.
 *
 * @param {string} url This gadget's url (may not yet be accessible in all cases from the holder).
 * @param {number} mid The numerical moduleId for this gadget to use.
 * @param {function} opt_callback Optional callback to run when the moduleId is set.
 * @private
 */
osapi.container.GadgetSite.prototype.setModuleId_ = function(url, mid, opt_callback) {
  if (mid && this.moduleId_ != mid) {
    var self = this,
        url = osapi.container.util.buildTokenRequestUrl(url, mid);

    if (!self.service_.getCachedGadgetToken(url)) {
      // We need to request a security token for this gadget instance.
      var request = osapi.container.util.newTokenRequest([url]);
      self.service_.getGadgetToken(request, function(response) {
        var ttl, mid;
        if (response && response[url]) {
          if (ttl = response[url][osapi.container.TokenResponse.TOKEN_TTL]) {
            self.container_.scheduleRefreshTokens_(ttl);
          }
          var mid = response[url][osapi.container.TokenResponse.MODULE_ID];
          if (mid || mid == 0) {
            self.moduleId_ = mid;
          }
        }
        if (opt_callback) {
          opt_callback();
        }
      });
      return;
    }
  }
  if (opt_callback) {
    opt_callback();
  }
};

/**
 * @inheritDoc
 */
osapi.container.GadgetSite.prototype.getActiveSiteHolder = function() {
  return this.loadingGadgetHolder_ || this.currentGadgetHolder_;
};

/**
 * @inheritDoc
 */
osapi.container.GadgetSite.prototype.setTitle = function(title) {
  osapi.container.Site.prototype.setTitle.call(this, title);
  // sometimes there are 2 holders
  if (this.loadingGadgetHolder_ && this.currentGadgetHolder_) {
    // loadingGadgetHolder_ was set by super call
    this.currentGadgetHolder_.setTitle(title); // set my other one.
  }
  return this;
};

/**
 * Returns configuration of a feature with a given name. Defaults to current
 * loading or visible gadget if no metadata is passed in.
 * @param {string} name Name of the feature.
 * @param {Object=} opt_gadgetInfo Optional gadget info.
 * @return {Object} JSON representing the feature.
 */
osapi.container.GadgetSite.prototype.getFeature = function(name, opt_gadgetInfo) {
  var gadgetInfo = opt_gadgetInfo || this.getActiveSiteHolder().getGadgetInfo();
  return gadgetInfo[osapi.container.MetadataResponse.FEATURES] &&
      gadgetInfo[osapi.container.MetadataResponse.FEATURES][name];
};

/**
 * Render a gadget in the site, by URI of the gadget XML.
 * @param {string} gadgetUrl The absolute URL to gadget.
 * @param {Object} viewParams Look at osapi.container.ViewParam.
 * @param {Object} renderParams Look at osapi.container.RenderParam.
 * @param {function(Object)=} opt_callback Function called with gadget info
 *     after navigation has occurred.
 */
osapi.container.GadgetSite.prototype.navigateTo = function(
    gadgetUrl, viewParams, renderParams, opt_callback) {
  var start = osapi.container.util.getCurrentTimeMs();
  var cached = this.service_.getCachedGadgetMetadata(gadgetUrl);
  var callback = opt_callback || function() {};
  var request = osapi.container.util.newMetadataRequest([gadgetUrl]);
  var self = this;

  this.service_.getGadgetMetadata(request, function(response) {
    var xrt = (!cached) ? (osapi.container.util.getCurrentTimeMs() - start) : 0;
    var gadgetInfo = response[gadgetUrl];
    if (gadgetInfo.error) {
      var message = ['Failed to navigate for gadget ', gadgetUrl, '.'].join('');
      gadgets.warn(message);

      message = ['Detailed error: ', gadgetInfo.error.code || '', ' ', gadgetInfo.error.message || ''].join('');
      gadgets.log(message);
      callback(gadgetInfo);
    } else {
      var moduleId = renderParams[osapi.container.RenderParam.MODULE_ID] || 0;
      self.setModuleId_(gadgetUrl, moduleId, function() {
        self.container_.applyLifecycleCallbacks_(osapi.container.CallbackType.ON_BEFORE_RENDER,
                gadgetInfo);
        self.render(gadgetInfo, viewParams, renderParams);
        callback(gadgetInfo);
      });
    }

    // Return metadata server response time.
    var timingInfo = {};
    timingInfo[osapi.container.NavigateTiming.URL] = gadgetUrl;
    timingInfo[osapi.container.NavigateTiming.ID] = self.id_;
    timingInfo[osapi.container.NavigateTiming.START] = start;
    timingInfo[osapi.container.NavigateTiming.XRT] = xrt;
    self.onNavigateTo(timingInfo);

  });
};


/**
 * Provide overridable callback invoked when navigateTo is completed.
 * @param {Object} data the statistic/timing information to return.
 */
osapi.container.GadgetSite.prototype.onNavigateTo = function(data) {
  if (this.navigateCallback_) {
    var func = window[this.navigateCallback_];
    if (typeof func === 'function') {
      func(data);
    }
  }
};


/**
 * Render a gadget in this site, using a JSON gadget description.
 *
 * Note: A view provided in either renderParams or viewParams is subject to aliasing if the gadget
 * does not support the view specified.
 *
 * @param {Object} gadgetInfo the JSON gadget description.
 * @param {Object} viewParams Look at osapi.container.ViewParam.
 * @param {Object} renderParams Look at osapi.container.RenderParam.
 * @override
 */
osapi.container.GadgetSite.prototype.render = function(
    gadgetInfo, viewParams, renderParams) {
  var curUrl = this.currentGadgetHolder_ ? this.currentGadgetHolder_.getUrl() : null;

  var previousView = null;
  if (curUrl == gadgetInfo['url']) {
    previousView = this.currentGadgetHolder_.getView();
  }

  // Simple function to find a suitable alias
  var findAliasInfo = function(viewConf) {
    if (typeof viewConf !== 'undefined' && viewConf != null) {
      var aliases = viewConf['aliases'] || [];
      for (var i = 0; i < aliases.length; i++) {
        if (gadgetInfo[osapi.container.MetadataResponse.VIEWS][aliases[i]]) {
          return {'view':aliases[i],
                  'viewInfo':gadgetInfo[osapi.container.MetadataResponse.VIEWS][aliases[i]]};
        }
      }
    }
    return null;
  };

  // Find requested view.
  var view = renderParams[osapi.container.RenderParam.VIEW] ||
      viewParams[osapi.container.ViewParam.VIEW] ||
      previousView;
  var viewInfo = gadgetInfo[osapi.container.MetadataResponse.VIEWS][view];
  if (view && !viewInfo) {
    var aliasInfo = findAliasInfo(gadgets.config.get('views')[view]);
    if (aliasInfo) {
      view = aliasInfo['view'];
      viewInfo = aliasInfo['viewInfo'];
    }
  }

  // Allow default view if requested view is not found.  No sense doing this if the view is already "default".
  if (!viewInfo &&
          renderParams[osapi.container.RenderParam.ALLOW_DEFAULT_VIEW]  &&
          view != osapi.container.GadgetSite.DEFAULT_VIEW_) {
    view = osapi.container.GadgetSite.DEFAULT_VIEW_;
    viewInfo = gadgetInfo[osapi.container.MetadataResponse.VIEWS][view];
    if (!viewInfo) {
      var aliasInfo = findAliasInfo(gadgets.config.get('views')[view]);
      if (aliasInfo) {
        view = aliasInfo['view'];
        viewInfo = aliasInfo['viewInfo'];
      }
    }
  }

  // Check if view exists.
  if (!viewInfo) {
    gadgets.warn(['Unsupported view ', view, ' for gadget ', gadgetInfo['url'], '.'].join(''));
    return;
  }

  // Set the loading gadget holder:
  // 1. If the gadget site already has currentGadgetHolder_ set and no loading element passed,
  //    simply set the current gadget holder as the loading gadget holder.
  // 2. Else, check if caller pass the loading gadget element. If it does then use it to create new
  //    instance of osapi.container.GadgetHolder as the loading gadget holder.
  if (this.currentGadgetHolder_ && !this.loadingGadgetEl_) {
    this.loadingGadgetHolder_ = this.currentGadgetHolder_;
    this.currentGadgetHolder_ = null;
  }
  else {
    // Check if we are passed the loading gadget element
    var el = this.loadingGadgetEl_ || this.el_;
    this.loadingGadgetHolder_ = new osapi.container.GadgetHolder(this, el, this.gadgetOnLoad_);
  }

  var localRenderParams = {};
  for (var key in renderParams) {
    localRenderParams[key] = renderParams[key];
  }

  localRenderParams[osapi.container.RenderParam.VIEW] = view;
  localRenderParams[osapi.container.RenderParam.HEIGHT] =
      renderParams[osapi.container.RenderParam.HEIGHT] ||
      viewInfo[osapi.container.MetadataResponse.PREFERRED_HEIGHT] ||
      gadgetInfo[osapi.container.MetadataResponse.MODULE_PREFS][osapi.container.MetadataResponse.HEIGHT] ||
      String(osapi.container.GadgetSite.DEFAULT_HEIGHT_);
  localRenderParams[osapi.container.RenderParam.WIDTH] =
      renderParams[osapi.container.RenderParam.WIDTH] ||
      viewInfo[osapi.container.MetadataResponse.PREFERRED_WIDTH] ||
      gadgetInfo[osapi.container.MetadataResponse.MODULE_PREFS][osapi.container.MetadataResponse.WIDTH] ||
      String(osapi.container.GadgetSite.DEFAULT_WIDTH_);

  this.updateSecurityToken_(gadgetInfo, localRenderParams);

  this.loadingGadgetHolder_.render(gadgetInfo, viewParams, localRenderParams);
};


/**
 * Called when a gadget loads in the site. Uses double buffer, if present.
 */
osapi.container.GadgetSite.prototype.onRender = function() {
  this.swapBuffers_();

  // Only dispose the current holder set it to the loading holder if a loading holder exists.
  // This protects this method from re-entrant code that can cause the current holder to be
  // removed without a loading holder to take its place
  if (this.loadingGadgetHolder_) {
    if (this.currentGadgetHolder_) {
      this.currentGadgetHolder_.dispose();
    }

    this.currentGadgetHolder_ = this.loadingGadgetHolder_;
    this.loadingGadgetHolder_ = null;
  }
};


/**
 * Sends RPC call to the current/visible gadget.
 * @param {string} serviceName RPC service name to call.
 * @param {function(Object)} callback Function to call upon RPC completion.
 * @param {...number} var_args payload to pass to the recipient.
 */
osapi.container.GadgetSite.prototype.rpcCall = function(
    serviceName, callback, var_args) {
  if (this.currentGadgetHolder_) {
    gadgets.rpc.call(this.currentGadgetHolder_.getIframeId(),
        serviceName, callback, var_args);
  }
};


/**
 * If token has been fetched at least once, set the token to the most recent
 * one. Otherwise, leave it.
 * @param {Object} gadgetInfo The gadgetInfo used to update security token.
 * @param {Object} renderParams Look at osapi.container.RenderParam.
 * @private
 */
osapi.container.GadgetSite.prototype.updateSecurityToken_ = function(gadgetInfo, renderParams) {
  var url = osapi.container.util.buildTokenRequestUrl(gadgetInfo['url'], this.moduleId_),
    tokenInfo = this.service_.getCachedGadgetToken(url);

  if (tokenInfo) {
    var token = tokenInfo[osapi.container.TokenResponse.TOKEN];
    this.loadingGadgetHolder_.setSecurityToken(token);
  }
};

/**
 * @inheritDoc
 */
osapi.container.GadgetSite.prototype.close = function() {
  if (this.loadingGadgetHolder_) {
    this.loadingGadgetHolder_.dispose();
  }
  if (this.currentGadgetHolder_) {
    this.currentGadgetHolder_.dispose();
  }
};

/**
 * Swap the double buffer elements, if there is a double buffer.
 * @private
 */
osapi.container.GadgetSite.prototype.swapBuffers_ = function() {
  // Only process double buffering if loading gadget exists
  if (this.loadingGadgetEl_) {
    this.loadingGadgetEl_.style.left = '';
    this.loadingGadgetEl_.style.position = '';
    this.el_.style.position = 'absolute';
    this.el_.style.left = '-2000px';

    // Swap references;  cur_ will now again be what's visible
    var oldCur = this.el_;
    this.el_ = this.loadingGadgetEl_;
    this.loadingGadgetEl_ = oldCur;
  }
};


/**
 * Key to identify the calling gadget site.
 * @type {string}
 */
osapi.container.GadgetSite.RPC_ARG_KEY = 'gs';


/**
 * Default height of gadget. Refer to --
 * http://code.google.com/apis/gadgets/docs/legacy/reference.html.
 * @type {number}
 * @private
 */
osapi.container.GadgetSite.DEFAULT_HEIGHT_ = 200;


/**
 * Default width of gadget. Refer to --
 * http://code.google.com/apis/gadgets/docs/legacy/reference.html.
 * @type {number}
 * @private
 */
osapi.container.GadgetSite.DEFAULT_WIDTH_ = 320;


/**
 * Default view of gadget.
 * @type {string}
 * @private
 */
osapi.container.GadgetSite.DEFAULT_VIEW_ = 'default';
