# Eager loading makes it so that you can load all associated records for a
# set of objects in a single query, instead of a separate query for each object.
#
# Two separate implementations are provided.  #eager should be used most of the
# time, as it loads associated records using one query per association.  However,
# it does not allow you the ability to filter based on columns in associated tables.  #eager_graph loads
# all records in one query.  Using #eager_graph you can filter based on columns in associated
# tables.  However, #eager_graph can be much slower than #eager, especially if multiple
# *_to_many associations are joined.
#
# You can cascade the eager loading (loading associations' associations)
# with no limit to the depth of the cascades.  You do this by passing a hash to #eager or #eager_graph
# with the keys being associations of the current model and values being
# associations of the model associated with the current model via the key.
#  
# The arguments can be symbols or hashes with symbol keys (for cascaded
# eager loading). Examples:
#
#   Album.eager(:artist).all
#   Album.eager_graph(:artist).all
#   Album.eager(:artist, :genre).all
#   Album.eager_graph(:artist, :genre).all
#   Album.eager(:artist).eager(:genre).all
#   Album.eager_graph(:artist).eager(:genre).all
#   Artist.eager(:albums=>:tracks).all
#   Artist.eager_graph(:albums=>:tracks).all
#   Artist.eager(:albums=>{:tracks=>:genre}).all
#   Artist.eager_graph(:albums=>{:tracks=>:genre}).all
module Sequel::Model::Associations::EagerLoading
  # Add the #eager! and #eager_graph! mutation methods to the dataset.
  def self.extended(obj)
    obj.def_mutation_method(:eager, :eager_graph)
  end

  # The preferred eager loading method.  Loads all associated records using one
  # query for each association.
  #
  # The basic idea for how it works is that the dataset is first loaded normally.
  # Then it goes through all associations that have been specified via eager.
  # It loads each of those associations separately, then associates them back
  # to the original dataset via primary/foreign keys.  Due to the necessity of
  # all objects being present, you need to use .all to use eager loading, as it
  # can't work with .each.
  #
  # This implementation avoids the complexity of extracting an object graph out
  # of a single dataset, by building the object graph out of multiple datasets,
  # one for each association.  By using a separate dataset for each association,
  # it avoids problems such as aliasing conflicts and creating cartesian product
  # result sets if multiple *_to_many eager associations are requested.
  #
  # One limitation of using this method is that you cannot filter the dataset
  # based on values of columns in an associated table, since the associations are loaded
  # in separate queries.  To do that you need to load all associations in the
  # same query, and extract an object graph from the results of that query. If you
  # need to filter based on columns in associated tables, look at #eager_graph
  # or join the tables you need to filter on manually. 
  #
  # Each association's order, if defined, is respected. Eager also works
  # on a limited dataset, but does not use any :limit options for associations.
  # If the association uses a block or has an :eager_block argument, it is used.
  def eager(*associations)
    model = check_model
    opt = @opts[:eager]
    opt = opt ? opt.dup : {}
    associations.flatten.each do |association|
      case association
        when Symbol
          check_association(model, association)
          opt[association] = nil
        when Hash
          association.keys.each{|assoc| check_association(model, assoc)}
          opt.merge!(association)
        else raise(Sequel::Error, 'Associations must be in the form of a symbol or hash')
      end
    end
    clone(:eager=>opt)
  end

  # The secondary eager loading method.  Loads all associations in a single query. This
  # method should only be used if you need to filter based on columns in associated tables.
  #
  # This method builds an object graph using Dataset#graph.  Then it uses the graph
  # to build the associations, and finally replaces the graph with a simple array
  # of model objects.
  #
  # Be very careful when using this with multiple *_to_many associations, as you can
  # create large cartesian products.  If you must graph multiple *_to_many associations,
  # make sure your filters are specific if you have a large database.
  # 
  # Each association's order, if definied, is respected. #eager_graph probably
  # won't work correctly on a limited dataset, unless you are
  # only graphing many_to_one associations.
  # 
  # Does not use the block defined for the association, since it does a single query for
  # all objects.  You can use the :graph_join_type, :graph_conditions, and :graph_join_table_conditions
  # association options to modify the SQL query.
  def eager_graph(*associations)
    model = check_model
    table_name = model.table_name
    ds = if @opts[:eager_graph]
      self
    else
      # Each of the following have a symbol key for the table alias, with the following values: 
      # :reciprocals - the reciprocal instance variable to use for this association
      # :requirements - array of requirements for this association
      # :alias_association_type_map - the type of association for this association
      # :alias_association_name_map - the name of the association for this association
      clone(:eager_graph=>{:requirements=>{}, :master=>model.table_name, :alias_association_type_map=>{}, :alias_association_name_map=>{}, :reciprocals=>{}})
    end
    ds.eager_graph_associations(ds, model, table_name, [], *associations)
  end
  
  protected

  # Call graph on the association with the correct arguments,
  # update the eager_graph data structure, and recurse into
  # eager_graph_associations if there are any passed in associations
  # (which would be dependencies of the current association)
  #
  # Arguments:
  # * ds - Current dataset
  # * model - Current Model
  # * ta - table_alias used for the parent association
  # * requirements - an array, used as a stack for requirements
  # * r - association reflection for the current association
  # * *associations - any associations dependent on this one
  def eager_graph_association(ds, model, ta, requirements, r, *associations)
    klass = r.associated_class
    assoc_name = r[:name]
    assoc_table_alias = ds.eager_unique_table_alias(ds, assoc_name)
    ds = r[:eager_grapher].call(ds, assoc_table_alias, ta)
    ds = ds.order_more(*Array(r[:order]).map{|c| eager_graph_qualify_order(assoc_table_alias, c)}) if r[:order] and r[:order_eager_graph]
    eager_graph = ds.opts[:eager_graph]
    eager_graph[:requirements][assoc_table_alias] = requirements.dup
    eager_graph[:alias_association_name_map][assoc_table_alias] = assoc_name
    eager_graph[:alias_association_type_map][assoc_table_alias] = r.returns_array?
    ds = ds.eager_graph_associations(ds, r.associated_class, assoc_table_alias, requirements + [assoc_table_alias], *associations) unless associations.empty?
    ds
  end

  # Check the associations are valid for the given model.
  # Call eager_graph_association on each association.
  #
  # Arguments:
  # * ds - Current dataset
  # * model - Current Model
  # * ta - table_alias used for the parent association
  # * requirements - an array, used as a stack for requirements
  # * *associations - the associations to add to the graph
  def eager_graph_associations(ds, model, ta, requirements, *associations)
    return ds if associations.empty?
    associations.flatten.each do |association|
      ds = case association
      when Symbol
        ds.eager_graph_association(ds, model, ta, requirements, check_association(model, association))
      when Hash
        association.each do |assoc, assoc_assocs|
          ds = ds.eager_graph_association(ds, model, ta, requirements, check_association(model, assoc), assoc_assocs)
        end
        ds
      else raise(Sequel::Error, 'Associations must be in the form of a symbol or hash')
      end
    end
    ds
  end

  # Build associations out of the array of returned object graphs.
  def eager_graph_build_associations(record_graphs)
    eager_graph = @opts[:eager_graph]
    master = eager_graph[:master]
    requirements = eager_graph[:requirements]
    alias_map = eager_graph[:alias_association_name_map]
    type_map = eager_graph[:alias_association_type_map]
    reciprocal_map = eager_graph[:reciprocals]

    # Make dependency map hash out of requirements array for each association.
    # This builds a tree of dependencies that will be used for recursion
    # to ensure that all parts of the object graph are loaded into the
    # appropriate subordinate association.
    dependency_map = {}
    # Sort the associations be requirements length, so that
    # requirements are added to the dependency hash before their
    # dependencies.
    requirements.sort_by{|a| a[1].length}.each do |ta, deps|
      if deps.empty?
        dependency_map[ta] = {}
      else
        deps = deps.dup
        hash = dependency_map[deps.shift]
        deps.each do |dep|
          hash = hash[dep]
        end
        hash[ta] = {}
      end
    end

    # This mapping is used to make sure that duplicate entries in the
    # result set are mapped to a single record.  For example, using a
    # single one_to_many association with 10 associated records,
    # the main object will appear in the object graph 10 times.
    # We map by primary key, if available, or by the object's entire values,
    # if not. The mapping must be per table, so create sub maps for each table
    # alias.
    records_map = {master=>{}}
    alias_map.keys.each{|ta| records_map[ta] = {}}

    # This will hold the final record set that we will be replacing the object graph with.
    records = []
    record_graphs.each do |record_graph|
      primary_record = record_graph[master]
      key = primary_record.pk || primary_record.values.sort_by{|x| x[0].to_s}
      if cached_pr = records_map[master][key]
        primary_record = cached_pr
      else
        records_map[master][key] = primary_record
        # Only add it to the list of records to return if it is a new record
        records.push(primary_record)
      end
      # Build all associations for the current object and it's dependencies
      eager_graph_build_associations_graph(dependency_map, alias_map, type_map, reciprocal_map, records_map, primary_record, record_graph)
    end

    # Remove duplicate records from all associations if this graph could possibly be a cartesian product
    eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map) if type_map.values.select{|v| v}.length > 1
    
    # Replace the array of object graphs with an array of model objects
    record_graphs.replace(records)
  end

  # Creates a unique table alias that hasn't already been used in the query.
  # Will either be the table_alias itself or table_alias_N for some integer
  # N (starting at 0 and increasing until an unused one is found).
  def eager_unique_table_alias(ds, table_alias)
    used_aliases = ds.opts[:from]
    graph = ds.opts[:graph]
    used_aliases += graph[:table_aliases].keys if graph
    if used_aliases.include?(table_alias)
      i = 0
      loop do
        ta = :"#{table_alias}_#{i}"
        return ta unless used_aliases.include?(ta)
        i += 1
      end
    end
    table_alias
  end

  private

  # Make sure this dataset is associated with a model, and return the default model for it.
  def check_model
    raise(Sequel::Error, 'No model for this dataset') unless @opts[:models] && model = @opts[:models][nil]
    model
  end

  # Make sure the association is valid for this model, and return the related AssociationReflection.
  def check_association(model, association)
    raise(Sequel::Error, 'Invalid association') unless reflection = model.association_reflection(association)
    raise(Sequel::Error, "Eager loading is not allowed for #{model.name} association #{association}") if reflection[:allow_eager] == false
    reflection
  end

  # Build associations for the current object.  This is called recursively
  # to build object's dependencies.
  def eager_graph_build_associations_graph(dependency_map, alias_map, type_map, reciprocal_map, records_map, current, record_graph)
    return if dependency_map.empty?
    # Don't clobber the instance variable array for *_to_many associations if it has already been setup
    dependency_map.keys.each do |ta|
      assoc_name = alias_map[ta]
      current.associations[assoc_name] = type_map[ta] ? [] : nil unless current.associations.include?(assoc_name)
    end
    dependency_map.each do |ta, deps|
      next unless rec = record_graph[ta]
      key = rec.pk || rec.values.sort_by{|x| x[0].to_s}
      if cached_rec = records_map[ta][key]
        rec = cached_rec
      else
        records_map[ta][rec.pk] = rec
      end
      assoc_name = alias_map[ta]
      case type_map[ta]
      when false
        current.associations[assoc_name] = rec
      else
        current.associations[assoc_name].push(rec) 
        if reciprocal = reciprocal_map[ta]
          rec.associations[reciprocal] = current
        end
      end
      # Recurse into dependencies of the current object
      eager_graph_build_associations_graph(deps, alias_map, type_map, reciprocal_map, records_map, rec, record_graph)
    end
  end

  # If the result set is the result of a cartesian product, then it is possible that
  # there a multiple records for each association when there should only be one.
  # In that case, for each object in all associations loaded via #eager_graph, run
  # uniq! on the association instance variables to make sure no duplicate records show up.
  # Note that this can cause legitimate duplicate records to be removed.
  def eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map)
    records.each do |record|
      dependency_map.each do |ta, deps|
        list = if !type_map[ta]
          item = record.send(alias_map[ta])
          [item] if item
        else
          list = record.send(alias_map[ta])
          list.uniq!
        end
        # Recurse into dependencies
        eager_graph_make_associations_unique(list, deps, alias_map, type_map) if list
      end
    end
  end

  # Qualify the given expression if necessary.  The only expressions which are qualified are
  # unqualified symbols and identifiers, either of which may by sorted.
  def eager_graph_qualify_order(table_alias, expression)
    case expression
    when Symbol
      table, column, aliaz = split_symbol(expression)
      raise(Sequel::Error, "Can't use an aliased expression in the :order option") if aliaz
      table ? expression : Sequel::SQL::QualifiedIdentifier.new(table_alias, expression)
    when Sequel::SQL::Identifier
      Sequel::SQL::QualifiedIdentifier.new(table_alias, expression)
    when Sequel::SQL::OrderedExpression
      Sequel::SQL::OrderedExpression.new(eager_graph_qualify_order(table_alias, expression.expression), expression.descending)
    else
      expression
    end
  end

  # Eagerly load all specified associations 
  def eager_load(a)
    return if a.empty?
    # Current model class
    model = @opts[:models][nil]
    # All associations to eager load
    eager_assoc = @opts[:eager]
    # Key is foreign/primary key name symbol
    # Value is hash with keys being foreign/primary key values (generally integers)
    #  and values being an array of current model objects with that
    #  specific foreign/primary key
    key_hash = {}
    # Reflections for all associations to eager load
    reflections = eager_assoc.keys.collect{|assoc| model.association_reflection(assoc)}

    # Populate keys to monitor
    reflections.each{|reflection| key_hash[reflection.eager_loader_key] ||= Hash.new{|h,k| h[k] = []}}
    
    # Associate each object with every key being monitored
    a.each do |rec|
      key_hash.each do |key, id_map|
        id_map[rec[key]] << rec if rec[key]
      end
    end
    
    reflections.each do |r|
      r[:eager_loader].call(key_hash, a, eager_assoc[r[:name]])
      a.each{|object| object.send(:run_association_callbacks, r, :after_load, object.associations[r[:name]])} unless r[:after_load].empty?
    end 
  end

  # Build associations from the graph if #eager_graph was used, 
  # and/or load other associations if #eager was used.
  def post_load(all_records)
    eager_graph_build_associations(all_records) if @opts[:eager_graph]
    eager_load(all_records) if @opts[:eager]
  end
end
