# vim:ts=4:autoindent

require 'Log'
require 'socket'

#
# All DeeDialServ does is connect to a host, port, with a password.
# It optionally connects to the admin port of the huntgroup software.
# It then generates events for every line sent to it.
# It provides a method, write(), to send an unformatted line to the ddial.
#
class Event < Hash
    def linkline
        "#{self[:link].nil? ? "" : self[:link]}#{self[:line].nil? ? "" : self[:line]}"
    end
end

class DeeDialServ
    attr_accessor :admin_dump_on_bootup

    def initialize( host, port, passcode, do_admin )
        @host = host
        @port = port
        @passcode = passcode
        @do_admin = do_admin

        @raw_handlers = []
        @handlers = []
        @admin_handlers = []

        @admin_dump_on_bootup = []
    end


    def on_raw_output(&block)
        @raw_handlers << block
    end

    def on_event(post_filter=nil, &block)
        @handlers << [ block, post_filter ]
    end

    def on_admin(&block)
        @admin_handlers << block
    end

    HANDLE_REGEX = %q~[\[\]\-\\!@#$%&*(=_+{|;'",./<>?` A-Za-z0-9]*~
    STNAME_REGEX = %q~[\[\]\-\\!@#$%&*(=_+{|;'",./<>?` A-Za-z0-9):]*~   # how annoying
    LINE2COLON_REGEX = %q~(\d+)?[\#\/]([0-7])([\[(<])T([1-4])([\-=:])~

    def parse_loginsphead(md, ed)                                       # parse }-->; +^  and  }}-->. -^
        ed[:link]           =  md[1].to_i if md[1] != ""
        ed[:station_full]   = (md[2] == (":" or ";") ? true : false)
        ed[:station_locked] = (md[2] == ("," or ";") ? true : false)
        nil
    end

    def parse_line2handle(input, ed)                                    # parse /1[T4:Mouse, #6<T1-Chasm
        md = /#{LINE2COLON_REGEX}(#{HANDLE_REGEX})/.match(input) if input.class == String
        md = input if input.class == MatchData
        return nil if md.nil?

        ed[:link]           =  md[1].to_i if md[1] != nil
        ed[:line]           =  md[2].to_i
        ed[:validated]      = (md[3] == "(" ? false : true)
        ed[:hat]            = (md[3] == "<" ? true : false)
        ed[:channel]        =  md[4].to_i
        ed[:remote]         = (md[5] == ":" ? false : true)
        ed[:handle]         =  md[6]
        md
    end

    def parse_loginsptail(input, ed)                                    # parse :#059*  and :059* (and nothing)
        md = /^:#?(\d+)(\*?)/.match(input)
        if not md.nil?
            ed[:acct]       =  md[1].to_i
            ed[:acctstar]   = (md[2] == "*" ? true : false)
        end
        nil
    end


    def handle_raw_line(line)
        print "--> "
        p line
        # CALL RAW LINE HANDLERS
        @raw_handlers.each{ |h| h.call(line) }

        ed = nil
        catch :done do
            # PARSE LOGINS AND LOGOUTS
            if not (md = /^}+(\d*)\a?-->([.,:;]) ([-+])\^/.match(line)).nil?
                ed = Event.new
                ed[:type]           = :loginout
                ed[:login]          = (md[3] == "+" ? true : false)
                parse_loginsphead(md, ed)
                if not (moe = /^[\#\/](\d)\r/.match(md.post_match)).nil?    # for -->. +^#5 shortmoes
                    ed[:line]       = moe[1].to_i
                    ed[:validated]  = ed[:hat] = ed[:remote] = false
                    ed[:channel]    = 1 # a guess, if that
                    ed[:handle]     = "?"
                else
                    md = parse_line2handle(md.post_match, ed)
                    parse_loginsptail(md.post_match, ed)
                end
                throw :done
            end

            # PARSE /SP LISTS
            #-? added for Pete's dial - what causes the - to be dropped?!?!?!
            #if not (md = /}}}(\d*)-([.,:;])(#{STNAME_REGEX})\^/.match(line)).nil?
            if not (md = /}}}(\d*)-?([.,:;])(#{STNAME_REGEX})\^/.match(line)).nil?
                ed = Event.new
                ed[:type]           = :sp
                parse_loginsphead(md, ed)
                ed[:station_name]   = md[3]
                ed[:users]          = []

                loop do
                    user = Event.new
                    md = parse_line2handle(md.post_match, user)
                    break if md.nil?
                    user[:link] = ed[:link]
                    parse_loginsptail(md.post_match, user)
                    ed[:users][user[:line]] = user
                end
                throw :done
            end

            # PARSE /E~ (remote email)
            if not (md = /^\/E~(.)(\d\d\d)/.match(line)).nil?
                ed = Event.new
                ed[:type]           = :email
                ed[:line]           = md[1][0] - '@'[0]
                ed[:acct]           = md[2].to_i

                if not (mdd = /^(\d\d\d)/.match(md.post_match)).nil?
                    ed[:to]         = mdd[1].to_i
                    ed[:message]    = mdd.post_match.chomp
                end
                throw :done
            end

            # PARSE /P & CHATTER (no throw - be last one)
            ed = nil
            if not (md = /^\/P(S|\d+) /.match(line)).nil?
                ed = Event.new
                ed[:type]           = :privmsg
                ed[:priv_to]        = (md[1] == "S" ? :station : md[1].to_i)
            end

            line2 = if ed == nil
                        line
                    elsif ed.class == Event
                        md.post_match
                    else
                        raise "What's my line? (parse /p)"
                    end

            if not (md = /^#{LINE2COLON_REGEX}(#{HANDLE_REGEX})\) /.match(line2)).nil?
                if ed == nil
                    ed = Event.new
                    ed[:type]       = :chatter
                end
                parse_line2handle(md, ed)
                ed[:message]        = md.post_match.chomp
            end

        end

        return if ed == nil

        @handlers.each{ |h, post_filter|
            oneshot = false

            if !post_filter.nil?
                skip = false
                oneshot = post_filter.delete(:oneshot) if post_filter[:oneshot] == true

                post_filter.each{ |key, value|
                    if value.class == Regexp
                        if (value =~ ed[key]) == nil
                            skip = true
                            break
                        end
                    else
                        if ed[key] != value
                            skip = true
                            break
                        end
                    end
                }
                next if skip == true
            end
            h.call(ed)
            @handlers.delete(h) if oneshot
        }
    end


    def handle_admin(line)
        # These are the messages admin will send:

        # ACCEPT port %d host %s
        # REJECT FULL host %s
        # CONSOLE port %d
        # ASSIGN port %d to %s
        # SHUT port %d

        @admin_handlers.each{ |h| h.call(line) }
    end

    def write_admin(msg)
        print "<-- ADMIN "
        p msg
        @admin_out.puts(msg) if !@admin_out.nil?  # for testing
    end

    def write(msg)
        print "<-- "
        p msg
        @socket.puts(msg) if !@socket.nil?   # for testing
    end


    def connect_ddial
        TCPSocket.new @host, @port
    end


    def connect_admin
        fout = File.open("HGROUP_IN", "w")  # NP1
        fout.sync=true
        fout.puts("OK\n")
        fin  = File.open("HGROUP_OUT", "r") # NP2
        fin.sync=true

        return fout, fin
    end


    def start
        @socket = connect_ddial()
        @admin_out, @admin_in = connect_admin() if @do_admin

        begin
            # Dump bootup commands to admin_out
            log "*** DeeDialServ is starting!"
            @admin_dump_on_bootup.each do |line|
                write_admin(line)
            end

            # Wait for login prompt (here due to \a and not \r)
            while not @socket.recv(512).include? "\a"
                ;
            end
            sleep 1

            # Put passcode
            write("#{@passcode}\r")

            # Main loop proper.
            # Distribute events to watching bots.

            rfds = []
            rfds << @socket
            rfds << @admin_in if @do_admin

            loop do
                readyfd = select(rfds, nil, nil).flatten
                readyfd.each do |fd|
                    if fd.equal?(@socket)
                        line = @socket.gets("\r")
                        raise "nil from @socket" if line == nil
                        handle_raw_line(line)
                    elsif fd.equal?(@admin_in)
                        line = @admin_in.gets("\n")
                        raise "nil from @admin_in" if line == nil
                        handle_admin(line.chomp)
                    else
                        raise "unknown file descriptor from select!"
                    end
                end
            end
        rescue IOError => ioe
            raise ioe unless @quit
        end
    end

end