#
# $Id: netclient.icn,v 1.5 2004/10/10 17:45:20 rparlett Exp $
#

package net

import util
link posix

$include "posix.icn"

#
# Common base class for a client class which holds a connection to a
# server and communicates using CRLF-terminated lines.
#
class NetClient:Connectable:Error:SetFields(server, port, connection, sbuff, timeout)
   # 
   # Fail and set the error code, and close the connection.
   # @p
   method error_and_close(a)
      error(a)
      close()
   end

   #
   # Set the timeout in ms to use.  If a figure of zero, or &null, is given (the default)
   # then no timeout will be used.
   #
   method set_timeout(timeout)
      return self.timeout := timeout
   end

   #
   # Open the connection.
   #
   method open()
      local s, t

      close() | fail
      s := server || ":" || port
      fire("Connecting", "Connecting to " || s)

      if not(connection := ::open(s, "n", timeout)) then {
         if &errno = 0 then
            return error("Timed out connecting to " || s)
         else
            return error("Couldn't connect to " || s)
      }

      sbuff := ""
      fire("Connected")
      return connection
   end

   #
   # Close the connection
   #
   method close()
      if \connection then {
         ::close(connection) | return error("Failed to close connection")
         fire("Closed")
         connection := &null
      }
      return
   end

   #
   # Set the server to use
   #
   method set_server(s)
      server := s
   end

   #
   # Set the port.
   #
   method set_port(n)
      port := n
   end

   #
   # @p
   method send(s)
      local t

      if /timeout | (timeout = 0) then
         return writes(connection, s)

      fcntl(connection, "F", "d")
      t := curr_time_millis()
      repeat {
         if writes(connection,s) then
            return
         if curr_time_millis() - t > timeout then
            return error("Send timed out")
         sleep(50)
      }
   end

   #
   # @p
   method recv(len)
      if /timeout | (timeout = 0) | (*select(connection, timeout) > 0) then
         return sysread(connection, len) | error("EOF encountered")
      else
         return error("Failed to select")
   end

   #
   # Write the given string to the connection.
   #
   method write_str(s)
      fire("Sending string", s)
      return send(s)
   end

   #
   # Write a single line to the connection, appending CRLF to the output stream.
   # @p
   method write_line(s)
      /s := ""
      fire("Sending line", s)
      return send(s || "\r\n") 
   end

   #
   # Read a single line from the connection, and return it.
   # @p
   method read_line()
      local line

      repeat {
         #
         # Look for a line and if found return it.
         #
         sbuff ? {
            if line := tab(find("\r\n")) then {
               move(2)
               sbuff := tab(0)
               fire("Read line", line)
               return line
            }
         }
         sbuff ||:= recv(1024) | fail
      }
   end

   #
   # Read up to len bytes on the connection.
   #
   method read_str(len)
      local t
      #
      # Use the line buffer if it has anything in it.
      #
      if *sbuff > 0 then {
         sbuff ? {
            t := move(len) | tab(0)
            sbuff := tab(0)
         }
      } else {
         t := recv(len) | fail
      }
      fire("Read string", t)
      return t
   end

   method set_one(attr, val)
      case attr of {
         "timeout": set_timeout(int_val(attr, val))
         "server": set_server(string_val(attr, val))
         "port": set_port(int_val(attr, val))
         default : field_error("Unknown attribute " || attr)
      }
   end

   initially()
      self.Connectable.initially()
      sbuff := ""
end