# frozen_string_literal: true

class Pry
  # A super-class of Commands with structure.
  #
  # This class implements the bare-minimum functionality that a command should
  # have, namely a --help switch, and then delegates actual processing to its
  # subclasses.
  #
  # Create subclasses using {Pry::CommandSet#create_command}, and override the
  # `options(opt)` method to set up an instance of Pry::Slop, and the `process`
  # method to actually run the command. If necessary, you can also override
  # `setup` which will be called before `options`, for example to require any
  # gems your command needs to run, or to set up state.
  class ClassCommand < Command
    class << self
      # Ensure that subclasses inherit the options, description and
      # match from a ClassCommand super class.
      def inherited(klass)
        klass.match match
        klass.description description
        klass.command_options options
      end

      def source
        source_object.source
      end

      def doc
        new.help
      end

      def source_location
        source_object.source_location
      end

      def source_file
        source_object.source_file
      end
      alias file source_file

      def source_line
        source_object.source_line
      end
      alias line source_line

      private

      # The object used to extract the source for the command.
      #
      # This should be a `Pry::Method(block)` for a command made with `create_command`
      # and a `Pry::WrappedModule(self)` for a command that's a standard class.
      # @return [Pry::WrappedModule, Pry::Method]
      def source_object
        @source_object ||= if name =~ /^[A-Z]/
                             Pry::WrappedModule(self)
                           else
                             Pry::Method(block)
                           end
      end
    end

    attr_accessor :opts
    attr_accessor :args

    # Set up `opts` and `args`, and then call `process`.
    #
    # This method will display help if necessary.
    #
    # @param [Array<String>] args The arguments passed
    # @return [Object] The return value of `process` or VOID_VALUE
    def call(*args)
      setup

      self.opts = slop
      self.args = opts.parse!(args)

      if opts.present?(:help)
        output.puts slop.help
        void
      else
        process(*normalize_method_args(method(:process), args))
      end
    end

    # Return the help generated by Pry::Slop for this command.
    def help
      slop.help
    end

    # Return an instance of Pry::Slop that can parse either subcommands or the
    # options that this command accepts.
    def slop
      Pry::Slop.new do |opt|
        opt.banner(unindent(self.class.banner))
        subcommands(opt)
        options(opt)
        opt.on :h, :help, 'Show this message.'
      end
    end

    # Generate shell completions
    # @param [String] search  The line typed so far
    # @return [Array<String>]  the words to complete
    def complete(search)
      slop.flat_map do |opt|
        [opt.long && "--#{opt.long} " || opt.short && "-#{opt.short}"]
      end.compact + super
    end

    # A method called just before `options(opt)` as part of `call`.
    #
    # This method can be used to set up any context your command needs to run,
    # for example requiring gems, or setting default values for options.
    #
    # @example
    #   def setup
    #     require 'gist'
    #     @action = :method
    #   end
    def setup; end

    # A method to setup Pry::Slop commands so it can parse the subcommands your
    # command expects. If you need to set up default values, use `setup`
    # instead.
    #
    # @example A minimal example
    #   def subcommands(cmd)
    #     cmd.command :download do |opt|
    #       description 'Downloads a content from a server'
    #
    #       opt.on :verbose, 'Use verbose output'
    #
    #       run do |options, arguments|
    #         ContentDownloader.download(options, arguments)
    #       end
    #     end
    #   end
    #
    # @example Define the invokation block anywhere you want
    #   def subcommands(cmd)
    #     cmd.command :download do |opt|
    #       description 'Downloads a content from a server'
    #
    #       opt.on :verbose, 'Use verbose output'
    #     end
    #   end
    #
    #   def process
    #     # Perform calculations...
    #     opts.fetch_command(:download).run do |options, arguments|
    #       ContentDownloader.download(options, arguments)
    #     end
    #     # More calculations...
    #   end
    def subcommands(cmd); end

    # A method to setup Pry::Slop so it can parse the options your command expects.
    #
    # @note Please don't do anything side-effecty in the main part of this
    # method, as it may be called by Pry at any time for introspection reasons.
    # If you need to set up default values, use `setup` instead.
    #
    # @example
    #  def options(opt)
    #    opt.banner "Gists methods or classes"
    #    opt.on(:c, :class, "gist a class") do
    #      @action = :class
    #    end
    #  end
    def options(opt); end

    # The actual body of your command should go here.
    #
    # The `opts` mehod can be called to get the options that Pry::Slop has passed,
    # and `args` gives the remaining, unparsed arguments.
    #
    # The return value of this method is discarded unless the command was
    # created with `:keep_retval => true`, in which case it is returned to the
    # repl.
    #
    # @example
    #   def process
    #     if opts.present?(:class)
    #       gist_class
    #     else
    #       gist_method
    #     end
    #   end
    def process
      raise CommandError, "command '#{command_name}' not implemented"
    end
  end
end
