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. Set
// PkgNode.InGraph to include or exclude packages from processing.
type DependencyGraph struct {
	graphMap map[string]*PkgNode
	root     *PkgNode
}

// DependencyGraph resolves dependencies for the provided package names and
// returns a DependencyGraph containing all packages needed to build them.
func (rm RepoMap) DependencyGraph(pkgNames []string) DependencyGraph {
	dg := DependencyGraph{
		graphMap: make(map[string]*PkgNode, len(pkgNames)),
		root:     &PkgNode{},
	}
	rm.IterOver(pkgNames, func(pkg *Package) {
		pkgNode := dg.graphMap[pkg.Name]
		if pkgNode == nil {
			pkgNode = &PkgNode{Package: pkg}
			dg.graphMap[pkg.Name] = pkgNode
		}
		pkgNode.resolveDep(rm, dg)
	})
	return dg
}

// NamedPackageGraph returns a graph containing only the named packages, without
// resolving dependencies. The named packages are marked as in-graph.
// It panics if a package is not found in the repository map.
func (rm RepoMap) NamedPackageGraph(pkgNames []string) DependencyGraph {
	dg := DependencyGraph{
		graphMap: make(map[string]*PkgNode, len(pkgNames)),
		root:     &PkgNode{},
	}
	rm.IterOver(pkgNames, func(pkg *Package) {
		pkgNode := &PkgNode{Package: pkg, InGraph: true}
		dg.graphMap[pkg.Name] = pkgNode
		dg.root.AddDependent(pkgNode)
	})
	return dg
}

// ProcessTopological traverses the dependency graph in topological order and
// applies the function `f` to each package for which InGraph is true. The
// topological order ensures that a package is processed only after all of its
// dependencies have been processed.
func (dg DependencyGraph) ProcessTopological(f func(*PkgNode)) {
	var wg sync.WaitGroup
	wg.Add(1)
	dg.root.recTopo(f, &wg)
	wg.Wait()
}
func (pkg *PkgNode) recTopo(f func(*PkgNode), wg *sync.WaitGroup) {
	defer wg.Done()
	pkg.idep = 0
	if pkg.InGraph {
		f(pkg)
	}
	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 for which InGraph is true. The order of
// processing is not guaranteed.
func (dg DependencyGraph) ProcessUnordered(f func(*PkgNode)) {
	for _, pkg := range dg.graphMap {
		if pkg.InGraph {
			f(pkg)
		}
	}
}

// GetPkgNode returns the PkgNode for the given package name and true if it
// exists in the graph, or nil and false otherwise. PkgNode.InGraph is not
// checked.
func (dg DependencyGraph) GetPkgNode(pkgName string) (*PkgNode, bool) {
	pkgNode, ok := dg.graphMap[pkgName]
	return pkgNode, ok
}

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

// Remove sets InGraph to false for the given package names and returns the names
// of the packages that were removed. It does not remove the packages from the
// graph, so they can be re-added later if needed.
func (dg DependencyGraph) Remove(pkgNames []string) (removed []string) {
	for _, name := range pkgNames {
		if p, ok := dg.graphMap[name]; ok && p.InGraph {
			p.InGraph = false
			removed = append(removed, name)
		}
	}
	return
}

// FilterInstalledPkgs sets InGraph to false for packages that are already
// installed and up to date. docDir should be /usr/doc on a standard Slackware
// system, and kernelVersion should be the output of KernelVersion().
func (dg DependencyGraph) FilterInstalledPkgs(docDir, kernelVersion string) {
	Parallel(dg.ProcessUnordered)(func(pkg *PkgNode) {
		pkg.InGraph = !pkg.IsUpToDate(docDir, kernelVersion)
	})
}
