require 'pdk'

module PDK
  module CLI
    class ExecGroup
      # Execution Group (ExecGroup) factory.
      #
      # @param message [String] A name or message for this group. Provided for backwards compatibility during refactor
      #
      # @param create_options [Hash] A hash options used during creation of the ExecGroup.  This are not passed to the new object
      # @option create_options :parallel [Boolean] Whether the group should be executed in Parallel (True) or Serial (False)
      #
      # @param group_opts [Hash] A hash of options used to configure the execution group.  Provided for backwards compatibility during refactor
      #
      # @return [ExecGroup]
      def self.create(message, create_options = {}, group_opts = {})
        if create_options[:parallel]
          ParallelExecGroup.new(message, group_opts)
        else
          SerialExecGroup.new(message, group_opts)
        end
      end

      # Base class for an Exection Group
      #
      # @param message [String] A name or message for this group. Provided for backwards compatibility during refactor
      #
      # @param opts [Hash] A hash of options used to configure the execution group. Provided for backwards compatibility during refactor
      #
      # @api private
      def initialize(_message, opts = {})
        @options = opts
      end

      # Register something to execute as a group
      #
      # @param block [Block] A block of ruby to execute
      #
      # @api private
      def register(&block)
        raise PDK::CLI::FatalError, 'No block registered' unless block
      end

      # The return code of running all registered blocks
      #
      # @return [int] The highest exit code from the blocks
      #
      # @abstract
      def exit_code; end
    end

    # Executes registered blocks in serial
    #
    # @see PDK::CLI::ExecGroup
    class SerialExecGroup < ExecGroup
      def initialize(message, opts = {})
        super(message, opts)
        @procs = []
      end

      def register(&block)
        super(&block)

        @procs << block
      end

      def exit_code
        exit_codes = @procs.map(&:call)
        exit_codes.nil? ? 0 : exit_codes.max
      end
    end

    # Executes registered blocks in parallel using Ruby threads
    #
    # @see PDK::CLI::ExecGroup
    class ParallelExecGroup < ExecGroup
      def initialize(message, opts = {})
        super(message, opts)
        @threads = []
        @exit_codes = []
      end

      def register(&block)
        super(&block)

        # TODO: This executes the thread immediately, whereas the SerialExecGroup executes only when exit_code
        # is called.  Need to change this so it uses a kind of ThreadPool to limit to number on concurrent jobs
        # and only starts on the call to exit_code
        # e.g. max_threads = No. of CPUs
        @threads << Thread.new do
          @exit_codes << yield
        end
      end

      def exit_code
        @threads.each(&:join)
        return 0 if @exit_codes.empty?

        @exit_codes.max
      end
    end
  end
end
