module DryCrud
  # Sort functionality for the index table.
  # Define a default sort expression that is always appended to the
  # current sort params with the class attribute +default_sort+.
  module Sortable
    extend ActiveSupport::Concern

    included do
      class_attribute :sort_mappings_with_indifferent_access
      self.sort_mappings = {}

      class_attribute :default_sort

      helper_method :sortable?

      prepend Prepends
    end

    # Class methods for sorting.
    module ClassMethods
      # Define a map of (virtual) attributes to SQL order expressions.
      # May be used for sorting table columns that do not appear directly
      # in the database table. E.g., map city_id: 'cities.name' to
      # sort the displayed city names.
      def sort_mappings=(hash)
        self.sort_mappings_with_indifferent_access =
          hash.with_indifferent_access
      end
    end

    # Prepended methods for sorting.
    module Prepends
      private

      # Enhance the list entries with an optional sort order.
      def list_entries
        sortable = sortable?(params[:sort])
        if sortable || default_sort
          clause = [ sortable ? sort_expression : nil, default_sort ]
          super.reorder(Arel.sql(clause.compact.join(", ")))
        else
          super
        end
      end

      # Return the sort expression to be used in the list query.
      def sort_expression
        col = sort_mappings_with_indifferent_access[params[:sort]] ||
              "#{model_class.table_name}.#{params[:sort]}"
        "#{col} #{sort_dir}"
      end

      # The sort direction, either 'asc' or 'desc'.
      def sort_dir
        params[:sort_dir] == "desc" ? "DESC" : "ASC"
      end

      # Returns true if the passed attribute is sortable.
      def sortable?(attr)
        attr.present? && (
        model_class.column_names.include?(attr.to_s) ||
        sort_mappings_with_indifferent_access.include?(attr))
      end
    end
  end
end
