# encoding: ascii-8bit

# Copyright 2022 Ball Aerospace & Technologies Corp.
# All Rights Reserved.
#
# This program is free software; you can modify and/or redistribute it
# under the terms of the GNU Affero General Public License
# as published by the Free Software Foundation; version 3 with
# attribution addendums as found in the LICENSE.txt
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.

# Modified by OpenC3, Inc.
# All changes Copyright 2024, OpenC3, Inc.
# All Rights Reserved
#
# This file may also be used under the terms of a commercial license
# if purchased from OpenC3, Inc.

require 'fcntl'
require 'termios' # Requires ruby-termios gem
require 'timeout' # For Timeout::Error

module OpenC3
  # Serial driver for use on Posix serial ports found on UNIX based systems
  class PosixSerialDriver
    # (see SerialDriver#initialize)
    def initialize(port_name = '/dev/ttyS0',
                   baud_rate = 9600,
                   parity = :NONE,
                   stop_bits = 1,
                   write_timeout = 10.0,
                   read_timeout = nil,
                   flow_control = :NONE,
                   data_bits = 8)

      # Convert Baud Rate into Termios constant
      begin
        baud_rate = Object.const_get("Termios::B#{baud_rate}")
      rescue NameError
        raise(ArgumentError, "Invalid Baud Rate, Not Defined by Termios: #{baud_rate}")
      end

      # Verify Parameters
      raise(ArgumentError, "Invalid Data Bits: #{data_bits}") unless [5, 6, 7, 8].include?(data_bits)
      raise(ArgumentError, "Invalid parity: #{parity}") if parity and !SerialDriver::VALID_PARITY.include?(parity)
      raise(ArgumentError, "Invalid Stop Bits: #{stop_bits}") unless [1, 2].include?(stop_bits)

      @write_timeout = write_timeout
      @read_timeout = read_timeout

      parity = nil if parity == :NONE

      # Open the serial Port
      @handle = Kernel.open(port_name, File::RDWR | File::NONBLOCK)
      flags = @handle.fcntl(Fcntl::F_GETFL, 0)
      @handle.fcntl(Fcntl::F_SETFL, flags & ~File::NONBLOCK)
      @handle.extend Termios

      # Configure the serial Port
      tio = Termios.new_termios()
      iflags = 0
      iflags |= Termios::IGNPAR unless parity
      cflags = 0
      cflags |= Termios::CREAD # Enable receiver
      cflags |= Termios.const_get("CS#{data_bits}") # data bits
      cflags |= Termios::CLOCAL # Ignore Modem Control Lines
      cflags |= Termios::CSTOPB if stop_bits == 2
      cflags |= Termios::PARENB if parity
      cflags |= Termios::PARODD if parity == :ODD
      cflags |= Termios::CRTSCTS if flow_control == :RTSCTS
      tio.iflag = iflags
      tio.oflag = 0
      tio.cflag = cflags
      tio.lflag = 0
      tio.cc[Termios::VTIME] = 0
      tio.cc[Termios::VMIN] = 1
      tio.ispeed = baud_rate
      tio.ospeed = baud_rate
      @handle.tcflush(Termios::TCIOFLUSH)
      @handle.tcsetattr(Termios::TCSANOW, tio)

      @pipe_reader, @pipe_writer = IO.pipe
      @readers = [@handle, @pipe_reader]
    end

    # (see SerialDriver#close)
    def close
      if @handle
        # Close the serial Port
        @pipe_writer.write('.')
        @pipe_writer.close
        @handle.close
        @handle = nil
      end
    end

    # (see SerialDriver#closed?)
    def closed?
      if @handle
        false
      else
        true
      end
    end

    # (see SerialDriver#write)
    def write(data)
      num_bytes_to_send = data.length
      total_bytes_sent = 0
      bytes_sent = 0
      data_to_send = data

      loop do
        begin
          bytes_sent = @handle.write_nonblock(data_to_send)
        rescue Errno::EAGAIN, Errno::EWOULDBLOCK
          result = IO.fast_select(nil, [@handle], nil, @write_timeout)
          if result
            retry
          else
            raise Timeout::Error, "Write Timeout"
          end
        end
        total_bytes_sent += bytes_sent
        break if total_bytes_sent >= num_bytes_to_send

        data_to_send = data[total_bytes_sent..-1]
      end
    end

    # (see SerialDriver#read)
    def read
      begin
        data = @handle.read_nonblock(65535)
      rescue Errno::EAGAIN, Errno::EWOULDBLOCK
        begin
          read_ready, _ = IO.fast_select(@readers, nil, nil, @read_timeout)
        rescue IOError
          @pipe_reader.close unless @pipe_reader.closed?
          return ""
        end
        if read_ready
          if read_ready.include?(@pipe_reader)
            @pipe_reader.close unless @pipe_reader.closed?
            return ""
          else
            retry
          end
        else
          raise Timeout::Error, "Read Timeout"
        end
      end

      data
    end

    # (see SerialDriver#read_nonblock)
    def read_nonblock
      data = ''

      begin
        data = @handle.read_nonblock(65535)
      rescue Errno::EAGAIN, Errno::EWOULDBLOCK
        # Do Nothing
      end

      data
    end
  end # class PosixSerialDriver
end
