package syncUtil

import (
	"context"
	"sync"
	"time"
)

// Set the behavier on unlock unlocked mutex
var PanicOnBug = true

// another mutex implementation with TryLock method
type Mutex interface {
	Lock()
	UnLock()
	// TryLock return true if it fetch mutex
	TryLock() bool
	// TryLockTimeout return true if it fetch mutex, return false if timeout
	TryLockTimeout(timeout time.Duration) bool
	// TryLockTimeout return true if it fetch mutex, return false if context done
	TryLockContext(ctx context.Context) bool
}

func NewMutex() Mutex {
	m := &mutex{ch: make(chan struct{}, 1)}
	m.ch <- struct{}{}
	return m
}

type mutex struct {
	ch chan struct{}
}

func (m *mutex) Lock() {
	<-m.ch
}

func (m *mutex) UnLock() {
	select {
	case m.ch <- struct{}{}:
	default:
		if PanicOnBug {
			panic("unlock of unlocked mutex")
		}
	}
}

func (m *mutex) TryLock() bool {
	select {
	case <-m.ch:
		return true
	default:
	}
	return false
}

func (m *mutex) TryLockTimeout(timeout time.Duration) bool {
	tm := time.NewTimer(timeout)
	select {
	case <-m.ch:
		tm.Stop()
		return true
	case <-tm.C:
	}
	return false
}

func (m *mutex) TryLockContext(ctx context.Context) bool {
	select {
	case <-m.ch:
		return true
	case <-ctx.Done():
	}
	return false
}

type MutexGroup interface {
	Lock(i interface{})
	UnLock(i interface{})
	UnLockAndFree(i interface{})
	// TryLock return true if it fetch mutex
	TryLock(i interface{}) bool
	// TryLockTimeout return true if it fetch mutex, return false if timeout
	TryLockTimeout(i interface{}, timeout time.Duration) bool
	// TryLockTimeout return true if it fetch mutex, return false if context done
	TryLockContext(i interface{}, ctx context.Context) bool
}

func NewMutexGroup() MutexGroup {
	return &mutexGroup{group: make(map[interface{}]*entry)}
}

type mutexGroup struct {
	mu    sync.Mutex
	group map[interface{}]*entry
}

type entry struct {
	ref int
	mu  Mutex
}

func (m *mutexGroup) get(i interface{}, ref int) Mutex {
	m.mu.Lock()
	defer m.mu.Unlock()
	en, ok := m.group[i]
	if !ok {
		if ref > 0 {
			en = &entry{mu: NewMutex()}
			m.group[i] = en
		} else if PanicOnBug {
			panic("unlock of unlocked mutex")
		} else {
			return nil
		}
	}
	en.ref += ref
	return en.mu
}

func (m *mutexGroup) Lock(i interface{}) {
	m.get(i, 1).Lock()
}

func (m *mutexGroup) UnLock(i interface{}) {
	mu := m.get(i, -1)
	if mu != nil {
		mu.UnLock()
	}
}

func (m *mutexGroup) UnLockAndFree(i interface{}) {
	m.mu.Lock()
	defer m.mu.Unlock()
	en, ok := m.group[i]
	if !ok {
		if PanicOnBug {
			panic("unlock of unlocked mutex")
		}
		return
	}
	en.ref--
	if en.ref == 0 {
		delete(m.group, i)
	}
	en.mu.UnLock()
}

func (m *mutexGroup) TryLock(i interface{}) bool {
	locked := m.get(i, 1).TryLock()
	if !locked {
		m.get(i, -1)
	}
	return locked
}

func (m *mutexGroup) TryLockTimeout(i interface{}, timeout time.Duration) bool {
	locked := m.get(i, 1).TryLockTimeout(timeout)
	if !locked {
		m.get(i, -1)
	}
	return locked
}

func (m *mutexGroup) TryLockContext(i interface{}, ctx context.Context) bool {
	locked := m.get(i, 1).TryLockContext(ctx)
	if !locked {
		m.get(i, -1)
	}
	return locked
}
