=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 consists of a sequence of simple instructions, 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, 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, :hibernate, :resuming

  #####
  # 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
    @hibernate = false
    @resuming = 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)
    # Otherwise, gotta be a proper element
    #
    if (p_plan_element.class.to_s != "REXML::Element")
      puts("! Unsupported element", p_plan_element.class )
      return
    end
    if (p_plan_element.name != "Plan")
      puts("! Element not a Plan", p_plan_element.name )
      return
    end

    # If a file attribute exists, open that file and load that plan
    #
    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

    # Should I hibernate?
    #
    if ((hib = p_plan_element.attributes["hibernate"]) &&
	(hib.upcase == "TRUE"))
      @hibernate = true
    end

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


    # Sub-elements are behaviors. Use Factory to create Behavior objects
    # to populate array of behaviors.
    #
    p_plan_element.each do |e|
      b = Factory.behavior(e) if (e.class.to_s == "REXML::Element")
      @behaviors.push(b) if (b)
    end

  end
  #####


  #####
  # Prepare the Plan for execution: set initial conditions.
  # This method should be executed just before running the plan.
  #
  def init(p_plan_seq_num=1)
    # Initialize plan
    #
    console("_ #{@name} initializing...")

    @seq_num = p_plan_seq_num
    @seq_string = @seq_num.to_s
    if (@seq_num <= 9)
      @seq_string = "0" + @seq_string
    end
    @site_num = 1
    @behavior_sequence_number = 0
    @current_behavior = nil
    @state = READY
  end
  #####

  #####
  # Prepare plan for resuming execution after hibernation
  #
  def resume_init
    console("_ #{@name} resuming...")

    # Read state  from mdb
    #
    @seq_num = p_plan_seq_num
    @seq_string = @seq_num.to_s
    if (@seq_num <= 9)
      @seq_string = "0" + @seq_string
    end
    @site_num = 1
    @behavior_sequence_number = 0
    @current_behavior = nil
    @state = READY
  end
  #####


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

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

    console("_ Executing plan #{@name}")

    # Iterate over the sites
    #
    while (@site_num <= @sites)

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

      unless (@resuming)
        # Make a new directory for this site
        #
        mn_zero = @site_num > 9 ? "" : "0"
        log_name = @name.gsub(" ","_") + "-" + mn_zero + @site_num.to_s
        DataLog.plan_data_home = log_name
      end

      execute_behaviors
      @site_num += 1 # Next site
      break if (@state == ABORTING)
    end
    @state = FINISHED
  end     # execute
  ######

  ##
  # Iterate over and execute the behaviors in this plan
  #
  def execute_behaviors
    begin
      while (@behavior_sequence_number < @behaviors.length) do
        @current_behavior = @behaviors[@behavior_sequence_number]
        bclass = @current_behavior.class
        bname = @current_behavior.name
        console("_ Executing #{bclass} behavior #{@bname}")
        syslog("_ Executing #{bclass} behavior #{@bname}")

        # Initialize and execute the behavior
        #
        @current_behavior.init(@behavior_sequence_number+1)

        bthread = @current_behavior.run
        bthread.join

        @behavior_sequence_number += 1
        @current_behavior = nil
        break if (@state == ABORTING)
      end
    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
      
    rescue Hibernate => e
      # Prepare for a re-entry into execute
      #
      syslog(e.to_s)
      @state = READY
      syslog("_ prepare for hibernation")

      # Pass it on to parent
      #
      raise e

    rescue Exception => e
      self.clean_up
      syslog(e.to_s)
      raise e
    end
  end
  #####


  #####
  def clean_up
    Rover.instance.relay.allOff
    syslog("_ #{@name} done")
  end


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


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