ArJdbc.load_java_part :Derby

require 'arjdbc/util/table_copier'
require 'arjdbc/derby/schema_creation' # AR 4.x
require 'arel/visitors/derby'

module ArJdbc
  module Derby
    include Util::TableCopier

    def self.extended(adapter)
      require 'arjdbc/derby/active_record_patch'
    end

    def self.included(base)
      require 'arjdbc/derby/active_record_patch'
    end

    # @see ActiveRecord::ConnectionAdapters::JdbcAdapter#jdbc_connection_class
    def self.jdbc_connection_class
      ::ActiveRecord::ConnectionAdapters::DerbyJdbcConnection
    end

    # @see ActiveRecord::ConnectionAdapters::JdbcColumn#column_types
    def self.column_selector
      [ /derby/i, lambda { |config, column| column.extend(Column) } ]
    end

 	# @private
    @@emulate_booleans = true

    # Boolean emulation can be disabled using :
    #
    #   ArJdbc::Derby.emulate_booleans = false
    #
    def self.emulate_booleans?; @@emulate_booleans; end
    # @deprecated Use {#emulate_booleans?} instead.
    def self.emulate_booleans; @@emulate_booleans; end
    # @see #emulate_booleans?
    def self.emulate_booleans=(emulate); @@emulate_booleans = emulate; end

    # @note Part of this module is implemented in "native" Java.
    # @see ActiveRecord::ConnectionAdapters::JdbcColumn
    module Column

      private

      def extract_limit(sql_type)
        case @sql_type = sql_type.downcase
        when /^smallint/i    then @sql_type = 'smallint'; limit = 2
        when /^bigint/i      then @sql_type = 'bigint'; limit = 8
        when /^double/i      then @sql_type = 'double'; limit = 8 # DOUBLE PRECISION
        when /^real/i        then @sql_type = 'real'; limit = 4
        when /^integer/i     then @sql_type = 'integer'; limit = 4
        when /^datetime/i    then @sql_type = 'datetime'; limit = nil
        when /^timestamp/i   then @sql_type = 'timestamp'; limit = nil
        when /^time/i        then @sql_type = 'time'; limit = nil
        when /^date/i        then @sql_type = 'date'; limit = nil
        when /^xml/i         then @sql_type = 'xml'; limit = nil
        else
          limit = super
          # handle maximum length for a VARCHAR string :
          limit = 32672 if ! limit && @sql_type.index('varchar') == 0
        end
        limit
      end

      def simplified_type(field_type)
        case field_type
        when /^smallint/i    then Derby.emulate_booleans? ? :boolean : :integer
        when /^bigint|int/i  then :integer
        when /^real|double/i then :float
        when /^dec/i         then # DEC is a DECIMAL alias
          extract_scale(field_type) == 0 ? :integer : :decimal
        when /^timestamp/i   then :datetime
        when /^xml/i         then :xml
        when 'long varchar'  then :text
        when /for bit data/i then :binary
        # :name=>"long varchar for bit data", :limit=>32700
        # :name=>"varchar() for bit data", :limit=>32672
        # :name=>"char() for bit data", :limit=>254}
        else
          super
        end
      end

      # Post process default value from JDBC into a Rails-friendly format (columns{-internal})
      def default_value(value)
        # JDBC returns column default strings with actual single quotes around the value.
        return $1 if value =~ /^'(.*)'$/
        return nil if value == "GENERATED_BY_DEFAULT"
        value
      end

    end

    ADAPTER_NAME = 'Derby'.freeze

    def adapter_name
      ADAPTER_NAME
    end

    # @private
    def init_connection(jdbc_connection)
      md = jdbc_connection.meta_data
      major_version = md.database_major_version; minor_version = md.database_minor_version
      if major_version < 10 || (major_version == 10 && minor_version < 5)
        raise ::ActiveRecord::ConnectionNotEstablished, "Derby adapter requires Derby >= 10.5"
      end
      if major_version == 10 && minor_version < 8 # 10.8 ~ supports JDBC 4.1
        config[:connection_alive_sql] ||=
          'SELECT 1 FROM SYS.SYSSCHEMAS FETCH FIRST 1 ROWS ONLY' # FROM clause mandatory
      else
        # NOTE: since the loaded Java driver class can't change :
        Derby.send(:remove_method, :init_connection) rescue nil
      end
    end

    def configure_connection
      # must be done or SELECT...FOR UPDATE won't work how we expect :
      tx_isolation = config[:transaction_isolation] # set false to leave as is
      tx_isolation = :serializable if tx_isolation.nil?
      @connection.transaction_isolation = tx_isolation if tx_isolation
      # if a user name was specified upon connection, the user's name is the
      # default schema for the connection, if a schema with that name exists
      set_schema(config[:schema]) if config.key?(:schema)
    end

    def index_name_length
      128
    end

    NATIVE_DATABASE_TYPES = {
      :primary_key => "int GENERATED BY DEFAULT AS identity NOT NULL PRIMARY KEY",
      :string => { :name => "varchar", :limit => 255 }, # 32672
      :text => { :name => "clob" }, # 2,147,483,647
      :char => { :name => "char", :limit => 254 }, # JDBC :limit => 254
      :binary => { :name => "blob" }, # 2,147,483,647
      :float => { :name => "float", :limit => 8 }, # DOUBLE PRECISION
      :real => { :name => "real", :limit => 4 }, # JDBC :limit => 23
      :double => { :name => "double", :limit => 8 }, # JDBC :limit => 52
      :decimal => { :name => "decimal", :precision => 5, :scale => 0 }, # JDBC :limit => 31
      :numeric => { :name => "decimal", :precision => 5, :scale => 0 }, # JDBC :limit => 31
      :integer => { :name => "integer", :limit => 4 }, # JDBC :limit => 10
      :smallint => { :name => "smallint", :limit => 2 }, # JDBC :limit => 5
      :bigint => { :name => "bigint", :limit => 8 }, # JDBC :limit => 19
      :date => { :name => "date" },
      :time => { :name => "time" },
      :datetime => { :name => "timestamp" },
      :timestamp => { :name => "timestamp" },
      :xml => { :name => "xml" },
      :boolean => { :name => "smallint", :limit => 1 }, # TODO boolean (since 10.7)
      :object => { :name => "object" },
    }

    # @private
    def initialize_type_map(m)
      super
      m.register_type(%r(smallint)i) do |sql_type|
        if Derby.emulate_booleans?
          ActiveRecord::Type::Boolean.new
        else
          ActiveRecord::Type::Integer.new(:limit => 1)
        end
      end
      m.alias_type %r(real)i, 'float'
    end if AR42

    def reset_column_information
      initialize_type_map(type_map)
    end if AR42

    # @override
    def native_database_types
      NATIVE_DATABASE_TYPES
    end

    # UNTESTED
    def existing_savepoint_name?(name)
      (@connection.instance_variable_get('@savepoints') || {}).key? name
    end

    # Ensure the savepoint name is unused before creating it.
    # @override
    def create_savepoint(name = current_savepoint_name(true))
      release_savepoint(name) if existing_savepoint_name?(name)
      super(name)
    end

    # @override
    def quote(value, column = nil)
      return super if column && ArJdbc::AR42 && column.cast_type.is_a?(ActiveRecord::Type::Serialized)
      return value.quoted_id if value.respond_to?(:quoted_id)
      return value if sql_literal?(value)
      return 'NULL' if value.nil?

      column_type = column && column.type
      if column_type == :string || column_type == :text
        # Derby is not permissive
        # e.g. sending an Integer to a VARCHAR column will fail
        case value
        when BigDecimal then value = value.to_s('F')
        when Numeric then value = value.to_s
        when true, false then value = value.to_s
        when Date, Time then value = quoted_date(value)
        else # on 2.3 attribute serialization needs to_yaml here
          value = value.to_s if ActiveRecord::VERSION::MAJOR >= 3
        end
      end

      case value
      when String, ActiveSupport::Multibyte::Chars
        if column_type == :text
          "CAST('#{quote_string(value)}' AS CLOB)"
        elsif column_type == :binary
          "CAST(X'#{quote_binary(value)}' AS BLOB)"
        elsif column_type == :xml
          "XMLPARSE(DOCUMENT '#{quote_string(value)}' PRESERVE WHITESPACE)"
        elsif column_type == :integer
          value.to_i
        elsif column_type == :float
          value.to_f
        else
          "'#{quote_string(value)}'"
        end
      else
        super
      end
    end

    # @override
    def quoted_date(value)
      if value.acts_like?(:time) && value.respond_to?(:usec)
        usec = sprintf("%06d", value.usec)
        value = ::ActiveRecord::Base.default_timezone == :utc ? value.getutc : value.getlocal
        "#{value.strftime("%Y-%m-%d %H:%M:%S")}.#{usec}"
      else
        super
      end
    end if ::ActiveRecord::VERSION::MAJOR >= 3

    # @private In Derby, these cannot specify a limit.
    NO_LIMIT_TYPES = [ :integer, :boolean, :timestamp, :datetime, :date, :time ]

    # Convert the specified column type to a SQL string.
    # @override
    def type_to_sql(type, limit = nil, precision = nil, scale = nil)
      return super unless NO_LIMIT_TYPES.include?(t = type.to_s.downcase.to_sym)

      native_type = NATIVE_DATABASE_TYPES[t]
      native_type.is_a?(Hash) ? native_type[:name] : native_type
    end

    # @private
    class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition

      def xml(*args)
        options = args.extract_options!
        column(args[0], 'xml', options)
      end

    end

    def table_definition(*args)
      new_table_definition(TableDefinition, *args)
    end

    # @override
    def empty_insert_statement_value
      ::Arel::Visitors::Derby::VALUES_DEFAULT # Derby needs to know the columns
    end

    # Set the sequence to the max value of the table's column.
    # @override
    def reset_sequence!(table, column, sequence = nil)
      mpk = select_value("SELECT MAX(#{quote_column_name(column)}) FROM #{quote_table_name(table)}")
      execute("ALTER TABLE #{quote_table_name(table)} ALTER COLUMN #{quote_column_name(column)} RESTART WITH #{mpk.to_i + 1}")
    end

    def reset_pk_sequence!(table, pk = nil, sequence = nil)
      klasses = classes_for_table_name(table)
      klass   = klasses.nil? ? nil : klasses.first
      pk      = klass.primary_key unless klass.nil?
      if pk && klass.columns_hash[pk].type == :integer
        reset_sequence!(klass.table_name, pk)
      end
    end

    def classes_for_table_name(table)
      ActiveRecord::Base.send(:subclasses).select { |klass| klass.table_name == table }
    end
    private :classes_for_table_name

    # @override
    def remove_index(table_name, options)
      execute "DROP INDEX #{index_name(table_name, options)}"
    end

    # @override
    def rename_table(name, new_name)
      execute "RENAME TABLE #{quote_table_name(name)} TO #{quote_table_name(new_name)}"
    end

    def add_column(table_name, column_name, type, options = {})
      add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
      add_column_options!(add_column_sql, options)
      execute(add_column_sql)
    end unless const_defined? :SchemaCreation

    # @override fix case where AR passes `:default => nil, :null => true`
    def add_column_options!(sql, options)
      options.delete(:default) if options.has_key?(:default) && options[:default].nil?
      sql << " DEFAULT #{quote(options.delete(:default))}" if options.has_key?(:default)
      super
    end unless const_defined? :SchemaCreation

    if ActiveRecord::VERSION::MAJOR >= 4

    # @override
    def remove_column(table_name, column_name, type = nil, options = {})
      do_remove_column(table_name, column_name)
    end

    else

    # @override
    def remove_column(table_name, *column_names)
      for column_name in column_names.flatten
        do_remove_column(table_name, column_name)
      end
    end
    alias remove_columns remove_column

    end

    def do_remove_column(table_name, column_name)
      execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)} RESTRICT"
    end
    private :do_remove_column

    # @override
    def change_column(table_name, column_name, type, options = {})
      # TODO this needs a review since now we're likely to be on >= 10.8

      # Notes about changing in Derby:
      #    http://db.apache.org/derby/docs/10.2/ref/rrefsqlj81859.html#rrefsqlj81859__rrefsqlj37860)
      #
      # We support changing columns using the strategy outlined in:
      #    https://issues.apache.org/jira/browse/DERBY-1515
      #
      # This feature has not made it into a formal release and is not in Java 6.
      # We will need to conditionally support this (supposed to arrive for 10.3.0.0).

      # null/not nulling is easy, handle that separately
      if options.include?(:null)
        # This seems to only work with 10.2 of Derby
        if options.delete(:null) == false
          execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} NOT NULL"
        else
          execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} NULL"
        end
      end

      # anything left to do?
      unless options.empty?
        begin
          execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN " <<
                  " #{quote_column_name(column_name)} SET DATA TYPE #{type_to_sql(type, options[:limit])}"
        rescue
          transaction do
            temp_new_column_name = "#{column_name}_newtype"
            # 1) ALTER TABLE t ADD COLUMN c1_newtype NEWTYPE;
            add_column table_name, temp_new_column_name, type, options
            # 2) UPDATE t SET c1_newtype = c1;
            execute "UPDATE #{quote_table_name(table_name)} SET " <<
                    " #{quote_column_name(temp_new_column_name)} = " <<
                    " CAST(#{quote_column_name(column_name)} AS #{type_to_sql(type, options[:limit])})"
            # 3) ALTER TABLE t DROP COLUMN c1;
            remove_column table_name, column_name
            # 4) ALTER TABLE t RENAME COLUMN c1_newtype to c1;
            rename_column table_name, temp_new_column_name, column_name
          end
        end
      end
    end

    # @override
    def rename_column(table_name, column_name, new_column_name)
      execute "RENAME COLUMN #{quote_table_name(table_name)}.#{quote_column_name(column_name)} " <<
              " TO #{quote_column_name(new_column_name)}"
    end

    # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
    #
    # Derby requires the ORDER BY columns in the select list for distinct queries, and
    # requires that the ORDER BY include the distinct column.
    # ```
    #   distinct("posts.id", "posts.created_at desc")
    # ```
    # @note This is based on distinct method for the PostgreSQL Adapter.
    # @override
    def distinct(columns, order_by)
      "DISTINCT #{columns_for_distinct(columns, order_by)}"
    end

    # @override Since AR 4.0 (on 4.1 {#distinct} is gone and won't be called).
    def columns_for_distinct(columns, orders)
      return columns if orders.blank?

      # construct a clean list of column names from the ORDER BY clause,
      # removing any ASC/DESC modifiers
      order_columns = [ orders ]; order_columns.flatten! # AR 3.x vs 4.x
      order_columns.map! do |column|
        column = column.to_sql unless column.is_a?(String) # handle AREL node
        column.split(',').collect! { |s| s.split.first }
      end.flatten!
      order_columns.reject!(&:blank?)
      order_columns = order_columns.zip (0...order_columns.size).to_a
      order_columns = order_columns.map { |s, i| "#{s} AS alias_#{i}" }

      columns = [ columns ]; columns.flatten!
      columns.push( *order_columns ).join(', ')
      # return a DISTINCT clause that's distinct on the columns we want but
      # includes all the required columns for the ORDER BY to work properly
    end

    # @override
    def primary_keys(table_name)
      @connection.primary_keys table_name.to_s.upcase
    end

    # @override
    def tables(name = nil)
      @connection.tables(nil, current_schema)
    end

    # @override
    def supports_ddl_transactions?; true end
 
    # @override
    def supports_foreign_keys?; true end

    def truncate(table_name, name = nil)
      execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
    end

    # @return [String] the current schema name
    def current_schema
      @current_schema ||=
        select_value "SELECT CURRENT SCHEMA FROM SYS.SYSSCHEMAS FETCH FIRST 1 ROWS ONLY", 'SCHEMA'
    end

    # Change the current (implicit) Derby schema to be used for this connection.
    def set_schema(schema)
      @current_schema = nil
      execute "SET SCHEMA #{schema}", 'SCHEMA'
    end
    alias_method :current_schema=, :set_schema

    # Creates a new Derby schema.
    # @see #set_schema
    def create_schema(schema)
      execute "CREATE SCHEMA #{schema}", 'Create Schema'
    end

    # Drops an existing schema, needs to be empty (no DB objects).
    def drop_schema(schema)
      execute "DROP SCHEMA #{schema} RESTRICT", 'Drop Schema'
    end

    # @private
    def recreate_database(name = nil, options = {})
      drop_database(name)
      create_database(name, options)
    end

    # @private
    def create_database(name = nil, options = {}); end

    # @private
    def drop_database(name = nil)
      tables.each { |table| drop_table(table) }
    end

    # @override
    def quote_column_name(name)
      %Q{"#{name.to_s.upcase.gsub('"', '""')}"}
    end

    # @override
    def quote_table_name_for_assignment(table, attr)
      quote_column_name(attr)
    end if ::ActiveRecord::VERSION::MAJOR > 3

    # @note Only used with (non-AREL) ActiveRecord **2.3**.
    # @see Arel::Visitors::Derby
    def add_limit_offset!(sql, options)
      sql << " OFFSET #{options[:offset]} ROWS" if options[:offset]
      # ROWS/ROW and FIRST/NEXT mean the same
      sql << " FETCH FIRST #{options[:limit]} ROWS ONLY" if options[:limit]
    end if ::ActiveRecord::VERSION::MAJOR < 3

    # @override
    def execute(sql, name = nil, binds = [])
      sql = to_sql(sql, binds)
      insert = self.class.insert?(sql)
      update = ! insert && ! self.class.select?(sql)
      sql = correct_is_null(sql, insert || update)
      super(sql, name, binds)
    end

    # Returns the value of an identity column of the last *INSERT* statement
    # made over this connection.
    # @note Check the *IDENTITY_VAL_LOCAL* function for documentation.
    # @return [Fixnum]
    def last_insert_id
      @connection.identity_val_local
    end

    private

    def correct_is_null(sql, insert_or_update = false)
      if insert_or_update
        if ( i = sql =~ /\sWHERE\s/im )
          where_part = sql[i..-1]; sql = sql.dup
          where_part.gsub!(/!=\s*NULL/i, 'IS NOT NULL')
          where_part.gsub!(/=\sNULL/i, 'IS NULL')
          sql[i..-1] = where_part
        end
        sql
      else
        sql.gsub(/=\sNULL/i, 'IS NULL')
      end
    end

    # NOTE: only setup query analysis on AR <= 3.0 since on 3.1 {#exec_query},
    # {#exec_insert} will be used for AR generated queries/inserts etc.
    # Also there's prepared statement support and {#execute} is meant to stay
    # as a way of running non-prepared SQL statements (returning raw results).
    if ActiveRecord::VERSION::MAJOR < 3 ||
      ( ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR < 1 )

    def _execute(sql, name = nil)
      if self.class.insert?(sql)
        @connection.execute_insert(sql)
      elsif self.class.select?(sql)
        @connection.execute_query_raw(sql)
      else
        @connection.execute_update(sql)
      end
    end

    end

  end
end
