require 'active_record/connection_adapters/abstract_mysql_adapter'

module ActiveRecord
  module Mysql
    module Awesome
      class ChangeColumnDefinition < Struct.new(:column, :name)
      end

      module ColumnMethods
        def primary_key(name, type = :primary_key, **options)
          options[:auto_increment] = true if type == :bigint
          super
        end

        def unsigned_integer(*args, **options)
          args.each { |name| column(name, :unsigned_integer, options) }
        end
      end

      class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
        attr_accessor :auto_increment, :unsigned, :charset, :collation
      end

      class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
        include ColumnMethods

        def initialize(types, name, temporary, options, as = nil)
          super(types, name, temporary, options)
          @as = as
        end

        def new_column_definition(name, type, options)
          column = super
          case column.type
          when :primary_key
            column.type = :integer
            column.auto_increment = true
          when :unsigned_integer
            column.type = :integer
            column.unsigned = true
          end
          column.auto_increment ||= options[:auto_increment]
          column.unsigned ||= options[:unsigned]
          column.charset = options[:charset]
          column.collation = options[:collation]
          column
        end

        private

        def create_column_definition(name, type)
          ColumnDefinition.new(name, type)
        end
      end

      class Table < ActiveRecord::ConnectionAdapters::Table
        include ColumnMethods
      end

      module SchemaCreation
        def visit_AddColumn(o)
          add_column_position!("ADD #{accept(o)}", column_options(o))
        end

        private

        def visit_ColumnDefinition(o)
          o.sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale, o.unsigned)
          column_sql = "#{quote_column_name(o.name)} #{o.sql_type}"
          add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key
          column_sql
        end

        def visit_ChangeColumnDefinition(o)
          change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}"
          add_column_position!(change_column_sql, column_options(o.column))
        end

        def column_options(o)
          column_options = super
          column_options[:first] = o.first
          column_options[:after] = o.after
          column_options[:auto_increment] = o.auto_increment
          column_options[:primary_key] = o.primary_key
          column_options[:charset]   = o.charset
          column_options[:collation] = o.collation
          column_options
        end

        def add_column_options!(sql, options)
          if options[:charset]
            sql << " CHARACTER SET #{options[:charset]}"
          end
          if options[:collation]
            sql << " COLLATE #{options[:collation]}"
          end
          if options[:primary_key] == true
            sql << " PRIMARY KEY"
          end
          super
        end

        def add_column_position!(sql, options)
          if options[:first]
            sql << " FIRST"
          elsif options[:after]
            sql << " AFTER #{quote_column_name(options[:after])}"
          end
          sql
        end

        def type_to_sql(type, limit, precision, scale, unsigned)
          @conn.type_to_sql(type.to_sym, limit, precision, scale, unsigned)
        end
      end

      def update_table_definition(table_name, base)
        Table.new(table_name, base)
      end

      module Column
        def unsigned?
          sql_type =~ /unsigned/i
        end

        def bigint?
          sql_type =~ /bigint/i
        end

        def auto_increment?
          extra == 'auto_increment'
        end
      end

      def quote(value, column = nil)
        return super if value.nil? || !value.acts_like?(:time)
        return super unless column && /time/ === column.sql_type

        if value.acts_like?(:time)
          zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal

          if value.respond_to?(zone_conversion_method)
            value = value.send(zone_conversion_method)
          end
        end

        if (precision = column.precision) && value.respond_to?(:usec)
          number_of_insignificant_digits = 6 - precision
          round_power = 10 ** number_of_insignificant_digits
          value = value.change(usec: value.usec / round_power * round_power)
        end

        result = value.to_s(:db)
        if value.respond_to?(:usec) && value.usec > 0
          "'#{result}.#{sprintf("%06d", value.usec)}'"
        else
          "'#{result}'"
        end
      end

      if ActiveRecord::VERSION::STRING < "4.2.0"
        module Column
          def extract_limit(sql_type)
            case sql_type
            when /time/i; nil
            else
              super
            end
          end

          def extract_precision(sql_type)
            case sql_type
            when /time/i
              if sql_type =~ /\((\d+)(,\d+)?\)/
                $1.to_i
              else
                0
              end
            else
              super
            end
          end
        end
      else
        protected

        def initialize_type_map(m) # :nodoc:
          super
          register_class_with_precision m, %r(time)i,     Type::Time
          register_class_with_precision m, %r(datetime)i, Type::DateTime
        end

        def register_class_with_precision(mapping, key, klass) # :nodoc:
          mapping.register_type(key) do |*args|
            precision = extract_precision(args.last)
            klass.new(precision: precision)
          end
        end

        def extract_precision(sql_type)
          if /time/ === sql_type
            super || 0
          else
            super
          end
        end
      end

      public

      def supports_datetime_with_precision?
        version >= '5.6.4'
      end

      def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = false)
        sql = case type
        when :integer
          case limit
          when nil, 4, 11; 'int'  # compatibility with MySQL default
          else
            super(type, limit, precision, scale)
          end
        when :datetime, :time
          case precision
          when nil; super(type, limit, precision, scale)
          when 0..6; "#{type}(#{precision})"
          else raise(ActiveRecordError, "No #{type} type has precision of #{precision}. The allowed range of precision is from 0 to 6")
          end
        else
          super(type, limit, precision, scale)
        end

        sql << ' unsigned' if unsigned && type != :primary_key
        sql
      end

      def options_for_column_spec(table_name)
        if collation = select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"]
          super.merge(collation: collation)
        else
          super
        end
      end

      def column_spec_for_primary_key(column, options)
        spec = {}
        if column.auto_increment?
          spec[:id] = ':bigint' if column.bigint?
          spec[:unsigned] = 'true' if column.unsigned?
          return if spec.empty?
        else
          spec[:id] = column.type.inspect
          spec.merge!(prepare_column_options(column, options).delete_if { |key, _| [:name, :type, :null].include?(key) })
        end
        spec
      end

      def prepare_column_options(column, options) # :nodoc:
        spec = super
        spec.delete(:precision) if /time/ === column.sql_type && column.precision == 0
        spec.delete(:limit) if :boolean === column.type
        spec[:unsigned] = 'true' if column.unsigned?
        if column.collation && column.collation != options[:collation]
          spec[:collation] = column.collation.inspect
        end
        spec
      end

      def migration_keys
        super | [:unsigned, :collation]
      end

      def table_options(table_name)
        create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]

        return unless create_table_info

        # strip create_definitions and partition_options
        raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip

        # strip AUTO_INCREMENT
        raw_table_options.sub(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1')
      end

      def drop_table(table_name, options = {})
        execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}"
      end

      protected

      def add_column_sql(table_name, column_name, type, options = {})
        td = create_table_definition(table_name)
        cd = td.new_column_definition(column_name, type, options)
        schema_creation.visit_AddColumn cd
      end

      def change_column_sql(table_name, column_name, type, options = {})
        column = column_for(table_name, column_name)

        unless options_include_default?(options)
          options[:default] = column.default
        end

        unless options.has_key?(:null)
          options[:null] = column.null
        end

        td = create_table_definition(table_name)
        cd = td.new_column_definition(column.name, type, options)
        schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
      end

      def rename_column_sql(table_name, column_name, new_column_name)
        column  = column_for(table_name, column_name)
        options = {
          default: column.default,
          null: column.null,
          auto_increment: column.auto_increment?
        }

        current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
        td = create_table_definition(table_name)
        cd = td.new_column_definition(new_column_name, current_type, options)
        schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
      end

      private

      def configure_connection
        _config = @config
        if [':default', :default].include?(@config[:strict])
          @config = @config.deep_merge(variables: { sql_mode: :default })
        end
        super
      ensure
        @config = _config
      end

      def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc:
        TableDefinition.new(native_database_types, name, temporary, options, as)
      end
    end
  end

  module ConnectionAdapters
    class AbstractMysqlAdapter < AbstractAdapter
      prepend Mysql::Awesome

      class Column < ConnectionAdapters::Column # :nodoc:
        prepend Mysql::Awesome::Column
      end

      class SchemaCreation < AbstractAdapter::SchemaCreation
        prepend Mysql::Awesome::SchemaCreation
      end
    end
  end
end
