#######################  msg.rb -- brent@mbari.org  #######################
# $Source: /home/cvs/ESP/gen2/software/esp/lib/msg.rb,v $
#    Copyright (C) 2003 MBARI
#    MBARI Proprietary Information. All rights reserved.
# $Id: msg.rb,v 1.33 2005/11/01 18:23:08 brent Exp $
#
#   Abstract Control Messages
#  All specific Msg classes inherit from this one.
#  Msg::Target associates a dispatch thread with a queue
#
############################################################################

require 'thread'
require 'threads'
require 'delegate'
require 'log'

class String
  def asHex
  #utility for message logs
    s=""
    each_byte {|byte| s+="%02X " % byte}
    s.chop
  end
end

module Msg  

  class Error < StandardError; end
  class NodeOffline < Error; end

  def self.recordException exception, thread=Thread.current
    thread.recordException exception
  end

  def self.recordMsg target, rawMsg
    target.recordMsg rawMsg
  end

  def self.recordComment text, thread=Thread.current
  # record a comment log entry
    puts "#{thread.name}:#{text}"
  end
  def self.record text, thread=Thread.current
    recordComment text, thread
  end

  def self.denote ignoreHash
  end
  def self.terminate
  end


  def self.lengthErr msgType, body
  #handy method to raise exception on message length errors
    raise Error, 
      "Truncated #{msgType} (length=#{body.length})\n"+body.asHex
  end

  def self.parseFields string, cursor, *field
  #parse fixed length fields in string from cursor position 
  #The field parameter consists of zero or
  #more triples defining each field to assign. These
  #consist of an assignment Proc/Method (typically dstObj.method :fieldName=),
  #the field's width in bytes and the string method used to
  #convert the field
  #returns the number of fields successfully assigned
  #   or # of excess bytes in string as a negative value
    items=0
    len=string.length      
    while len > cursor
      return cursor - len if field.size < 3
      assignment=field.shift
      width=field.shift
      conversion=field.shift
      assignment[string[cursor,width].method(conversion).call]
      cursor+=width
      items+=1
    end
    items
  end
  

  class Target < SimpleDelegator
    #the Target class manages a server thread for each message target
    #it delegates to a class (like Queue) that delivers each message
    
    include Msg
    @@abort_on_exception = false
    def self.abort_on_exception 
      @@abort_on_exception
    end
    def self.abort_on_exception= abort
      @@abort_on_exception=abort
    end
        
    def msgText rawMsg
      txt = "#{name}: "
      begin
        txt << parser.parse(rawMsg)
      rescue
        txt << "?? " << rawMsg[1..-1].asHex
      end
      txt
    end

    def initialize (name, parser, hash={})
    #create a new Msg::Target instance
    #name = the name of the new server Thread
    #parser implements #parse to convert raw message into a message object
    #options specified in hash:
    # :abort_on_exception {false|true} 
    # :source is Queue-like object that implements #pop, #<< and #push
    #         (the source of raw messages to this Msg::Target)
    #         (defaults to Queue.new)
    # :log is a Log* object for recording messages received (see log.rb)
      @source = Queue.new unless @source = hash[:source]
      unless @log = hash[:log]  #if no logger specified...
        sch = Thread.schedule   #try 1st to use thread's logger then default
        @log = sch ? sch.log : Log.default
      end
      @abort_on_exception = @@abort_on_exception unless
         @abort_on_exception = hash[:abort_on_exception]
      @parser = parser
      super @source unless @source.kind_of? type
      @dispatcher = Thread (name) do
        @dispatcher = Thread.current
        begin
          loop {
            @log.recordMsg self, (rawMsg = pop)
            (@parser.parse rawMsg).process self
          }
        rescue StandardError, ScriptError => err
          raise if @abort_on_exception || @@abort_on_exception
          case err
          when Thread::ParentDied, Thread::Aborted
          else
            @log.recordException err, @dispatcher
            retry
          end
        end
      end
    end
    attr_reader :dispatcher, :source
    attr_accessor :parser, :log, :abort_on_exception

    def name
      @dispatcher.name
    end
    
    def terminate
      @dispatcher.abort ("Terminated by "+Thread.current.name.inspect)
    end
  end
  
  
  class Parser 
  # handle the parsing of an arbitrary raw message

    class Error < StandardError; end

    def initialize (mapping)
    # create a parser object for a given hash of message to Methods or Procs
      unless mapping.kind_of? Hash
        ary = mapping  #if not a hash, assume it's an array of message classes
        mapping = {}
        ary.each { |msgClass|
          key = msgClass.const_get(:ID)
          key = key[0] if key.kind_of? String
          mapping[key] = msgClass.method :parse
        }
      end
      @mapping=mapping
    end
    
    attr_reader :mapping
    
    def handler key
      @mapping[key]
    end
    
    def default= action
      @mapping.default = action
    end
    
    def merge (other=self)
    # merge this parser with another
    # report an error if there is a parsing conflict
      unless other == self  #merging with self is a NOP
        sz = @mapping.size
        @mapping.update (hsh=other.mapping)
        conflicts = (sz+hsh.size) - @mapping.size
        if conflicts != 0
          ambiguous = []
          previousMethod = Object.new
          (@mapping.to_a + hsh.to_a).sort.each { |assoc|
            thisMethod = assoc[1]
            ambiguous.concat(assoc) if prevousMethod == thisMethod
            previousMethod = thisMethod
          }
          raise Error, "Conflicts: #{ambiguous}"
        end
      end
      self
    end
    
  end  #Msg::Parser class
      
end
