Solver

use col = "collections"
use "../range"

class Solver
  let source: ArtifactSource

  new create(source': ArtifactSource) =>
    source = source'

  fun ref solve(constraints: Iterator[Constraint]): Result =>
    var pending_cells = Array[_Cell]
    for c in constraints do
      pending_cells.push(_Cell(c))
    end

    let activated_cells_by_name = col.Map[String, _Cell]
    var first_conflict: (_ConflictSnapshot | None) = None

    while pending_cells.size() > 0 do
      let new_pending_cells = Array[_Cell]

      for pcell in pending_cells.values() do
        if pcell.garbage then continue end

        let constraint = pcell.constraint
        let name = constraint.artifact_name

        // Artifact never seen before, activate its cell
        //  - record its activation with the global list
        //  - add its dependencies to the tail of the pending cells

        let existing_cell = try
          activated_cells_by_name(name)?
        else
          activated_cells_by_name(name) = pcell
          pcell.activated = true

          pcell.picks = _all_versions_of(name)
          (let matchIndex, let ok) = _index_of_first_match(pcell.picks, [constraint.range])
          if (not ok) then
            return Result(where err' = "no artifacts match " + constraint.string())
          end

          _pick(pcell, matchIndex, new_pending_cells)
          pcell
        end

        // New constraint is compatible with existing pick

        try
          if (constraint.range.contains(existing_cell.picks(0)?.version)) then continue end
        end

        // New constraint is incompatible with existing pick
        //  - log if this is the first such conflict (in case we can't find a solution)
        //  - backtrack up the tree, until an alternative path is found

        if (first_conflict is None) then
          first_conflict = _ConflictSnapshot(
            Array[_Cell].>concat(activated_cells_by_name.values()),
            pcell.constraint,
            pcell.parent
          )
        end

        var cell: (_Cell | None) = existing_cell
        let conflicting_constraint = constraint

        while true do
          match cell
          | let c: _Cell =>
            let ranges = Array[Range].>push(c.constraint.range)

            // Blend in the constraint that kicked this backtracking off
            if (conflicting_constraint.artifact_name == c.constraint.artifact_name) then
              ranges.push(conflicting_constraint.range)
            end

            (let matchIndex, let ok) = _index_of_first_match(c.picks.slice(1), ranges)
            if (ok) then
              _prune_children(c, activated_cells_by_name)
              _pick(c, matchIndex + 1, new_pending_cells)
              break
            end

            cell = c.parent
          else
            return Result(where err' = "no solutions found: " + first_conflict.string())
          end
        end
      end

      pending_cells = new_pending_cells
    end

    let result = Result
    for cell in activated_cells_by_name.values() do
      try result.solution.push(cell.picks(0)?) end
    end
    result

  fun ref _all_versions_of(artifact_name: String): Array[Artifact] =>
    // copy for isolation
    let versions = Array[Artifact].>concat(source.all_versions_of(artifact_name))
    // reverse sort to make all the 'default to latest' optimizations work
    col.Sort[Array[Artifact], Artifact](versions).reverse()

  fun _index_of_first_match(
    artifacts: Array[Artifact],
    ranges: Seq[Range]): (USize, Bool)
  =>
    for (i, a) in artifacts.pairs() do
      var allMatch = true

      for r in ranges.values() do
        if (not r.contains(a.version)) then
          allMatch = false
          break
        end
      end

      if (allMatch) then return (i, true) end
    end

    (0, false)

  fun _pick(cell: _Cell, index: USize, newCells: Array[_Cell]) =>
    cell.picks = cell.picks.slice(index)

    let cellsFromDeps = Array[_Cell]
    try
      for dep in cell.picks(0)?.depends_on.values() do
        let c = _Cell(dep)
        c.parent = cell
        cellsFromDeps.push(c)
      end
    end

    cell.children.concat(cellsFromDeps.values())
    newCells.concat(cellsFromDeps.values())

  fun _prune_children(
    fromCell: _Cell,
    activated_cells_by_name: col.Map[String, _Cell])
  =>
    for cell in fromCell.children.values() do
      if cell.activated then
        try activated_cells_by_name.remove(cell.constraint.artifact_name)? end
      end

      cell.garbage = true
      _prune_children(cell, activated_cells_by_name)
    end

    fromCell.children.clear()