# vim:ts=4:autoindent

require 'yaml'
require 'DeeDialServ'
require 'Log'

#
# DeeDial is the class with the action, baby.  It gets a connection to
# DeeDialServ and reads the raw events, and creates Stations and Users
# based on those events.  DeeDial will generate smart events and pass the
# User objects along with it.
#

#
# Smart events generated:
#
# :type => :login       :user => u
# :type => :logout      :user => u
# :type => :privmsg     :user => u      :message => txt     :priv_to => :station || line
# :type => :chatter     :user => u      :message => txt
# :type => :u_change    :kind => (:handle || :channel)      :old => (txt || num)
# :type => :u_change    :kind => (:validated || :lose_hat || :gain_hat)


# This hash is to convert admin-handler ports to the corresponding line.
# XXX IT needs to be changed if the bot is moved! XXX
PORT_TO_LINK = { "Main" => "", "Subs" => "7", "City" => "76" }

class DeeDial
    Station = Struct.new(:line, :name, :is_full, :is_locked, :users)
    class User < Struct.new(:line, :handle, :channel, :is_validated,
                            :is_hat, :is_remote, :acct, :acctstar,
                            :on_station, :on_port, :idle, :last_chatter, :dd )
        def lha
            linehandleacct(false)
        end

        def llha
            linehandleacct(true)
        end

        def linehandleacct(append_link)
            "##{append_link ? self.line : self.line[-1,1]}:#{self.handle}" +
                (self.acct.nil? ? "" : (":%03d" % self.acct)) + (self.acctstar.nil? ? "" : "*")
        end

        def privmsg(txt)
            self.dd.write( "/P#{self.line}" + txt )
        end
    end

    attr_reader :stations, :users, :ports, :ips_to_accts, :accts

    def initialize(host, port, passcode, do_admin, opts=nil)
        @dd = DeeDialServ.new(host, port, passcode, do_admin)

        @services = {}
        @stations = {}
        @users = {}
        @ports = []
        @handlers = []

        if opts && opts[:load_services]
            opts[:load_services].each{ |s| load_service(s) }
        end
        
        File.open( "ips_to_accts.yaml" ){ |yf| @ips_to_accts = YAML.load(yf) }
        File.open( "accts.yaml" ){ |yf| @accts = YAML.load(yf) }

        @dd.on_event( :type => :loginout, :acct => ACCT, :oneshot => true ){ |ev| on_login(ev) }

        @dd.on_admin{ |line| handle_admin(line) }
        @dd.on_event( :type => :sp ){ |ev| did_sp(ev) }
        @dd.on_event( :type => :loginout ){ |ev| did_loginout(ev) }
        @dd.on_event( :type => :privmsg ){ |ev| did_privmsg(ev) }
        @dd.on_event( :type => :chatter ){ |ev| did_chatter(ev) }
        @dd.on_event( :type => :email ){ |ev| did_email(ev) }

        self.on_event( self, :message => /ragnarok, list services/i ){ |ev| do_listservices(ev) }
        self.on_event( self, :message => /ragnarok, unload /i ){ |ev, md| do_unload(ev, md) }
        self.on_event( self, :message => /ragnarok, load /i ){ |ev, md| do_load(ev, md) }

        @dd.admin_dump_on_bootup << "DUMP\n"

    end

    def load_service(s)
        log "Loading service #{s}.."
        if @services.has_key? s
            log "  ** Error: Service already loaded!"
            return
        end

        require "services/#{s}"
        @services[s] = eval(s).new(self)
    end

    def unload_service(s)
        log "Unloading service #{s}.."
        if !@services.has_key?(s)
            log "  ** Error: No service to unload!"
            return
        end

        serv = @services.delete(s)
        @handlers.each do |h|
            action, post_filter, service = h[0], h[1], h[2]
            if serv == service
                log "  Removing handler: #{post_filter.inspect}"
                @handlers.delete(h)
            end
        end

        raise "Couldn't remove_const" if Object.send(:remove_const, s.to_sym) == nil
        raise "Couldnt remove serv from $\"" if $".delete("services/#{s}.rb") == nil
    end

    def do_listservices(ev)
        @dd.write( "#0<T1:Ragnarok) I currently have these services loaded:\r" )
        @services.each{ |key, value| @dd.write( "#0<T1:Ragnarok) #{key}\r" ) }
    end

    def do_unload(ev, md)
        s = md.post_match
        if @services.has_key? s
            @dd.write( "#0<T1:Ragnarok) Unloading service #{s}..\r" )
            unload_service(s)
        else
            @dd.write( "#0<T1:Ragnarok) I don't seem to have service #{s}.\r")
        end
    end

    def do_load(ev, md)
        s = md.post_match
        if @services.has_key? s
            @dd.write( "#0<T1:Ragnarok) I already seem to have service #{s}.\r")
        else
            @dd.write( "#0<T1:Ragnarok) Loading service #{s}..\r" )
            load_service(s)
        end
    end

    def handle_admin(line)
        puts "ADMIN --> #{line}"
           if not (md = /ACCEPT port (\d+) host /.match(line)).nil?
            pnum = md[1].to_i
            host = md.post_match
            log "@ports[#{pnum}] had value on ACCEPT (should be nil)" if !@ports[pnum].nil?
            @ports[pnum] = [ host, nil ]
            broadcast_event( :type => :admin, :kind => :accept, :port => pnum,
                             :host => host, :accts => @ips_to_accts[host] )
        elsif not (md = /ASSIGN port (\d+) to (\w+) (\d+)/.match(line)).nil?
            pnum = md[1].to_i
            p2l = PORT_TO_LINK[md[2]] + md[3]
            log "@ports[#{pnum}] had nil on ASSIGN (should contain host)" if @ports[pnum].nil?
            log "@users[#{p2l}] had value on ASSIGN (should be nil)" if !@users[p2l].nil?
            p = @ports[pnum]
            u = p[1] = @users[p2l] = User.new
            u.idle = Time.now
            u.on_port = p
            broadcast_event( :type => :admin, :kind => :assign, :port => pnum, :where => "#{md[2]} #{md[3]}" )
        elsif not (md = /SHUT port (\d+)/.match(line)).nil?
            pnum = md[1].to_i
            log "@ports[#{pnum}] had nil on SHUT (should contain host/user)" if @ports[pnum].nil?
            u = @ports[pnum][1]
            u.on_port = nil if !u.nil?   # the if is vday bugfix!
            @ports[pnum] = nil
            broadcast_event( :type => :admin, :kind => :shut, :port => pnum )
        elsif not (md = /REJECT FULL host /.match(line)).nil?
            broadcast_event( :type => :admin, :kind => :reject, :host => md.post_match )
        elsif not (md = /CONSOLE port (\d+)/.match(line)).nil?
            pnum = md[1].to_i
            broadcast_event( :type => :admin, :kind => :console, :port => pnum )
        end
    end

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

    def write(line)
        @dd.write(line)
    end

    def on_login(ev)  # on BOT login ...
        @dd.write "/sp\r"
    end

    # Monitor station lists.  At bootup, initializes @stations and @users.
    def did_sp(ev)
        if !$test && @stations.include?( ev.linkline )
            File.open("ips_to_accts.yaml", "w"){ |yf| YAML.dump(@ips_to_accts, yf) }
            File.open("accts.yaml", "w"){ |yf| YAML.dump(@accts, yf) }
            log "Flushing ips_to_accts, accts"
        end

        return if @stations.include?( ev.linkline )

        # We have a station we've never seen before.  Log the station..
        new_st = @stations[ev.linkline.to_s] = Station.new
        new_st.users = []

        [ [:station_name, :name], [:station_full, :is_full], [:station_locked, :is_locked],
          [:link, :line] ].each{ |ev_key, st_key| new_st[st_key] = ev[ev_key] }

        # And its users.
        ev[:users].each do |ev_u|
            next if ev_u.nil?
            add_user(ev_u, new_st)

            # Send a /p to any remotes on our links to receive station lists from them.
            @dd.write "/p#{ev[:link]}#{ev_u[:line]}/sp\r" if ev_u[:remote]
        end
    end

    # After bootup and initial station list import monitor logins and logouts.
    def did_loginout(ev)
        st = @stations[ev[:link].to_s]
        return if st.nil? #toss if we don't monitor this station - we'll pick it up on /sp

        if ev[:login] == true
            user = add_user(ev, st)
            did_login(user)
            return if user.on_port.nil?
            host = user.on_port[0]
            acct = user.acct.nil? ? 1000 : user.acct
            @ips_to_accts[host] ||= {}
            @ips_to_accts[host][acct] ||= 0
            @ips_to_accts[host][acct] += 1
        else
            user = @users[ev.linkline]
            log "#{ev.linkline} logging out, but we didn't have in @users!" if user.nil?
            did_logout(@users[ev.linkline]) if !user.nil?   # DON'T broadcast if for some reason we don't have!!
            st.users[ev[:line]] = nil
            user = @users.delete(ev.linkline)
            if !user.acct.nil?
                @accts[user.acct] ||= {}
                @accts[user.acct][:last_seen] = Time.now
                @accts[user.acct][:last_chatter] = user.last_chatter
            end
        end
    end

    # Monitor events and changes in user status.
    def did_privmsg(ev)
        u = @users[ev.linkline]
        log "receiving privmsg but don't have @users[#{ev.linkline}] - adding with limited info" if u.nil?
        u = add_user(ev, @stations[(ev[:link].nil? ? "" : ev[:link].to_s)]) if u.nil?

        check_for_change(ev, u)
        broadcast_event( :type => :privmsg, :user => u, :priv_to => ev[:priv_to], :message => ev[:message] )
    end

    def did_chatter(ev)
        u = @users[ev.linkline]
        log "receiving chatter but didn't have @users[#{ev.linkline}]" if u.nil?
        return if u.nil?

        u.idle = Time.now
        u.last_chatter = ev[:message]
        check_for_change(ev, u)
        broadcast_event( :type => :chatter, :user => u, :message => ev[:message] )
    end

    # Add user, for station list initial login & subsequent logins.
    def add_user(ev_u, st)
        @users[ev_u.linkline] ||= User.new
        user = st.users[ev_u[:line]] = @users[ev_u.linkline]
        user.line = ev_u.linkline
        user.on_station = st
        user.dd = @dd

        [ [:handle, :handle], [:channel, :channel], [:validated, :is_validated],
          [:hat, :is_hat], [:remote, :is_remote] ].each{ |ev_key, u_key| user[u_key] = ev_u[ev_key] }
        [ :acct, :acctstar ].each{ |key| user[key] = ev_u[key] } if !ev_u[:acct].nil?

        user
    end

    def check_for_change(ev, u) #and update
        return if u.nil?
        return if u.line != ev.linkline

        if u.handle != ev[:handle]
            old, u.handle = u.handle, ev[:handle]
            log "#{u.handle} changed his handle (from #{old})"
            broadcast_event( :type => :u_change, :kind => :handle, :old => old )
        end
        if (u.is_validated == false) && (ev[:validated] == true)
            u.is_validated = true
            log "#{u.handle} got validated!"
            broadcast_event( :type => :u_change, :kind => :validated )
        end
        if (u.is_hat == true) && (ev[:hat] == false)
            u.is_hat = false
            log "#{u.handle} lost a hat!"
            broadcast_event( :type => :u_change, :kind => :lose_hat )
        end
        if (u.is_hat == false) && (ev[:hat] == true)
            u.is_hat = true
            log "#{u.handle} gained a hat!"
            broadcast_event( :type => :u_change, :kind => :gain_hat )
        end
        if u.channel != ev[:channel]
            old, u.channel = u.channel, ev[:channel]
            log "#{u.handle} changed to channel #{u.channel} (from #{ev[:channel]})"
            broadcast_event( :type => :u_change, :kind => :channel, :old => old )
        end
    end

    def did_login(u)
        log "#{u.handle} logged in!"
        broadcast_event( :type => :login, :user => u )
    end


    def did_logout(u)
        log "#{u.handle} logged out!"

        broadcast_event( :type => :logout, :user => u )
    end

    def did_email(ev)
        ev[:user] = @users[ev[:line].to_s]
        broadcast_event(ev)
    end

    def broadcast_event(ev)
        @handlers.each{ |h|
            action, post_filter, service = h[0], h[1], h[2]
            oneshot = false
            md = nil

            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 (md = value.match(ev[key])) == nil
                            skip = true
                            break
                        end
                    else
                        if ev[key] != value
                            skip = true
                            break
                        end
                    end
                }
                next if skip == true
            end
            if md
                action.call(ev, md)
            else
                action.call(ev)
            end
            @handlers.delete(h) if oneshot
        }
    end

    def handle_raw_admin(line)
        @dd.handle_admin(line)
    end
    def handle_raw_line(line)
        @dd.handle_raw_line(line)
    end
    def on_raw_event(post_filter=nil, &block)
        @dd.on_event(post_filter, &block)
    end
    def start
        @dd.start
    end
end