require 'eventmachine'

module Ably::Modules
  # Provides methods to convert synchronous operations into async operations through the use of
  # {http://www.rubydoc.info/github/eventmachine/eventmachine/EventMachine#defer-class_method EventMachine#defer}.
  # The async_wrap method can only be called from within an EventMachine reactor, and must be thread safe.
  #
  # @note using this AsyncWrapper should only be used for methods that are used less frequently and typically
  #       not run with levels of concurrency due to the limited number of threads available to EventMachine by default.
  #       This module requires that the method #logger is defined.
  #
  # @example
  #   class BlockingOperation
  #     include Aby::Modules::AsyncWrapper
  #
  #     def operation(&success_callback)
  #       async_wrap(success_callback) do
  #         sleep 1
  #         'slept'
  #       end
  #     end
  #   end
  #
  #   blocking_object = BlockingOperation.new
  #   deferrable = blocking_object.operation do |result|
  #     puts "Done with result: #{result}"
  #   end
  #   puts "Starting"
  #
  #   # => 'Starting'
  #   # => 'Done with result: slept'
  #
  module AsyncWrapper
    private

    # Will yield the provided block in a new thread and return an {Ably::Util::SafeDeferrable}
    #
    # @yield [Object] operation block that is run in a thread
    # @return [Ably::Util::SafeDeferrable]
    #
    def async_wrap(success_callback = nil, custom_error_handling = nil)
      raise ArgumentError, 'Block required' unless block_given?

      Ably::Util::SafeDeferrable.new(logger).tap do |deferrable|
        deferrable.callback(&success_callback) if success_callback

        operation_with_exception_handling = proc do
          begin
            yield
          rescue StandardError => err
            if custom_error_handling
              custom_error_handling.call err, deferrable
            else
              logger.error { "An exception in an AsyncWrapper block was caught. #{err.class}: #{err.message}\n#{err.backtrace.join("\n")}" }
              deferrable.fail err
            end
          end
        end

        complete_callback = proc do |result|
          deferrable.succeed result
        end

        EventMachine.defer operation_with_exception_handling, complete_callback
      end
    end
  end
end