# 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 2022, OpenC3, Inc.
# All Rights Reserved
#
# This file may also be used under the terms of a commercial license 
# if purchased from OpenC3, Inc.

require 'openc3/core_ext/kernel'
require 'openc3/core_ext/openc3_io'

class IO
  include OpenC3IO

  # Initial timeout to call IO.select with. Timeouts are increased by doubling
  # this value until the SELET_MAX_TIMEOUT value is reached.
  SELECT_BASE_TIMEOUT = 0.0004
  # The maximum timeout at which point we call IO.select with whatever
  # remaining timeout is left.
  SELECT_MAX_TIMEOUT = 0.016

  class << self
    # Alias the original IO.select method
    alias_method :__select__, :select

    # On Windows the IO.select function (when called with no timeout) takes
    # a minimum of 10 msec to return, even if one of the IO objects is
    # ready to read/write sooner than that.
    #
    # This method is identical to IO.select but instead of calling IO.select with
    # the full timeout, it calls IO.select with a small timeout and then
    # doubles the timeout twice until eventually it calls IO.select with the
    # remaining passed in timeout value.
    #
    # @param read_sockets [Array<IO>] IO objects to wait to be ready to read
    # @param write_sockets [Array<IO>] IO objects to wait to be ready to write
    # @param error_array [Array<IO>] IO objects to wait for exceptions
    # @param timeout [Numeric] Number of seconds to wait
    def fast_select(read_sockets = nil, write_sockets = nil, error_array = nil, timeout = nil)
      # Always try a zero timeout first
      current_timeout = SELECT_BASE_TIMEOUT
      total_timeout = 0.0

      while true
        begin
          result = IO.__select__(read_sockets, write_sockets, error_array, current_timeout)
        # All OS errors are subclassed to SystemCallError as Errno::<Subclass>
        # See https://ruby-doc.org/core-3.1.0/Errno.html
        rescue SystemCallError
          return nil
        end
        return result if result or current_timeout.nil?
        return nil if timeout and total_timeout >= timeout

        if current_timeout <= 0.0001
          # Always try the base timeout next
          current_timeout = SELECT_BASE_TIMEOUT
          total_timeout = SELECT_BASE_TIMEOUT
        else
          # Then start doubling the timeout
          current_timeout = current_timeout * 2

          # Until it is bigger than our max timeout
          if current_timeout >= SELECT_MAX_TIMEOUT
            if timeout
              # Block for the remaining requested timeout
              current_timeout = timeout - total_timeout
              total_timeout = timeout
            else
              # Or block forever
              current_timeout = nil
            end
          else
            # Or it is bigger than the given timeout
            if timeout and current_timeout >= timeout
              # Block for the remaining requested timeout
              current_timeout = timeout - total_timeout
              total_timeout = timeout
            else
              # Up our total time in select
              total_timeout += current_timeout
            end
            if timeout and total_timeout > timeout
              # Block for the remaining requested timeout
              current_timeout = timeout - total_timeout
              total_timeout = timeout
            end
          end
          return nil if current_timeout and current_timeout < 0
        end
      end # while true
    end # fast_select

    # For non-windows systems, leave IO.select alone, but for windows
    # monkey-patch IO.select to use our fast-select implementation.
    if Kernel.is_windows?
      def select(read_sockets = nil, write_sockets = nil, error_array = nil, timeout = nil)
        return fast_select(read_sockets, write_sockets, error_array, timeout)
      end
    end

    # @param read_sockets [Array<IO>] IO objects to wait to be ready to read
    # @param timeout [Numeric] Number of seconds to wait
    def fast_read_select(read_sockets, timeout)
      return fast_select(read_sockets, nil, nil, timeout)
    end

    # @param write_sockets [Array<IO>] IO objects to wait to be ready to write
    # @param timeout [Numeric] Number of seconds to wait
    def fast_write_select(write_sockets, timeout)
      return fast_select(nil, write_sockets, nil, timeout)
    end
  end

  # Alias the original close method
  alias_method :__close__, :close

  # Patch the close method so that it won't raise any exceptions
  def close
    __close__
  rescue
  end
end
