// Copyright 2017 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 gps

type selection struct {
	// projects is a stack of the atoms that have currently been selected by the
	// solver. It can also be thought of as the vertex set of the current
	// selection graph.
	projects []selected
	// deps records the set of dependers on a given ProjectRoot. It is
	// essentially an adjacency list of *inbound* edges.
	deps map[ProjectRoot][]dependency
	// foldRoots records a mapping from a canonical, case-folded form of
	// ProjectRoots to the particular case variant that has currently been
	// selected.
	foldRoots map[string]ProjectRoot
}

type selected struct {
	a     atomWithPackages
	first bool
}

func (s *selection) getDependenciesOn(id ProjectIdentifier) []dependency {
	if deps, exists := s.deps[id.ProjectRoot]; exists {
		return deps
	}

	return nil
}

// getIdentFor returns the ProjectIdentifier (so, the network name) currently in
// use for the provided ProjectRoot.
//
// If no dependencies are present yet that designate a network name for
// the provided root, this will return an empty ProjectIdentifier and false.
func (s *selection) getIdentFor(pr ProjectRoot) (ProjectIdentifier, bool) {
	deps := s.getDependenciesOn(ProjectIdentifier{ProjectRoot: pr})
	if len(deps) == 0 {
		return ProjectIdentifier{}, false
	}

	// For now, at least, the solver maintains (assumes?) the invariant that
	// whatever is first in the deps list decides the net name to be used.
	return deps[0].dep.Ident, true
}

// pushSelection pushes a new atomWithPackages onto the selection stack, along
// with an indicator as to whether this selection indicates a new project *and*
// packages, or merely some new packages on a project that was already selected.
func (s *selection) pushSelection(a atomWithPackages, pkgonly bool) {
	s.projects = append(s.projects, selected{
		a:     a,
		first: !pkgonly,
	})
}

// popSelection removes and returns the last atomWithPackages from the selection
// stack, along with an indication of whether that element was the first from
// that project - that is, if it represented an addition of both a project and
// one or more packages to the overall selection.
func (s *selection) popSelection() (atomWithPackages, bool) {
	var sel selected
	sel, s.projects = s.projects[len(s.projects)-1], s.projects[:len(s.projects)-1]
	return sel.a, sel.first
}

// findCaseConflicts checks to see if the given ProjectRoot has a
// case-insensitive overlap with another, different ProjectRoot that's already
// been picked.
func (s *selection) findCaseConflicts(pr ProjectRoot) (bool, ProjectRoot) {
	if current, has := s.foldRoots[toFold(string(pr))]; has && pr != current {
		return true, current
	}

	return false, ""
}

func (s *selection) pushDep(dep dependency) {
	pr := dep.dep.Ident.ProjectRoot
	deps := s.deps[pr]
	if len(deps) == 0 {
		s.foldRoots[toFold(string(pr))] = pr
	}

	s.deps[pr] = append(deps, dep)
}

func (s *selection) popDep(id ProjectIdentifier) (dep dependency) {
	deps := s.deps[id.ProjectRoot]
	dlen := len(deps)
	if dlen == 1 {
		delete(s.foldRoots, toFold(string(id.ProjectRoot)))
	}

	dep, s.deps[id.ProjectRoot] = deps[dlen-1], deps[:dlen-1]
	return dep
}

func (s *selection) depperCount(id ProjectIdentifier) int {
	return len(s.deps[id.ProjectRoot])
}

// Compute a list of the unique packages within the given ProjectIdentifier that
// have dependers, and the number of dependers they have.
func (s *selection) getRequiredPackagesIn(id ProjectIdentifier) map[string]int {
	// TODO(sdboyer) this is horribly inefficient to do on the fly; we need a method to
	// precompute it on pushing a new dep, and preferably with an immut
	// structure so that we can pop with zero cost.
	uniq := make(map[string]int)
	for _, dep := range s.deps[id.ProjectRoot] {
		for _, pkg := range dep.dep.pl {
			uniq[pkg]++
		}
	}

	return uniq
}

// Suppress unused linting warning.
var _ = (*selection)(nil).getSelectedPackagesIn
var _ = (*selection)(nil).getProjectImportMap

// Compute a list of the unique packages within the given ProjectIdentifier that
// are currently selected, and the number of times each package has been
// independently selected.
func (s *selection) getSelectedPackagesIn(id ProjectIdentifier) map[string]int {
	// TODO(sdboyer) this is horribly inefficient to do on the fly; we need a method to
	// precompute it on pushing a new dep, and preferably with an immut
	// structure so that we can pop with zero cost.
	uniq := make(map[string]int)
	for _, p := range s.projects {
		if p.a.a.id.eq(id) {
			for _, pkg := range p.a.pl {
				uniq[pkg]++
			}
		}
	}

	return uniq
}

// getProjectImportMap extracts the set of package imports from the used
// packages in each selected project.
func (s *selection) getProjectImportMap() map[ProjectRoot]map[string]struct{} {
	importMap := make(map[ProjectRoot]map[string]struct{})
	for _, edges := range s.deps {
		for _, edge := range edges {
			var curmap map[string]struct{}
			if imap, has := importMap[edge.depender.id.ProjectRoot]; !has {
				curmap = make(map[string]struct{})
			} else {
				curmap = imap
			}

			for _, pl := range edge.dep.pl {
				curmap[pl] = struct{}{}
			}
			importMap[edge.depender.id.ProjectRoot] = curmap
		}
	}

	return importMap
}

func (s *selection) getConstraint(id ProjectIdentifier) Constraint {
	deps, exists := s.deps[id.ProjectRoot]
	if !exists || len(deps) == 0 {
		return any
	}

	// TODO(sdboyer) recomputing this sucks and is quite wasteful. Precompute/cache it
	// on changes to the constraint set, instead.

	// The solver itself is expected to maintain the invariant that all the
	// constraints kept here collectively admit a non-empty set of versions. We
	// assume this is the case here while assembling a composite constraint.

	// Start with the open set
	var ret Constraint = any
	for _, dep := range deps {
		ret = ret.Intersect(dep.dep.Constraint)
	}

	return ret
}

// selected checks to see if the given ProjectIdentifier has been selected, and
// if so, returns the corresponding atomWithPackages.
//
// It walks the projects selection list from front to back and returns the first
// match it finds, which means it will always and only return the base selection
// of the project, without any additional package selections that may or may not
// have happened later.
func (s *selection) selected(id ProjectIdentifier) (atomWithPackages, bool) {
	for _, p := range s.projects {
		if p.a.a.id.ProjectRoot == id.ProjectRoot {
			return p.a, true
		}
	}

	return atomWithPackages{a: nilpa}, false
}

type unselected struct {
	sl  []bimodalIdentifier
	cmp func(i, j int) bool
}

func (u unselected) Len() int {
	return len(u.sl)
}

func (u unselected) Less(i, j int) bool {
	return u.cmp(i, j)
}

func (u unselected) Swap(i, j int) {
	u.sl[i], u.sl[j] = u.sl[j], u.sl[i]
}

func (u *unselected) Push(x interface{}) {
	u.sl = append(u.sl, x.(bimodalIdentifier))
}

func (u *unselected) Pop() (v interface{}) {
	v, u.sl = u.sl[len(u.sl)-1], u.sl[:len(u.sl)-1]
	return v
}

// remove takes a bimodalIdentifier out of the priority queue, if present. Only
// the first matching bmi will be removed.
//
// There are two events that cause this to be called: bmi selection, when the
// bmi at the front of the queue is removed, and backtracking, when a bmi
// becomes unnecessary because the dependency that induced it was backtracked
// and popped off.
//
// The worst case for both of these is O(n), but in practice the first case is
// O(1), as we iterate the queue from front to back.
func (u *unselected) remove(bmi bimodalIdentifier) {
	plen := len(bmi.pl)
outer:
	for i, pi := range u.sl {
		if pi.id.eq(bmi.id) && len(pi.pl) == plen {
			// Simple slice comparison - assume they're both sorted the same
			for i2, pkg := range pi.pl {
				if bmi.pl[i2] != pkg {
					continue outer
				}
			}

			if i == len(u.sl)-1 {
				// if we're on the last element, just pop, no splice
				u.sl = u.sl[:len(u.sl)-1]
			} else {
				u.sl = append(u.sl[:i], u.sl[i+1:]...)
			}
			break
		}
	}
}
