require 'sequel'

Sequel.extension :blank

require 'philtre/predicate_splitter'
require 'philtre/predicate_dsl'
require 'philtre/predicates'

module Philtre
  # Parse the predicates on the end of field names, and round-trip the search fields
  # between incoming params, controller and views.
  # So,
  #
  #   filter_parameters = {
  #     birth_year: ['2012', '2011'],
  #     title_like: 'sir',
  #     order: ['title', 'name_asc', 'birth_year_desc'],
  #   }
  #
  #   Philtre.new( filter_parameters ).apply( Personage.dataset ).sql
  #
  # should result in
  #
  #   SELECT * FROM "personages" WHERE (("birth_year" IN ('2012', '2011')) AND ("title" ~* 'bar')) ORDER BY ("title" ASC, "name" ASC, "date" DESC)
  #
  # TODO pass a predicates: parameter in here to specify a predicates object.
  class Filter
    def initialize( filter_parameters = nil, &custom_predicate_block )
      # This must be a new instance of Hash, because sometimes
      # HashWithIndifferentAccess is passed in, which breaks things in here.

      # Don't use symbolize_keys because that creates a dependency on ActiveSupport
      # have to iterate anyway to convert keys to symbols
      @filter_parameters =
      if filter_parameters
        filter_parameters.each_with_object(Hash.new){|(k,v),ha| ha[k.to_sym] = v}
      else
        {}
      end

      if block_given?
        predicates.extend_with &custom_predicate_block
      end
    end

    attr_reader :filter_parameters

    def empty?; filter_parameters.empty? end

    # return a modified dataset containing all the predicates
    def call( dataset )
      # mainly for Sequel::Model
      dataset = dataset.dataset if dataset.respond_to? :dataset

      # clone here so later order! calls don't mess with a Model's default dataset
      dataset = expressions.inject(dataset.clone) do |dataset, filter_expr|
        dataset.filter( filter_expr )
      end

      # preserve existing order if we don't have one.
      if order_clause.empty?
        dataset
      else
        # There might be multiple orderings in the order_clause
        dataset.order *order_clause
      end
    end

    alias apply call

    # called by valued_parameters to generate the set of expressions. This
    # returns true for a value that show up in the set of expressions, false
    # otherwise.
    #
    # Intended to be overridden if necessary.
    def valued_parameter?( key, value )
      value.is_a?(Array) || !value.blank?
    end

    # Values in the parameter list which are not blank, and not
    # an ordering. That is, parameters which will be used to generate
    # the filter expression.
    def valued_parameters
      @valued_parameters ||= filter_parameters.select do |key,value|
        # :order is special, it must always be excluded
        key.to_sym != :order && valued_parameter?(key,value)
      end
    end

    # The set of expressions from the filter_parameters with values.
    def expressions
      valued_parameters.map do |key, value|
        to_expr(key, value)
      end
    end

    def self.predicates
      @predicates ||= Predicates.new
    end

    # Hash of predicate names to blocks. One way to get custom predicates is
    # to subclass filter and override this.
    def predicates
      # don't mess with the class' minimal set
      @predicates ||= self.class.predicates.clone
    end

    attr_writer :predicates

    def order_expr( order_predicate )
      return if order_predicate.blank?

      splitter = PredicateSplitter.new( order_predicate, nil )
      case
      when splitter === :asc
        Sequel.asc splitter.field
      when splitter === :desc
        Sequel.desc splitter.field
      else
        Sequel.asc splitter.field
      end
    end

    def order_for( order_field )
      order_hash[order_field]
    end

    # return a possibly empty array of Sequel order expressions
    def order_clause
      @order_clause ||= order_expressions.map{|e| e.last}
    end

    # Associative array (not a Hash) of names to order expressions
    # TODO this should just be a hash
    def order_expressions
      @order_expressions ||=
      [filter_parameters[:order]].flatten.map do |order_predicate|
        next if order_predicate.blank?
        expr = order_expr order_predicate
        [expr.expression, expr]
      end.compact
    end

    def order_hash
      @order_hash ||= Hash[ order_expressions ]
    end

    # turn a filter_parameter key => value into a Sequel::SQL::Expression subclass
    # field will be the field name ultimately used in the expression. Defaults to key.
    def to_expr( key, value, field = nil )
      Sequel.expr( predicates[key, value, field] )
    end

    # turn the expression at predicate into a Sequel expression with
    # field, having the value for predicate. Will be nil if the
    # predicate has no value in valued_parameters.
    # Will always be a Sequel::SQL::Expression.
    def expr_for( predicate, field = nil )
      unless (value = valued_parameters[predicate]).blank?
        to_expr( predicate, value, field )
      end
    end

    # for use in forms
    def to_h(all=false)
      filter_parameters.select{|k,v| all || !v.blank?}
    end

    attr_writer :filter_parameters
    protected :filter_parameters=

    # deallocate any cached lazies
    def initialize_copy( *args )
      super
      @order_expressions = nil
      @order_hash = nil
      @order_clause = nil
      @valued_parameters = nil
    end

    def clone( extra_parameters = {} )
      new_filter = super()

      # and explicitly clone these because they may well be modified
      new_filter.filter_parameters = filter_parameters.clone
      new_filter.predicates = predicates.clone

      extra_parameters.each do |key,value|
        new_filter[key] = value
      end

      new_filter
    end

    # return a new filter including only the specified filter parameters/predicates.
    # NOTE predicates are not the same as field names.
    # args to select_block are the same as to filter_parameters, ie it's a Hash
    # TODO should use clone
    def subset( *keys, &select_block )
      subset_params =
      if block_given?
        filter_parameters.select &select_block
      else
        filter_parameters.slice( *keys )
      end
      subset = self.class.new( subset_params )
      subset.predicates = predicates.clone
      subset
    end

    # return a subset of filter parameters/predicates,
    # but leave this object without the matching keys.
    # NOTE does not operate on field names.
    def extract!( *keys, &select_block )
      rv = subset( *keys, &select_block )
      rv.to_h.keys.each do |key|
        filter_parameters.delete( key )
      end
      rv
    end

    # hash of keys to expressions, but only where
    # there are values.
    def expr_hash
      vary = valued_parameters.map do |key, value|
        [ key, to_expr(key, value) ]
      end

      Hash[ vary ]
    end

    # easier access for filter_parameters
    # return nil for nil and '' and []
    def []( key )
      rv = filter_parameters[key]
      rv unless rv.blank?
    end

    # easier access for filter_parameters
    def []=(key, value)
      filter_parameters[key] = value
    end
  end
end