require "./mu"
require "./type"
require "./errors"
require "./lockable"

module Sync
  # A mutual exclusion lock to protect critical sections.
  #
  # A single fiber can acquire the lock at a time. No other fiber can acquire
  # the lock while a fiber holds it.
  #
  # This lock can for example be used to protect the access to some resources,
  # with the guarantee that only one section of code can ever read, write or
  # mutate said resources.
  #
  # NOTE: Consider `Exclusive(T)` to protect a value `T` with a `Mutex`.
  class Mutex
    include Lockable

    def initialize(@type : Type = :checked)
      @counter = 0
      @mu = MU.new
    end

    # Acquires the exclusive lock for the duration of the block. The lock will
    # be released automatically before returning, or if the block raises an
    # exception.
    def synchronize(& : -> _)
      lock
      begin
        yield
      ensure
        unlock
      end
    end

    # Acquires the exclusive lock.
    def lock : Nil
      unless @mu.try_lock?
        unless @type.unchecked?
          if owns_lock?
            raise Error::Deadlock.new("Can't lock mutex recursively") unless @type.reentrant?
            @counter += 1
            return
          end
        end
        @mu.lock_slow
      end

      unless @type.unchecked?
        @locked_by = Fiber.current
        @counter = 1 if @type.reentrant?
      end
    end

    # Releases the exclusive lock.
    def unlock : Nil
      unless @type.unchecked?
        unless owns_lock?
          message =
            if @locked_by
              "Can't unlock Sync::Mutex locked by another fiber"
            else
              "Can't unlock Sync::Mutex that isn't locked"
            end
          raise Error.new(message)
        end
        if @type.reentrant?
          return unless (@counter -= 1) == 0
        end
        @locked_by = nil
      end
      @mu.unlock
    end

    protected def wait(cv : Pointer(CV)) : Nil
      counter = 1

      unless @type.unchecked?
        if @mu.held?
          raise Error.new("Can't unlock Sync::Mutex locked by another fiber") unless owns_lock?
          @locked_by = nil
          counter, @counter = @counter, 0 if @type.reentrant?
        else
          raise Error.new("Can't unlock Sync::Mutex that isn't locked")
        end
      end

      cv.value.wait pointerof(@mu)

      unless @type.unchecked?
        @locked_by = Fiber.current
        @counter = counter if @type.reentrant?
      end
    end

    protected def owns_lock? : Bool
      @locked_by == Fiber.current
    end

    # :nodoc:
    def dup
      {% raise "Can't dup {{@type}}" %}
    end
  end
end
