package builder

import (
	"bytes"
	"fmt"
	"go/scanner"
	"go/token"
	"os"
	"os/exec"
	"regexp"
	"strconv"
	"strings"
)

// runCCompiler invokes a C compiler with the given arguments.
func runCCompiler(flags ...string) error {
	// Find the right command to run Clang.
	var cmd *exec.Cmd
	if hasBuiltinTools {
		// Compile this with the internal Clang compiler.
		cmd = exec.Command(os.Args[0], append([]string{"clang"}, flags...)...)
	} else {
		// Compile this with an external invocation of the Clang compiler.
		name, err := LookupCommand("clang")
		if err != nil {
			return err
		}
		cmd = exec.Command(name, flags...)
	}

	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	// Make sure the command doesn't use any environmental variables.
	// Most importantly, it should not use C_INCLUDE_PATH and the like.
	cmd.Env = []string{}

	// Let some environment variables through. One important one is the
	// temporary directory, especially on Windows it looks like Clang breaks if
	// the temporary directory has not been set.
	// See: https://github.com/tinygo-org/tinygo/issues/4557
	// Also see: https://github.com/llvm/llvm-project/blob/release/18.x/llvm/lib/Support/Unix/Path.inc#L1435
	for _, env := range os.Environ() {
		// We could parse the key and look it up in a map, but since there are
		// only a few keys iterating through them is easier and maybe even
		// faster.
		for _, prefix := range []string{"TMPDIR=", "TMP=", "TEMP=", "TEMPDIR="} {
			if strings.HasPrefix(env, prefix) {
				cmd.Env = append(cmd.Env, env)
				break
			}
		}
	}

	return cmd.Run()
}

// link invokes a linker with the given name and flags.
func link(linker string, flags ...string) error {
	// We only support LLD.
	if linker != "ld.lld" && linker != "wasm-ld" {
		return fmt.Errorf("unexpected: linker %s should be ld.lld or wasm-ld", linker)
	}

	var cmd *exec.Cmd
	if hasBuiltinTools {
		cmd = exec.Command(os.Args[0], append([]string{linker}, flags...)...)
	} else {
		name, err := LookupCommand(linker)
		if err != nil {
			return err
		}
		cmd = exec.Command(name, flags...)
	}
	var buf bytes.Buffer
	cmd.Stdout = os.Stdout
	cmd.Stderr = &buf
	err := cmd.Run()
	if err != nil {
		if buf.Len() == 0 {
			// The linker failed but there was no output.
			// Therefore, show some output anyway.
			return fmt.Errorf("failed to run linker: %w", err)
		}
		return parseLLDErrors(buf.String())
	}
	return nil
}

// Split LLD errors into individual erros (including errors that continue on the
// next line, using a ">>>" prefix). If possible, replace the raw errors with a
// more user-friendly version (and one that's more in a Go style).
func parseLLDErrors(text string) error {
	// Split linker output in separate error messages.
	lines := strings.Split(text, "\n")
	var errorLines []string // one or more line (belonging to a single error) per line
	for _, line := range lines {
		line = strings.TrimRight(line, "\r") // needed for Windows
		if len(errorLines) != 0 && strings.HasPrefix(line, ">>> ") {
			errorLines[len(errorLines)-1] += "\n" + line
			continue
		}
		if line == "" {
			continue
		}
		errorLines = append(errorLines, line)
	}

	// Parse error messages.
	var linkErrors []error
	var flashOverflow, ramOverflow uint64
	for _, message := range errorLines {
		parsedError := false

		// Check for undefined symbols.
		// This can happen in some cases like with CGo and //go:linkname tricker.
		if matches := regexp.MustCompile(`^ld.lld(-[0-9]+)?: error: undefined symbol: (.*)\n`).FindStringSubmatch(message); matches != nil {
			symbolName := matches[2]
			for line := range strings.SplitSeq(message, "\n") {
				matches := regexp.MustCompile(`referenced by .* \(((.*):([0-9]+))\)`).FindStringSubmatch(line)
				if matches != nil {
					parsedError = true
					line, _ := strconv.Atoi(matches[3])
					msg := "linker could not find symbol " + symbolName
					switch symbolName {
					case "runtime.alloc":
						msg = "object allocated on the heap with -gc=none"
					case "runtime.alloc_noheap":
						msg = "object allocated on the heap in //go:noheap function"
					}
					linkErrors = append(linkErrors, scanner.Error{
						Pos: token.Position{
							Filename: matches[2],
							Line:     line,
						},
						Msg: msg,
					})
				}
			}
		}

		// Check for flash/RAM overflow.
		if matches := regexp.MustCompile(`^ld.lld(-[0-9]+)?: error: section '(.*?)' will not fit in region '(.*?)': overflowed by ([0-9]+) bytes$`).FindStringSubmatch(message); matches != nil {
			region := matches[3]
			n, err := strconv.ParseUint(matches[4], 10, 64)
			if err != nil {
				// Should not happen at all (unless it overflows an uint64 for some reason).
				continue
			}

			// Check which area overflowed.
			// Some chips use differently named memory areas, but these are by
			// far the most common.
			switch region {
			case "FLASH_TEXT":
				if n > flashOverflow {
					flashOverflow = n
				}
				parsedError = true
			case "RAM":
				if n > ramOverflow {
					ramOverflow = n
				}
				parsedError = true
			}
		}

		// If we couldn't parse the linker error: show the error as-is to
		// the user.
		if !parsedError {
			linkErrors = append(linkErrors, LinkerError{message})
		}
	}

	if flashOverflow > 0 {
		linkErrors = append(linkErrors, LinkerError{
			Msg: fmt.Sprintf("program too large for this chip (flash overflowed by %d bytes)\n\toptimization guide: https://tinygo.org/docs/guides/optimizing-binaries/", flashOverflow),
		})
	}
	if ramOverflow > 0 {
		linkErrors = append(linkErrors, LinkerError{
			Msg: fmt.Sprintf("program uses too much static RAM on this chip (RAM overflowed by %d bytes)", ramOverflow),
		})
	}

	return newMultiError(linkErrors, "")
}

// LLD linker error that could not be parsed or doesn't refer to a source
// location.
type LinkerError struct {
	Msg string
}

func (e LinkerError) Error() string {
	return e.Msg
}
