// Copyright 2021 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 vta

import (
	"go/types"
	"iter"
	"slices"

	"golang.org/x/tools/go/callgraph/vta/internal/trie"
	"golang.org/x/tools/go/ssa"

	"golang.org/x/tools/go/types/typeutil"
)

// scc computes strongly connected components (SCCs) of `g` using the
// classical Tarjan's algorithm for SCCs. The result is two slices:
//   - sccs: the SCCs, each represented as a slice of node indices
//   - idxToSccID: the inverse map, from node index to SCC number.
//
// The SCCs are sorted in reverse topological order: for SCCs
// with ids X and Y s.t. X < Y, Y comes before X in the topological order.
func scc(g *vtaGraph) (sccs [][]idx, idxToSccID []int) {
	// standard data structures used by Tarjan's algorithm.
	type state struct {
		pre     int // preorder of the node (0 if unvisited)
		lowLink int
		onStack bool
	}
	states := make([]state, g.numNodes())
	var stack []idx

	idxToSccID = make([]int, g.numNodes())
	nextPre := 0

	var doSCC func(idx)
	doSCC = func(n idx) {
		nextPre++
		ns := &states[n]
		*ns = state{pre: nextPre, lowLink: nextPre, onStack: true}
		stack = append(stack, n)

		for s := range g.successors(n) {
			if ss := &states[s]; ss.pre == 0 {
				// Analyze successor s that has not been visited yet.
				doSCC(s)
				ns.lowLink = min(ns.lowLink, ss.lowLink)
			} else if ss.onStack {
				// The successor is on the stack, meaning it has to be
				// in the current SCC.
				ns.lowLink = min(ns.lowLink, ss.pre)
			}
		}

		// if n is a root node, pop the stack and generate a new SCC.
		if ns.lowLink == ns.pre {
			sccStart := slicesLastIndex(stack, n)
			scc := slices.Clone(stack[sccStart:])
			stack = stack[:sccStart]
			sccID := len(sccs)
			sccs = append(sccs, scc)
			for _, w := range scc {
				states[w].onStack = false
				idxToSccID[w] = sccID
			}
		}
	}

	for n, nn := 0, g.numNodes(); n < nn; n++ {
		if states[n].pre == 0 {
			doSCC(idx(n))
		}
	}

	return sccs, idxToSccID
}

// slicesLastIndex returns the index of the last occurrence of v in s, or -1 if v is
// not present in s.
//
// slicesLastIndex iterates backwards through the elements of s, stopping when the ==
// operator determines an element is equal to v.
func slicesLastIndex[S ~[]E, E comparable](s S, v E) int {
	// TODO: move to / dedup with slices.LastIndex
	for i := len(s) - 1; i >= 0; i-- {
		if s[i] == v {
			return i
		}
	}
	return -1
}

// propType represents type information being propagated
// over the vta graph. f != nil only for function nodes
// and nodes reachable from function nodes. There, we also
// remember the actual *ssa.Function in order to more
// precisely model higher-order flow.
type propType struct {
	typ types.Type
	f   *ssa.Function
}

// propTypeMap is an auxiliary structure that serves
// the role of a map from nodes to a set of propTypes.
type propTypeMap map[node]*trie.MutMap

// propTypes returns an iterator for the propTypes associated with
// node `n` in map `ptm`.
func (ptm propTypeMap) propTypes(n node) iter.Seq[propType] {
	return func(yield func(propType) bool) {
		if types := ptm[n]; types != nil {
			types.M.Range(func(_ uint64, elem any) bool {
				return yield(elem.(propType))
			})
		}
	}
}

// propagate reduces the `graph` based on its SCCs and
// then propagates type information through the reduced
// graph. The result is a map from nodes to a set of types
// and functions, stemming from higher-order data flow,
// reaching the node. `canon` is used for type uniqueness.
func propagate(graph *vtaGraph, canon *typeutil.Map) propTypeMap {
	sccs, idxToSccID := scc(graph)

	// propTypeIds are used to create unique ids for
	// propType, to be used for trie-based type sets.
	propTypeIds := make(map[propType]uint64)
	// Id creation is based on == equality, which works
	// as types are canonicalized (see getPropType).
	propTypeId := func(p propType) uint64 {
		if id, ok := propTypeIds[p]; ok {
			return id
		}
		id := uint64(len(propTypeIds))
		propTypeIds[p] = id
		return id
	}
	builder := trie.NewBuilder()
	// Initialize sccToTypes to avoid repeated check
	// for initialization later.
	sccToTypes := make([]*trie.MutMap, len(sccs))
	for sccID, scc := range sccs {
		typeSet := builder.MutEmpty()
		for _, idx := range scc {
			if n := graph.node[idx]; hasInitialTypes(n) {
				// add the propType for idx to typeSet.
				pt := getPropType(n, canon)
				typeSet.Update(propTypeId(pt), pt)
			}
		}
		sccToTypes[sccID] = &typeSet
	}

	for i := len(sccs) - 1; i >= 0; i-- {
		nextSccs := make(map[int]empty)
		for _, n := range sccs[i] {
			for succ := range graph.successors(n) {
				nextSccs[idxToSccID[succ]] = empty{}
			}
		}
		// Propagate types to all successor SCCs.
		for nextScc := range nextSccs {
			sccToTypes[nextScc].Merge(sccToTypes[i].M)
		}
	}
	nodeToTypes := make(propTypeMap, graph.numNodes())
	for sccID, scc := range sccs {
		types := sccToTypes[sccID]
		for _, idx := range scc {
			nodeToTypes[graph.node[idx]] = types
		}
	}
	return nodeToTypes
}

// hasInitialTypes check if a node can have initial types.
// Returns true iff `n` is not a panic, recover, nestedPtr*
// node, nor a node whose type is an interface.
func hasInitialTypes(n node) bool {
	switch n.(type) {
	case panicArg, recoverReturn, nestedPtrFunction, nestedPtrInterface:
		return false
	default:
		return !types.IsInterface(n.Type())
	}
}

// getPropType creates a propType for `node` based on its type.
// propType.typ is always node.Type(). If node is function, then
// propType.val is the underlying function; nil otherwise.
func getPropType(node node, canon *typeutil.Map) propType {
	t := canonicalize(node.Type(), canon)
	if fn, ok := node.(function); ok {
		return propType{f: fn.f, typ: t}
	}
	return propType{f: nil, typ: t}
}
