#!/usr/local/bin/ruby
=begin
 ******************************************************************************
 * Copyright 1990-2010 MBARI
 * MBARI Proprietary Information. All rights reserved.
 ******************************************************************************
 * Summary  : Benthic Rover remote client
 * Filename : remote_client.rb
 * Author   : Henthorn
 * Project  : Benthic Rover
 * Version  : 1
 * Created  : March 2010
 * Modified : 
 ******************************************************************************
=end

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

#

# Now use subdirectory name for remaining rover-code require statements.
# Example:
# require 'utils/datalog'
#

require 'utils/datalog'

##########
# RemoteClient for the Rover supervisor. Intended to be used with a
# RemoteServer process for remote communications conduit
# to the rover control system. Designed to be used inside a remote agent
# process to listen for messages from multiple sources (modem, control
# panel). Use @connected to check connection (will be nil if there is no
# connection.
#
class RemoteClient

  attr_reader :host, :port, :client, :connected, :recv_ok

  #####
  # Initialize with port and hostname
  #
  def initialize(port, host='localhost')
    @log = nil
    @host = host
    @port = port
    @client = nil
    @recv_ok = false
    @connected = nil
  end
  #####

  #####
  # Initialize the client. Return nil if server not responding.
  #
  def init(logfile="#{ENV['ROVER_HOME']}/logs/client.log")
    @log = DataLog.new(logfile)
    @client = UDPSocket.new()
    self.open()
  end
  #####


  #####
  # Open the client connection. Return nil if no server.
  #
  def open()
    begin
      self.init() unless @client
      @connected = @client.connect(@host, @port)
    rescue Exception => e
      # Probably no server (supervisor not running)
      @log.write("RemoteClient(#{@port}),No server?")
      @connected = nil
    end
    return @connected
  end

  #####
  # Close the client connection
  #
  def close()
    if (@client)
      @client.shutdown
      @client = @connected = nil
    end
  end
  #####


  #####
  # Send a string message to the server. Returns nil if
  # the message was not sent. Otherwise returns the number
  # of bytes sent.
  # 
  def send_msg(msg)
    return nil unless (msg)

    unless (@connected)
      self.open()
      return nil unless (@connected)
    end

    @log.write("RemoteClient(#{@port}),Send", "#{msg}")
    begin
      @client.send(msg,0)
    rescue Exception => e
      # Supervisor probably not running yet
      @log.write("RemoteClient(#{@port})",e)
      return nil
    end
  end
  #####

  #####
  # Get a message from the port. Blocking read on the port.
  # Returns "external" message string to the caller, or nil.
  # The caller should just ignore nil messages and call
  # get_msg() again.
  #
  def get_msg(timeout=0.1)

    ##
    # Initialize the UDP connection if needed
    #
    unless (@connected)
      self.open()
    end
    return nil unless (@connected)
    return nil unless (self.data?(timeout))

    ##
    # Read the message packet
    #
    begin
      msg = @client.recvfrom(200)
      @recv_ok = true
    rescue Exception => e
      if (@recv_ok) # Log only when going from "ok" to "not ok" 
        @log.write("RemoteClient(#{@port})",e)
      end 
      @recv_ok = false
      return nil
    end

    ##
    # A nil value means the client disconnected abruptly
    #
    unless (msg)
      @log.write("RemoteClient(#{@port}),disconnect")
      @connected = nil
      return nil
    end

    ##
    # Process 'internal' messages (client-server handshaking)
    # Other messages are returned for the caller to handle.
    #
    @log.write("RemoteClient(#{@port}),Receive", "#{msg}")
    case msg[0].strip.downcase

    # Internal message: Server ping. Acknowledge
    #
    when "ping"
      send_msg("ping ack", msg[1])
      return nil

    when "ping ack"
      return nil

    # Internal message: Server is closing it's session
    #
    when "close"
      #self.close
      return msg

    # External messages are returned
    #
    else
      return msg
    end
  end
  #####


  #####
  # Read a message and compare it to the given string.
  # Return true on a positive match. Wait 10 seconds for
  # the message by default.
  #
  def confirm(match, timeout=5)
    if (msg = self.get_msg(timeout))
      if (msg[0].strip.downcase == match.strip.downcase)
        return true
      end
    end
    false
  end
  #####


  #####
  # Return true is data is pending on the port. Otherwise false.
  # Default timeout on the select is 0.5 seconds.
  #
  def data?(t_o = 0.5) 
    return false unless (@client)

    d = IO.select([@client], nil, nil, t_o)
    return false unless (d)
    return true
  end
  #####

end


#####
# Test method. Use to run standard test of MyClass from
# a ruby script.
# 
#def MyClass.test
#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 including the command line options
# (e.g., "$ ruby my_class.rb --sim").
#
if __FILE__ == $0
  # Test code here. Using the MyClass.test function is often enough
  # MyClass.test

  client = RemoteClient.new(6601) # Hookup with executive
  client.open()

  print("exec$ ")
  $stdout.flush
  begin
    while(true)
      # echo all messages from executive

      unless (client.connected)
        puts("Not connected to server")
      else
        msg = nil
        if ((msg = client.get_msg(0.5)))
          puts("\t<#{msg[0]}>")
        end
      end

      # read command from stdin
      if (IO.select([$stdin], nil, nil,0.5))
        msg = gets.strip!
        if (msg.downcase == "connect")
          client.open()
        elsif (msg.downcase == "exit")
          exit
        end

        # send to server if we're connected
        #
        unless (client.connected)
          puts("Not connected to server!")
        else
          client.send_msg(msg)
        end
        print("exec$ ")
        $stdout.flush
      end
    end
  end
end
