class Graphiti::Sideload::PolymorphicBelongsTo < Graphiti::Sideload::BelongsTo
  class Group
    attr_reader :name, :calls

    def initialize(name)
      @name = name
      @calls = []
    end

    def method_missing(name, *args, &blk)
      @calls << [name, args, blk]
    end
  end

  class Grouper
    attr_reader :field_name

    def initialize(field_name)
      @field_name = field_name
      @groups = []
    end

    def on(name, &blk)
      group = Group.new(name)
      @groups << group
      group
    end

    def apply(sideload, resource_class)
      @groups.each do |group|
        if group.calls.empty?
          group.belongs_to(group.name.to_s.underscore.to_sym)
        end
        group.calls.each do |call|
          args = call[1]
          opts = args.extract_options!
          opts.merge! as: sideload.name,
            parent: sideload,
            group_name: group.name,
            polymorphic_child: true
          if !sideload.resource.class.abstract_class?
            opts[:foreign_key] ||= sideload.foreign_key
            opts[:primary_key] ||= sideload.primary_key
          end
          args << opts
          resource_class.send(call[0], *args, &call[2])
        end
      end
    end
  end

  class_attribute :grouper
  attr_accessor :children
  self.grouper = Grouper.new(:default)

  def type
    :polymorphic_belongs_to
  end

  def infer_foreign_key
    :"#{name}_id"
  end

  def self.group_by(name, &blk)
    self.grouper = Grouper.new(name)
    self.grouper.instance_eval(&blk)
  end

  def initialize(name, opts)
    super
    self.children = {}
    grouper.apply(self, parent_resource_class)
  end

  def child_for_type(type)
    children.values.find do |sideload|
      sideload.resource.type == type
    end
  end

  def resolve(parents, query)
    parents.group_by(&grouper.field_name).each_pair do |group_name, group|
      next if group_name.nil?

      match = ->(c) { c.group_name == group_name.to_sym }
      if sideload = children.values.find(&match)
        query = remove_invalid_sideloads(sideload.resource, query)
        sideload.resolve(group, query)
      else
        err = ::Graphiti::Errors::PolymorphicChildNotFound
        raise err.new(self, group_name)
      end
    end
  end

  private

  # We may be requesting a relationship that some subclasses support,
  # but not others. Remove anything we don't support.
  def remove_invalid_sideloads(resource, query)
    query = query.dup
    query.sideloads.each_pair do |key, value|
      unless resource.class.sideload(key)
        query.sideloads.delete(key)
      end
    end
    query
  end
end