package ccTopology

import (
	"fmt"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"

	cclogger "github.com/ClusterCockpit/cc-metric-collector/internal/ccLogger"
)

const SYSFS_NUMABASE = `/sys/devices/system/node`
const SYSFS_CPUBASE = `/sys/devices/system/cpu`
const PROCFS_CPUINFO = `/proc/cpuinfo`

// intArrayContains scans an array of ints if the value str is present in the array
// If the specified value is found, the corresponding array index is returned.
// The bool value is used to signal success or failure
func intArrayContains(array []int, str int) (int, bool) {
	for i, a := range array {
		if a == str {
			return i, true
		}
	}
	return -1, false
}

func fileToInt(path string) int {
	buffer, err := ioutil.ReadFile(path)
	if err != nil {
		log.Print(err)
		cclogger.ComponentError("ccTopology", "Reading", path, ":", err.Error())
		return -1
	}
	sbuffer := strings.Replace(string(buffer), "\n", "", -1)
	var id int64
	//_, err = fmt.Scanf("%d", sbuffer, &id)
	id, err = strconv.ParseInt(sbuffer, 10, 32)
	if err != nil {
		cclogger.ComponentError("ccTopology", "Parsing", path, ":", sbuffer, err.Error())
		return -1
	}
	return int(id)
}

func SocketList() []int {
	buffer, err := ioutil.ReadFile(string(PROCFS_CPUINFO))
	if err != nil {
		log.Print(err)
		return nil
	}
	ll := strings.Split(string(buffer), "\n")
	var packs []int
	for _, line := range ll {
		if strings.HasPrefix(line, "physical id") {
			lv := strings.Fields(line)
			id, err := strconv.ParseInt(lv[3], 10, 32)
			if err != nil {
				log.Print(err)
				return packs
			}
			_, found := intArrayContains(packs, int(id))
			if !found {
				packs = append(packs, int(id))
			}
		}
	}
	return packs
}

func CpuList() []int {
	buffer, err := ioutil.ReadFile(string(PROCFS_CPUINFO))
	if err != nil {
		log.Print(err)
		return nil
	}
	ll := strings.Split(string(buffer), "\n")
	cpulist := make([]int, 0)
	for _, line := range ll {
		if strings.HasPrefix(line, "processor") {
			lv := strings.Fields(line)
			id, err := strconv.ParseInt(lv[2], 10, 32)
			if err != nil {
				log.Print(err)
				return cpulist
			}
			_, found := intArrayContains(cpulist, int(id))
			if !found {
				cpulist = append(cpulist, int(id))
			}
		}
	}
	return cpulist
}

func CoreList() []int {
	buffer, err := ioutil.ReadFile(string(PROCFS_CPUINFO))
	if err != nil {
		log.Print(err)
		return nil
	}
	ll := strings.Split(string(buffer), "\n")
	corelist := make([]int, 0)
	for _, line := range ll {
		if strings.HasPrefix(line, "core id") {
			lv := strings.Fields(line)
			id, err := strconv.ParseInt(lv[3], 10, 32)
			if err != nil {
				log.Print(err)
				return corelist
			}
			_, found := intArrayContains(corelist, int(id))
			if !found {
				corelist = append(corelist, int(id))
			}
		}
	}
	return corelist
}

func NumaNodeList() []int {
	numaList := make([]int, 0)
	globPath := filepath.Join(string(SYSFS_NUMABASE), "node*")
	regexPath := filepath.Join(string(SYSFS_NUMABASE), "node(\\d+)")
	regex := regexp.MustCompile(regexPath)
	files, err := filepath.Glob(globPath)
	if err != nil {
		cclogger.ComponentError("CCTopology", "NumaNodeList", err.Error())
	}
	for _, f := range files {
		if !regex.MatchString(f) {
			continue
		}
		finfo, err := os.Lstat(f)
		if err != nil {
			continue
		}
		if !finfo.IsDir() {
			continue
		}
		matches := regex.FindStringSubmatch(f)
		if len(matches) == 2 {
			id, err := strconv.Atoi(matches[1])
			if err == nil {
				if _, found := intArrayContains(numaList, id); !found {
					numaList = append(numaList, id)
				}
			}
		}

	}
	return numaList
}

func DieList() []int {
	cpulist := CpuList()
	dielist := make([]int, 0)
	for _, c := range cpulist {
		diepath := filepath.Join(string(SYSFS_CPUBASE), fmt.Sprintf("cpu%d", c), "topology/die_id")
		dieid := fileToInt(diepath)
		if dieid > 0 {
			_, found := intArrayContains(dielist, int(dieid))
			if !found {
				dielist = append(dielist, int(dieid))
			}
		}
	}
	return dielist
}

type CpuEntry struct {
	Cpuid      int
	SMT        int
	Core       int
	Socket     int
	Numadomain int
	Die        int
}

func CpuData() []CpuEntry {

	fileToInt := func(path string) int {
		buffer, err := ioutil.ReadFile(path)
		if err != nil {
			log.Print(err)
			//cclogger.ComponentError("ccTopology", "Reading", path, ":", err.Error())
			return -1
		}
		sbuffer := strings.Replace(string(buffer), "\n", "", -1)
		var id int64
		//_, err = fmt.Scanf("%d", sbuffer, &id)
		id, err = strconv.ParseInt(sbuffer, 10, 32)
		if err != nil {
			cclogger.ComponentError("ccTopology", "Parsing", path, ":", sbuffer, err.Error())
			return -1
		}
		return int(id)
	}
	getCore := func(basepath string) int {
		return fileToInt(fmt.Sprintf("%s/core_id", basepath))
	}

	getSocket := func(basepath string) int {
		return fileToInt(fmt.Sprintf("%s/physical_package_id", basepath))
	}

	getDie := func(basepath string) int {
		return fileToInt(fmt.Sprintf("%s/die_id", basepath))
	}

	getSMT := func(cpuid int, basepath string) int {
		buffer, err := ioutil.ReadFile(fmt.Sprintf("%s/thread_siblings_list", basepath))
		if err != nil {
			cclogger.ComponentError("CCTopology", "CpuData:getSMT", err.Error())
		}
		threadlist := make([]int, 0)
		sbuffer := strings.Replace(string(buffer), "\n", "", -1)
		for _, x := range strings.Split(sbuffer, ",") {
			id, err := strconv.ParseInt(x, 10, 32)
			if err != nil {
				cclogger.ComponentError("CCTopology", "CpuData:getSMT", err.Error())
			}
			threadlist = append(threadlist, int(id))
		}
		for i, x := range threadlist {
			if x == cpuid {
				return i
			}
		}
		return 1
	}

	getNumaDomain := func(basepath string) int {
		globPath := filepath.Join(basepath, "node*")
		regexPath := filepath.Join(basepath, "node(\\d+)")
		regex := regexp.MustCompile(regexPath)
		files, err := filepath.Glob(globPath)
		if err != nil {
			cclogger.ComponentError("CCTopology", "CpuData:getNumaDomain", err.Error())
		}
		for _, f := range files {
			finfo, err := os.Lstat(f)
			if err == nil && finfo.IsDir() {
				matches := regex.FindStringSubmatch(f)
				if len(matches) == 2 {
					id, err := strconv.Atoi(matches[1])
					if err == nil {
						return id
					}
				}
			}
		}
		return 0
	}

	clist := make([]CpuEntry, 0)
	for _, c := range CpuList() {
		clist = append(clist, CpuEntry{Cpuid: c})
	}
	for _, centry := range clist {
		centry.Socket = -1
		centry.Numadomain = -1
		centry.Die = -1
		centry.Core = -1
		// Set base directory for topology lookup
		cpustr := fmt.Sprintf("cpu%d", centry.Cpuid)
		base := filepath.Join("/sys/devices/system/cpu", cpustr)
		topoBase := filepath.Join(base, "topology")

		// Lookup CPU core id
		centry.Core = getCore(topoBase)

		// Lookup CPU socket id
		centry.Socket = getSocket(topoBase)

		// Lookup CPU die id
		centry.Die = getDie(topoBase)
		if centry.Die < 0 {
			centry.Die = centry.Socket
		}

		// Lookup SMT thread id
		centry.SMT = getSMT(centry.Cpuid, topoBase)

		// Lookup NUMA domain id
		centry.Numadomain = getNumaDomain(base)

	}
	return clist
}

type CpuInformation struct {
	NumHWthreads   int
	SMTWidth       int
	NumSockets     int
	NumDies        int
	NumCores       int
	NumNumaDomains int
}

func CpuInfo() CpuInformation {
	var c CpuInformation

	smtList := make([]int, 0)
	numaList := make([]int, 0)
	dieList := make([]int, 0)
	socketList := make([]int, 0)
	coreList := make([]int, 0)
	cdata := CpuData()
	for _, d := range cdata {
		if _, ok := intArrayContains(smtList, d.SMT); !ok {
			smtList = append(smtList, d.SMT)
		}
		if _, ok := intArrayContains(numaList, d.Numadomain); !ok {
			numaList = append(numaList, d.Numadomain)
		}
		if _, ok := intArrayContains(dieList, d.Die); !ok {
			dieList = append(dieList, d.Die)
		}
		if _, ok := intArrayContains(socketList, d.Socket); !ok {
			socketList = append(socketList, d.Socket)
		}
		if _, ok := intArrayContains(coreList, d.Core); !ok {
			coreList = append(coreList, d.Core)
		}
	}
	c.NumNumaDomains = len(numaList)
	c.SMTWidth = len(smtList)
	c.NumDies = len(dieList)
	c.NumCores = len(coreList)
	c.NumSockets = len(socketList)
	c.NumHWthreads = len(cdata)
	return c
}

func GetCpuSocket(cpuid int) int {
	cdata := CpuData()
	for _, d := range cdata {
		if d.Cpuid == cpuid {
			return d.Socket
		}
	}
	return -1
}

func GetCpuNumaDomain(cpuid int) int {
	cdata := CpuData()
	for _, d := range cdata {
		if d.Cpuid == cpuid {
			return d.Numadomain
		}
	}
	return -1
}

func GetCpuDie(cpuid int) int {
	cdata := CpuData()
	for _, d := range cdata {
		if d.Cpuid == cpuid {
			return d.Die
		}
	}
	return -1
}

func GetCpuCore(cpuid int) int {
	cdata := CpuData()
	for _, d := range cdata {
		if d.Cpuid == cpuid {
			return d.Core
		}
	}
	return -1
}

func GetSocketCpus(socket int) []int {
	all := CpuData()
	cpulist := make([]int, 0)
	for _, d := range all {
		if d.Socket == socket {
			cpulist = append(cpulist, d.Cpuid)
		}
	}
	return cpulist
}

func GetNumaDomainCpus(domain int) []int {
	all := CpuData()
	cpulist := make([]int, 0)
	for _, d := range all {
		if d.Numadomain == domain {
			cpulist = append(cpulist, d.Cpuid)
		}
	}
	return cpulist
}

func GetDieCpus(die int) []int {
	all := CpuData()
	cpulist := make([]int, 0)
	for _, d := range all {
		if d.Die == die {
			cpulist = append(cpulist, d.Cpuid)
		}
	}
	return cpulist
}

func GetCoreCpus(core int) []int {
	all := CpuData()
	cpulist := make([]int, 0)
	for _, d := range all {
		if d.Core == core {
			cpulist = append(cpulist, d.Cpuid)
		}
	}
	return cpulist
}