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

// Custom binding for the fileSystemProvider API.

var binding = require('binding').Binding.create('fileSystemProvider');
var fileSystemProviderInternal =
    require('binding').Binding.create('fileSystemProviderInternal').generate();
var eventBindings = require('event_bindings');

/**
 * Maximum size of the thumbnail in bytes.
 * @type {number}
 * @const
 */
var METADATA_THUMBNAIL_SIZE_LIMIT = 32 * 1024 * 1024;

/**
 * Regular expression to validate if the thumbnail URI is a valid data URI,
 * taking into account allowed formats.
 * @type {RegExp}
 * @const
 */
var METADATA_THUMBNAIL_FORMAT = new RegExp(
    '^data:image/(png|jpeg|webp);', 'i');

/**
 * Annotates a date with its serialized value.
 * @param {Date} date Input date.
 * @return {Date} Date with an extra <code>value</code> attribute.
 */
function annotateDate(date) {
  // Copy in case the input date is frozen.
  var result = new Date(date.getTime());
  result.value = result.toString();
  return result;
}

/**
 * Verifies if the passed image URI is valid.
 * @param {*} uri Image URI.
 * @return {boolean} True if valid, valse otherwise.
 */
function verifyImageURI(uri) {
  // The URI is specified by a user, so the type may be incorrect.
  if (typeof uri != 'string' && !(uri instanceof String))
    return false;

  return METADATA_THUMBNAIL_FORMAT.test(uri);
}

/**
 * Verifies if the passed metadata is valid.
 * @param {!GetMetadataOptions|!ReadDirectoryOptions} options
 * @return {boolean} True if valid, false if invalid.
 */
function verifyMetadata(options, metadata) {
  // Ideally we'd like to consider the following as errors, but for backward
  // compatibility they are warnings only.
  if (!options.isDirectory && metadata.isDirectory !== undefined)
    console.warn('IsDirectory specified, but not requested.');
  if (!options.name && metadata.name !== undefined)
    console.warn('Name specified, but not requested.');
  if (!options.size && metadata.size !== undefined)
    console.warn('Size specified, but not requested.');
  if (!options.modificationTime && metadata.modificationTime !== undefined)
    console.warn('Last modification time specified, but not requested.');
  if (!options.mimeType && metadata.mimeType !== undefined) {
    console.warn('MIME type specified, but not requested.');
  } else {
    if (metadata.mimeType === '') {
      warning = 'MIME type must not be an empty string.' +
          'If unknown, then do not set it.';
    }
  }

  if (options.isDirectory && metadata.isDirectory === undefined) {
    console.error('IsDirectory is required for this request.');
    return false;
  }

  if (options.name && metadata.name === undefined) {
    console.error('Name is required for this request.');
    return false;
  }

  if (options.size && metadata.size === undefined) {
    console.error('Size is required for this request.');
    return false;
  }

  if (options.modificationTime && metadata.modificationTime === undefined) {
    console.error('Last modification time is required for this request.');
    return false;
  }

  // It is invalid to return a thumbnail when it's not requested. The
  // restriction is added in order to avoid fetching the thumbnail while
  // it's not needed.
  if (!options.thumbnail && metadata.thumbnail !== undefined) {
    console.error('Thumbnail data provided, but not requested.');
    return false;
  }

  // Check the format and size. Note, that in the C++ layer, there is
  // another sanity check to avoid passing any evil URL.
  if (metadata.thumbnail !== undefined && !verifyImageURI(metadata.thumbnail)) {
    console.error('Thumbnail format invalid.');
    return false;
  }

  if (metadata.thumbnail !== undefined &&
      metadata.thumbnail.length > METADATA_THUMBNAIL_SIZE_LIMIT) {
    console.error('Thumbnail data too large.');
    return false;
  }

  return true;
}

/**
 * Annotates an entry metadata by serializing its modifiedTime value.
 * @param {EntryMetadata} metadata Input metadata.
 * @return {EntryMetadata} metadata Annotated metadata, which can be passed
 *     back to the C++ layer.
 */
function annotateMetadata(metadata) {
  var result = {};
  if (metadata.isDirectory !== undefined)
    result.isDirectory = metadata.isDirectory;
  if (metadata.name !== undefined)
    result.name = metadata.name;
  if (metadata.size !== undefined)
    result.size = metadata.size;
  if (metadata.modificationTime !== undefined)
    result.modificationTime = annotateDate(metadata.modificationTime);
  if (metadata.mimeType !== undefined)
    result.mimeType = metadata.mimeType;
  if (metadata.thumbnail !== undefined)
    result.thumbnail = metadata.thumbnail;
  return result;
}

/**
 * Massages arguments of an event raised by the File System Provider API.
 * @param {Array<*>} args Input arguments.
 * @param {function(Array<*>)} dispatch Closure to be called with massaged
 *     arguments.
 */
function massageArgumentsDefault(args, dispatch) {
  var executionStart = Date.now();
  var options = args[0];
  var onSuccessCallback = function(hasNext) {
    fileSystemProviderInternal.operationRequestedSuccess(
        options.fileSystemId, options.requestId, Date.now() - executionStart);
  };
  var onErrorCallback = function(error) {
    fileSystemProviderInternal.operationRequestedError(
        options.fileSystemId, options.requestId, error,
        Date.now() - executionStart);
  }
  dispatch([options, onSuccessCallback, onErrorCallback]);
}

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onUnmountRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onGetMetadataRequested',
    function(args, dispatch) {
      var executionStart = Date.now();
      var options = args[0];
      var onSuccessCallback = function(metadata) {
        if (!verifyMetadata(options, metadata)) {
          fileSystemProviderInternal.operationRequestedError(
              options.fileSystemId, options.requestId, 'FAILED',
              Date.now() - executionStart);
          return;
        }

        fileSystemProviderInternal.getMetadataRequestedSuccess(
            options.fileSystemId,
            options.requestId,
            annotateMetadata(metadata),
            Date.now() - executionStart);
      };

      var onErrorCallback = function(error) {
        fileSystemProviderInternal.operationRequestedError(
            options.fileSystemId, options.requestId, error,
            Date.now() - executionStart);
      }

      dispatch([options, onSuccessCallback, onErrorCallback]);
    });

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onGetActionsRequested',
    function(args, dispatch) {
      var executionStart = Date.now();
      var options = args[0];
      var onSuccessCallback = function(actions) {
        fileSystemProviderInternal.getActionsRequestedSuccess(
            options.fileSystemId,
            options.requestId,
            actions,
            Date.now() - executionStart);
      };

      var onErrorCallback = function(error) {
        fileSystemProviderInternal.operationRequestedError(
            options.fileSystemId, options.requestId, error,
            Date.now() - executionStart);
      }

      dispatch([options, onSuccessCallback, onErrorCallback]);
    });

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onReadDirectoryRequested',
    function(args, dispatch) {
      var executionStart = Date.now();
      var options = args[0];
      var onSuccessCallback = function(entries, hasNext) {
        var error = false;
        for (var i = 0; i < entries; i++) {
          if (!validateMetadata(options, entries[i])) {
            error = true;
            return;
          }
        }

        if (error) {
          fileSystemProviderInternal.operationRequestedError(
              options.fileSystemId, options.requestId, 'FAILED',
              Date.now() - executionStart);
          return;
        }

        var annotatedEntries = entries.map(annotateMetadata);
        fileSystemProviderInternal.readDirectoryRequestedSuccess(
            options.fileSystemId, options.requestId, annotatedEntries, hasNext,
            Date.now() - executionStart);
      };

      var onErrorCallback = function(error) {
        fileSystemProviderInternal.operationRequestedError(
            options.fileSystemId, options.requestId, error,
            Date.now() - executionStart);
      }
      dispatch([options, onSuccessCallback, onErrorCallback]);
    });

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onOpenFileRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onCloseFileRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onReadFileRequested',
    function(args, dispatch) {
      var executionStart = Date.now();
      var options = args[0];
      var onSuccessCallback = function(data, hasNext) {
        fileSystemProviderInternal.readFileRequestedSuccess(
            options.fileSystemId, options.requestId, data, hasNext,
            Date.now() - executionStart);
      };
      var onErrorCallback = function(error) {
        fileSystemProviderInternal.operationRequestedError(
            options.fileSystemId, options.requestId, error,
            Date.now() - executionStart);
      }
      dispatch([options, onSuccessCallback, onErrorCallback]);
    });

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onCreateDirectoryRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onDeleteEntryRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onCreateFileRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onCopyEntryRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onMoveEntryRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onTruncateRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onWriteFileRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onAbortRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onObserveDirectoryRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onUnobserveEntryRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onAddWatcherRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onRemoveWatcherRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onConfigureRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onExecuteActionRequested',
    massageArgumentsDefault);

eventBindings.registerArgumentMassager(
    'fileSystemProvider.onMountRequested',
    function(args, dispatch) {
      var onSuccessCallback = function() {
        // TODO(mtomasz): To be implemented.
      };
      var onErrorCallback = function(error) {
        // TODO(mtomasz): To be implemented.
      }
      dispatch([onSuccessCallback, onErrorCallback]);
    });

exports.binding = binding.generate();
