module Graphiti
  module Adapters
    class ActiveRecord < ::Graphiti::Adapters::Abstract
      require 'graphiti/adapters/active_record/inferrence'
      require 'graphiti/adapters/active_record/has_many_sideload'
      require 'graphiti/adapters/active_record/belongs_to_sideload'
      require 'graphiti/adapters/active_record/has_one_sideload'
      require 'graphiti/adapters/active_record/many_to_many_sideload'

      def self.sideloading_classes
        {
          has_many: Graphiti::Adapters::ActiveRecord::HasManySideload,
          has_one: Graphiti::Adapters::ActiveRecord::HasOneSideload,
          belongs_to: Graphiti::Adapters::ActiveRecord::BelongsToSideload,
          many_to_many: Graphiti::Adapters::ActiveRecord::ManyToManySideload
        }
      end

      def filter_eq(scope, attribute, value)
        scope.where(attribute => value)
      end
      alias :filter_integer_eq :filter_eq
      alias :filter_float_eq :filter_eq
      alias :filter_big_decimal_eq :filter_eq
      alias :filter_date_eq :filter_eq
      alias :filter_boolean_eq :filter_eq
      alias :filter_uuid_eq :filter_eq

      def filter_not_eq(scope, attribute, value)
        scope.where.not(attribute => value)
      end
      alias :filter_integer_not_eq :filter_not_eq
      alias :filter_float_not_eq :filter_not_eq
      alias :filter_big_decimal_not_eq :filter_not_eq
      alias :filter_date_not_eq :filter_not_eq
      alias :filter_boolean_not_eq :filter_not_eq
      alias :filter_uuid_not_eq :filter_not_eq

      def filter_string_eq(scope, attribute, value, is_not: false)
        column = scope.klass.arel_table[attribute]
        clause = column.lower.eq_any(value.map(&:downcase))
        is_not ? scope.where.not(clause) : scope.where(clause)
      end

      def filter_string_eql(scope, attribute, value, is_not: false)
        clause = { attribute => value }
        is_not ? scope.where.not(clause) : scope.where(clause)
      end

      def filter_string_not_eq(scope, attribute, value)
        filter_string_eq(scope, attribute, value, is_not: true)
      end

      def filter_string_not_eql(scope, attribute, value)
        filter_string_eql(scope, attribute, value, is_not: true)
      end

      def filter_string_prefix(scope, attribute, value, is_not: false)
        column = scope.klass.arel_table[attribute]
        map = value.map { |v| "#{v}%" }
        clause = column.lower.matches_any(map)
        is_not ? scope.where.not(clause) : scope.where(clause)
      end

      def filter_string_not_prefix(scope, attribute, value)
        filter_string_prefix(scope, attribute, value, is_not: true)
      end

      def filter_string_suffix(scope, attribute, value, is_not: false)
        column = scope.klass.arel_table[attribute]
        map = value.map { |v| "%#{v}" }
        clause = column.lower.matches_any(map)
        is_not ? scope.where.not(clause) : scope.where(clause)
      end

      def filter_string_not_suffix(scope, attribute, value)
        filter_string_suffix(scope, attribute, value, is_not: true)
      end

      def filter_string_match(scope, attribute, value, is_not: false)
        column = scope.klass.arel_table[attribute]
        map = value.map { |v| "%#{v.downcase}%" }
        clause = column.lower.matches_any(map)
        is_not ? scope.where.not(clause) : scope.where(clause)
      end

      def filter_string_not_match(scope, attribute, value)
        filter_string_match(scope, attribute, value, is_not: true)
      end

      def filter_gt(scope, attribute, value)
        column = scope.klass.arel_table[attribute]
        scope.where(column.gt_any(value))
      end
      alias :filter_integer_gt :filter_gt
      alias :filter_float_gt :filter_gt
      alias :filter_big_decimal_gt :filter_gt
      alias :filter_datetime_gt :filter_gt
      alias :filter_date_gt :filter_gt

      def filter_gte(scope, attribute, value)
        column = scope.klass.arel_table[attribute]
        scope.where(column.gteq_any(value))
      end
      alias :filter_integer_gte :filter_gte
      alias :filter_float_gte :filter_gte
      alias :filter_big_decimal_gte :filter_gte
      alias :filter_datetime_gte :filter_gte
      alias :filter_date_gte :filter_gte

      def filter_lt(scope, attribute, value)
        column = scope.klass.arel_table[attribute]
        scope.where(column.lt_any(value))
      end
      alias :filter_integer_lt :filter_lt
      alias :filter_float_lt :filter_lt
      alias :filter_big_decimal_lt :filter_lt
      alias :filter_datetime_lt :filter_lt
      alias :filter_date_lt :filter_lt

      def filter_lte(scope, attribute, value)
        column = scope.klass.arel_table[attribute]
        scope.where(column.lteq_any(value))
      end
      alias :filter_integer_lte :filter_lte
      alias :filter_float_lte :filter_lte
      alias :filter_big_decimal_lte :filter_lte
      alias :filter_date_lte :filter_lte

      # Ensure fractional seconds don't matter
      def filter_datetime_eq(scope, attribute, value, is_not: false)
        ranges = value.map { |v| (v..v+1.second-0.00000001) }
        clause = { attribute => ranges }
        is_not ? scope.where.not(clause) : scope.where(clause)
      end

      def filter_datetime_not_eq(scope, attribute, value)
        filter_datetime_eq(scope, attribute, value, is_not: true)
      end

      def filter_datetime_lte(scope, attribute, value)
        value = value.map { |v| v + 1.second-0.00000001 }
        column = scope.klass.arel_table[attribute]
        scope.where(column.lteq_any(value))
      end

      def base_scope(model)
        model.all
      end

      # (see Adapters::Abstract#order)
      def order(scope, attribute, direction)
        scope.order(attribute => direction)
      end

      # (see Adapters::Abstract#paginate)
      def paginate(scope, current_page, per_page)
        scope.page(current_page).per(per_page)
      end

      # (see Adapters::Abstract#count)
      def count(scope, attr)
        if attr.to_sym == :total
          scope.distinct.count
        else
          scope.distinct.count(attr)
        end
      end

      # (see Adapters::Abstract#average)
      def average(scope, attr)
        scope.average(attr).to_f
      end

      # (see Adapters::Abstract#sum)
      def sum(scope, attr)
        scope.sum(attr)
      end

      # (see Adapters::Abstract#maximum)
      def maximum(scope, attr)
        scope.maximum(attr)
      end

      # (see Adapters::Abstract#minimum)
      def minimum(scope, attr)
        scope.minimum(attr)
      end

      # (see Adapters::Abstract#resolve)
      def resolve(scope)
        scope.to_a
      end

      # Run this write request within an ActiveRecord transaction
      # @param [Class] model_class The ActiveRecord class we are saving
      # @return Result of yield
      # @see Adapters::Abstract#transaction
      def transaction(model_class)
        model_class.transaction do
          yield
        end
      end

      def associate_all(parent, children, association_name, association_type)
        if activerecord_associate?(parent, children[0], association_name)
          association = parent.association(association_name)
          association.loaded!

          children.each do |child|
            if association_type == :many_to_many &&
                [:create, :update].include?(Graphiti.context[:namespace]) &&
                !parent.send(association_name).exists?(child.id)
              parent.send(association_name) << child
            else
              target = association.instance_variable_get(:@target)
              target |= [child]
              association.instance_variable_set(:@target, target)
            end
          end
        else
          super
        end
      end

      def associate(parent, child, association_name, association_type)
        if activerecord_associate?(parent, child, association_name)
          association = parent.association(association_name)
          association.loaded!
          association.instance_variable_set(:@target, child)
        else
          super
        end
      end

      # When a has_and_belongs_to_many relationship, we don't have a foreign
      # key that can be null'd. Instead, go through the ActiveRecord API.
      # @see Adapters::Abstract#disassociate
      def disassociate(parent, child, association_name, association_type)
        if association_type == :many_to_many
          parent.send(association_name).delete(child)
        else
          # Nothing to do here, happened when we merged foreign key
        end
      end

      # (see Adapters::Abstract#create)
      def create(model_class, create_params)
        instance = model_class.new(create_params)
        instance.save
        instance
      end

      # (see Adapters::Abstract#update)
      def update(model_class, update_params)
        instance = model_class.find(update_params.delete(:id))
        instance.update_attributes(update_params)
        instance
      end

      def destroy(model)
        model.destroy
        model
      end

      def save(model_instance)
        model_instance.save
        model_instance
      end

      def destroy(model_instance)
        model_instance.destroy
        model_instance
      end
    end
  end
end
