require 'date'
require 'rational' unless defined?(Rational)

module TZInfo
  
  # Methods to support different versions of Ruby.
  #
  # @private
  module RubyCoreSupport #:nodoc:
  
    # Use Rational.new! for performance reasons in Ruby 1.8.
    # This has been removed from 1.9, but Rational performs better.        
    if Rational.respond_to? :new!
      def self.rational_new!(numerator, denominator = 1)
        Rational.new!(numerator, denominator)
      end
    else
      def self.rational_new!(numerator, denominator = 1)
        Rational(numerator, denominator)
      end
    end
    
    # Ruby 1.8.6 introduced new! and deprecated new0.
    # Ruby 1.9.0 removed new0.
    # Ruby trunk revision 31668 removed the new! method.
    # Still support new0 for better performance on older versions of Ruby (new0 indicates
    # that the rational has already been reduced to its lowest terms).
    # Fallback to jd with conversion from ajd if new! and new0 are unavailable.
    if DateTime.respond_to? :new!
      def self.datetime_new!(ajd = 0, of = 0, sg = Date::ITALY)
        DateTime.new!(ajd, of, sg)
      end
    elsif DateTime.respond_to? :new0
      def self.datetime_new!(ajd = 0, of = 0, sg = Date::ITALY)
        DateTime.new0(ajd, of, sg)
      end
    else
      HALF_DAYS_IN_DAY = rational_new!(1, 2)

      def self.datetime_new!(ajd = 0, of = 0, sg = Date::ITALY)
        # Convert from an Astronomical Julian Day number to a civil Julian Day number.
        jd = ajd + of + HALF_DAYS_IN_DAY
        
        # Ruby trunk revision 31862 changed the behaviour of DateTime.jd so that it will no
        # longer accept a fractional civil Julian Day number if further arguments are specified.
        # Calculate the hours, minutes and seconds to pass to jd.
        
        jd_i = jd.to_i
        jd_i -= 1 if jd < 0
        hours = (jd - jd_i) * 24
        hours_i = hours.to_i
        minutes = (hours - hours_i) * 60
        minutes_i = minutes.to_i
        seconds = (minutes - minutes_i) * 60
        
        DateTime.jd(jd_i, hours_i, minutes_i, seconds, of, sg)
      end
    end
    
    # DateTime in Ruby 1.8.6 doesn't consider times within the 60th second to be
    # valid. When attempting to specify such a DateTime, subtract the fractional
    # part and then add it back later
    if Date.respond_to?(:valid_time?) && !Date.valid_time?(0, 0, rational_new!(59001, 1000)) # 0:0:59.001
      def self.datetime_new(y=-4712, m=1, d=1, h=0, min=0, s=0, of=0, sg=Date::ITALY)
        if !s.kind_of?(Integer) && s > 59
          dt = DateTime.new(y, m, d, h, min, 59, of, sg)
          dt + (s - 59) / 86400
        else
          DateTime.new(y, m, d, h, min, s, of, sg)
        end
      end
    else
      def self.datetime_new(y=-4712, m=1, d=1, h=0, min=0, s=0, of=0, sg=Date::ITALY)
        DateTime.new(y, m, d, h, min, s, of, sg)
      end    
    end    
    
    # Returns true if Time on the runtime platform supports Times defined
    # by negative 32-bit timestamps, otherwise false.
    begin
      Time.at(-1)
      Time.at(-2147483648)
      
      def self.time_supports_negative
        true
      end
    rescue ArgumentError
      def self.time_supports_negative
        false
      end
    end
    
    # Returns true if Time on the runtime platform supports Times defined by
    # 64-bit timestamps, otherwise false.
    begin
      Time.at(-2147483649)
      Time.at(2147483648)
      
      def self.time_supports_64bit
        true
      end
    rescue RangeError
      def self.time_supports_64bit
        false
      end
    end
    
    # Return the result of Time#nsec if it exists, otherwise return the
    # result of Time#usec * 1000.
    if Time.method_defined?(:nsec)
      def self.time_nsec(time)
        time.nsec
      end
    else
      def self.time_nsec(time)
        time.usec * 1000
      end
    end
    
    # Call String#force_encoding if this version of Ruby has encoding support
    # otherwise treat as a no-op.
    if String.method_defined?(:force_encoding)
      def self.force_encoding(str, encoding)
        str.force_encoding(encoding)
      end
    else
      def self.force_encoding(str, encoding)
        str
      end
    end
    
    
    # Wrapper for File.open that supports passing hash options for specifying
    # encodings on Ruby 1.9+. The options are ignored on earlier versions of
    # Ruby.
    if RUBY_VERSION =~ /\A1\.[0-8]\./
      def self.open_file(file_name, mode, opts, &block)
        File.open(file_name, mode, &block)
      end
    elsif RUBY_VERSION =~ /\A1\.9\./
      def self.open_file(file_name, mode, opts, &block)
        File.open(file_name, mode, opts, &block)
      end
    else
      # Evaluate method as a string because **opts isn't valid syntax prior to
      # Ruby 2.0.
      eval(<<-EOF
        def self.open_file(file_name, mode, opts, &block)
          File.open(file_name, mode, **opts, &block)
        end
      EOF
      )
    end


    # Object#untaint is deprecated and becomes a no-op in Ruby >= 2.7. It has
    # been removed from Ruby 3.2.
    if !Object.new.respond_to?(:untaint) || RUBY_VERSION =~ /\A(\d+)\.(\d+)(?:\.|\z)/ && ($1 == '2' && $2.to_i >= 7 || $1.to_i >= 3)
      # Returns the supplied `Object`
      #
      # @param o [Object] the `Object` to untaint.
      # @return [Object] `o`.
      def self.untaint(o)
        o
      end
    else
      # Untaints and returns the supplied `Object`.
      #
      # @param o [Object] the `Object` to untaint.
      # @return [Object] `o`.
      def self.untaint(o)
        o.untaint
      end
    end
  end
end
