# backtick_javascript: true
module Browser

# Class to easily create and dispatch an immediate call.
#
# Immediate calls are deferred function calls that happen as soon as they can
# be scheduled.
#
# Compatibility
# -------------
# The compatibility layer will try various implementations in the following
# order.
#
# + [setImmediate](https://developer.mozilla.org/en-US/docs/Web/API/Window.setImmediate)
# + [postMessage](https://developer.mozilla.org/en-US/docs/Web/API/Window.postMessage)
# + [readystatechange](https://developer.mozilla.org/en-US/docs/Web/Reference/Events/readystatechange)
# + [setTimeout](https://developer.mozilla.org/en/docs/Web/API/window.setTimeout)
#
# The order has been chosen from best to worst for both performance and
# preemptiveness.
class Immediate
  # Create an immediate for the given function which will be called with the
  # arguments and block.
  #
  # @param func [Proc] the function to call
  # @param args [Array] the arguments to call it with
  def initialize(func, args, &block)
    @aborted   = false
    @function  = func
    @arguments = args
    @block     = block
  end

  # @!method dispatch
  #   Dispatch the immediate.

  # @!method prevent
  #   Prevent the immediate from being called once scheduled.
  if Browser.supports? 'Immediate'
    def dispatch
      @id = `window.setImmediate(function() {
        #{@function.call(*@arguments, &@block)};
      })`
    end

    def prevent
      `window.clearImmediate(#@id)`
    end
  elsif Browser.supports? 'Immediate (Internet Explorer)'
    def dispatch
      @id = `window.msSetImmediate(function() {
        #{@function.call(*@arguments, &@block)};
      })`
    end

    def prevent
      `window.msClearImmediate(#@id)`
    end
  elsif Browser.supports? 'Window.send (Asynchronous)'
    # @private
    @@tasks  = {}

    # @private
    @@prefix = "opal.browser.immediate.#{rand(1_000_000)}."

    $window.on :message do |e|
      if String === e.data && e.data.start_with?(@@prefix)
        if task = @@tasks.delete(e.data[@@prefix.length .. -1])
          task[0].call(*task[1], &task[2])
        end
      end
    end

    def dispatch
      @id          = rand(1_000_000).to_s
      @@tasks[@id] = [@function, @arguments, @block]

      $window.send "#{@@prefix}#{@id}"
    end

    def prevent
      @@tasks.delete(@id)
    end
  elsif Browser.supports? 'Event.readystatechange'
    def dispatch
      %x{
        var script = document.createElement("script");

        script.onreadystatechange = function() {
          if (!#{aborted?}) {
            #{@function.call(*@arguments, &@block)};
          }

          script.onreadystatechange = null;
          script.parentNode.removeChild(script);
        };

        document.documentElement.appendChild(script);
      }
    end

    def prevent; end
  else
    def dispatch
      @id = `window.setTimeout(function() {
        #{@function.call(*@arguments, &@block)};
      }, 0)`
    end

    def prevent
      `window.clearTimeout(#@id)`
    end
  end

  # Abort the immediate.
  def abort
    return if aborted?

    @aborted = true
    prevent

    self
  end

  # Check if the immediate has been aborted.
  def aborted?
    @aborted
  end
end

end

module Kernel
  # (see Immediate.new)
  def defer(*args, &block)
    Browser::Immediate.new(block, args).tap(&:dispatch)
  end
end

class Proc
  # (see Immediate.new)
  def defer(*args, &block)
    Browser::Immediate.new(self, args, &block).tap(&:dispatch)
  end
end

class Promise
  # Create a promise which will be resolved with the result of the immediate.
  #
  # @param args [Array] the arguments the block will be called with
  def self.defer(*args, &block)
    new.tap {|promise|
      proc {
        begin
          promise.resolve(block.call(*args))
        rescue Exception => e
          promise.reject(e)
        end
      }.defer
    }
  end
end
