defmodule HexSolver.Failure do @moduledoc false alias HexSolver.Incompatibility def write(root, opts \\ []) do derivations = count_derivations(%{}, root) state = state(root, derivations, opts) state = case root.cause do {:conflict, _, _} -> visit(state, root) _ -> write(state, root, false, [ "Because ", Incompatibility.to_string(root, ansi: state.ansi), " version solving failed." ]) end numbers = state.lines |> Enum.map(fn {number, _message} -> number end) |> Enum.filter(&is_integer/1) indent? = numbers != [] indent = if indent?, do: Enum.max_by(numbers, &byte_size(Integer.to_string(&1))) + 3 Enum.map_join(Enum.reverse(state.lines), "\n", fn {number, message} -> if number do "(#{number}) #{message}" else "#{message && indent && String.duplicate(" ", indent)}#{message}" end end) end defp count_derivations(derivations, incompatibility) do case Map.fetch(derivations, incompatibility) do {:ok, value} -> Map.put(derivations, incompatibility, value + 1) :error -> derivations = case incompatibility.cause do {:conflict, conflict, other} -> derivations |> count_derivations(conflict) |> count_derivations(other) _ -> derivations end Map.update(derivations, incompatibility, 1, &(&1 + 1)) end end defp visit(state, incompatibility, conclusion? \\ false) do numbered? = conclusion? or Map.fetch!(state.derivations, incompatibility) > 1 conjunction = if conclusion? or incompatibility == state.root, do: "So,", else: "And" incompatibility_string = Incompatibility.to_string(incompatibility, ansi: state.ansi) {:conflict, conflict, other} = incompatibility.cause case {conflict.cause, other.cause} do {{:conflict, _, _}, {:conflict, _, _}} -> conflict_line = state.line_numbers[conflict] other_line = state.line_numbers[other] single_line_conflict? = single_line?(conflict.cause) single_line_other? = single_line?(other.cause) cond do conflict_line && other_line -> write(state, incompatibility, numbered?, [ "Because ", Incompatibility.to_string_and(conflict, other, left_line: conflict_line, right_line: other_line, ansi: state.ansi ), ", ", incompatibility_string ]) conflict_line || other_line -> {with_line, without_line, line} = if conflict_line do {conflict, other, conflict_line} else {other, conflict, other_line} end state |> visit(without_line) |> write(incompatibility, numbered?, [ conjunction, " because ", Incompatibility.to_string(with_line, ansi: state.ansi), " (#{line}), ", incompatibility_string ]) single_line_conflict? or single_line_other? -> first = if single_line_other?, do: conflict, else: other second = if single_line_other?, do: other, else: conflict state |> visit(first) |> visit(second) |> write(incompatibility, numbered?, ["This, ", incompatibility_string]) true -> state |> visit(conflict, _conclusion? = true) |> add_line() |> visit(other) |> write(incompatibility, numbered?, fn state -> [ conjunction, " because ", Incompatibility.to_string(conflict, ansi: state.ansi), maybe_line(state.line_numbers[conflict]), ", ", incompatibility_string, "." ] end) end {left, right} when elem(left, 0) == :conflict when elem(right, 0) == :conflict -> derived = if match?({:conflict, _, _}, conflict.cause), do: conflict, else: other ext = if match?({:conflict, _, _}, conflict.cause), do: other, else: conflict cond do derived_line = state.line_numbers[derived] -> write(state, incompatibility, numbered?, [ "Because ", Incompatibility.to_string_and(ext, derived, right_line: derived_line, ansi: state.ansi ), ", ", incompatibility_string ]) collapsible?(state, derived) -> {:conflict, derived_conflict, derived_other} = derived.cause collapsed_derived = if match?({:conflict, _, _}, derived_conflict.cause), do: derived_conflict, else: derived_other collapsed_ext = if match?({:conflict, _, _}, derived_conflict.cause), do: derived_other, else: derived_conflict state |> visit(collapsed_derived) |> write(incompatibility, numbered?, [ conjunction, " because ", Incompatibility.to_string_and(collapsed_ext, ext, ansi: state.ansi), ", ", incompatibility_string, "." ]) true -> state |> visit(derived) |> write(incompatibility, numbered?, [ conjunction, " because ", Incompatibility.to_string(ext, ansi: state.ansi), ", ", incompatibility_string, "." ]) end _ -> write(state, incompatibility, numbered?, [ "Because ", Incompatibility.to_string_and(conflict, other, ansi: state.ansi), ", ", incompatibility_string, "." ]) end end defp write(state, incompatibility, numbered?, message) do message = maybe_fun(state, message) if numbered? do number = map_size(state.line_numbers) + 1 %{ state | lines: [{number, message} | state.lines], line_numbers: Map.put(state.line_numbers, incompatibility, number) } else %{state | lines: [{nil, message} | state.lines]} end end defp add_line(state, message \\ nil) do %{state | lines: [{nil, message} | state.lines]} end defp maybe_fun(state, fun) when is_function(fun, 1), do: fun.(state) defp maybe_fun(_state, other), do: other defp maybe_line(nil), do: "" defp maybe_line(line), do: " (#{line})" defp collapsible?( state, %Incompatibility{cause: {:conflict, conflict, other}} = incompatibility ) do complex = if match?({:conflict, _, _}, conflict), do: conflict, else: other cond do state.derivations[incompatibility] > 1 -> false match?({:conflict, _, _}, conflict) and match?({:conflict, _, _}, other) -> false not match?({:conflict, _, _}, conflict) and not match?({:conflict, _, _}, other) -> false true -> not Map.has_key?(state.line_numbers, complex) end end defp single_line?({ :conflict, %Incompatibility{cause: {:conflict, _, _}}, %Incompatibility{cause: {:conflict, _, _}} }), do: true defp single_line?({:conflict, %Incompatibility{}, %Incompatibility{}}), do: false defp state(root, derivations, opts) do %{ root: root, lines: [], line_numbers: %{}, derivations: derivations, ansi: Keyword.get(opts, :ansi, false) } end end