# frozen_string_literal: true

require "active_support/core_ext/array/extract"

module ActiveRecord
  class Relation
    class WhereClause # :nodoc:
      delegate :any?, :empty?, to: :predicates

      def initialize(predicates)
        @predicates = predicates
      end

      def +(other)
        WhereClause.new(predicates + other.predicates)
      end

      def -(other)
        WhereClause.new(predicates - other.predicates)
      end

      def |(other)
        WhereClause.new(predicates | other.predicates)
      end

      def merge(other, rewhere = nil)
        predicates = if rewhere
          except_predicates(other.extract_attributes)
        else
          predicates_unreferenced_by(other)
        end

        WhereClause.new(predicates | other.predicates)
      end

      def except(*columns)
        WhereClause.new(except_predicates(columns))
      end

      def or(other)
        left = self - other
        common = self - left
        right = other - common

        if left.empty? || right.empty?
          common
        else
          left = left.ast
          left = left.expr if left.is_a?(Arel::Nodes::Grouping)

          right = right.ast
          right = right.expr if right.is_a?(Arel::Nodes::Grouping)

          or_clause = Arel::Nodes::Or.new(left, right)

          common.predicates << Arel::Nodes::Grouping.new(or_clause)
          common
        end
      end

      def to_h(table_name = nil)
        equalities(predicates).each_with_object({}) do |node, hash|
          next if table_name&.!= node.left.relation.name
          name = node.left.name.to_s
          value = extract_node_value(node.right)
          hash[name] = value
        end
      end

      def ast
        predicates = predicates_with_wrapped_sql_literals
        predicates.one? ? predicates.first : Arel::Nodes::And.new(predicates)
      end

      def ==(other)
        other.is_a?(WhereClause) &&
          predicates == other.predicates
      end

      def invert(as = :nand)
        if predicates.size == 1
          inverted_predicates = [ invert_predicate(predicates.first) ]
        elsif as == :nor
          inverted_predicates = predicates.map { |node| invert_predicate(node) }
        else
          inverted_predicates = [ Arel::Nodes::Not.new(ast) ]
        end

        WhereClause.new(inverted_predicates)
      end

      def self.empty
        @empty ||= new([]).freeze
      end

      def contradiction?
        predicates.any? do |x|
          case x
          when Arel::Nodes::In
            Array === x.right && x.right.empty?
          when Arel::Nodes::Equality
            x.right.respond_to?(:unboundable?) && x.right.unboundable?
          end
        end
      end

      def extract_attributes
        predicates.each_with_object([]) do |node, attrs|
          attr = extract_attribute(node) || begin
            node.left if node.equality? && node.left.is_a?(Arel::Predications)
          end
          attrs << attr if attr
        end
      end

      protected
        attr_reader :predicates

        def referenced_columns
          predicates.each_with_object({}) do |node, hash|
            attr = extract_attribute(node) || begin
              node.left if equality_node?(node) && node.left.is_a?(Arel::Predications)
            end

            hash[attr] = node if attr
          end
        end

      private
        def extract_attribute(node)
          attr_node = nil
          Arel.fetch_attribute(node) do |attr|
            return if attr_node&.!= attr # all attr nodes should be the same
            attr_node = attr
          end
          attr_node
        end

        def equalities(predicates)
          equalities = []

          predicates.each do |node|
            if equality_node?(node)
              equalities << node
            elsif node.is_a?(Arel::Nodes::And)
              equalities.concat equalities(node.children)
            end
          end

          equalities
        end

        def predicates_unreferenced_by(other)
          referenced_columns = other.referenced_columns

          predicates.reject do |node|
            attr = extract_attribute(node) || begin
              node.left if equality_node?(node) && node.left.is_a?(Arel::Predications)
            end
            next false unless attr

            ref = referenced_columns[attr]
            next false unless ref

            if equality_node?(node) && equality_node?(ref) || node == ref
              true
            else
              ActiveSupport::Deprecation.warn(<<-MSG.squish)
                Merging (#{node.to_sql}) and (#{ref.to_sql}) no longer maintain
                both conditions, and will be replaced by the latter in Rails 6.2.
                To migrate to Rails 6.2's behavior, use `relation.merge(other, rewhere: true)`.
              MSG
              false
            end
          end
        end

        def equality_node?(node)
          !node.is_a?(String) && node.equality?
        end

        def invert_predicate(node)
          case node
          when NilClass
            raise ArgumentError, "Invalid argument for .where.not(), got nil."
          when String
            Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(node))
          else
            node.invert
          end
        end

        def except_predicates(columns)
          attrs = columns.extract! { |node| node.is_a?(Arel::Attribute) }
          non_attrs = columns.extract! { |node| node.is_a?(Arel::Predications) }

          predicates.reject do |node|
            if !non_attrs.empty? && node.equality? && node.left.is_a?(Arel::Predications)
              non_attrs.include?(node.left)
            end || Arel.fetch_attribute(node) do |attr|
              attrs.include?(attr) || columns.include?(attr.name.to_s)
            end
          end
        end

        def predicates_with_wrapped_sql_literals
          non_empty_predicates.map do |node|
            case node
            when Arel::Nodes::SqlLiteral, ::String
              wrap_sql_literal(node)
            else node
            end
          end
        end

        ARRAY_WITH_EMPTY_STRING = [""]
        def non_empty_predicates
          predicates - ARRAY_WITH_EMPTY_STRING
        end

        def wrap_sql_literal(node)
          if ::String === node
            node = Arel.sql(node)
          end
          Arel::Nodes::Grouping.new(node)
        end

        def extract_node_value(node)
          case node
          when Array
            node.map { |v| extract_node_value(v) }
          when Arel::Nodes::BindParam, Arel::Nodes::Casted, Arel::Nodes::Quoted
            node.value_before_type_cast
          end
        end
    end
  end
end
