package sboRepo

import (
	"io/fs"
	"path/filepath"
	"sync"

	"gitlab.com/M0M097/pkg/lib/utils"
)

// RepoMap is a map of package names to their corresponding Package structs.
type RepoMap map[string]*Package

// NewRepoMap creates a new RepoMap from the given repository path.
// RepoPath must be the path to a directory two levels above the packages,
// i.e. if the packages live at /path/to/repo/categories/package_name,
// RepoPath should be /path/to/repo/.
//
// Since this routine is usually the bottleneck, it is parallelized.
func NewRepoMap(RepoPath string) RepoMap {

	var (
		wg   sync.WaitGroup
		mu   sync.Mutex
		repo = make(RepoMap, 10100)
	)

	for _, file := range utils.ReadDir(RepoPath) {
		// We parallelize over the `categories` in the repository
		wg.Add(1)
		go func(file fs.DirEntry) {
			defer wg.Done()

			if file.Name()[0] == '.' || !file.IsDir() {
				return
			}

			subdirPath := filepath.Join(RepoPath, file.Name())
			subfiles := utils.ReadDir(subdirPath)

			// We fill a local slice that we merge into the actual map
			// so that we only need to lock the map once.
			local_repo := make([]Package, 0, len(subfiles))
			for _, subfile := range subfiles {
				pkgName := subfile.Name()
				local_repo = append(local_repo, Package{name: pkgName, path: subdirPath})
			}

			mu.Lock() // Merge
			for i := range local_repo {
				p := &local_repo[i]
				repo[p.name] = p
			}
			mu.Unlock()
		}(file)
	}

	wg.Wait()
	return repo
}

// Get returns the package with the given name from the repository map.
// It panics if the package is not found.
func (rm RepoMap) Get(pkgname string) *Package {
	if pkg, ok := rm[pkgname]; ok {
		return pkg
	}
	panic(utils.Banner("Fatel Error", "Package not found in the repository: "+pkgname))
}

// Contains checks if the package with the given name exists in the repository map.
// It returns true if the package exists, false otherwise.
func (rm RepoMap) Contains(pkgname string) bool { _, ok := rm[pkgname]; return ok }

// ProcessList resolves dependencies for the provided package names and returns
// a linked list of packages starting from packages with no dependencies. It is
// ensured that if the packages are processed in the order they are provided,
// all dependencies are processed before the package itself is processed. It
// also adds dependees to each package, building a dependency graph. Note, that
// in this graph, only the packages that are in the process list are included.
// Use Package.Dependees() and Package.IterateDependees() to accsess it.
func (rm RepoMap) ProcessList(pkgNames []string) Head {
	pkgs := &Package{}
	pkgs.resolvDep("", pkgNames, rm)
	return pkgs
}

// PkgNames2List returns a linked list of packages from the provided package names.
// It panics if a package is not found in the repository map.
func (rm RepoMap) PkgNames2List(pkgNames []string) Head {
	head := &Package{}
	tail := head
	rm.IterOver(pkgNames, func(pkg *Package) { tail = pkg.Append(tail) })
	return head
}

// IterOver iterates over the provided package names and applies the function
// f to each package. It panics if a package is not found in the repository map.
func (rm RepoMap) IterOver(pNames []string, f func(*Package)) {
	for _, pName := range pNames {
		f(rm.Get(pName))
	}
}
