// Copyright 2010 Google Inc. All Rights Reserved.

/**
 * @fileoverview Classes and functions used during recording and playback.
 */

var Benchmark = Benchmark || {};

Benchmark.functionList = [
  ['setTimeout', 'setTimeout'],
  ['clearTimeout', 'clearTimeout'],
  ['setInterval', 'setInterval'],
  ['clearInterval', 'clearInterval'],
  ['XMLHttpRequest', 'XMLHttpRequest'],
  ['addEventListenerToWindow', 'addEventListener'],
  ['addEventListenerToNode', 'addEventListener', ['Node', 'prototype']],
  ['removeEventListenerFromNode', 'removeEventListener', ['Node', 'prototype']],
  ['addEventListenerToXHR', 'addEventListener',
   ['XMLHttpRequest', 'prototype']],
  ['random', 'random', ['Math']],
  ['Date', 'Date'],
  ['documentWriteln', 'writeln', ['document']],
  ['documentWrite', 'write', ['document']]
];

Benchmark.timeoutMapping = [];

Benchmark.ignoredListeners = ['mousemove', 'mouseover', 'mouseout'];

Benchmark.originals = {};

Benchmark.overrides = {
  setTimeout: function(callback, timeout) {
    var event = {type: 'timeout', timeout: timeout};
    var eventId = Benchmark.agent.createAsyncEvent(event);
    var timerId = Benchmark.originals.setTimeout.call(this, function() {
      Benchmark.agent.fireAsyncEvent(eventId, callback);
    }, Benchmark.playback ? 0 : timeout);
    Benchmark.timeoutMapping[timerId] = eventId;
    return timerId;
  },

  clearTimeout: function(timerId) {
    var eventId = Benchmark.timeoutMapping[timerId];
    if (eventId == undefined) return;
    Benchmark.agent.cancelAsyncEvent(eventId);
    Benchmark.originals.clearTimeout.call(this, timerId);
  },

  setInterval: function(callback, timeout) {
    console.warn('setInterval');
  },

  clearInterval: function(timerId) {
    console.warn('clearInterval');
  },

  XMLHttpRequest: function() {
    return new Benchmark.XMLHttpRequestWrapper();
  },

  addEventListener: function(type, listener, useCapture, target, targetType,
                             originalFunction) {
    var event = {type: 'addEventListener', target: targetType, eventType: type};
    var eventId = Benchmark.agent.createAsyncEvent(event);
    listener.eventId = eventId;
    listener.wrapper = function(e) {
      Benchmark.agent.fireAsyncEvent(eventId, function() {
        listener.call(target, e);
      });
    };
    originalFunction.call(target, type, listener.wrapper, useCapture);
  },

  addEventListenerToWindow: function(type, listener, useCapture) {
    if (Benchmark.ignoredListeners.indexOf(type) != -1) return;
    Benchmark.overrides.addEventListener(
        type, listener, useCapture, this, 'window',
        Benchmark.originals.addEventListenerToWindow);
  },

  addEventListenerToNode: function(type, listener, useCapture) {
    if (Benchmark.ignoredListeners.indexOf(type) != -1) return;
    Benchmark.overrides.addEventListener(
        type, listener, useCapture, this, 'node',
        Benchmark.originals.addEventListenerToNode);
  },

  addEventListenerToXHR: function(type, listener, useCapture) {
    Benchmark.overrides.addEventListener(
        type, listener, useCapture, this, 'xhr',
        Benchmark.originals.addEventListenerToXHR);
  },

  removeEventListener: function(type, listener, useCapture, target,
                                originalFunction) {
    Benchmark.agent.cancelAsyncEvent(listener.eventId);
    originalFunction.call(target, listener.wrapper, useCapture);
  },

  removeEventListenerFromWindow: function(type, listener, useCapture) {
    removeEventListener(type, listener, useCapture, this,
                        Benchmark.originals.removeEventListenerFromWindow);
  },

  removeEventListenerFromNode: function(type, listener, useCapture) {
    removeEventListener(type, listener, useCapture, this,
        Benchmark.originals.removeEventListenerFromNode);
  },

  removeEventListenerFromXHR: function(type, listener, useCapture) {
    removeEventListener(type, listener, useCapture, this,
        Benchmark.originals.removeEventListenerFromXHR);
  },

  random: function() {
    return Benchmark.agent.random();
  },

  Date: function() {
    var a = arguments;
    var D = Benchmark.originals.Date, d;
    switch(a.length) {
      case 0: d = new D(Benchmark.agent.dateNow()); break;
      case 1: d = new D(a[0]); break;
      case 2: d = new D(a[0], a[1]); break;
      case 3: d = new D(a[0], a[1], a[2]); break;
      default: Benchmark.die('window.Date', arguments);
    }
    d.getTimezoneOffset = function() { return -240; };
    return d;
  },

  dateNow: function() {
    return Benchmark.agent.dateNow();
  },

  documentWriteln: function() {
    console.warn('writeln');
  },

  documentWrite: function() {
    console.warn('write');
  }
};

/**
 * Replaces window functions specified by Benchmark.functionList with overrides
 * and optionally saves original functions to Benchmark.originals.
 * @param {Object} wnd Window object.
 * @param {boolean} storeOriginals When true, original functions are saved to
 * Benchmark.originals.
 */
Benchmark.installOverrides = function(wnd, storeOriginals) {
  // Substitute window functions with overrides.
  for (var i = 0; i < Benchmark.functionList.length; ++i) {
    var info = Benchmark.functionList[i], object = wnd;
    var propertyName = info[1], pathToProperty = info[2];
    if (pathToProperty)
      for (var j = 0; j < pathToProperty.length; ++j)
        object = object[pathToProperty[j]];
    if (storeOriginals)
      Benchmark.originals[info[0]] = object[propertyName];
    object[propertyName] = Benchmark.overrides[info[0]];
  }
  wnd.__defineSetter__('onload', function() {
    console.warn('window.onload setter')}
  );

  // Substitute window functions of static frames when DOM content is loaded.
  Benchmark.originals.addEventListenerToWindow.call(wnd, 'DOMContentLoaded',
                                                    function() {
    var frames = document.getElementsByTagName('iframe');
    for (var i = 0, frame; frame = frames[i]; ++i) {
      Benchmark.installOverrides(frame.contentWindow);
    }
  }, true);

  // Substitute window functions of dynamically added frames.
  Benchmark.originals.addEventListenerToWindow.call(
      wnd, 'DOMNodeInsertedIntoDocument', function(e) {
        if (e.target.tagName && e.target.tagName.toLowerCase() != 'iframe')
          return;
        if (e.target.contentWindow)
          Benchmark.installOverrides(e.target.contentWindow);
      }, true);
};

// Install overrides on top window.
Benchmark.installOverrides(window, true);

/**
 * window.XMLHttpRequest wrapper. Notifies Benchmark.agent when request is
 * opened, aborted, and when it's ready state changes to DONE.
 * @constructor
 */
Benchmark.XMLHttpRequestWrapper = function() {
  this.request = new Benchmark.originals.XMLHttpRequest();
  this.wrapperReadyState = 0;
};

// Create XMLHttpRequestWrapper functions and property accessors using original
// ones.
(function() {
  var request = new Benchmark.originals.XMLHttpRequest();
  for (var property in request) {
    if (property === 'channel') continue; // Quick fix for FF.
    if (typeof(request[property]) == 'function') {
      (function(property) {
        var f = Benchmark.originals.XMLHttpRequest.prototype[property];
        Benchmark.XMLHttpRequestWrapper.prototype[property] = function() {
          f.apply(this.request, arguments);
        };
      })(property);
    } else {
      (function(property) {
        Benchmark.XMLHttpRequestWrapper.prototype.__defineGetter__(property,
            function() { return this.request[property]; });
        Benchmark.XMLHttpRequestWrapper.prototype.__defineSetter__(property,
            function(value) {
              this.request[property] = value;
            });

      })(property);
    }
  }
})();

// Define onreadystatechange getter.
Benchmark.XMLHttpRequestWrapper.prototype.__defineGetter__('onreadystatechange',
    function() { return this.clientOnReadyStateChange; });

// Define onreadystatechange setter.
Benchmark.XMLHttpRequestWrapper.prototype.__defineSetter__('onreadystatechange',
    function(value) { this.clientOnReadyStateChange = value; });

Benchmark.XMLHttpRequestWrapper.prototype.__defineGetter__('readyState',
    function() { return this.wrapperReadyState; });

Benchmark.XMLHttpRequestWrapper.prototype.__defineSetter__('readyState',
    function() {});


/**
 * Wrapper for XMLHttpRequest.open.
 */
Benchmark.XMLHttpRequestWrapper.prototype.open = function() {
  var url = Benchmark.extractURL(arguments[1]);
  var event = {type: 'request', method: arguments[0], url: url};
  this.eventId = Benchmark.agent.createAsyncEvent(event);

  var request = this.request;
  var requestWrapper = this;
  Benchmark.originals.XMLHttpRequest.prototype.open.apply(request, arguments);
  request.onreadystatechange = function() {
    if (this.readyState != 4 || requestWrapper.cancelled) return;
    var callback = requestWrapper.clientOnReadyStateChange || function() {};
    Benchmark.agent.fireAsyncEvent(requestWrapper.eventId, function() {
      requestWrapper.wrapperReadyState = 4;
      callback.call(request);
    });
  }
};

/**
 * Wrapper for XMLHttpRequest.abort.
 */
Benchmark.XMLHttpRequestWrapper.prototype.abort = function() {
  this.cancelled = true;
  Benchmark.originals.XMLHttpRequest.prototype.abort.apply(
      this.request, arguments);
  Benchmark.agent.cancelAsyncEvent(this.eventId);
};

/**
 * Driver url for reporting results.
 * @const {string}
 */
Benchmark.DRIVER_URL = '/benchmark/';

/**
 * Posts request as json to Benchmark.DRIVER_URL.
 * @param {Object} request Request to post.
 */
Benchmark.post = function(request, async) {
  if (async === undefined) async = true;
  var xmlHttpRequest = new Benchmark.originals.XMLHttpRequest();
  xmlHttpRequest.open("POST", Benchmark.DRIVER_URL, async);
  xmlHttpRequest.setRequestHeader("Content-type", "application/json");
  xmlHttpRequest.send(JSON.stringify(request));
};

/**
 * Extracts url string.
 * @param {(string|Object)} url Object or string representing url.
 * @return {string} Extracted url.
 */
Benchmark.extractURL = function(url) {
  if (typeof(url) == 'string') return url;
  return url.nI || url.G || '';
};


/**
 * Logs error message to console and throws an exception.
 * @param {string} message Error message
 */
Benchmark.die = function(message) {
  // Debugging stuff.
  var position = top.Benchmark.playback ? top.Benchmark.agent.timelinePosition :
                 top.Benchmark.agent.timeline.length;
  message = message + ' at position ' + position;
  console.error(message);
  Benchmark.post({error: message});
  console.log(Benchmark.originals.setTimeout.call(window, function() {}, 9999));
  try { (0)() } catch(ex) { console.error(ex.stack); }
  throw message;
};