package sborepo

import "sync"

// PkgNode wraps a Package with dependency-graph state.
//
// Dependents are packages that depend on this node. InGraph marks whether the
// node is part of the current processing graph.
type PkgNode struct {
	*Package
	InGraph    bool
	dependents []*PkgNode // packages that depend on this package
	idep, ndep int32
}

// Dependents returns a slice of packages previously added by AddDependent.
// It can be used by functions which build a dependency graph.
func (pkg *PkgNode) Dependents() []*PkgNode { return pkg.dependents }

// IndirectDependents returns in-graph packages that depend on this package,
// directly or indirectly.
func (pkg *PkgNode) IndirectDependents() (ps []*PkgNode) {
	pkg.ProcessDependents(func(d *PkgNode) { ps = append(ps, d) })
	if len(ps) == 0 {
		return nil
	}
	return ps[1:] // skip self
}

// AddDependent adds a package that depends on this package to the list of
// dependents. This is used to build a dependency graph.
func (pkg *PkgNode) AddDependent(dependent *PkgNode) {
	dependent.ndep++
	pkg.dependents = append(pkg.dependents, dependent)
}

// ProcessDependents iterates over the dependents (previously set by
// AddDependent) of the package and their dependents recursively and applies the
// function `f` to each in-graph dependent. It ensures that every package is only
// visited once. Can be parallelized with Parallel.
func (pkg *PkgNode) ProcessDependents(f func(*PkgNode)) {
	pkg.recUnordered(f, make(map[*PkgNode]bool))
}
func (p *PkgNode) recUnordered(f func(*PkgNode), visited map[*PkgNode]bool) {
	if visited[p] {
		return
	}
	visited[p] = true
	if p.InGraph {
		f(p)
	}
	for _, dependent := range p.Dependents() {
		dependent.recUnordered(f, visited)
	}
}

// PkgNodeIter represents functions that iterate over PkgNodes.
// Such functions can be parallelized with Parallel.
type PkgNodeIter func(func(*PkgNode))

// Parallel wraps `iter` so that it iterates concurrently over the packages. It
// ensures that every package is only visited once, but does not guarantee any
// order of processing.
func Parallel(iter PkgNodeIter) PkgNodeIter {
	return func(f func(*PkgNode)) {
		var wg sync.WaitGroup
		iter(func(pkg *PkgNode) {
			wg.Add(1)
			go func() { defer wg.Done(); f(pkg) }()
		})
		wg.Wait()
	}
}

// resolveDep recursively resolves and adds dependencies of package parent. If
// the package has no dependencies, it is added to dg.root.
func (parent *PkgNode) resolveDep(repository RepoMap, dg DependencyGraph) {
	parent.InGraph = true
	dg.graphMap[parent.Name] = parent

	for _, pkgName := range parent.Dependencies() {
		switch pkgName {
		case "%README%", "":
			continue
		case parent.Name:
			panic("Circular dependency detected: " + parent.Name)
		}
		pkgPtr, ok := dg.graphMap[pkgName]
		if !ok {
			pkgPtr = &PkgNode{Package: repository.GetPkg(pkgName)}
		}
		pkgPtr.AddDependent(parent)
		if !ok {
			pkgPtr.resolveDep(repository, dg)
		}
	}
	if parent.ndep == 0 {
		dg.root.AddDependent(parent)
	}
}
