=begin
 ******************************************************************************
 * Copyright 1990-2009 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 :
 ******************************************************************************
=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"

##########
# 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
  attr_reader :site_num, :current_behavior, :state, :backup

  ######## 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
    @updated_sites = nil
    @name = "plan"
    @behaviors = Array.new
    @state = HERE
    @resume = false
    @backup = false

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

    #### 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)
    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}")
    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

    #### 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.
    #
    num_attempted = 0
    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)
        num_attempted += 1
      end
    end

    puts("_ #{b_list.length} of #{num_attempted} behaviors loaded")
  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
  #####


  ###############################################################
  ######## 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']
    @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()

      if (Supervisor.instance.deployment.tar_zip)
        ##
        # 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 the tar/zip file, then remove the data directory
          #
          syslog("_ tar and zip data directory #{dres} #{pres}")
          if (system("cd #{depl_dir} ; tar czf #{plan_dir}.tar.gz #{plan_dir}"))
            system("cd #{depl_dir} ; rm -rf #{plan_dir}")
          end
        else
          syslog("! unable to tar and zip data directory #{dres} #{pres}")
        end
      end
      
      #
      # Prepare for next site
      #
      @behavior_sequence_number = 0
      @site_num += 1

      break if (@state == ABORTING)
    end

    #
    # Done with the plan
    #
    @state = FINISHED
  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
        #
        b.run()

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

    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()

    # Store the mission status in the hibernation db for
    # retrieval by the executive.
    #
    hd = nil
    unless (hd = Supervisor.instance.dbif.load_hibernate_data)
      hd = Hash.new
    end
    hd["deploy_name"] = Supervisor.instance.deployment.name
    hd["plan_name"] = @name
    hd["site_num"] = @site_num
    hd["behavior_name"] = @current_behavior.name
    hd["behavior_status"] = @current_behavior.status
    Supervisor.instance.dbif.save_hibernate_data(hd)
  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

    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
