module Whenner
  # A deferred object is an operation that will eventually resolve to a result
  # value. A deferred can be in three possible states:
  #
  # * Pending: it has been created but not yet resolved.
  # * Fulfilled: it has been successfully resolved.
  # * Rejected: it has been unsuccessfully resolved.
  #
  # A deferred might transition from pending to fulfilled or rejected, but it
  # will not transition again once resolved (resolved can be either fulfilled
  # or rejected).
  #
  # When a deferred does resolve, it will trigger any applicable callbacks. You
  # can stack on callbacks on a deferred object before it has been resolved and
  # they will be called later. When you register callbacks on an already
  # resolved deferred, the callback will be called immediately. Note that a
  # callback will only be run once.
  #
  # When a callback is in the fulfilled state, it has a value that represents
  # its eventual outcome. When it is rejected, it has a reason.
  class Deferred
    # @return [Promise] a promise for this deferred
    attr_reader :promise

    def initialize
      @promise             = Promise.new(self)
      @state               = :pending
      @fulfilled_callbacks = []
      @rejected_callbacks  = []
      @always_callbacks    = []
    end

    # The value the deferred was resolved with.
    #
    # @raise [UnresolvedError] when the deferred is still pending
    def value
      raise UnresolvedError unless resolved?
      @value
    end

    # The reason the deferred was rejected.
    #
    # @raise [UnresolvedError] when the deferred is still pending
    def reason
      raise UnresolvedError unless resolved?
      @reason
    end

    # @return [Boolean] whether the deferred has not been resolved yet.
    def pending?
      state == :pending
    end

    # @return [Boolean] whether the deferred was successfully resolved.
    def fulfilled?
      state == :fulfilled
    end

    # @return [Boolean] whether the deferred was rejected.
    def rejected?
      state == :rejected
    end

    # @return [Boolean] whether the deferred was either fulfilled or rejected.
    def resolved?
      fulfilled? || rejected?
    end

    # Fulfill this promise with an optional value. The value will be stored in
    # the deferred and passed along to any registered `done` callbacks.
    #
    # When fulfilling a deferred twice, nothing happens.
    #
    # @raise [CannotTransitionError] when it was already fulfilled.
    # @return [Deferred] self
    def fulfill(value = nil)
      raise CannotTransitionError if rejected?
      return if fulfilled?
      unless resolved?
        self.value = value
        resolve_to(:fulfilled)
      end
      self
    end

    # Reject this promise with an optional reason. The reason will be stored in
    # the deferred and passed along to any registered `fail` callbacks.
    #
    # When rejecting a deferred twice, nothing happens.
    #
    # @raise [CannotTransitionError] when it was already fulfilled.
    # @return [Deferred] self
    def reject(reason = nil)
      raise CannotTransitionError if fulfilled?
      return if rejected?
      unless resolved?
        self.reason = reason
        resolve_to(:rejected)
      end
      self
    end

    # @return [Promise]
    def to_promise
      promise
    end


    # Register a callback to be run when the deferred is fulfilled.
    #
    # @yieldparam [DeferredProxy] proxy
    # @return [Promise] a new promise representing the return value
    #   of the deferred, or -- when that return value is a promise itself
    #   -- a promise mimicking that promise.
    def then
      Whenner.defer do |d|
        proxy = DeferredProxy.new(self)
        yield proxy
        pdone = done(&proxy.done)
        pfail = fail(&proxy.fail)
        pdone.done { |v| d.fulfill(v) }
        pdone.fail { |v| d.reject(v) }
        pfail.done { |v| d.reject(v) }
        pfail.fail { |v| d.reject(v) }
      end
    end

    # Register a callback to be run when the deferred is fulfilled.
    #
    # @yieldparam [Object] value
    # @return [Promise] a new promise representing the return value
    #   of the callback, or -- when that return value is a promise itself
    #   -- a promise mimicking that promise.
    def done(&block)
      cb = Callback.new(block)
      fulfilled_callbacks << cb
      cb.call(*callback_response) if fulfilled?
      cb.promise
    end

    # Register a callback to be run when the deferred is rejected.
    #
    # @yieldparam [Object] reason
    # @return [Promise] a new promise representing the return value
    #   of the callback, or -- when that return value is a promise itself
    #   -- a promise mimicking that promise.
    def fail(&block)
      cb = Callback.new(block)
      rejected_callbacks << cb
      cb.call(*callback_response) if rejected?
      cb.promise
    end

    # Register a callback to be run when the deferred is resolved.
    #
    # @yieldparam [Object] value
    # @yieldparam [Object] reason
    # @return [Promise] a new promise representing the return value
    #   of the callback, or -- when that return value is a promise itself
    #   -- a promise mimicking that promise.
    def always(&block)
      cb = Callback.new(block)
      always_callbacks << cb
      cb.call(*callback_response) if resolved?
      cb.promise
    end

    private

    attr_accessor :state
    attr_writer :value, :reason
    attr_reader :fulfilled_callbacks, :rejected_callbacks, :always_callbacks

    def resolve_to(state)
      self.state = state
      flush
    end

    def result_callbacks
      fulfilled? ? fulfilled_callbacks : rejected_callbacks
    end

    def callback_response
      fulfilled? ? value : reason
    end

    def flush
      (result_callbacks + always_callbacks).each do |cb|
        cb.call(callback_response)
      end
    end
  end
end
