// Copyright 2025 The Forgejo Authors. All rights reserved.
// SPDX-License-Identifier: GPL-3.0-or-later

package cache

import (
	"sync"
)

// MutexMap is basically a map[string]sync.Mutex which allows you to have one mutex per string key being locked. Unlike
// a map[string]sync.Mutex, this map will automatically remove the Mutexes from itself when they are not being waited
// for, preventing resource waste. It does this by keeping a reference count of the current Lock calls for the given
// key.
type MutexMap struct {
	mu       sync.Mutex // mutex to be held when accessing mutexMap
	mutexMap map[string]*refcountMutex
}

type refcountMutex struct {
	refCount int // access to refCount is protected by the MutexMap's mu

	sync.Mutex
}

// Locks the given key, and returns a function that must be invoked to unlock the key.
func (m *MutexMap) Lock(key string) func() {
	m.mu.Lock()
	if m.mutexMap == nil {
		m.mutexMap = make(map[string]*refcountMutex)
	}
	mutex, ok := m.mutexMap[key]
	if !ok {
		mutex = &refcountMutex{}
		m.mutexMap[key] = mutex
	}
	mutex.refCount++
	m.mu.Unlock()

	mutex.Lock()

	unlockPending := true

	return func() {
		if !unlockPending {
			// unlocking twice would cause incorrect reference counts and might release another goroutine's mutex -- try
			// to detect and panic so that this programming error can be found closest to the source.
			panic("MutexMap unlock invoked twice")
		}

		unlockPending = false
		mutex.Unlock()

		m.mu.Lock()
		mutex.refCount--
		if mutex.refCount == 0 {
			delete(m.mutexMap, key)
		}
		m.mu.Unlock()
	}
}
