class ReeDao::Association

def add_scopes(scope, named_scope)

def add_scopes(scope, named_scope)
  res = if named_scope
    named_scope.call(scope)
  else
    scope
  end
  res.all
end

def foreign_key_from_dao(dao)

def foreign_key_from_dao(dao)
  "#{dao.first_source_table.to_s.gsub(/s$/, '')}_id".to_sym
end

def handle_field(field_proc)

def handle_field(field_proc)
  field_proc.call
end

def initialize(parent, parent_dao, list, **global_opts)

def initialize(parent, parent_dao, list, **global_opts)
  @parent = parent
  @parent_dao = parent_dao
  @list = list
  @global_opts = global_opts
end

def load(assoc_type, assoc_name, **__opts, &block)

def load(assoc_type, assoc_name, **__opts, &block)
  load_association(assoc_type, assoc_name, **__opts, &block)
end

def load_association(assoc_type, assoc_name, **__opts, &block)

def load_association(assoc_type, assoc_name, **__opts, &block)
  __opts[:autoload_children] ||= false
  assoc_index = load_association_by_type(
    assoc_type,
    assoc_name,
    **__opts
  )
  scope = __opts[:scope]
  dao = if scope.is_a?(Array)
    return [] if scope.empty?
    nil
  else
    find_dao(assoc_name, parent, scope)
  end
  process_block(assoc_index, __opts[:autoload_children], __opts[:to_dto], dao, &block) if block_given?
  list
end

def load_association_by_type(type, assoc_name, **__opts)

def load_association_by_type(type, assoc_name, **__opts)
  case type
  when :belongs_to
    one_to_one(
      parent_dao,
      assoc_name,
      list,
      scope: __opts[:scope],
      primary_key: __opts[:primary_key],
      foreign_key: __opts[:foreign_key],
      setter: __opts[:setter],
      reverse: false
    )
  when :has_one
    one_to_one(
      parent_dao,
      assoc_name,
      list,
      scope: __opts[:scope],
      primary_key: __opts[:primary_key],
      foreign_key: __opts[:foreign_key],
      to_dto: __opts[:to_dto],
      setter: __opts[:setter],
      reverse: true
    )
  when :has_many
    one_to_many(
      parent_dao,
      assoc_name,
      list,
      scope: __opts[:scope],
      primary_key: __opts[:primary_key],
      foreign_key: __opts[:foreign_key],
      to_dto: __opts[:to_dto],
      setter: __opts[:setter]
    )
  end
end

def method_missing(method, *args, &block)

def method_missing(method, *args, &block)
  return super if !parent.agg_caller.private_methods(false).include?(method)
  parent.agg_caller.send(method, *args, &block)
end

def one_to_many(parent_dao, assoc_name, list, primary_key: nil, foreign_key: nil, scope: nil, setter: nil, to_dto: nil)

def one_to_many(parent_dao, assoc_name, list, primary_key: nil, foreign_key: nil, scope: nil, setter: nil, to_dto: nil)
  return {} if list.empty?
  primary_key ||= :id
  if scope.is_a?(Array)
    items = scope
  else
    assoc_dao = nil
    assoc_dao = find_dao(assoc_name, parent, scope)
    if !foreign_key
      if parent_dao.nil?
        raise ArgumentError.new("foreign_key should be provided for :#{assoc_name} association")
      end
      foreign_key = foreign_key_from_dao(parent_dao)
    end
    root_ids = list.map(&:"#{primary_key}")
    scope ||= assoc_dao
    scope = scope.where(foreign_key => root_ids)
    items = add_scopes(scope, global_opts[assoc_name])
  end
  assoc = if foreign_key
    group_by(items) { _1.send(foreign_key) }
  else
    items
  end
  populate_association(
    list,
    assoc,
    assoc_name,
    setter: setter,
    primary_key: primary_key,
    foreign_key: foreign_key,
    to_dto: to_dto,
    multiple: true
  )
  assoc
end

def one_to_one(parent_dao, assoc_name, list, scope: nil, primary_key: :id, foreign_key: nil, setter: nil, to_dto: nil, reverse: true)

def one_to_one(parent_dao, assoc_name, list, scope: nil, primary_key: :id, foreign_key: nil, setter: nil, to_dto: nil, reverse: true)
  return {} if list.empty?
  primary_key ||= :id
  if scope.is_a?(Array)
    items = scope
  else
    assoc_dao = find_dao(assoc_name, parent, scope)
    if reverse
      # has_one
      if !foreign_key
        if parent_dao.nil?
          raise ArgumentError.new("foreign_key should be provided for :#{assoc_name} association")
        end
        foreign_key = foreign_key_from_dao(parent_dao)
      end
      root_ids = list.map(&:id).uniq
    else
      # belongs_to
      if !foreign_key
        foreign_key = :"#{assoc_name}_id"
      end
      root_ids = list.map(&:"#{foreign_key}").compact
    end
    scope ||= assoc_dao
    scope = scope.where((reverse ? foreign_key : :id) => root_ids)
    items = add_scopes(scope, global_opts[assoc_name])
  end
  assoc = index_by(items) { _1.send(reverse ? foreign_key : :id) }
  populate_association(
    list,
    assoc,
    assoc_name,
    setter: setter,
    reverse: reverse,
    primary_key: primary_key,
    foreign_key: foreign_key,
    to_dto: to_dto
  )
  assoc
end

def populate_association(list, association_index, assoc_name, primary_key: nil, foreign_key: nil, reverse: nil, setter: nil, to_dto: nil, multiple: false)

def populate_association(list, association_index, assoc_name, primary_key: nil, foreign_key: nil, reverse: nil, setter: nil, to_dto: nil, multiple: false)
  assoc_setter = if setter
    setter
  else
    "set_#{assoc_name}"
  end
  list.each do |item|
    if setter && setter.is_a?(Proc)
      if to_dto
        assoc_index = {}
        association_index.each do |key, value|
          if value.is_a?(Array)
            assoc_index[key] = value.map { to_dto.call(_1) }
          else
            assoc_index[key] = to_dto.call(value)
          end
        end
        self.instance_exec(item, assoc_index, &assoc_setter)
      else
        self.instance_exec(item, association_index, &assoc_setter)
      end
    else
      key = if reverse.nil?
        primary_key
      else
        if reverse
          primary_key
        else
          foreign_key ? foreign_key : "#{assoc_name}_id"
        end
      end
      value = association_index[item.send(key)]
      if to_dto && !value.nil?
        value = if value.is_a?(Array)
          value.map { to_dto.call(_1) }
        else
          to_dto.call(value)
        end
      end
      value = [] if value.nil? && multiple
      begin
        item.send(assoc_setter, value)
      rescue NoMethodError
        item.send("#{assoc_name}=", value)
      end
    end
  end
end

def process_block(assoc, autoload_children, to_dto, parent_dao, &block)

def process_block(assoc, autoload_children, to_dto, parent_dao, &block)
  assoc_list = assoc.is_a?(Array) ? assoc : assoc.values.flatten
  if to_dto
    assoc_list = assoc_list.map do |item|
      to_dto.call(item)
    end
  end
  associations = ReeDao::Associations.new(
    parent.agg_caller,
    assoc_list,
    parent.local_vars,
    parent_dao,
    autoload_children,
    **global_opts
  )
  if parent_dao.nil? || parent_dao.db.in_transaction? || ReeDao::Associations.sync_mode?
    associations.instance_exec(assoc_list, &block)
  else
    threads = associations.instance_exec(assoc_list, &block)
    threads[:association_threads].map do |association, assoc_type, assoc_name, __opts, block|
        association.load(assoc_type, assoc_name, **__opts, &block)
    end
    threads[:field_threads].map do |association, field_proc|
      association.handle_field(field_proc)
    end
  end
end