##################################################################
#                  Licensing Information                         #
#                                                                #
#  The following code is licensed, as standalone code, under     #
#  the Ruby License, unless otherwise directed within the code.  #
#                                                                #
#  For information on the license of this code when distributed  #
#  with and used in conjunction with the other modules in the    #
#  Amp project, please see the root-level LICENSE file.          #
#                                                                #
#  © Michael J. Edgar and Ari Brown, 2009-2010                   #
#                                                                #
##################################################################

module Amp
  ##
  # The module covering all the help subsystems for the Amp binary.
  module Help
    
    ##
    # Module handling the registration and retrieval of entries in the
    # help system.
    #
    # This is a singleton module. Don't mix it in anywhere. That'd be silly.
    module HelpRegistry
      extend self
      
      ##
      # Retrives the entries hash which stores all the help entrys
      #
      # @return [Hash{String => Array<HelpEntry>}] the entry table for the help system
      def entries
        @entries ||= Hash.new() {|h,k| h[k] = []}
      end
      
      ##
      # Returns a list of HelpEntrys with the given name. Since we allow for
      # the possibility of overlap by name, this returns an array.
      #
      # @param [String, #to_s] entry the name of the entry(ies) to retrieve
      # @return [Array<HelpEntry>] the help entries stored under the given name
      def [](entry)
        entries[entry]
      end
      
      ##
      # Adds an entry to the registry. We take a name and an entry, and store
      # the entry under the list of entries with the given name.
      #
      # @param [String, #to_s] name the name of the help entry. Allowed to
      #   conflict with other entries.
      # @param [HelpEntry] entry the entry to store in the registry
      def register(name, entry)
        entries[name] << entry
      end
      
      ##
      # Unregisters the given entry from the registry. Not sure why you might
      # use this, but it's a capability.
      #
      # @param [String, #to_s] name the name of the entry. Note - you will also
      #   need to provide the entry, because there might be naming conflicts.
      # @param [HelpEntry] entry the entry to remove from the registry.
      def unregister(name, entry=nil)
        case entries[name].size
        when 0
          raise ArgumentError.new("No help entry named '#{name}' found.")
        when 1
          entries[name].clear
        else
          if entry.nil?
            raise ArgumentError.new("Multiple help entries named '#{name}': " +
                                    'you must provide which one to remove to ' +
                                    '#unregister.')
          else
            entries[name].delete entry
          end
        end
      end
    end
    
    ##
    # The generic HelpEntry class encapsulates a entry in the help system. The
    # entry has text that it provides to the user, as well as a name. The base
    # HelpEntry class does not track its own name, because well, that's not
    # useful! All it needs to know how to do is present its text when asked for it.
    class HelpEntry
      class << self
        ##
        # Singleton method that opens a file and returns a HelpEntry representing it.
        # What makes this method spiffy is that it tries to detect the type of file
        # -- markdown, ERb, et cetera, based on the file's extension, and picks
        # the appropriate class to represent that help entry.
        #
        # The entry is registered under the name of the file - without any extensions -
        # and the file's full contents are provided as the initial text.
        #
        # @param [String] filename the path to the file to load
        # @return [HelpEntry] a help entry representing the file as best as we can.
        def from_file(filename)
          klass = case File.extname(filename).downcase
                  when ".md", ".markdown"
                    MarkdownHelpEntry
                  when ".erb"
                    ErbHelpEntry
                  else
                    HelpEntry
                  end
          name = File.basename(filename).split(".", 2).first
          klass.new(name, File.read(filename))
        end
      end
      
      ##
      # Creates a new HelpEntry, and registers it in the Help system, making it
      # immediately available. It is for this reason that all subclasses should
      # call +super+, because that registration is important!
      #
      # @param [String, #to_s] name the name under which to register this help entry
      # @param [String] text ("") the text for the entry.
      def initialize(name, text = "")
        @text = text
        HelpRegistry.register(name, self)
      end
      
      ##
      # Returns the help text to display for this entry.
      #
      # In the generic case, just return the @text variable.
      #
      # @param [Hash] options the options for the process - that way the help commands
      #   can access the user's run-time options and global configuration. For example,
      #   if the user passes in --verbose or --quiet, each help entry could handle that
      #   differently. Who are we to judge?
      # @return [String] the help text for the entry.
      def text(options = {})
        @text
      end
      
      ##
      # Describes the entry briefly, so if the user must pick, they have a decent
      # shot at knowing what this entry is about. Hopefully.
      #
      # In the generic case, use the text and grab the first few words.
      #
      # @return a description of the entry based on its content
      def desc
        %Q{a regular help entry ("#{text.split[0..5].join(" ")} ...")}
      end
    end
    
    ##
    # Represents a help entry that filters its help text through a Markdown parser
    # before returning.
    #
    # This makes it very easy to make very pretty help files, that are smart enough
    # to look good in both HTML form and when printed to a terminal. This uses our
    # additions to the markdown parser to provide an "ANSI" output format.
    class MarkdownHelpEntry < HelpEntry
      ##
      # Returns the help text to display for this entry.
      #
      # For a markdown entry, we run this through Maruku and our special to_ansi
      # output formatter. This will make things like *this* underlined and **these**
      # bolded. Code blocks will be given a colored background, and headings are
      # accentuated.
      #
      # @param [Hash] options the options for the process - that way the help commands
      #   can access the user's run-time options and global configuration. For example,
      #   if the user passes in --verbose or --quiet, each help entry could handle that
      #   differently. Who are we to judge?
      # @return [String] the help text for the entry.
      def text(options = {})
        Maruku.new(super, {}).to_ansi
      end
    end 
    
    ##
    # Represents a help entry that filters its help text through ERB before returning.
    #
    # This is useful because some entries might have programmatic logic to them - 
    # for example, the built in "commands" entry lists all the commands in the
    # user's current workflow. That requires logic, and while we used to simply
    # have that be its own class, we can now stuff it in an ERB file.
    #
    # Note: if you want to use pretty text in an ERB entry, you will have to use
    # ruby code to do so. Use the following shortcuts:
    #
    #     <%= "Ampfiles".bold.underline %> # bolds and underlines
    #     <%= "some.code()".black.on_green %> # changes to black and sets green bg color
    #
    # See our extensions to the String class for more.
    class ErbHelpEntry < HelpEntry
      ##
      # Returns the help text to display for this entry.
      #
      # For an ERB entry, we run ERB on the text in the entry, while also exposing the
      # options variable as local, so the ERB can access the user's runtime options.
      #
      # @param [Hash] options the options for the process - that way the help commands
      #   can access the user's run-time options and global configuration. For example,
      #   if the user passes in --verbose or --quiet, each help entry could handle that
      #   differently. Who are we to judge?
      # @return [String] the help text for the entry.
      def text(options = {})
        full_text = super(options)
        
        erb = ERB.new(full_text, 0, "-")
        erb.result binding
      end
    end
    
    ##
    # Represents a command's help entry. All commands have one of these, and in fact,
    # when the command is created, it creates a help entry to go with it.
    #
    # Commands are actually quite complicated, and themselves know how to educate
    # users about their use, so we have surprisingly little logic in this class.
    class CommandHelpEntry < HelpEntry      
      ##
      # Creates a new command help entry. Differing arguments, because instead of
      # text, we need the command itself. One might think: why not just pass in
      # the command's help information instead? If you have a command object, you
      # have command.help, no? Well, the reason is two-fold: the help information
      # might be updated later, and there is more to printing a command's help entry
      # than just the command.help() method.
      #
      # @param [String] name the name of the command
      # @param [Amp::Command] command the command being represented.
      def initialize(name, command)
        super(name)
        @command = command
      end
      
      ##
      # Returns the help text to display for this entry.
      #
      # For a command-based entry, simply run its educate method, since commands know
      # how to present their help information.
      #
      # @param [Hash] options the options for the process - that way the help commands
      #   can access the user's run-time options and global configuration. For example,
      #   if the user passes in --verbose or --quiet, each help entry could handle that
      #   differently. Who are we to judge?
      # @return [String] the help text for the entry.
      def text(options = {})
        instantiated = @command.new
        instantiated.collect_options([])
        "#{@command.desc}\n#{instantiated.education}"
      end
      
      ##
      # Describes the entry briefly, so if the user must pick, they have a decent
      # shot at knowing what this entry is about. Hopefully.
      #
      # In the case of a command, grab the command's "desc" information.
      #
      # @return a description of the entry based on its content
      def desc
        %Q{a command help entry ("#{@command.desc}")}
      end
    end
    
    ##
    # The really public-facing part of the Help system - the Help's UI.
    # This lets the outside world get at entries based on their names.
    module HelpUI
      extend self
      
      ##
      # Asks the UI system to print the entry with the given name, with the
      # process's current options.
      #
      # This method is "smart" - it has to check to see what entries are
      # available. If there's more than one with the provided name, it 
      # helps the user pick the appropriate entry.
      #
      # @param [String] name the name of the entry to print
      # @param [Hash] options the process's options
      def print_entry(name, options = {})
        result = HelpRegistry[name.to_s]
        case result.size
        when 0
          raise abort("Could not find help entry \"#{name}\"")
        when 1
          puts result.first.text(options)
        when 2
          UI.choose do |menu|
            result.each do |entry|
              menu.choice("#{name} - #{entry.desc}") { puts entry.text(options) }
            end
          end
        end
      end
    end
    
    ##
    # A method that loads in the default entries for the help system.
    # Normally, I'd just put this code in the module itself, or perhaps
    # at the end of the file, but I'm experimenting with an approach
    # where I try to minimize the bare code, leaving only the invocation
    # of this method to sit in the module.
    def self.load_default_entries
      Dir[File.join(File.dirname(__FILE__), "entries", "**")].each do |file|
        HelpEntry.from_file(file)
      end
    end
    
    # Load the default entries.
    self.load_default_entries
  end
end