#--
#******************************************************************************
#* Copyright 1990-2007 MBARI
#* MBARI Proprietary Information. All rights reserved.
#******************************************************************************
#* Summary  : Common Utilities used through Rover code
#* Filename : rover_utils.rb
#* Author   : Chase/Henthorn
#* Project  : Benthic Rover
#* Version  : V1.1
#* Created  : Nov-06   Basic functionality
#* Modified : May-07   Upgraded logging functions
#*            Oct-07   Implemented a simple time-warping utility
#******************************************************************************

require "singleton"

########################################################################
# LogDir - Attempts to manage log directories for missions and tests
# while allowing classes to continue to use LogHelper as a mixin
# ######################################################################
class LogDir
  include Singleton

  # These are the directories that LogHelper uses to locate data and
  # log files
  attr_accessor :dir_name, :datalog_dir, :syslog_dir, :eventlog_dir, :mst, :ctotal
  attr_reader   :syslogname, :eventlogname

  def initialize
    # Define the CSV comma table for the classes in the system.
    # If you don't care, the leave the class name out of the hash.
    # Default value is 1 comma.
    #
    @class_comma_table = { \
     'Rover'=>1, 'BattMan'=>1, 'SimBattMan'=>1, 'ConfigMan'=>1, \
     'Main'=>2, 'RoverMission'=>2, \
     'ControlSystem'=>3, 'Navigator'=>3, \
     'Ezservo'=>4, 'SimEzservo'=>4, \
     'Measure'=>5, \
     'Rack'=>6, 'SimRack'=>6, \
     'Aanderaa'=>7, 'SimAanderaa'=>7, 'Acm'=>7, 'SimAcm'=>7, \
     'RelayMan'=>8, 'SimRelayMan'=>8, 'RelayBoard'=>8, 'SimRelayBoard'=>8 }



    @class_comma_table.default=1
    
    # Determine the maximum number of commas
    # 
    @ctotal = 0
    @class_comma_table.each {|k,v| @ctotal = v if v > @ctotal}
    @ctotal += 1
     
    # Point the "latest" sym link to the testlogs directory as
    # default data and log location.
    # These strings should eventually come from a config object
    #
    @latest = "/cf/rover/logs/latest"
    @testlogs = "/cf/rover/logs/testlogs"

    self.setlogdir(@testlogs)

    @rt = RoverTime.instance
    @mst = Time.now.gmtime

  end

  def setlogdir(pathname)
    puts("Log directory is #{pathname}")

    Dir.mkdir(pathname) unless File.exist?(pathname)


    symname = @latest
    File.unlink(symname) if File.exist?(symname)
    File.symlink(pathname, symname)

    @datalog_dir = pathname + "/"
    @syslog_dir = pathname + "/"
    @eventlog_dir = pathname + "/"
    @syslogname = @syslog_dir + "syslog.csv"
    @eventlogname = @eventlog_dir + "eventlog.csv"

    @dir_name = pathname
  end

  # Create a uniquely named directory for mission log files. This method
  # is supposed to be invoked at the start of a mission.
  #
  # home - string: the top level logs directory name (i.e., "/cf/rover/logs")
  # prefix - string: optional prefix for the directory name (i.e., "tanktest")
  #
  # If successful, a uniquely-named directory will be created in home
  # directory. A ".n" will be appended to the prefix to make a unique name,
  # where 'n' are successive integers beginning with zero.
  #
  def newlogdir(home, prefix=nil)
 
    # Create a new log dir in the home directory. Perhaps the home
    # directory should come from a configuration object.
    #
    # First ensure that the home directory exists
    #
    return false if (File.exists?(home) == false ||
                     File.stat(home).directory? == false)

    # Define a log directory prefix if necessary using today's date
    #
    unless prefix
      today = Time.now #Date.today
      mzero = ""
      mzero = "0" if today.month < 10  # leading zero?
      dzero = ""
      dzero = "0" if today.day < 10    # leading zero?

      prefix = today.year.to_s + "." + mzero + today.month.to_s + "." +
               dzero + today.day.to_s
    end

    # Create a unique directory name using the home directory and the prefix
    #
    ii = 0
    puts("rover_utils: create unique log directory name")
    while true
      dir_name = home + "/" + prefix + "." + ii.to_s
      if false == File.exist?(dir_name)
        break
      end
      ii += 1
    end

    setlogdir(dir_name)
    true
  end

  # Method returns the number of commas to insert into a log
  # message before the class name. The number is stored in the
  # @class_comma_table hash. Returns 1 if the class name is not
  # a key in the hash.
  #  
  def comma_offset(class_name)
    return 1 if class_name == nil
    return @class_comma_table[class_name]
  end
  
end

module LogHelper

  def log_timestamp(t)
    return t.gmtime.strftime("%Y-%m-%dT%H:%M:%S.") + t.usec.to_s[0..1]
  end

  # Removed postfix, added filename as optional parameter.
  # If no filename is given, we use the default filename using the class name.
  # Changed the format of the timestamp, and commas delimit fields in a record.
  # Timestamp includes more precision.
  # 
  def datalog(msg, filename=nil)
    t = RoverTime.instance.rTime
    t.gmtime
    filename = t.strftime("#{self.class.name}%Y%m%d.dat") if !filename
    logfilename = LogDir.instance.datalog_dir + filename
    hh = t.usec.to_s
    elapsed = t.to_f - LogDir.instance.mst.to_f
    estring = sprintf("%.2f", elapsed)
    File.open(logfilename, "a") do |file|
      file.puts log_timestamp(t) << ", " << estring <<
                ", " << msg
    end 
  end
  
  # Write string to data file without a timestamp. Intended for header info.
  # 
  def datawrite(string, filename=nil)
    t = Time.now
    t = RoverTime.instance.rTime
    t.gmtime
    filename = t.strftime("#{self.class.name}%Y%m%d.dat") if !filename
    logfilename = LogDir.instance.datalog_dir + filename
    File.open(logfilename, "a") do |file|
      file.puts string
    end 
  end

  # Syslog file is based on mission not the date
  # 
  def syslog(message)
    t = Time.now
    t = RoverTime.instance.rTime
    elapsed = t.to_f - LogDir.instance.mst.to_f
    estring = sprintf("%.2f", elapsed)

    # Build the comma prefix and postfix strings
    #     
    cname = self.class.name
    co = LogDir.instance.comma_offset(cname)
    pre = "," * co
    post = "," * (LogDir.instance.ctotal - co)
    
    File.open(LogDir.instance.syslogname, "a") do |file|
      file.puts(log_timestamp(t) << ","  << estring << pre << cname <<
                post << '"' << message << '"')
      file.flush
    end
  end

  # Eventlog file is based on mission not the date
  # 
  def eventlog(message)
    syslog message
    t = Time.now
    t = RoverTime.instance.rTime
    elapsed = t.to_f - LogDir.instance.mst.to_f
    estring = sprintf("%.2f", elapsed)

    # Build the comma prefix and postfix strings
    #     
    cname = self.class.name
    co = LogDir.instance.comma_offset(cname)
    pre = "," * co
    post = "," * (LogDir.instance.ctotal - co)
    
    File.open(LogDir.instance.eventlogname, "a") do |file|
      file.puts log_timestamp(t) << "," << estring << pre << cname <<
                post << '"' << message << '"'
    end
  end

  def LogHelper.syslog(message)
    t = Time.now
    t = RoverTime.instance.rTime

    logfilename = LogDir.instance.syslog_dir + "syslog.csv"
    elapsed = t.to_f - LogDir.instance.mst.to_f
    estring = sprintf("%.2f", elapsed)
    File.open(logfilename, "a") do |file|
      file.puts log_timestamp(t) << "," << estring <<
                ",#{self.class.name}," << message
    end
  end

end

# Time abstraction that allows for a simple time-warping capability when
# running test missions
#
class RoverTime < Time
  include Singleton
  include LogHelper

  attr_accessor :warp, :warpThresh
  
  def initialize
    @warp = false
    @warpAmount = 0
    @warpThresh = 0.5  # Warp only if sleep time is greater than threshold
  end
  
  def config(cObj)
    return unless cObj
    @warp = cObj.warpTime? if cObj.respond_to?(:warpTime?)
    @warpThresh = configObj.warpThresh if @warp && cObj.respond_to?(:warpThresh)
  end

  # Rather than use Time class directly, rover code should
  # use this method that includes the amount of fast-forward time
  #  
  def rTime
    return (Time.new + @warpAmount)
  end
  
  # Rather than use the Thread.sleep method directly, Rover code can
  # used this method that will pretend to sleep the given amount of time
  # if the warp setting is true, add the given time to a time accumulator,
  # and return "immediately" after a set minimum sleep time.
  #
  # WARNING! This method is not thread-safe and should only be used when
  # the calling thread essentially has total control of the vehicle (like
  # during launch and measurement phases), and is basically just waiting
  # around for something to happen (example: Dust settle period).
  # 
  def rSleep(seconds)
    unless(seconds) && (seconds.is_a?(Integer) || seconds.is_a?(Float))
      puts "RoverTime: rSleep requires an Integer or Float parameter"
      return 0
    end

    # Sleep longer than the threshold amount? Add surplus time to
    # warp time accumulator and return after sleeping for the
    # threshold amount.
    #     
    if (warp && seconds > @warpThresh)
      sleep(@warpThresh)

      wa = seconds - @warpThresh
      @warpAmount += wa
      syslog("_ warping #{wa} seconds, mission total #{@warpAmount}")
    else
      sleep(seconds)
    end
  end
end
