require 'active_record/connection_adapters/abstract_adapter'

module ActiveRecord
  class Base
    def self.jdbc_connection(config)
      ConnectionAdapters::JdbcAdapter.new(ConnectionAdapters::JdbcConnection.new(config), logger, config)
    end
  end

  module ConnectionAdapters
    module Jdbc
      require 'java'
      include_class 'java.sql.DriverManager'
      include_class 'java.sql.Statement'
      include_class 'java.sql.Types'
    end

    # I want to use JDBC's DatabaseMetaData#getTypeInfo to choose the best native types to
    # use for ActiveRecord's Adapter#native_database_types in a database-independent way,
    # but apparently a database driver can return multiple types for a given
    # java.sql.Types constant.  So this type converter uses some heuristics to try to pick
    # the best (most common) type to use.  It's not great, it would be better to just
    # delegate to each database's existin AR adapter's native_database_types method, but I
    # wanted to try to do this in a way that didn't pull in all the other adapters as
    # dependencies.  Suggestions appreciated.
    class JdbcTypeConverter
      # The basic ActiveRecord types, mapped to an array of procs that are used to #select
      # the best type.  The procs are used as selectors in order until there is only one
      # type left.  If all the selectors are applied and there is still more than one
      # type, an exception will be raised.
      AR_TO_JDBC_TYPES = {
        :string      => [ proc {|r| Jdbc::Types::VARCHAR == r['data_type']},
                          proc {|r| r['type_name'] =~ /^varchar/i} ],
        :text        => [ proc {|r| [Jdbc::Types::LONGVARCHAR, Jdbc::Types::CLOB].include?(r['data_type'])},
                          proc {|r| r['type_name'] =~ /^(text|clob)/i} ],
        :integer     => [ proc {|r| Jdbc::Types::INTEGER == r['data_type']},
                          proc {|r| r['type_name'] =~ /^integer/i} ],
        :float       => [ proc {|r| [Jdbc::Types::FLOAT,Jdbc::Types::DOUBLE].include?(r['data_type'])},
                          proc {|r| r['type_name'] =~ /^float/i},
                          proc {|r| r['type_name'] =~ /^double$/i} ],
        :datetime    => [ proc {|r| Jdbc::Types::TIMESTAMP == r['data_type']},
                          proc {|r| r['type_name'] =~ /^datetime/i} ],
        :timestamp   => [ proc {|r| Jdbc::Types::TIMESTAMP == r['data_type']},
                          proc {|r| r['type_name'] =~ /^timestamp/i},
                          proc {|r| r['type_name'] =~ /^datetime/i} ],
        :time        => [ proc {|r| Jdbc::Types::TIME == r['data_type']} ],
        :date        => [ proc {|r| Jdbc::Types::DATE == r['data_type']} ],
        :binary      => [ proc {|r| Jdbc::Types::LONGVARBINARY == r['data_type']},
                          proc {|r| r['type_name'] =~ /^blob/i} ],
        :boolean     => [ proc {|r| Jdbc::Types::TINYINT == r['data_type']} ]
      }

      def initialize(types)
        @types = types
      end

      def choose_best_types
        type_map = {}
        AR_TO_JDBC_TYPES.each_key do |k|
          typerow = choose_type(k)
          type_map[k] = { :name => typerow['type_name']  }
          type_map[k][:limit] = typerow['precision'] if [:integer,:string].include?(k)
          type_map[k][:limit] = 1 if k == :boolean
        end
        type_map
      end

      def choose_type(ar_type)
        procs = AR_TO_JDBC_TYPES[ar_type]
        types = @types
        procs.each do |p|
          new_types = types.select(&p)
          return new_types.first if new_types.length == 1
          types = new_types if new_types.length > 0
        end
        raise "unable to choose type from: #{types.collect{|t| t['type_name']}.inspect}"
      end
    end

    class JdbcDriver
      def self.load(driver)
        driver_class_const = (driver[0...1].capitalize + driver[1..driver.length]).gsub(/\./, '_')
        unless Jdbc.const_defined?(driver_class_const)
          Jdbc.module_eval do
            include_class(driver) {|p,c| driver_class_const }
          end
          driver_class = Jdbc.const_get(driver_class_const.to_sym)
          Jdbc::DriverManager.registerDriver(driver_class.new)
        end
      end
    end

    class JdbcConnection
      def initialize(config)
        config = config.symbolize_keys
        driver = config[:driver].to_s
        user   = config[:username].to_s
        pass   = config[:password].to_s
        url    = config[:url].to_s

        unless driver && url
          raise ArgumentError, "jdbc adapter requires driver class and url"
        end

        JdbcDriver.load(driver)
        @connection = Jdbc::DriverManager.getConnection(url, user, pass)
        set_native_database_types
      end

      def set_native_database_types
        types = unmarshal_result(@connection.getMetaData.getTypeInfo)
        @native_types = {
          # TODO: this is copied from MySQL -- figure out how to
          # generalize the primary key type
          :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
        }
        @native_types.update(JdbcTypeConverter.new(types).choose_best_types)
      end

      def native_database_types
        @native_types
      end

      def columns(table_name, name = nil)
        metadata = @connection.getMetaData
        results = metadata.getColumns(nil, nil, table_name, nil)
        columns = []
        unmarshal_result(results).each do |col|
          columns << ActiveRecord::ConnectionAdapters::Column.new(col['column_name'], col['column_def'],
              "#{col['type_name']}(#{col['column_size']})", col['is_nullable'] != 'NO')
        end
        columns
      end

      def tables
        metadata = @connection.getMetaData
        results = metadata.getTables(nil, nil, nil, nil)
        unmarshal_result(results).collect {|t| t['table_name']}
      end

      def execute_insert(sql, pk)
        stmt = @connection.createStatement
        stmt.executeUpdate(sql, Jdbc::Statement::RETURN_GENERATED_KEYS)
        row = unmarshal_result(stmt.getGeneratedKeys)
        row.first && row.first.values.first
      ensure
        stmt.close
      end

      def execute_update(sql)
        stmt = @connection.createStatement
        stmt.executeUpdate(sql)
      ensure
        stmt.close
      end

      def execute_query(sql)
        stmt = @connection.createStatement
        unmarshal_result(stmt.executeQuery(sql))
      ensure
        stmt.close
      end

      private
      def unmarshal_result(resultset)
        metadata = resultset.getMetaData
        column_count = metadata.getColumnCount
        column_names = ['']
        column_types = ['']
        column_scale = ['']

        1.upto(column_count) do |i|
          column_names << metadata.getColumnName(i)
          column_types << metadata.getColumnType(i)
          column_scale << metadata.getScale(i)
        end

        results = []

        while resultset.next
          row = {}
          1.upto(column_count) do |i|
            row[column_names[i].downcase] = convert_jdbc_type_to_ruby(i, column_types[i], column_scale[i], resultset)
          end
          results << row
        end

        results
      end

      def to_ruby_time(java_date)
        tm = java_date.getTime
        Time.at(tm / 1000, (tm % 1000) * 1000)
      end

      def convert_jdbc_type_to_ruby(row, type, scale, resultset)
        if scale != 0
          decimal = resultset.getString(row)
          decimal.to_f
        else
          case type
          when Jdbc::Types::CHAR, Jdbc::Types::VARCHAR, Jdbc::Types::LONGVARCHAR
            resultset.getString(row)
          when Jdbc::Types::SMALLINT, Jdbc::Types::INTEGER, Jdbc::Types::NUMERIC, Jdbc::Types::BIGINT
            resultset.getInt(row)
          when Jdbc::Types::BIT, Jdbc::Types::BOOLEAN
            resultset.getBoolean(row)
          when Jdbc::Types::TIMESTAMP
            to_ruby_time(resultset.getTimestamp(row))
          when Jdbc::Types::TIME
            to_ruby_time(resultset.getTime(row))
          when Jdbc::Types::DATE
            to_ruby_time(resultset.getDate(row))
          else
            types = Jdbc::Types.constants
            name = types.find {|t| Jdbc::Types.const_get(t.to_sym) == type}
            raise "jdbc_adapter: type #{name} not supported yet"
          end
        end
      end
    end

    module JdbcAdapterMethods
      def initialize(connection, logger, config)
        super(connection, logger)
        @config = config
      end

      def adapter_name #:nodoc:
        'JDBC'
      end

      def supports_migrations?
        true
      end

      def native_database_types #:nodoc
        @connection.native_database_types
      end

      def active?
        true
      end

      def reconnect!
        @connection.close rescue nil
        @connection = JdbcConnection.new(@config)
      end

      def select_all(sql, name = nil)
        select(sql, name)
      end

      def select_one(sql, name = nil)
        select(sql, name).first
      end

      def execute(sql, name = nil)
        log(sql, name)
        if sql =~ /^select/i
          @connection.execute_query(sql)
        else
          @connection.execute_update(sql)
        end
      end

      alias :update :execute
      alias :delete :execute

      def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
        log(sql, name)
        id = @connection.execute_insert(sql, pk)
        id ||= id_value
        id
      end

      def columns(table_name, name = nil)
        @connection.columns(table_name)
      end

      def tables
        @connection.tables
      end

      private
      def select(sql, name)
        log(sql, name)
        @connection.execute_query(sql)
      end
    end

    class JdbcAdapter < AbstractAdapter
      include JdbcAdapterMethods
    end

  end
end
