package interp

import (
	"errors"
	"fmt"
	"io/fs"
	"path"
	"path/filepath"
	"strings"
)

// importSrc calls gta on the source code for the package identified by
// importPath. rPath is the relative path to the directory containing the source
// code for the package. It can also be "main" as a special value.
func (interp *Interpreter) importSrc(rPath, importPath string, skipTest bool) (string, error) {
	var dir string
	var err error

	if interp.srcPkg[importPath] != nil {
		name, ok := interp.pkgNames[importPath]
		if !ok {
			return "", fmt.Errorf("inconsistent knowledge about %s", importPath)
		}
		return name, nil
	}

	// For relative import paths in the form "./xxx" or "../xxx", the initial
	// base path is the directory of the interpreter input file, or "." if no file
	// was provided.
	// In all other cases, absolute import paths are resolved from the GOPATH
	// and the nested "vendor" directories.
	if isPathRelative(importPath) {
		if rPath == mainID {
			rPath = "."
		}
		dir = path.Join(path.Dir(interp.name), rPath, importPath)
	} else if dir, rPath, err = interp.pkgDir(filepath.ToSlash(interp.context.GOPATH), rPath, importPath); err != nil {
		// Try again, assuming a root dir at the source location.
		if rPath, err = interp.rootFromSourceLocation(); err != nil {
			return "", err
		}
		if dir, rPath, err = interp.pkgDir(filepath.ToSlash(interp.context.GOPATH), rPath, importPath); err != nil {
			return "", err
		}
	}

	if interp.rdir[importPath] {
		return "", fmt.Errorf("import cycle not allowed\n\timports %s", importPath)
	}
	interp.rdir[importPath] = true

	files, err := fs.ReadDir(interp.opt.filesystem, dir)
	if err != nil {
		return "", err
	}

	var initNodes []*node
	var rootNodes []*node
	revisit := make(map[string][]*node)

	var root *node
	var pkgName string

	// Parse source files.
	for _, file := range files {
		name := file.Name()
		if skipFile(&interp.context, name, skipTest) {
			continue
		}

		name = path.Join(dir, name)
		var buf []byte
		if buf, err = fs.ReadFile(interp.opt.filesystem, name); err != nil {
			return "", err
		}

		n, err := interp.parse(string(buf), name, false)
		if err != nil {
			return "", err
		}
		if n == nil {
			continue
		}

		var pname string
		if pname, root, err = interp.ast(n); err != nil {
			return "", err
		}
		if root == nil {
			continue
		}

		if interp.astDot {
			dotCmd := interp.dotCmd
			if dotCmd == "" {
				dotCmd = defaultDotCmd(name, "yaegi-ast-")
			}
			root.astDot(dotWriter(dotCmd), name)
		}
		if pkgName == "" {
			pkgName = pname
		} else if pkgName != pname && skipTest {
			return "", fmt.Errorf("found packages %s and %s in %s", pkgName, pname, dir)
		}
		rootNodes = append(rootNodes, root)

		subRPath := effectivePkg(rPath, importPath)
		var list []*node
		list, err = interp.gta(root, subRPath, importPath, pkgName)
		if err != nil {
			return "", err
		}
		revisit[subRPath] = append(revisit[subRPath], list...)
	}

	// Revisit incomplete nodes where GTA could not complete.
	for _, nodes := range revisit {
		if err = interp.gtaRetry(nodes, importPath, pkgName); err != nil {
			return "", err
		}
	}

	// Generate control flow graphs.
	for _, root := range rootNodes {
		var nodes []*node
		if nodes, err = interp.cfg(root, nil, importPath, pkgName); err != nil {
			return "", err
		}
		initNodes = append(initNodes, nodes...)
	}

	// Register source package in the interpreter. The package contains only
	// the global symbols in the package scope.
	interp.mutex.Lock()
	gs := interp.scopes[importPath]
	if gs == nil {
		interp.mutex.Unlock()
		// A nil scope means that no even an empty package is created from source.
		return "", fmt.Errorf("no Go files in %s", dir)
	}
	interp.srcPkg[importPath] = gs.sym
	interp.pkgNames[importPath] = pkgName

	interp.frame.mutex.Lock()
	interp.resizeFrame()
	interp.frame.mutex.Unlock()
	interp.mutex.Unlock()

	// Once all package sources have been parsed, execute entry points then init functions.
	for _, n := range rootNodes {
		if err = genRun(n); err != nil {
			return "", err
		}
		interp.run(n, nil)
	}

	// Wire and execute global vars in global scope gs.
	n, err := genGlobalVars(rootNodes, gs)
	if err != nil {
		return "", err
	}
	interp.run(n, nil)

	// Add main to list of functions to run, after all inits.
	if m := gs.sym[mainID]; pkgName == mainID && m != nil && skipTest {
		initNodes = append(initNodes, m.node)
	}

	for _, n := range initNodes {
		interp.run(n, interp.frame)
	}

	return pkgName, nil
}

// rootFromSourceLocation returns the path to the directory containing the input
// Go file given to the interpreter, relative to $GOPATH/src.
// It is meant to be called in the case when the initial input is a main package.
func (interp *Interpreter) rootFromSourceLocation() (string, error) {
	sourceFile := interp.name
	if sourceFile == DefaultSourceName {
		return "", nil
	}

	_, isRealFS := interp.opt.filesystem.(*realFS)
	if isRealFS {
		// In the "real" FS, GOPATH will be an absolute path, so we need to convert
		// the source file to an absolute path to compare them.
		absPath, err := filepath.Abs(filepath.FromSlash(sourceFile))
		if err != nil {
			return "", err
		}
		sourceFile = filepath.ToSlash(absPath)
	}
	pkgDir := path.Dir(sourceFile)
	goPath := path.Join(filepath.ToSlash(interp.context.GOPATH), "src") + "/"
	if !strings.HasPrefix(pkgDir, goPath) {
		return "", fmt.Errorf("package location %s not in GOPATH", pkgDir)
	}
	return strings.TrimPrefix(pkgDir, goPath), nil
}

// pkgDir returns the absolute path in filesystem for a package given its import path
// and the root of the subtree dependencies.
func (interp *Interpreter) pkgDir(goPath string, root, importPath string) (string, string, error) {
	rPath := path.Join(root, "vendor")
	dir := path.Join(goPath, "src", rPath, importPath)

	if _, err := fs.Stat(interp.opt.filesystem, dir); err == nil {
		return dir, rPath, nil // found!
	}

	dir = path.Join(goPath, "src", effectivePkg(root, importPath))

	if _, err := fs.Stat(interp.opt.filesystem, dir); err == nil {
		return dir, root, nil // found!
	}

	if root == "" {
		if interp.context.GOPATH == "" {
			return "", "", fmt.Errorf("unable to find source related to: %q. Either the GOPATH environment variable, or the Interpreter.Options.GoPath needs to be set", importPath)
		}
		return "", "", fmt.Errorf("unable to find source related to: %q", importPath)
	}

	rootPath := path.Join(goPath, "src", root)
	prevRoot, err := previousRoot(interp.opt.filesystem, rootPath, root)
	if err != nil {
		return "", "", err
	}

	return interp.pkgDir(goPath, prevRoot, importPath)
}

const vendor = "vendor"

// Find the previous source root (vendor > vendor > ... > GOPATH).
func previousRoot(filesystem fs.FS, rootPath, root string) (string, error) {
	rootPath = path.Clean(rootPath)
	parent, final := path.Split(rootPath)
	parent = path.Clean(parent)

	// TODO(mpl): maybe it works for the special case main, but can't be bothered for now.
	if root != mainID && final != vendor {
		root = strings.TrimSuffix(root, "/")
		prefix := strings.TrimSuffix(strings.TrimSuffix(rootPath, root), "/")

		// look for the closest vendor in one of our direct ancestors, as it takes priority.
		var vendored string
		for {
			fi, err := fs.Stat(filesystem, path.Join(parent, vendor))
			if err == nil && fi.IsDir() {
				vendored = strings.TrimPrefix(strings.TrimPrefix(parent, prefix), "/")
				break
			}
			if !errors.Is(err, fs.ErrNotExist) {
				return "", err
			}
			// stop when we reach GOPATH/src
			if parent == prefix {
				break
			}

			// stop when we reach GOPATH/src/blah
			parent = path.Dir(parent)
			if parent == prefix {
				break
			}

			// just an additional failsafe, stop if we reach the filesystem root, or dot (if
			// we are dealing with relative paths).
			// TODO(mpl): It should probably be a critical error actually,
			// as we shouldn't have gone that high up in the tree.
			// TODO(dennwc): This partially fails on Windows, since it cannot recognize drive letters as "root".
			if parent == "/" || parent == "." || parent == "" {
				break
			}
		}

		if vendored != "" {
			return vendored, nil
		}
	}

	// TODO(mpl): the algorithm below might be redundant with the one above,
	// but keeping it for now. Investigate/simplify/remove later.
	splitRoot := strings.Split(root, "/")
	var index int
	for i := len(splitRoot) - 1; i >= 0; i-- {
		if splitRoot[i] == "vendor" {
			index = i
			break
		}
	}

	if index == 0 {
		return "", nil
	}

	return path.Join(splitRoot[:index]...), nil
}

func effectivePkg(root, p string) string {
	splitRoot := strings.Split(root, "/")
	splitPath := strings.Split(p, "/")

	var result []string

	rootIndex := 0
	prevRootIndex := 0
	for i := 0; i < len(splitPath); i++ {
		part := splitPath[len(splitPath)-1-i]

		index := len(splitRoot) - 1 - rootIndex
		if index > 0 && part == splitRoot[index] && i != 0 {
			prevRootIndex = rootIndex
			rootIndex++
		} else if prevRootIndex == rootIndex {
			result = append(result, part)
		}
	}

	var frag string
	for i := len(result) - 1; i >= 0; i-- {
		frag = path.Join(frag, result[i])
	}

	return path.Join(root, frag)
}

// isPathRelative returns true if path starts with "./" or "../".
// It is intended for use on import paths, where "/" is always the directory separator.
func isPathRelative(s string) bool {
	return strings.HasPrefix(s, "./") || strings.HasPrefix(s, "../")
}
