// Copyright 2012 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.

// Package envcmd implements the ``go env'' command.
package envcmd

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
	"unicode/utf8"

	"cmd/go/internal/base"
	"cmd/go/internal/cache"
	"cmd/go/internal/cfg"
	"cmd/go/internal/load"
	"cmd/go/internal/modload"
	"cmd/go/internal/work"
)

var CmdEnv = &base.Command{
	UsageLine: "go env [-json] [-u] [-w] [var ...]",
	Short:     "print Go environment information",
	Long: `
Env prints Go environment information.

By default env prints information as a shell script
(on Windows, a batch file). If one or more variable
names is given as arguments, env prints the value of
each named variable on its own line.

The -json flag prints the environment in JSON format
instead of as a shell script.

The -u flag requires one or more arguments and unsets
the default setting for the named environment variables,
if one has been set with 'go env -w'.

The -w flag requires one or more arguments of the
form NAME=VALUE and changes the default settings
of the named environment variables to the given values.

For more about environment variables, see 'go help environment'.
	`,
}

func init() {
	CmdEnv.Run = runEnv // break init cycle
}

var (
	envJson = CmdEnv.Flag.Bool("json", false, "")
	envU    = CmdEnv.Flag.Bool("u", false, "")
	envW    = CmdEnv.Flag.Bool("w", false, "")
)

func MkEnv() []cfg.EnvVar {
	var b work.Builder
	b.Init()

	envFile, _ := cfg.EnvFile()
	env := []cfg.EnvVar{
		{Name: "GO111MODULE", Value: cfg.Getenv("GO111MODULE")},
		{Name: "GOARCH", Value: cfg.Goarch},
		{Name: "GOBIN", Value: cfg.GOBIN},
		{Name: "GOCACHE", Value: cache.DefaultDir()},
		{Name: "GOENV", Value: envFile},
		{Name: "GOEXE", Value: cfg.ExeSuffix},
		{Name: "GOFLAGS", Value: cfg.Getenv("GOFLAGS")},
		{Name: "GOHOSTARCH", Value: runtime.GOARCH},
		{Name: "GOHOSTOS", Value: runtime.GOOS},
		{Name: "GONOPROXY", Value: cfg.GONOPROXY},
		{Name: "GONOSUMDB", Value: cfg.GONOSUMDB},
		{Name: "GOOS", Value: cfg.Goos},
		{Name: "GOPATH", Value: cfg.BuildContext.GOPATH},
		{Name: "GOPROXY", Value: cfg.GOPROXY},
		{Name: "GOROOT", Value: cfg.GOROOT},
		{Name: "GOSUMDB", Value: cfg.GOSUMDB},
		{Name: "GOTMPDIR", Value: cfg.Getenv("GOTMPDIR")},
		{Name: "GOTOOLDIR", Value: base.ToolDir},
	}

	if work.GccgoBin != "" {
		env = append(env, cfg.EnvVar{Name: "GCCGO", Value: work.GccgoBin})
	} else {
		env = append(env, cfg.EnvVar{Name: "GCCGO", Value: work.GccgoName})
	}

	key, val := cfg.GetArchEnv()
	if key != "" {
		env = append(env, cfg.EnvVar{Name: key, Value: val})
	}

	cc := cfg.DefaultCC(cfg.Goos, cfg.Goarch)
	if env := strings.Fields(cfg.Getenv("CC")); len(env) > 0 {
		cc = env[0]
	}
	cxx := cfg.DefaultCXX(cfg.Goos, cfg.Goarch)
	if env := strings.Fields(cfg.Getenv("CXX")); len(env) > 0 {
		cxx = env[0]
	}
	env = append(env, cfg.EnvVar{Name: "AR", Value: envOr("AR", "ar")})
	env = append(env, cfg.EnvVar{Name: "CC", Value: cc})
	env = append(env, cfg.EnvVar{Name: "CXX", Value: cxx})

	if cfg.BuildContext.CgoEnabled {
		env = append(env, cfg.EnvVar{Name: "CGO_ENABLED", Value: "1"})
	} else {
		env = append(env, cfg.EnvVar{Name: "CGO_ENABLED", Value: "0"})
	}

	return env
}

func envOr(name, def string) string {
	val := cfg.Getenv(name)
	if val != "" {
		return val
	}
	return def
}

func findEnv(env []cfg.EnvVar, name string) string {
	for _, e := range env {
		if e.Name == name {
			return e.Value
		}
	}
	return ""
}

// ExtraEnvVars returns environment variables that should not leak into child processes.
func ExtraEnvVars() []cfg.EnvVar {
	gomod := ""
	if modload.HasModRoot() {
		gomod = filepath.Join(modload.ModRoot(), "go.mod")
	} else if modload.Enabled() {
		gomod = os.DevNull
	}
	return []cfg.EnvVar{
		{Name: "GOMOD", Value: gomod},
	}
}

// ExtraEnvVarsCostly returns environment variables that should not leak into child processes
// but are costly to evaluate.
func ExtraEnvVarsCostly() []cfg.EnvVar {
	var b work.Builder
	b.Init()
	cppflags, cflags, cxxflags, fflags, ldflags, err := b.CFlags(&load.Package{})
	if err != nil {
		// Should not happen - b.CFlags was given an empty package.
		fmt.Fprintf(os.Stderr, "go: invalid cflags: %v\n", err)
		return nil
	}
	cmd := b.GccCmd(".", "")

	return []cfg.EnvVar{
		// Note: Update the switch in runEnv below when adding to this list.
		{Name: "CGO_CFLAGS", Value: strings.Join(cflags, " ")},
		{Name: "CGO_CPPFLAGS", Value: strings.Join(cppflags, " ")},
		{Name: "CGO_CXXFLAGS", Value: strings.Join(cxxflags, " ")},
		{Name: "CGO_FFLAGS", Value: strings.Join(fflags, " ")},
		{Name: "CGO_LDFLAGS", Value: strings.Join(ldflags, " ")},
		{Name: "PKG_CONFIG", Value: b.PkgconfigCmd()},
		{Name: "GOGCCFLAGS", Value: strings.Join(cmd[3:], " ")},
	}
}

// argKey returns the KEY part of the arg KEY=VAL, or else arg itself.
func argKey(arg string) string {
	i := strings.Index(arg, "=")
	if i < 0 {
		return arg
	}
	return arg[:i]
}

func runEnv(cmd *base.Command, args []string) {
	if *envJson && *envU {
		base.Fatalf("go env: cannot use -json with -u")
	}
	if *envJson && *envW {
		base.Fatalf("go env: cannot use -json with -w")
	}
	if *envU && *envW {
		base.Fatalf("go env: cannot use -u with -w")
	}
	env := cfg.CmdEnv
	env = append(env, ExtraEnvVars()...)

	// Do we need to call ExtraEnvVarsCostly, which is a bit expensive?
	// Only if we're listing all environment variables ("go env")
	// or the variables being requested are in the extra list.
	needCostly := true
	if len(args) > 0 {
		needCostly = false
		for _, arg := range args {
			switch argKey(arg) {
			case "CGO_CFLAGS",
				"CGO_CPPFLAGS",
				"CGO_CXXFLAGS",
				"CGO_FFLAGS",
				"CGO_LDFLAGS",
				"PKG_CONFIG",
				"GOGCCFLAGS":
				needCostly = true
			}
		}
	}
	if needCostly {
		env = append(env, ExtraEnvVarsCostly()...)
	}

	if *envW {
		// Process and sanity-check command line.
		if len(args) == 0 {
			base.Fatalf("go env -w: no KEY=VALUE arguments given")
		}
		osEnv := make(map[string]string)
		for _, e := range cfg.OrigEnv {
			if i := strings.Index(e, "="); i >= 0 {
				osEnv[e[:i]] = e[i+1:]
			}
		}
		add := make(map[string]string)
		for _, arg := range args {
			i := strings.Index(arg, "=")
			if i < 0 {
				base.Fatalf("go env -w: arguments must be KEY=VALUE: invalid argument: %s", arg)
			}
			key, val := arg[:i], arg[i+1:]
			if err := checkEnvWrite(key, val, env); err != nil {
				base.Fatalf("go env -w: %v", err)
			}
			if _, ok := add[key]; ok {
				base.Fatalf("go env -w: multiple values for key: %s", key)
			}
			add[key] = val
			if osVal := osEnv[key]; osVal != "" && osVal != val {
				fmt.Fprintf(os.Stderr, "warning: go env -w %s=... does not override conflicting OS environment variable\n", key)
			}
		}
		updateEnvFile(add, nil)
		return
	}

	if *envU {
		// Process and sanity-check command line.
		if len(args) == 0 {
			base.Fatalf("go env -u: no arguments given")
		}
		del := make(map[string]bool)
		for _, arg := range args {
			if err := checkEnvWrite(arg, "", env); err != nil {
				base.Fatalf("go env -u: %v", err)
			}
			del[arg] = true
		}
		updateEnvFile(nil, del)
		return
	}

	if len(args) > 0 {
		if *envJson {
			var es []cfg.EnvVar
			for _, name := range args {
				e := cfg.EnvVar{Name: name, Value: findEnv(env, name)}
				es = append(es, e)
			}
			printEnvAsJSON(es)
		} else {
			for _, name := range args {
				fmt.Printf("%s\n", findEnv(env, name))
			}
		}
		return
	}

	if *envJson {
		printEnvAsJSON(env)
		return
	}

	for _, e := range env {
		if e.Name != "TERM" {
			switch runtime.GOOS {
			default:
				fmt.Printf("%s=\"%s\"\n", e.Name, e.Value)
			case "plan9":
				if strings.IndexByte(e.Value, '\x00') < 0 {
					fmt.Printf("%s='%s'\n", e.Name, strings.ReplaceAll(e.Value, "'", "''"))
				} else {
					v := strings.Split(e.Value, "\x00")
					fmt.Printf("%s=(", e.Name)
					for x, s := range v {
						if x > 0 {
							fmt.Printf(" ")
						}
						fmt.Printf("%s", s)
					}
					fmt.Printf(")\n")
				}
			case "windows":
				fmt.Printf("set %s=%s\n", e.Name, e.Value)
			}
		}
	}
}

func printEnvAsJSON(env []cfg.EnvVar) {
	m := make(map[string]string)
	for _, e := range env {
		if e.Name == "TERM" {
			continue
		}
		m[e.Name] = e.Value
	}
	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "\t")
	if err := enc.Encode(m); err != nil {
		base.Fatalf("go env -json: %s", err)
	}
}

func checkEnvWrite(key, val string, env []cfg.EnvVar) error {
	switch key {
	case "GOEXE", "GOGCCFLAGS", "GOHOSTARCH", "GOHOSTOS", "GOMOD", "GOTOOLDIR":
		return fmt.Errorf("%s cannot be modified", key)
	case "GOENV":
		return fmt.Errorf("%s can only be set using the OS environment", key)
	}

	// To catch typos and the like, check that we know the variable.
	if !cfg.CanGetenv(key) {
		return fmt.Errorf("unknown go command variable %s", key)
	}

	if !utf8.ValidString(val) {
		return fmt.Errorf("invalid UTF-8 in %s=... value", key)
	}
	if strings.Contains(val, "\x00") {
		return fmt.Errorf("invalid NUL in %s=... value", key)
	}
	if strings.ContainsAny(val, "\v\r\n") {
		return fmt.Errorf("invalid newline in %s=... value", key)
	}
	return nil
}

func updateEnvFile(add map[string]string, del map[string]bool) {
	file, err := cfg.EnvFile()
	if file == "" {
		base.Fatalf("go env: cannot find go env config: %v", err)
	}
	data, err := ioutil.ReadFile(file)
	if err != nil && (!os.IsNotExist(err) || len(add) == 0) {
		base.Fatalf("go env: reading go env config: %v", err)
	}

	lines := strings.SplitAfter(string(data), "\n")
	if lines[len(lines)-1] == "" {
		lines = lines[:len(lines)-1]
	} else {
		lines[len(lines)-1] += "\n"
	}

	// Delete all but last copy of any duplicated variables,
	// since the last copy is the one that takes effect.
	prev := make(map[string]int)
	for l, line := range lines {
		if key := lineToKey(line); key != "" {
			if p, ok := prev[key]; ok {
				lines[p] = ""
			}
			prev[key] = l
		}
	}

	// Add variables (go env -w). Update existing lines in file if present, add to end otherwise.
	for key, val := range add {
		if p, ok := prev[key]; ok {
			lines[p] = key + "=" + val + "\n"
			delete(add, key)
		}
	}
	for key, val := range add {
		lines = append(lines, key+"="+val+"\n")
	}

	// Delete requested variables (go env -u).
	for key := range del {
		if p, ok := prev[key]; ok {
			lines[p] = ""
		}
	}

	// Sort runs of KEY=VALUE lines
	// (that is, blocks of lines where blocks are separated
	// by comments, blank lines, or invalid lines).
	start := 0
	for i := 0; i <= len(lines); i++ {
		if i == len(lines) || lineToKey(lines[i]) == "" {
			sortKeyValues(lines[start:i])
			start = i + 1
		}
	}

	data = []byte(strings.Join(lines, ""))
	err = ioutil.WriteFile(file, data, 0666)
	if err != nil {
		// Try creating directory.
		os.MkdirAll(filepath.Dir(file), 0777)
		err = ioutil.WriteFile(file, data, 0666)
		if err != nil {
			base.Fatalf("go env: writing go env config: %v", err)
		}
	}
}

// lineToKey returns the KEY part of the line KEY=VALUE or else an empty string.
func lineToKey(line string) string {
	i := strings.Index(line, "=")
	if i < 0 || strings.Contains(line[:i], "#") {
		return ""
	}
	return line[:i]
}

// sortKeyValues sorts a sequence of lines by key.
// It differs from sort.Strings in that GO386= sorts after GO=.
func sortKeyValues(lines []string) {
	sort.Slice(lines, func(i, j int) bool {
		return lineToKey(lines[i]) < lineToKey(lines[j])
	})
}
