module Vagrant
  module Action
    module Builtin
      # This middleware class allows a sort of "conditional" run within
      # a single middlware sequence. It takes another middleware runnable,
      # runs it with the same environment, then yields the resulting env to a block,
      # allowing that block to determine the next course of action in the
      # middleware sequence.
      #
      # The first argument to this middleware sequence is anywhere middleware
      # runnable, whether it be a class, lambda, or something else that
      # responds to `call`. This middleware runnable is run with the same
      # environment as this class.
      #
      # After running, {Call} takes the environment and yields it to a block
      # given to initialize the class, along with an instance of {Builder}.
      # The result is used to build up a new sequence on the given builder.
      # This builder is then run.
      class Call
        # For documentation, read the description of the {Call} class.
        #
        # @param [Object] callable A valid middleware runnable object. This
        #   can be a class, a lambda, or an object that responds to `call`.
        # @yield [result, builder] This block is expected to build on `builder`
        #   which is the next middleware sequence that will be run.
        def initialize(app, env, callable, *callable_args, &block)
          raise ArgumentError, "A block must be given to Call" if !block

          @app      = app
          @callable = callable
          @callable_args = callable_args
          @block    = block
          @child_app = nil
        end

        def call(env)
          runner  = Runner.new

          # Build the callable that we'll run
          callable = Builder.build(@callable, *@callable_args)

          # Run our callable with our environment
          new_env = runner.run(callable, env)

          # Build our new builder based on the result
          builder = Builder.new
          @block.call(new_env, builder)

          # Run the result with our new environment
          @child_app = builder.to_app(new_env)
          final_env = runner.run(@child_app, new_env)

          # Merge the environment into our original environment
          env.merge!(final_env)

          # Call the next step using our final environment
          @app.call(env)
        end

        def recover(env)
          # Call back into our compiled application and recover it.
          @child_app.recover(env) if @child_app
        end
      end
    end
  end
end
