Skip to main content

lib/hex_solver/constraints/range.ex

defmodule HexSolver.Constraints.Range do
  @moduledoc false

  use HexSolver.Constraints.Impl

  alias HexSolver.Constraint
  alias HexSolver.Constraints.{Empty, Range, Union, Util, Version}

  # %Range{min: nil, max: nil} allows any version.
  # %Range{min: min, max: max, include_min: true, include_max: true}
  # when min == max is a single version.
  defstruct min: nil,
            max: nil,
            include_min: false,
            include_max: false

  def valid?(%Range{min: min, max: max, include_min: include_min, include_max: include_max}) do
    case version_compare(min, max) do
      :lt -> true
      :eq -> include_min and include_max
      :gt -> false
    end
  end

  def any?(%Range{min: nil, max: nil}), do: true
  def any?(%Range{}), do: false

  def empty?(%Range{}), do: false

  def allows?(%Range{} = range, %Elixir.Version{} = version) do
    compare_min = version_compare(range.min, version)

    if compare_min == :lt or (compare_min == :eq and range.include_min) do
      compare_max = version_compare(version, range.max)
      compare_max == :lt or (compare_max == :eq and range.include_max)
    else
      false
    end
  end

  def allows_all?(%Range{}, %Empty{}), do: true
  def allows_all?(%Range{} = range, %Elixir.Version{} = version), do: allows?(range, version)

  def allows_all?(%Range{} = left, %Range{} = right) do
    not allows_lower?(right, left) and not allows_higher?(right, left)
  end

  def allows_all?(%Range{} = range, %Union{ranges: ranges}) do
    Enum.all?(ranges, &allows_all?(range, &1))
  end

  def allows_any?(%Range{}, %Empty{}), do: true
  def allows_any?(%Range{} = range, %Elixir.Version{} = version), do: allows?(range, version)

  def allows_any?(%Range{} = left, %Range{} = right) do
    not strictly_lower?(right, left) and not strictly_higher?(right, left)
  end

  def allows_any?(%Range{} = range, %Union{ranges: ranges}) do
    Enum.any?(ranges, &allows_any?(range, &1))
  end

  def allows_lower?(%Range{} = left, %Range{} = right) do
    cond do
      left.min == nil ->
        right.min != nil

      right.min == nil ->
        false

      true ->
        case Version.compare(left.min, right.min) do
          :lt -> true
          :gt -> false
          :eq -> left.include_min and not right.include_min
        end
    end
  end

  def allows_higher?(%Range{} = left, %Range{} = right) do
    cond do
      left.max == nil ->
        right.max != nil

      right.max == nil ->
        false

      true ->
        case Version.compare(left.max, right.max) do
          :lt -> false
          :gt -> true
          :eq -> left.include_max and not right.include_max
        end
    end
  end

  def strictly_lower?(%Elixir.Version{} = left, %Elixir.Version{} = right) do
    Version.compare(left, right) == :lt
  end

  def strictly_lower?(%Range{} = range, %Elixir.Version{} = version) do
    range.max != nil and Version.compare(range.max, version) == :lt
  end

  def strictly_lower?(%Elixir.Version{} = version, %Range{} = range) do
    range.min != nil and Version.compare(version, range.min) == :lt
  end

  def strictly_lower?(%Range{} = left, %Range{} = right) do
    if left.max == nil or right.min == nil do
      false
    else
      case Version.compare(left.max, right.min) do
        :lt -> true
        :gt -> false
        :eq -> not left.include_max or not right.include_min
      end
    end
  end

  def strictly_higher?(left, right) do
    strictly_lower?(right, left)
  end

  def difference(%Range{} = range, %Empty{}) do
    range
  end

  def difference(%Range{} = range, %Elixir.Version{} = version) do
    cond do
      not allows?(range, version) ->
        range

      range.min == version ->
        %{range | include_min: false}

      range.max == version ->
        %{range | include_max: false}

      true ->
        %Union{
          ranges: [
            %{range | max: version, include_max: false},
            %{range | min: version, include_min: false}
          ]
        }
    end
  end

  def difference(%Range{} = left, %Range{} = right) do
    if allows_any?(left, right) do
      before_range =
        cond do
          not allows_lower?(left, right) ->
            nil

          left.min == right.min ->
            true = left.include_min and not right.include_min
            true = left.min != nil
            left.min

          true ->
            %Range{
              min: left.min,
              max: right.min,
              include_min: left.include_min,
              include_max: not right.include_min
            }
        end

      after_range =
        cond do
          not allows_higher?(left, right) ->
            nil

          left.max == right.max ->
            true = left.include_max and not right.include_max
            true = left.max != nil
            left.max

          true ->
            %Range{
              min: right.max,
              max: left.max,
              include_min: not right.include_max,
              include_max: left.include_max
            }
        end

      cond do
        before_range == nil and after_range == nil -> %Empty{}
        before_range == nil -> after_range
        after_range == nil -> before_range
        true -> %Union{ranges: [before_range, after_range]}
      end
    else
      left
    end
  end

  def difference(%Range{} = range, %Union{} = union) do
    {range, ranges} =
      Enum.reduce_while(union.ranges, {range, []}, fn union_range, {range, acc} ->
        cond do
          strictly_lower?(union_range, range) ->
            {:cont, {range, acc}}

          strictly_higher?(union_range, range) ->
            {:halt, {range, acc}}

          true ->
            case Constraint.difference(range, union_range) do
              %Empty{} -> {:halt, {range, %Empty{}}}
              %Union{ranges: [first, last]} -> {:cont, {last, [first | acc]}}
              constraint -> {:cont, {constraint, acc}}
            end
        end
      end)

    case ranges do
      %Empty{} -> %Empty{}
      [] -> range
      _ -> %Union{ranges: Enum.reverse([range | ranges])}
    end
  end

  def intersect(%Range{}, %Empty{}) do
    %Empty{}
  end

  def intersect(%Range{} = range, %Elixir.Version{} = version) do
    if allows?(range, version) do
      version
    else
      %Empty{}
    end
  end

  def intersect(%Range{} = left, %Range{} = right) do
    if strictly_lower?(left, right) or strictly_lower?(right, left) do
      %Empty{}
    else
      {intersect_min, intersect_include_min} =
        if allows_lower?(left, right) do
          {right.min, right.include_min}
        else
          {left.min, left.include_min}
        end

      {intersect_max, intersect_include_max} =
        if allows_higher?(left, right) do
          {right.max, right.include_max}
        else
          {left.max, left.include_max}
        end

      cond do
        intersect_min == nil and intersect_max == nil ->
          # Open range
          %Range{}

        intersect_min == intersect_max ->
          # There must be overlap since we already checked none of
          # the ranges are strictly lower
          true = intersect_include_min and intersect_include_max
          intersect_min

        true ->
          %Range{
            min: intersect_min,
            max: intersect_max,
            include_min: intersect_include_min,
            include_max: intersect_include_max
          }
      end
    end
  end

  def intersect(%Range{} = range, %Union{} = union) do
    Union.intersect(union, range)
  end

  def union(%Range{} = range, %Empty{}) do
    range
  end

  def union(%Range{} = range, %Elixir.Version{} = version) do
    if allows?(range, version) do
      range
    else
      case range do
        %Range{min: ^version} -> %{range | include_min: true}
        %Range{max: ^version} -> %{range | include_max: true}
        _ -> Util.union([range, version])
      end
    end
  end

  def union(%Range{} = left, %Range{} = right) do
    if not edges_touch?(left, right) and not Range.allows_any?(left, right) do
      Util.union([left, right])
    else
      min = if allows_lower?(left, right), do: left, else: right
      max = if allows_higher?(left, right), do: left, else: right

      %Range{
        min: min.min,
        max: max.max,
        include_min: min.include_min,
        include_max: max.include_max
      }
    end
  end

  def union(%Range{} = range, %Union{} = union) do
    Util.union([range, union])
  end

  defp edges_touch?(left, right) do
    (left.max != nil and left.max == right.min and (left.include_max or right.include_min)) or
      (left.min != nil and left.min == right.max and (left.include_min or right.include_max))
  end

  def compare(%Range{min: min, include_min: include_min}, %Elixir.Version{} = version) do
    if min == nil do
      :lt
    else
      case Version.compare(min, version) do
        :eq when include_min -> :eq
        :eq -> :gt
        :lt -> :lt
        :gt -> :gt
      end
    end
  end

  def compare(%Range{} = left, %Range{} = right) do
    cond do
      left.min == nil and right.min == nil ->
        :eq

      left.min == nil ->
        :lt

      right.min == nil ->
        :gt

      true ->
        left_include_min = left.include_min
        right_include_min = right.include_min

        case Version.compare(left.min, right.min) do
          :eq when left_include_min == right_include_min -> :eq
          :eq when left_include_min -> :lt
          :eq when right_include_min -> :gt
          :lt -> :lt
          :gt -> :gt
        end
    end
  end

  def compare(%Range{} = left, %Union{ranges: [right | _]}) do
    compare(left, right)
  end

  def single_version?(%Range{min: min, max: max, include_min: true, include_max: true}) do
    min == max
  end

  def single_version?(%Range{}) do
    false
  end

  defp version_compare(nil, _right), do: :lt
  defp version_compare(_left, nil), do: :lt
  defp version_compare(left, right), do: Version.compare(left, right)

  def normalize(%Range{min: version, max: version, include_min: true, include_max: true}),
    do: version

  def normalize(%Range{} = range), do: range
  def normalize(%Elixir.Version{} = version), do: version

  def to_string(%Range{min: nil, max: nil}) do
    "any"
  end

  def to_string(%Range{min: version, max: version, include_min: true, include_max: true}) do
    Kernel.to_string(version)
  end

  def to_string(%Range{
        min: %Elixir.Version{major: min_major, minor: min_minor, patch: 0, pre: pre},
        max: %Elixir.Version{major: max_major, minor: 0, patch: 0, pre: [0]},
        include_min: true,
        include_max: false
      })
      when min_major + 1 == max_major do
    "~> #{min_major}.#{min_minor}#{pre_to_string(pre)}"
  end

  def to_string(%Range{
        min: %Elixir.Version{major: min_major, minor: min_minor, patch: min_patch, pre: pre},
        max: %Elixir.Version{major: max_major, minor: max_minor, patch: 0, pre: [0]},
        include_min: true,
        include_max: false
      })
      when min_major == max_major and min_minor + 1 == max_minor do
    "~> #{min_major}.#{min_minor}.#{min_patch}#{pre_to_string(pre)}"
  end

  def to_string(%Range{min: min, max: max, include_min: include_min, include_max: include_max}) do
    min_string = if min, do: ">#{include(include_min)} #{min}"
    max_string = if max, do: "<#{include(include_max)} #{max}"

    if min_string && max_string do
      "#{min_string} and #{max_string}"
    else
      min_string || max_string
    end
  end

  defp pre_to_string([]), do: ""
  defp pre_to_string(pre), do: "-" <> Enum.join(pre, ".")

  defp include(true), do: "="
  defp include(false), do: ""

  defimpl String.Chars do
    defdelegate to_string(range), to: HexSolver.Constraints.Range
  end

  defimpl Inspect do
    def inspect(range, _opts) do
      "#Range<#{range}>"
    end
  end
end