require "./mutex"

module Sync
  # Safely share a value `T` across fibers and execution contexts using a
  # `Mutex`, so only one critical section can access the value at any time.
  #
  # For example:
  #
  # ```
  # require "sync/exclusive"
  #
  # class Queue
  #   @@running : Sync::Exclusive.new([] of Queue)
  #
  #   def self.on_started(queue)
  #     @@running.lock(&.push(queue))
  #   end
  #
  #   def self.on_stopped(queue)
  #     @@running.lock(&.delete(queue))
  #   end
  #
  #   def self.each(&)
  #     @@running.lock do |list|
  #       list.each { |queue| yield queue }
  #     end
  #   end
  # end
  # ```
  #
  # Consider an `Exclusive(T)` if your workload mostly needs to own the value,
  # and most, if not all, critical sections need to mutate the inner state of
  # the value for example.
  class Exclusive(T)
    include Lockable

    {% if compare_versions(Crystal::VERSION, "1.12.0") >= 0 %}
      @lock = uninitialized ReferenceStorage(Mutex)
    {% else %}
      @lock = uninitialized Mutex
    {% end %}

    def initialize(@value : T, type : Type = :checked)
      {% if compare_versions(Crystal::VERSION, "1.12.0") >= 0 %}
        @lock = uninitialized ReferenceStorage(Mutex)
        Mutex.unsafe_construct(pointerof(@lock), type)
      {% else %}
        @lock = Mutex.new(type)
      {% end %}
    end

    private def lock : Mutex
      {% if compare_versions(Crystal::VERSION, "1.12.0") >= 0 %}
        @lock.to_reference
      {% else %}
        @lock
      {% end %}
    end

    # Locks the mutex and yields the value. The lock is released before
    # returning.
    #
    # The value is owned for the duration of the block, and can be safely
    # mutated.
    #
    # WARNING: The value musn't be retained and accessed after the block has
    # returned.
    def lock(& : T -> _)
      lock.synchronize { yield @value }
    end

    # Locks the mutex, yields the value and eventually replaces the value with
    # the one returned by the block. The lock is released before returning.
    #
    # The current value is now owned: it can be safely retained and mutated even
    # after the block returned.
    #
    # WARNING: The new value musn't be retained and accessed after the block has
    # returned.
    def replace(& : T -> T) : Nil
      lock.synchronize { @value = yield @value }
    end

    # Locks the mutex and returns the value. Unlocks before returning.
    #
    # Always acquires the lock, so reading the value is synchronized in relation
    # with the other methods. However, safely accessing the returned value
    # entirely depends on the safety of `T`.
    #
    # Prefer `#lock(&.dup)` or `#lock(&.clone)` to get a shallow or deep copy of
    # the value instead.
    #
    # WARNING: Breaks the mutual exclusion guarantee since the returned value
    # outlives the lock, the value can be accessed concurrently to the
    # synchronized methods.
    def get : T
      lock.synchronize { @value }
    end

    # Locks the mutex and sets the value. Unlocks the mutex before returning.
    #
    # Always acquires and releases the lock, so writing the value is always
    # synchronized with the other methods.
    def set(value : T) : Nil
      lock.synchronize { @value = value }
    end

    # Returns the value without any synchronization.
    #
    # WARNING: Breaks the mutual exclusion constraint! Should only be called
    # after acquiring the lock.
    def unsafe_get : T
      @value
    end

    # Sets the value without any synchronization.
    #
    # WARNING: Breaks the mutual exclusion constraint! Should only be called
    # after acquiring the lock.
    def unsafe_set(@value : T) : T
    end

    protected def wait(cv : Pointer(CV)) : Nil
      lock.wait(cv)
    end
  end
end
