Skip to main content

lib/hex_solver/term.ex

defmodule HexSolver.Term do
  @moduledoc false

  alias HexSolver.{Constraint, PackageRange, Term}
  alias HexSolver.Constraints.Empty

  require Logger

  defstruct positive: true,
            package_range: nil,
            optional: false

  def relation(left, right, opts \\ []) do
    if Keyword.get(opts, :optionals, true) do
      case do_relation(left, right) do
        :disjoint -> if left.optional and not right.optional, do: :overlapping, else: :disjoint
        other -> if left.optional and right.optional, do: :disjoint, else: other
      end
    else
      do_relation(left, right)
    end
  end

  def do_relation(%Term{} = left, %Term{} = right) do
    left_constraint = constraint(left)
    right_constraint = constraint(right)

    cond do
      right.positive and left.positive ->
        cond do
          not compatible_package?(left, right) -> :disjoint
          Constraint.allows_all?(right_constraint, left_constraint) -> :subset
          not Constraint.allows_any?(left_constraint, right_constraint) -> :disjoint
          true -> :overlapping
        end

      right.positive and not left.positive ->
        cond do
          not compatible_package?(left, right) -> :overlapping
          Constraint.allows_all?(left_constraint, right_constraint) -> :disjoint
          true -> :overlapping
        end

      not right.positive and left.positive ->
        cond do
          not compatible_package?(left, right) -> :subset
          not Constraint.allows_any?(right_constraint, left_constraint) -> :subset
          Constraint.allows_all?(right_constraint, left_constraint) -> :disjoint
          true -> :overlapping
        end

      not right.positive and not left.positive ->
        cond do
          not compatible_package?(left, right) -> :overlapping
          Constraint.allows_all?(left_constraint, right_constraint) -> :subset
          true -> :overlapping
        end
    end
  end

  def intersect(%Term{} = left, %Term{} = right) do
    if compatible_package?(left, right) do
      optional = left.optional and right.optional

      cond do
        left.positive != right.positive ->
          positive = if left.positive, do: left, else: right
          negative = if left.positive, do: right, else: left

          constraint = Constraint.difference(constraint(positive), constraint(negative))
          non_empty_term(left, constraint, optional, true)

        left.positive and right.positive ->
          constraint = Constraint.intersect(constraint(left), constraint(right))
          non_empty_term(left, constraint, optional, true)

        not left.positive and not right.positive ->
          constraint = Constraint.union(constraint(left), constraint(right))
          non_empty_term(left, constraint, optional, false)
      end
    else
      nil
    end
  end

  def difference(%Term{} = left, %Term{} = right) do
    intersect(left, inverse(right))
  end

  def satisfies?(%Term{} = left, %Term{} = right, opts \\ []) do
    compatible_package?(left, right) and relation(left, right, opts) == :subset
  end

  def inverse(%Term{} = term) do
    %{term | positive: not term.positive}
  end

  def compatible_package?(%Term{package_range: %PackageRange{name: "$root"}}, %Term{}) do
    true
  end

  def compatible_package?(%Term{}, %Term{package_range: %PackageRange{name: "$root"}}) do
    true
  end

  def compatible_package?(%Term{package_range: left}, %Term{package_range: right}) do
    {left.repo, left.name} == {right.repo, right.name}
  end

  defp constraint(%Term{package_range: %PackageRange{constraint: constraint}}) do
    constraint
  end

  defp non_empty_term(_term, %Empty{}, _optional, _positive) do
    nil
  end

  defp non_empty_term(term, constraint, optional, positive) do
    %Term{
      package_range: %{term.package_range | constraint: constraint},
      positive: positive,
      optional: optional
    }
  end

  def to_string(%Term{package_range: package_range, positive: positive}) do
    "#{positive(positive)}#{package_range}"
  end

  defp positive(true), do: ""
  defp positive(false), do: "not "

  defimpl String.Chars do
    defdelegate to_string(term), to: HexSolver.Term
  end

  defimpl Inspect do
    def inspect(
          %Term{package_range: package_range, positive: positive, optional: optional},
          _opts
        ) do
      "#Term<#{positive(positive)}#{package_range}#{optional(optional)}>"
    end

    defp positive(true), do: ""
    defp positive(false), do: "not "

    defp optional(true), do: " (optional)"
    defp optional(false), do: ""
  end
end