// goredo -- djb's redo implementation on pure Go
// Copyright (C) 2020-2026 Sergey Matveev <stargrave@stargrave.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

// Jobserver

package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"regexp"
	"strconv"
	"sync"
)

const (
	EnvMakeFlags = "MAKEFLAGS"

	EnvJSFd    = "REDO_JS_FD"
	EnvJobs    = "REDO_JOBS"
	EnvJSToken = "REDO_JS_TOKEN"
	EnvMake    = "REDO_MAKE"

	MakeTypeNone  = "none"
	MakeTypeBmake = "bmake"
	MakeTypeGmake = "gmake"
)

var (
	// bmake (NetBSD make)
	BMakeGoodToken = byte('+')
	BMakeJSArg     = "-j 1 -J "
	BMakeJSArgRe   = regexp.MustCompile(`(.*)\s*-J (\d+),(\d+)\s*(.*)`)

	// GNU Make
	GMakeJSArg   = "--jobserver-auth="
	GMakeJSArgRe = regexp.MustCompile(`(.*)\s*--jobserver-auth=(\d+),(\d+)\s*(.*)`)

	// dummy make
	DMakeJSArg   = ""
	DMakeJSArgRe = regexp.MustCompile(`(.*)\s*(\d+),(\d+)\s*(.*)`)

	MakeFlagsName = EnvMakeFlags
	MakeFlags     string
	MakeJSArg     string

	JSR *os.File
	JSW *os.File

	jsToken   byte // got via EnvJSToken
	jsTokens  map[byte]int
	jsTokensM sync.Mutex

	flagJobs *int
)

func init() {
	cmdName := CmdName()
	if !(cmdName == CmdNameRedo || cmdName == CmdNameRedoIfchange) {
		return
	}
	flagJobs = flag.Int("j", -1,
		fmt.Sprintf("number of parallel jobs (0=inf, <0=1) (%s)", EnvJobs))
}

func jsStart(jobsEnv string) {
	jobs := uint64(1)
	var err error
	switch {
	case *flagJobs == 0:
		jobs = 0
	case *flagJobs > 0:
		jobs = uint64(*flagJobs)
	case jobsEnv != "":
		jobs, err = strconv.ParseUint(jobsEnv, 10, 64)
		if err != nil {
			log.Fatalln("can not parse", EnvJobs, err)
		}
	}
	if jobs == 0 {
		// infinite jobs
		return
	}
	JSR, JSW, err = os.Pipe()
	if err != nil {
		log.Fatal(err)
	}
	tracef(CJS, "initial fill with %d", jobs)
	jsTokens[BMakeGoodToken] = int(jobs)
	for ; jobs > 0; jobs-- {
		jsReleaseNoLock(BMakeGoodToken)
	}
}

func jsInit() {
	jsTokens = make(map[byte]int)

	makeType := os.Getenv(EnvMake)
	var makeArgRe *regexp.Regexp
	switch makeType {
	case MakeTypeGmake:
		makeArgRe = GMakeJSArgRe
		MakeJSArg = GMakeJSArg
	case MakeTypeBmake:
		makeArgRe = BMakeJSArgRe
		MakeJSArg = BMakeJSArg
	case "":
		fallthrough
	case MakeTypeNone:
		MakeFlagsName = EnvJSFd
		makeArgRe = DMakeJSArgRe
		MakeJSArg = DMakeJSArg
	default:
		log.Fatalln("unknown", EnvMake, "type")
	}

	MakeFlags = os.Getenv(MakeFlagsName)
	jobsEnv := os.Getenv(EnvJobs)
	if jobsEnv == "NO" {
		// jobserver disabled, infinite jobs
		return
	}
	if MakeFlags == "" {
		// we are not running under make
		jsStart(jobsEnv)
		return
	}

	match := makeArgRe.FindStringSubmatch(MakeFlags)
	if len(match) == 0 {
		// MAKEFLAGS does not contain anything related to jobserver
		jsStart(jobsEnv)
		return
	}
	MakeFlags = match[1] + " " + match[4]

	func() {
		defer func() {
			if err := recover(); err != nil {
				log.Fatal(err)
			}
		}()
		JSR = mustParseFd(match[2], "JSR")
		JSW = mustParseFd(match[3], "JSW")
	}()

	if token := os.Getenv(EnvJSToken); token != "" {
		jsTokenInt, err := strconv.ParseUint(token, 10, 8)
		if err != nil {
			log.Fatalln("invalid", EnvJSToken, "format:", err)
		}
		jsToken = byte(jsTokenInt)
		jsTokens[jsToken]++
		jsRelease("ifchange entered", jsToken)
	}
}

func jsReleaseNoLock(token byte) {
	if n, err := JSW.Write([]byte{token}); err != nil || n != 1 {
		log.Fatalln("can not write JSW:", err)
	}
}

func jsRelease(ctx string, token byte) {
	if JSW == nil {
		return
	}
	tracef(CJS, "release from: %s", ctx)
	jsTokensM.Lock()
	jsTokens[token]--
	jsReleaseNoLock(token)
	jsTokensM.Unlock()
}

func jsReleaseAll() {
	jsTokensM.Lock()
	for token, i := range jsTokens {
		for ; i > 0; i-- {
			jsReleaseNoLock(token)
		}
	}
	jsTokensM.Unlock()
}

func jsAcquire(ctx string) byte {
	if JSR == nil {
		return BMakeGoodToken
	}
	tracef(CJS, "acquire for: %s", ctx)
	token := []byte{0}
	if n, err := JSR.Read(token); err != nil || n != 1 {
		log.Fatalln("can not read JSR:", err)
	}
	jsTokensM.Lock()
	jsTokens[token[0]]++
	jsTokensM.Unlock()
	tracef(CJS, "acquired for: %s", ctx)
	return token[0]
}
