// Copyright 2023-2024 Princess Beef Heavy Industries, LLC / Dave Shanley
// https://pb33f.io
// MIT License

package low

import (
	"context"
	"sync"

	"github.com/pb33f/libopenapi/orderedmap"
	"gopkg.in/yaml.v3"
)

// HasNodes is an interface that defines a method to get a map of nodes
type HasNodes interface {
	GetNodes() map[int][]*yaml.Node
}

// AddNodes is an interface that defined a method to add nodes.
type AddNodes interface {
	AddNode(key int, node *yaml.Node)
}

// NodeMap represents a map of yaml nodes
type NodeMap struct {
	// Nodes is a sync map of nodes for this object, and the key is the line number of the node
	// a line can contain many nodes (in JSON), so the value is a slice of *yaml.Node
	Nodes *sync.Map `yaml:"-" json:"-"`
}

// AddNode will add a node to the NodeMap
func (nm *NodeMap) AddNode(key int, node *yaml.Node) {
	if existing, ok := nm.Nodes.Load(key); ok {
		if ext, ko := existing.(*yaml.Node); ko {
			nm.Nodes.Store(key, []*yaml.Node{ext, node})
		}
		if ext, ko := existing.([]*yaml.Node); ko {
			ext = append(ext, node)
			nm.Nodes.Store(key, ext)
		}
	} else {
		nm.Nodes.Store(key, []*yaml.Node{node})
	}
}

// GetNodes will return the map of nodes
func (nm *NodeMap) GetNodes() map[int][]*yaml.Node {
	composed := make(map[int][]*yaml.Node)
	if nm.Nodes != nil {
		nm.Nodes.Range(func(key, value interface{}) bool {
			if v, ok := value.([]*yaml.Node); ok {
				composed[key.(int)] = v
			}
			if v, ok := value.(*yaml.Node); ok {
				composed[key.(int)] = []*yaml.Node{v}
			}

			return true
		})
	}
	if len(composed) <= 0 {
		composed[0] = []*yaml.Node{} // return an empty slice if there are no nodes
	}
	return composed
}

// ExtractNodes will iterate over a *yaml.Node and extract all nodes with a line number into a map
func (nm *NodeMap) ExtractNodes(node *yaml.Node, recurse bool) {
	if node == nil {
		return
	}
	// if the node has content, iterate over it and extract every top level line number
	if node.Content != nil {
		for i := 0; i < len(node.Content); i++ {
			if node.Content[i].Line != 0 && len(node.Content[i].Content) <= 0 {
				nm.AddNode(node.Content[i].Line, node.Content[i])
			}
			if node.Content[i].Line != 0 && len(node.Content[i].Content) > 0 {
				if recurse {
					nm.AddNode(node.Content[i].Line, node.Content[i])
					nm.ExtractNodes(node.Content[i], recurse)
				}
			}
		}
	}
}

// ContainsLine will return true if the NodeMap contains a node with the supplied line number
func (nm *NodeMap) ContainsLine(line int) bool {
	if _, ok := nm.Nodes.Load(line); ok {
		return true
	}
	return false
}

// ExtractNodes will extract all nodes from a yaml.Node and return them in a map
func ExtractNodes(_ context.Context, root *yaml.Node) *sync.Map {
	var syncMap sync.Map
	nm := &NodeMap{Nodes: &syncMap}
	nm.ExtractNodes(root, false)
	return nm.Nodes
}

// ExtractNodesRecursive will extract all nodes from a yaml.Node and return them in a map, just like ExtractNodes
// however, this version will dive-down the tree and extract all nodes from all child nodes as well until the tree
// is done.
func ExtractNodesRecursive(_ context.Context, root *yaml.Node) *sync.Map {
	var syncMap sync.Map
	nm := &NodeMap{Nodes: &syncMap}
	nm.ExtractNodes(root, true)
	return nm.Nodes
}

// ExtractExtensionNodes will extract all extension nodes from a map of extensions, recursively.
func ExtractExtensionNodes(_ context.Context,
	extensionMap *orderedmap.Map[KeyReference[string],
		ValueReference[*yaml.Node]], nodeMap *sync.Map,
) {
	// range over the extension map and extract all nodes
	for k, v := range extensionMap.FromOldest() {
		results := []*yaml.Node{k.KeyNode}
		var newNodeMap sync.Map
		nm := &NodeMap{Nodes: &newNodeMap}
		if len(v.ValueNode.Content) > 0 {
			nm.ExtractNodes(v.ValueNode, true)
			nm.Nodes.Range(func(key, value interface{}) bool {
				for _, n := range value.([]*yaml.Node) {
					results = append(results, n)
				}
				return true
			})
		} else {
			results = append(results, v.ValueNode)
		}
		if nodeMap != nil {
			if k.KeyNode.Line == v.ValueNode.Line {
				nodeMap.Store(k.KeyNode.Line, results)
			} else {
				nodeMap.Store(k.KeyNode.Line, results[0])
				for _, y := range results[1:] {
					nodeMap.Store(y.Line, []*yaml.Node{y})
				}
			}
		}
	}
}
