# frozen_string_literal: true

require 'concurrent/promise'
require 'dry/effects/provider'
require 'dry/effects/frame'

module Dry
  module Effects
    module Providers
      class Defer < Provider[:defer]
        include ::Dry::Equalizer(:executor, inspect: false)

        option :executor, default: -> { :io }

        attr_reader :later_calls

        attr_reader :stack

        def defer(block, executor)
          stack = self.stack.dup
          at = Undefined.default(executor, self.executor)
          ::Concurrent::Promise.execute(executor: at) do
            Frame.spawn_fiber(stack, &block)
          end
        end

        def later(block, executor)
          if @later_calls.frozen?
            Instructions.Raise(Errors::EffectRejectedError.new(<<~MSG))
              .later calls are not allowed, they would be processed
              by another stack. Add another defer handler to the current stack
            MSG
          else
            at = Undefined.default(executor, self.executor)
            stack = self.stack.dup
            @later_calls << ::Concurrent::Promise.new(executor: at) do
              Frame.spawn_fiber(stack, &block)
            end
            nil
          end
        end

        def wait(promises)
          if promises.is_a?(::Array)
            ::Concurrent::Promise.zip(*promises).value!
          else
            promises.value!
          end
        end

        # Yield the block with the handler installed
        #
        # @api private
        def call(options = { executor: Undefined })
          unless Undefined.equal?(options[:executor])
            @executor = options[:executor]
          end

          @stack = Frame.stack
          @later_calls = []
          yield
        ensure
          later_calls.each(&:execute)
        end

        def dup
          if defined? @later_calls
            super.tap { |p| p.instance_variable_set(:@later_calls, EMPTY_ARRAY) }
          else
            super
          end
        end

        # @return [String]
        # @api public
        def represent
          info = []
          info << executor.to_s if executor.is_a?(::Symbol)
          info << "call_later=#{later_calls.size}" if later_calls.any?

          if info.empty?
            'defer'
          else
            "defer[#{info.join(' ')}]"
          end
        end
      end
    end
  end
end