// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package compliance

// EdgeContextProvider is an interface for injecting edge-specific context
// into walk paths.
type EdgeContextProvider interface {
	// Context returns the context for `edge` when added to `path`.
	Context(lg *LicenseGraph, path TargetEdgePath, edge *TargetEdge) interface{}
}

// NoEdgeContext implements EdgeContextProvider for walks that use no context.
type NoEdgeContext struct{}

// Context returns nil.
func (ctx NoEdgeContext) Context(lg *LicenseGraph, path TargetEdgePath, edge *TargetEdge) interface{} {
	return nil
}

// ApplicableConditionsContext provides the subset of conditions in `universe`
// that apply to each edge in a path.
type ApplicableConditionsContext struct {
	universe LicenseConditionSet
}

// Context returns the LicenseConditionSet applicable to the edge.
func (ctx ApplicableConditionsContext) Context(lg *LicenseGraph, path TargetEdgePath, edge *TargetEdge) interface{} {
	universe := ctx.universe
	if len(path) > 0 {
		universe = path[len(path)-1].ctx.(LicenseConditionSet)
	}
	return conditionsAttachingAcrossEdge(lg, edge, universe)
}

// VisitNode is called for each root and for each walked dependency node by
// WalkTopDown and WalkTopDownBreadthFirst. When VisitNode returns true, WalkTopDown will proceed to walk
// down the dependences of the node
type VisitNode func(lg *LicenseGraph, target *TargetNode, path TargetEdgePath) bool

// WalkTopDown does a top-down walk of `lg` calling `visit` and descending
// into depenencies when `visit` returns true.
func WalkTopDown(ctx EdgeContextProvider, lg *LicenseGraph, visit VisitNode) {
	path := NewTargetEdgePath(32)

	var walk func(fnode *TargetNode)
	walk = func(fnode *TargetNode) {
		visitChildren := visit(lg, fnode, *path)
		if !visitChildren {
			return
		}
		for _, edge := range fnode.edges {
			var edgeContext interface{}
			if ctx == nil {
				edgeContext = nil
			} else {
				edgeContext = ctx.Context(lg, *path, edge)
			}
			path.Push(edge, edgeContext)
			walk(edge.dependency)
			path.Pop()
		}
	}

	for _, r := range lg.rootFiles {
		path.Clear()
		walk(lg.targets[r])
	}
}

// WalkTopDownBreadthFirst performs a Breadth-first top down walk of `lg` calling `visit` and descending
// into depenencies when `visit` returns true.
func WalkTopDownBreadthFirst(ctx EdgeContextProvider, lg *LicenseGraph, visit VisitNode) {
	path := NewTargetEdgePath(32)

	var walk func(fnode *TargetNode)
	walk = func(fnode *TargetNode) {
		edgesToWalk := make(TargetEdgeList, 0, len(fnode.edges))
		for _, edge := range fnode.edges {
			var edgeContext interface{}
			if ctx == nil {
				edgeContext = nil
			} else {
				edgeContext = ctx.Context(lg, *path, edge)
			}
			path.Push(edge, edgeContext)
			if visit(lg, edge.dependency, *path){
				edgesToWalk = append(edgesToWalk, edge)
			}
			path.Pop()
		}

		for _, edge := range(edgesToWalk) {
			var edgeContext interface{}
			if ctx == nil {
				edgeContext = nil
			} else {
				edgeContext = ctx.Context(lg, *path, edge)
			}
			path.Push(edge, edgeContext)
			walk(edge.dependency)
			path.Pop()
		}
	}

	path.Clear()
	rootsToWalk := make([]*TargetNode, 0, len(lg.rootFiles))
	for _, r := range lg.rootFiles {
		if visit(lg, lg.targets[r], *path){
			rootsToWalk = append(rootsToWalk, lg.targets[r])
		}
	}

	for _, rnode := range(rootsToWalk) {
		walk(rnode)
	}
}

// resolutionKey identifies results from walking a specific target for a
// specific set of conditions.
type resolutionKey struct {
	target *TargetNode
	cs     LicenseConditionSet
}

// WalkResolutionsForCondition performs a top-down walk of the LicenseGraph
// resolving all distributed works for `conditions`.
func WalkResolutionsForCondition(lg *LicenseGraph, conditions LicenseConditionSet) ResolutionSet {
	shipped := ShippedNodes(lg)

	// rmap maps 'attachesTo' targets to the `actsOn` targets and applicable conditions
	rmap := make(map[resolutionKey]ActionSet)

	// cmap identifies previously walked target/condition pairs.
	cmap := make(map[resolutionKey]struct{})

	// result accumulates the resolutions to return.
	result := make(ResolutionSet)
	WalkTopDown(ApplicableConditionsContext{conditions}, lg, func(lg *LicenseGraph, tn *TargetNode, path TargetEdgePath) bool {
		universe := conditions
		if len(path) > 0 {
			universe = path[len(path)-1].ctx.(LicenseConditionSet)
		}

		if universe.IsEmpty() {
			return false
		}
		key := resolutionKey{tn, universe}

		if _, alreadyWalked := cmap[key]; alreadyWalked {
			pure := true
			for _, p := range path {
				target := p.Target()
				tkey := resolutionKey{target, universe}
				if _, ok := rmap[tkey]; !ok {
					rmap[tkey] = make(ActionSet)
				}
				// attach prior walk outcome to ancestor
				for actsOn, cs := range rmap[key] {
					rmap[tkey][actsOn] = cs
				}
				// if prior walk produced results, copy results
				// to ancestor.
				if _, ok := result[tn]; ok && pure {
					if _, ok := result[target]; !ok {
						result[target] = make(ActionSet)
					}
					for actsOn, cs := range result[tn] {
						result[target][actsOn] = cs
					}
					pure = target.IsContainer()
				}
			}
			// if all ancestors are pure aggregates, attach
			// matching prior walk conditions to self. Prior walk
			// will not have done so if any ancestor was not an
			// aggregate.
			if pure {
				match := rmap[key][tn].Intersection(universe)
				if !match.IsEmpty() {
					if _, ok := result[tn]; !ok {
						result[tn] = make(ActionSet)
					}
					result[tn][tn] = match
				}
			}
			return false
		}
		// no need to walk node or dependencies if not shipped
		if !shipped.Contains(tn) {
			return false
		}
		if _, ok := rmap[key]; !ok {
			rmap[key] = make(ActionSet)
		}
		// add self to walk outcome
		rmap[key][tn] = tn.resolution
		cmap[key] = struct{}{}
		cs := tn.resolution
		if !cs.IsEmpty() {
			cs = cs.Intersection(universe)
			pure := true
			for _, p := range path {
				target := p.Target()
				tkey := resolutionKey{target, universe}
				if _, ok := rmap[tkey]; !ok {
					rmap[tkey] = make(ActionSet)
				}
				// copy current node's action into ancestor
				rmap[tkey][tn] = tn.resolution
				// conditionally put matching conditions into
				// result
				if pure && !cs.IsEmpty() {
					if _, ok := result[target]; !ok {
						result[target] = make(ActionSet)
					}
					result[target][tn] = cs
					pure = target.IsContainer()
				}
			}
			// if all ancestors are pure aggregates, attach
			// matching conditions to self.
			if pure && !cs.IsEmpty() {
				if _, ok := result[tn]; !ok {
					result[tn] = make(ActionSet)
				}
				result[tn][tn] = cs
			}
		}
		return true
	})

	return result
}

// WalkActionsForCondition performs a top-down walk of the LicenseGraph
// resolving all distributed works for `conditions`.
func WalkActionsForCondition(lg *LicenseGraph, conditions LicenseConditionSet) ActionSet {
	// amap maps 'actsOn' targets to the applicable conditions
	//
	// amap is the resulting ActionSet
	amap := make(ActionSet)

	for tn := range ShippedNodes(lg) {
		if cs := conditions.Intersection(tn.resolution); !cs.IsEmpty() {
			amap[tn] = cs
		}
	}

	return amap
}
