// Copyright 2017 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 dep

import (
	"log"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"time"

	"github.com/golang/dep/gps"
	"github.com/golang/dep/gps/paths"
	"github.com/golang/dep/gps/pkgtree"
	"github.com/golang/dep/gps/verify"
	"github.com/golang/dep/internal/fs"
	"github.com/pkg/errors"
)

// Ctx defines the supporting context of dep.
//
// A properly initialized Ctx has a GOPATH containing the project root and non-nil Loggers.
//
//	ctx := &dep.Ctx{
//		WorkingDir: GOPATH + "/src/project/root",
//		GOPATH: GOPATH,
//		Out: log.New(os.Stdout, "", 0),
//		Err: log.New(os.Stderr, "", 0),
//	}
//
// Ctx.DetectProjectGOPATH() helps with setting the containing GOPATH.
//
//	ctx.GOPATH, err := Ctx.DetectProjectGOPATH(project)
//	if err != nil {
//		// Could not determine which GOPATH to use for the project.
//	}
//
type Ctx struct {
	WorkingDir     string        // Where to execute.
	GOPATH         string        // Selected Go path, containing WorkingDir.
	GOPATHs        []string      // Other Go paths.
	ExplicitRoot   string        // An explicitly-set path to use as the project root.
	Out, Err       *log.Logger   // Required loggers.
	Verbose        bool          // Enables more verbose logging.
	DisableLocking bool          // When set, no lock file will be created to protect against simultaneous dep processes.
	Cachedir       string        // Cache directory loaded from environment.
	CacheAge       time.Duration // Maximum valid age of cached source data. <=0: Don't cache.
}

// SetPaths sets the WorkingDir and GOPATHs fields. If GOPATHs is empty, then
// the GOPATH environment variable (or the default GOPATH) is used instead.
func (c *Ctx) SetPaths(wd string, GOPATHs ...string) error {
	if wd == "" {
		return errors.New("cannot set Ctx.WorkingDir to an empty path")
	}
	c.WorkingDir = wd

	if len(GOPATHs) == 0 {
		GOPATH := os.Getenv("GOPATH")
		if GOPATH == "" {
			GOPATH = defaultGOPATH()
		}
		GOPATHs = filepath.SplitList(GOPATH)
	}

	c.GOPATHs = append(c.GOPATHs, GOPATHs...)

	c.ExplicitRoot = os.Getenv("DEPPROJECTROOT")

	return nil
}

// defaultGOPATH gets the default GOPATH that was added in 1.8
// copied from go/build/build.go
func defaultGOPATH() string {
	env := "HOME"
	if runtime.GOOS == "windows" {
		env = "USERPROFILE"
	} else if runtime.GOOS == "plan9" {
		env = "home"
	}
	if home := os.Getenv(env); home != "" {
		def := filepath.Join(home, "go")
		if def == runtime.GOROOT() {
			// Don't set the default GOPATH to GOROOT,
			// as that will trigger warnings from the go tool.
			return ""
		}
		return def
	}
	return ""
}

// SourceManager produces an instance of gps's built-in SourceManager
// initialized to log to the receiver's logger.
func (c *Ctx) SourceManager() (*gps.SourceMgr, error) {
	cachedir := c.Cachedir
	if cachedir == "" {
		// When `DEPCACHEDIR` isn't set in the env, use the default - `$GOPATH/pkg/dep`.
		cachedir = filepath.Join(c.GOPATH, "pkg", "dep")
		// Create the default cachedir if it does not exist.
		if err := os.MkdirAll(cachedir, 0777); err != nil {
			return nil, errors.Wrap(err, "failed to create default cache directory")
		}
	}

	return gps.NewSourceManager(gps.SourceManagerConfig{
		CacheAge:       c.CacheAge,
		Cachedir:       cachedir,
		Logger:         c.Out,
		DisableLocking: c.DisableLocking,
	})
}

// LoadProject starts from the current working directory and searches up the
// directory tree for a project root.  The search stops when a file with the name
// ManifestName (Gopkg.toml, by default) is located.
//
// The Project contains the parsed manifest as well as a parsed lock file, if
// present.  The import path is calculated as the remaining path segment
// below Ctx.GOPATH/src.
func (c *Ctx) LoadProject() (*Project, error) {
	root, err := findProjectRoot(c.WorkingDir)
	if err != nil {
		return nil, err
	}

	err = checkGopkgFilenames(root)
	if err != nil {
		return nil, err
	}

	p := new(Project)

	if err = p.SetRoot(root); err != nil {
		return nil, err
	}

	c.GOPATH, err = c.DetectProjectGOPATH(p)
	if err != nil {
		return nil, err
	}

	if c.ExplicitRoot != "" {
		p.ImportRoot = gps.ProjectRoot(c.ExplicitRoot)
	} else {
		ip, err := c.ImportForAbs(p.AbsRoot)
		if err != nil {
			return nil, errors.Wrap(err, "root project import")
		}
		p.ImportRoot = gps.ProjectRoot(ip)
	}

	mp := filepath.Join(p.AbsRoot, ManifestName)
	mf, err := os.Open(mp)
	if err != nil {
		if os.IsNotExist(err) {
			// TODO: list possible solutions? (dep init, cd $project)
			return nil, errors.Errorf("no %v found in project root %v", ManifestName, p.AbsRoot)
		}
		// Unable to read the manifest file
		return nil, err
	}
	defer mf.Close()

	var warns []error
	p.Manifest, warns, err = readManifest(mf)
	for _, warn := range warns {
		c.Err.Printf("dep: WARNING: %v\n", warn)
	}
	if err != nil {
		return nil, errors.Wrapf(err, "error while parsing %s", mp)
	}

	// Parse in the root package tree.
	ptree, err := p.parseRootPackageTree()
	if err != nil {
		return nil, err
	}

	lp := filepath.Join(p.AbsRoot, LockName)
	lf, err := os.Open(lp)
	if err == nil {
		defer lf.Close()

		p.Lock, err = readLock(lf)
		if err != nil {
			return nil, errors.Wrapf(err, "error while parsing %s", lp)
		}

		// If there's a current Lock, apply the input and pruneopt changes that we
		// can know without solving.
		if p.Lock != nil {
			p.ChangedLock = p.Lock.dup()
			p.ChangedLock.SolveMeta.InputImports = externalImportList(ptree, p.Manifest)

			for k, lp := range p.ChangedLock.Projects() {
				vp := lp.(verify.VerifiableProject)
				vp.PruneOpts = p.Manifest.PruneOptions.PruneOptionsFor(lp.Ident().ProjectRoot)
				p.ChangedLock.P[k] = vp
			}
		}

	} else if !os.IsNotExist(err) {
		// It's fine for the lock not to exist, but if a file does exist and we
		// can't open it, that's a problem.
		return nil, errors.Wrapf(err, "could not open %s", lp)
	}

	return p, nil
}

func externalImportList(rpt pkgtree.PackageTree, m gps.RootManifest) []string {
	rm, _ := rpt.ToReachMap(true, true, false, m.IgnoredPackages())
	reach := rm.FlattenFn(paths.IsStandardImportPath)
	req := m.RequiredPackages()

	// If there are any requires, slide them into the reach list, as well.
	if len(req) > 0 {
		// Make a map of imports that are both in the import path list and the
		// required list to avoid duplication.
		skip := make(map[string]bool, len(req))
		for _, r := range reach {
			if req[r] {
				skip[r] = true
			}
		}

		for r := range req {
			if !skip[r] {
				reach = append(reach, r)
			}
		}
	}

	sort.Strings(reach)
	return reach
}

// DetectProjectGOPATH attempt to find the GOPATH containing the project.
//
//  If p.AbsRoot is not a symlink and is within a GOPATH, the GOPATH containing p.AbsRoot is returned.
//  If p.AbsRoot is a symlink and is not within any known GOPATH, the GOPATH containing p.ResolvedAbsRoot is returned.
//
// p.AbsRoot is assumed to be a symlink if it is not the same as p.ResolvedAbsRoot.
//
// DetectProjectGOPATH will return an error in the following cases:
//
//  If p.AbsRoot is not a symlink and is not within any known GOPATH.
//  If neither p.AbsRoot nor p.ResolvedAbsRoot are within a known GOPATH.
//  If both p.AbsRoot and p.ResolvedAbsRoot are within the same GOPATH.
//  If p.AbsRoot and p.ResolvedAbsRoot are each within a different GOPATH.
func (c *Ctx) DetectProjectGOPATH(p *Project) (string, error) {
	if p.AbsRoot == "" || p.ResolvedAbsRoot == "" {
		return "", errors.New("project AbsRoot and ResolvedAbsRoot must be set to detect GOPATH")
	}

	if c.ExplicitRoot != "" {
		// If an explicit root is set, just use the first GOPATH in the list.
		return c.GOPATHs[0], nil
	}

	pGOPATH, perr := c.detectGOPATH(p.AbsRoot)

	// If p.AbsRoot is a not a symlink, attempt to detect GOPATH for p.AbsRoot only.
	if equal, _ := fs.EquivalentPaths(p.AbsRoot, p.ResolvedAbsRoot); equal {
		return pGOPATH, perr
	}

	rGOPATH, rerr := c.detectGOPATH(p.ResolvedAbsRoot)

	// If detectGOPATH() failed for both p.AbsRoot and p.ResolvedAbsRoot, then both are not within any known GOPATHs.
	if perr != nil && rerr != nil {
		return "", errors.Errorf("both %s and %s are not within any known GOPATH", p.AbsRoot, p.ResolvedAbsRoot)
	}

	// If pGOPATH equals rGOPATH, then both are within the same GOPATH.
	if equal, _ := fs.EquivalentPaths(pGOPATH, rGOPATH); equal {
		return "", errors.Errorf("both %s and %s are in the same GOPATH %s", p.AbsRoot, p.ResolvedAbsRoot, pGOPATH)
	}

	if pGOPATH != "" && rGOPATH != "" {
		return "", errors.Errorf("%s and %s are both in different GOPATHs", p.AbsRoot, p.ResolvedAbsRoot)
	}

	// Otherwise, either the p.AbsRoot or p.ResolvedAbsRoot is within a GOPATH.
	if pGOPATH == "" {
		return rGOPATH, nil
	}

	return pGOPATH, nil
}

// detectGOPATH detects the GOPATH for a given path from ctx.GOPATHs.
func (c *Ctx) detectGOPATH(path string) (string, error) {
	for _, gp := range c.GOPATHs {
		isPrefix, err := fs.HasFilepathPrefix(path, gp)
		if err != nil {
			return "", errors.Wrap(err, "failed to detect GOPATH")
		}
		if isPrefix {
			return filepath.Clean(gp), nil
		}
	}
	return "", errors.Errorf("%s is not within a known GOPATH/src", path)
}

// ImportForAbs returns the import path for an absolute project path by trimming the
// `$GOPATH/src/` prefix.  Returns an error for paths equal to, or without this prefix.
func (c *Ctx) ImportForAbs(path string) (string, error) {
	srcprefix := filepath.Join(c.GOPATH, "src") + string(filepath.Separator)
	isPrefix, err := fs.HasFilepathPrefix(path, srcprefix)
	if err != nil {
		return "", errors.Wrap(err, "failed to find import path")
	}
	if isPrefix {
		if len(path) <= len(srcprefix) {
			return "", errors.New("dep does not currently support using GOPATH/src as the project root")
		}

		// filepath.ToSlash because we're dealing with an import path now,
		// not an fs path
		return filepath.ToSlash(path[len(srcprefix):]), nil
	}

	return "", errors.Errorf("%s is not within any GOPATH/src", path)
}

// AbsForImport returns the absolute path for the project root
// including the $GOPATH. This will not work with stdlib packages and the
// package directory needs to exist.
func (c *Ctx) AbsForImport(path string) (string, error) {
	posspath := filepath.Join(c.GOPATH, "src", path)
	dirOK, err := fs.IsDir(posspath)
	if err != nil {
		return "", errors.Wrapf(err, "checking if %s is a directory", posspath)
	}
	if !dirOK {
		return "", errors.Errorf("%s does not exist", posspath)
	}
	return posspath, nil
}

// ValidateParams ensure that solving can be completed with the specified params.
func (c *Ctx) ValidateParams(sm gps.SourceManager, params gps.SolveParameters) error {
	err := gps.ValidateParams(params, sm)
	if err != nil {
		if deduceErrs, ok := err.(gps.DeductionErrs); ok {
			c.Err.Println("The following errors occurred while deducing packages:")
			for ip, dErr := range deduceErrs {
				c.Err.Printf("  * \"%s\": %s", ip, dErr)
			}
			c.Err.Println()
		}
	}

	return errors.Wrap(err, "validateParams")
}
