// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build ignore

package main

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"os/exec"
	"regexp"
	"sort"
	"strconv"
	"strings"
)

var (
	goos, goarch string
)

// cmdLine returns this programs's commandline arguments
func cmdLine() string {
	return "go run linux/mksysnum.go " + strings.Join(os.Args[1:], " ")
}

// goBuildTags returns build tags in the go:build format.
func goBuildTags() string {
	return fmt.Sprintf("%s && %s", goarch, goos)
}

func format(name string, num int, offset int) (int, string) {
	if num > 999 {
		// ignore deprecated syscalls that are no longer implemented
		// https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/tree/include/uapi/asm-generic/unistd.h?id=refs/heads/master#n716
		return 0, ""
	}
	name = strings.ToUpper(name)
	num = num + offset
	return num, fmt.Sprintf("	SYS_%s = %d;\n", name, num)
}

func checkErr(err error) {
	if err != nil {
		fmt.Fprintf(os.Stderr, "%v\n", err)
		os.Exit(1)
	}
}

// source string and substring slice for regexp
type re struct {
	str string   // source string
	sub []string // matched sub-string
}

// Match performs regular expression match
func (r *re) Match(exp string) bool {
	r.sub = regexp.MustCompile(exp).FindStringSubmatch(r.str)
	if r.sub != nil {
		return true
	}
	return false
}

// syscallNum holds the syscall number and the string
// we will write to the generated file.
type syscallNum struct {
	num         int
	declaration string
}

// syscallNums is a slice of syscallNum sorted by the syscall number in ascending order.
type syscallNums []syscallNum

// addSyscallNum adds the syscall declaration to syscallNums.
func (nums *syscallNums) addSyscallNum(num int, declaration string) {
	if declaration == "" {
		return
	}
	if len(*nums) == 0 || (*nums)[len(*nums)-1].num <= num {
		// This is the most common case as the syscall declarations output by the preprocessor
		// are almost always sorted.
		*nums = append(*nums, syscallNum{num, declaration})
		return
	}
	i := sort.Search(len(*nums), func(i int) bool { return (*nums)[i].num >= num })

	// Maintain the ordering in the preprocessor output when we have multiple definitions with
	// the same value. i cannot be > len(nums) - 1 as nums[len(nums)-1].num > num.
	for ; (*nums)[i].num == num; i++ {
	}
	*nums = append((*nums)[:i], append([]syscallNum{{num, declaration}}, (*nums)[i:]...)...)
}

func main() {
	// Get the OS and architecture (using GOARCH_TARGET if it exists)
	goos = os.Getenv("GOOS")
	goarch = os.Getenv("GOARCH_TARGET")
	if goarch == "" {
		goarch = os.Getenv("GOARCH")
	}
	// Check if GOOS and GOARCH environment variables are defined
	if goarch == "" || goos == "" {
		fmt.Fprintf(os.Stderr, "GOARCH or GOOS not defined in environment\n")
		os.Exit(1)
	}
	// Check that we are using the new build system if we should
	if os.Getenv("GOLANG_SYS_BUILD") != "docker" {
		fmt.Fprintf(os.Stderr, "In the new build system, mksysnum should not be called directly.\n")
		fmt.Fprintf(os.Stderr, "See README.md\n")
		os.Exit(1)
	}

	cc := os.Getenv("CC")
	if cc == "" {
		fmt.Fprintf(os.Stderr, "CC is not defined in environment\n")
		os.Exit(1)
	}
	args := os.Args[1:]
	args = append([]string{"-E", "-dD"}, args...)
	cmd, err := exec.Command(cc, args...).Output() // execute command and capture output
	if err != nil {
		fmt.Fprintf(os.Stderr, "can't run %s", cc)
		os.Exit(1)
	}

	switch goarch {
	case "riscv64", "loong64", "arm64":
		// Kernel linux v6.11 removed some __NR_* macros that only
		// existed on some architectures as an implementation detail. In
		// order to keep backwards compatibility we add them back.
		//
		// See https://lkml.org/lkml/2024/8/5/1283.
		if !bytes.Contains(cmd, []byte("#define __NR_arch_specific_syscall")) {
			cmd = append(cmd, []byte("#define __NR_arch_specific_syscall 244\n")...)
		}
	}

	s := bufio.NewScanner(strings.NewReader(string(cmd)))
	var offset, prev, asOffset int
	var nums syscallNums
	for s.Scan() {
		t := re{str: s.Text()}

		// The generated zsysnum_linux_*.go files for some platforms (arm64, loong64, riscv64)
		// treat SYS_ARCH_SPECIFIC_SYSCALL as if it's a syscall which it isn't.  It's an offset.
		// However, as this constant is already part of the public API we leave it in place.
		// Lines of type SYS_ARCH_SPECIFIC_SYSCALL = 244 are thus processed twice, once to extract
		// the offset and once to add the constant.

		if t.Match(`^#define __NR_arch_specific_syscall\s+([0-9]+)`) {
			// riscv: extract arch specific offset
			asOffset, _ = strconv.Atoi(t.sub[1]) // Make asOffset=0 if empty or non-numeric
		}

		if t.Match(`^#define __NR_Linux\s+([0-9]+)`) {
			// mips/mips64: extract offset
			offset, _ = strconv.Atoi(t.sub[1]) // Make offset=0 if empty or non-numeric
		} else if t.Match(`^#define __NR(\w*)_SYSCALL_BASE\s+([0-9]+)`) {
			// arm: extract offset
			offset, _ = strconv.Atoi(t.sub[1]) // Make offset=0 if empty or non-numeric
		} else if t.Match(`^#define __NR_syscalls\s+`) {
			// ignore redefinitions of __NR_syscalls
		} else if t.Match(`^#define __NR_(\w*)Linux_syscalls\s+`) {
			// mips/mips64: ignore definitions about the number of syscalls
		} else if t.Match(`^#define __NR_(\w+)\s+([0-9]+)`) {
			prev, err = strconv.Atoi(t.sub[2])
			checkErr(err)
			nums.addSyscallNum(format(t.sub[1], prev, offset))
		} else if t.Match(`^#define __NR3264_(\w+)\s+([0-9]+)`) {
			prev, err = strconv.Atoi(t.sub[2])
			checkErr(err)
			nums.addSyscallNum(format(t.sub[1], prev, offset))
		} else if t.Match(`^#define __NR_(\w+)\s+\(\w+\+\s*([0-9]+)\)`) {
			r2, err := strconv.Atoi(t.sub[2])
			checkErr(err)
			nums.addSyscallNum(format(t.sub[1], prev+r2, offset))
		} else if t.Match(`^#define __NR_(\w+)\s+\(__NR_(?:SYSCALL_BASE|Linux) \+ ([0-9]+)`) {
			r2, err := strconv.Atoi(t.sub[2])
			checkErr(err)
			nums.addSyscallNum(format(t.sub[1], r2, offset))
		} else if asOffset != 0 && t.Match(`^#define __NR_(\w+)\s+\(__NR_arch_specific_syscall \+ ([0-9]+)`) {
			r2, err := strconv.Atoi(t.sub[2])
			checkErr(err)
			nums.addSyscallNum(format(t.sub[1], r2, asOffset))
		}
	}
	err = s.Err()
	checkErr(err)
	var text strings.Builder
	for _, num := range nums {
		text.WriteString(num.declaration)
	}
	fmt.Printf(template, cmdLine(), goBuildTags(), text.String())
}

const template = `// %s
// Code generated by the command above; see README.md. DO NOT EDIT.

//go:build %s

package unix

const(
%s)`
