=begin
 ******************************************************************************
 * Copyright 1990-2024 MBARI
 * MBARI Proprietary Information. All rights reserved.
 ******************************************************************************
 * Summary  : Implements the Plan class
 * Filename : plan.rb
 * Author   : Henthorn
 * Project  : Benthic Rover
 * Version  : 1
 * Created  : Nov 2008
 * Modified : Sept 2014 - Backup plan data directories
 * Modified : Oct  2024 - Add event detection
 ******************************************************************************
=end

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

require "utils/misc"
require "control/supervisor"
require "control/factory"
require "control/event_detection"     # Handle image processing and event detection logic

##########
# The Plan class implements the concept of  Benthic Rover execution plan.
# An execution plan is a sequence of simple operations, or "behaviors",
# that the Rover is capable of performing. This sequence of behaviors can
# be repeated a number of times, but that is the only sequencing control
# allowed at the moment (no branching statements in the plan, etc).
#
# A plan is specified inside an XML document containing a Plan element. A Plan
# element contains a sequence of Behavior elements.
#
class Plan
  include SyslogWriter
  include Dump

  HERE = 0
  READY = 1
  EXECUTING = 2
  ABORTING = 3
  FINISHED = 4

  attr_reader :name, :behaviors, :sites, :behavior_sequence_number, :sed
  attr_reader :site_num, :current_behavior, :state, :backup, :tar_zip

  ######## Create a Plan object from a Plan element ########
  #
  # Construct a Plan given a Plan XML element
  #
  def initialize(p_plan_element=nil)

    @current_behavior = nil
    @behavior_sequence_number = 0
    @site_num = 0
    @sites = 1
    @name = "plan"
    @behaviors = Array.new
    @state = HERE
    @resume = false
    @backup = false
    @tar_zip = false
    @sed = nil

    # No element? We're done.
    return unless (p_plan_element)

    self.load_plan(p_plan_element)
  end
  #####


  #####
  # Load the Plan given a Plan XML element
  #
  def load_plan(p_plan_element)

    lo_lim = EventDetection::DEFAULT_SED_LIM
    hi_lim = EventDetection::DEFAULT_SED_LIM
    lim_ratio = 2.0
    bg = EventDetection::DEFAULT_BG
    rm = EventDetection::DEFAULT_RM
    samples = EventDetection::DEFAULT_SAMPLES

    #### Check element type ####
    #
    # First of all, it has gotta be an XML element of type
    # Plan or AbortPlan.
    #
    if (p_plan_element.class.to_s != "REXML::Element")
      puts("! Unsupported element", p_plan_element.class )
      return
    end
    if ((p_plan_element.name.downcase != "plan") &&
        (p_plan_element.name.downcase != "abortplan"))
      puts("! Element not a recognized type of plan", p_plan_element.name )
      return
    end

    ##
    # If the plan element has a file attribute, open that file and
    # load that plan (sort of like "include").
    #
    if (fname = p_plan_element.attributes["file"])
      puts("...loading from #{fname}")
      doc = REXML::Document.new(File.new(Misc.find_plan(fname)))
      load_plan(doc.root)
      return
    end

    #### Get the plan name and number of sites ####
    #
    pl_name = p_plan_element.attribute("name").to_s
    @name = pl_name if (pl_name)
    pl_sites = p_plan_element.attribute("sites")
    @sites = pl_sites.to_s.to_i if (pl_sites)

    ##
    # Load event detection and image processing parameters here
    #
    if (t = p_plan_element.attribute("lo_lim"))
      lo_lim = t.to_s.to_f
    end

    if (t = p_plan_element.attribute("hi_lim"))
      hi_lim = t.to_s.to_f
    end

    if (t = p_plan_element.attribute("lim_ratio"))
      lim_ratio = t.to_s.to_f
    end

    if (b = p_plan_element.attribute("background"))
      bg = (b.to_s.to_i)
    end

    if (r = p_plan_element.attribute("rolling_mean"))
      rm = (r.to_s.to_i)
    end

    if (s = p_plan_element.attribute("samples"))
      samples = s.to_s.to_i
    end

    #
    # Setup the backup indicator
    #
    bk = p_plan_element.attribute("backup")
    if (bk)
      syslog("_ backup set to #{bk.to_s}")
      @backup = (bk.to_s.downcase == "true") if (bk)
      syslog("_ @backup set to #{@backup}")
    else
      syslog("_ backup s not set")
    end

    puts("_ Backup requested for plan #{@name}") if (@backup)
    if (@backup)
      unless (File.exist?(DataLog.backup_home))
        @backup = false
        puts("_ Backup not possible - no backup drive detected")
      end
    end

    ##
    # Should the plans tar and zip themselves after execution
    #
    if ((att = p_plan_element.attributes["tar_zip"]) &&
        (att.upcase == "TRUE"))
      @tar_zip = true
    end


    #### Load in the behavior sequence ####
    #
    # Sub-elements must be known Behavior type.
    # Use BehaviorFactory to create the specific Behavior objects
    # to populate array of behaviors.
    #
    b_list = @behaviors  # Start by populating plan sequence
    p_plan_element.each do |e|
      if (e.class.to_s == "REXML::Element")
        b = Factory.behavior(e) if (e.class.to_s == "REXML::Element")
        b_list.push(b) if (b)
      end
    end

    # Create EventDetection object with loaded parameters
    @sed = EventDetection.new(lo_lim, hi_lim, lim_ratio, bg, rm, samples)
  end
  #####


  ##############################################
  ######## Prepare a plan for execution ########
  #
  # Prepare the Plan for execution: set initial conditions, create log
  # directory, re-initialize Rover-level drivers and syslog, etc.
  # This method should be executed just before running the plan.
  #
  def init(p_plan_seq_num=1)
    #
    # Initialize Rover components
    #
    console("_ #{@name} initializing...")

    #
    # Sequence number comes in handy
    #
    @seq_num = p_plan_seq_num
    @seq_string = @seq_num.to_s
    if (@seq_num <= 9)
      @seq_string = "0" + @seq_string
    end

    #
    # Site and behavior counters
    #
    @site_num = 1
    @behavior_sequence_number = 0
    @current_behavior = nil
    @state = READY

    #
    # Clear the plan state in deployment database
    #
    Supervisor.instance.deployment.dbif.save_plan_data(nil)
    Supervisor.instance.deployment.dbif.save_behavior_data(nil)

    # Clear the mission_parameters as well (in rover.db, not deploy.db)
    #
    Supervisor.instance.dbif.save_mission_params(nil)

  end
  #####

  def sed_event()
    if (@sed.samples_used < @sed.num_samples)
      Supervisor.instance.deployment.sed_event = true
      @sed.samples_used=@sed.samples_used?+1
    else
      syslog("_, sed_event(): #{@sed.num_samples} plan samples used up")
    end
  end

  ###############################################################
  ######## Prepare a plan for resuming after hibernation ########
  #
  def resume_init

    #
    # Read the saved plan state from the deployment database
    #
    ps = Supervisor.instance.deployment.dbif.load_plan_data()
    mp = Supervisor.instance.dbif.load_mission_params()

    @updated_sites = mp['sites'] if (mp)
    @site_num = ps['site_num']
    @behavior_sequence_number = ps['behavior_num']
    DataLog.set_plan_data_home = ps['log_dir']
    @sed.samples_used=ps['samples_used']
    syslog("_ Resuming plan #{@name} at #{@site_num} with #{@sed.samples_used?} samples used")
    @state = READY

    # This is the resume flag for this plan, not the deployment
    #
    @resume = true
  end
  #####

  ######## Execute the Plan ########
  #
  def execute

    ##
    # Have we been initialized?
    #
    if (@state != READY)
      console("! Cannot execute plan #{@name}. Not in ready state")
      return
    end

    ##
    # Do we have an updated number of sites to run?
    #
    if (@updated_sites)
      @sites = @updated_sites
      syslog("_  Updated to run #{@updated_sites} sites")
    end

    ##
    # Iterate through the number of sites
    #
    console("_ Executing plan #{@name}")
    while (@site_num <= @sites)
      #
      # Make a new directory for this site unless we're resuming
      #
      mn_zero = @site_num > 9 ? "" : "0"
      log_name = @name.gsub(" ","_") + "-" + mn_zero + @site_num.to_s
      DataLog.plan_data_home = log_name unless @resume

      #
      console("_ Executing #{@site_num} of #{@sites} sites")
      syslog("_ Executing #{@site_num} of #{@sites} sites")

      #
      # Sequence through the behaviors
      #
      execute_behaviors()

=begin
      # Backup the plan data if requested - might move this to a
      # more appropriate place like the HomeClean behavior
      #
      if (@backup)
        begin
          bt = RoverThread.new do
            syslog("_ #{DataLog.plan_data_home} => #{DataLog.deployment_data_backup_home}")
            syslog(DataLog.perform_backup(DataLog.plan_data_home))
            syslog(DataLog.perform_backup(DataLog.system_logs))
            syslog(`ls -l #{DataLog.deployment_data_backup_home}`)
          end
          n = 0
          while(bt.alive?) do
            syslog("_ waiting for backup #{n}...")
            sleep(1)
            n += 1
            bt.exit if (n > 300)
          end
          bt.join
        rescue Exception => e
          syslog(e.to_s)
        end
      end
=end

      # create a 'latest' link to the just completed plan data directory

      #
      # Prepare for next site
      #
      @behavior_sequence_number = 0
      @site_num += 1

      break if (@state == ABORTING)
    end

    #
    # Done with the plan
    #
    @state = FINISHED
  end
  ######


  def tar_and_zip_plan_data()

    #return

    ##
    # Test new backup function
    # This seems to hang every so often. The USB driver becomes wedged
    # where you cannot even execute a 'ls' command without hanging.
    #

    syslog("_ begin backup")
    st = Time.now.to_i
    #`cp -au #{DataLog.deployment_data_home} /media/backup/logs/.`
    `cp -avu #{DataLog.deployment_data_home} /media/backup/logs/. && sync -f /media/backup`
    # No /media/aux-backup on V3
    # `cp -avu #{DataLog.deployment_data_home} /media/aux-backup/logs/.`
    st = Time.now.to_i - st
    syslog("_ backup finished in #{st} seconds")

    return

    ##
    # Done with site - tar and zip the data directory.
    # Take some steps to ensure we don't delete anything
    # other than a plan data directory.
    #
    depl_dir = DataLog.deployment_data_home
    plan_dirs = DataLog.plan_data_home.split('/')
    plan_dir = plan_dirs[-1]

    # Make sure data directory is the target for tar/zip/delete
    #
    dres = depl_dir.scan(/\/logs\//)
    pres = plan_dir.scan(/^plan-/)

    if ((dres.length > 0) && (pres.length > 0))
      # Create commands to tar/zip plan data directory, then remove
      #
      syslog("_ tar and zip data directory #{dres} #{pres}")
      tz_cmd = "cd #{depl_dir} && tar czf #{plan_dir}.tar.gz #{plan_dir}"
      response = `#{tz_cmd}`
      syslog("_ #{tz_cmd} : #{response}")
      if (response.length == 0)
        syslog("_ rm -rf #{DataLog.plan_data_home}")
        if (system("rm -rf #{DataLog.plan_data_home}"))
           syslog("_ tar-zipped plan data removed")
        else
           syslog("_ plan dir not removed")
        end
      else
        syslog("_ #{DataLog.plan_data_home} not zipped")
      end
    else
      syslog("! unable to tar and zip data directory #{dres} #{pres}")
    end
  end


  ####################################################
  ######## Execute the behaviors in this plan ########
  #
  def execute_behaviors
    begin
      while (@behavior_sequence_number < @behaviors.length) do

        ##
        # Update the plan state
        #
        save_plan_state()

        ##
        # Refer to the next behavior in the sequence
        #
        @current_behavior = @behaviors[@behavior_sequence_number]
        b = @current_behavior

        ##
        # Write a blurb about what we're doing
        #
        console("_ Executing #{b.class} behavior #{b.name}")
        syslog("_ Executing #{b.class} behavior #{b.name}")

        ##
        # Initialize and execute the behavior, allowing for resumption
        #
        if (@resume)
          @resume = false
          console("_ Resuming behavior number #{@behavior_sequence_number}")
          b.resume_init()
        else
          b.init(@behavior_sequence_number+1)
        end

        ##
        # Run the behavior in the current thread
        # If a skip rate is specified, only execute
        # if site_num is a even multiple of skip_rate
        # (e.g., execute every third iteration when
        # skip_rate is 3)
        # Combine skip_rate and one_timer:
        #   If one_timer then only execute the behavior once
        if (b.skip_rate == 0)
          b.run()
        else
          syslog("_ #{b.name}: site = #{@site_num} skip_rate = #{b.skip_rate}")
          if ((@site_num % b.skip_rate) == 0)
            # Check one timer status
            if (b.one_timer)
              # one_timer? Run just once when site_num equals skip_rate
              if (@site_num == b.skip_rate)
                b.run()
              else
                syslog("_ Skipping #{b.name}, already executed once")
              end
            else
              b.run()  # execute every b.skip_rate iterations
            end
          else
            syslog("_ Skipping #{b.name}")
          end
        end

        ##
        # Prepare for the next behavior
        #
        @behavior_sequence_number += 1
        @current_behavior = nil

        break if (@state == ABORTING)
      end

      syslog("_ Done with behaviors.")


    rescue OkToContinue => e
      # Depending on what kind of Exception, perhaps we can
      # continue to the next behavior, abort the plan, abort the
      # deployment, etc.
      # TODO: Create exception hierarchy (OkToContinue, AbortPlan, etc.)
      #
      # For now, continue with the plan.
      syslog(e.to_s)
      @behavior_sequence_number += 1
      retry  # Continue with behaviors


    ############################################
    ######## Catch Hibernate exceptions ########
    rescue Hibernate => e

      #
      # Save the state of the plan and raise the exception
      #
      prep_hibernation()
      self.clean_up()
      raise e


    ########################################
    ###### Catch all other exceptions ######
    rescue Exception => e
      syslog("!Caught exception: #{e.to_s}")
      syslog("#{e.backtrace}")
      ##
      # This should result in simply moving on to the next behavior
      #
      self.clean_up()
      @behavior_sequence_number += 1
      retry  # Continue with behaviors

    ensure
      tar_and_zip_plan_data() # if (@tar_zip)
    end

  end
  #####


  ######## Prepare plan for hibernation ########
  #
  # Save the plan state for use when resuming
  #
  def prep_hibernation()
    console("_ Hibernating behavior number #{@behavior_sequence_number}")
    syslog("_ preparing for hibernation")
    save_plan_state()
  end
  #####


  #####
  # Save the current state of the plan to the database
  #
  def save_plan_state()
    ps = nil
    unless (ps = Supervisor.instance.deployment.dbif.load_plan_data)
      ps = Hash.new
    end
    ps["site_num"] = @site_num
    ps["behavior_num"] = @behavior_sequence_number
    ps["log_dir"] = DataLog.plan_data_home
    ps["samples_used"] = @sed.samples_used?

    Supervisor.instance.deployment.dbif.save_plan_data(ps)
    syslog("_ state saved")
  end
  #####


  #####
  # Tidy-up as if we're done or hibernating
  #
  def clean_up
    Rover.instance.relay.allOff
  end


  #####
  def abort
    @state = ABORTING
    self.clean_up
    @current_behavior.abort if @current_behavior
  end
  #####


  #####
  # Dump plan contents to a string and return
  #
  def ddump
    s = (Dump.dump)
    @behaviors.each { |b| s += "\n" + (b.dump) }
  end
  #####

  #####
  # Generic test method
  #
  def Plan.test
    require 'rexml/document'

    # Test code here
    doc = REXML::Document.new(File.new(Misc.find_plan("test_plan.xml")))
    p = Plan.new(doc.root)
    puts("Plan loaded with #{p.behaviors.size} behaviors")
    puts(p.dump)
    p.behaviors.each do |b|
      puts(b.dump)
    end

    p.init
    p.execute
    p.execute
    p.init
    p.execute

    # Indirect plan
    doc = REXML::Document.new(File.new(Misc.find_plan("test_plan_file_ref.xml")))
    p = Plan.new(doc.root)
    puts("Plan loaded with #{p.behaviors.size} behaviors")

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

end

##
# Standalone unit test (recommended)
#
if __FILE__ == $0
  Plan.test
end
