require 'data_objects'
if RUBY_PLATFORM =~ /java/
  require 'do_jdbc'
  require 'java'
  require 'do_jdbc/sqlserver'   # the JDBC driver, packaged as a gem
else # MRI and Ruby 1.9
  require 'dbi' unless defined?(DBI)
  require 'dbd_odbc_patch'      # a monkey patch for DNS-less connections
  #require 'core_ext/dbi'       # a hack to work around ODBC millisecond handling in Timestamps
end

require 'bigdecimal'
require 'date'
require 'base64'
require 'do_sqlserver/do_sqlserver' if RUBY_PLATFORM =~ /java/
require 'do_sqlserver/version'
# JDBC driver has transactions implementation in Java
require 'do_sqlserver/transaction' if RUBY_PLATFORM !~ /java/

if RUBY_PLATFORM !~ /java/
  module DataObjects
    module SqlServer
      Mode = :odbc
#      The ADO mode requires a very old DBI to work with the unmaintained DBI::ADO adapter. Don't enable this unless you fix that.
#      Mode = begin
#          require "ADO"
#          :ado
#        rescue LoadError => e
#          :odbc
#        end

      class Connection < DataObjects::Connection
        def initialize uri
          # REVISIT: Allow uri.query to modify this connection's mode?
          #host = uri.host.blank? ? "localhost" : uri.host
          host = uri.host.blank? ? nil : uri.host
          user = uri.user || "sa"
          password = uri.password || ""
          path = uri.path.sub(%r{^/*}, '')
          port = uri.port || "1433"
          if Mode == :ado
            connection_string = "DBI:ADO:Provider=SQLOLEDB;Data Source=#{host};Initial Catalog=#{path};User ID=#{user};Password=#{password};"
          else
            # FIXME: Cannot get a DNS-less configuration without freetds.conf to
            # connect successfully, i.e.:
            # connection_string = "DBI:ODBC:DRIVER=FreeTDS;SERVER=#{host};DATABASE=#{path};TDS_Version=5.0;Port=#{port}"
            #
            # Currently need to setup a dataserver entry in freetds.conf (if
            # using MacPorts, full path is /opt/local/etc/freetds/freetds.conf):
            #
            # [sqlserver]
            #   host = hostname
            #   port = 1433
            #   instance = SQLEXPRESS
            #   tds version = 8.0
            #
            connection_string = "DBI:ODBC:DRIVER=FreeTDS;SERVERNAME=sqlserver;DATABASE=#{path};"
          end

          begin
            @connection = DBI.connect(connection_string, user, password)
          rescue DBI::DatabaseError => e
            # Place to debug connection failures
            raise
          end

          @encoding = uri.query && uri.query["encoding"] || "utf8"

          set_date_format = create_command("SET DATEFORMAT YMD").execute_non_query
          options_reader = create_command("DBCC USEROPTIONS").execute_reader
          while options_reader.next!
            key, value = *options_reader.values
            value = options_reader.values
            case key
            when "textsize"                     # "64512"
            when "language"                     # "us_english", "select * from master..syslanguages" for info
            when "dateformat"                   # "ymd"
            when "datefirst"                    # "7" = Sunday, first day of the week, change with "SET DATEFIRST"
            when "quoted_identifier"            # "SET"
            when "ansi_null_dflt_on"            # "SET"
            when "ansi_defaults"                # "SET"
            when "ansi_warnings"                # "SET"
            when "ansi_padding"                 # "SET"
            when "ansi_nulls"                   # "SET"
            when "concat_null_yields_null"      # "SET"
            else
            end
          end
        end

        def using_socket?
          # This might be an unnecessary feature dragged from the mysql driver
          raise "Not yet implemented"
        end

        def character_set
          @encoding
        end

        def dispose
          @connection.disconnect
          true
        rescue
          false
        end

        def raw
          @connection
        end
      end

      class Command < DataObjects::Command
        # Theoretically, SCOPE_IDENTIY should be preferred, but there are cases where it returns a stale ID, and I don't know why.
        #IDENTITY_ROWCOUNT_QUERY = 'SELECT SCOPE_IDENTITY(), @@ROWCOUNT'
        IDENTITY_ROWCOUNT_QUERY = 'SELECT @@IDENTITY, @@ROWCOUNT'

        attr_reader :types

        def set_types *t
          @types = t.flatten
        end

        def execute_non_query *args
          DataObjects::SqlServer.check_params @text, args
          begin
            handle = @connection.raw.execute(@text, *args)
          rescue DBI::DatabaseError => e
            handle = @connection.raw.handle
            handle.finish if handle && handle.respond_to?(:finish) && !handle.finished?
            DataObjects::SqlServer.raise_db_error(e, @text, args)
          end
          handle.finish if handle && handle.respond_to?(:finish) && !handle.finished?

          # Get the inserted ID and the count of affected rows:
          inserted_id, row_count = nil, nil
          if (handle = @connection.raw.execute(IDENTITY_ROWCOUNT_QUERY))
            row1 = Array(Array(handle)[0])
            inserted_id, row_count = row1[0].to_i, row1[1].to_i
            handle.finish
          end
          Result.new(self, row_count, inserted_id)
        end

        def execute_reader *args
          DataObjects::SqlServer.check_params @text, args
          massage_limit_and_offset args
          begin
            handle = @connection.raw.execute(@text, *args)
          rescue DBI::DatabaseError => e
            handle = @connection.raw.handle
            DataObjects::SqlServer.raise_db_error(e, @text, args)
            handle.finish if handle && handle.respond_to?(:finish) && !handle.finished?
          rescue
            handle = @connection.raw.handle
            handle.finish if handle && handle.respond_to?(:finish) && !handle.finished?
            raise
          end
          Reader.new(self, handle)
        end

      private
        def massage_limit_and_offset args
          @text.sub!(%r{SELECT (.*) ORDER BY (.*) LIMIT ([?0-9]*)( OFFSET ([?0-9]*))?}) {
            what, order, limit, offset = $1, $2, $3, $5

            # LIMIT and OFFSET will probably be set by args. We need exact values, so must
            # do substitution here, and remove those args from the array. This is made easier
            # because LIMIT and OFFSET are always the last args in the array.
            offset = args.pop if offset == '?'
            limit = args.pop if limit == '?'
            offset = offset.to_i
            limit = limit.to_i

            # Reverse the sort direction of each field in the ORDER BY:
            rev_order = order.split(/, */).map{ |f|
              f =~ /(.*) DESC *$/ ? $1 : f+" DESC"
            }*", "

            "SELECT TOP #{limit} * FROM (SELECT TOP #{offset+limit} #{what} ORDER BY #{rev_order}) ORDER BY #{order}"
          }
        end
      end

      class Result < DataObjects::Result
      end

      # REVISIT: There is no data type conversion happening here. That will make DataObjects sad.
      class Reader < DataObjects::Reader
        def initialize command, handle
          @command, @handle = command, handle
          return unless @handle

          @fields = handle.column_names

          # REVISIT: Prefetch results like AR's adapter does. ADO is a bit strange about handle lifetimes, don't move this until you can test it.
          @rows = []
          types = @command.types
          if types && types.size != @fields.size
            @handle.finish if @handle && @handle.respond_to?(:finish) && !@handle.finished?
            raise ArgumentError, "Field-count mismatch. Expected #{types.size} fields, but the query yielded #{@fields.size}"
          end
          @handle.each do |row|
            field = -1
            @rows << row.map do |value|
              field += 1
              next value unless types
              if (t = types[field]) == Integer
                Integer(value)
              elsif t == Float
                Float(value)
              else
                t.new(value)
              end
            end
          end
          @handle.finish if @handle && @handle.respond_to?(:finish) && !@handle.finished?
          @current_row = -1
        end

        def close
          if @handle
            @handle.finish if  @handle.respond_to?(:finish) && !@handle.finished?
            @handle = nil
            true
          else
            false
          end
        end

        def next!
          (@current_row += 1) < @rows.size
        end

        def values
          raise StandardError.new("First row has not been fetched") if @current_row < 0
          raise StandardError.new("Last row has been processed") if @current_row >= @rows.size
          @rows[@current_row]
        end

        def fields
          @fields
        end

        def field_count
          @fields.size
        end

        # REVISIT: This is being deprecated
        def row_count
          @rows.size
        end
      end

    private
      def self.check_params cmd, args
        actual = args.size
        expected = param_count(cmd)
        raise ArgumentError.new("Binding mismatch: #{actual} for #{expected}") if actual != expected
      end

      def self.raise_db_error(e, cmd, args)
        msg = e.to_str
        case msg
        when /Too much parameters/, /No data found/
          #puts "'#{cmd}' (#{args.map{|a| a.inspect}*", "}): #{e.to_str}"
          check_params(cmd, args)
        else
          e.errstr << " running '#{cmd}'"
          #puts "'#{cmd}' (#{args.map{|a| a.inspect}*", "}): #{e.to_str}"
          #debugger
        end
        raise
      end

      def self.param_count cmd
        cmd.gsub(/'[^']*'/,'').scan(/\?/).size
      end

    end

  end

else

  module DataObjects
    module SqlServer
      JDBC_DRIVER = 'net.sourceforge.jtds.jdbc.Driver'
    end
  end

  # Register SqlServer JDBC driver
  java.sql.DriverManager.registerDriver Java::net.sourceforge.jtds.jdbc.Driver.new

  DataObjects::SqlServer::Connection.class_eval do

    def quote_boolean(value)
      value ? 1 : 0
    end

  end

end
