package dependency

import (
	"container/list"
	"errors"
	"runtime"
	"sort"

	//"go/build"
	"os"
	"path/filepath"
	"strings"

	"github.com/Masterminds/glide/cfg"
	"github.com/Masterminds/glide/msg"
	gpath "github.com/Masterminds/glide/path"
	"github.com/Masterminds/glide/util"
)

// MissingPackageHandler handles the case where a package is missing during scanning.
//
// It returns true if the package can be passed to the resolver, false otherwise.
// False may be returned even if error is nil.
type MissingPackageHandler interface {
	// NotFound is called when the Resolver fails to find a package with the given name.
	//
	// NotFound returns true when the resolver should attempt to re-resole the
	// dependency (e.g. when NotFound has gone and fetched the missing package).
	//
	// When NotFound returns false, the Resolver does not try to do any additional
	// work on the missing package.
	//
	// NotFound only returns errors when it fails to perform its internal goals.
	// When it returns false with no error, this indicates that the handler did
	// its job, but the resolver should not do any additional work on the
	// package.
	NotFound(pkg string, addTest bool) (bool, error)

	// OnGopath is called when the Resolver finds a dependency, but it's only on GOPATH.
	//
	// OnGopath provides an opportunity to copy, move, warn, or ignore cases like this.
	//
	// OnGopath returns true when the resolver should attempt to re-resolve the
	// dependency (e.g. when the dependency is copied to a new location).
	//
	// When OnGopath returns false, the Resolver does not try to do any additional
	// work on the package.
	//
	// An error indicates that OnGopath cannot complete its intended operation.
	// Not all false results are errors.
	OnGopath(pkg string, addTest bool) (bool, error)

	// InVendor is called when the Resolver finds a dependency in the vendor/ directory.
	//
	// This can be used update a project found in the vendor/ folder.
	InVendor(pkg string, addTest bool) error

	// PkgPath is called to find the location locally to scan. This gives the
	// handler to do things such as use a cached location.
	PkgPath(pkg string) string
}

// DefaultMissingPackageHandler is the default handler for missing packages.
//
// When asked to handle a missing package, it will report the miss as a warning,
// and then store the package in the Missing slice for later access.
type DefaultMissingPackageHandler struct {
	Missing []string
	Gopath  []string
	Prefix  string
}

// NotFound prints a warning and then stores the package name in Missing.
//
// It never returns an error, and it always returns false.
func (d *DefaultMissingPackageHandler) NotFound(pkg string, addTest bool) (bool, error) {
	msg.Warn("Package %s is not installed", pkg)
	d.Missing = append(d.Missing, pkg)
	return false, nil
}

// OnGopath is run when a package is missing from vendor/ but found in the GOPATH
func (d *DefaultMissingPackageHandler) OnGopath(pkg string, addTest bool) (bool, error) {
	msg.Warn("Package %s is only on GOPATH.", pkg)
	d.Gopath = append(d.Gopath, pkg)
	return false, nil
}

// InVendor is run when a package is found in the vendor/ folder
func (d *DefaultMissingPackageHandler) InVendor(pkg string, addTest bool) error {
	msg.Info("Package %s found in vendor/ folder", pkg)
	return nil
}

// PkgPath returns the path to the package
func (d *DefaultMissingPackageHandler) PkgPath(pkg string) string {
	if d.Prefix != "" {
		return filepath.Join(d.Prefix, pkg)
	}
	return pkg
}

// VersionHandler sets the version for a package when found while scanning.
//
// When a package if found it needs to be on the correct version before
// scanning its contents to be sure to pick up the right elements for that
// version.
type VersionHandler interface {

	// Process provides an opportunity to process the codebase for version setting.
	Process(pkg string) error

	// SetVersion sets the version for a package. An error is returned if there
	// was a problem setting the version.
	SetVersion(pkg string, testDep bool) error
}

// DefaultVersionHandler is the default handler for setting the version.
//
// The default handler leaves the current version and skips setting a version.
// For a handler that alters the version see the handler included in the repo
// package as part of the installer.
type DefaultVersionHandler struct{}

// Process a package to aide in version setting.
func (d *DefaultVersionHandler) Process(pkg string) error {
	return nil
}

// SetVersion here sends a message when a package is found noting that it
// did not set the version.
func (d *DefaultVersionHandler) SetVersion(pkg string, testDep bool) error {
	msg.Warn("Version not set for package %s", pkg)
	return nil
}

// Resolver resolves a dependency tree.
//
// It operates in two modes:
// - local resolution (ResolveLocal) determines the dependencies of the local project.
// - vendor resolving (Resolve, ResolveAll) determines the dependencies of vendored
//   projects.
//
// Local resolution is for guessing initial dependencies. Vendor resolution is
// for determining vendored dependencies.
type Resolver struct {
	Handler        MissingPackageHandler
	VersionHandler VersionHandler
	VendorDir      string
	BuildContext   *util.BuildCtxt
	Config         *cfg.Config

	// ResolveAllFiles toggles deep scanning.
	// If this is true, resolve by scanning all files, not by walking the
	// import tree.
	ResolveAllFiles bool

	// ResolveTest sets if test dependencies should be resolved.
	ResolveTest bool

	// Items already in the queue.
	alreadyQ map[string]bool

	// Attempts to scan that had unrecoverable error.
	hadError map[string]bool

	basedir string
	seen    map[string]bool

	// findCache caches hits from Find. This reduces the number of filesystem
	// touches that have to be done for dependency resolution.
	findCache map[string]*PkgInfo
}

// NewResolver returns a new Resolver initialized with the DefaultMissingPackageHandler.
//
// This will return an error if the given path does not meet the basic criteria
// for a Go source project. For example, basedir must have a vendor subdirectory.
//
// The BuildContext uses the "go/build".Default to resolve dependencies.
func NewResolver(basedir string) (*Resolver, error) {

	var err error
	basedir, err = filepath.Abs(basedir)
	if err != nil {
		return nil, err
	}

	basedir, err = checkForBasedirSymlink(basedir)

	if err != nil {
		return nil, err
	}

	vdir := filepath.Join(basedir, "vendor")

	buildContext, err := util.GetBuildContext()
	if err != nil {
		return nil, err
	}

	r := &Resolver{
		Handler:        &DefaultMissingPackageHandler{Missing: []string{}, Gopath: []string{}},
		VersionHandler: &DefaultVersionHandler{},
		basedir:        basedir,
		VendorDir:      vdir,
		BuildContext:   buildContext,
		seen:           map[string]bool{},
		alreadyQ:       map[string]bool{},
		hadError:       map[string]bool{},
		findCache:      map[string]*PkgInfo{},

		// The config instance here should really be replaced with a real one.
		Config: &cfg.Config{},
	}

	// TODO: Make sure the build context is correctly set up. Especially in
	// regards to GOROOT, which is not always set.

	return r, nil
}

// Resolve takes a package name and returns all of the imported package names.
//
// If a package is not found, this calls the Fetcher. If the Fetcher returns
// true, it will re-try traversing that package for dependencies. Otherwise it
// will add that package to the deps array and continue on without trying it.
// And if the Fetcher returns an error, this will stop resolution and return
// the error.
//
// If basepath is set to $GOPATH, this will start from that package's root there.
// If basepath is set to a project's vendor path, the scanning will begin from
// there.
func (r *Resolver) Resolve(pkg, basepath string) ([]string, error) {
	target := filepath.Join(basepath, filepath.FromSlash(pkg))
	//msg.Debug("Scanning %s", target)
	l := list.New()
	l.PushBack(target)

	// In this mode, walk the entire tree.
	if r.ResolveAllFiles {
		return r.resolveList(l, false, false)
	}
	return r.resolveImports(l, false, false)
}

// dirHasPrefix tests whether the directory dir begins with prefix.
func dirHasPrefix(dir, prefix string) bool {
	if runtime.GOOS != "windows" {
		return strings.HasPrefix(dir, prefix)
	}
	return len(dir) >= len(prefix) && strings.EqualFold(dir[:len(prefix)], prefix)
}

// ResolveLocal resolves dependencies for the current project.
//
// This begins with the project, builds up a list of external dependencies.
//
// If the deep flag is set to true, this will then resolve all of the dependencies
// of the dependencies it has found. If not, it will return just the packages that
// the base project relies upon.
func (r *Resolver) ResolveLocal(deep bool) ([]string, []string, error) {
	// We build a list of local source to walk, then send this list
	// to resolveList.
	msg.Debug("Resolving local dependencies")
	l := list.New()
	tl := list.New()
	alreadySeen := map[string]bool{}
	talreadySeen := map[string]bool{}
	err := filepath.Walk(r.basedir, func(path string, fi os.FileInfo, err error) error {
		if err != nil && err != filepath.SkipDir {
			return err
		}
		pt := strings.TrimPrefix(path, r.basedir+string(os.PathSeparator))
		pt = strings.TrimSuffix(pt, string(os.PathSeparator))
		if r.Config.HasExclude(pt) {
			msg.Debug("Excluding %s", pt)
			return filepath.SkipDir
		}
		if !fi.IsDir() {
			return nil
		}
		if !srcDir(fi) {
			return filepath.SkipDir
		}

		// Scan for dependencies, and anything that's not part of the local
		// package gets added to the scan list.
		var imps []string
		var testImps []string
		p, err := r.BuildContext.ImportDir(path, 0)
		if err != nil {
			if strings.HasPrefix(err.Error(), "no buildable Go source") {
				return nil
			} else if strings.HasPrefix(err.Error(), "found packages ") {
				// If we got here it's because a package and multiple packages
				// declared. This is often because of an example with a package
				// or main but +build ignore as a build tag. In that case we
				// try to brute force the packages with a slower scan.
				imps, testImps, err = IterativeScan(path)
				if err != nil {
					return err
				}
			} else {
				return err
			}
		} else {
			imps = p.Imports
			testImps = dedupeStrings(p.TestImports, p.XTestImports)
		}

		// We are only looking for dependencies in vendor. No root, cgo, etc.
		for _, imp := range imps {
			if r.Config.HasIgnore(imp) {
				continue
			}
			if alreadySeen[imp] {
				continue
			}
			alreadySeen[imp] = true
			info := r.FindPkg(imp)
			switch info.Loc {
			case LocUnknown, LocVendor:
				l.PushBack(filepath.Join(r.VendorDir, filepath.FromSlash(imp))) // Do we need a path on this?
			case LocGopath:
				if !dirHasPrefix(info.Path, r.basedir) {
					// FIXME: This is a package outside of the project we're
					// scanning. It should really be on vendor. But we don't
					// want it to reference GOPATH. We want it to be detected
					// and moved.
					l.PushBack(filepath.Join(r.VendorDir, filepath.FromSlash(imp)))
				}
			case LocRelative:
				if strings.HasPrefix(imp, "./"+gpath.VendorDir) {
					msg.Warn("Go package resolving will resolve %s without the ./%s/ prefix", imp, gpath.VendorDir)
				}
			}
		}

		if r.ResolveTest {
			for _, imp := range testImps {
				if talreadySeen[imp] {
					continue
				}
				talreadySeen[imp] = true
				info := r.FindPkg(imp)
				switch info.Loc {
				case LocUnknown, LocVendor:
					tl.PushBack(filepath.Join(r.VendorDir, filepath.FromSlash(imp))) // Do we need a path on this?
				case LocGopath:
					if !dirHasPrefix(info.Path, r.basedir) {
						// FIXME: This is a package outside of the project we're
						// scanning. It should really be on vendor. But we don't
						// want it to reference GOPATH. We want it to be detected
						// and moved.
						tl.PushBack(filepath.Join(r.VendorDir, filepath.FromSlash(imp)))
					}
				case LocRelative:
					if strings.HasPrefix(imp, "./"+gpath.VendorDir) {
						msg.Warn("Go package resolving will resolve %s without the ./%s/ prefix", imp, gpath.VendorDir)
					}
				}
			}
		}

		return nil
	})

	if err != nil {
		msg.Err("Failed to build an initial list of packages to scan: %s", err)
		return []string{}, []string{}, err
	}

	if deep {
		if r.ResolveAllFiles {
			re, err := r.resolveList(l, false, false)
			if err != nil {
				return []string{}, []string{}, err
			}
			tre, err := r.resolveList(l, false, true)
			return re, tre, err
		}
		re, err := r.resolveImports(l, false, false)
		if err != nil {
			return []string{}, []string{}, err
		}
		tre, err := r.resolveImports(tl, true, true)
		return re, tre, err
	}

	// If we're not doing a deep scan, we just convert the list into an
	// array and return.
	res := make([]string, 0, l.Len())
	for e := l.Front(); e != nil; e = e.Next() {
		res = append(res, e.Value.(string))
	}
	tres := make([]string, 0, l.Len())
	if r.ResolveTest {
		for e := tl.Front(); e != nil; e = e.Next() {
			tres = append(tres, e.Value.(string))
		}
	}

	return res, tres, nil
}

// ResolveAll takes a list of packages and returns an inclusive list of all
// vendored dependencies.
//
// While this will scan all of the source code it can find, it will only return
// packages that were either explicitly passed in as deps, or were explicitly
// imported by the code.
//
// Packages that are either CGO or on GOROOT are ignored. Packages that are
// on GOPATH, but not vendored currently generate a warning.
//
// If one of the passed in packages does not exist in the vendor directory,
// an error is returned.
func (r *Resolver) ResolveAll(deps []*cfg.Dependency, addTest bool) ([]string, error) {

	queue := sliceToQueue(deps, r.VendorDir)

	if r.ResolveAllFiles {
		return r.resolveList(queue, false, addTest)
	}
	return r.resolveImports(queue, false, addTest)
}

// Stripv strips the vendor/ prefix from vendored packages.
func (r *Resolver) Stripv(str string) string {
	return strings.TrimPrefix(str, r.VendorDir+string(os.PathSeparator))
}

// vpath adds an absolute vendor path.
func (r *Resolver) vpath(str string) string {
	return filepath.Join(r.basedir, "vendor", str)
}

// resolveImports takes a list of existing packages and resolves their imports.
//
// It returns a list of all of the packages that it can determine are required
// for the given code to function.
//
// The expectation is that each item in the queue is an absolute path to a
// vendored package. This attempts to read that package, and then find
// its referenced packages. Those packages are then added to the list
// to be scanned next.
//
// The resolver's handler is used in the cases where a package cannot be
// located.
//
// testDeps specifies if the test dependencies should be resolved and addTest
// specifies if the dependencies should be added to the Config.DevImports. This
// is important because we may resolve normal dependencies of test deps and add
// them to the DevImports list.
func (r *Resolver) resolveImports(queue *list.List, testDeps, addTest bool) ([]string, error) {
	msg.Debug("Resolving import path")

	// When test deps passed in but not resolving return empty.
	if (testDeps || addTest) && !r.ResolveTest {
		return []string{}, nil
	}

	alreadySeen := make(map[string]bool, queue.Len())

	for e := queue.Front(); e != nil; e = e.Next() {
		vdep := e.Value.(string)
		dep := r.Stripv(vdep)
		// Check if marked in the Q and then explicitly mark it. We want to know
		// if it had previously been marked and ensure it for the future.

		if alreadySeen[dep] {
			continue
		}
		alreadySeen[dep] = true

		_, foundQ := r.alreadyQ[dep]
		r.alreadyQ[dep] = true

		// If we've already encountered an error processing this dependency
		// skip it.
		_, foundErr := r.hadError[dep]
		if foundErr {
			continue
		}

		// Skip ignored packages
		if r.Config.HasIgnore(dep) {
			msg.Debug("Ignoring: %s", dep)
			continue
		}
		r.VersionHandler.Process(dep)
		// Here, we want to import the package and see what imports it has.
		msg.Debug("Trying to open %s (%s)", dep, r.Handler.PkgPath(dep))
		var imps []string
		pkg, err := r.BuildContext.ImportDir(r.Handler.PkgPath(dep), 0)
		if err != nil && strings.HasPrefix(err.Error(), "found packages ") {
			// If we got here it's because a package and multiple packages
			// declared. This is often because of an example with a package
			// or main but +build ignore as a build tag. In that case we
			// try to brute force the packages with a slower scan.
			msg.Debug("Using Iterative Scanning for %s", dep)
			if testDeps {
				_, imps, err = IterativeScan(r.Handler.PkgPath(dep))
			} else {
				imps, _, err = IterativeScan(r.Handler.PkgPath(dep))
			}

			if err != nil {
				msg.Err("Iterative scanning error %s: %s", dep, err)
				continue
			}
		} else if err != nil {
			errStr := err.Error()
			msg.Debug("ImportDir error on %s: %s", r.Handler.PkgPath(dep), err)
			if strings.HasPrefix(errStr, "no buildable Go source") {
				msg.Debug("No subpackages declared. Skipping %s.", dep)
				continue
			} else if osDirNotFound(err, r.Handler.PkgPath(dep)) && !foundErr && !foundQ {
				// If the location doesn't exist, there hasn't already been an
				// error, it's not already been in the Q then try to fetch it.
				// When there's an error or it's already in the Q (it should be
				// fetched if it's marked in r.alreadyQ) we skip to make sure
				// not to get stuck in a recursion.

				// If the location doesn't exist try to fetch it.
				if ok, err2 := r.Handler.NotFound(dep, addTest); ok {
					r.alreadyQ[dep] = true
					alreadySeen[dep] = false

					// By adding to the queue it will get reprocessed now that
					// it exists.
					queue.PushBack(r.vpath(dep))
					r.VersionHandler.SetVersion(dep, addTest)
				} else if err2 != nil {
					r.hadError[dep] = true
					msg.Err("Error looking for %s: %s", dep, err2)
				} else {
					r.hadError[dep] = true
					// TODO (mpb): Should we toss this into a Handler to
					// see if this is on GOPATH and copy it?
					msg.Info("Not found in vendor/: %s (1)", dep)
				}
			} else if strings.Contains(errStr, "no such file or directory") {
				r.hadError[dep] = true
				msg.Err("Error scanning %s: %s", dep, err)
				msg.Err("This error means the referenced package was not found.")
				msg.Err("Missing file or directory errors usually occur when multiple packages")
				msg.Err("share a common dependency and the first reference encountered by the scanner")
				msg.Err("sets the version to one that does not contain a subpackage needed required")
				msg.Err("by another package that uses the shared dependency. Try setting a")
				msg.Err("version in your glide.yaml that works for all packages that share this")
				msg.Err("dependency.")
			} else {
				r.hadError[dep] = true
				msg.Err("Error scanning %s: %s", dep, err)
			}
			continue
		} else {
			if testDeps {
				imps = dedupeStrings(pkg.TestImports, pkg.XTestImports)
			} else {
				imps = pkg.Imports
			}

		}

		// Range over all of the identified imports and see which ones we
		// can locate.
		for _, imp := range imps {
			if r.Config.HasIgnore(imp) {
				msg.Debug("Ignoring: %s", imp)
				continue
			}
			pi := r.FindPkg(imp)
			if pi.Loc != LocCgo && pi.Loc != LocGoroot && pi.Loc != LocAppengine {
				msg.Debug("Package %s imports %s", dep, imp)
			}
			switch pi.Loc {
			case LocVendor:
				msg.Debug("In vendor: %s", imp)
				if _, ok := r.alreadyQ[imp]; !ok {
					msg.Debug("Marking %s to be scanned.", imp)
					r.alreadyQ[imp] = true
					queue.PushBack(r.vpath(imp))
					if err := r.Handler.InVendor(imp, addTest); err == nil {
						r.VersionHandler.SetVersion(imp, addTest)
					} else {
						msg.Warn("Error updating %s: %s", imp, err)
					}
				}
			case LocUnknown:
				msg.Debug("Missing %s. Trying to resolve.", imp)
				if ok, err := r.Handler.NotFound(imp, addTest); ok {
					r.alreadyQ[imp] = true
					queue.PushBack(r.vpath(imp))
					r.VersionHandler.SetVersion(imp, addTest)
				} else if err != nil {
					r.hadError[imp] = true
					msg.Err("Error looking for %s: %s", imp, err)
				} else {
					r.hadError[imp] = true
					msg.Err("Not found: %s (2)", imp)
				}
			case LocGopath:
				msg.Debug("Found on GOPATH, not vendor: %s", imp)
				if _, ok := r.alreadyQ[imp]; !ok {
					// Only scan it if it gets moved into vendor/
					if ok, _ := r.Handler.OnGopath(imp, addTest); ok {
						r.alreadyQ[imp] = true
						queue.PushBack(r.vpath(imp))
						r.VersionHandler.SetVersion(imp, addTest)
					}
				}
			}
		}

	}

	if len(r.hadError) > 0 {
		// Errors occurred so we return.
		return []string{}, errors.New("Error resolving imports")
	}

	// FIXME: From here to the end is a straight copy of the resolveList() func.
	res := make([]string, 0, queue.Len())

	// In addition to generating a list
	for e := queue.Front(); e != nil; e = e.Next() {
		t := r.Stripv(e.Value.(string))
		root, sp := util.NormalizeName(t)

		if root == r.Config.Name {
			continue
		}

		// Skip ignored packages
		if r.Config.HasIgnore(e.Value.(string)) {
			msg.Debug("Ignoring: %s", e.Value.(string))
			continue
		}

		// TODO(mattfarina): Need to eventually support devImport
		existing := r.Config.Imports.Get(root)
		if existing == nil && addTest {
			existing = r.Config.DevImports.Get(root)
		}
		if existing != nil {
			if sp != "" && !existing.HasSubpackage(sp) {
				existing.Subpackages = append(existing.Subpackages, sp)
			}
		} else {
			newDep := &cfg.Dependency{
				Name: root,
			}
			if sp != "" {
				newDep.Subpackages = []string{sp}
			}

			if addTest {
				r.Config.DevImports = append(r.Config.DevImports, newDep)
			} else {
				r.Config.Imports = append(r.Config.Imports, newDep)
			}
		}
		res = append(res, t)
	}

	return res, nil
}

// resolveList takes a list and resolves it.
//
// This walks the entire file tree for the given dependencies, not just the
// parts that are imported directly. Using this will discover dependencies
// regardless of OS, and arch.
func (r *Resolver) resolveList(queue *list.List, testDeps, addTest bool) ([]string, error) {
	// When test deps passed in but not resolving return empty.
	if testDeps && !r.ResolveTest {
		return []string{}, nil
	}

	var failedDep string
	var failedDepPath string
	var pkgPath string
	for e := queue.Front(); e != nil; e = e.Next() {
		dep := e.Value.(string)
		t := strings.TrimPrefix(dep, r.VendorDir+string(os.PathSeparator))
		if r.Config.HasIgnore(t) {
			msg.Debug("Ignoring: %s", t)
			continue
		}
		r.VersionHandler.Process(t)
		//msg.Warn("#### %s ####", dep)
		//msg.Info("Seen Count: %d", len(r.seen))
		// Catch the outtermost dependency.
		pkgPath = r.Handler.PkgPath(t)
		failedDep = t
		failedDepPath = pkgPath
		err := filepath.Walk(pkgPath, func(path string, fi os.FileInfo, err error) error {
			if err != nil && err != filepath.SkipDir {
				return err
			}

			// Skip files.
			if !fi.IsDir() {
				return nil
			}
			// Skip dirs that are not source.
			if !srcDir(fi) {
				//msg.Debug("Skip resource %s", fi.Name())
				return filepath.SkipDir
			}

			// Anything that comes through here has already been through
			// the queue.
			r.alreadyQ[path] = true
			e := r.queueUnseen(path, queue, testDeps, addTest)
			if e != nil {
				failedDepPath = path
				//msg.Err("Failed to fetch dependency %s: %s", path, err)
			}
			return e
		})
		if err != nil && err != filepath.SkipDir {
			msg.Err("Dependency %s (%s) failed to resolve: %s.", failedDep, failedDepPath, err)
			return []string{}, err
		}
	}

	res := make([]string, 0, queue.Len())

	// In addition to generating a list
	for e := queue.Front(); e != nil; e = e.Next() {
		t := strings.TrimPrefix(e.Value.(string), r.VendorDir+string(os.PathSeparator))
		root, sp := util.NormalizeName(t)

		if root == r.Config.Name {
			continue
		}

		existing := r.Config.Imports.Get(root)
		if existing == nil && addTest {
			existing = r.Config.DevImports.Get(root)
		}

		if existing != nil {
			if sp != "" && !existing.HasSubpackage(sp) {
				existing.Subpackages = append(existing.Subpackages, sp)
			}
		} else {
			newDep := &cfg.Dependency{
				Name: root,
			}
			if sp != "" {
				newDep.Subpackages = []string{sp}
			}

			if addTest {
				r.Config.DevImports = append(r.Config.DevImports, newDep)
			} else {
				r.Config.Imports = append(r.Config.Imports, newDep)
			}
		}
		res = append(res, e.Value.(string))
	}

	return res, nil
}

// queueUnseenImports scans a package's imports and adds any new ones to the
// processing queue.
func (r *Resolver) queueUnseen(pkg string, queue *list.List, testDeps, addTest bool) error {
	// A pkg is marked "seen" as soon as we have inspected it the first time.
	// Seen means that we have added all of its imports to the list.

	// Already queued indicates that we've either already put it into the queue
	// or intentionally not put it in the queue for fatal reasons (e.g. no
	// buildable source).

	deps, err := r.imports(pkg, testDeps, addTest)
	if err != nil && !strings.HasPrefix(err.Error(), "no buildable Go source") {
		msg.Err("Could not find %s: %s", pkg, err)
		return err
		// NOTE: If we uncomment this, we get lots of "no buildable Go source" errors,
		// which don't ever seem to be helpful. They don't actually indicate an error
		// condition, and it's perfectly okay to run into that condition.
		//} else if err != nil {
		//	msg.Warn(err.Error())
	}

	for _, d := range deps {
		if _, ok := r.alreadyQ[d]; !ok {
			r.alreadyQ[d] = true
			queue.PushBack(d)
		}
	}
	return nil
}

// imports gets all of the imports for a given package.
//
// If the package is in GOROOT, this will return an empty list (but not
// an error).
// If it cannot resolve the pkg, it will return an error.
func (r *Resolver) imports(pkg string, testDeps, addTest bool) ([]string, error) {

	if r.Config.HasIgnore(pkg) {
		msg.Debug("Ignoring %s", pkg)
		return []string{}, nil
	}

	// If this pkg is marked seen, we don't scan it again.
	if _, ok := r.seen[pkg]; ok {
		msg.Debug("Already saw %s", pkg)
		return []string{}, nil
	}

	// FIXME: On error this should try to NotFound to the dependency, and then import
	// it again.
	var imps []string
	p, err := r.BuildContext.ImportDir(pkg, 0)
	if err != nil && strings.HasPrefix(err.Error(), "found packages ") {
		// If we got here it's because a package and multiple packages
		// declared. This is often because of an example with a package
		// or main but +build ignore as a build tag. In that case we
		// try to brute force the packages with a slower scan.
		if testDeps {
			_, imps, err = IterativeScan(r.Handler.PkgPath(pkg))
		} else {
			imps, _, err = IterativeScan(r.Handler.PkgPath(pkg))
		}

		if err != nil {
			return []string{}, err
		}
	} else if err != nil {
		return []string{}, err
	} else {
		if testDeps {
			imps = dedupeStrings(p.TestImports, p.XTestImports)
		} else {
			imps = p.Imports
		}
	}

	// It is okay to scan a package more than once. In some cases, this is
	// desirable because the package can change between scans (e.g. as a result
	// of a failed scan resolving the situation).
	msg.Debug("=> Scanning %s (%s)", p.ImportPath, pkg)
	r.seen[pkg] = true

	// Optimization: If it's in GOROOT, it has no imports worth scanning.
	if p.Goroot {
		return []string{}, nil
	}

	// We are only looking for dependencies in vendor. No root, cgo, etc.
	buf := []string{}
	for _, imp := range imps {
		if r.Config.HasIgnore(imp) {
			msg.Debug("Ignoring %s", imp)
			continue
		}
		info := r.FindPkg(imp)
		switch info.Loc {
		case LocUnknown:
			// Do we resolve here?
			found, err := r.Handler.NotFound(imp, addTest)
			if err != nil {
				msg.Err("Failed to fetch %s: %s", imp, err)
			}
			if found {
				buf = append(buf, filepath.Join(r.VendorDir, filepath.FromSlash(imp)))
				r.VersionHandler.SetVersion(imp, addTest)
				continue
			}
			r.seen[info.Path] = true
		case LocVendor:
			//msg.Debug("Vendored: %s", imp)
			buf = append(buf, info.Path)
			if err := r.Handler.InVendor(imp, addTest); err == nil {
				r.VersionHandler.SetVersion(imp, addTest)
			} else {
				msg.Warn("Error updating %s: %s", imp, err)
			}
		case LocGopath:
			found, err := r.Handler.OnGopath(imp, addTest)
			if err != nil {
				msg.Err("Failed to fetch %s: %s", imp, err)
			}
			// If the Handler marks this as found, we drop it into the buffer
			// for subsequent processing. Otherwise, we assume that we're
			// in a less-than-perfect, but functional, situation.
			if found {
				buf = append(buf, filepath.Join(r.VendorDir, filepath.FromSlash(imp)))
				r.VersionHandler.SetVersion(imp, addTest)
				continue
			}
			msg.Warn("Package %s is on GOPATH, but not vendored. Ignoring.", imp)
			r.seen[info.Path] = true
		default:
			// Local packages are an odd case. CGO cannot be scanned.
			msg.Debug("===> Skipping %s", imp)
		}
	}

	return buf, nil
}

// sliceToQueue is a special-purpose function for unwrapping a slice of
// dependencies into a queue of fully qualified paths.
func sliceToQueue(deps []*cfg.Dependency, basepath string) *list.List {
	l := list.New()
	for _, e := range deps {
		if len(e.Subpackages) > 0 {
			for _, v := range e.Subpackages {
				ip := e.Name
				if v != "." && v != "" {
					ip = ip + "/" + v
				}
				msg.Debug("Adding local Import %s to queue", ip)
				l.PushBack(filepath.Join(basepath, filepath.FromSlash(ip)))
			}
		} else {
			msg.Debug("Adding local Import %s to queue", e.Name)
			l.PushBack(filepath.Join(basepath, filepath.FromSlash(e.Name)))
		}

	}
	return l
}

// PkgLoc describes the location of the package.
type PkgLoc uint8

const (
	// LocUnknown indicates the package location is unknown (probably not present)
	LocUnknown PkgLoc = iota
	// LocLocal inidcates that the package is in a local dir, not GOPATH or GOROOT.
	LocLocal
	// LocVendor indicates that the package is in a vendor/ dir
	LocVendor
	// LocGopath inidcates that the package is in GOPATH
	LocGopath
	// LocGoroot indicates that the package is in GOROOT
	LocGoroot
	// LocCgo indicates that the package is a a CGO package
	LocCgo
	// LocAppengine indicates the package is part of the appengine SDK. It's a
	// special build mode. https://blog.golang.org/the-app-engine-sdk-and-workspaces-gopath
	// Why does a Google product get a special case build mode with a local
	// package?
	LocAppengine
	// LocRelative indicates the package is a relative directory
	LocRelative
)

// PkgInfo represents metadata about a package found by the resolver.
type PkgInfo struct {
	Name, Path string
	Vendored   bool
	Loc        PkgLoc
}

// PackagesAddedToStdlib is the list of packages added to the go standard lib
// at various points.
var PackagesAddedToStdlib = map[string]struct{}{
	// context and net/http/httptrace are packages being added to
	// the Go 1.7 standard library. Some packages, such as golang.org/x/net
	// are importing it with build flags in files for go1.7.
	"context":            struct{}{},
	"net/http/httptrace": struct{}{},

	// math.bits are packages being added to the Go 1.9 standard library.
	// Some packages, such as github.com/RoaringBitmap/roaring are importing
	// it with build flags in files for go1.9.
	"math/bits": struct{}{},

	// crypto/ed25519 is a package being added to the Go 1.13 standard library.
	// It is importing itself  with build flags in files for go1.13.
	"crypto/ed25519": struct{}{},
}

// FindPkg takes a package name and attempts to find it on the filesystem
//
// The resulting PkgInfo will indicate where it was found.
func (r *Resolver) FindPkg(name string) *PkgInfo {
	// We cachae results for FindPkg to reduce the number of filesystem ops
	// that we have to do. This is a little risky because certain directories,
	// like GOPATH, can be modified while we're running an operation, and
	// render the cache inaccurate.
	//
	// Unfound items (LocUnknown) are never cached because we assume that as
	// part of the response, the Resolver may fetch that dependency.
	if i, ok := r.findCache[name]; ok {
		//msg.Info("Cache hit on %s", name)
		return i
	}

	// 502 individual packages scanned.
	// No cache:
	// glide -y etcd.yaml list  0.27s user 0.19s system 85% cpu 0.534 total
	// With cache:
	// glide -y etcd.yaml list  0.22s user 0.15s system 85% cpu 0.438 total

	var p string
	info := &PkgInfo{
		Name: name,
	}

	if strings.HasPrefix(name, "./") || strings.HasPrefix(name, "../") {
		info.Loc = LocRelative
		r.findCache[name] = info
		return info
	}

	// Check _only_ if this dep is in the current vendor directory.
	p = filepath.Join(r.VendorDir, filepath.FromSlash(name))
	if pkgExists(p) {
		info.Path = p
		info.Loc = LocVendor
		info.Vendored = true
		r.findCache[name] = info
		return info
	}

	// TODO: Do we need this if we always flatten?
	// Recurse backward to scan other vendor/ directories
	//for wd := cwd; wd != "/"; wd = filepath.Dir(wd) {
	//p = filepath.Join(wd, "vendor", filepath.FromSlash(name))
	//if fi, err = os.Stat(p); err == nil && (fi.IsDir() || isLink(fi)) {
	//info.Path = p
	//info.PType = ptypeVendor
	//info.Vendored = true
	//return info
	//}
	//}

	// Check $GOPATH
	for _, rr := range filepath.SplitList(r.BuildContext.GOPATH) {
		p = filepath.Join(rr, "src", filepath.FromSlash(name))
		if pkgExists(p) {
			info.Path = p
			info.Loc = LocGopath
			r.findCache[name] = info
			return info
		}
	}

	// Check $GOROOT
	for _, rr := range filepath.SplitList(r.BuildContext.GOROOT) {
		p = filepath.Join(rr, "src", filepath.FromSlash(name))
		if pkgExists(p) {
			info.Path = p
			info.Loc = LocGoroot
			r.findCache[name] = info
			return info
		}
	}

	// If this is "C", we're dealing with cgo
	if name == "C" {
		info.Loc = LocCgo
		r.findCache[name] = info
	} else if name == "appengine" || name == "appengine_internal" ||
		strings.HasPrefix(name, "appengine/") ||
		strings.HasPrefix(name, "appengine_internal/") {
		// Appengine is a special case when it comes to Go builds. It is a local
		// looking package only available within appengine. It's a special case
		// where Google products are playing with each other.
		// https://blog.golang.org/the-app-engine-sdk-and-workspaces-gopath
		info.Loc = LocAppengine
		r.findCache[name] = info
	} else if _, ok := PackagesAddedToStdlib[name]; ok {
		// Various packages are being added to the Go standard library, and being imported
		// with build flags. Need to detect this and handle it.
		info.Loc = LocGoroot
		r.findCache[name] = info
	}

	return info
}

func pkgExists(path string) bool {
	fi, err := os.Stat(path)
	return err == nil && (fi.IsDir() || isLink(fi))
}

// isLink returns true if the given FileInfo is a symbolic link.
func isLink(fi os.FileInfo) bool {
	return fi.Mode()&os.ModeSymlink == os.ModeSymlink
}

// IsSrcDir returns true if this is a directory that could have source code,
// false otherwise.
//
// Directories with _ or . prefixes are skipped, as are testdata and vendor.
func IsSrcDir(fi os.FileInfo) bool {
	return srcDir(fi)
}

func srcDir(fi os.FileInfo) bool {
	if !fi.IsDir() {
		return false
	}

	// Ignore _foo and .foo
	if strings.HasPrefix(fi.Name(), "_") || strings.HasPrefix(fi.Name(), ".") {
		return false
	}

	// Ignore testdata. For now, ignore vendor.
	if fi.Name() == "testdata" || fi.Name() == "vendor" {
		return false
	}

	return true
}

// checkForBasedirSymlink checks to see if the given basedir is actually a
// symlink. In the case that it is a symlink, the symlink is read and returned.
// If the basedir is not a symlink, the provided basedir argument is simply
// returned back to the caller.
func checkForBasedirSymlink(basedir string) (string, error) {
	fi, err := os.Lstat(basedir)
	if err != nil {
		return "", err
	}

	if fi.Mode()&os.ModeSymlink != 0 {
		return os.Readlink(basedir)
	}

	return basedir, nil
}

// helper func to merge, dedupe, and sort strings
func dedupeStrings(s1, s2 []string) (r []string) {
	dedupe := make(map[string]bool)

	if len(s1) > 0 && len(s2) > 0 {
		for _, i := range s1 {
			dedupe[i] = true
		}
		for _, i := range s2 {
			dedupe[i] = true
		}

		for i := range dedupe {
			r = append(r, i)
		}
		// And then re-sort them
		sort.Strings(r)
	} else if len(s1) > 0 {
		r = s1
	} else if len(s2) > 0 {
		r = s2
	}

	return
}

// In Go 1.9 go/build.ImportDir changed so that a missing dir
// no longer responses with os.IsNotExist. Instead the error changed
// one in the form of fmt.Errorf("cannot find package %q in:\n\t%s", path, p.Dir)
// which is similar to other go/build.ImportDir errors. This function
// attempts to detect when ImportDir thinks something is not found
func osDirNotFound(err error, p string) bool {

	if os.IsNotExist(err) {
		return true
	}

	// Since there are multiple errors that start like this we need to make
	// sure the directory is not present
	if strings.HasPrefix(err.Error(), "cannot find package ") {
		_, nferr := os.Stat(p)
		if os.IsNotExist(nferr) {
			return true
		}
	}

	return false
}
