// SPDX-License-Identifier: BSD-3-Clause
//go:build darwin

package cpu

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"unsafe"

	"github.com/tklauser/go-sysconf"
	"golang.org/x/sys/unix"

	"github.com/shirou/gopsutil/v4/internal/common"
)

// sys/resource.h
const (
	CPUser    = 0
	cpNice    = 1
	cpSys     = 2
	cpIntr    = 3
	cpIdle    = 4
	cpUStates = 5
)

// mach/machine.h
const (
	cpuStateUser   = 0
	cpuStateSystem = 1
	cpuStateIdle   = 2
	cpuStateNice   = 3
	cpuStateMax    = 4
)

// mach/processor_info.h
const (
	processorCpuLoadInfo = 2
)

type hostCpuLoadInfoData struct {
	cpuTicks [cpuStateMax]uint32
}

// default value. from time.h
var ClocksPerSec = float64(128)

func init() {
	clkTck, err := sysconf.Sysconf(sysconf.SC_CLK_TCK)
	// ignore errors
	if err == nil {
		ClocksPerSec = float64(clkTck)
	}
}

func Times(percpu bool) ([]TimesStat, error) {
	return TimesWithContext(context.Background(), percpu)
}

func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) {
	lib, err := common.NewLibrary(common.System)
	if err != nil {
		return nil, err
	}
	defer lib.Close()

	if percpu {
		return perCPUTimes(lib)
	}

	return allCPUTimes(lib)
}

// Returns only one CPUInfoStat on FreeBSD
func Info() ([]InfoStat, error) {
	return InfoWithContext(context.Background())
}

func InfoWithContext(ctx context.Context) ([]InfoStat, error) {
	var ret []InfoStat

	c := InfoStat{}
	c.ModelName, _ = unix.Sysctl("machdep.cpu.brand_string")
	family, _ := unix.SysctlUint32("machdep.cpu.family")
	c.Family = strconv.FormatUint(uint64(family), 10)
	model, _ := unix.SysctlUint32("machdep.cpu.model")
	c.Model = strconv.FormatUint(uint64(model), 10)
	stepping, _ := unix.SysctlUint32("machdep.cpu.stepping")
	c.Stepping = int32(stepping)
	features, err := unix.Sysctl("machdep.cpu.features")
	if err == nil {
		for _, v := range strings.Fields(features) {
			c.Flags = append(c.Flags, strings.ToLower(v))
		}
	}
	leaf7Features, err := unix.Sysctl("machdep.cpu.leaf7_features")
	if err == nil {
		for _, v := range strings.Fields(leaf7Features) {
			c.Flags = append(c.Flags, strings.ToLower(v))
		}
	}
	extfeatures, err := unix.Sysctl("machdep.cpu.extfeatures")
	if err == nil {
		for _, v := range strings.Fields(extfeatures) {
			c.Flags = append(c.Flags, strings.ToLower(v))
		}
	}
	cores, _ := unix.SysctlUint32("machdep.cpu.core_count")
	c.Cores = int32(cores)
	cacheSize, _ := unix.SysctlUint32("machdep.cpu.cache.size")
	c.CacheSize = int32(cacheSize)
	c.VendorID, _ = unix.Sysctl("machdep.cpu.vendor")

	v, err := getFrequency()
	if err == nil {
		c.Mhz = v
	}

	return append(ret, c), nil
}

func CountsWithContext(ctx context.Context, logical bool) (int, error) {
	var cpuArgument string
	if logical {
		cpuArgument = "hw.logicalcpu"
	} else {
		cpuArgument = "hw.physicalcpu"
	}

	count, err := unix.SysctlUint32(cpuArgument)
	if err != nil {
		return 0, err
	}

	return int(count), nil
}

func perCPUTimes(machLib *common.Library) ([]TimesStat, error) {
	machHostSelf := common.GetFunc[common.MachHostSelfFunc](machLib, common.MachHostSelfSym)
	machTaskSelf := common.GetFunc[common.MachTaskSelfFunc](machLib, common.MachTaskSelfSym)
	hostProcessorInfo := common.GetFunc[common.HostProcessorInfoFunc](machLib, common.HostProcessorInfoSym)
	vmDeallocate := common.GetFunc[common.VMDeallocateFunc](machLib, common.VMDeallocateSym)

	var count, ncpu uint32
	var cpuload *hostCpuLoadInfoData

	status := hostProcessorInfo(machHostSelf(), processorCpuLoadInfo, &ncpu, uintptr(unsafe.Pointer(&cpuload)), &count)

	if status != common.KERN_SUCCESS {
		return nil, fmt.Errorf("host_processor_info error=%d", status)
	}

	defer vmDeallocate(machTaskSelf(), uintptr(unsafe.Pointer(cpuload)), uintptr(ncpu))

	ret := []TimesStat{}
	loads := unsafe.Slice(cpuload, ncpu)

	for i := 0; i < int(ncpu); i++ {
		c := TimesStat{
			CPU:    fmt.Sprintf("cpu%d", i),
			User:   float64(loads[i].cpuTicks[cpuStateUser]) / ClocksPerSec,
			System: float64(loads[i].cpuTicks[cpuStateSystem]) / ClocksPerSec,
			Nice:   float64(loads[i].cpuTicks[cpuStateNice]) / ClocksPerSec,
			Idle:   float64(loads[i].cpuTicks[cpuStateIdle]) / ClocksPerSec,
		}

		ret = append(ret, c)
	}

	return ret, nil
}

func allCPUTimes(machLib *common.Library) ([]TimesStat, error) {
	machHostSelf := common.GetFunc[common.MachHostSelfFunc](machLib, common.MachHostSelfSym)
	hostStatistics := common.GetFunc[common.HostStatisticsFunc](machLib, common.HostStatisticsSym)

	var cpuload hostCpuLoadInfoData
	count := uint32(cpuStateMax)

	status := hostStatistics(machHostSelf(), common.HOST_CPU_LOAD_INFO,
		uintptr(unsafe.Pointer(&cpuload)), &count)

	if status != common.KERN_SUCCESS {
		return nil, fmt.Errorf("host_statistics error=%d", status)
	}

	c := TimesStat{
		CPU:    "cpu-total",
		User:   float64(cpuload.cpuTicks[cpuStateUser]) / ClocksPerSec,
		System: float64(cpuload.cpuTicks[cpuStateSystem]) / ClocksPerSec,
		Nice:   float64(cpuload.cpuTicks[cpuStateNice]) / ClocksPerSec,
		Idle:   float64(cpuload.cpuTicks[cpuStateIdle]) / ClocksPerSec,
	}

	return []TimesStat{c}, nil
}
