#!/usr/local/bin/ruby
=begin
 ******************************************************************************
 * Copyright 1990-2010 MBARI
 * MBARI Proprietary Information. All rights reserved.
 ******************************************************************************
 * Summary  : Executive class - top level Rover command and control
 * Filename : executive.rb
 * Author   : Henthorn
 * Project  : Benthic Rover
 * Version  : 1
 * Created  : March 2010
 * Modified : June  2012 - removed call to modem.init() in the init() method
 ******************************************************************************
=end

class Executive

  attr_accessor :supervisor_task, :supervisor_exited, :mon_thread, :super_conn
  attr_accessor :modem_wake, :normal_wake, :dbif, :expected_restart, :uptime
  attr_accessor :last_surface_msg_time, :requested_hibernation_seconds

  #####
  # Initializer
  #
  def initialize()

    @normal_wake = @modem_wake = false
    @dbif = RoverStore.new

    ##
    # Remote control channels
    #
    @supervisor_if = RemoteClient.new(6600)  # Here I am the server
    @topside_if = RemoteServer.new(6601)     # Here I am the client

    ##
    # Set to true when connected to supervisor
    #
    @super_conn = false

    ##
    # Alternate topside client
    #
    if (Options.instance.sim)
      @modem_if = SimModem.new()
      @lfmodem_if = SimModem.new('/dev/lfmodem')
    else
      @modem_if = BenthosModem.rover()
      @lfmodem_if = BenthosModem.new('/dev/lfmodem')
    end

    @supervisor_task = nil
    @supervisor_exited = nil
    @mon_thread = nil

    @log = nil
    @dbif = nil
  end

  #####
  # Initialize the communication channels
  #
  def init()
    ##
    # Get the command-line options
    #
    Options.instance

    ##
    # Battery monitor
    #
    if Options.instance.sim
      @bat_mon = SimBatteryMon.new
    else
      @bat_mon = BatteryMon.new
    end

    @dbif = RoverStore.new
    @logfilename = "#{ENV['ROVER_HOME']}/logs/executive.csv"
    @log = DataLog.new(@logfilename)
    @log.write("Executive,_ initializing")

    begin
      @supervisor_if.init(@logfilename)
      @topside_if.init(@logfilename)

      ##
      # Calling modem.init() methods each reboot will  quickly wear
      # down the modem batteries.
      #
      # @modem_if.init()
      # @lfmodem_if.init()
    rescue Exception => e
      puts(e)
      return nil
    end
  end
  #####

  #####
  # Relay message to interested parties
  #
  def forward_to_topside(msg)
    @log.write("Executive,_ forwarding #{msg}")
    #
    # Forward message to topside connections
    #
    @topside_if.send_msg(msg)
    @modem_if.send_msg(msg)
    @lfmodem_if.send_msg(msg)
  end
  #####

  #####
  # Get a sample of data from the latest optode measurement
  #
  def get_optode_snapshot(plan_data_home)

    ##
    # Validate input directory name
    #
    return "No plan data dir" unless (File.exist?(plan_data_home))
    stat = File.stat(plan_data_home)
    return "No plan data dir" unless (stat.directory?)

    ##
    # Ensure there is optode data in this plan
    #
    optode_dir_names = Dir.glob("#{plan_data_home}/*-optodes")
    unless (optode_dir_names.size > 0)
      return "No optode data directories"
    end

    ##
    # Choose the best candidate file,  ignoring reference data
    #
    opt_file_names = Dir.glob("#{optode_dir_names[0]}/*_optode.csv")
    opt_file_names.delete_if { |f| f.include?("ref") }
    return "No optode data files"  unless (opt_file_names.size > 0)

    ##
    # Use file with the most data
    #
    largest_size = -1
    largest_file = nil
    opt_file_names.each do |f|
      stat = File.stat(f)
      if (stat.size > largest_size)
        largest_size = stat.size
        largest_file = f
      end
    end

    ##
    # Create an array containing the %sat for every 5th measurement
    #
    f = File.open(largest_file)
    f.gets # Throw away the header line

    ##
    # Get the approximate number of lines in the file
    # to determine how to decimate the data
    #
    nlines = (largest_size / 40).to_i
    reset = 10

    ##
    # Determine how to decimate the data
    #
    if (nlines < 20)
      reset = 0             # 0 - 19 elements
    elsif (nlines < 45)
      reset = 1             # 10 - 22 elements
    elsif (nlines < 100)
      reset = 3             # 11 - 25 elements
    elsif (nlines < 200)
      reset = 5             # 16 - 33 elements
    elsif (nlines < 500)
      reset = 10            # 20 - 49 elements
    else
      reset = (nlines / 45).to_i # 45 elements
    end

    ##
    # Decimate the data, extract the %saturation
    # from each data line
    #
    basename = largest_file.split("/").last
    data = "#{basename}:%sat:"
    counter = 0

    while (l = f.gets)             # Read every line
      if (0 == counter)
        parsed = l.split(",")      # Get data from this line
        data += parsed[2]
        data += ","
        counter = reset            # Reset the counter
      else
        counter -= 1               # Skip this line
      end
    end
    ##
    # Return the data packet
    #
    return data.slice!(0..-2)
  end
  #####

  def reset_last_surface_msg_time()
    @last_surface_msg_time = Time.now
    @log.write("Executive,_ reset last surface msg time to #{@last_surface_msg_time}")
  end
  

  #####
  # Begin the main loop
  #
  def run()

    @uptime = Time.now
    
    ##
    # Monitor the battery voltage
    #
    # @bat_mon.run(DataLog.log_home)
    
    ##
    # First, determine how the vehicle was powered-up
    #
    wake_event = `/cf/rover/barbo/device/wakey.rb read_wake`.strip
    @log.write("Executive,_ wake event, #{wake_event}")

    ##
    # Get the expected restart time from the database
    # and cmopare to the current time.
    #
    hd = @dbif.load_hibernate_data()
    diff = "none"
    if (hd)
      if (@expected_restart = hd['expected_restart'])
        @requested_hibernation_seconds = 0 unless (@requested_hibernation_seconds = hd['requested_hibernation_seconds'])
        diff = @expected_restart - @uptime.to_i
        @log.write("Executive,_ requested hibernation = #{@requested_hibernation_seconds}")
        @log.write("Executive,_ now - expected restart = #{diff}")
      else
        @log.write("Executive,_ no expected restart in rover.db")
      end
      @log.write("Executive,_ hibernate record in rover.db")
    end
        
    if (wake_event.include?("modem"))
      # On a modem wakeup, I need to converse with the topside
      # Otherwise, continue as before
      #
      @normal_wake = false
      @modem_wake = true
      @log.write("Executive,_ Detected modem wakeup")
      
      # Let the world know I'm up and at 'em
      #
      forward_to_topside("Ok, I'm up #{diff} seconds early")
    else
      ##
      # Spawn the supervisor task.
      # Pass the command-line arguments through to the supervisor.
      #
      @normal_wake = true
      @modem_wake = false
      @log.write("Executive,_ Normal wakeup, run supervisor")
      run_supervisor(ARGV)
    end


    ##
    # Run message loop
    #
    # Keep track of the last message from the surface
    reset_last_surface_msg_time()
    while (true)

      ##
      # Connect to Supervisor and establish me as default client
      # then get a Supervisor message.
      #
      msg = nil
      if (@supervisor_if.connected)
        msg = @supervisor_if.get_msg(0.5)

        if (@supervisor_task != nil)
          unless (@super_conn)
            @log.write("establishing default supervisor connection")
            @supervisor_if.send_msg("default")
            @super_conn = @supervisor_if.confirm("connect ack", 2)
            @log.write("Executive,_ attempted to connect to supervisor, @super_conn = #{@super_conn}")
            @log.write("Executive,_ connected to supervisor") if (@super_conn)
          end
        end
      else
        @supervisor_if.open() if (@supervisor_task != nil)
      end

      ##
      # Operate on messages from the supervisor
      #
      if (msg)
        
        @log.write("Executive,_ Supervisor said \"#{msg[0]}\"")
        tokens = msg[0].strip.split

        if (tokens && tokens.length > 0)
          @last_msg_time = Time.now

          case tokens[0].strip.downcase
          when "completed"
            @log.write("Executive,_ closing supervisor connection")
            forward_to_topside(msg[0])
            @supervisor_if.close
            @super_conn = false
            @log.write("Executive,_ mission completedf, @super_conn = #{@super_conn}")

          when "data"
            #
            # Look for optode data in the plan data directory
            #
            if (tokens.length > 1)
              @log.write("Get optode data from #{tokens[1]}")
              forward_to_topside(get_optode_snapshot(tokens[1]))
            end

          when "status"
            #
            # Append the battery voltage to the status message
            #
            if (tokens.length > 0)
              # volt = @bat_mon.volt.to_s
              volt = `/cf/rover/barbo/device/wakey.rb read_batt`
              volt.strip!
              wake = `/cf/rover/barbo/device/wakey.rb read_wake`
              wake.strip!
              status_string = tokens[1] + "|#{wake}|#{volt}V"
              forward_to_topside(status_string)
            end
          else
            forward_to_topside(msg[0])
          end
        end
      end

      ##
      # Open topside server and wait for messages
      #
      top_msg = nil
      if (@topside_if.server)
        top_msg = @topside_if.get_msg(0.5)
      else
        @log.write("Executive,_ reinitializing topside server")
        @topside_if.init(@logfilename)
      end

      ##
      # If no topside socket message, look in modem
      #
      unless (top_msg)
        if (msg = @modem_if.get_msg(0.5))
          top_msg = [msg]
        elsif (msg = @lfmodem_if.get_msg(0.5))
          top_msg = [msg]
        end
      end


      ##
      # We are handling 6 messages from the topside:
      #   (1) quit!       - The executive should exit
      #   (2) abort       - Abort the deployment running in the supervisor
      #   (3) status      - Request the status of deployment
      #   (4) deploy      - Run a deployment
      #   (5) supervisor? - Return "no" unless the supervisor is running
      #   (6) data        - Get a snapshot of optode data
      #   (7) continue    - Continue with normal operation after a modem wakeup
      #
      if (top_msg)
        tokens = top_msg[0].strip.split

        ##
        # After ensuring there is a message, take the appropriate action
        #
        if (tokens && tokens.length > 0)
          @last_msg_time = Time.now

          case tokens[0].strip.downcase

          ##
          # Request for the executive to quit
          #
          when "quit!"
            @log.write("Executive,_ exiting upon request")
            reply("Executive exiting...", top_msg)
            sleep(1)

            @supervisor_if.close
            @topside_if.close
            exit

          ##
          # Abort the running deployment
          #
          when "abort"
            # Forward the abort request on to the supervisor
            #
            if (@super_conn)
              @log.write("Executive,_ relaying abort to supervisor")
              @supervisor_if.send_msg("abort")
            else
              @log.write("Executive,_ not connected to supervisor")
              reply("No supervisor", top_msg)
            end

          ##
          # Report the status of the running deployment
          #
          when "status"
            # Forward the status request on to the supervisor
            #
            if (@super_conn)
              @log.write("Executive,_ getting status of deployment")
              @supervisor_if.send_msg("status")
            else
              @log.write("Executive,_ not connected to Supervisor")
              reply("No supervisor", top_msg)
            end
            
          when "continue"
            # Continue operations after a modem wakeup
            #
            if (@modem_wake)
              @log.write("Executive,_ received continue")
              rehibernate_or_run_after_modem_wake()
            end

          ##
          # Request for data snapshot
          #
          when "data"
            # Forward the data request on to the supervisor
            #
            if (@super_conn)
              @log.write("Executive,_ getting data directory of plan")
              @supervisor_if.send_msg("data")
            else
              @log.write("Executive,_ not connected to Supervisor")
              reply("No supervisor", top_msg)
            end

          ##
          # Someone wants to know if the supervisor is running a deployment
          #
          when "supervisor?"
            if (Misc.running?("supervisor.rb"))
              reply("Yes", top_msg)
            else
              reply("No", top_msg)
            end
            
          when "reset"

          ##
          # Start a deployment
          #
          when "deploy"
            @log.write("Executive,_ starting supervisor with #{tokens[1]}")
            if (Misc.find_plan(tokens[1]))

              ##
              # Reset the resume option flag since we're kicking-off
              # a new deployment.
              #
              Options.instance.resume = false

              ##
              # Run the supervisor with the message tokens containing
              # the deployment filename.
              #
              if (run_supervisor([tokens[1]]))
                reply("Deploying #{tokens[1]}", top_msg)
                @super_conn = false
                @log.write("Executive,_ mission started, @super_conn = #{@super_conn}")

              ##
              # Supevisor is already running a deployment
              #
              else
                reply("Supervisor busy", top_msg)
              end

            ##
            # Could not find the plan after looking in the normal spots.
            #
            else
              reply("No file:#{tokens[1]}", top_msg)
            end
          else
            reply("#{top_msg[0]}??", top_msg)
            next
          end

          reset_last_surface_msg_time()
          
        end
        
      else    # No topside msg
        # See how long it's been since we've had a topside message
        #
        if (@modem_wake && !Misc.running?("supervisor.rb"))
          now = Time.now
          if (now.to_i - @last_surface_msg_time.to_i > 170)  # Five minutes between messages
            @log.write("Executive,_ modem conversation time has expired")
            rehibernate_or_run_after_modem_wake()
            reset_last_surface_msg_time()
          end
        end
      end

    end

  end

  ##
  # Decide either to rehibernate after a modem wakeup or run the supervisor
  #
  def rehibernate_or_run_after_modem_wake()
    hib_time = @expected_restart - Time.now.to_i
    if (hib_time < 200)
      @log.write("Executive,_ run the supervisor")
      forward_to_topside("Executive resuming mission, starting Supervisor")
      run_supervisor(ARGV)
    else
      forward_to_topside("Executive resuming hibernation, #{hib_time} seconds ")
      @log.write("Executive,_ hibernate for #{hib_time} seconds")
      run_supervisor(ARGV)
    end
  end
  
  
  def reply(string, msg_recv)
    if (msg_recv.length > 1)
      @topside_if.send_msg(string, msg_recv[1])
    elsif (msg_recv.length == 1)
      @modem_if.send_msg(string)
      @lfmodem_if.send_msg(string)
    end
  end


  #####
  # Spawn the supervisor task with the arguments provided to
  # the Executive.
  #
  def run_supervisor(argv)

    ##
    # If a supervisor process is already running, do not start another
    #
    if (Misc.running?("./supervisor.rb"))
      return nil
    end

    ##
    # Build command used for spawning supervisor process.
    # Use same arguments and options used for executive.
    #
    cmd = "./supervisor.rb"
    argv.each {|arg| cmd += " #{arg}"}
    cmd += " -s" if Options.instance.sim
    cmd += " -r" if Options.instance.resume
    cmd += " -w" if Options.instance.warp
    puts("_ Launching #{cmd}")

    ##
    # Fork a child process and execute the supervisor command.
    # Forked process exits.
    #
    unless (@supervisor_task = fork)
      @supervisor_exited = nil
      exec(cmd)
      exit

    ##
    # Parent process creates new thread to wait for supervisor to
    # finish. 
    else
      @mon_thread = Thread.new do
        Process.wait

        ##
        # Examine last hibernation status of supervisor.
        # 
        hs = @dbif.load_hibernate_data
        if (hs)
          status = hs["status"]

          ##
          # Set flag if supervisor exited abnormally (i.e., it crashed)
          #
          if (status == "executing")
            puts("! Supervisor exited abnormally")
            @supervisor_exited = true
          else
            puts("_ Deployment #{status}")
            @supervisor_exited = false
            @supervisor_task = nil # unless (@supervisor_task.alive?)
          end
        end
      end   # @mon_thread

    end     # fork

    return true
  end

end

##
# Run Executive
#

##
# Don't assume ROVER_HOME is defined.
# When running on the target after hibernation the system
# starts the executive automatically and ensures that it
# restarts when it crashes. The issue is that ROVER_HOME
# isn't defined at that point.
#
# To stop the automatic nature of executive, comment-out
# the "brbo" line in /etc/inittab then reboot the system.
#
rover_home = ENV["ROVER_HOME"]
unless (rover_home)

  ##
  # This is the only place in the system where
  # the home directory is specified.
  #
  ROVER_HOME = "/cf/rover/barbo"
  ENV["ROVER_HOME"] = ROVER_HOME
  Dir.chdir(ROVER_HOME)
  Dir.chdir("control")

  ##
  # Add the Rover bin directory to the path
  #
  path = ENV["PATH"]
  path += ":/cf/rover/lib"
  ENV["PATH"] = path
else
  puts("_ Executive running under pre-existing environment...")
end

##
# Refer to Rover-code home directory as base
#
require "#{ENV['ROVER_HOME']}/utils/rover_environment"
#

require 'device/modem'
require 'device/battery'
require 'utils/options'
require 'utils/misc'
require 'utils/datalog'
require 'utils/rover_thread'
require 'utils/remote_server'
require 'utils/remote_client'

pids = Misc.running?("/executive.rb")
if (pids && pids.length > 1)
  puts("! Sorry, executive process already running")
else

  puts("============================")
  puts(Time.now.to_s)
  puts("============================")
  exec = Executive.new()
  exec.init()

  Kernel.trap("CHLD") do
    sleep(1.5)
    exec.super_conn = false
    if (exec.supervisor_exited)
      exec.supervisor_exited = nil
      puts("_ Restarting Supervisor")
      if (exec.mon_thread && exec.mon_thread.alive?)
        join(exec.mon_thread)
        exec.mon_thread = nil
      end
      exec.run_supervisor(["--resume"])
    end

  end

  exec.run
end
