module ActiveRecord
  module Associations
    class JoinDependency

      class JoinAssociation < JoinPart # :nodoc:
        private
          def append_constraints(join, constraints)
            if join.is_a?(Arel::Nodes::StringJoin)
              join_string = Arel::Nodes::And.new(constraints.unshift join.left)
              join.left = Arel.sql(base_klass.connection.visitor.compile(join_string))
            else
              right = join.right
              # CPK
              if right.expr.is_a?(Arel::Nodes::And) && right.expr.children.empty?
                right.expr = Arel::Nodes::And.new(constraints)
              else
                right.expr = Arel::Nodes::And.new(constraints.unshift right.expr)
              end
            end
          end
      end

      class Aliases # :nodoc:
        def column_alias(node, column)
          # CPK
          #@alias_cache[node][column]
          if column.kind_of?(Array)
            column.map do |a_column|
              @alias_cache[node][a_column]
            end
          else
            @alias_cache[node][column]
          end
        end
      end

      def instantiate(result_set, strict_loading_value, &block)
        primary_key = aliases.column_alias(join_root, join_root.primary_key)

        seen = Hash.new { |i, parent|
          i[parent] = Hash.new { |j, child_class|
            j[child_class] = {}
          }
        }.compare_by_identity

        model_cache = Hash.new { |h, klass| h[klass] = {} }
        parents = model_cache[join_root]

        column_aliases = aliases.column_aliases(join_root)
        column_names = []

        result_set.columns.each do |name|
          column_names << name unless /\At\d+_r\d+\z/.match?(name)
        end

        if column_names.empty?
          column_types = {}
        else
          column_types = result_set.column_types
          unless column_types.empty?
            attribute_types = join_root.attribute_types
            column_types = column_types.slice(*column_names).delete_if { |k, _| attribute_types.key?(k) }
          end
          column_aliases += column_names.map! { |name| Aliases::Column.new(name, name) }
        end

        message_bus = ActiveSupport::Notifications.instrumenter

        payload = {
          record_count: result_set.length,
          class_name: join_root.base_klass.name
        }

        message_bus.instrument("instantiation.active_record", payload) do
          result_set.each { |row_hash|
            # CPK
            # parent_key = primary_key ? row_hash[primary_key] : row_hash
            parent_key = if primary_key.kind_of?(Array)
                           primary_key.map {|key| row_hash[key]}
                         else
                           primary_key ? row_hash[primary_key] : row_hash
                         end

            parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases, column_types, &block)
            construct(parent, join_root, row_hash, seen, model_cache, strict_loading_value)
          }
        end

        parents.values
      end

      def construct(ar_parent, parent, row, seen, model_cache, strict_loading_value)
        return if ar_parent.nil?

        parent.children.each do |node|
          if node.reflection.collection?
            other = ar_parent.association(node.reflection.name)
            other.loaded!
          elsif ar_parent.association_cached?(node.reflection.name)
            model = ar_parent.association(node.reflection.name).target
            construct(model, node, row, seen, model_cache, strict_loading_value)
            next
          end

          key = aliases.column_alias(node, node.primary_key)
          # CPK
          if key.is_a?(Array)
            id = Array(key).map do |column_alias|
              row[column_alias]
            end
            # At least the first value in the key has to be set.  Should we require all values to be set?
            id = nil if id.first.nil?
          else # original
            id = row[key]
          end

          if id.nil?
            nil_association = ar_parent.association(node.reflection.name)
            nil_association.loaded!
            next
          end

          model = seen[ar_parent][node][id]

          if model
            construct(model, node, row, seen, model_cache, strict_loading_value)
          else
            model = construct_model(ar_parent, node, row, model_cache, id, strict_loading_value)

            seen[ar_parent][node][id] = model
            construct(model, node, row, seen, model_cache, strict_loading_value)
          end
        end
      end
    end
  end
end
