# This solution provides a framework for handling user input at a higher level
# than "gets" and "chomp". Sorry to James for being so late...
#
# Usage:
#
# Define a class which inherits from HighLine::ValueInput,
# HighLine::ChoiceInput or HighLine::MenuInput, using the class methods
# (see below) to define the way the input will be handled. Then call the
# #ask class method to prompt for the input, passing additional definitions
# in an optional block (see examples at end of file).
#
#
# Class Methods (all classes):
# 
# transform <Proc object p> [, *args]
#   Calls r = p.call(r, *args), where r is the user's response
# transform symbol [, *args]
#   Calls r = r.method(symbol).call(*args)
# transform :with_my, symbol [, *args]
#   Calls r = self.method(symbol).call(r, *args), where self is an instance
#   of the class
#
#  Transformations are applied to the input in the order in which they were
#  defined, and they are inherited from parent classes cumulatively (is that
#  a word?)
#
# synonym <base string>, <synonym string> [, *<synonym strings>]
#   Creates a transformation which maps each synonym string to the
#   base string.
#
# okay_if <Proc object p> [, *args]
#   Uses the return value of p.call(r, *args) to determine if the validation
#   phase can be skipped. (r is the user's response after transformation)
# okay_if <Regexp re>
#   As above, with the result of r =~ re
# okay_if symbol [, *args]
#   As above, with the result of r.method(symbol).call(*args)
# okay_if :with_my, symbol [, *args]
#   As above, with the result of self.method(symbol).call(r, *args)
#
#  Definitions for okay values are inherited.
#
# error_message string
#   Causes the string to be printed if the validation phase fails
# error_message symbol
#   As above, with the return value from self.method(symbol).call(r)
# error_message <Proc object p>
#   As above, with the return value from p.call(r)
#
#  A class will only use one error_message definition. See the section on
#  validation below for more details about the difference in error handling
#  between ValueInput and ChoiceInput/MenuInput.
#
#
# Class Methods (ValueInput):
#
# format_hint string
#   A string which is appended to the prompt. The default 
#   ValueInput#prompt_suffix method wraps it in square brackets, e.g. 
#   "[YYYY-MM-DD]".
#
#  Format hints are inherited, but not cumulatively--if a class provides one,
#  it will override any hints in its parent classes.
#
# validate
# 
#  The syntax for the validate method is the same as that for okay_if.
#  However, procs and methods that it causes to be called have different
#  return signatures: 
#    bool                  -> indicates validity
#    bool, str_or_nil      -> as above, plus an error message that overrides
#                             any error_message definitions in the class
#    bool, str_or_nil, val -> as above, plus an alternate version of the
#                             response string
#
#  When an alternate value is returned, subsequent validations that would
#  have operated on the response string will operate on this alternate value
#  instead (see the description of the #ask method below for details on using
#  this value).
#
#  Validators are cumulatively inherited, along with the error_message
#  defintions (so if a validator from a parent class fails, the error_message
#  from that class will be used).
#
# output_format
#  
#  The syntax for output_format is the same as that for transform. It provides
#  a way to format the response after it has been validated. Like validators,
#  output format procedures will operate on the response or on the alternate
#  value if one is present.
#
#
# Class Methods (ChoiceInput, MenuInput):
#
# choices *args
#   Adds its arguments to the list of valid string responses for the class.
#   The default ChoiceInput#prompt_suffix method displays it like this--
#   "[y/(n)/m]". The parentheses indicate the default answer (see #ask below).
#
#  Instead of using validators, ChoiceInput classes simply check for a
#  response that is in the list of choices.
#
# header string
#   (MenuInput only) A string that is printed before the list of choices is
#   displayed.
#
# items *args
#   (MenuInput only) The arguments are matched to the choices.
#
#  When one of the choices is selected, the matching element in the list of
#  items is returned as the alternate value. The default
#  MenuInput#prompt_suffix displays the header string, and one line for each
#  choice in the format "#{choice[i]}\t#{item[i]}\n".
#
# The #ask method
#
# ask(prompt, default_value=nil)
#
#  When the ask method is called, things get done in this order:
#  1. The prompt is printed, with the result of #prompt_suffix appended.
#  2. $stdin_gets is called, and the raw input is saved.
#  3. All defined transformations are applied to the response.
#  4. If any okay_if tests pass, the response is returned.
#  5. If the response is empty, and there is a default_value, the default is
#     returned.
#  6. Validation occurs. For ChoiceInput/MenuInput, this just involves
#     matching the response to the list of choices. For ValueInput, all
#     defined validation tests are run.
#  7. If validation succeeded, all output_format rules are applied.
#  8. If validation failed, and an error message was returned, it is printed,
#     and the process loops back to step 1.
#
#  The return value is an object of class ResponseString, which is a subclass
#  of normal String. It provides a #raw_input method, an #error_message method,
#  a #valid? method, and and #alternate method, for getting details about the
#  response.

module HighLine
  
  class BaseInput
    
    def self.get_error_message
      @em ||= nil
    end
    
    def self.error_message(em)
      @em = em
    end
  
    def self.get_transformers
      @ts ||= []
    end

    def self.transform(*ts)
      @ts ||= []
      @ts << ts
    end
    
    def self.get_okays
      @oi ||= []
    end
    
    def self.okay_if(*oi)
      @oi ||= []
      @oi << oi
    end
    
    def self.synonym(*syns)
      default = syns[0]
      synonyms = syns[1..-1]
      transform :with_my, :synonymize, synonyms, default
    end
    
    def self.ask(prompt, default_response=nil, &block)
      prompt = prompt + " " if prompt !~ /\s$/ and !prompt.empty?
      if block_given?
        klass = Class.new(self, &block)
      else
        klass = self
      end
      inputter = klass.new(default_response)
      while true
        print prompt
        $stdout.flush
        response = inputter.gets
        break if response.valid?
        if response.error_message
          puts response.error_message
          $stdout.flush
        end
      end
      return response
    end
    
    def initialize(default_response)
      @default_response = default_response
      @klasses = []
      klass = self.class
      while klass.respond_to? :get_transformers
        @klasses.unshift(klass)
        klass = klass.superclass
      end
      @okays = @klasses.collect { |klass|
        curry_okays(klass.get_okays)
      }.flatten
      @transformers = @klasses.collect { |klass|
        curry_transformers(klass.get_transformers)
      }.flatten
    end
    
    def gets
      print prompt_suffix
      $stdout.flush
      raw_input = $stdin.gets.chomp
      response = raw_input.dup
      @transformers.each do |transformer|
        response = transformer.call(response)
      end
      if @default_response and response.empty?
        return ResponseString.new(@default_response, raw_input, true)
      end

      @okays.each do |okay|
        if okay.call(response)
          return ResponseString.new(response, raw_input, true)
        end
      end
        
      do_validate(response, raw_input)
    end
    
    def prompt_suffix
      if @default_response and !@default_response.empty?
        "(#{@default_response}) "
      else
        ""
      end
    end
    
    def synonymize(r, synonyms, default)
      return r unless synonyms.member? r
      default
    end
    
    def curry_error_message(klass_error_message)
      case klass_error_message
      when Symbol
        proc { |r| method(klass_error_message).call(r) }
      when Proc
        proc { |r| klass_error_message.call(r) }
      when String
        proc { |r| klass_error_message }
      end
    end
    
    def curry_transformers(klass_transformers)
      klass_transformers.collect do |transformer|
        p_name = transformer[0]
        args = transformer[1..-1]
        case p_name
        when Symbol
          if p_name == :with_my
            proc { |r| method(args[0]).call(r, *args[1..-1]) }
          else
            proc { |r| r.method(p_name).call(*args) }
          end
        when Proc
          proc { |r| p_name.call(r, *args) }
        end
      end
    end
    
    def curry_okays(klass_okays)
      klass_okays.collect { |okay|
        p_name = okay[0]
        args = okay[1..-1]
        case p_name
        when Symbol
          if p_name == :with_my
            proc { |r| method(args[0]).call(r, *args[1..-1]) }
          else
            proc { |r| r.method(p_name).call(*args) }
          end
        when Regexp
          proc { |r| r.to_s =~ p_name }
        when Proc
          proc { |r| p_name.call(r, *args) }
        end
      }.compact
    end
    
    def do_validate(response, raw_input)
      ResponseString.new(response, raw_input, true)
    end
  end

  class ValueInput < BaseInput
  
    def self.get_validators
      @vld ||= []
    end
    
    def self.validate(*vld)
      @vld ||= []
      @vld << vld
    end
    
    def self.get_output_formats
      @of ||= []
    end
    
    def self.output_format(*of)
      @of ||= []
      @of << of
    end
    
    def self.get_format_hint
      @fh ||= nil
    end
    
    def self.format_hint(fh)
      @fh = fh
    end
    
    def initialize(default_response)
      super
      klasses = @klasses[1..-1]
      @validators = klasses.collect { |klass|
        curry_okays(klass.get_validators)
      }
      @error_messages = klasses.collect do |klass|
        curry_error_message(klass.get_error_message)
      end
      @output_formats = klasses.collect { |klass|
        curry_transformers(klass.get_output_formats)
      }.flatten
      klasses.reverse.each do |klass|
        @format_hint = klass.get_format_hint
        break if @format_hint
      end
    end
    
    def prompt_suffix
      super << (@format_hint ? "[#{@format_hint}] " : "")
    end
    
    def do_validate(response, raw_input)
      valid = true
      err_msg = nil
      @alternate = nil
      resp = response
      @validators.each_index do |i|
        @validators[i].each do |validator|
          error_message = nil
          valid, err_msg, alt = validator.call(resp)
          if !valid and !err_msg
            while !@error_messages[i]
              i = i + 1
              break if i == @error_messages.length
            end
            klass_error_message = @error_messages[i]
            err_msg = klass_error_message.call(resp) if klass_error_message
          end
          if alt
            resp = alt
            @alternate = alt
          end
          break unless valid
        end
        break unless valid
      end
      resp = response if @output_formats.empty?
      @output_formats.each do |output_format|
        resp = output_format.call(resp)
      end
      ResponseString.new(resp, raw_input, valid, err_msg, @alternate)
    end
  end

  class ChoiceInput < BaseInput
    error_message :default_error_message
    
    def self.get_choices
      @cs ||= []
    end
    
    def self.choices(*cs)
      if !get_choices.empty?
        raise SyntaxError, "cannot add multiple choice sets", caller
      end
      @cs = cs
    end
    
    def initialize(default_response)
      super
      @choices = self.class.get_choices
      @klasses.reverse.each do |klass|
        @error_message = curry_error_message(klass.get_error_message)
        break if @error_message
      end
    end
    
    def wrap_default(choice)
      choice == @default_response ? "(#{choice})" : choice
    end
    
    def prompt_suffix
      "[" + @choices.collect { |ch| wrap_default(ch) }.join('/') + "] "
    end
    
    def default_error_message(response)
      "Please enter one of #{@choices[0..-2].join(', ')} or #{@choices[-1]}"
    end
    
    def do_validate(response, raw_input)
      error_message = nil
      valid = @choices.member? response
      if !valid
        error_message = @error_message.call(response)
      end
      ResponseString.new(response, raw_input, valid, error_message)
    end
  end

  class MenuInput < ChoiceInput
  
    def self.get_items
      @its ||= []
    end
    
    def self.items(*its)
      @its ||= []
      if get_choices.empty?
        raise SyntaxError, "choices must be added before items", caller
      end
      if its.length != @cs.length
        raise SyntaxError, "number of items must match choices", caller
      end
      @its = its
    end
    
    def self.get_header
      @hdr ||= nil
    end
    
    def self.header(hdr)
      @hdr = hdr
    end
    
    def initialize(default_response)
      super
      klasses = @klasses[2..-1]
      @items = self.class.get_items
      klasses.reverse.each do |klass|
        @header = klass.get_header
        break if @header
      end
    end
    
    def prompt_suffix
      if @header
        ps = "#{@header}\n"
      else
        ps = "\n"
      end
      0.upto(@choices.length - 1) do |i|
        ps << " " if @choices[i] != @default_response
        ps << wrap_default(@choices[i]) + "\t" + @items[i] + "\n"
      end
      return ps
    end
  
    def default_error_message(response)
      "Please select one of the given options"
    end
    
    def do_validate(response, raw_input)
      rs = super
      rs.alternate = @items[@choices.index(response)] if rs.valid?
      rs
    end
  end
  
  class ResponseString < String
    attr_accessor :alternate
    attr_reader :error_message, :raw_input
    
    def initialize(resp, raw, valid, err_msg=nil, alt=nil)
      @raw_input = raw
      @valid = valid
      @error_message = err_msg
      @alternate = alt || resp
      super(resp)
    end
    
    def valid?
      @valid
    end
  end
  
end

if __FILE__ == $0

require 'date'

class IntegerInput < HighLine::ValueInput
  validate /^\d+$/
  validate proc { |r| [true, nil, r.to_i] }
end

puts IntegerInput.ask("Enter a number from 1 to 10, or Q to quit:") {
  okay_if /^q$/i
  validate :between?, 1, 10
}

class DateInput < HighLine::ValueInput
  validate :with_my, :check_date
  
  def check_date(r)
    begin
      test_date = Date.parse(r)
    rescue
      false
    else
      [true, nil, test_date]
    end
  end
end

puts DateInput.ask("Enter a date:") {
  output_format :to_s
  error_message "That is not a date!"
}

class YesOrNo < HighLine::ChoiceInput
  transform :downcase
  choices "y", "n"
  synonym "y", "yes", "oui", "si"
end

puts YesOrNo.ask("Is your computer turned on?", "y") 

class EditorMenu < HighLine::MenuInput
  header "Please select an editor:"
  choices "1", "2", "3"
  items "vim", "vim", "vim"
  error_message "There are no other editors!"
end

puts EditorMenu.ask("", "1").alternate

end