# frozen_string_literal: true

require 'active_record/connection_adapters/column'

module ActiveRecord::ConnectionAdapters
  module Jdbc
    # Type casting methods taken from AR 4.1's Column class.
    # @private Simply to quickly "hack-in" 4.2 compatibility.
    module TypeCast
      TRUE_VALUES = Column::TRUE_VALUES if Column.const_defined?(:TRUE_VALUES)
      FALSE_VALUES = if defined?(ActiveModel::Type::Boolean::FALSE_VALUES)
        ActiveModel::Type::Boolean::FALSE_VALUES
      else
        Column::FALSE_VALUES
      end

      #module Format
      ISO_DATE = if defined?(ActiveModel::Type::Date::ISO_DATE)
        ActiveModel::Type::Date::ISO_DATE
      else
        Column::Format::ISO_DATE
      end

      ISO_DATETIME = if defined?(ActiveModel::Type::Helpers::TimeValue::ISO_DATETIME)
        ActiveModel::Type::Helpers::TimeValue::ISO_DATETIME
      else
        Column::Format::ISO_DATETIME
      end
      #end

      # Used to convert from BLOBs to Strings
      def binary_to_string(value)
        value
      end

      def value_to_date(value)
        if value.is_a?(String)
          return nil if value.empty?
          fast_string_to_date(value) || fallback_string_to_date(value)
        elsif value.respond_to?(:to_date)
          value.to_date
        else
          value
        end
      end

      def string_to_time(string)
        return string unless string.is_a?(String)
        return nil if string.empty?
        return string if string =~ /^-?infinity$/.freeze

        fast_string_to_time(string) || fallback_string_to_time(string)
      end

      def string_to_dummy_time(string)
        return string unless string.is_a?(String)
        return nil if string.empty?

        dummy_time_string = "2000-01-01 #{string}"

        fast_string_to_time(dummy_time_string) || begin
          time_hash = Date._parse(dummy_time_string)
          return nil if time_hash[:hour].nil?
          new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
        end
      end

      # convert something to a boolean
      def value_to_boolean(value)
        if value.is_a?(String) && value.empty?
          nil
        else
          TRUE_VALUES.include?(value)
        end
      end if const_defined?(:TRUE_VALUES) # removed on AR 5.0

      # convert something to a boolean
      def value_to_boolean(value)
        if value.is_a?(String) && value.empty?
          nil
        else
          ! FALSE_VALUES.include?(value)
        end
      end unless const_defined?(:TRUE_VALUES)

      # Used to convert values to integer.
      # handle the case when an integer column is used to store boolean values
      def value_to_integer(value)
        case value
        when TrueClass, FalseClass
          value ? 1 : 0
        else
          value.to_i rescue nil
        end
      end

      # convert something to a BigDecimal
      def value_to_decimal(value)
        # Using .class is faster than .is_a? and
        # subclasses of BigDecimal will be handled
        # in the else clause
        if value.class == BigDecimal
          value
        elsif value.respond_to?(:to_d)
          value.to_d
        else
          value.to_s.to_d
        end
      end

      protected
        # '0.123456' -> 123456
        # '1.123456' -> 123456
        def microseconds(time)
          time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
        end

        def new_date(year, mon, mday)
          if year && year != 0
            Date.new(year, mon, mday) rescue nil
          end
        end

        def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
          # Treat 0000-00-00 00:00:00 as nil.
          return nil if year.nil? || (year == 0 && mon == 0 && mday == 0)

          if offset
            time = Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
            return nil unless time

            time -= offset
            ActiveRecord.default_timezone == :utc ? time : time.getlocal
          else
            timezone = ActiveRecord.default_timezone
            Time.public_send(timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
          end
        end

        def fast_string_to_date(string)
          if string =~ ISO_DATE
            new_date $1.to_i, $2.to_i, $3.to_i
          end
        end

        # Doesn't handle time zones.
        def fast_string_to_time(string)
          if string =~ ISO_DATETIME
            microsec = ($7.to_r * 1_000_000).to_i
            new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
          end
        end

        def fallback_string_to_date(string)
          new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
        end

        def fallback_string_to_time(string)
          time_hash = Date._parse(string)
          time_hash[:sec_fraction] = microseconds(time_hash)
          time_hash[:year] *= -1 if time_hash[:zone] == 'BC'

          new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
        end

    end
  end
end
