require 'active_support'
require 'active_record'
require 'active_support/core_ext/module/delegation'

module ActiveRecord
  module NamedScope
    module ClassMethods
      # Initialize a MassiveScope, which, when inspected, does not generate a huge string.
      def massive_scoped(options = {}, &block)
        if options.present?
          MassiveScope.init(self, options, &block)
        else
          raise "MassiveScopes should be created with options"
        end
      end
    end
    class MassiveScope < Scope
      # Don't try to output a massive string.
      def inspect
        "<Massive scope: #{count} members>"
      end
      # Don't try to put everything into json.
      def to_json(*args)
        { :members => count }.to_json
      end
    end
  end
end

module CohortScope
  def self.extended(base)
    base.class_eval do
      cattr_accessor :minimum_cohort_size, :instance_writer => false
    end
  end

  # Find the biggest scope possible by removing constraints <b>in any order</b>.
  # Returns an empty scope if it can't meet the minimum scope size.
  def big_cohort(constraints)
    raise ArgumentError, "You can't give a big_cohort an OrderedHash; do you want strict_cohort?" if constraints.is_a?(ActiveSupport::OrderedHash)
    _cohort_massive_scope constraints
  end

  # Find the first acceptable scope by removing constraints <b>in strict order</b>, starting with the last constraint.
  # Returns an empty scope if it can't meet the minimum scope size.
  #
  # <tt>constraints</tt> must be an <tt>ActiveSupport::OrderedHash</tt> (no support for ruby 1.9's natively ordered hashes yet).
  #
  # Note that the first constraint is implicitly required.
  #
  # Take this example, where favorite color is considered to be "more important" than birthdate:
  #
  #   ordered_constraints = ActiveSupport::OrderedHash.new
  #   ordered_constraints[:favorite_color] = 'heliotrope'
  #   ordered_constraints[:birthdate] = '1999-01-01'
  #   Citizen.strict_cohort(ordered_constraints) #=> [...]
  #
  # If the original constraints don't meet the minimum scope size, then the only constraint that can be removed is birthdate.
  # In other words, this would never return a scope that was constrained on birthdate but not on favorite_color.
  def strict_cohort(constraints)
    raise ArgumentError, "You need to give strict_cohort an OrderedHash" unless constraints.is_a?(ActiveSupport::OrderedHash)
    _cohort_massive_scope constraints
  end

  protected

  # Recursively look for a scope that meets the constraints and is at least <tt>minimum_cohort_size</tt>.
  def _cohort_massive_scope(constraints)
    raise RuntimeError, "You need to set #{name}.minimum_cohort_size = X" unless minimum_cohort_size.present?
    
    if constraints.values.none? # failing base case
      return massive_scoped(:conditions => 'false')
    end
    
    this_hash = _cohort_constraints constraints
    this_count = scoped(this_hash).count
    
    if this_count >= minimum_cohort_size # successful base case
      massive_scoped this_hash
    else
      _cohort_massive_scope _cohort_reduce_constraints(constraints)
    end
  end
  
  # Sanitize constraints by
  # * removing nil constraints (so constraints like "X IS NULL" are impossible, sorry)
  # * converting ActiveRecord::Base objects into integer foreign key constraints
  def _cohort_constraints(constraints)
    new_hash = constraints.is_a?(ActiveSupport::OrderedHash) ? ActiveSupport::OrderedHash.new : Hash.new
    conditions = constraints.inject(new_hash) do |memo, tuple|
      k, v = tuple
      if v.kind_of?(ActiveRecord::Base)
        condition = { _cohort_association_primary_key(k) => v.to_param }
      elsif !v.nil?
        condition = { k => v }
      end
      memo.merge! condition if condition.is_a? Hash
      memo
    end
    { :conditions => conditions }
  end
  
  # Convert constraints that are provided as ActiveRecord::Base objects into their corresponding integer primary keys.
  #
  # Only works for <tt>belongs_to</tt> relationships.
  #
  # For example, :car => <#Car> might get translated into :car_id => 44.
  def _cohort_association_primary_key(name)
    @_cohort_association_primary_keys ||= {}
    return @_cohort_association_primary_keys[name] if @_cohort_association_primary_keys.has_key? name
    a = reflect_on_association name
    raise "can't use cohort scope on :through associations (#{self.name} #{name})" if a.options.has_key? :through
    if !a.primary_key_name.blank?
      @_cohort_association_primary_keys[name] = a.primary_key_name
    else
      raise "we need some other way to find primary key"
    end
  end
  
  # Choose how to reduce constraints based on whether we're looking for a big cohort or a strict cohort.
  def _cohort_reduce_constraints(constraints)
    case constraints
    when ActiveSupport::OrderedHash
      _cohort_reduce_constraints_in_order constraints
    when Hash
      _cohort_reduce_constraints_seeking_maximum_count constraints
    else
      raise "what did you pass me? #{constraints}"
    end
  end
  
  # (Used by <tt>big_cohort</tt>)
  #
  # Reduce constraints by removing them one by one and counting the results.
  #
  # The constraint whose removal leads to the highest record count is removed from the overall constraint set.
  def _cohort_reduce_constraints_seeking_maximum_count(constraints)
    highest_count_after_removal = nil
    losing_key = nil
    constraints.keys.each do |key|
      test_constraints = constraints.except(key)
      count_after_removal = scoped(_cohort_constraints(test_constraints)).count
      if highest_count_after_removal.nil? or count_after_removal > highest_count_after_removal
        highest_count_after_removal = count_after_removal
        losing_key = key
      end
    end
    constraints.except losing_key
  end

  # (Used by <tt>strict_cohort</tt>)
  #
  # Reduce constraints by removing the least important one.
  def _cohort_reduce_constraints_in_order(constraints)
    reduced_constraints = constraints.dup
    reduced_constraints.delete constraints.keys.last
    reduced_constraints
  end
end