module GLI
  module Commands
    # Takes a DocListener which will be called with all of the meta-data and documentation
    # about your app, so as to create documentation in whatever format you want
    class Doc < Command
      FORMATS = {
        'rdoc' => GLI::Commands::RdocDocumentListener,
      }
      # Create the Doc generator based on the GLI app passed in
      def initialize(app)
        super(:names       => "_doc",
              :description => "Generate documentation of your application's UI",
              :long_desc   => "Introspects your application's UI meta-data to generate documentation in a variety of formats.  This is intended to be extensible via the DocumentListener interface, so that you can provide your own documentation formats without them being a part of GLI",
              :skips_pre   => true, :skips_post => true, :skips_around => true, :hidden => true)

        @app = app
        @parent = @app
        @subcommand_option_handling_strategy = @app.subcommand_option_handling_strategy

        desc          'The format name of the documentation to generate or the class name to use to generate it'
        default_value 'rdoc'
        arg_name      'name_or_class'
        flag          :format

        action do |global_options,options,arguments|
          self.document(format_class(options[:format]).new(global_options,options,arguments,app))
        end
      end

      def nodoc
        true
      end

      # Generates documentation using the listener
      def document(document_listener)
        document_listener.beginning
        document_listener.program_desc(@app.program_desc) unless @app.program_desc.nil?
        document_listener.program_long_desc(@app.program_long_desc) unless @app.program_long_desc.nil?
        document_listener.version(@app.version_string)
        if any_options?(@app)
          document_listener.options 
        end
        document_flags_and_switches(document_listener,
                                    @app.flags.values.sort(&by_name),
                                    @app.switches.values.sort(&by_name))
        if any_options?(@app)
          document_listener.end_options 
        end
        document_listener.commands
        document_commands(document_listener,@app)
        document_listener.end_commands
        document_listener.ending
      end

      # Interface for a listener that is called during various parts of the doc process
      class DocumentListener
        def initialize(global_options,options,arguments,app)
          @global_options = global_options
          @options        = options
          @arguments      = arguments
          @app            = app
        end
        # Called before processing begins
        def beginning
          abstract!
        end

        # Called when processing has completed
        def ending
          abstract!
        end

        # Gives you the program description
        def program_desc(desc)
          abstract!
        end

        # Gives you the program long description
        def program_long_desc(desc)
          abstract!
        end

        # Gives you the program version
        def version(version)
          abstract!
        end

        # Called at the start of options for the current context
        def options
          abstract!
        end

        # Called when all options for the current context have been vended
        def end_options
          abstract!
        end

        # Called at the start of commands for the current context
        def commands
          abstract!
        end

        # Called when all commands for the current context have been vended
        def end_commands
          abstract!
        end

        # Gives you a flag in the current context
        def flag(name,aliases,desc,long_desc,default_value,arg_name,must_match,type)
          abstract!
        end

        # Gives you a switch in the current context
        def switch(name,aliases,desc,long_desc,negatable)
          abstract!
        end

        # Gives you the name of the current command in the current context
        def default_command(name)
          abstract!
        end

        # Gives you a command in the current context and creates a new context of this command
        def command(name,aliases,desc,long_desc,arg_name,arg_options)
          abstract!
        end

        # Ends a command, and "pops" you back up one context
        def end_command(name)
          abstract!
        end

      private
        def abstract!
          raise "Subclass must implement"
        end
      end

    private

      def format_class(format_name)
        FORMATS.fetch(format_name) { 
          begin
            return format_name.split(/::/).reduce(Kernel) { |context,part| context.const_get(part) }
          rescue => ex
            raise IndexError,"Couldn't find formatter or class named #{format_name}"
          end
        }
      end

      def document_commands(document_listener,context)
        context.commands.values.reject {|_| _.nodoc }.sort(&by_name).each do |command|
          call_command_method_being_backwards_compatible(document_listener,command)
          document_listener.options if any_options?(command)
          document_flags_and_switches(document_listener,command_flags(command),command_switches(command))
          document_listener.end_options if any_options?(command)
          document_listener.commands if any_commands?(command)
          document_commands(document_listener,command)
          document_listener.end_commands if any_commands?(command)
          document_listener.end_command(command.name)
        end
        document_listener.default_command(context.get_default_command)
      end

      def call_command_method_being_backwards_compatible(document_listener,command)
        command_args = [command.name,
                        Array(command.aliases),
                        command.description,
                        command.long_description,
                        command.arguments_description]
        if document_listener.method(:command).arity == 6
          command_args << command.arguments_options
        end
        document_listener.command(*command_args)
      end

      def by_name
        lambda { |a,b| a.name.to_s <=> b.name.to_s }
      end

      def command_flags(command)
        if @subcommand_option_handling_strategy == :legacy
          command.topmost_ancestor.flags.values.select { |flag| flag.associated_command == command }.sort(&by_name)
        else
          command.flags.values.sort(&by_name)
        end
      end

      def command_switches(command)
        if @subcommand_option_handling_strategy == :legacy
          command.topmost_ancestor.switches.values.select { |switch| switch.associated_command == command }.sort(&by_name)
        else
          command.switches.values.sort(&by_name)
        end
      end

      def document_flags_and_switches(document_listener,flags,switches)
        flags.each do |flag|
          document_listener.flag(flag.name,
                                 Array(flag.aliases),
                                 flag.description,
                                 flag.long_description,
                                 flag.safe_default_value,
                                 flag.argument_name,
                                 flag.must_match,
                                 flag.type)
        end
        switches.each do |switch|
          document_listener.switch(switch.name,
                                   Array(switch.aliases),
                                   switch.description,
                                   switch.long_description,
                                   switch.negatable)
        end
      end

      def any_options?(context)
        options = if context.kind_of?(Command)
                    command_flags(context) + command_switches(context)
                  else
                    context.flags.values + context.switches.values
                  end
        !options.empty?
      end

      def any_commands?(command)
        !command.commands.empty?
      end
    end
  end
end
