"use strict";

var URL = require("url");
var CSSStyleDeclaration = require("cssstyle").CSSStyleDeclaration;
var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
var dom = require("../level1/core");
var NOT_IMPLEMENTED = require("./utils").NOT_IMPLEMENTED;
var createFrom = require("../utils").createFrom;
var History = require("./history");
var VirtualConsole = require("../virtual-console");

var cssSelectorSplitRE = /((?:[^,"']|"[^"]*"|'[^']*')+)/;

function matchesDontThrow(el, selector) {
  try {
    return el.matches(selector);
  } catch (e) {
    return false;
  }
}

function startTimer(window, startFn, stopFn, callback, ms) {
  var res = startFn(callback, ms);
  window.__timers.push([res, stopFn]);
  return res;
}

function stopTimer(window, id) {
  if (typeof id === "undefined") {
    return;
  }
  for (var i in window.__timers) {
    if (window.__timers[i][0] === id) {
      window.__timers[i][1].call(window, id);
      window.__timers.splice(i, 1);
      break;
    }
  }
}

function stopAllTimers(window) {
  window.__timers.forEach(function (t) {
    t[1].call(window, t[0]);
  });
  window.__timers = [];
}

function Window(document) {
  this.__timers = [];

  // TODO: very little of this belongs on the instance; they should be prototype methods instead.

  var window = this;
  this._document = document;
  this.history = new History(this);

  this.addEventListener = function () {
    dom.Node.prototype.addEventListener.apply(window, arguments);
  };
  this.removeEventListener = function () {
    dom.Node.prototype.removeEventListener.apply(window, arguments);
  };
  this.dispatchEvent = function () {
    dom.Node.prototype.dispatchEvent.apply(window, arguments);
  };
  this.raise = function () {
    dom.Node.prototype.raise.apply(window.document, arguments);
  };

  this.setTimeout = function (fn, ms) { return startTimer(window, setTimeout, clearTimeout, fn, ms); };
  this.setInterval = function (fn, ms) { return startTimer(window, setInterval, clearInterval, fn, ms); };
  this.clearInterval = stopTimer.bind(this, window);
  this.clearTimeout = stopTimer.bind(this, window);
  this.__stopAllTimers = stopAllTimers.bind(this, window);
  this.Image = function (width, height) {
    var element = window._document.createElement("img");
    element.width = width;
    element.height = height;
    return element;
  };

  this._virtualConsole = new VirtualConsole();

  function wrapConsoleMethod(method) {
    return function () {
      var args = Array.prototype.slice.call(arguments);
      window._virtualConsole.emit.apply(window._virtualConsole, [method].concat(args));
    };
  }

  this.console = {
    assert: wrapConsoleMethod("assert"),
    clear: wrapConsoleMethod("clear"),
    count: wrapConsoleMethod("count"),
    debug: wrapConsoleMethod("debug"),
    error: wrapConsoleMethod("error"),
    group: wrapConsoleMethod("group"),
    groupCollapse: wrapConsoleMethod("groupCollapse"),
    groupEnd: wrapConsoleMethod("groupEnd"),
    info: wrapConsoleMethod("info"),
    log: wrapConsoleMethod("log"),
    table: wrapConsoleMethod("table"),
    time: wrapConsoleMethod("time"),
    timeEnd: wrapConsoleMethod("timeEnd"),
    trace: wrapConsoleMethod("trace"),
    warn: wrapConsoleMethod("warn")
  };

  this.XMLHttpRequest = function () {
    var xhr = new XMLHttpRequest();
    var lastUrl = "";
    xhr._open = xhr.open;
    xhr.open = function (method, url, async, user, password) {
      url = URL.resolve(window.document.URL, url);
      lastUrl = url;
      return xhr._open(method, url, async, user, password);
    };
    xhr._send = xhr.send;
    xhr.send = function (data) {
      if (window.document.cookie) {
        var cookieDomain = window.document._cookieDomain;
        var url = URL.parse(lastUrl);
        var host = url.host.split(":")[0];
        if (host.indexOf(cookieDomain, host.length - cookieDomain.length) !== -1) {
          xhr.setDisableHeaderCheck(true);
          xhr.setRequestHeader("cookie", window.document.cookie);
          xhr.setDisableHeaderCheck(false);
        }
      }
      return xhr._send(data);
    };
    return xhr;
  };
}

Window.prototype = createFrom(dom || null, {
  constructor: Window,
  // This implements window.frames.length, since window.frames returns a
  // self reference to the window object.  This value is incremented in the
  // HTMLFrameElement init function (see: level2/html.js).
  _length: 0,
  get length() {
    return this._length;
  },
  get document() {
    return this._document;
  },
  get location() {
    return this._document._location;
  },
  close: function () {
    // Recursively close child frame windows, then ourselves.
    var currentWindow = this;
    (function windowCleaner(window) {
      var i;
      // We could call window.frames.length etc, but window.frames just points
      // back to window.
      if (window.length > 0) {
        for (i = 0; i < window.length; i++) {
          windowCleaner(window[i]);
        }
      }
      // We"re already in our own window.close().
      if (window !== currentWindow) {
        window.close();
      }
    })(this);

    if (this._document) {
      if (this._document.body) {
        this._document.body.innerHTML = "";
      }

      if (this._document.close) {
        // We need to empty out the event listener array because
        // document.close() causes "load" event to re-fire.
        this._document._listeners = [];
        this._document.close();
      }
      delete this._document;
    }

    stopAllTimers(currentWindow);
    // Clean up the window"s execution context.
    // dispose() is added by Contextify.
    this.dispose();
  },
  getComputedStyle: function (node) {
    var s = node.style,
        cs = new CSSStyleDeclaration(),
        forEach = Array.prototype.forEach;

    function setPropertiesFromRule(rule) {
      if (!rule.selectorText) {
        return;
      }

      var selectors = rule.selectorText.split(cssSelectorSplitRE);
      var matched = false;
      selectors.forEach(function (selectorText) {
        if (selectorText !== "" && selectorText !== "," && !matched && matchesDontThrow(node, selectorText)) {
          matched = true;
          forEach.call(rule.style, function (property) {
            cs.setProperty(property, rule.style.getPropertyValue(property), rule.style.getPropertyPriority(property));
          });
        }
      });
    }

    forEach.call(node.ownerDocument.styleSheets, function (sheet) {
      forEach.call(sheet.cssRules, function (rule) {
        if (rule.media) {
          if (Array.prototype.indexOf.call(rule.media, "screen") !== -1) {
            forEach.call(rule.cssRules, setPropertiesFromRule);
          }
        } else {
          setPropertiesFromRule(rule);
        }
      });
    });

    forEach.call(s, function (property) {
      cs.setProperty(property, s.getPropertyValue(property), s.getPropertyPriority(property));
    });

    return cs;
  },

  // TODO: all of the below data properties should be getters; right now they are shared between Window instances
  // which is of course bad.
  navigator: {
    get userAgent() { return "Node.js (" + process.platform + "; U; rv:" + process.version + ")"; },
    get appName() { return "Node.js jsDom"; },
    get platform() { return process.platform; },
    get appVersion() { return process.version; },
    noUI: true,
    get cookieEnabled() { return true; }
  },

  name: "nodejs",
  innerWidth: 1024,
  innerHeight: 768,
  outerWidth: 1024,
  outerHeight: 768,
  pageXOffset: 0,
  pageYOffset: 0,
  screenX: 0,
  screenY: 0,
  screenLeft: 0,
  screenTop: 0,
  scrollX: 0,
  scrollY: 0,
  scrollTop: 0,
  scrollLeft: 0,
  alert: NOT_IMPLEMENTED("window.alert"),
  blur: NOT_IMPLEMENTED("window.blur"),
  confirm: NOT_IMPLEMENTED("window.confirm"),
  createPopup: NOT_IMPLEMENTED("window.createPopup"),
  focus: NOT_IMPLEMENTED("window.focus"),
  moveBy: NOT_IMPLEMENTED("window.moveBy"),
  moveTo: NOT_IMPLEMENTED("window.moveTo"),
  open: NOT_IMPLEMENTED("window.open"),
  print: NOT_IMPLEMENTED("window.print"),
  prompt: NOT_IMPLEMENTED("window.prompt"),
  resizeBy: NOT_IMPLEMENTED("window.resizeBy"),
  resizeTo: NOT_IMPLEMENTED("window.resizeTo"),
  scroll: NOT_IMPLEMENTED("window.scroll"),
  scrollBy: NOT_IMPLEMENTED("window.scrollBy"),
  scrollTo: NOT_IMPLEMENTED("window.scrollTo"),
  screen: {
    width: 0,
    height: 0
  },

  // Note: these will not be necessary for newer Node.js versions, which have
  // typed arrays in V8 and thus on every global object. (That is, in newer
  // versions we"ll get `ArrayBuffer` just as automatically as we get
  // `Array`.) But to support older versions, we explicitly set them here.
  Int8Array: global.Int8Array,
  Int16Array: global.Int16Array,
  Int32Array: global.Int32Array,
  Float32Array: global.Float32Array,
  Float64Array: global.Float64Array,
  Uint8Array: global.Uint8Array,
  Uint8ClampedArray: global.Uint8ClampedArray,
  Uint16Array: global.Uint16Array,
  Uint32Array: global.Uint32Array,
  ArrayBuffer: global.ArrayBuffer
});

module.exports = Window;
