require 'dm-migrations/auto_migration'
require 'dm-migrations/adapters/dm-do-adapter'

module DataMapper
  module Migrations
    module PostgresAdapter

      include DataObjectsAdapter

      # @api private
      def self.included(base)
        base.extend DataObjectsAdapter::ClassMethods
        base.extend ClassMethods
      end

      # @api semipublic
      def upgrade_model_storage(model)
        without_notices { super }
      end

      # @api semipublic
      def create_model_storage(model)
        without_notices { super }
      end

      # @api semipublic
      def destroy_model_storage(model)
        if supports_drop_table_if_exists?
          without_notices { super }
        else
          super
        end
      end

      module SQL #:nodoc:
#        private  ## This cannot be private for current migrations

        # @api private
        def supports_drop_table_if_exists?
          @supports_drop_table_if_exists ||= postgres_version >= '8.2'
        end

        # @api private
        def schema_name
          @schema_name ||= select('SELECT current_schema()').first.freeze
        end

        # @api private
        def postgres_version
          @postgres_version ||= select('SELECT version()').first.split[1].freeze
        end

        # @api private
        def without_notices
          # execute the block with NOTICE messages disabled
          begin
            execute('SET client_min_messages = warning')
            yield
          ensure
            execute('RESET client_min_messages')
          end
        end

        # @api private
        def property_schema_hash(property)
          schema = super

          primitive = property.primitive

          # Postgres does not support precision and scale for Float
          if primitive == Float
            schema.delete(:precision)
            schema.delete(:scale)
          end

          if property.kind_of?(Property::Integer)
            min = property.min
            max = property.max

            schema[:primitive] = integer_column_statement(min..max) if min && max
          end

          if schema[:serial]
            schema[:primitive] = serial_column_statement(min..max)
          end

          schema
        end

        private

        # Return SQL statement for the integer column
        #
        # @param [Range] range
        #   the min/max allowed integers
        #
        # @return [String]
        #   the statement to create the integer column
        #
        # @api private
        def integer_column_statement(range)
          min = range.first
          max = range.last

          smallint = 2**15
          integer  = 2**31
          bigint   = 2**63

          if    min >= -smallint && max < smallint then 'SMALLINT'
          elsif min >= -integer  && max < integer  then 'INTEGER'
          elsif min >= -bigint   && max < bigint   then 'BIGINT'
          else
            raise ArgumentError, "min #{min} and max #{max} exceeds supported range"
          end
        end

        # Return SQL statement for the serial column
        #
        # @param [Integer] max
        #   the max allowed integer
        #
        # @return [String]
        #   the statement to create the serial column
        #
        # @api private
        def serial_column_statement(range)
          max = range.last

          if    max.nil? || max < 2**31 then 'SERIAL'
          elsif             max < 2**63 then 'BIGSERIAL'
          else
            raise ArgumentError, "min #{range.first} and max #{max} exceeds supported range"
          end
        end
      end # module SQL

      include SQL

      module ClassMethods
        # Types for PostgreSQL databases.
        #
        # @return [Hash] types for PostgreSQL databases.
        #
        # @api private
        def type_map
          precision = Property::Numeric::DEFAULT_PRECISION
          scale     = Property::Decimal::DEFAULT_SCALE

          @type_map ||= super.merge(
            Property::Binary => { :primitive => 'BYTEA'                                                      },
            BigDecimal       => { :primitive => 'NUMERIC',          :precision => precision, :scale => scale },
            Float            => { :primitive => 'DOUBLE PRECISION'                                           }
          ).freeze
        end
      end

    end
  end
end
