#!/usr/local/bin/ruby
############################################################################
### FRuby Client 1.0 by Retnur
### This code is released under GNU General Public License (GPL) version 2.
### 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"

# For standalone colors. 
# Ripped out of Rocketmud
$ansi_table = [
  [ ?d,  "30",  false ],
  [ ?D,  "30",  true ],
  [ ?r,  "31",  false ],
  [ ?R,  "31",  true ],
  [ ?g,  "32",  false ],
  [ ?G,  "32",  true ],
  [ ?y,  "33",  false ],
  [ ?Y,  "33",  true ],
  [ ?b,  "34",  false ],
  [ ?B,  "34",  true ],
  [ ?p,  "35",  false ],
  [ ?P,  "35",  true ],
  [ ?c,  "36",  false ],
  [ ?C,  "36",  true ],
  [ ?w,  "37",  false ],
  [ ?W,  "37",  true ]
]
### Ripped out of RocketMUD
# takes a string and returns a string with color codes added.
def render_color txt
    output = ""
    underline = bold = false
    last = -1

    i = 0
    while i < txt.size
      if txt[i] == ?#
        i += 1
        # toggle underline on/off with #u
        if txt[i] == ?u
          i += 1
          if underline
            underline = false
            output << "\e[0"
            output << ';1' if bold
            if last != -1
              output << ';'
              output << $ansi_table[last][1]
            end
            output << 'm'
          else
            underline = true
            output << "\e[4m"
          end
        # parse ## to #
        elsif txt[i] == ?#
          i += 1
          output << '#'
        # #n should clear all tags
        elsif txt[i] == ?n
          i += 1
          if last != -1 || underline || bold
            underline = false
            bold = false
            output << "\e[0m"
          end
          last = -1
        # check for valid color tag and parse
        else
          validTag = false
          $ansi_table.size.times do |j|
            if txt[i] == $ansi_table[j][0]
              validTag = true
              # we only add the color sequence if it's needed
              if last != j
                cSequence = false
                # escape sequence
                output << "\e["
                # remember if a color change is needed
                cSequence = true if last == -1 || last / 2 != j / 2
                # handle font boldness
                if bold && $ansi_table[j][2] == false
                  output << '0'
                  bold = false
                  output << ";4" if underline
                  # changing from bold wipes the old color
                  output << ';'
                  cSequence = true
                elsif !bold && $ansi_table[j][2] == true
                  output << '1'
                  bold = true
                  output << ';' if cSequence
                end
                # add color sequence if needed
                output << $ansi_table[j][1] if cSequence
                output << 'm'
              end
              # remember the last color
              last = j
            end
          end
          # it wasn't a valid color tag
          if !validTag
            output << '#'
          else
            i += 1
          end
        end
      else
        output << txt[i].chr
        i += 1
      end
    end # while
    # and terminate it with the standard color
    if last != -1 || underline || bold
      output << "\e[0m"
    end
    return output
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(1 == 1 and 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" => "server01.mudbytes.net",
                                    "Port" => 5000,
                                    "Telnetmode" => false,
                                    "Prompt" => /\n/)
    
    # 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+ homework2 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!"
    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.scan( /(\S+)=([^"\s]+)/) do |k,v|
        new_data[k] = v
      end
      data.scan( /(\S+)="([^"\\]*(\\.[^"\\]*)*)"/) do |k,v|
        new_data[k] = v
      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[#Y#{data['channel']}#R] #C#{sender}: #c#{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



### Beginning of actual execution.
### You ned to change this name and password.
### create our client and a lock for it.
client = IMCclient.new("Rstandalone", "mypass")
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

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

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

  
  ### Syncs our thread just in case we do something that isn't totally thread safe there.
  imclock.synchronize do
    client.channel_send("#{name.capitalize}", "Server01:ichat", s)
    puts render_color("#R[#YServer01:ichat#R] #C#{name.capitalize}:#c #{s}#n")
  end
end

client.shutdown