// Copyright 2022 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build lockdep
// +build lockdep

package locking

import (
	"fmt"
	"reflect"
	"strings"

	"gvisor.dev/gvisor/pkg/goid"
	"gvisor.dev/gvisor/pkg/log"
)

// NewMutexClass allocates a new mutex class.
func NewMutexClass(t reflect.Type, lockNames []string) *MutexClass {
	c := &MutexClass{
		typ:               t,
		nestedLockNames:   lockNames,
		nestedLockClasses: make([]*MutexClass, len(lockNames)),
	}
	for i := range lockNames {
		c.nestedLockClasses[i] = NewMutexClass(t, nil)
		c.nestedLockClasses[i].lockName = lockNames[i]
	}
	return c
}

// MutexClass describes dependencies of a specific class.
type MutexClass struct {
	// The type of the mutex.
	typ reflect.Type

	// Name of the nested lock of the above type.
	lockName string

	// ancestors are locks that are locked before the current class.
	ancestors ancestorsAtomicPtrMap
	// nestedLockNames is a list of names for nested locks which are considered difference instances
	// of the same lock class.
	nestedLockNames []string
	// namedLockClasses is a list of MutexClass instances of the same mutex class, but that are
	// considered OK to lock simultaneously with each other, as well as with this mutex class.
	// This is used for nested locking, where multiple instances of the same lock class are used
	// simultaneously.
	// Maps one-to-one with nestedLockNames.
	nestedLockClasses []*MutexClass
}

func (m *MutexClass) String() string {
	if m.lockName == "" {
		return m.typ.String()
	}
	return fmt.Sprintf("%s[%s]", m.typ.String(), m.lockName)
}

type goroutineLocks map[*MutexClass]bool

var routineLocks goroutineLocksAtomicPtrMap

// checkLock checks that class isn't in the ancestors of prevClass.
func checkLock(class *MutexClass, prevClass *MutexClass, chain []*MutexClass) {
	chain = append(chain, prevClass)
	if c := prevClass.ancestors.Load(class); c != nil {
		var b strings.Builder
		fmt.Fprintf(&b, "WARNING: circular locking detected: %s -> %s:\n%s\n",
			chain[0], class, log.LocalStack(3))

		fmt.Fprintf(&b, "known lock chain: ")
		c := class
		for i := len(chain) - 1; i >= 0; i-- {
			fmt.Fprintf(&b, "%s -> ", c)
			c = chain[i]
		}
		fmt.Fprintf(&b, "%s\n", chain[0])
		c = class
		for i := len(chain) - 1; i >= 0; i-- {
			fmt.Fprintf(&b, "\n====== %s -> %s =====\n%s",
				c, chain[i], *chain[i].ancestors.Load(c))
			c = chain[i]
		}
		panic(b.String())
	}
	prevClass.ancestors.RangeRepeatable(func(parentClass *MutexClass, stacks *string) bool {
		// The recursion is fine here. If it fails, you need to reduce
		// a number of nested locks.
		checkLock(class, parentClass, chain)
		return true
	})
}

// AddGLock records a lock to the current goroutine and updates dependencies.
func AddGLock(class *MutexClass, lockNameIndex int) {
	gid := goid.Get()

	if lockNameIndex != -1 {
		class = class.nestedLockClasses[lockNameIndex]
	}
	currentLocks := routineLocks.Load(gid)
	if currentLocks == nil {
		locks := goroutineLocks(make(map[*MutexClass]bool))
		locks[class] = true
		routineLocks.Store(gid, &locks)
		return
	}

	// Check dependencies and add locked mutexes to the ancestors list.
	for prevClass := range *currentLocks {
		if prevClass == class {
			panic(fmt.Sprintf("nested locking: %s:\n%s", class, log.LocalStack(2)))
		}
		checkLock(class, prevClass, nil)

		if c := class.ancestors.Load(prevClass); c == nil {
			stacks := string(log.LocalStack(2))
			class.ancestors.Store(prevClass, &stacks)
		}
	}
	(*currentLocks)[class] = true
}

// DelGLock deletes a lock from the current goroutine.
func DelGLock(class *MutexClass, lockNameIndex int) {
	if lockNameIndex != -1 {
		class = class.nestedLockClasses[lockNameIndex]
	}
	gid := goid.Get()
	currentLocks := routineLocks.Load(gid)
	if currentLocks == nil {
		panic("the current goroutine doesn't have locks")
	}
	if _, ok := (*currentLocks)[class]; !ok {
		var b strings.Builder
		fmt.Fprintf(&b, "Lock not held: %s:\n", class)
		fmt.Fprintf(&b, "Current stack:\n%s\n", string(log.LocalStack(2)))
		fmt.Fprintf(&b, "Current locks:\n")
		for c := range *currentLocks {
			heldToClass := class.ancestors.Load(c)
			classToHeld := c.ancestors.Load(class)
			if heldToClass == nil && classToHeld == nil {
				fmt.Fprintf(&b, "\t- Holding lock: %s (no dependency to/from %s found)\n", c, class)
			} else if heldToClass != nil && classToHeld != nil {
				fmt.Fprintf(&b, "\t- Holding lock: %s (mutual dependency with %s found, this should never happen)\n", c, class)
			} else if heldToClass != nil && classToHeld == nil {
				fmt.Fprintf(&b, "\t- Holding lock: %s (dependency: %s -> %s)\n", c, c, class)
				fmt.Fprintf(&b, "%s\n\n", *heldToClass)
			} else if heldToClass == nil && classToHeld != nil {
				fmt.Fprintf(&b, "\t- Holding lock: %s (dependency: %s -> %s)\n", c, class, c)
				fmt.Fprintf(&b, "%s\n\n", *classToHeld)
			}
		}
		fmt.Fprintf(&b, "** End of locks held **\n")
		panic(b.String())
	}

	delete(*currentLocks, class)
	if len(*currentLocks) == 0 {
		routineLocks.Store(gid, nil)
	}
}
