module AutocompleteRails
  module Controller
    def self.included(target)
      target.extend AutocompleteRails::Controller::ClassMethods
    end

    module ClassMethods
      #
      # Generate an autocomplete controller action.
      #
      # The controller action is intended to interact with jquery UI's autocomplete widget. The autocomplete
      # controller provides suggestions while you type into a field that is provisioned with JQuery's autocomplete
      # widget.
      #
      # The generated method is named  "autocomplete_#{model_symbol}_#{value_method}", for example:
      #
      #   class UsersController
      #     autocomplete :user, :email
      #   end
      #
      # generates a method named `autocomplete_user_email`.
      #
      #
      # Parameters:
      # * model_symbol - model class to autocomplete, e.g.
      # * value_method - method on model to autocomplete. This supplies the 'value' field in results.
      #                  Also used as the label unless you supply options[:label_method].
      # * options - hash of optional settings.
      #
      #
      # Options accepts a hash of:
      # * :label_method - call a separate method for the label, otherwise defaults to value_method. If your label
      #                   method is a method that is *not* a column in your DB, you may need options[:full_model].
      # * :full_model - load full model instead of only selecting the specified values. Default is false.
      # * :limit - default is 10.
      # * :case_sensitive - if true, the search is case sensitive. Default is false.
      # * :additional_data - collect additional data. Will be added to select unless full_model is invoked.
      # * :full_search - search the entire value string for the term. Defaults to false, in which case the value
      #                  field being searched (see value_method above) must start with the search term.
      # * :scopes - limit query to these ActiveRecord scopes, passed in as an array,
      #             for example: `scopes: [:scope1, :scope2]`
      #
      # Be sure to add a route to reach the generated controller method. Example:
      #
      #   resources :users do
      #     get :autocomplete_user_email, on: :collection
      #   end
      #
      # The following example searches for users by email, but displays their :full_name as the label.
      # The full_model flag is also loaded, as full_name is a method that synthesizes multiple columns.
      #
      #   class UsersController
      #     autocomplete :user, :email, label_method: full_name, full_model: true
      #   end
      #
      def autocomplete(model_symbol, value_method, options = {})
        label_method = options[:label_method] || value_method
        model = model_symbol.to_s.camelize.constantize
        autocomplete_method_name = "autocomplete_#{model_symbol}_#{value_method}"

        define_method(autocomplete_method_name) do
          results = autocomplete_results(model, value_method, label_method, options)
          render json: autocomplete_build_json(results, value_method, label_method, options), root: false
        end
      end
    end

    protected

    def autocomplete_results(model, value_method, label_method = nil, options)
      term = params[:term]
      return {} if term.blank?

      results = model.where(nil) # make an empty scope to add select, where, etc, to.
      scopes  = Array(options[:scopes])
      scopes.each { |scope| results = results.send(scope) } unless scopes.empty?
      results = results.select(autocomplete_select_clause(model, value_method, label_method, options)) unless
        options[:full_model]
      results.
        where(autocomplete_where_clause(term, model, value_method, options)).
        limit(autocomplete_limit_clause(options)).
        order(autocomplete_order_clause(model, value_method, options))
    end

    def autocomplete_select_clause(model, value_method, label_method, options)
      table_name = model.table_name
      selects = []
      selects << "#{table_name}.#{model.primary_key}"
      selects << "#{table_name}.#{value_method}"
      selects << "#{table_name}.#{label_method}" if label_method
      options[:additional_data].each { |datum| selects << "#{table_name}.#{datum}" } if options[:additional_data]
      selects
    end

    def autocomplete_where_clause(term, model, value_method, options)
      term = term.gsub(/[_%]/) { |x| "\\#{x}" } # escape any _'s or %'s in the search term
      term = "#{term}%"
      term = "%#{term}" if options[:full_search]
      table_name = model.table_name
      lower = options[:case_sensitive] ? '' : 'LOWER'
      ["#{lower}(#{table_name}.#{value_method}) LIKE #{lower}(?) ESCAPE \"\\\"", term]
    end

    def autocomplete_limit_clause(options)
      options[:limit] ||= 10
    end

    def autocomplete_order_clause(model, value_method, options)
      return options[:order] if options[:order]

      # default to ASC order
      table_prefix = "#{model.table_name}."
      "LOWER(#{table_prefix}#{value_method}) ASC"
    end

    def autocomplete_build_json(results, value_method, label_method, options)
      results.collect do |result|
        data = HashWithIndifferentAccess.new(id: result.id,
                                             label: result.send(label_method),
                                             value: result.send(value_method))
        options[:additional_data].each do |method|
          data[method] = result.send(method)
        end if options[:additional_data]
        data
      end
    end

    def postgres?(model_class)
      model_class.connection.class.to_s.match /Postgre/
    end
  end
end