module DryCrud
  # The search functionality for the index table.
  # Define an array of searchable string columns in your subclassing
  # controllers using the class attribute +search_columns+.
  module Searchable
    extend ActiveSupport::Concern

    included do
      class_attribute :search_columns
      self.search_columns = []

      helper_method :search_support?

      prepend Prepends
    end

    # Prepended methods for searching.
    module Prepends
      private

      # Enhance the list entries with an optional search criteria
      def list_entries
        super.where(search_conditions)
      end

      # Concat the word clauses with AND.
      def search_conditions
        if search_support? && params[:q].present?
          search_word_conditions.reduce do |query, condition|
            query.and(condition)
          end
        end
      end

      # Split the search query in single words and create a list of word
      # clauses.
      def search_word_conditions
        params[:q].split(/\s+/).map { |w| search_word_condition(w) }
      end

      # Concat the column queries of the given word with OR.
      def search_word_condition(word)
        search_column_condition(word).reduce do |query, condition|
          query.or(condition)
        end
      end

      # Create a list of Arel #matches queries for each column and the given
      # word.
      def search_column_condition(word)
        self.class.search_tables_and_fields.map do |table_name, field|
          table = Arel::Table.new(table_name)
          table[field].matches(Arel::Nodes::Quoted.new("%#{word}%"))
        end
      end

      # Returns true if this controller has searchable columns.
      def search_support?
        search_columns.present?
      end
    end

    # Class methods for Searchable.
    module ClassMethods
      # All search columns divided in table and field names.
      def search_tables_and_fields
        @search_tables_and_fields ||= search_columns.map do |f|
          if f.to_s.include?(".")
            f.split(".", 2)
          else
            [ model_class.table_name, f ]
          end
        end
      end
    end
  end
end
