/**
Some credit for this helper goes to http://github.com/YuzuJS/setImmediate
*/

import { root } from './root';

export class ImmediateDefinition {
  setImmediate: (cb: () => void) => number;

  clearImmediate: (handle: number) => void;

  private identify(o: any): string {
    return this.root.Object.prototype.toString.call(o);
  }

  tasksByHandle: any;

  nextHandle: number;

  currentlyRunningATask: boolean;

  constructor(private root: any) {
    if (root.setImmediate && typeof root.setImmediate === 'function') {
      this.setImmediate = root.setImmediate.bind(root);
      this.clearImmediate = root.clearImmediate.bind(root);
    } else {
      this.nextHandle = 1;
      this.tasksByHandle = {};
      this.currentlyRunningATask = false;

      // Don't get fooled by e.g. browserify environments.
      if (this.canUseProcessNextTick()) {
        // For Node.js before 0.9
        this.setImmediate = this.createProcessNextTickSetImmediate();
      } else if (this.canUsePostMessage()) {
        // For non-IE10 modern browsers
        this.setImmediate = this.createPostMessageSetImmediate();
      } else if (this.canUseMessageChannel()) {
        // For web workers, where supported
        this.setImmediate = this.createMessageChannelSetImmediate();
      } else if (this.canUseReadyStateChange()) {
        // For IE 6–8
        this.setImmediate = this.createReadyStateChangeSetImmediate();
      } else {
        // For older browsers
        this.setImmediate = this.createSetTimeoutSetImmediate();
      }

      let ci = function clearImmediate(handle: any) {
        delete (<any>clearImmediate).instance.tasksByHandle[handle];
      };

      (<any>ci).instance = this;

      this.clearImmediate = ci;
    }
  }

  canUseProcessNextTick() {
    return this.identify(this.root.process) === '[object process]';
  }

  canUseMessageChannel() {
    return Boolean(this.root.MessageChannel);
  }

  canUseReadyStateChange() {
    const document = this.root.document;
    return Boolean(document && 'onreadystatechange' in document.createElement('script'));
  }

  canUsePostMessage() {
    const root = this.root;
    // The test against `importScripts` prevents this implementation from being installed inside a web worker,
    // where `root.postMessage` means something completely different and can't be used for this purpose.
    if (root.postMessage && !root.importScripts) {
      let postMessageIsAsynchronous = true;
      let oldOnMessage = root.onmessage;
      root.onmessage = function() {
        postMessageIsAsynchronous = false;
      };
      root.postMessage('', '*');
      root.onmessage = oldOnMessage;
      return postMessageIsAsynchronous;
    }

    return false;
  }

  // This function accepts the same arguments as setImmediate, but
  // returns a function that requires no arguments.
  partiallyApplied(handler: any, ...args: any[]) {
    let fn = function result () {
      const { handler, args } = <any>result;
      if (typeof handler === 'function') {
        handler.apply(undefined, args);
      } else {
        (new Function('' + handler))();
      }
    };

    (<any>fn).handler = handler;
    (<any>fn).args = args;

    return fn;
  }

  addFromSetImmediateArguments(args: any[]) {
    this.tasksByHandle[this.nextHandle] = this.partiallyApplied.apply(undefined, args);
    return this.nextHandle++;
  }

  createProcessNextTickSetImmediate() {
    let fn = function setImmediate() {
      const { instance } = (<any>setImmediate);
      let handle = instance.addFromSetImmediateArguments(arguments);
      instance.root.process.nextTick(instance.partiallyApplied(instance.runIfPresent, handle));
      return handle;
    };

    (<any>fn).instance = this;

    return fn;
  }

  createPostMessageSetImmediate() {
    // Installs an event handler on `global` for the `message` event: see
    // * https://developer.mozilla.org/en/DOM/window.postMessage
    // * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages
    const root = this.root;

    let messagePrefix = 'setImmediate$' + root.Math.random() + '$';
    let onGlobalMessage = function globalMessageHandler(event: any) {
      const instance = (<any>globalMessageHandler).instance;
      if (event.source === root &&
        typeof event.data === 'string' &&
        event.data.indexOf(messagePrefix) === 0) {
        instance.runIfPresent(+event.data.slice(messagePrefix.length));
      }
    };
    (<any>onGlobalMessage).instance = this;

    root.addEventListener('message', onGlobalMessage, false);

    let fn = function setImmediate() {
      const { messagePrefix, instance } = (<any>setImmediate);
      let handle = instance.addFromSetImmediateArguments(arguments);
      instance.root.postMessage(messagePrefix + handle, '*');
      return handle;
    };

    (<any>fn).instance = this;
    (<any>fn).messagePrefix = messagePrefix;

    return fn;
  }

  runIfPresent(handle: any) {
    // From the spec: 'Wait until any invocations of this algorithm started before this one have completed.'
    // So if we're currently running a task, we'll need to delay this invocation.
    if (this.currentlyRunningATask) {
      // Delay by doing a setTimeout. setImmediate was tried instead, but in Firefox 7 it generated a
      // 'too much recursion' error.
      this.root.setTimeout(this.partiallyApplied(this.runIfPresent, handle), 0);
    } else {
      let task = this.tasksByHandle[handle];
      if (task) {
        this.currentlyRunningATask = true;
        try {
          task();
        } finally {
          this.clearImmediate(handle);
          this.currentlyRunningATask = false;
        }
      }
    }
  }

  createMessageChannelSetImmediate() {
    let channel = new this.root.MessageChannel();
    channel.port1.onmessage = (event: any) => {
      let handle = event.data;
      this.runIfPresent(handle);
    };

    let fn = function setImmediate() {
      const { channel, instance } = (<any>setImmediate);
      let handle = instance.addFromSetImmediateArguments(arguments);
      channel.port2.postMessage(handle);
      return handle;
    };

    (<any>fn).channel = channel;
    (<any>fn).instance = this;

    return fn;
  }

  createReadyStateChangeSetImmediate() {
    let fn = function setImmediate() {
      const instance = (<any>setImmediate).instance;
      const root = instance.root;
      const doc = root.document;
      const html = doc.documentElement;

      let handle = instance.addFromSetImmediateArguments(arguments);
      // Create a <script> element; its readystatechange event will be fired asynchronously once it is inserted
      // into the document. Do so, thus queuing up the task. Remember to clean up once it's been called.
      let script = doc.createElement('script');
      script.onreadystatechange = () => {
        instance.runIfPresent(handle);
        script.onreadystatechange = null;
        html.removeChild(script);
        script = null;
      };
      html.appendChild(script);
      return handle;
    };

    (<any>fn).instance = this;

    return fn;
  }

  createSetTimeoutSetImmediate() {
    let fn = function setImmediate() {
      const instance = (<any>setImmediate).instance;
      let handle = instance.addFromSetImmediateArguments(arguments);
      instance.root.setTimeout(instance.partiallyApplied(instance.runIfPresent, handle), 0);
      return handle;
    };

    (<any>fn).instance = this;

    return fn;
  }
}
export const Immediate = new ImmediateDefinition(root);
