class Bundler::PubGrub::VersionSolver

def add_incompatibility(incompatibility)

def add_incompatibility(incompatibility)
  logger.debug { "fact: #{incompatibility}" }
  incompatibility.terms.each do |term|
    package = term.package
    @incompatibilities[package] << incompatibility
  end
end

def choose_package_version

def choose_package_version
  if solution.unsatisfied.empty?
    logger.info "No packages unsatisfied. Solving complete!"
    return nil
  end
  package = next_package_to_try
  unsatisfied_term = solution.unsatisfied.find { |t| t.package == package }
  version = source.versions_for(package, unsatisfied_term.constraint.range).first
  if version.nil?
    add_incompatibility source.no_versions_incompatibility_for(package, unsatisfied_term)
    return package
  end
  conflict = false
  source.incompatibilities_for(package, version).each do |incompatibility|
    if @seen_incompatibilities.include?(incompatibility)
      logger.debug { "knew: #{incompatibility}" }
      next
    end
    @seen_incompatibilities[incompatibility] = true
    add_incompatibility incompatibility
    conflict ||= incompatibility.terms.all? do |term|
      term.package == package || solution.satisfies?(term)
    end
  end
  unless conflict
    logger.info { "selecting #{package} #{version}" }
    solution.decide(package, version)
  end
  package
end

def initialize(source:, root: Package.root, logger: Bundler::PubGrub.logger)

def initialize(source:, root: Package.root, logger: Bundler::PubGrub.logger)
  @logger = logger
  @source = source
  # { package => [incompatibility, ...]}
  @incompatibilities = Hash.new do |h, k|
    h[k] = []
  end
  @seen_incompatibilities = {}
  @solution = PartialSolution.new
  add_incompatibility Incompatibility.new([
    Term.new(VersionConstraint.any(root), false)
  ], cause: :root)
  propagate(root)
end

def next_package_to_try

def next_package_to_try
  solution.unsatisfied.min_by do |term|
    package = term.package
    range = term.constraint.range
    matching_versions = source.versions_for(package, range)
    higher_versions = source.versions_for(package, range.upper_invert)
    [matching_versions.count <= 1 ? 0 : 1, higher_versions.count]
  end.package
end

def propagate(initial_package)

def propagate(initial_package)
  changed = [initial_package]
  while package = changed.shift
    @incompatibilities[package].reverse_each do |incompatibility|
      result = propagate_incompatibility(incompatibility)
      if result == :conflict
        root_cause = resolve_conflict(incompatibility)
        changed.clear
        changed << propagate_incompatibility(root_cause)
      elsif result # should be a Package
        changed << result
      end
    end
    changed.uniq!
  end
end

def propagate_incompatibility(incompatibility)

def propagate_incompatibility(incompatibility)
  unsatisfied = nil
  incompatibility.terms.each do |term|
    relation = solution.relation(term)
    if relation == :disjoint
      return nil
    elsif relation == :overlap
      # If more than one term is inconclusive, we can't deduce anything
      return nil if unsatisfied
      unsatisfied = term
    end
  end
  if !unsatisfied
    return :conflict
  end
  logger.debug { "derived: #{unsatisfied.invert}" }
  solution.derive(unsatisfied.invert, incompatibility)
  unsatisfied.package
end

def resolve_conflict(incompatibility)

def resolve_conflict(incompatibility)
  logger.info { "conflict: #{incompatibility}" }
  new_incompatibility = false
  while !incompatibility.failure?
    most_recent_term = nil
    most_recent_satisfier = nil
    difference = nil
    previous_level = 1
    incompatibility.terms.each do |term|
      satisfier = solution.satisfier(term)
      if most_recent_satisfier.nil?
        most_recent_term = term
        most_recent_satisfier = satisfier
      elsif most_recent_satisfier.index < satisfier.index
        previous_level = [previous_level, most_recent_satisfier.decision_level].max
        most_recent_term = term
        most_recent_satisfier = satisfier
        difference = nil
      else
        previous_level = [previous_level, satisfier.decision_level].max
      end
      if most_recent_term == term
        difference = most_recent_satisfier.term.difference(most_recent_term)
        if difference.empty?
          difference = nil
        else
          difference_satisfier = solution.satisfier(difference.inverse)
          previous_level = [previous_level, difference_satisfier.decision_level].max
        end
      end
    end
    if previous_level < most_recent_satisfier.decision_level ||
        most_recent_satisfier.decision?
      logger.info { "backtracking to #{previous_level}" }
      solution.backtrack(previous_level)
      if new_incompatibility
        add_incompatibility(incompatibility)
      end
      return incompatibility
    end
    new_terms = []
    new_terms += incompatibility.terms - [most_recent_term]
    new_terms += most_recent_satisfier.cause.terms.reject { |term|
      term.package == most_recent_satisfier.term.package
    }
    if difference
      new_terms << difference.invert
    end
    incompatibility = Incompatibility.new(new_terms, cause: Incompatibility::ConflictCause.new(incompatibility, most_recent_satisfier.cause))
    new_incompatibility = true
    partially = difference ? " partially" : ""
    logger.info { "! #{most_recent_term} is#{partially} satisfied by #{most_recent_satisfier.term}" }
    logger.info { "! which is caused by #{most_recent_satisfier.cause}" }
    logger.info { "! thus #{incompatibility}" }
  end
  raise SolveFailure.new(incompatibility)
end

def solve

def solve
  work until solved?
  solution.decisions
end

def solved?

def solved?
  solution.unsatisfied.empty?
end

def work

Returns true if there is more work to be done, false otherwise
def work
  return false if solved?
  next_package = choose_package_version
  propagate(next_package)
  if solved?
    logger.info { "Solution found after #{solution.attempted_solutions} attempts:" }
    solution.decisions.each do |package, version|
      next if Package.root?(package)
      logger.info { "* #{package} #{version}" }
    end
    false
  else
    true
  end
end