// Copyright 2018 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 packages

import (
	"cmp"
	"fmt"
	"iter"
	"os"
	"slices"
)

// Visit visits all the packages in the import graph whose roots are
// pkgs, calling the optional pre function the first time each package
// is encountered (preorder), and the optional post function after a
// package's dependencies have been visited (postorder).
// The boolean result of pre(pkg) determines whether
// the imports of package pkg are visited.
//
// Example:
//
//	pkgs, err := Load(...)
//	if err != nil { ... }
//	Visit(pkgs, nil, func(pkg *Package) {
//		log.Println(pkg)
//	})
//
// In most cases, it is more convenient to use [Postorder]:
//
//	for pkg := range Postorder(pkgs) {
//		log.Println(pkg)
//	}
func Visit(pkgs []*Package, pre func(*Package) bool, post func(*Package)) {
	seen := make(map[*Package]bool)
	var visit func(*Package)
	visit = func(pkg *Package) {
		if !seen[pkg] {
			seen[pkg] = true

			if pre == nil || pre(pkg) {
				for _, imp := range sorted(pkg.Imports) { // for determinism
					visit(imp)
				}
			}

			if post != nil {
				post(pkg)
			}
		}
	}
	for _, pkg := range pkgs {
		visit(pkg)
	}
}

// PrintErrors prints to os.Stderr the accumulated errors of all
// packages in the import graph rooted at pkgs, dependencies first.
// PrintErrors returns the number of errors printed.
func PrintErrors(pkgs []*Package) int {
	var n int
	errModules := make(map[*Module]bool)
	for pkg := range Postorder(pkgs) {
		for _, err := range pkg.Errors {
			fmt.Fprintln(os.Stderr, err)
			n++
		}

		// Print pkg.Module.Error once if present.
		mod := pkg.Module
		if mod != nil && mod.Error != nil && !errModules[mod] {
			errModules[mod] = true
			fmt.Fprintln(os.Stderr, mod.Error.Err)
			n++
		}
	}
	return n
}

// Postorder returns an iterator over the the packages in
// the import graph whose roots are pkg.
// Packages are enumerated in dependencies-first order.
func Postorder(pkgs []*Package) iter.Seq[*Package] {
	return func(yield func(*Package) bool) {
		seen := make(map[*Package]bool)
		var visit func(*Package) bool
		visit = func(pkg *Package) bool {
			if !seen[pkg] {
				seen[pkg] = true
				for _, imp := range sorted(pkg.Imports) { // for determinism
					if !visit(imp) {
						return false
					}
				}
				if !yield(pkg) {
					return false
				}
			}
			return true
		}
		for _, pkg := range pkgs {
			if !visit(pkg) {
				break
			}
		}
	}
}

// -- copied from golang.org.x/tools/gopls/internal/util/moremaps --

// sorted returns an iterator over the entries of m in key order.
func sorted[M ~map[K]V, K cmp.Ordered, V any](m M) iter.Seq2[K, V] {
	// TODO(adonovan): use maps.Sorted if proposal #68598 is accepted.
	return func(yield func(K, V) bool) {
		keys := keySlice(m)
		slices.Sort(keys)
		for _, k := range keys {
			if !yield(k, m[k]) {
				break
			}
		}
	}
}

// KeySlice returns the keys of the map M, like slices.Collect(maps.Keys(m)).
func keySlice[M ~map[K]V, K comparable, V any](m M) []K {
	r := make([]K, 0, len(m))
	for k := range m {
		r = append(r, k)
	}
	return r
}
