=begin
 ******************************************************************************
 * Copyright 1990-2009 MBARI
 * MBARI Proprietary Information. All rights reserved.
 ******************************************************************************
 * Summary  : What is contained within the file
 * Filename : 
 * Author   : 
 * Project  : Benthic Rover
 * Version  : 
 * Created  : 
 * Modified : Sept 2014 - tar_zip attribute, voltage limit
 ******************************************************************************
=end

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

##########
# Deployment class - Simple container object for a Rover deployment specification.
# Contains Plan objects, which in turn contain behavior objects.
#
class Deployment
  include Dump
  include SyslogWriter

  attr_reader :name, :plans, :current_plan, :plan_sequence_number, \
              :behaviors, :hibernate, :dbif, :mars, :tar_zip, \
              :voltage_limit
  attr_accessor :resume, :depl_samples_used


  #####
  # Constructor - Create a Deployment object using the xml element argument
  #
  #               d_doc:REXML::Element
  def initialize(d_doc)
    @name = "deployment"
    @plans = Array.new
    @behaviors = Array.new
    @current_plan = nil
    @plan_sequence_number = 0
    @depl_samples_used = 0
    @abort_plan = nil
    @mars = false
    @tar_zip = false
    @hibernate = false
    @dbif = nil
    @d_thread = nil
    @abort = false
    @voltage_limit
    @sedevent = false
    @event_mutex = Mutex.new

    ##
    # Give me a name
    #
    dname = d_doc.root.attributes["name"]
    @name = dname if dname
    puts("_ Supervisor...loading deployment #{@name}")

    ##
    # Simulation specified in the deployment file?
    #
    if ((sim = d_doc.root.attributes["sim"]) &&
        (sim.upcase == "TRUE"))
      @sim = true
    end

    ##
    # Should I behave as if I am on MARS (leave video on, etc.)?
    #
    if ((att = d_doc.root.attributes["mars"]) &&
    (att.upcase == "TRUE"))
      @mars = true
    end

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

    ##
    # Does the Rover hibernate during this deployment?
    #
    if ((hib = d_doc.root.attributes["hibernate"]) &&
	(hib.upcase == "TRUE"))
      @hibernate = true
    end

    # Is there a voltage limit assigned to this deployment?
    #
    if ( (volt = d_doc.root.attributes["voltage_limit"]) )
      @voltage_limit = volt.to_s.to_f
      # Range check
      #
      if (@voltage_limit < 15 || @voltage_limit > 35)
        @voltage_limit = nil
      end
    end

    ##
    # Create a Plan object for each Plan element in the Deployment doc.
    # Manage list of normal plans in addition to a special abort plan.
    #
    d_doc.root.elements.each do |e|
      e_type = e.name.to_s
      case e_type.downcase


      when "plan"
        ##
        # When a Plan element is encountered, create a Plan object
        # and add it to my list of plans.
        #
        console("_ Deployment...loading plan #{e.attributes['name']}")
        p = Plan.new(e)
        @plans.push(p)
        sleep(0.2)


      when "abortplan"
        ##
        # When a AbortPlan element is encountered, create a Plan object
        # and keep that as the abort plan.
        #
        console("_ Deployment...loading abort plan #{e.attributes['name']}")
        p = Plan.new(e)
        @abort_plan = p
        sleep(0.2)

      end

    end
  end
  #####

  def sed_event?()
    event = false
    @event_mutex.synchronize do
      event = @sedevent
    end
    return event
  end

  def sed_event=(event)
    syslog("_, Setting sedevent to #{event}")
    @event_mutex.synchronize do
      @sedevent = event
    end
  end

  #################################################
  ######## Run the plans in the deployment ########
  #
  # In a separate thread, execute the plans in the plan list.
  #
  def run

    abort_on_voltage = false
    @d_thread = RoverThread.new do

      ##
      #
      syslog(`du -h /var/log`)
      syslog(`du -sh /home/ses/*`)

      # Perform a humidity check
      #
      Rover.instance.humidity()

      # Perform a voltage check if required
      #
      volt = Rover.instance.wakey_msg("read_batt")
      #syslog("Deployment voltage: #{voltage}")
      if (@voltage_limit)
        # volt = `/cf/rover/barbo/device/wakey.rb read_batt`
        volt.strip!
        voltage = volt.to_f
        if (voltage > 0 && voltage < @voltage_limit)
          abort_on_voltage = true
          syslog("! Deployment will not run: Current voltage #{voltage} lower than")
          syslog("  threshold voltage of #{@voltage_limit}")
          puts(  "! Deployment will not run: Current voltage #{voltage} lower than")
          puts(  "  threshold voltage of #{@voltage_limit}")
          sleep(30)
        end
      end

      if (abort_on_voltage)
        if (@hibernate)
          syslog("! Unscheduled hibernation")
          puts(  "! Unscheduled hibernation")
          # `/cf/rover/barbo/device/wakey.rb hibernate 1440`
          Rover.instance.wakey_msg("hibernate 1440")
          sleep(5)
        end
      else
        execute_plans()
      end
    end

    @d_thread.join()
    syslog("_ #{@name} completed normally")

    ##
    # If the deployment was aborted, run the abort plan if available
    #
    if (@abort)
      @plans = []
      if (@abort_plan)
        syslog("_ #{name} starting abort plan")
        @plans = @abort_plan
        @abort_plan = nil
        self.run
      else
        syslog("_ #{name}: no abort plan")
      end
      @abort = false
    end
  end
  #####


  #####
  # Start non-sequential behaviors (not implemented)
  #
  def start_behaviors
    @behaviors.each do |b|
      b.run
    end
  end
  #####


  #####
  # Stop non-sequential behaviors (not implemented)
  #
  def stop_behaviors
    @behaviors.each do |b|
      b.abort
    end
  end
  #####


  #####
  # Abort the deployment thread that is executing
  # the deployment plans.
  #
  def abort
    if (@d_thread && @d_thread.alive?)
      syslog("_ #{@name} aborting...")

      ##
      # Kill the execution thread
      #
      @d_thread.kill
      @d_thread = nil
      Rover.instance.relay.allOff
      
      @abort = true
      @resume = false
    else
      @abort = false
      syslog("_ deployment thread not running")
    end
  end
  #####


  ###########################################
  ######## Execute the list of plans ########
  #
  # Iterate over the plan list, executing them one at a time.
  #
  def execute_plans()

    return unless (@plans)
    
    ##
    # Open the deployment database used for saving the
    # state of the deployment.
    #
    db_file =  DataLog.deployment_data_home + "deploy.db"
    db_file =  "/tmp/deploy.db" if (Options.instance.sim)
    @dbif = DeployStore.new(db_file)

    ##
    # We start with the first plan, unless resuming
    #
    unless (@resume)
      @plan_index = 1

    ##
    # If we're resuming from a hibernating deployment, load
    # the deployment state from the database and use it as
    # a starting point now.
    #
    else
      ds = nil
      ds = @dbif.load_deploy_data()
      @plan_index = ds["plan_num"]
      unless (DataLog.set_deployment_data_home = ds["log_dir"])
        syslog("_ Deployment data home not found: #{ds['log_dir']}")
        console("_ Deployment data home not found: #{ds['log_dir']}")
        return
      end
      @depl_samples_used = ds["samples_used"]
      syslog("_ Deployment samples used: #{@depl_samples_used}")
    end

    #### Execute the plans in the list ####
    #
    # Iterate over the plans in the deployment.
    # Initialize and execute each.
    #
    console("========== Executing Deployment #{@name} ==========")
    for @plan_sequence_number in @plan_index..@plans.length

      begin
        @current_plan = @plans[@plan_sequence_number-1]

        ##
        # Initialize the plan
        #
        if (@resume)
          #
          # Allow the plan to reload it's state.
          #
          @current_plan.resume_init
        else
          #
          # Standard plan initialization
          #
          @current_plan.init(@plan_sequence_number)
        end

        save_state()

        @resume = false
        syslog("_ plan sequence number #{@plan_sequence_number} executing")
        syslog("_ executing #{@current_plan.name}")
        @current_plan.execute

        ##
        # Done with current plan - fall through to next plan

      rescue Hibernate => e

        #### Catch Hibernate exception ####
        #
        console("======= #{@name} preparing for hibernation ========")

        ##
        # Handle the hibernation request:
        # Save state of deployment,
        # pass the exception on to the Supervisor
        #
        prep_hibernation()
        raise e
        return


      ensure
      end

    end

    ##
    # Done with all plans in plan list
    #
    @current_plan = nil
  end
  #####


  #################################################
  ######## Prepare for mission hibernation ########
  #
  # Save state needed when resuming.
  #
  def prep_hibernation()
    ds = nil
    unless (ds = Supervisor.instance.deployment.dbif.load_deploy_data)
      ds = Hash.new
    end
    #
    # Save sequence number and location of data directory
    #
    ds["plan_num"] = @plan_sequence_number
    ds["log_dir"] = DataLog.deployment_data_home
    ds["samples_used"] = @depl_samples_used
    @dbif.save_deploy_data(ds)
  end

  alias save_state prep_hibernation

  #####


end
##########

##
# Standalone unit test (recommended)
# Include at the end of the file as a hook to run a unit test of
# the code from the command line (e.g., "$ ruby  my_class.rb")
#
if __FILE__ == $0
  # Test code here
  doc = REXML::Document.new(File.new(Misc.find_plan("test_deploy.xml")))
  d = Deployment.new(doc.root)
  puts(d.dump)
  #d.execute
end
