################################################################################
# K5agent: login to kuro5hin.org and do things there.
#
# c:\K5agent>ruby K5agent.rb
#
# How to add functions at runtime:
#
# > hello
# Default response
#
# > add pattern /^hello/, hello
#
# > hello
# Default response from hello
#
# > >  def hello(pattern=nil,input=nil)
# > >    if @count==nil; @count=1; return 'I am ready for my first lesson.'; end
# > >    return 'Hello again.'
# > >  end # method_def
#
# > hello
# I am ready for my first lesson.
#
# > hi is a synonym for hello
# Okay, (?i-mx:^hi) has been added, and will now call hello.
#
# > hi
# Hello again.
#
# >
#
# K5agent-specific commands:
#
# > Use the login name trane.
# Okay I have set the login name to trane.
#
# > Use the password boogit.
# Okay I have set the password to ******.
#
# > Login to K$5.
# Okay I am logged in to kuro5hin.org. The current page title is: http://kuro5hin.org/...
#
# > Upvote the first page of hidden comments.
# ...
# Okay I have upvoted the comments on the first page of Review Hidden Comments.
#
# > Post a diary with the first page of hidden comments.
#...
# Okay I have posted a diary at: http://kuro5hin.org/...
#
# > Logout
# Okay, I have logged out.
#
# > quit
# Bye
#
################################################################################

require 'yaml'
require './K5'

$Quitwords = [':q', 'quit', 'exit', 'bye']

class MyAgent 

   attr_accessor :DEBUG

   def initialize(args=Array.new)
     @workingDir = File.expand_path(File.dirname(__FILE__))
     @filename = @workingDir + "/K5agent-api.yaml"
     unless load_message = load_synonyms
       @patterns = [ [/^(.*?) is a synonym for (.*)/, :add_synonym], 
                     [/^save syn/, :save_synonyms],
                     [/^load syn/, :load_synonyms], 
                     [/^add pattern (.*), (.*)/, :add_pattern], 
                     [/^>(.*)/, :defining_method],
                     [/^restart/, :restart],
                     [/^show api/, :show_api],
                     [/^show (.*)'s api/, :show_one_method_api]
                   ]
       save_synonyms # creates patterns.yaml on first use.
     else
       puts load_message 
     end
     @method_def = ''
     @DEBUG = 0

     @k5 = K5.new
   end

   def def_method method_def
     file = IO.read(__FILE__)
     return "Doesn't have 'end # class'" unless file =~ /(.*)end # class(.*)if \$0 == __FILE__(.*)/m
     grp1 = $1.to_s; grp2 = $2.to_s; grp3 = $3.to_s

     new_filename = get_new_filename
     File.open(new_filename, "w+") { |f|
        f.puts grp1 + method_def + "\n\nend # class\n\nif $0 == __FILE__" + grp3
     }
     @patterns.push( [@new_regexp, @new_symbol] )
     save_synonyms
     puts "#{method_def}"
     MyAgent.class_eval(method_def)
   end

   # If this file has a '.' + a number before the '.rb' extension 
   # (i.e. foo.0.rb), increment the number (i.e. return foo.1.rb).
   # Otherwise return the name of this file unchanged.
   def get_new_filename
      old_filename = __FILE__
      if old_filename =~ /(.*)\.(\d+)\.rb$/
        incr = $2.to_i + 1
        return new_filename = $1.to_s + "." + incr.to_s + ".rb"
      end
      return old_filename  # Dangerous!
   end

   # Match input against each regexp pattern in @patterns; 
   # if there's a match, call the method associated with the pattern.
   def getResponse input
     r = "Default response."

     @score = 2

     puts "input==#{input}" unless @DEBUG == 0
     @patterns.each { |pattern|
       if input =~ pattern[0]
          return [self.send(pattern[1], pattern[0], input), @score] rescue return r
       end
     }
     return r
   end

   def load_synonyms(pattern=nil, input=nil)
     if File.exist?(@filename)
        @patterns = YAML.load_file(@filename)
        return "I have loaded #{@filename}."
     end 
     return false
   end

   def save_synonyms(pattern=nil, input=nil)
      r = "Eep! error saving synonyms! "
      begin
        File.open(@filename, 'w') do |out|
          YAML.dump(@patterns, out)
        end
        return "Okay I have saved the synonyms to disk."
      rescue => error
        return r << error
      end
   end

   # a is a synonym for b, or /a/ is a synonym for /b/
   def add_synonym(pattern, input)
     return "Problem matching pattern." unless input =~ pattern
     g1 = $1; g2 = $2; #puts "[add_synonym] g1==#{g1}; g2=#{g2}" 
     # Remove enclosing "/", if present.
     if g1 =~ /^\// then g1.sub!(/^\//, '') end
     if g1 =~ /\/$/ then g1.sub!(/\/$/, '') end
     if g2 =~ /^\// then g2.sub!(/^\//, '') end
     if g2 =~ /\/$/ then g2.sub!(/\/$/, '') end
     #puts "[add_synonym] g1==#{g1}; g2=#{g2}"
     grp1 = %r{(?i-mx:^#{g1})}
     grp2 = %r{(?i-mx:^#{g2})}
     #puts "[add_synonym] grp1==#{grp1}; grp2=#{grp2}"
     method =''
     index = -1 # To add the new pattern before the old one.
     @patterns.each_with_index { |p,i| 
       #puts "p[0]==#{p[0]}; p[0].class==#{p[0].class}; grp2==#{grp2}; grp2.class==#{grp2.class}"
       if p[0].to_s == grp2.to_s
          method = p[1]
          index = i
          break
       end
     }
     # If still no match, try again with slightly different regexps.
    if method == '' then grp1 = %r{(?i-mx:#{g1})}; grp2 = %r{(?i-mx:#{g2})}; end
     @patterns.each_with_index {|p,i| 
       if p[0].to_s == grp2.to_s; method = p[1]; index = i; break; end}
     if method == '' then grp1 = %r{#{g1}}; grp2 = %r{#{g2}}; end
     @patterns.each_with_index {|p,i| 
       if p[0].to_s == grp2.to_s; method = p[1]; index = i; break; end}
     if method == '' then grp1 = %r{^#{g1}}; grp2 = %r{^#{g2}}; end
     @patterns.each_with_index {|p,i| 
       if p[0].to_s == grp2.to_s; method = p[1]; index = i; break; end}
 
     return unless method.to_s != nil
     puts "add_synonym: index==#{index}" unless @DEBUG == 0
     @patterns.insert(index, [grp1, method] )
     save_synonyms
     return "Okay, #{grp1} has been added, and will now call #{method}." 
   end

   def add_pattern(pattern=nil, input=nil)
     if input =~ pattern 
       @new_regexp = $1
       @new_symbol = $2
       @new_regexp.gsub!(/\//, '')
       new_re = %r{#{@new_regexp}}

       # Make the regexp case insensitive.
       new_re_s = new_re.to_s
       if new_re_s =~ /(-mix)/
         new_re_s = $~.pre_match << "i-mx" << $~.post_match
       end
       @new_regexp = %r{#{new_re_s}}
       puts "@new_regexp==#{@new_regexp}" unless @DEBUG == 0

       method_def = "   def #{@new_symbol}(pattern=nil, input=nil)\n     return \"#\{input} doesn't match #\{pattern}\" unless input =~ pattern\n     return 'Default response from " + @new_symbol + "'\n   end # method_def"
       return def_method(method_def)
     else
       return "#{input} doesn't match #{pattern}."
     end
   end

   def defining_method(pattern=nil, input=nil)
     if input =~ pattern
       @method_def << "\n " << $1.to_s
       if @method_def =~ /end # method_def/
         return def_method(@method_def)
       end
       return ''
     else
       return "#{input} doesn't match #{pattern}."
     end
   end

   def method_missing input, *rest
     return getResponse(input.to_s) 
   end

   # Strips delimiters.
   def process str
      str.strip!
      str.sub!(/\?$/, '')
      str.sub!(/^\[/, '')
      str.sub!(/\]$/, '')
      str.sub!(/^(?:"|')/, '')
      str.sub!(/(?:"|')$/, '')
      str.sub!(/\.$/, '')
      str.sub!(/;$/, '')
      str.sub!(/,$/, '')
      str.sub!(/!$/, '')
      str.sub!(/\?$/, '')
      str.sub!(/^\//, '')
      str.sub!(/\/$/, '')
      return str 
   end

   def restart(pattern=nil,input=nil)
     exec "ruby #{__FILE__}" 
   end # method_def

   def show_api(pattern=nil, input=nil)
     return "#{input} doesn't match #{pattern}" unless input =~ pattern
     apiarr= Array.new # Contains methods and patterns from MyAgent-api.yaml.
     @patterns.each { |p|
       regexp = p[0].to_s; method = p[1].to_s; found = false
       puts "regexp=#{regexp}; method=#{method}" unless @DEBUG == 0
       apiarr.each { |arr| if arr.include? method then found = true end }
       next unless not found
       apiarr.push [regexp, method] 
     } # If more than one pattern maps to a method, only the first is stored.
     # Try to pretty-print.
     toReturn = ''
     puts "apiarr.inspect=#{apiarr.inspect}" unless @DEBUG == 0
     apiarr.each { |arr|
       puts "1. arr=#{arr}" unless @DEBUG == 0
       regex = arr[0]; method = arr[1] 
       if regex =~ /\(\?(?:[i\-mx]*)(?:\:)?(?:\^)?(.*)\)/ # try to clean regex
         regex = $1.to_s
       end
       if method == nil or method == '' then method = "Default response" end
       toReturn << method.to_s << " | " << regex.to_s << "\n"
     }
     return toReturn
   end # method_def

   def show_one_method_api(pattern=nil, input=nil)
     return "#{input} doesn't match #{pattern}" unless input =~ pattern
     grp1 = process $1 # Supplied method name.
     apiarr= Array.new # To hold methods and patterns from MyAgent-api.yaml.
     @patterns.each { |p| # Collect all the regexps for the supplied method.
       regexp = p[0].to_s; method = p[1].to_s
       puts "regexp=#{regexp}; method=#{method}." unless @DEBUG == 0
       if method == grp1 then apiarr.push [regexp, method] end 
     } 
     # Try to pretty-print.
     toReturn = ''
     puts "apiarr.inspect=#{apiarr.inspect}" unless @DEBUG == 0
     apiarr.each { |arr|
       puts "1. arr=#{arr}" unless @DEBUG == 0
       regex = arr[0]; method = arr[1] 
       if regex =~ /\(\?(?:[i\-mx]*)(?:\:)?(?:\^)?(.*)\)/ # try to clean regex
         regex = $1.to_s
       end
       if method == nil or method == '' then method = "Default response" end
       toReturn << method.to_s << " | " << regex.to_s << "\n"
     }
     return toReturn
   end # method_def

   def use_the_login_name(pattern=nil, input=nil)
     return "#{input} doesn't match #{pattern}" unless input =~ pattern
     grp1 = process $1.to_s # does process() strip out valid login characters?
     @k5.name = grp1
     return "Okay I have set the login name to #{grp1}."
   end # method_def

   def use_the_password(pattern=nil, input=nil)
     return "#{input} doesn't match #{pattern}" unless input =~ pattern
     grp1 = $1.to_s # don't use process() here?
     @k5.password = grp1
     mask = grp1.gsub(/./, '*')
     return "Okay I have set the password to #{mask}."
   end # method_def 

   def upvote_the_first_page_of_hidden_comments(pattern=nil, input=nil)
     return "#{input} doesn't match #{pattern}" unless input =~ pattern
     @k5.review_hidden_comments
     return "Okay I have upvoted the comments on the first page of Review Hidden Comments."
   end # method_def

   def upvote_the_first_n_comments(pattern=nil, input=nil)
     return "#{input} doesn't match #{pattern}" unless input =~ pattern
     grp1 = process $1.to_s
     n = 0
     begin 
       n = grp1.to_i 
     rescue Error => e
       return "Sorry I can't convert #{grp1} to an integer." 
     end
     if n == 0 then return "No comments upvoted; n == #{n}." end
     n = n - 1
     @k5.review_hidden_comments n
     s = 's'
     if n == 0 then s = '' end
     if grp1 > 29 then return "Sorry I can only upvote up to 30 comments." end
     return "Okay I have upvoted the first #{n+1} hidden comment#{s}."
   end # method_def

   def post_a_diary_of_the_first_page_of_hidden_comments(pattern=nil, input=nil)
     return "#{input} doesn't match #{pattern}" unless input =~ pattern
     response = @k5.grab_hidden_comments_text
     return response 
   end # method_def

   def logout(pattern=nil, input=nil)
     return "#{input} doesn't match #{pattern}" unless input =~ pattern
     #@k5.b.logout # TODO: implement.
     @k5.b.close # closes the browser.
   end # method_def

   def login_to_k5(pattern=nil, input=nil)
     return "#{input} doesn't match #{pattern}" unless input =~ pattern
     title = @k5.login
     return "Okay I am logged in to kuro5hin.org. The current page is #{title}."
   end # method_def

   def set_the_sleep_to(pattern=nil, input=nil)
     return "#{input} doesn't match #{pattern}" unless input =~ pattern
     puts "set_the_sleep_to #{$1}." unless @DEBUG == 0
     grp1 = $1.to_s
     sleep_value = 3
     begin
       sleep_value = grp1.to_i
     rescue Error => e
       return "Sorry, I can't convert #{grp1} to an integer."
     end
     @k5.sleep_value = sleep_value
     return "Okay I have set the sleep to #{sleep_value}."
   end # method_def

   def use_the_login_name_and_the_password(pattern=nil, input=nil)
     return "#{input} doesn't match #{pattern}" unless input =~ pattern
     grp1 = process $1.to_s # will process cause login names to be truncated?
     grp2 = $2.to_s
     r1 = self.send("Use the login name #{grp1}")
     r2 = self.send("Use the password #{grp2}")
     return "#{r1[0]} #{r2[0]}"
   end # method_def

   def use_page_n(pattern=nil, input=nil)
     return "#{input} doesn't match #{pattern}" unless input =~ pattern
     grp1 = process $1.to_s
     page = 1
     begin
       page = grp1.to_i
     rescue Error => e
       return "I can't interpret #{grp1} as an integer, sorry!"
     end
     @k5.page = page
     return "Okay I will use page #{page}."
   end # method_def

end # class

if $0 == __FILE__
  bot = MyAgent.new
  print "\n> "; $stdout.flush
  while (line = gets) !~ /^#{$Quitwords.join('|')}$/i and line !~ /^$/
    response = bot.send(line.to_s).to_s
    if response.class == "Array" then response = response[0] end 
    response = response.chop # remove the score
    puts response + "\n\n"
    print "> "; $stdout.flush
  end
  puts "Bye"
end # of file
