// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @typedef {{
 *   cache: (boolean|undefined),
 *   priority: (number|undefined),
 *   taskId: number,
 *   timestamp: (number|undefined),
 *   url: string
 * }}
 */
var LoadImageRequest;

/**
 * Creates and starts downloading and then resizing of the image. Finally,
 * returns the image using the callback.
 *
 * @param {string} id Request ID.
 * @param {Cache} cache Cache object.
 * @param {LoadImageRequest} request Request message as a hash array.
 * @param {function(Object)} callback Callback used to send the response.
 * @constructor
 */
function Request(id, cache, request, callback) {
  /**
   * @type {string}
   * @private
   */
  this.id_ = id;

  /**
   * @type {Cache}
   * @private
   */
  this.cache_ = cache;

  /**
   * @type {LoadImageRequest}
   * @private
   */
  this.request_ = request;

  /**
   * @type {function(Object)}
   * @private
   */
  this.sendResponse_ = callback;

  /**
   * Temporary image used to download images.
   * @type {Image}
   * @private
   */
  this.image_ = new Image();

  /**
   * MIME type of the fetched image.
   * @type {?string}
   * @private
   */
  this.contentType_ = null;

  /**
   * Used to download remote images using http:// or https:// protocols.
   * @type {AuthorizedXHR}
   * @private
   */
  this.xhr_ = new AuthorizedXHR();

  /**
   * Temporary canvas used to resize and compress the image.
   * @type {HTMLCanvasElement}
   * @private
   */
  this.canvas_ =
      /** @type {HTMLCanvasElement} */ (document.createElement('canvas'));

  /**
   * @type {CanvasRenderingContext2D}
   * @private
   */
  this.context_ =
      /** @type {CanvasRenderingContext2D} */ (this.canvas_.getContext('2d'));

  /**
   * Callback to be called once downloading is finished.
   * @type {?function()}
   * @private
   */
  this.downloadCallback_ = null;
}

/**
 * Returns ID of the request.
 * @return {string} Request ID.
 */
Request.prototype.getId = function() {
  return this.id_;
};

/**
 * Returns priority of the request. The higher priority, the faster it will
 * be handled. The highest priority is 0. The default one is 2.
 *
 * @return {number} Priority.
 */
Request.prototype.getPriority = function() {
  return (this.request_.priority !== undefined) ? this.request_.priority : 2;
};

/**
 * Tries to load the image from cache if exists and sends the response.
 *
 * @param {function()} onSuccess Success callback.
 * @param {function()} onFailure Failure callback.
 */
Request.prototype.loadFromCacheAndProcess = function(onSuccess, onFailure) {
  this.loadFromCache_(
      function(data, width, height) {  // Found in cache.
        this.sendImageData_(data, width, height);
        onSuccess();
      }.bind(this),
      onFailure);  // Not found in cache.
};

/**
 * Tries to download the image, resizes and sends the response.
 * @param {function()} callback Completion callback.
 */
Request.prototype.downloadAndProcess = function(callback) {
  if (this.downloadCallback_)
    throw new Error('Downloading already started.');

  this.downloadCallback_ = callback;
  this.downloadOriginal_(this.onImageLoad_.bind(this),
                         this.onImageError_.bind(this));
};

/**
 * Fetches the image from the persistent cache.
 *
 * @param {function(string, number, number)} onSuccess Success callback.
 * @param {function()} onFailure Failure callback.
 * @private
 */
Request.prototype.loadFromCache_ = function(onSuccess, onFailure) {
  var cacheKey = Cache.createKey(this.request_);

  if (!cacheKey) {
    // Cache key is not provided for the request.
    onFailure();
    return;
  }

  if (!this.request_.cache) {
    // Cache is disabled for this request; therefore, remove it from cache
    // if existed.
    this.cache_.removeImage(cacheKey);
    onFailure();
    return;
  }

  if (!this.request_.timestamp) {
    // Persistent cache is available only when a timestamp is provided.
    onFailure();
    return;
  }

  this.cache_.loadImage(cacheKey,
                        this.request_.timestamp,
                        onSuccess,
                        onFailure);
};

/**
 * Saves the image to the persistent cache.
 *
 * @param {string} data The image's data.
 * @param {number} width Image width.
 * @param {number} height Image height.
 * @private
 */
Request.prototype.saveToCache_ = function(data, width, height) {
  if (!this.request_.cache || !this.request_.timestamp) {
    // Persistent cache is available only when a timestamp is provided.
    return;
  }

  var cacheKey = Cache.createKey(this.request_);
  if (!cacheKey) {
    // Cache key is not provided for the request.
    return;
  }

  this.cache_.saveImage(cacheKey,
                        data,
                        width,
                        height,
                        this.request_.timestamp);
};

/**
 * Downloads an image directly or for remote resources using the XmlHttpRequest.
 *
 * @param {function()} onSuccess Success callback.
 * @param {function()} onFailure Failure callback.
 * @private
 */
Request.prototype.downloadOriginal_ = function(onSuccess, onFailure) {
  this.image_.onload = function() {
    URL.revokeObjectURL(this.image_.src);
    onSuccess();
  }.bind(this);
  this.image_.onerror = function() {
    URL.revokeObjectURL(this.image_.src);
    onFailure();
  }.bind(this);

  // Download data urls directly since they are not supported by XmlHttpRequest.
  var dataUrlMatches = this.request_.url.match(/^data:([^,;]*)[,;]/);
  if (dataUrlMatches) {
    this.image_.src = this.request_.url;
    this.contentType_ = dataUrlMatches[1];
    return;
  }

  // Fetch the image via authorized XHR and parse it.
  var parseImage = function(contentType, blob) {
    if (contentType)
      this.contentType_ = contentType;

    this.image_.src = URL.createObjectURL(blob);
  }.bind(this);

  // Request raw data via XHR.
  this.xhr_.load(this.request_.url, parseImage, onFailure);
};

/**
 * Creates a XmlHttpRequest wrapper with injected OAuth2 authentication headers.
 * @constructor
 */
function AuthorizedXHR() {
  this.xhr_ = null;
  this.aborted_ = false;
}

/**
 * A map which is used to estimate content type from extension.
 * @enum {string}
 */
AuthorizedXHR.ExtensionContentTypeMap = {
  gif: 'image/gif',
  png: 'image/png',
  svg: 'image/svg',
  bmp: 'image/bmp',
  jpg: 'image/jpeg',
  jpeg: 'image/jpeg'
};

/**
 * Aborts the current request (if running).
 */
AuthorizedXHR.prototype.abort = function() {
  this.aborted_ = true;
  if (this.xhr_)
    this.xhr_.abort();
};

/**
 * Loads an image using a OAuth2 token. If it fails, then tries to retry with
 * a refreshed OAuth2 token.
 *
 * @param {string} url URL to the resource to be fetched.
 * @param {function(string, Blob)} onSuccess Success callback with the content
 *     type and the fetched data.
 * @param {function()} onFailure Failure callback.
 */
AuthorizedXHR.prototype.load = function(url, onSuccess, onFailure) {
  this.aborted_ = false;

  // Do not call any callbacks when aborting.
  var onMaybeSuccess = /** @type {function(string, Blob)} */ (
      function(contentType, response) {
        // When content type is not available, try to estimate it from url.
        if (!contentType) {
          contentType = AuthorizedXHR.ExtensionContentTypeMap[
              this.extractExtension_(url)];
        }

        if (!this.aborted_)
          onSuccess(contentType, response);
      }.bind(this));

  var onMaybeFailure = /** @type {function(number=)} */ (
      function(opt_code) {
        if (!this.aborted_)
          onFailure();
      }.bind(this));

  // Fetches the access token and makes an authorized call. If refresh is true,
  // then forces refreshing the access token.
  var requestTokenAndCall = function(refresh, onInnerSuccess, onInnerFailure) {
    chrome.fileManagerPrivate.requestAccessToken(refresh, function(token) {
      if (this.aborted_)
        return;
      if (!token) {
        onInnerFailure();
        return;
      }
      this.xhr_ = AuthorizedXHR.load_(
          token, url, onInnerSuccess, onInnerFailure);
    }.bind(this));
  }.bind(this);

  // Refreshes the access token and retries the request.
  var maybeRetryCall = function(code) {
    if (this.aborted_)
      return;
    requestTokenAndCall(true, onMaybeSuccess, onMaybeFailure);
  }.bind(this);

  // Do not request a token for local resources, since it is not necessary.
  if (/^filesystem:/.test(url)) {
    // The query parameter is workaround for
    // crbug.com/379678, which force to obtain the latest contents of the image.
    var noCacheUrl = url + '?nocache=' + Date.now();
    this.xhr_ = AuthorizedXHR.load_(
        null,
        noCacheUrl,
        onMaybeSuccess,
        onMaybeFailure);
    return;
  }

  // Make the request with reusing the current token. If it fails, then retry.
  requestTokenAndCall(false, onMaybeSuccess, maybeRetryCall);
};

/**
 * Extracts extension from url.
 * @param {string} url Url.
 * @return {string} Extracted extensiion, e.g. png.
 */
AuthorizedXHR.prototype.extractExtension_ = function(url) {
  var result = (/\.([a-zA-Z]+)$/i).exec(url);
  return result ? result[1] : '';
};

/**
 * Fetches data using authorized XmlHttpRequest with the provided OAuth2 token.
 * If the token is invalid, the request will fail.
 *
 * @param {?string} token OAuth2 token to be injected to the request. Null for
 *     no token.
 * @param {string} url URL to the resource to be fetched.
 * @param {function(string, Blob)} onSuccess Success callback with the content
 *     type and the fetched data.
 * @param {function(number=)} onFailure Failure callback with the error code
 *     if available.
 * @return {XMLHttpRequest} XHR instance.
 * @private
 */
AuthorizedXHR.load_ = function(token, url, onSuccess, onFailure) {
  var xhr = new XMLHttpRequest();
  xhr.responseType = 'blob';

  xhr.onreadystatechange = function() {
    if (xhr.readyState != 4)
      return;
    if (xhr.status != 200) {
      onFailure(xhr.status);
      return;
    }
    var contentType = xhr.getResponseHeader('Content-Type');
    onSuccess(contentType, /** @type {Blob} */ (xhr.response));
  }.bind(this);

  // Perform a xhr request.
  try {
    xhr.open('GET', url, true);
    if (token)
      xhr.setRequestHeader('Authorization', 'Bearer ' + token);
    xhr.send();
  } catch (e) {
    onFailure();
  }

  return xhr;
};

/**
 * Sends the resized image via the callback. If the image has been changed,
 * then packs the canvas contents, otherwise sends the raw image data.
 *
 * @param {boolean} imageChanged Whether the image has been changed.
 * @private
 */
Request.prototype.sendImage_ = function(imageChanged) {
  var imageData;
  var width;
  var height;
  if (!imageChanged) {
    // The image hasn't been processed, so the raw data can be directly
    // forwarded for speed (no need to encode the image again).
    imageData = this.image_.src;
    width = this.image_.width;
    height = this.image_.height;
  } else {
    // The image has been resized or rotated, therefore the canvas has to be
    // encoded to get the correct compressed image data.
    width = this.canvas_.width;
    height = this.canvas_.height;

    switch (this.contentType_) {
      case 'image/gif':
      case 'image/png':
      case 'image/svg':
      case 'image/bmp':
        imageData = this.canvas_.toDataURL('image/png');
        break;
      case 'image/jpeg':
      default:
        imageData = this.canvas_.toDataURL('image/jpeg', 0.9);
    }
  }

  // Send and store in the persistent cache.
  this.sendImageData_(imageData, width, height);
  this.saveToCache_(imageData, width, height);
};

/**
 * Sends the resized image via the callback.
 * @param {string} data Compressed image data.
 * @param {number} width Width.
 * @param {number} height Height.
 * @private
 */
Request.prototype.sendImageData_ = function(data, width, height) {
  this.sendResponse_({
    status: 'success', data: data, width: width, height: height,
    taskId: this.request_.taskId
  });
};

/**
 * Handler, when contents are loaded into the image element. Performs resizing
 * and finalizes the request process.
 * @private
 */
Request.prototype.onImageLoad_ = function() {
  // Perform processing if the url is not a data url, or if there are some
  // operations requested.
  if (!this.request_.url.match(/^data/) ||
      ImageLoader.shouldProcess(this.image_.width,
                                this.image_.height,
                                this.request_)) {
    ImageLoader.resize(this.image_, this.canvas_, this.request_);
    this.sendImage_(true);  // Image changed.
  } else {
    this.sendImage_(false);  // Image not changed.
  }
  this.cleanup_();
  this.downloadCallback_();
};

/**
 * Handler, when loading of the image fails. Sends a failure response and
 * finalizes the request process.
 * @private
 */
Request.prototype.onImageError_ = function() {
  this.sendResponse_(
      {status: 'error', taskId: this.request_.taskId});
  this.cleanup_();
  this.downloadCallback_();
};

/**
 * Cancels the request.
 */
Request.prototype.cancel = function() {
  this.cleanup_();

  // If downloading has started, then call the callback.
  if (this.downloadCallback_)
    this.downloadCallback_();
};

/**
 * Cleans up memory used by this request.
 * @private
 */
Request.prototype.cleanup_ = function() {
  this.image_.onerror = function() {};
  this.image_.onload = function() {};

  // Transparent 1x1 pixel gif, to force garbage collecting.
  this.image_.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAA' +
      'ABAAEAAAICTAEAOw==';

  this.xhr_.onload = function() {};
  this.xhr_.abort();

  // Dispose memory allocated by Canvas.
  this.canvas_.width = 0;
  this.canvas_.height = 0;
};
