#!/usr/local/bin/ruby
############################################################################
### FRuby Client 1.1 by Retnur
### A big thanks goes out to Kiasyn.
############################################################################
# Copyright (c) 2009, Jeffrey Heath Basurto <bigng22@gmail.com>
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
############################################################################
require "net/telnet.rb"
require 'thread'
require "eventmachine"
require 'strscan'
# For standalone colors. 
$color_table = {
    "#z" => "\e[0;30m", 
    "#Z" => "\e[1;30m",
    "#r" => "\e[0;31m",
    "#R" => "\e[1;31m",
    "#g" => "\e[0;32m",
    "#G" => "\e[1;32m",
    "#y" => "\e[0;33m",
    "#Y" => "\e[1;33m",
    "#b" => "\e[0;34m",
    "#B" => "\e[1;34m",
    "#m" => "\e[0;35m",
    "#M" => "\e[1;35m",
    "#c" => "\e[0;36m",
    "#C" => "\e[1;36m",
    "#w" => "\e[0;37m",
    "#W" => "\e[1;37m",
    "#n" => "\e[0m",
}
$comp_tab = Regexp.union *$color_table.keys
def render_color(data)
  last_code = nil
  data.gsub($comp_tab) do |s|
    if last_code != nil && last_code.eql?(s)
      ""
    else
      last_code = s
      $color_table[s]
    end
  end
end


### String extensions 
###
class String
  # Return the first line in a string
  def pop_line
    if (pos = index("\n")) != nil then
      return slice!(0..pos).chomp
    end
    nil
  end

  def encapsulate
    s = self
    s = s.reverse
    s.concat '"'
    s = s.reverse
    s.concat '"'
    return s
  end
end

### Net::Telnet extensions
###
class Net::Telnet
  def read_from_telnet # :yield: recvdata
    time_out = @options["Timeout"]
    waittime = @options["Waittime"]
    if time_out == false
      time_out = nil
    end
    line = ''
    buf = ''
    rest = ''
    until(not IO::select([@sock], nil, nil, waittime))
      unless IO::select([@sock], nil, nil, time_out)
        raise TimeoutError, "timed out while waiting for more data"
      end
      begin
        c = @sock.readpartial(1024 * 1024)
        @dumplog.log_dump('<', c) if @options.has_key?("Dump_log")
        buf = c
        buf.gsub!(/#{EOL}/no, "\n") unless @options["Binmode"]
        rest = ''
        @log.print(buf) if @options.has_key?("Output_log")
        line += buf
        yield buf if block_given?
      rescue # End of file reached
        if line == ''
          line = nil
          yield nil if block_given?
        end
        break
      end
    end
    line
  end
end
### Look for usage below for examples.
### You should only need to instantiate this a single time.
class IMCclient
  ### This is called when IMCclient is instantiated.
  def initialize(name, pw)
    @connection = Net::Telnet::new( "Host" => "www.mudbytes.net",
                                    "Port" => 5000,
                                    "Telnetmode" => false,
                                    "Prompt" => /\n/)
    @pw = pw
    # Start authenticating. Will autosetup if IMC server does not have configuration.
    @connection.puts "PW #{name} #{pw} version=2 autosetup #{pw}2"
    # Set the sequence, which is a number associated with each packet
    @sequence = Time.now.to_i
    @myname = name
  end
 
  ### call this in some event.  It really should called at least twice a second.
  ### This continually checks for incoming packets
  def accept_data 
    # empty string
    s= ""
    ### read from connection and put it in s
    @connection.read_from_telnet do |c|
      break if c == nil
      s << c
    end

    ### If s is empty we return.
    return if s.chomp.empty? 
    ### Search for new packets line by line and process.
    while ( (line = s.pop_line) != nil )
      ### Does something with each line. Probably a chat message.
      handle_server_input line
    end
  end
 
  ### Handles raw input
  ### Directs towards packets.
  def handle_server_input(s)
    # <sender>@<origin> <sequence> <route> <packet-type> <target>@<destination> <data...>
    
    case s.strip
      #On initial client connection:
      #SERVER Sends: autosetup <servername> accept <networkname> (SHA256-SET)
    when /^autosetup (\S+) accept (\S+)$/
      puts "Autosetup complete. Connected to #{$1} on network #{$2}\n"
    when /^PW Server\d+ #{@pw}2 version=2 (\S+)$/i
      puts "IMC Authentication complete\n"
      send_isalive
    when /^(\S+) \d+ \S+ (\S+) (\S+)$/i
      handle_packet( $1, $2, $3 )
    when /^(\S+) \d+ \S+ (\S+) (\S+) (.*)$/i
      handle_packet( $1, $2, $3, $4 )
    else
      puts "Not found! #{s}"
    end
  end
  ### Processes all packets and directs it.
  def handle_packet( sender, type, target, data=nil )
    if data != nil and data.is_a?( String ) then
      new_data = Hash.new
      data.strip!
      data = StringScanner.new(data) 
      while !data.eos?
        key = data.scan(/\w+/) ### grab a key
        data.skip(/=/) ### skip the =
        if data.peek(1).eql?('"')
          val = data.scan(/"([^"\\]*(\\.[^"\\]*)*)"/)
          val.slice!(0)
          val.slice!(-1)
        else
          val = ""
          while (!data.eos? && !data.peek(1).eql?(' '))
            val << data.getch
          end
        end
        data.skip(/\s+/)
        new_data[key] = val
      end
      data = new_data
    end

    return if sender.include?("@#{@myname}")
    return if data != nil and data.include? 'sender' and data['sender'].include? "@#{@myname}"

    case type
    when "keepalive-request"
      send_isalive
    when "ice-msg-b"
      puts render_color("#R[#{data['channel']}] #B#{sender}: #b#{data['text']}")
    end
  end

  ### Sends a packet in the right format.
  def packet_send( sender, type, target, destination, data)
    data_out = ""
    if data.is_a?( Hash ) then
      hash = data.to_hash
      hash.each do |k, v|
        if v.to_s.include? " " then
          v = v.encapsulate
        end
        data_out.concat "#{k}=#{v} "
      end
    end
    packet = "#{sender}@#{@myname} #{@sequence} #{@myname} #{type} #{target}@#{destination} #{data_out}"
    @sequence += 1
    @connection.puts packet
  end

  ### This formats for a specific channel.
  ### channel_send "Retnur", Server01:ichat, "example string"
  def channel_send( sender, channel, message )
    packet_send( sender, 'ice-msg-b', '*', '*', {:channel => channel, :text => message, :emote => 0, :echo => 1 })
  end

  ### Sends info to IMC server about mud.
  ### host is your muds address, port is its port.  url is your web site.
  ### This isn't required.
  def send_isalive
    packet_send( "*", "is-alive", "*", "*", { :versionid => "FRuby Client1.0", :url => "", :host => "", :port => 0} )
  end

  ### Shut the IMCclient down.
  def shutdown
    puts "Connection closed."
    @connection.close
  end
end

### A list of all known IMC channels.
$imc_channel_list = {"icode"=>"Server02:icode", 
                     "igame"=>"Server02:igame",
                     "ichat"=>"Server01:ichat", 
                     "inews"=>"Server02:inews", 
                     "imudnews"=>"Server02:imudnews", 
                     "ibuild"=>"Server01:ibuild", 
                     "imusic"=>"Server02:imusic"}
$chan_compiled = Regexp.union *$imc_channel_list.keys

### Get your name.
puts  "Type quit to exit."
print "What is your name? "
name = gets
name = name.chomp

### Beginning of actual execution.
### You ned to change this name and password.
### create our client and a lock for it.
client = IMCclient.new("RClient_Test", "RClient")
imclock = Mutex.new

### Run EventMachine in a new thread. 
### This lets it autonomously receive data 
### even when the application stops waiting on user input.
t = Thread.new() do
  EventMachine::run do
    EM::add_periodic_timer(0.01) do
      ### takes control of the lock for our IMC client.
      imclock.synchronize do
        client.accept_data
      end
    end
  end
end


### Defaults to ichat
$cur_channel = "Server01:ichat"
### This loop for all processing of input.
### accept input forever
loop do
  print "\n>>>"

  ### Gets a full line of input, and stops this thread while we wait.
  s = gets
  s = s.strip

  next if s.empty?

  ### Type quit to end the stand alone client.
  break if s =~ /quit|shutdown/i

  if s =~ $chan_compiled
    ### Channel found so set our chan to this
    $cur_channel = $imc_channel_list[s] ### Should set our current channel to this
    puts "Channel set to " + s + "."
    next
  end
  print "\n"
  ### Syncs our thread just in case we do something that isn't totally thread safe there.
  imclock.synchronize do
    client.channel_send("#{name.capitalize}", $cur_channel, s)
    puts render_color("#R[#{$cur_channel}] #b#{name.capitalize}:#C #{s}#n")
  end
end

client.shutdown