lib/utils/mutable_order.ex

defmodule CPSolver.Utils.MutableOrder do
  alias CPSolver.Utils.MutableArray
  import CPSolver.Utils.MutableArray

  @moduledoc """
  'mutable order' structure.

  Currently being used by AllDifferent.BC
  The motivation:
  every time the filtering happens, it needs to update some already sorted lists of lower and/or upper bounds.

  Doing sorting from scratch is expensive, so the goal is to update and keep the array sorted
  based only on the incoming changes.

  `values` is a list of (unsorted) values - this would be a list of variables' lower or upper bounds
  `sorted_index is a list, s.t. sorted_index[i] holds the position of values[i] in the sorted list.
  The upshot is that `values` and `sorted_index` represent a sorted list of `values.
  `updated_index` - the index of the changed value in `values`
  `updated_value` - the new value for values[updated_index]

  For instance:
  values = [2, 8, 3, 5, 2]
  The sorted index would be:
  sorted_index = [0, 4, 2, 3, 1]

  updated_index = 1, updated_value = 2
  means that values[1] (that had value 8) had been updated to 2

  Note: `value` and `sorted_index` are represented by :atomics
  in order to facilitate fast access and updates in place

  """

  @doc """
  Creates an order structure from (unsorted) array
  """
  def new(values) when is_list(values) do
    n = length(values)

    values_ref = MutableArray.new(values)
    sort_index_ref = MutableArray.new(n)
    value_positions_ref = MutableArray.new(n)

    values
    |> Enum.with_index()
    |> Enum.sort()
    |> Enum.reduce(0, fn {_val, idx}, pos_acc ->
      array_update(sort_index_ref, pos_acc, idx)
      array_update(value_positions_ref, idx, pos_acc)
      pos_acc + 1
    end)

    %{values: values_ref, sort_index: sort_index_ref, positions: value_positions_ref}
  end

  @doc "Get value by index in sorted array"
  def get(order_rec, index) do
    array_get(order_rec.values, array_get(order_rec.sort_index, index))
  end

  def update(
        %{values: values_ref, positions: positions_ref, sort_index: sort_index_ref} = _order_rec,
        change
      ) do
    update(values_ref, positions_ref, sort_index_ref, change)
  end

  def update(values, positions, sort_index, {change_index, new_value} = _change)
      when is_reference(values) and is_reference(sort_index) and is_integer(change_index) and
             is_integer(new_value) do
    case array_get(values, change_index) do
      current_value when current_value == new_value ->
        :ok

      current_value ->
        change_pos = array_get(positions, change_index)

        update_order_impl(
          change_pos,
          values,
          positions,
          sort_index,
          new_value,
          (current_value > new_value && 0) || array_size(values) - 1
        )

        array_update(values, change_index, new_value)
    end
  end

  defp update_order_impl(current_pos, _values, _positions, _sort_index, _new_value, last_pos)
       when current_pos == last_pos do
    :ok
  end

  defp update_order_impl(pos, values, positions, sort_index, new_value, 0) do
    next_pos = pos - 1
    ## The sort index is sorted by values indices refer to
    ## To find an actual value in referred lists (values, positions), we need to get an index value
    next_pos_pointer = array_get(sort_index, next_pos)
    if array_get(values, next_pos_pointer) > new_value do
      swap(sort_index, pos, next_pos)
      swap(positions, next_pos_pointer, array_get(sort_index, next_pos))
      update_order_impl(next_pos, values, positions, sort_index, new_value, 0)
    else
      :ok
    end
  end

  defp update_order_impl(pos, values, positions, sort_index, new_value, last_index) do
    next_pos = pos + 1
    ## The sort index is sorted by values indices refer to
    ## To find an actual value in referred lists (values, positions), we need to get an index value
    next_pos_pointer = array_get(sort_index, next_pos)
    if array_get(values, next_pos_pointer) < new_value do
      swap(sort_index, pos, next_pos)
      swap(positions, next_pos_pointer, array_get(sort_index, next_pos))
      update_order_impl(next_pos, values, positions, sort_index, new_value, last_index)
    else
      :ok
    end
  end

  def to_sorted(%{values: values, sort_index: sort_index} = _order_rec, order \\ :asc) do
    to_sorted(values, sort_index, order)
  end

  def to_sorted(values, sort_index, order) do
    Enum.reduce(1..array_size(sort_index), [], fn idx, acc ->
      sort_pos = array_get(sort_index, idx - 1)
      [{sort_pos, array_get(values, sort_pos)} | acc]
    end)
    |> then(fn desc -> (order == :asc && Enum.reverse(desc)) || desc end)
  end

  def valid?(order_rec, order \\ :asc) do
    ordered_values = to_sorted(order_rec, order) |> Enum.unzip() |> elem(1)
    Enum.sort(ordered_values, order) == ordered_values
  end
end