require 'active_record'
require 'rails_admin/adapters/active_record/abstract_object'
require 'rails_admin/adapters/active_record/association'
require 'rails_admin/adapters/active_record/property'

module RailsAdmin
  module Adapters
    module ActiveRecord
      DISABLED_COLUMN_TYPES = [:tsvector, :blob, :binary, :spatial, :hstore, :geometry].freeze

      def new(params = {})
        AbstractObject.new(model.new(params))
      end

      def get(id)
        return unless object = model.where(primary_key => id).first
        AbstractObject.new object
      end

      def scoped
        model.all
      end

      def first(options = {}, scope = nil)
        all(options, scope).first
      end

      def all(options = {}, scope = nil)
        unless scope
          scope = scoped
          if scope.respond_to?(:current_locale)
            scope = scope.current_locale
          end
        end
        scope = scope.includes(options[:include]) if options[:include]
        scope = scope.limit(options[:limit]) if options[:limit]
        scope = scope.where(primary_key => options[:bulk_ids]) if options[:bulk_ids]
        scope = query_scope(scope, options[:query]) if options[:query]
        scope = filter_scope(scope, options[:filters]) if options[:filters]
        if options[:page] && options[:per]
          scope = scope.send(Kaminari.config.page_method_name, options[:page]).per(options[:per])
        end
        scope = scope.reorder("#{options[:sort]} #{options[:sort_reverse] ? 'asc' : 'desc'}") if options[:sort]
        scope
      end

      def count(options = {}, scope = nil)
        all(options.merge(limit: false, page: false), scope).count(:all)
      end

      def destroy(objects)
        Array.wrap(objects).each(&:destroy)
      end

      def associations
        model.reflect_on_all_associations.collect do |association|
          Association.new(association, model)
        end
      end

      def properties
        columns = model.columns.reject do |c|
          c.type.blank? ||
            DISABLED_COLUMN_TYPES.include?(c.type.to_sym) ||
            c.try(:array)
        end
        columns.collect do |property|
          Property.new(property, model)
        end
      end

      delegate :primary_key, :table_name, to: :model, prefix: false

      def encoding
        case ::ActiveRecord::Base.connection_config[:adapter]
        when 'postgresql'
          ::ActiveRecord::Base.connection.select_one("SELECT ''::text AS str;").values.first.encoding
        when 'mysql2'
          ::ActiveRecord::Base.connection.instance_variable_get(:@connection).encoding
        when 'oracle_enhanced'
          ::ActiveRecord::Base.connection.select_one("SELECT dummy FROM DUAL").values.first.encoding
        else
          ::ActiveRecord::Base.connection.select_one("SELECT '' AS str;").values.first.encoding
        end
      end

      def embedded?
        false
      end

      def cyclic?
        false
      end

      def adapter_supports_joins?
        true
      end

      class WhereBuilder
        def initialize(scope)
          @statements = []
          @values = []
          @tables = []
          @scope = scope
        end

        def add(field, value, operator)
          field.searchable_columns.flatten.each do |column_infos|
            statement, value1, value2 = StatementBuilder.new(column_infos[:column], column_infos[:type], value, operator).to_statement
            @statements << statement if statement.present?
            @values << value1 unless value1.nil?
            @values << value2 unless value2.nil?
            table, column = column_infos[:column].split('.')
            @tables.push(table) if column
          end
        end

        def build
          scope = @scope.where(@statements.join(' OR '), *@values)
          scope = scope.references(*@tables.uniq) if @tables.any?
          scope
        end
      end

      def query_scope(scope, query, fields = config.list.fields.select(&:queryable?))
        wb = WhereBuilder.new(scope)
        fields.each do |field|
          value = parse_field_value(field, query)
          wb.add(field, value, field.search_operator)
        end
        # OR all query statements
        wb.build
      end

      # filters example => {"string_field"=>{"0055"=>{"o"=>"like", "v"=>"test_value"}}, ...}
      # "0055" is the filter index, no use here. o is the operator, v the value
      def filter_scope(scope, filters, fields = config.list.fields.select(&:filterable?))
        filters.each_pair do |field_name, filters_dump|
          filters_dump.each do |_, filter_dump|
            wb = WhereBuilder.new(scope)
            field = fields.detect { |f| f.name.to_s == field_name }
            value = parse_field_value(field, filter_dump[:v])

            wb.add(field, value, (filter_dump[:o] || 'default'))
            # AND current filter statements to other filter statements
            scope = wb.build
          end
        end
        scope
      end

      def build_statement(column, type, value, operator)
        StatementBuilder.new(column, type, value, operator).to_statement
      end

      class StatementBuilder < RailsAdmin::AbstractModel::StatementBuilder
      protected

        def unary_operators
          case @type
          when :boolean
            boolean_unary_operators
          when :integer, :decimal, :float
            numeric_unary_operators
          else
            generic_unary_operators
          end
        end

      private

        def generic_unary_operators
          {
            '_blank' => ["(#{@column} IS NULL OR #{@column} = '')"],
            '_present' => ["(#{@column} IS NOT NULL AND #{@column} != '')"],
            '_null' => ["(#{@column} IS NULL)"],
            '_not_null' => ["(#{@column} IS NOT NULL)"],
            '_empty' => ["(#{@column} = '')"],
            '_not_empty' => ["(#{@column} != '')"],
          }
        end

        def boolean_unary_operators
          generic_unary_operators.merge(
            '_blank' => ["(#{@column} IS NULL)"],
            '_empty' => ["(#{@column} IS NULL)"],
            '_present' => ["(#{@column} IS NOT NULL)"],
            '_not_empty' => ["(#{@column} IS NOT NULL)"],
          )
        end
        alias_method :numeric_unary_operators, :boolean_unary_operators

        def range_filter(min, max)
          if min && max
            ["(#{@column} BETWEEN ? AND ?)", min, max]
          elsif min
            ["(#{@column} >= ?)", min]
          elsif max
            ["(#{@column} <= ?)", max]
          end
        end

        def build_statement_for_type
          case @type
          when :boolean                   then build_statement_for_boolean
          when :integer, :decimal, :float then build_statement_for_integer_decimal_or_float
          when :string, :text             then build_statement_for_string_or_text
          when :enum                      then build_statement_for_enum
          when :belongs_to_association    then build_statement_for_belongs_to_association
          when :uuid                      then build_statement_for_uuid
          end
        end

        def build_statement_for_boolean
          return ["(#{@column} IS NULL OR #{@column} = ?)", false] if %w(false f 0).include?(@value)
          return ["(#{@column} = ?)", true] if %w(true t 1).include?(@value)
        end

        def column_for_value(value)
          ["(#{@column} = ?)", value]
        end

        def build_statement_for_belongs_to_association
          return if @value.blank?
          ["(#{@column} = ?)", @value.to_i] if @value.to_i.to_s == @value
        end

        def build_statement_for_string_or_text
          return if @value.blank?

          unless ['postgresql', 'postgis'].include? ar_adapter
            @value = @value.mb_chars.downcase
          end

          @value = begin
            case @operator
            when 'default', 'like'
              "%#{@value}%"
            when 'starts_with'
              "#{@value}%"
            when 'ends_with'
              "%#{@value}"
            when 'is', '='
              @value
            else
              return
            end
          end

          if ['postgresql', 'postgis'].include? ar_adapter
            ["(#{@column} ILIKE ?)", @value]
          else
            ["(LOWER(#{@column}) LIKE ?)", @value]
          end
        end

        def build_statement_for_enum
          return if @value.blank?
          ["(#{@column} IN (?))", Array.wrap(@value)]
        end

        def build_statement_for_uuid
          if @value.to_s =~ /\A[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}\z/
            column_for_value(@value)
          end
        end

        def ar_adapter
          ::ActiveRecord::Base.connection.adapter_name.downcase
        end
      end
    end
  end
end
