package sborepo

import (
	"sync"
	"sync/atomic"
)

// DependencyGraph represents the dependency graph of SBo packages.
//
// It must be built with `RepoMap.DependencyGraph` or
// `RepoMap.NamedPackageGraph`. It provides methods to process the graph in
// topological order or to process dependents of a package.
type DependencyGraph struct {
	graphMap map[PkgName]*pkgNode
	root     *pkgNode
}

// newDepGraph returns a DependencyGraph using size as the initial size of the
// graphMap.
func newDepGraph(size int) DependencyGraph {
	return DependencyGraph{
		graphMap: make(map[PkgName]*pkgNode, size),
		root:     &pkgNode{},
	}
}

// DependencyGraph resolves dependencies for the provided package names and
// returns a graph containing all packages needed to build the packages found in
// the repository. If any requested package is missing, it returns the partial
// graph together with an error.
//
// It panics if the repository is internally inconsistent, such as when a
// resolved package depends on a package that is missing from the repository.
func (rm RepoMap) DependencyGraph(pkgNames []PkgName) (DependencyGraph, error) {
	dg := newDepGraph(len(pkgNames))
	iter, err := rm.IterOver(pkgNames)
	iter(func(pkg *Package) {
		pNode := dg.graphMap[pkg.Name]
		if pNode == nil {
			pNode = &pkgNode{Package: pkg}
			dg.graphMap[pkg.Name] = pNode
		}
		pNode.resolveDep(rm, dg)
	})
	return dg, err
}

// NamedPackageGraph returns a graph containing only the named packages found
// in the repository, without resolving dependencies. If any requested package
// is missing, it returns the partial graph together with an error.
func (rm RepoMap) NamedPackageGraph(pkgNames []PkgName) (DependencyGraph, error) {
	dg := newDepGraph(len(pkgNames))
	iter, err := rm.IterOver(pkgNames)
	iter(func(pkg *Package) {
		pkgNode := &pkgNode{Package: pkg, InGraph: true}
		dg.graphMap[pkg.Name] = pkgNode
		dg.root.AddDependent(pkgNode)
	})
	return dg, err
}

// ProcessTopological traverses the dependency graph in topological order and
// applies the function `f` to each package in it. The topological order
// ensures that a package is processed only after all of its dependencies have
// been processed.
func (dg DependencyGraph) ProcessTopological(f PkgFunc) {
	var wg sync.WaitGroup
	wg.Add(1)
	dg.root.recTopo(f, &wg)
	wg.Wait()
}
func (pkg *pkgNode) recTopo(f PkgFunc, wg *sync.WaitGroup) {
	defer wg.Done()
	pkg.idep = 0
	if pkg.InGraph {
		f(pkg.Package)
	}
	for _, dependent := range pkg.Dependents() {
		if atomic.AddInt32(&dependent.idep, 1) == dependent.ndep {
			wg.Add(1)
			go dependent.recTopo(f, wg)
		}
	}
}

// ProcessUnordered iterates over all packages in the graph and applies the
// function `f` to each package in it. The order of processing is not
// guaranteed.
func (dg DependencyGraph) ProcessUnordered(f PkgFunc) {
	for _, pkg := range dg.graphMap {
		if pkg.InGraph {
			f(pkg.Package)
		}
	}
}

// IterDependents returns an iterator over rootName and all in-graph packages
// that directly or indirectly depend on it. rootName must exist in the graph.
func (dg DependencyGraph) IterDependents(rootName PkgName) PkgIter {
	return onPackage(dg.graphMap[rootName].ProcessDependents)
}

// IsEmpty returns true if the graph has no in-graph packages.
func (dg DependencyGraph) IsEmpty() bool {
	for _, pkg := range dg.graphMap {
		if pkg.InGraph {
			return false
		}
	}
	return true
}

// Remove excludes the named packages from processing. The packages remain in
// the graph, so dependency and dependent relationships are still available.
func (dg DependencyGraph) Remove(names ...PkgName) {
	for _, name := range names {
		if node, ok := dg.graphMap[name]; ok {
			node.InGraph = false
		}
	}
}

// RemoveDependents excludes rootName and all in-graph packages that directly or
// indirectly depend on it from processing. It returns the names that were
// removed. rootName must exist in the graph.
func (dg DependencyGraph) RemoveDependents(rootName PkgName) (removed []PkgName) {
	dg.graphMap[rootName].ProcessDependents(func(pkg *pkgNode) {
		pkg.InGraph = false
		removed = append(removed, pkg.Name)
	})
	return removed
}

// FilterOut excludes every in-graph package for which predicate returns true.
func (dg DependencyGraph) FilterOut(predicate func(pkg *Package) bool) {
	var wg sync.WaitGroup
	for _, pkg := range dg.graphMap {
		if pkg.InGraph {
			wg.Add(1)
			go func(pkg *pkgNode) {
				pkg.InGraph = !predicate(pkg.Package)
				wg.Done()
			}(pkg)
		}
	}
	wg.Wait()
}
