// +build openbsd

package cpu

import (
	"bytes"
	"context"
	"encoding/binary"
	"fmt"
	"os/exec"
	"runtime"
	"strconv"
	"strings"
	"syscall"

	"github.com/shirou/gopsutil/internal/common"
	"golang.org/x/sys/unix"
)

// sys/sched.h
var (
	CPUser    = 0
	CPNice    = 1
	CPSys     = 2
	CPIntr    = 3
	CPIdle    = 4
	CPUStates = 5
)

// sys/sysctl.h
const (
	CTLKern     = 1  // "high kernel": proc, limits
	CTLHw       = 6  // CTL_HW
	SMT         = 24 // HW_SMT
	NCpuOnline  = 25 // HW_NCPUONLINE
	KernCptime  = 40 // KERN_CPTIME
	KernCptime2 = 71 // KERN_CPTIME2
)

var ClocksPerSec = float64(128)

func init() {
	func() {
		getconf, err := exec.LookPath("getconf")
		if err != nil {
			return
		}
		out, err := invoke.Command(getconf, "CLK_TCK")
		// ignore errors
		if err == nil {
			i, err := strconv.ParseFloat(strings.TrimSpace(string(out)), 64)
			if err == nil {
				ClocksPerSec = float64(i)
			}
		}
	}()
	func() {
		v, err := unix.Sysctl("kern.osrelease") // can't reuse host.PlatformInformation because of circular import
		if err != nil {
			return
		}
		v = strings.ToLower(v)
		version, err := strconv.ParseFloat(v, 64)
		if err != nil {
			return
		}
		if version >= 6.4 {
			CPIntr = 4
			CPIdle = 5
			CPUStates = 6
		}
	}()
}

func smt() (bool, error) {
	mib := []int32{CTLHw, SMT}
	buf, _, err := common.CallSyscall(mib)
	if err != nil {
		return false, err
	}

	var ret bool
	br := bytes.NewReader(buf)
	if err := binary.Read(br, binary.LittleEndian, &ret); err != nil {
		return false, err
	}

	return ret, nil
}

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

func TimesWithContext(ctx context.Context, percpu bool) ([]TimesStat, error) {
	var ret []TimesStat

	var ncpu int
	if percpu {
		ncpu, _ = Counts(true)
	} else {
		ncpu = 1
	}

	smt, err := smt()
	if err == syscall.EOPNOTSUPP {
		// if hw.smt is not applicable for this platform (e.g. i386),
		// pretend it's enabled
		smt = true
	} else if err != nil {
		return nil, err
	}

	for i := 0; i < ncpu; i++ {
		j := i
		if !smt {
			j *= 2
		}

		var cpuTimes = make([]int32, CPUStates)
		var mib []int32
		if percpu {
			mib = []int32{CTLKern, KernCptime2, int32(j)}
		} else {
			mib = []int32{CTLKern, KernCptime}
		}
		buf, _, err := common.CallSyscall(mib)
		if err != nil {
			return ret, err
		}

		br := bytes.NewReader(buf)
		err = binary.Read(br, binary.LittleEndian, &cpuTimes)
		if err != nil {
			return ret, err
		}
		c := TimesStat{
			User:   float64(cpuTimes[CPUser]) / ClocksPerSec,
			Nice:   float64(cpuTimes[CPNice]) / ClocksPerSec,
			System: float64(cpuTimes[CPSys]) / ClocksPerSec,
			Idle:   float64(cpuTimes[CPIdle]) / ClocksPerSec,
			Irq:    float64(cpuTimes[CPIntr]) / ClocksPerSec,
		}
		if percpu {
			c.CPU = fmt.Sprintf("cpu%d", j)
		} else {
			c.CPU = "cpu-total"
		}
		ret = append(ret, c)
	}

	return ret, nil
}

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

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

	c := InfoStat{}

	var u32 uint32
	if u32, err = unix.SysctlUint32("hw.cpuspeed"); err != nil {
		return nil, err
	}
	c.Mhz = float64(u32)

	mib := []int32{CTLHw, NCpuOnline}
	buf, _, err := common.CallSyscall(mib)
	if err != nil {
		return nil, err
	}

	var ncpu int32
	br := bytes.NewReader(buf)
	err = binary.Read(br, binary.LittleEndian, &ncpu)
	if err != nil {
		return nil, err
	}
	c.Cores = ncpu

	if c.ModelName, err = unix.Sysctl("hw.model"); err != nil {
		return nil, err
	}

	return append(ret, c), nil
}

func CountsWithContext(ctx context.Context, logical bool) (int, error) {
	return runtime.NumCPU(), nil
}
