package yqlib

import (
	"fmt"

	yaml "gopkg.in/yaml.v3"
)

type NodeContext struct {
	Node      *yaml.Node
	Head      interface{}
	Tail      []interface{}
	PathStack []interface{}
}

func NewNodeContext(node *yaml.Node, head interface{}, tail []interface{}, pathStack []interface{}) NodeContext {
	newTail := make([]interface{}, len(tail))
	copy(newTail, tail)

	newPathStack := make([]interface{}, len(pathStack))
	copy(newPathStack, pathStack)
	return NodeContext{
		Node:      node,
		Head:      head,
		Tail:      newTail,
		PathStack: newPathStack,
	}
}

type NavigationStrategy interface {
	FollowAlias(nodeContext NodeContext) bool
	AutoCreateMap(nodeContext NodeContext) bool
	Visit(nodeContext NodeContext) error
	// node key is the string value of the last element in the path stack
	// we use it to match against the pathExpression in head.
	ShouldTraverse(nodeContext NodeContext, nodeKey string) bool
	ShouldDeeplyTraverse(nodeContext NodeContext) bool
	// when deeply traversing, should we visit all matching nodes, or just leaves?
	ShouldOnlyDeeplyVisitLeaves(NodeContext) bool
	GetVisitedNodes() []*NodeContext
	DebugVisitedNodes()
	GetPathParser() PathParser
}

type NavigationStrategyImpl struct {
	followAlias                 func(nodeContext NodeContext) bool
	autoCreateMap               func(nodeContext NodeContext) bool
	visit                       func(nodeContext NodeContext) error
	shouldVisitExtraFn          func(nodeContext NodeContext) bool
	shouldDeeplyTraverse        func(nodeContext NodeContext) bool
	shouldOnlyDeeplyVisitLeaves func(nodeContext NodeContext) bool
	visitedNodes                []*NodeContext
	pathParser                  PathParser
}

func (ns *NavigationStrategyImpl) GetPathParser() PathParser {
	return ns.pathParser
}

func (ns *NavigationStrategyImpl) GetVisitedNodes() []*NodeContext {
	return ns.visitedNodes
}

func (ns *NavigationStrategyImpl) FollowAlias(nodeContext NodeContext) bool {
	if ns.followAlias != nil {
		return ns.followAlias(nodeContext)
	}
	return true
}

func (ns *NavigationStrategyImpl) AutoCreateMap(nodeContext NodeContext) bool {
	if ns.autoCreateMap != nil {
		return ns.autoCreateMap(nodeContext)
	}
	return false
}

func (ns *NavigationStrategyImpl) ShouldDeeplyTraverse(nodeContext NodeContext) bool {
	if ns.shouldDeeplyTraverse != nil {
		return ns.shouldDeeplyTraverse(nodeContext)
	}
	return true
}

func (ns *NavigationStrategyImpl) ShouldOnlyDeeplyVisitLeaves(nodeContext NodeContext) bool {
	if ns.shouldOnlyDeeplyVisitLeaves != nil {
		return ns.shouldOnlyDeeplyVisitLeaves(nodeContext)
	}
	return true

}

func (ns *NavigationStrategyImpl) ShouldTraverse(nodeContext NodeContext, nodeKey string) bool {
	// we should traverse aliases (if enabled), but not visit them :/
	if len(nodeContext.PathStack) == 0 {
		return true
	}

	if ns.alreadyVisited(nodeContext.PathStack) {
		return false
	}

	return (nodeKey == "<<" && ns.FollowAlias(nodeContext)) || (nodeKey != "<<" &&
		ns.pathParser.MatchesNextPathElement(nodeContext, nodeKey))
}

func (ns *NavigationStrategyImpl) shouldVisit(nodeContext NodeContext) bool {
	pathStack := nodeContext.PathStack
	if len(pathStack) == 0 {
		return true
	}
	log.Debug("tail len %v", len(nodeContext.Tail))

	if ns.alreadyVisited(pathStack) || len(nodeContext.Tail) != 0 {
		return false
	}

	nodeKey := fmt.Sprintf("%v", pathStack[len(pathStack)-1])
	log.Debug("nodeKey: %v, nodeContext.Head: %v", nodeKey, nodeContext.Head)

	// only visit aliases if its an exact match
	return ((nodeKey == "<<" && nodeContext.Head == "<<") || (nodeKey != "<<" &&
		ns.pathParser.MatchesNextPathElement(nodeContext, nodeKey))) && (ns.shouldVisitExtraFn == nil || ns.shouldVisitExtraFn(nodeContext))
}

func (ns *NavigationStrategyImpl) Visit(nodeContext NodeContext) error {
	log.Debug("Visit?, %v, %v", nodeContext.Head, pathStackToString(nodeContext.PathStack))
	DebugNode(nodeContext.Node)
	if ns.shouldVisit(nodeContext) {
		log.Debug("yep, visiting")
		//  pathStack array must be
		// copied, as append() may sometimes reuse and modify the array
		ns.visitedNodes = append(ns.visitedNodes, &nodeContext)
		ns.DebugVisitedNodes()
		return ns.visit(nodeContext)
	}
	log.Debug("nope, skip it")
	return nil
}

func (ns *NavigationStrategyImpl) DebugVisitedNodes() {
	log.Debug("Visited Nodes:")
	for _, candidate := range ns.visitedNodes {
		log.Debug(" - %v", pathStackToString(candidate.PathStack))
	}
}

func (ns *NavigationStrategyImpl) alreadyVisited(pathStack []interface{}) bool {
	log.Debug("checking already visited pathStack: %v", pathStackToString(pathStack))
	for _, candidate := range ns.visitedNodes {
		candidatePathStack := candidate.PathStack
		if patchStacksMatch(candidatePathStack, pathStack) {
			log.Debug("paths match, already seen it")
			return true
		}

	}
	log.Debug("never seen it before!")
	return false
}

func patchStacksMatch(path1 []interface{}, path2 []interface{}) bool {
	log.Debug("checking against path: %v", pathStackToString(path1))

	if len(path1) != len(path2) {
		return false
	}
	for index, p1Value := range path1 {

		p2Value := path2[index]
		if p1Value != p2Value {
			return false
		}
	}
	return true

}