#!/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 : For SES Oct 2011 - no modems or battery monitor
 ******************************************************************************
=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

  @@mission_link = "/home/ses/barbo/mission-file"

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

    @cmd = nil

    ##
    # 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)
      @lfmodem_if = SimModem.new('/dev/lfmodem')
    else
      @lfmodem_if = BenthosModem.new('/dev/lfmodem')
    end

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

    @log = nil
    @dbif = nil

    @sites = @orig_sites = nil
    @collection = @orig_collection = nil
    @flow_thru  = @orig_flow_thru  = nil

  end

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

    @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)
      #@lfmodem_if.init() # Calling modem.init() methods each reboot soon kill the battery
    rescue Exception => e
      puts(e)
      return nil
    end
  end
  #####

  #####
  # Relay message to interested parties
  #
  def forward_to_topside(msg)
    #
    # Forward message to topside connections
    #
    @topside_if.send_msg(msg)
    @lfmodem_if.send_msg(msg)
  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()

    ##
    # First, determine how the vehicle was powered-up
    #
    wake_event = `/cf/barbo/device/wakey.rb read_wake`.strip
    @log.write("Executive,_ wake event, #{wake_event}")

    cron = `run-parts -v /etc/cron.hourly`
    @log.write("Executive,_ cron job, #{cron}")

    ##
    # Next, set the maximum time for the wakey to let me stay awake and the wedge-sleep time
    # We'll add a 10 min margin or error
    #
    if (Options.instance.resume)
      tot = 15
    else
      tot = 0
    end

    wtimeout = `/cf/barbo/device/wakey.rb set_timeout #{tot}`.strip
    @log.write("Executive,_ wakey timeout, #{wtimeout}")
    wsleep = `/cf/barbo/device/wakey.rb set_hibernate_time 2`.strip
    @log.write("Executive,_ wakey sleep, #{wsleep}")
                                            
    ##
    # Get the mission parameters
    #
    mission_params = Hash.new unless (mission_params = @dbif.load_mission_params())
    @orig_sites = mission_params['sites']
    @orig_collection  = mission_params['collection']
    @orig_flow_thru  = mission_params['flow_thru']

    ##
    # 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
      #
      @lfmodem_if.write("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)
          case tokens[0].strip.downcase
          when "completed"
            @log.write("Executive,_ closing supervisor connection")
            forward_to_topside(msg[0])
            @supervisor_if.close
            @super_conn = false

          when "relaunch"
            @log.write("Executive,_ closing supervisor connection")
            forward_to_topside(msg[0])
            @supervisor_if.close
            @super_conn = false
            relaunch()
=begin
          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
=end

          when "status"
            #
            # Append the battery voltage to the status message
            #
            if (tokens.length > 0)
              volt = `/cf/barbo/device/wakey.rb read_batt`
              volt.strip!
              wake = `/cf/barbo/device/wakey.rb read_wake`
              wake.strip!
              status_string = tokens[1] + "|#{wake}|#{volt}V"
              forward_to_topside(status_string)
              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 = @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
      #   (8) sites       - Set the number of sites in the deployment
      #   (9) dust        - Set the dust settle time after a move
      #  (10) write       - Write the updated sites and/or dust settings
      #  (11) reset       - Reset the updated sites and/or dust settings
      #  (12) time        - Return the time on the vehicle
      #  (13) errors      - Return a summary of errors
      #
      if (top_msg)
        tokens = top_msg[0].strip.split

        ##
        # After ensuring there is a message, take the appropriate action
        #
        if (tokens && tokens.length > 0)
          reset_last_surface_msg_time()

          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")
              @log.write("Executive,_ retrieving stored status")
              stored_status = ""
              unless (stored_status = hd["status"])
                stored_status = "No supervisor"
              end
              volt = `/cf/barbo/device/wakey.rb read_batt`
              volt.strip!
              wake = `/cf/barbo/device/wakey.rb read_wake`
              wake.strip!
              reply("#{stored_status}|#{wake} (#{diff} seconds)|#{volt}", top_msg)
            end

          when "continue"
            # Continue operations after a modem wakeup
            #
            if (@modem_wake)
              @log.write("Executive,_ received continue")
              rehibernate_after_modem_wake()
            end

          ##
          # Return an error summary
          #
          when "errors"

          ##
          # Client wants to change the number of sites in the current plan
          #
          when "sites"
            if (tokens.length > 1)
              @sites = tokens[1].to_i
            else
              @sites = 0
            end

            @log.write("Executive,_ sites request: #{@sites}")
            reply("Received <sites #{@sites}>", top_msg)

          ##
          # Client wants to change the duration of the collection time
          #
          when "collect"
            if (tokens.length > 1)
              @collection = tokens[1].to_i
            else
              @collection = 0
            end

            @log.write("Executive,_ collect request: #{@collection}")
            reply("Received <collect #{@collection}>", top_msg)

          ##
          # Client wants to change the duration of the flow through time
          #
          when "flow"
            if (tokens.length > 1)
              @flow_thru = tokens[1].to_i
            else
              @flow_thru = 0
            end

            @log.write("Executive,_ flow request: #{@flow_thru}")
            reply("Received <flow #{@flow_thru}>", top_msg)

          ##
          # Client wants to save the sites and dust parameters
          # for use during the remainder of the plan
          #
          when "write"
            mission_params['sites'] = @sites if (@sites)
            mission_params['collection'] =  @collection  if (@collection)
            mission_params['flow_thru'] =  @flow_thru  if (@flow_thru)
            @dbif.save_mission_params(mission_params)

            @log.write("Executive,_ write request: sites=#{@sites} collect=#{@collection} flow=#{@flow_thru}")
            reply("Received <write sites=#{@sites} collect=#{@collection} flow=#{@flow_thru}>", top_msg)

          ##
          # Client wants to know what time it is on the target
          #
          when "time"

            t = Time.now
            @log.write("Executive,_ time request: #{t}")
            reply("Received <time #{t}>", top_msg)


          ##
          # Client wants to reset the sites and dust params to what
          # they were before this session.
          #
          when "reset"
            @sites = @dust = nil
            mission_params['sites'] = @orig_sites
            mission_params['collection'] =  @orig_collection
            mission_params['flow_thru'] =  @orig_flow_thru
            @dbif.save_mission_params(mission_params)

            @log.write("Executive,_ reset request: sites=#{@sites} collect=#{@collection} flow=#{@flow_thru}")
            reply("Received <reset sites=#{@sites} collect=#{@collection} flow=#{@flow_thru}>", top_msg)

          ##
          # 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

              ##
              # 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)
          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)  # 3 minutes between messages
            @log.write("Executive,_ modem conversation time has expired")
            rehibernate_after_modem_wake()
            reset_last_surface_msg_time()
          end
        end
      end

    end
  end

  ##
  # Always hibernate even if only for a minute just so that wakey reads
  # normal wake event
  #
  def rehibernate_after_modem_wake()
    hib_time = @expected_restart - Time.now.to_i
    hib_time = 60 if (hib_time < 60*1 || hib_time > 60*90)

    forward_to_topside("Executive resuming hibernation, #{hib_time} seconds ")
    @log.write("Executive,_ hibernate for #{hib_time} seconds")
    hib_time = hib_time / 60   # convert to minutes
    response = `/cf/barbo/device/wakey.rb hibernate #{hib_time}`
    unless (response.strip.to_i == hib_time)
      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}")

    ##
    # Check the integrity of the database
    #
    if (Options.instance.resume)

      unless (hd = @dbif.load_hibernate_data())

        if (File.exist?(@dbif.file))
          # Instrument database file seems to corrupted
          #
          puts("! Check the backup database")
          @dbif = RoverStore.use_backup(@dbif)

          if (@dbif && (hd = @dbif.load_hibernate_data()))
            @log.write("! Using the backup database")

          else
            # No useable backup found.
            puts("! No useable backup file exists, we need to start over")
            @log.write("! No useable backup file exists, we need to start over")

            # Options.instance.resume = false
            if (relaunch())

            else
              puts("Relaunch denied - mission link file #{@@mission_link} not found")
              @log.write("Relaunch denied - mission link file #{@@mission_link} not found")
              return
            end

          end  # Useable backup db

        end    # db file exists

      else
        puts("_ Create a backup of a good database")
        RoverStore.make_backup(@dbif)
      end      # There is hibernate data

    end

    ##
    # 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

  def relaunch()
    # We need to start the mission over
    # Remove the instrument database file.
    # Start the mission using the last-chance
    # mission file link.
    if (File.exist?(@@mission_link) &&
        File.symlink?(@@mission_link))
      #
      # The mission-file link is there, so get the target name
      # and use it to start the supervisor on a new mission
      #
      bname = File.basename(File.readlink(@@mission_link))
      @cmd = "./supervisor.rb #{bname}"

      unless (@supervisor_task = fork)
        @supervisor_exited = nil
        exec(@cmd)
        exit
      end
      return @supervisor_task
    else
      return nil
    end
  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 "rovr" 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/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/bin"
  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
      if (exec.mon_thread && exec.mon_thread.alive?)
        join(exec.mon_thread)
        exec.mon_thread = nil
      end
      sleep(18)  # wait long enough for the system to shutdown if necessary
      puts("_ Restarting Supervisor")
      #
      # Instead of resuming, perhaps restart using mission-file link
      #
      exec.run_supervisor(["--resume"])
    end

  end

  exec.run
end
