#!/usr/local/bin/ruby
# So this file can be used as a command
#
#
=begin
 ******************************************************************************
 * Copyright 1990-2009 MBARI
 * MBARI Proprietary Information. All rights reserved.
 ******************************************************************************
 * Summary  : Supervisor class - top level routine (main). Deployment class.
 * Filename : supervisor.rb
 * Author   : Henthorn
 * Project  : Benthic Rover
 * Version  : 1
 * Created  : Nov 2008
 * Modified :
 ******************************************************************************
=end

require "#{ENV['ROVER_HOME']}/utils/rover_environment"

require 'singleton'
require 'rexml/document'
require 'utils/misc'
require 'utils/datalog'
require 'utils/remote_server'
require 'control/deployment'
require 'device/rover'

$default_config = "Rover.cfg"

############################################
############################################
#
# Supervisor class - Top-level singleton object to manage
# Rover runtime behavior.
#
#
class Supervisor
  include Singleton
  include SyslogWriter

  attr_reader :deployment, :deployment_filename, :server
  attr_reader :config_filename, :sim, :resume, :dbif
  attr_reader :current_plan, :plan_sequence_number

  ##
  # Create a Supervisor object.
  # 
  def initialize
    @deploy_thread = nil
    @config_filename = $default_config
    @deployment = nil
    @deployment_filename = nil
    @current_plan = nil
    @plan_sequence_number = 0
    @sim = false
    @dbif = RoverStore.new
    @resume = false
  end

  ##
  # Load the Rover configuration
  #
  def load_configuration(p_filename=$default_config)
    @config_filename = p_filename
  end


  ######################################################################
  ######## Construct a Deployment object from a deployment file ########
  #
  # Make a deployment object from a filename located in the plans directory.
  # Point to the new deployment object with @@deployment Class variable.
  #
  #                     string
  def make_deployment(p_filename)

    #### Find the deployment file ####
    #
    # First, locate and open the deployment file.
    #
    puts("_ Supervisor...making deployment from #{p_filename}")
    @deployment_filename = Misc.find_plan(p_filename)

    unless (@deployment_filename && File.exist?(@deployment_filename))
      puts("! Supervisor...file not found: #{@deployment_filename}")
      return
    end

    #### Load Rover configuration the file ####
    #
    # Next, load the Rover configuration file first. Devices, Behaviors,
    # and Plans make use of the settings to modify processing.
    #
    load_configuration()


    #### Load and parse the deployment XML file ####
    #
    # Next, load the specified deployment file into an REXML doc.
    # We can accept files that contain a single deployment, plan,
    # or behavior.
    #
    doc = REXML::Document.new(File.new(@deployment_filename))

    #### Create a Deployment object ####
    #
    # Take different steps depending on the type of root element (Deployment,
    # Plan, or Behavior).
    #
    root_type = doc.root.name.to_s
    case root_type.downcase


      when "deployment"

      ##
      # The root of the document is a Deployment element. Use the
      # element to construct a Deployment object.
      #
      @deployment = Deployment.new(doc.root)


      when "plan"

      ##
      # The root of the document is a Plan element.
      # Create a root Deployment element to contain the Plan,
      # then proceed normally.
      #
      pl_doc = REXML::Document.new
      pl_doc.add_element(REXML::Element.new("Deployment"))

      ##
      # Use the plan name as the deployment name if available
      #
      pl_name = doc.root.attribute("name")
      if (pl_name.nil?)
        pl_doc.root.attributes["name"] = "container_dep"
      else
        pl_doc.root.attributes["name"] = pl_name
      end

      ##
      # Insert the Plan element into the container deployment and go.
      #
      pl_doc.root.add_element(doc.root)
      @deployment = Deployment.new(pl_doc)



      else

      ##
      # We can assume we have a single behavior.
      #
      # Create a Plan element to contain the Behavior,
      # and a Deployment element to contain the Plan.
      # Then proceed.
      #
      plan = REXML::Element.new("Plan")
      plan.attributes["name"] ="container_plan"
      plan.attributes["sites"] ="1"

      ##
      # Insert the behavior into the container plan.
      #
      plan.add_element(doc.root)

      ##
      # Now create a document and a root Deployment element
      # to contain the above plan. Use the behavior type
      # for the Deployment name.
      #
      b_doc = REXML::Document.new
      b_doc.add_element(REXML::Element.new("Deployment"))
      b_doc.root.attributes["name"] = root_type

      ##
      # Insert the plan into the container Deployment and go.
      #
      b_doc.root.add_element(plan)
      @deployment = Deployment.new(b_doc)

    end
  end
  #####


  #####
  # Create a mission status string
  #
  def make_status_string(suffix=nil)
    status = @deployment.name + "|"
    if (plan = @deployment.current_plan)
      status += plan.name
      status += "[#{plan.site_num}]|"
      if (behavior = plan.current_behavior)
        status += behavior.name
        if (bstatus = behavior.status())
          status += "|#{bstatus}"
        end
      end
    end
    if (suffix)
      status += "|#{suffix}"
    end
    syslog("_ status string = #{status}")
    status
  end
  #####


  ########################################
  ######## Execute the deployment ########
  #
  # Execute the deployment
  #
  def execute

    unless @deployment
      puts("! Supervisor: No valid deployment to execute")
      return
    end

    ##
    # Set-up environment for deployment.
    #
    unless (@resume)
      ##
      # Create a new deployment log directory
      #
      DataLog.deployment_data_home = @deployment.name.gsub(" ","_")
      Syslog.instance.dir = DataLog.system_logs
      Console.instance.dir = DataLog.system_logs
      console("_ Supervisor...Setting up #{@deployment.name}")

      # Copy the deployment and configuration files to the data directory
      #
      copy_source_files()

      ##
      # Environment setup. We're good to go.
      #
      console("_ Supervisor...Running #{@deployment.name}")


    else
      ### Resuming a hibernating deployment.
      #
      # When resuming a deployment, data directories, log files, and
      # run-time databases already exist.
      #
      # Ensure that 'logs/latest' points to a log directory
      # of a hibernating deployment. Then
      # look in that directory for (1) a deployment filename that
      # matches the name of the loaded deployment file and
      # (2) a deployment database file.
      #
      hd = @dbif.load_hibernate_data
      latest = hd['deployment_data_home']
      unless (File.exists?(latest))
        console("_ Supervisor...Cannot resume #{@deployment.name}")
        console("    - Directory logs/latest not found")
        return
      end

      ##
      # OK, we found a valid deployment data directory we may use.
      #
      DataLog.set_deployment_data_home = latest
      Syslog.instance.dir = DataLog.system_logs
      Console.instance.dir = DataLog.system_logs

      ##
      # Now make sure a copy of the deployment file exists in
      # the data directory and that it matches the one we've
      # been asked to resume.
      #
      parts = File.split(Supervisor.instance.deployment_filename)
      d_file  = latest + "/" + parts[1]
      unless (File.exists?(d_file))
        console("_ Supervisor...Cannot resume #{@deployment.name}")
        console("    - Deployment in logs/latest does not match #{d_file}")
        return
      end

      ##
      # Finally, make sure that the deployment was requested to hibernate.
      #
      db_file = latest + "/" + "deploy.db"
      db_file = "/tmp/" + "deploy.db" if (Options.instance.sim)
      unless (File.exists?(db_file))
        console("_ Supervisor...Cannot resume #{@deployment.name}")
        console("    - Database file #{db_file} not found")
        return
      end

      ##
      # Environment restored. We're good to go.
      #
      console("_ Supervisor...Resuming #{@deployment.name}")
      if (warp = hd["warp"])
        Options.instance.warp = true
      end

    end

    ##
    # Create the Rover instance
    #
    Rover.instance

    ##
    # Start a communication message loop
    # TODO: Move this code into a separate method
    #
    @server = RemoteServer.new(6600)
    @server.init("#{ENV['ROVER_HOME']}/logs/supervisor_server.csv")

    ##
    console("_ Supervisor entering comms loop")
    c_loop = RoverThread.new do
      while (true)
        ##
        # Get a message
        #
        msg = @server.get_msg(2)
        if (msg)
          syslog("_ received message #{msg[0]}")

          case msg[0].strip.downcase

          ##
          # Handle abort message. Acknowledge and abort deployment.
          #
          when "abort"
            @server.send_msg("#{msg[0]} ack",msg[1])

            if (@deployment)
              if (@deployment)
                @deployment.abort() if (@deployment)
                @server.send_msg("#{@deployment.name} aborted",msg[1])

                #@deployment.run()
              end
            end
            
          ##
          # Handle status message. Get deployment status and return it.
          #
          when "status"
            sup_status = make_status_string().gsub!(/ /, '-')
            sup_status = "none" unless (sup_status)
            status = "status: " + sup_status
            syslog("_ status is #{status}")
            @server.send_msg(status,msg[1])

          ##
          # Return the data directory of the executing plan
          #
          when "data"
            @server.send_msg("data #{DataLog.plan_data_home}", msg[1])
          end

        end
      end
    end

    ##
    # Execute deployment
    #
    @deployment.resume = @resume
    @resume = false

    begin
      save_state("executing")

      @deployment.run()

      syslog("_ Deployment finished executing")
      console("_ #{@deployment.name} completed")

      
      hs = save_state("done")

      ##
      # Let default client (if any) know we're done
      #
      @server.send_msg("completed #{@deployment.name}")
      sleep(1)

      ##
      # 
    ##
    # Catch Hibernate exceptions
    #
    rescue Hibernate => e
      ##
      # Handle the hibernation request
      # Save information about deployment
      # Issue shutdown request to HESC-SERD board
      @server.send_msg("#{@deployment.name} hibernating #{e.duration} minutes")
      #request_hibernate(e.duration)
      request_hibernate_wakey(e.duration)

      c_loop.kill if (c_loop)

    end

  end
  #####

  #####
  # Copy mission source files to the data directory
  #
  def copy_source_files()
    cmd = "cp #{@deployment_filename} " + DataLog.deployment_data_home + "/."
#    console(cmd)
    system(cmd)
  end
  #####


  #####
  #
  #
  def request_hibernate_wakey(minutes)

    minutes = minutes.to_i - 1
    minutes = 1 unless (minutes > 0)

    done = false

    for i in 1..5    # give it 5 tries
      # Set the hibernation duration in minutes
      #
      # response = `/cf/barbo/device/wakey.rb set_hibernate_time #{minutes}`
      response = Rover.instance.wakey_msg("read_hibernate").strip.to_i
      syslog("_ wakey response to read_hibernate): #{response}")
      if (response != minutes)
        response = Rover.instance.wakey_msg("set_hibernate_time #{minutes}").strip.to_i
        syslog("_ wakey response to set_hibernate_time(#{minutes}): #{response}")
      end

      next unless (response == minutes)

      # Set the delay time dt in seconds before shutdown
      #
      dt = 19
      response = `/cf/barbo/device/wakey.rb set_delay_time #{dt}`
      syslog("_ wakey responce to set_delay_time(#{dt}): #{response}")
      next unless (response.strip.to_i == dt)

      # Save both the hibernation duration and the expected
      # start time upon wakeup for use when resuming.
      #
      hd = @dbif.load_hibernate_data()
      hd['hibernate_duration'] = minutes
      expected_restart = Time.now + minutes * 60
      syslog("_ expected restart at #{expected_restart}")
      hd['expected_restart'] = expected_restart.to_i
      hd['requested_hibernation_seconds'] = minutes * 60
      @dbif.save_hibernate_data(hd)

      # Put me to sleep
      #
      syslog("_ Initiating hibernation of #{minutes} minutes")
      response = `/cf/barbo/device/wakey.rb hibernate`
      # response = Rover.instance.wakey_msg("hibernate")
      syslog("_ wakey response to hibernate: #{response}")
      next unless (response.strip.to_i == minutes)

      # We're here so it must have worked
      #
      syslog("_ wakey pulling power plug...goodnight")
      done = true
      `poweroff`

      break

    end

    # Fall through in case hibernate does not succeed
    #
    syslog("_ Waiting for hibernation of #{minutes} minutes")
    sleep(minutes.to_i*60)
    `reboot`

  end
  #####
  

  ############################################
  ######## Hibernate and wake-up call ########
  #
  # ! This method deprecated. Using request_hibernate_wakey
  #
  # Use the HESC board interface to request a power shutdown
  # and wakeup call.
  #
  def request_hibernate(duration_minutes)

    done = false

    ##
    # Don't bother running the behavior unless the health
    # of the HESC-SERD is OK.
    #
    health = Supervisor.instance.dbif.load_component_state(RoverStore::Hesc)
    if (health[RoverStore::Health])
      @state = FINISHED
      syslog("! HESC-SERD health is poor. No go")
    else

      ##
      # Shutdown and rebooting takes about 50 seconds, so we'll
      # subtract that from requested time.
      #
      # It's gotta be at least a minute to bother.
      #
      duration_seconds = (duration_minutes*60) - 65
      duration_seconds = 0 if (duration_seconds < 0)

      if (duration_seconds < 60)
        console("_ Supervisor rejecting #{duration_minutes} minute hibernation")
      else

        console("_ Supervisor requesting #{duration_minutes} minute hibernation")

        ##
        # If warping through time, set a very short duration of 10 seconds
        #
        if (Options.instance.warp)
          duration_minutes = 1.05
          duration_seconds = duration_minutes * 60
        end
        
        set_interval = prep_hibernation(duration_minutes)

        ##
        # Give it up to five tries, as the HESC can be finiky.
        #
        for i in 1..5
          ##
          # Just call startup if the duration is the same as last hibernation
          #
          if (set_interval)
            done = Rover.instance.hesc.hibernate_sequence(duration_seconds)
          else
            done = Rover.instance.hesc.startup()
          end
          if (done)       # It's a go
            break
          else
            sleep(1.5)    # Try again	
            Rover.instance.hesc.ver()
          end

        end   # for loop
    
      end     # duration

    end       # health

    ##
    # If hibernation request denied, sleep and reboot instead
    # to keep mission rolling along.
    #
    unless (done)
      syslog("! Hibernation request failed. Sleep and reboot")
      console("_ Sleeping rather than hibernating...then reboot")
      sleep(duration_seconds)

      unless (Options.instance.sim)
        system("reboot")
      else
#        Thread.exit
      end
    end
  end
  #####


  #####
  # Save the hibernation info
  #
  def save_state(status)
    hs = nil
    unless(hs = @dbif.load_hibernate_data())
      hs = Hash.new
    end

    ##
    # Save the state
    #
    hs["deployment_file"] = @deployment_filename
    hs["deployment_data_home"] = DataLog.deployment_data_home
    hs["status"] = status
    hs["warp"] = Options.instance.warp
    hs["hibernation_requested"] = (status == "hibernating")

#    puts(hs["status"])
#    puts(hs["hibernation_requested"])

    @dbif.save_hibernate_data(hs)
    hs
  end
  #####

  ######## Prepare for hibernation ########
  #
  # Prepare for hibernation.
  # We minimize the use of HESC-SERD sensitive data by only
  # running the full sequence if the requested duration is
  # different from the last request.
  #
  def prep_hibernation(duration_minutes)
    hs = save_state("hibernating")   # Save standard data and hibernation status

    ##
    # Compare durations
    #
    last_duration = hs["duration"]
    syslog("_ last duration=#{last_duration},this duration=#{duration_minutes}")
    if (last_duration == duration_minutes)
    #if (false)  # set interval every time
      return false                         # Durations are the same
    else
      hs["duration"] = duration_minutes
      @dbif.save_hibernate_data(hs)        # Durations not are the same
      return true                          # Need to set the sleep interval
    end

  end
  #####


  ######## Can we resume the given deployment? ########
  #
  # Look in the RoverStore database to see if we should
  # consider resuming from a hibernating deployment.
  #
  def do_i_resume?
    ##
    # Look for a hibernation record in the main Rover database.
    #
    # Open the RoverStore database. There must be a hibernation record.
    #
    hd = nil
    unless (hd = (@dbif.load_hibernate_data))
      console("_ Cannot resume deployment - no deployment record in rover db")
      return false
    end

    ##
    # Did the Supervisor request hibernation?
    #
    unless (hd['hibernation_requested'])

      console("_ Hibernation not requested by deployment...")
      ##
      # The deployment may have been interrupted because of a crash, or some
      # unhandled exception.
      # Check the "status" value. Unless it is "done", then resume.
      #
      status = hd['status'].downcase
      if ("done" == status)
        console("_ Cannot resume deployment - deployment complete")
        return false
      else
        console("_ Resuming interrupted deployment: Saved status was #{status}")
      end
    end

    ##
    # Is this the same deployment?
    #
    dfile =  nil
    unless (dfile = (hd['deployment_file']))
      console("_ Cannot resume deployment - no deployment file specified")
      return false
    end

    ##
    # If we're here, then we must be resuming a deployment.
    # Reset hibernation_requested flag.
    #
    hd['hibernation_requested'] = false
    @dbif.save_hibernate_data(hd)

    @deployment_filename = dfile
    @resume = true

  end

  #####
  #
  def Supervisor.test
    # Test code here

    # Try to load a non-existent file, dump the contents
    s = Supervisor.instance
    s.make_deployment("bumpus_plan.xml")
    puts(Supervisor.instance.deployment_filename)
    puts(Supervisor.instance.config_filename)

    # Load a full deployment file, dump the contents, and execute
    Supervisor.instance.make_deployment("test_deploy.xml")
    puts(Supervisor.instance.deployment_filename)
    puts(Supervisor.instance.config_filename)
    puts(Supervisor.instance.deployment.dump)
    Supervisor.instance.execute
    exit

    # Load a behavior file, dump the contents, and execute
    Supervisor.instance.make_deployment("test_sample.xml")
    puts(Supervisor.instance.deployment_filename)
    puts(Supervisor.instance.config_filename)
    puts(Supervisor.instance.deployment.dump)
    Supervisor.instance.execute

    # Load a plan file, dump the contents, and execute
    Supervisor.instance.make_deployment("test_plan.xml")
    puts(Supervisor.instance.deployment_filename)
    puts(Supervisor.instance.config_filename)
    puts(Supervisor.instance.deployment.dump)
    Supervisor.instance.execute

    puts("Done")
  end
  #####

end


#########################################################
#########################################################
#
# Top-level Rover script. This is where is all starts.
#
#########################################################

##
# User executing supervisor from command line
#
pids = Misc.running?("/supervisor.rb")
if (pids && pids.length > 1)
  puts("_ Sorry, supervisor process #{pids.join(',')} already running")
elsif __FILE__ == $0

  ##
  # Is this just a simple test?
  # If the deployment file is just "test_me", then run Supervisor.test
  if (Options.instance.deployment_file == "test_me")
    Supervisor.test
  end

  supe = Supervisor.instance

  Kernel.trap("INT")  { supe.save_state("done"); exit }
  Kernel.trap("TERM") { supe.server.send_msg("Supervisor killed") if (supe.server); exit }

  #`mount /dev/nbd0p1 /sd`

  ##
  # This is an actual deployment, either real or simulated.
  #
  # Determine if we're starting with a new initial deployment or
  # resuming a hibernating deployment.
  #
  # Did the user use the "--resume" command-line option?
  #
  if (Options.instance.resume)

    ##
    # We're resuming from a hibernating deployment.
    #
    # The deployment databases and the latest log directory
    # information must match as well.
    #
    unless (supe.do_i_resume?)
      exit   # If not, we're done
    end
    puts("_ Preparing to resume #{supe.deployment_filename}")

    ##
    # Use the deployment file from the hibernating deployment
    #
    deployment_file = supe.deployment_filename

  else
    ##
    # OK, we're not resuming.
    #
    # Clear the hibernate record and start new deployment using the
    # deployment file specified on the command line.
    #
    supe.dbif.save_hibernate_data(nil)
    deployment_file = Options.instance.deployment_file
  end

  ##
  # Whether we're resuming or starting new, we begin by
  # loading the deployment plan file and running.
  #
  supe.make_deployment(deployment_file)
  #
#  puts(supe.deployment_filename)
#  puts(supe.config_filename)
#  puts(supe.deployment.dump)

  ##
  # Do it
  #
  supe.execute()

  exit
end

