// Copyright 2011-2019 Canonical Ltd
// Copyright 2025 The go-yaml Project Contributors
// SPDX-License-Identifier: Apache-2.0

// Node types and constants for YAML tree representation.
// Defines Kind, Style, and Node structure for intermediate YAML representation.

package libyaml

import (
	"reflect"
	"strings"
	"unicode"
	"unicode/utf8"
)

// Tag constants for YAML types
const (
	nullTag      = "!!null"
	boolTag      = "!!bool"
	strTag       = "!!str"
	intTag       = "!!int"
	floatTag     = "!!float"
	timestampTag = "!!timestamp"
	seqTag       = "!!seq"
	mapTag       = "!!map"
	binaryTag    = "!!binary"
	mergeTag     = "!!merge"
)

const longTagPrefix = "tag:yaml.org,2002:"

var (
	longTags  = make(map[string]string)
	shortTags = make(map[string]string)
)

func init() {
	for _, stag := range []string{nullTag, boolTag, strTag, intTag, floatTag, timestampTag, seqTag, mapTag, binaryTag, mergeTag} {
		ltag := longTag(stag)
		longTags[stag] = ltag
		shortTags[ltag] = stag
	}
}

func shortTag(tag string) string {
	if strings.HasPrefix(tag, longTagPrefix) {
		if stag, ok := shortTags[tag]; ok {
			return stag
		}
		return "!!" + tag[len(longTagPrefix):]
	}
	return tag
}

func longTag(tag string) string {
	if strings.HasPrefix(tag, "!!") {
		if ltag, ok := longTags[tag]; ok {
			return ltag
		}
		return longTagPrefix + tag[2:]
	}
	return tag
}

// Kind represents the type of YAML node
type Kind uint32

const (
	DocumentNode Kind = 1 << iota
	SequenceNode
	MappingNode
	ScalarNode
	AliasNode
	StreamNode
)

// Style represents the formatting style of a YAML node
type Style uint32

const (
	TaggedStyle Style = 1 << iota
	DoubleQuotedStyle
	SingleQuotedStyle
	LiteralStyle
	FoldedStyle
	FlowStyle
)

// StreamVersionDirective represents a YAML %YAML version directive for stream nodes.
type StreamVersionDirective struct {
	Major int
	Minor int
}

// StreamTagDirective represents a YAML %TAG directive for stream nodes.
type StreamTagDirective struct {
	Handle string
	Prefix string
}

// Node represents an element in the YAML document hierarchy. While documents
// are typically encoded and decoded into higher level types, such as structs
// and maps, Node is an intermediate representation that allows detailed
// control over the content being decoded or encoded.
//
// It's worth noting that although Node offers access into details such as
// line numbers, columns, and comments, the content when re-encoded will not
// have its original textual representation preserved. An effort is made to
// render the data pleasantly, and to preserve comments near the data they
// describe, though.
//
// Values that make use of the Node type interact with the yaml package in the
// same way any other type would do, by encoding and decoding yaml data
// directly or indirectly into them.
//
// For example:
//
//	var person struct {
//	        Name    string
//	        Address yaml.Node
//	}
//	err := yaml.Unmarshal(data, &person)
//
// Or by itself:
//
//	var person Node
//	err := yaml.Unmarshal(data, &person)
type Node struct {
	// Kind defines whether the node is a document, a mapping, a sequence,
	// a scalar value, or an alias to another node. The specific data type of
	// scalar nodes may be obtained via the ShortTag and LongTag methods.
	Kind Kind

	// Style allows customizing the appearance of the node in the tree.
	Style Style

	// Tag holds the YAML tag defining the data type for the value.
	// When decoding, this field will always be set to the resolved tag,
	// even when it wasn't explicitly provided in the YAML content.
	// When encoding, if this field is unset the value type will be
	// implied from the node properties, and if it is set, it will only
	// be serialized into the representation if TaggedStyle is used or
	// the implicit tag diverges from the provided one.
	Tag string

	// Value holds the unescaped and unquoted representation of the value.
	Value string

	// Anchor holds the anchor name for this node, which allows aliases to point to it.
	Anchor string

	// Alias holds the node that this alias points to. Only valid when Kind is AliasNode.
	Alias *Node

	// Content holds contained nodes for documents, mappings, and sequences.
	Content []*Node

	// HeadComment holds any comments in the lines preceding the node and
	// not separated by an empty line.
	HeadComment string

	// LineComment holds any comments at the end of the line where the node is in.
	LineComment string

	// FootComment holds any comments following the node and before empty lines.
	FootComment string

	// Line and Column hold the node position in the decoded YAML text.
	// These fields are not respected when encoding the node.
	Line   int
	Column int

	// StreamNode-specific fields (only valid when Kind == StreamNode)

	// Encoding holds the stream encoding (UTF-8, UTF-16LE, UTF-16BE).
	// Only valid for StreamNode.
	Encoding Encoding

	// Version holds the YAML version directive (%YAML).
	// Only valid for StreamNode.
	Version *StreamVersionDirective

	// TagDirectives holds the %TAG directives.
	// Only valid for StreamNode.
	TagDirectives []StreamTagDirective
}

// IsZero returns whether the node has all of its fields unset.
func (n *Node) IsZero() bool {
	return n.Kind == 0 && n.Style == 0 && n.Tag == "" && n.Value == "" && n.Anchor == "" && n.Alias == nil && n.Content == nil &&
		n.HeadComment == "" && n.LineComment == "" && n.FootComment == "" && n.Line == 0 && n.Column == 0 &&
		n.Encoding == 0 && n.Version == nil && n.TagDirectives == nil
}

// LongTag returns the long form of the tag that indicates the data type for
// the node. If the Tag field isn't explicitly defined, one will be computed
// based on the node properties.
func (n *Node) LongTag() string {
	return longTag(n.ShortTag())
}

// ShortTag returns the short form of the YAML tag that indicates data type for
// the node. If the Tag field isn't explicitly defined, one will be computed
// based on the node properties.
func (n *Node) ShortTag() string {
	if n.indicatedString() {
		return strTag
	}
	if n.Tag == "" || n.Tag == "!" {
		switch n.Kind {
		case MappingNode:
			return mapTag
		case SequenceNode:
			return seqTag
		case AliasNode:
			if n.Alias != nil {
				return n.Alias.ShortTag()
			}
		case ScalarNode:
			return strTag
		case 0:
			// Special case to make the zero value convenient.
			if n.IsZero() {
				return nullTag
			}
		}
		return ""
	}
	return shortTag(n.Tag)
}

func (n *Node) indicatedString() bool {
	return n.Kind == ScalarNode &&
		(shortTag(n.Tag) == strTag ||
			(n.Tag == "" || n.Tag == "!") && n.Style&(SingleQuotedStyle|DoubleQuotedStyle|LiteralStyle|FoldedStyle) != 0)
}

// shouldUseLiteralStyle determines if a string should use literal style.
// It returns true if the string contains newlines AND meets additional criteria:
// - is at least 2 characters long
// - contains at least one non-whitespace character
func shouldUseLiteralStyle(s string) bool {
	if !strings.Contains(s, "\n") || len(s) < 2 {
		return false
	}
	// Must contain at least one non-whitespace character
	for _, r := range s {
		if !unicode.IsSpace(r) {
			return true
		}
	}
	return false
}

// SetString is a convenience function that sets the node to a string value
// and defines its style in a pleasant way depending on its content.
func (n *Node) SetString(s string) {
	n.Kind = ScalarNode
	if utf8.ValidString(s) {
		n.Value = s
		n.Tag = strTag
	} else {
		n.Value = encodeBase64(s)
		n.Tag = binaryTag
	}
	if shouldUseLiteralStyle(n.Value) {
		n.Style = LiteralStyle
	}
}

// Decode decodes the node and stores its data into the value pointed to by v.
//
// See the documentation for Unmarshal for details about the
// conversion of YAML into a Go value.
func (n *Node) Decode(v any) (err error) {
	d := NewConstructor(DefaultOptions)
	defer handleErr(&err)
	out := reflect.ValueOf(v)
	if out.Kind() == reflect.Pointer && !out.IsNil() {
		out = out.Elem()
	}
	d.Construct(n, out)
	if len(d.TypeErrors) > 0 {
		return &LoadErrors{Errors: d.TypeErrors}
	}
	return nil
}

// Load decodes the node and stores its data into the value pointed to by v,
// applying the given options.
//
// This method is useful when you need to preserve options like WithKnownFields()
// inside custom UnmarshalYAML implementations.
//
// Maps and pointers (to a struct, string, int, etc) are accepted as v
// values. If an internal pointer within a struct is not initialized,
// the yaml package will initialize it if necessary. The v parameter
// must not be nil.
//
// See the documentation of the package-level Load function for details
// about YAML to Go conversion and tag options.
func (n *Node) Load(v any, opts ...Option) (err error) {
	defer handleErr(&err)
	o, err := ApplyOptions(opts...)
	if err != nil {
		return err
	}
	d := NewConstructor(o)
	out := reflect.ValueOf(v)
	if out.Kind() == reflect.Pointer && !out.IsNil() {
		out = out.Elem()
	}
	d.Construct(n, out)
	if len(d.TypeErrors) > 0 {
		return &LoadErrors{Errors: d.TypeErrors}
	}
	return nil
}

// Encode encodes value v and stores its representation in n.
//
// See the documentation for Marshal for details about the
// conversion of Go values into YAML.
func (n *Node) Encode(v any) (err error) {
	defer handleErr(&err)
	e := NewRepresenter(noWriter, DefaultOptions)
	defer e.Destroy()
	e.MarshalDoc("", reflect.ValueOf(v))
	e.Finish()
	p := NewComposer(e.Out)
	p.Textless = true
	defer p.Destroy()
	doc := p.Parse()
	*n = *doc.Content[0]
	return nil
}

// Dump encodes value v and stores its representation in n,
// applying the given options.
//
// This method is useful when you need to apply specific encoding options
// while building Node trees programmatically.
//
// See the documentation for Marshal for details about the
// conversion of Go values into YAML.
func (n *Node) Dump(v any, opts ...Option) (err error) {
	defer handleErr(&err)
	o, err := ApplyOptions(opts...)
	if err != nil {
		return err
	}
	e := NewRepresenter(noWriter, o)
	defer e.Destroy()
	e.MarshalDoc("", reflect.ValueOf(v))
	e.Finish()
	p := NewComposer(e.Out)
	p.Textless = true
	defer p.Destroy()
	doc := p.Parse()
	*n = *doc.Content[0]
	return nil
}
