# frozen_string_literal: true

require 'dry/equalizer'
require 'dry/effects/provider'
require 'dry/effects/initializer'

module Dry
  module Effects
    module Providers
      class Lock < Provider[:lock]
        class Handle
          include ::Dry::Equalizer(:key)

          extend Initializer

          param :key

          param :meta
        end

        class Backend
          extend Initializer

          param :locks, default: -> { ::Hash.new }

          param :mutex, default: -> { ::Mutex.new }

          def lock(key, meta)
            mutex.synchronize do
              if locked?(key)
                nil
              else
                locks[key] = Handle.new(key, meta)
              end
            end
          end

          def locked?(key)
            locks.key?(key)
          end

          def unlock(handle)
            mutex.synchronize do
              if locked?(handle.key)
                locks.delete(handle.key)
                true
              else
                false
              end
            end
          end

          def meta(key)
            meta = Undefined.map(locks.fetch(key, Undefined), &:meta)
            Undefined.default(meta, nil)
          end
        end

        Locate = Effect.new(type: :lock, name: :locate)

        option :backend, default: -> { Backend.new }

        def lock(key, meta = Undefined)
          locked = backend.lock(key, meta)
          owned << locked if locked
          locked
        end

        def locked?(key)
          backend.locked?(key)
        end

        def unlock(handle)
          backend.unlock(handle)
        end

        def meta(key)
          backend.meta(key)
        end

        # Locate handler in the stack
        #
        # @return [Provider]
        # @api private
        def locate
          self
        end

        # Yield the block with the handler installed
        #
        # @api private
        def call(stack, backend = Undefined)
          backend_replace = Undefined.default(backend) do
            parent = ::Dry::Effects.yield(Locate) { Undefined }
            Undefined.map(parent, &:backend)
          end

          with_backend(backend_replace) do
            begin
              super(stack)
            ensure
              owned.each { |handle| unlock(handle) }
            end
          end
        end

        def with_backend(backend)
          if Undefined.equal?(backend)
            yield
          else
            begin
              before, @backend = @backend, backend
              yield
            ensure
              @backend = before
            end
          end
        end

        def owned
          @owned ||= []
        end

        def represent
          if owned.empty?
            super
          else
            "lock[owned=#{owned.size}]"
          end
        end
      end
    end
  end
end