##################################################################
#                  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                   #
#                                                                #
##################################################################

require 'singleton'
require 'tempfile'

module Amp
  ##
  # Represents an extremely simple menu that the UI can use to ask for input.
  # Replacement for Highline's "choose" method - and is extremely, extremely basic.
  # In fact, I might strip out some flexibility for now. I just did. It only does
  # numbered lists with no intelligently figuring things out.
  class UIMenu
    # The prompt to ask at the top of the menu
    attr_accessor :prompt
    # How to index results - left in for compatibility and possibly future flexibility
    attr_accessor :index
    
    ##
    # Creates a new UIMenu. Initializes ivars. Does nothing else of use.
    #
    # @return [UIMenu] the new menu
    def initialize
      @choices = []
      @prompt  = "Pick an option, please."
      @index   = :number
      @config  = nil
    end
    
    ##
    # Adds a choice to the list of choices to be presented to the user.
    # Choices are ordered in the order in which this method is called.
    # The block is stored and executed if the choice is selected.
    #
    # @param [String] prompt the prompt to display to the user for this
    #   choice
    # @param [Hash] opts options for this choice (unused for now)
    # @param [Proc] &block a block that will be executed later if the choice
    #   is selected
    def choice(prompt, opts={}, &block)
      @choices << {:text => prompt, :action => block}
    end
    
    ##
    # Runs the UIMenu. Will loop until an acceptable choice is selected
    # by the user. Prints the choices in the order in which they were added,
    # in a numbered list. The user must select the number of their desired
    # option. After an acceptable choice is selected, its associated block
    # is executed.
    def run
      input = -1
      while input <= 0 || input > @choices.size do
        UI.say @prompt
        @choices.each_with_index {|item, idx| UI::say "#{idx+1} #{item[:text]}"}
        UI::say
        UI::tell "Your choice: "
        UI::say "[1-#{@choices.size}]"
        input = UI::ask('', Integer)
      end
      choice = @choices[input - 1]
      choice[:action].call
    end
  end
  
  module UI
    extend self
    
    class << self
      attr_accessor :config
    end
    ##
    # Prints a warning. Can be disabled using configuration files. These are
    # informational in nature but imply the user did something wrong, or
    # that something is impossible.
    #
    # @param [#to_s] warning the warning to print to standard output
    def warn(warning)
      if !config || config["ui", "amp-show-warnings", Boolean, true]
        err "warning: #{warning}"
      end
    end
    
    ##
    # Produces a menu for the user. Home-rolled to avoid rubygems dependencies!
    # 
    # @yield yields the menu object, which is configured inside the block, and then
    #   the menu is run
    # @yieldparam menu the menu object. Should be configured inside the given block.
    def choose
      menu = UIMenu.new
      yield menu
      menu.run
    end
    
    ##
    # Gets the user's password, while hiding it from prying eyes.
    # 
    # REQUIRES `stty`
    #
    # @return [String] the user's password.
    def get_pass
      system "stty -echo"
      pass = gets.chomp
      tell "\n"
      system "stty echo"
      pass
    end
    
    ##
    # Prints a status update. Can be disabled using configuration files.
    # These are casual updates to let the user know the progress in an
    # operation.
    # 
    # @param [#to_s] update the message to print to standard output
    # @return [NilClass]
    def status(update='')
      return unless $display
      if !config || config["ui", "amp-show-status", Boolean, true]
        say "status: #{update}"
      end
    end
    
    ##
    # Notes the message - ignored unless in debug mode
    #
    # @param [#to_s] note the note to print to standard output
    # @return [NilClass]
    def note(note='')
      return unless $display
      if !config || config["ui", "amp-show-notes", Boolean, false]
        say "note: #{note}"
      end
    end
    
    ##
    # Prints +message+ to standard out with a trailing newline
    #
    # @param [#to_s] message the message to be printed.
    # @return [NilClass]
    def say(message='')
      tell "#{message.to_s}\n"
    end
    
    ##
    # Prints +message+ to standard out without a trailing newline.
    #
    # @param [#to_s] message the message to be printed.
    # @return [NilClass]
    def tell(message='')
      $stdout.print message.to_s
    end
    
    ##
    # Prints +message+ to standard error.
    #
    # @param [#to_s] message the message to be errored.
    # @return [NilClass]
    def err(message='')
      $stderr.puts message.to_s
    end
    
    ##
    # Ask +question+ and return the answer, chomped
    # 
    # @param [#to_s] question question to ask
    # @param [Class, #to_s, Symbol] type the type to cast the answer to
    # @return [String] their response without trailing newline
    def ask(question='', type=String)
      type = type.to_s.downcase.to_sym
      
      tell question.to_s
      result = $stdin.gets.chomp unless type == :password
      
      # type conversion
      case type
      when :string
        result
      when :integer, :fixnum, :bignum, :numeric
        result.to_i
      when :float
        result.to_f
      when :array
        result.split(',').map {|e| e.strip }
      when :password
        get_pass
      else
        raise abort("Don't know how to convert to type #{type}")
      end
    end
    
    ##
    # Ask a yes or no question (accepts a variety of inputs)
    # 
    # @param [#to_s] question question to ask
    # @return [Boolean] their agreement
    def yes_or_no(question='')
      result = ask(question.to_s + ' [y/n/r/h] ')
      case result.downcase[0, 1]
      when 'r'
        yes_or_no question # recurse
      when 'h'
        tell <<-EOS
[y/n/r/h] means you can type anything starting with a 'y', 'n', 'r', or an 'h' to select those options.

'y'\tyes
'n'\tno
'r'\trepeat
'h'\thelp
EOS
        yes_or_no question
      else
        result.downcase.start_with? 'y'
      end
    end
    alias_method :agree, :yes_or_no
    
    ##
    # Opens the user's editor for the file at the given path, with no extra
    # processing afterward.
    #
    # @param [String, #inspect] path The path to locate the file to open.
    # @return [Boolean] whether or not the editor successfully launched
    def edit_file(path)
      editor = get_editor
      system "#{editor} #{path.inspect}"
    end
    
    ##
    # Prints this message, only if the debug flag is set.
    #
    # @param [String, #to_s] message The debug message to be printed
    def debug(message='')
      if config && config["debug","messages", Boolean, false]
        say message
      end
    end
    
    ##
    # Opens the editor for editing. Uses a temporary file to capture the
    # user's input. The user must have either HGEDITOR, AMPEDITOR, VISUAL,
    # EDITOR, or the configuration "ui"->"editor" set. Or, it'll just use
    # vi.
    #
    # @param [String] text the text with which to fill the file
    # @param [String] username the username to set the ENV var as
    # @return [String] the file after being edited
    def edit(text="", username="")
      tempfile = Tempfile.new("amp-editor-")
      path = tempfile.path
      tempfile.write text
      tempfile.close
      
      ENV["HGUSER"] = username
      edit_file path
      
      text = File.open(path) {|tf| tf.read } || ''
      
      FileUtils.safe_unlink path
      
      text.gsub!(/^AMP:.*\n/,"")
      text
    end
      
    ##
    # Gets the editor for the current system using environment variables or 
    # the configuration files.
    #
    # @return [String] the name of the editor command to execute
    def get_editor
      return ENV["AMPEDITOR"] || ENV["HGEDITOR"] || (config && config["ui","editor"]) ||
             ENV["VISUAL"] || ENV["EDITOR"] || "vi"
    end
  end
end