// Copyright 2024 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 imports

import (
	"context"
	"path/filepath"
	"strings"
	"sync"

	"golang.org/x/sync/errgroup"
	"golang.org/x/tools/internal/gopathwalk"
)

// ProcessEnvSource implements the [Source] interface using the legacy
// [ProcessEnv] abstraction.
type ProcessEnvSource struct {
	env      *ProcessEnv
	srcDir   string
	filename string
	pkgName  string
}

// NewProcessEnvSource returns a [ProcessEnvSource] wrapping the given
// env, to be used for fixing imports in the file with name filename in package
// named pkgName.
func NewProcessEnvSource(env *ProcessEnv, filename, pkgName string) (*ProcessEnvSource, error) {
	abs, err := filepath.Abs(filename)
	if err != nil {
		return nil, err
	}
	srcDir := filepath.Dir(abs)
	return &ProcessEnvSource{
		env:      env,
		srcDir:   srcDir,
		filename: filename,
		pkgName:  pkgName,
	}, nil
}

func (s *ProcessEnvSource) LoadPackageNames(ctx context.Context, srcDir string, unknown []string) (map[string]string, error) {
	r, err := s.env.GetResolver()
	if err != nil {
		return nil, err
	}
	return r.loadPackageNames(unknown, srcDir)
}

func (s *ProcessEnvSource) ResolveReferences(ctx context.Context, filename string, refs map[string]map[string]bool) ([]*Result, error) {
	var mu sync.Mutex
	found := make(map[string][]pkgDistance)
	callback := &scanCallback{
		rootFound: func(gopathwalk.Root) bool {
			return true // We want everything.
		},
		dirFound: func(pkg *pkg) bool {
			return pkgIsCandidate(filename, refs, pkg)
		},
		packageNameLoaded: func(pkg *pkg) bool {
			if _, want := refs[pkg.packageName]; !want {
				return false
			}
			if pkg.dir == s.srcDir && s.pkgName == pkg.packageName {
				// The candidate is in the same directory and has the
				// same package name. Don't try to import ourselves.
				return false
			}
			if !canUse(filename, pkg.dir) {
				return false
			}
			mu.Lock()
			defer mu.Unlock()
			found[pkg.packageName] = append(found[pkg.packageName], pkgDistance{pkg, distance(s.srcDir, pkg.dir)})
			return false // We'll do our own loading after we sort.
		},
	}
	resolver, err := s.env.GetResolver()
	if err != nil {
		return nil, err
	}
	if err := resolver.scan(ctx, callback); err != nil {
		return nil, err
	}

	g, ctx := errgroup.WithContext(ctx)

	searcher := symbolSearcher{
		logf:        s.env.logf,
		srcDir:      s.srcDir,
		xtest:       strings.HasSuffix(s.pkgName, "_test"),
		loadExports: resolver.loadExports,
	}

	var resultMu sync.Mutex
	results := make(map[string]*Result, len(refs))
	for pkgName, symbols := range refs {
		g.Go(func() error {
			found, err := searcher.search(ctx, found[pkgName], pkgName, symbols)
			if err != nil {
				return err
			}
			if found == nil {
				return nil // No matching package.
			}

			imp := &ImportInfo{
				ImportPath: found.importPathShort,
			}
			pkg := &PackageInfo{
				Name:    pkgName,
				Exports: symbols,
			}
			resultMu.Lock()
			results[pkgName] = &Result{Import: imp, Package: pkg}
			resultMu.Unlock()
			return nil
		})
	}
	if err := g.Wait(); err != nil {
		return nil, err
	}
	var ans []*Result
	for _, x := range results {
		ans = append(ans, x)
	}
	return ans, nil
}
