// Copyright (c) 2012 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 webRequestInternal API.

var binding = require('binding').Binding.create('webRequestInternal');
var eventBindings = require('event_bindings');
var sendRequest = require('sendRequest').sendRequest;
var validate = require('schemaUtils').validate;
var utils = require('utils');
var idGeneratorNatives = requireNative('id_generator');

var webRequestInternal;

function GetUniqueSubEventName(eventName) {
  return eventName + "/" + idGeneratorNatives.GetNextId();
}

// WebRequestEventImpl object. This is used for special webRequest events
// with extra parameters. Each invocation of addListener creates a new named
// sub-event. That sub-event is associated with the extra parameters in the
// browser process, so that only it is dispatched when the main event occurs
// matching the extra parameters.
//
// Example:
//   chrome.webRequest.onBeforeRequest.addListener(
//       callback, {urls: 'http://*.google.com/*'});
//   ^ callback will only be called for onBeforeRequests matching the filter.
function WebRequestEventImpl(eventName, opt_argSchemas, opt_extraArgSchemas,
                             opt_eventOptions, opt_webViewInstanceId) {
  if (typeof eventName != 'string')
    throw new Error('chrome.WebRequestEvent requires an event name.');

  this.eventName = eventName;
  this.argSchemas = opt_argSchemas;
  this.extraArgSchemas = opt_extraArgSchemas;
  this.webViewInstanceId = opt_webViewInstanceId || 0;
  this.subEvents = [];
  this.eventOptions = eventBindings.parseEventOptions(opt_eventOptions);
  if (this.eventOptions.supportsRules) {
    this.eventForRules =
        new eventBindings.Event(eventName, opt_argSchemas, opt_eventOptions,
                                opt_webViewInstanceId);
  }
}

// Test if the given callback is registered for this event.
WebRequestEventImpl.prototype.hasListener = function(cb) {
  if (!this.eventOptions.supportsListeners)
    throw new Error('This event does not support listeners.');
  return this.findListener_(cb) > -1;
};

// Test if any callbacks are registered fur thus event.
WebRequestEventImpl.prototype.hasListeners = function() {
  if (!this.eventOptions.supportsListeners)
    throw new Error('This event does not support listeners.');
  return this.subEvents.length > 0;
};

// Registers a callback to be called when this event is dispatched. If
// opt_filter is specified, then the callback is only called for events that
// match the given filters. If opt_extraInfo is specified, the given optional
// info is sent to the callback.
WebRequestEventImpl.prototype.addListener =
    function(cb, opt_filter, opt_extraInfo) {
  if (!this.eventOptions.supportsListeners)
    throw new Error('This event does not support listeners.');
  // NOTE(benjhayden) New APIs should not use this subEventName trick! It does
  // not play well with event pages. See downloads.onDeterminingFilename and
  // ExtensionDownloadsEventRouter for an alternative approach.
  var subEventName = GetUniqueSubEventName(this.eventName);
  // Note: this could fail to validate, in which case we would not add the
  // subEvent listener.
  validate($Array.slice(arguments, 1), this.extraArgSchemas);
  webRequestInternal.addEventListener(
      cb, opt_filter, opt_extraInfo, this.eventName, subEventName,
      this.webViewInstanceId);

  var subEvent = new eventBindings.Event(subEventName, this.argSchemas);
  var subEventCallback = cb;
  if (opt_extraInfo && opt_extraInfo.indexOf('blocking') >= 0) {
    var eventName = this.eventName;
    subEventCallback = function() {
      var requestId = arguments[0].requestId;
      try {
        var result = $Function.apply(cb, null, arguments);
        webRequestInternal.eventHandled(
            eventName, subEventName, requestId, result);
      } catch (e) {
        webRequestInternal.eventHandled(
            eventName, subEventName, requestId);
        throw e;
      }
    };
  } else if (opt_extraInfo && opt_extraInfo.indexOf('asyncBlocking') >= 0) {
    var eventName = this.eventName;
    subEventCallback = function() {
      var details = arguments[0];
      var requestId = details.requestId;
      var handledCallback = function(response) {
        webRequestInternal.eventHandled(
            eventName, subEventName, requestId, response);
      };
      $Function.apply(cb, null, [details, handledCallback]);
    };
  }
  $Array.push(this.subEvents,
      {subEvent: subEvent, callback: cb, subEventCallback: subEventCallback});
  subEvent.addListener(subEventCallback);
};

// Unregisters a callback.
WebRequestEventImpl.prototype.removeListener = function(cb) {
  if (!this.eventOptions.supportsListeners)
    throw new Error('This event does not support listeners.');
  var idx;
  while ((idx = this.findListener_(cb)) >= 0) {
    var e = this.subEvents[idx];
    e.subEvent.removeListener(e.subEventCallback);
    if (e.subEvent.hasListeners()) {
      console.error(
          'Internal error: webRequest subEvent has orphaned listeners.');
    }
    $Array.splice(this.subEvents, idx, 1);
  }
};

WebRequestEventImpl.prototype.findListener_ = function(cb) {
  for (var i in this.subEvents) {
    var e = this.subEvents[i];
    if (e.callback === cb) {
      if (e.subEvent.hasListener(e.subEventCallback))
        return i;
      console.error('Internal error: webRequest subEvent has no callback.');
    }
  }

  return -1;
};

WebRequestEventImpl.prototype.addRules = function(rules, opt_cb) {
  if (!this.eventOptions.supportsRules)
    throw new Error('This event does not support rules.');
  this.eventForRules.addRules(rules, opt_cb);
};

WebRequestEventImpl.prototype.removeRules =
    function(ruleIdentifiers, opt_cb) {
  if (!this.eventOptions.supportsRules)
    throw new Error('This event does not support rules.');
  this.eventForRules.removeRules(ruleIdentifiers, opt_cb);
};

WebRequestEventImpl.prototype.getRules = function(ruleIdentifiers, cb) {
  if (!this.eventOptions.supportsRules)
    throw new Error('This event does not support rules.');
  this.eventForRules.getRules(ruleIdentifiers, cb);
};

binding.registerCustomHook(function(api) {
  var apiFunctions = api.apiFunctions;

  apiFunctions.setHandleRequest('addEventListener', function() {
    var args = $Array.slice(arguments);
    sendRequest(this.name, args, this.definition.parameters,
                {forIOThread: true});
  });

  apiFunctions.setHandleRequest('eventHandled', function() {
    var args = $Array.slice(arguments);
    sendRequest(this.name, args, this.definition.parameters,
                {forIOThread: true});
  });
});

var WebRequestEvent = utils.expose('WebRequestEvent',
                                   WebRequestEventImpl,
                                   { functions: [
  'hasListener',
  'hasListeners',
  'addListener',
  'removeListener',
  'addRules',
  'removeRules',
  'getRules'
] });

webRequestInternal = binding.generate();
exports.binding = webRequestInternal;
exports.WebRequestEvent = WebRequestEvent;