package pkgpath

import (
	"fmt"
	"path"
	"sync"
)

// Main returns the import path of package main, the currently running
// command.  If the import path could not be determined, it returns "main" and
// false.
//
// GOMODULES support:
//
// This works by looking at debug.ReadBuildInfo() added in Go1.12. It does not work
// if the main package is ran/built with the filename.
//
//     go run ./cmd/app/*.go
//
// It is recommended to run/build the binary with the package path.
//
//     go run ./cmd/app
//
// GOPATH support:
//
// It does not work on binaries compiled on filesystems that use a separator
// other than '/'.  It does not correctly detect the import path if a segment
// of the path is "src".
//
// This works by looking at the file path where the main.init function is
// defined, searching for the rightmost "src" directory if the recorded file
// path is absolute.
//
// If the command has been compiled with a trimpath argument in gcflags, the
// available file path is relative. In this case, the directory of the file is
// assumed to be exactly the import path. When using the trimpath compiler
// flag, be sure to trim off the relevant GOPATH segment and the "src" path
// segment that follows it, like so (in the case of a single-element GOPATH):
//
//     go build -gcflags "-trimpath $(go env GOPATH)/src" ./...
func Main() (string, bool) {
	const strMain = "main"

	findMainOnce.Do(func() {
		path, err := findMainPath()
		if err == nil {
			mainPath = path
		}
		if path == "" {
			// best guess for a weird binary
			mainPath = strMain
		}
	})

	return mainPath, (mainPath != strMain)
}

var (
	findMainOnce sync.Once
	// the import path of package main for the current program, used by
	// pkgpath.Main
	mainPath string
)

var unsupportedGoVersion = fmt.Errorf("unsupported Go version")

var mainStackFilename, mainStackErr = findMainInit()

func findMainPath() (string, error) {
	// First choice: check build info
	path := buildInfoMainPath()
	if path != "" {
		return path, nil
	}

	// Second choice: walk up the stack during the init process. Available on
	// all platforms and reliable for some Go versions, but uses undocumented
	// details of how the Go compiler and linker generate init functions, how
	// they encode line info for the generated init functions, and how they
	// lay out the TEXT segment.
	//
	// Updated for Go 1.10, may break on subsequent releases.
	file, err := mainStackFilename, mainStackErr
	if err == nil {
		return fileImportPath(file), nil
	}

	// Third choice: read the symbol table of the executable from disk.
	// Requires access to the filesystem (won't work on NaCl), and a bit of
	// code for each executable format (ELF and Mach-O are done, Windows' PE
	// is not).
	file, err = findExecutableMainFile()
	if err == nil {
		return fileImportPath(file), nil
	}

	return "", unsupportedGoVersion
}

func fileImportPath(filename string) string {
	if !path.IsAbs(filename) {
		// A relative path implies the "trimpath" compiler flag, which should
		// cut off exactly the right prefix. Chop off the name of the file and
		// we're left with the import path.
		return path.Dir(filename)
	}

	prefix, suffix := filename, ""
	var revparts, parts []string
	for {
		prefix, suffix = path.Split(prefix)
		prefix = path.Clean(prefix)

		if len(suffix) == 0 {
			// We got to the root without finding a "src" segment - maybe this
			// is "go run" of a file that's not in GOPATH?
			return ""
		}
		if suffix == "src" {
			break
		}

		revparts = append(revparts, suffix)
	}

	for i := len(revparts) - 1; i >= 0; i-- {
		parts = append(parts, revparts[i])
	}
	return path.Dir(path.Join(parts...))
}
