module ActiveType
  class TypeCaster

    def self.get(type)
      native_caster = if ActiveRecord::VERSION::STRING < '4.2'
        NativeCasters::DelegateToColumn.new(type)
      elsif ActiveRecord::VERSION::STRING < '5'
        NativeCasters::DelegateToRails4Type.new(type)
      else
        NativeCasters::DelegateToRails5Type.new(type)
      end
      new(type, native_caster)
    end

    def initialize(type, native_caster)
      @type = type
      @native_caster = native_caster
    end

    def type_cast_from_user(value)
      # For some reason, Rails defines additional type casting logic
      # outside the classes that have that responsibility.
      case @type
      when :integer
        cast_integer(value)
      when :timestamp, :datetime
        cast_time(value)
      else
        native_type_cast_from_user(value)
      end
    end

    def native_type_cast_from_user(value)
      @native_caster.type_cast_from_user(value)
    end

    private

    def cast_integer(value)
      if value == ''
        nil
      else
        native_type_cast_from_user(value)
      end
    end

    def cast_time(value)
      time = nil
      if ActiveRecord::Base.time_zone_aware_attributes
        if value.is_a?(String)
          time = Time.zone.parse(value) rescue nil
        end
        time ||= native_type_cast_from_user(value)
        if time
          if value.is_a?(String) && value !~ /[+\-Z][\d:]*$/
            # time was given without an explicit zone, so assume it was given in Time.zone
            ActiveSupport::TimeWithZone.new(nil, Time.zone, time)
          else
            time.in_time_zone rescue time
          end
        end
      else
        native_type_cast_from_user(value)
      end
    end

    module NativeCasters

      # Adapter for Rails 3.0 - 4.1.
      # In these versions, casting logic lives in ActiveRecord::ConnectionAdapters::Column
      class DelegateToColumn

        def initialize(type)
          # the Column initializer expects type as returned from the database, and
          # resolves them to our types
          # fortunately, for all types wie support, type.to_s is a valid sql_type
          sql_type = type.to_s
          @column = ActiveRecord::ConnectionAdapters::Column.new('foo', nil, sql_type)
        end

        def type_cast_from_user(value)
          @column.type_cast(value)
        end

      end

      # Adapter for Rails 4.2+.
      # In these versions, casting logic lives in subclasses of ActiveRecord::Type::Value
      class DelegateToRails4Type

        def initialize(type)
          # The specified type (e.g. "string") may not necessary match the
          # native type ("varchar") expected by the connection adapter.
          # PostgreSQL is one of these. Perform a translation if the adapter
          # supports it (but don't turn a mysql boolean into a tinyint).
          @active_record_type = ActiveRecord::ConnectionAdapters::AbstractAdapter.new(nil).lookup_cast_type(type)
        end

        def type_cast_from_user(value)
          @active_record_type.type_cast_from_user(value)
        end

      end

      # Adapter for Rails 5+.
      # In these versions, casting logic lives in subclasses of ActiveRecord::Type::Value
      class DelegateToRails5Type

        def initialize(type)
          @active_record_type = lookup(type)
        end

        def type_cast_from_user(value)
          @active_record_type.cast(value)
        end

        private

        def lookup(type)
          if type.respond_to?(:cast)
            type
          else
            ActiveRecord::Type.lookup(type, adapter: nil)
          end
        rescue ::ArgumentError
          ActiveRecord::Type::Value.new
        end

      end

    end

  end

end
