package system

import (
	"context"
	"sync"

	"emperror.dev/errors"
)

var ErrLockerLocked = errors.Sentinel("locker: cannot acquire lock, already locked")

type Locker struct {
	mu sync.RWMutex
	ch chan bool
}

// NewLocker returns a new Locker instance.
func NewLocker() *Locker {
	return &Locker{
		ch: make(chan bool, 1),
	}
}

// IsLocked returns the current state of the locker channel. If there is
// currently a value in the channel, it is assumed to be locked.
func (l *Locker) IsLocked() bool {
	l.mu.RLock()
	defer l.mu.RUnlock()
	return len(l.ch) == 1
}

// Acquire will acquire the power lock if it is not currently locked. If it is
// already locked, acquire will fail to acquire the lock, and will return false.
func (l *Locker) Acquire() error {
	l.mu.Lock()
	defer l.mu.Unlock()
	select {
	case l.ch <- true:
	default:
		return ErrLockerLocked
	}
	return nil
}

// TryAcquire will attempt to acquire a power-lock until the context provided
// is canceled.
func (l *Locker) TryAcquire(ctx context.Context) error {
	select {
	case l.ch <- true:
		return nil
	case <-ctx.Done():
		if err := ctx.Err(); err != nil {
			if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) {
				return ErrLockerLocked
			}
		}
		return nil
	}
}

// Release will drain the locker channel so that we can properly re-acquire it
// at a later time. If the channel is not currently locked this function is a
// no-op and will immediately return.
func (l *Locker) Release() {
	l.mu.Lock()
	select {
	case <-l.ch:
	default:
	}
	l.mu.Unlock()
}

// Destroy cleans up the power locker by closing the channel.
func (l *Locker) Destroy() {
	l.mu.Lock()
	if l.ch != nil {
		select {
		case <-l.ch:
		default:
		}
		close(l.ch)
	}
	l.mu.Unlock()
}