//go:build !windows && !plan9 && !js

package unix

import (
	"fmt"
	"math"
	"strconv"
	"sync"

	"golang.org/x/sys/unix"

	"src.elv.sh/pkg/eval/errs"
	"src.elv.sh/pkg/eval/vals"
	"src.elv.sh/pkg/eval/vars"
)

const (
	validUmaskMsg = "integer in the range [0..0o777]"
)

// UmaskVariable is a variable whose value always reflects the current file
// creation permission mask. Setting it changes the current file creation
// permission mask for the process (not an individual thread).
type UmaskVariable struct{}

var _ vars.Var = UmaskVariable{}

// There is no way to query the current umask without changing it, so we store
// the umask value in a variable, and initialize it during startup. It needs to
// be mutex-guarded since it could be read or written concurrently.
//
// This assumes no other part of the Elvish code base involved in the
// interpreter ever calls unix.Umask, which is guaranteed by the
// check-content.sh script.
var (
	umaskVal   int
	umaskMutex sync.RWMutex
)

func init() {
	// Init functions are run concurrently, so it's normally impossible to
	// observe the temporary value.
	//
	// Even if there is some pathological init logic (e.g. goroutine from init
	// functions), the failure pattern is relative safe because we are setting
	// the temporary umask to the most restrictive value possible.
	umask := unix.Umask(0o777)
	unix.Umask(umask)
	umaskVal = umask
}

// Get returns the current file creation umask as a string.
func (UmaskVariable) Get() any {
	umaskMutex.RLock()
	defer umaskMutex.RUnlock()
	return fmt.Sprintf("0o%03o", umaskVal)
}

// Set changes the current file creation umask. It can be called with a string
// or a number. Strings are treated as octal numbers by default, unless they
// have an explicit base prefix like 0x or 0b.
func (UmaskVariable) Set(v any) error {
	umask, err := parseUmask(v)
	if err != nil {
		return err
	}

	umaskMutex.Lock()
	defer umaskMutex.Unlock()
	unix.Umask(umask)
	umaskVal = umask
	return nil
}

func parseUmask(v any) (int, error) {
	var umask int

	switch v := v.(type) {
	case string:
		i, err := strconv.ParseInt(v, 8, 0)
		if err != nil {
			i, err = strconv.ParseInt(v, 0, 0)
			if err != nil {
				return -1, errs.BadValue{
					What: "umask", Valid: validUmaskMsg, Actual: vals.ToString(v)}
			}
		}
		umask = int(i)
	case int:
		umask = v
	case float64:
		intPart, fracPart := math.Modf(v)
		if fracPart != 0 {
			return -1, errs.BadValue{
				What: "umask", Valid: validUmaskMsg, Actual: vals.ToString(v)}
		}
		umask = int(intPart)
	default:
		// We don't bother supporting big.Int or bit.Rat because no valid umask
		// value would be represented by those types.
		return -1, errs.BadValue{
			What: "umask", Valid: validUmaskMsg, Actual: vals.Kind(v)}
	}

	if umask < 0 || umask > 0o777 {
		return -1, errs.OutOfRange{
			What: "umask", ValidLow: "0", ValidHigh: "0o777",
			Actual: fmt.Sprintf("%O", umask)}
	}
	return umask, nil
}
