module Ransack
  module Nodes
    class Grouping < Node
      attr_reader :conditions
      attr_accessor :combinator
      alias :m :combinator
      alias :m= :combinator=

      i18n_word :condition, :and, :or
      i18n_alias :c => :condition, :n => :and, :o => :or

      delegate :each, :to => :values

      def initialize(context, combinator = nil)
        super(context)
        self.combinator = combinator.to_s if combinator
      end

      def persisted?
        false
      end

      def translate(key, options = {})
        super or Translate.attribute(
          key.to_s, options.merge(:context => context)
        )
      end

      def conditions
        @conditions ||= []
      end
      alias :c :conditions

      def conditions=(conditions)
        case conditions
        when Array
          conditions.each do |attrs|
            condition = Condition.new(@context).build(attrs)
            self.conditions << condition if condition.valid?
          end
        when Hash
          conditions.each do |index, attrs|
            condition = Condition.new(@context).build(attrs)
            self.conditions << condition if condition.valid?
          end
        end
        remove_duplicate_conditions!
      end
      alias :c= :conditions=

      def [](key)
        conditions.detect { |c| c.key == key.to_s }
      end

      def []=(key, value)
        conditions.reject! { |c| c.key == key.to_s }
        self.conditions << value
      end

      def values
        conditions + groupings
      end

      def respond_to?(method_id)
        super or begin
          method_name = method_id.to_s
          attribute_method?(method_name) ? true : false
        end
      end

      def build_condition(opts = {})
        new_condition(opts).tap do |condition|
          self.conditions << condition
        end
      end

      def new_condition(opts = {})
        attrs = opts[:attributes] || 1
        vals = opts[:values] || 1
        condition = Condition.new(@context)
        attrs.times { condition.build_attribute }
        vals.times { condition.build_value }
        condition
      end

      def groupings
        @groupings ||= []
      end
      alias :g :groupings

      def groupings=(groupings)
        case groupings
        when Array
          groupings.each do |attrs|
            grouping_object = new_grouping(attrs)
            self.groupings << grouping_object if grouping_object.values.any?
          end
        when Hash
          groupings.each do |index, attrs|
            grouping_object = new_grouping(attrs)
            self.groupings << grouping_object if grouping_object.values.any?
          end
        else
          raise ArgumentError,
            "Invalid argument (#{groupings.class}) supplied to groupings="
        end
      end
      alias :g= :groupings=

      def method_missing(method_id, *args)
        method_name = method_id.to_s.dup
        writer = method_name.sub!(/\=$/, ''.freeze)
        if attribute_method?(method_name)
          if writer
            write_attribute(method_name, *args)
          else
            read_attribute(method_name)
          end
        else
          super
        end
      end

      def attribute_method?(name)
        stripped_name = strip_predicate_and_index(name)
        return true if @context.attribute_method?(stripped_name) ||
                       @context.attribute_method?(name)
        case stripped_name
        when /^(g|c|m|groupings|conditions|combinator)=?$/
          true
        else
          stripped_name
          .split(/_and_|_or_/)
          .none? { |n| !@context.attribute_method?(n) }
        end
      end

      def build_grouping(params = {})
        params ||= {}
        new_grouping(params).tap do |new_grouping|
          self.groupings << new_grouping
        end
      end

      def new_grouping(params = {})
        Grouping.new(@context).build(params)
      end

      def build(params)
        params.with_indifferent_access.each do |key, value|
          case key
          when /^(g|c|m)$/
            self.send("#{key}=", value)
          else
            write_attribute(key.to_s, value)
          end
        end
        self
      end

      def inspect
        data = [
          ['conditions'.freeze, conditions], [Constants::COMBINATOR, combinator]
        ]
        .reject { |e| e[1].blank? }
        .map { |v| "#{v[0]}: #{v[1]}" }
        .join(', '.freeze)
        "Grouping <#{data}>"
      end

      private

      def write_attribute(name, val)
        # TODO: Methods
        if condition = Condition.extract(@context, name, val)
          self[name] = condition
        end
      end

      def read_attribute(name)
        if self[name].respond_to?(:value)
          self[name].value
        else
          self[name]
        end
      end

      def strip_predicate_and_index(str)
        string = str[/(.+?)\(/, 1] || str.dup
        Predicate.detect_and_strip_from_string!(string)
        string
      end

      def remove_duplicate_conditions!
        # If self.conditions.uniq! is called without passing a block, then
        # conditions differing only by ransacker_args within attributes are
        # wrongly considered equal and are removed.
        self.conditions.uniq! do |c|
          c.attributes.map { |a| [a.name, a.ransacker_args] }.flatten +
          [c.predicate.name] +
          c.values.map { |v| v.value }
        end
      end
    end
  end
end
