#!/usr/bin/env ruby
###################  logreader.rb -- brent@mbari.org  #######################
# $Source: /home/cvs/ESP/gen2/software/esp/lib/logreader.rb,v $
#    Copyright (C) 2003 MBARI
#    MBARI Proprietary Information. All rights reserved.
# $Id: logreader.rb,v 1.25 2005/11/01 07:46:50 brent Exp $
#
#   Read a log file produced by the Log class
# (see log.rb)
#
############################################################################

require 'log'
require 'timehash'

class Log

  module Entry
  # common operations on all log entry types
    attr_reader :time, :source
    def context
    #return log entry context prefix string
      s=''    
      if @time and @time != (lastDate = (t=Thread.current)[:logDate])
        s<<'@'<<@time.asLogString(lastDate)
        t[:logDate]=@time
      end
      if @source and @source != t[:logSrc]
        s<<' ' unless s.empty?        
        s<<@source.to_s<<':'
        t[:logSrc]=@source
      end
      s<<' ' unless s.empty?
      s
    end
    def logEntry
      context<<to_s
    end
    protected
    def setContext time, source
      @time = time
      @source = source
    end
  end

  
  class Time < ::Time
  # simulation time at which log entry was written
    rename_method :asLog, :asLogString
    protected :asLog
    def asLogString ignored=nil
      to_f.asLogString
    end
  end
  
  class Date < Time
    def asLogString lastTime=nil
      asLog lastTime
    end
  end
  

  class Comment < String
  # a comment string in the log
    include Entry
    def initialize str, time=nil, source=nil
      super str
      setContext time, source
    end
  end
  
  class Method < Comment
  # a special comment string that records a method invocation
  end
  
  class Object
    include Entry
  # an (exception) object in the log
    def initialize object, time=nil, source=nil
      @object = object
      setContext time, source
    end
    attr_reader :object
    def to_s
      "#{@object.type}: #{@object}"
    end
  end  
  
  class Msg < Object
    def to_s
      @object.to_s
    end
  end


  class Reader
    include Enumerable
      
    class Error < StandardError
      def initialize text, lineno, err=nil
        @lineno = lineno
        @err = err
        super text
      end
      attr_reader :lineno, :err
      def logEntry
        to_s+" at line #"+@lineno.to_s
      end
    end
    
    def initialize inputStream, parsers={}
    # create a new Log::Reader object for the specified inputStream      
      @parserHash=parsers  #hash of target symbols => message parsers
      @initial={}
      @pos=(@stream=inputStream).pos
      @lineno=@stream.lineno
    end
    attr_reader :time, :source, :pos, :lineno
        
    def each 
    # execute supplied block on object denoted by each log file entry
    # the block will be passed a Msg, Log::Comment, Log::Time, Time, 
    # or an Exception object
      @stream.pos=@pos          #where we last left off parsing
      @stream.lineno=@lineno
      Thread.current[:logDate]=nil
      Thread.current[:logSrc]=nil
      until @stream.eof?
        entry=''
        until @stream.eof?
          entry+=@stream.gets    #lines ending in \ are continuations
          if entry[-2]!=?\\ or (entry[-3]==?\\ and entry[-4]!=?\\)
            entry.chomp!
            break
          end
        end
        @pos = @stream.pos  #multiple Log::Readers may operate on this stream
        @lineno = @stream.lineno
        yield parse entry   #as yield may raise an exception
      end
    end


    @@unesc = /\\./m
    @@quote = /\A\"(.*?[^\\])\"/m
    @@halfEPS = Time::EPS/2.0
    
    def unescape string
      string.gsub (@@unesc) {|escaped| escaped[1..1]}
    end
    private :unescape
    
    def getText tail
    # parse through the source of the log entry
    # set @source to the text source (thread name)
    # returns the remaining (unescaped) entry text
      if quoted=@@quote.match(tail)
        @source = unescape(quoted[1]).intern
        unescape quoted.post_match
      else
        if name=@initial[tail[0]]
          @source=name
          unescape tail[1..-1]
        else
          unescape tail
        end
      end
    end
    private :getText  
      
    def parse entry
    # parse log entry String into a:
    #  nil,Msg,Log::Comment,Log::Method,Log::Time,Time, or an Exception object
      return nil unless entry && entry.length > 0
      begin
        case entry[0]
        
          when ?@  #time (real or simulated)
            raise Error, 
              "Truncated Time entry", @stream.lineno unless entry.length > 1
            tail=entry[1..-1]
            @time = if tail.include? ?:
              Log::Date.at ParseDate.fromString (tail, @time, 0)
            else
              Log::Time.at Float(tail)+@@halfEPS               
            end
            nil  #beccause this just prefixes next comment, method, msg, etc.
            
          when ?#  #comment
            Log::Comment.new getText(entry[1..-1]), @time, @source

          when ?.  #method inovation comment
            Log::Method.new getText(entry[1..-1]), @time, @source
            
          when ?!  #(exception) object dump
            Log::Object.new Marshal.load (getText entry[1..-1]), @time, @source
            
          when ?=  #message target legend
            case entry.length
              when 1
                @initial={}
              when 2
                @initial.delete (entry[1])
              else
                @initial[entry[1]] = unescape(entry[2..-1]).intern
            end
            nil  #because this entry just changes our internal state
            
          else     #message
            msg = getText entry
            unless parser = @parserHash[@source]
             return Error.new "Cannot parse #{@source} message", @stream.lineno
            end
            Log::Msg.new parser.msgText(msg), @time, @source
        end  #case        
      rescue ScriptError, StandardError => err
        return Error.new "#{err.type}: #{err.message}", @stream.lineno, err
      end
    end
    private :parse
    
  end #Log::Reader
  
end


if $0 == __FILE__  #test Log::Reader class by displaying output

  logFile = File.new (ARGV[0])

  require 'base'
  require 'i2c/sleepy'
  require 'valve'
  require 'shaft'
  require 'syringe'
  require 'gripper'
  require 'thermal'
  require 'sampler'
  require 'irb'

  dwarfParser = I2C::DwarfParser
  targets={:Dwarf=>dwarfParser, Can.name.intern=>Can}
  I2C::DwarfAdr.each_key {|dwarfName| targets[dwarfName]=dwarfParser}  
  start = Log::Reader.new logFile, targets
  reader = start.dup
  reader.each {|e| puts e.logEntry if e}
if false
  puts '#'*50
  puts 'User Commands & Comments (using Enumerable)'
  reader = start.dup
  reader.each {|e| puts e.logEntry if e.is_a? Log::Comment}
end
  puts '#'*50
  puts 'Log Reader Errors'
  reader = start.dup
  reader.each {|e| puts e.logEntry if e.is_a? Log::Reader::Error}
  puts '#'*50
  puts 'Exceptions'
  reader = start.dup
  reader.each {|e| puts e.logEntry if 
    e.is_a? Log::Object and e.object.kind_of? Exception}
end
