package format

import (
	"strings"

	"github.com/goccy/go-yaml/ast"
	"github.com/goccy/go-yaml/token"
)

func FormatNodeWithResolvedAlias(n ast.Node, anchorNodeMap map[string]ast.Node) string {
	tk := getFirstToken(n)
	if tk == nil {
		return ""
	}
	formatter := newFormatter(tk, hasComment(n))
	formatter.anchorNodeMap = anchorNodeMap
	return formatter.format(n)
}

func FormatNode(n ast.Node) string {
	tk := getFirstToken(n)
	if tk == nil {
		return ""
	}
	return newFormatter(tk, hasComment(n)).format(n)
}

func FormatFile(file *ast.File) string {
	if len(file.Docs) == 0 {
		return ""
	}
	tk := getFirstToken(file.Docs[0])
	if tk == nil {
		return ""
	}
	return newFormatter(tk, hasCommentFile(file)).formatFile(file)
}

func hasCommentFile(f *ast.File) bool {
	for _, doc := range f.Docs {
		if hasComment(doc.Body) {
			return true
		}
	}
	return false
}

func hasComment(n ast.Node) bool {
	if n == nil {
		return false
	}
	switch nn := n.(type) {
	case *ast.DocumentNode:
		return hasComment(nn.Body)
	case *ast.NullNode:
		return nn.Comment != nil
	case *ast.BoolNode:
		return nn.Comment != nil
	case *ast.IntegerNode:
		return nn.Comment != nil
	case *ast.FloatNode:
		return nn.Comment != nil
	case *ast.StringNode:
		return nn.Comment != nil
	case *ast.InfinityNode:
		return nn.Comment != nil
	case *ast.NanNode:
		return nn.Comment != nil
	case *ast.LiteralNode:
		return nn.Comment != nil
	case *ast.DirectiveNode:
		if nn.Comment != nil {
			return true
		}
		for _, value := range nn.Values {
			if hasComment(value) {
				return true
			}
		}
	case *ast.TagNode:
		if nn.Comment != nil {
			return true
		}
		return hasComment(nn.Value)
	case *ast.MappingNode:
		if nn.Comment != nil || nn.FootComment != nil {
			return true
		}
		for _, value := range nn.Values {
			if value.Comment != nil || value.FootComment != nil {
				return true
			}
			if hasComment(value.Key) {
				return true
			}
			if hasComment(value.Value) {
				return true
			}
		}
	case *ast.MappingKeyNode:
		return nn.Comment != nil
	case *ast.MergeKeyNode:
		return nn.Comment != nil
	case *ast.SequenceNode:
		if nn.Comment != nil || nn.FootComment != nil {
			return true
		}
		for _, entry := range nn.Entries {
			if entry.Comment != nil || entry.HeadComment != nil || entry.LineComment != nil {
				return true
			}
			if hasComment(entry.Value) {
				return true
			}
		}
	case *ast.AnchorNode:
		if nn.Comment != nil {
			return true
		}
		if hasComment(nn.Name) || hasComment(nn.Value) {
			return true
		}
	case *ast.AliasNode:
		if nn.Comment != nil {
			return true
		}
		if hasComment(nn.Value) {
			return true
		}
	}
	return false
}

func getFirstToken(n ast.Node) *token.Token {
	if n == nil {
		return nil
	}
	switch nn := n.(type) {
	case *ast.DocumentNode:
		if nn.Start != nil {
			return nn.Start
		}
		return getFirstToken(nn.Body)
	case *ast.NullNode:
		return nn.Token
	case *ast.BoolNode:
		return nn.Token
	case *ast.IntegerNode:
		return nn.Token
	case *ast.FloatNode:
		return nn.Token
	case *ast.StringNode:
		return nn.Token
	case *ast.InfinityNode:
		return nn.Token
	case *ast.NanNode:
		return nn.Token
	case *ast.LiteralNode:
		return nn.Start
	case *ast.DirectiveNode:
		return nn.Start
	case *ast.TagNode:
		return nn.Start
	case *ast.MappingNode:
		if nn.IsFlowStyle {
			return nn.Start
		}
		if len(nn.Values) == 0 {
			return nn.Start
		}
		return getFirstToken(nn.Values[0].Key)
	case *ast.MappingKeyNode:
		return nn.Start
	case *ast.MergeKeyNode:
		return nn.Token
	case *ast.SequenceNode:
		return nn.Start
	case *ast.AnchorNode:
		return nn.Start
	case *ast.AliasNode:
		return nn.Start
	}
	return nil
}

type Formatter struct {
	existsComment    bool
	tokenToOriginMap map[*token.Token]string
	anchorNodeMap    map[string]ast.Node
}

func newFormatter(tk *token.Token, existsComment bool) *Formatter {
	tokenToOriginMap := make(map[*token.Token]string)
	for tk.Prev != nil {
		tk = tk.Prev
	}
	tokenToOriginMap[tk] = tk.Origin

	var origin string
	for tk.Next != nil {
		tk = tk.Next
		if tk.Type == token.CommentType {
			origin += strings.Repeat("\n", strings.Count(normalizeNewLineChars(tk.Origin), "\n"))
			continue
		}
		origin += tk.Origin
		tokenToOriginMap[tk] = origin
		origin = ""
	}
	return &Formatter{
		existsComment:    existsComment,
		tokenToOriginMap: tokenToOriginMap,
	}
}

func getIndentNumByFirstLineToken(tk *token.Token) int {
	defaultIndent := tk.Position.Column - 1

	// key: value
	//    ^
	//   next
	if tk.Type == token.SequenceEntryType {
		// If the current token is the sequence entry.
		// the indent is calculated from the column value of the current token.
		return defaultIndent
	}

	// key: value
	//    ^
	//   next
	if tk.Next != nil && tk.Next.Type == token.MappingValueType {
		// If the current token is the key in the mapping-value,
		// the indent is calculated from the column value of the current token.
		return defaultIndent
	}

	if tk.Prev == nil {
		return defaultIndent
	}
	prev := tk.Prev

	// key: value
	//    ^
	//   prev
	if prev.Type == token.MappingValueType {
		// If the current token is the value in the mapping-value,
		// the indent is calculated from the column value of the key two steps back.
		if prev.Prev == nil {
			return defaultIndent
		}
		return prev.Prev.Position.Column - 1
	}

	// - value
	// ^
	// prev
	if prev.Type == token.SequenceEntryType {
		// If the value is not a mapping-value and the previous token was a sequence entry,
		// the indent is calculated using the column value of the sequence entry token.
		return prev.Position.Column - 1
	}

	return defaultIndent
}

func (f *Formatter) format(n ast.Node) string {
	return f.trimSpacePrefix(
		f.trimIndentSpace(
			getIndentNumByFirstLineToken(getFirstToken(n)),
			f.trimNewLineCharPrefix(f.formatNode(n)),
		),
	)
}

func (f *Formatter) formatFile(file *ast.File) string {
	if len(file.Docs) == 0 {
		return ""
	}
	var ret string
	for _, doc := range file.Docs {
		ret += f.formatDocument(doc)
	}
	return ret
}

func (f *Formatter) origin(tk *token.Token) string {
	if tk == nil {
		return ""
	}
	if f.existsComment {
		return tk.Origin
	}
	return f.tokenToOriginMap[tk]
}

func (f *Formatter) formatDocument(n *ast.DocumentNode) string {
	return f.origin(n.Start) + f.formatNode(n.Body) + f.origin(n.End)
}

func (f *Formatter) formatNull(n *ast.NullNode) string {
	return f.origin(n.Token) + f.formatCommentGroup(n.Comment)
}

func (f *Formatter) formatString(n *ast.StringNode) string {
	return f.origin(n.Token) + f.formatCommentGroup(n.Comment)
}

func (f *Formatter) formatInteger(n *ast.IntegerNode) string {
	return f.origin(n.Token) + f.formatCommentGroup(n.Comment)
}

func (f *Formatter) formatFloat(n *ast.FloatNode) string {
	return f.origin(n.Token) + f.formatCommentGroup(n.Comment)
}

func (f *Formatter) formatBool(n *ast.BoolNode) string {
	return f.origin(n.Token) + f.formatCommentGroup(n.Comment)
}

func (f *Formatter) formatInfinity(n *ast.InfinityNode) string {
	return f.origin(n.Token) + f.formatCommentGroup(n.Comment)
}

func (f *Formatter) formatNan(n *ast.NanNode) string {
	return f.origin(n.Token) + f.formatCommentGroup(n.Comment)
}

func (f *Formatter) formatLiteral(n *ast.LiteralNode) string {
	return f.origin(n.Start) + f.formatCommentGroup(n.Comment) + f.origin(n.Value.Token)
}

func (f *Formatter) formatMergeKey(n *ast.MergeKeyNode) string {
	return f.origin(n.Token)
}

func (f *Formatter) formatMappingValue(n *ast.MappingValueNode) string {
	return f.formatCommentGroup(n.Comment) +
		f.origin(n.Key.GetToken()) + ":" + f.formatCommentGroup(n.Key.GetComment()) + f.formatNode(n.Value) +
		f.formatCommentGroup(n.FootComment)
}

func (f *Formatter) formatDirective(n *ast.DirectiveNode) string {
	ret := f.origin(n.Start) + f.formatNode(n.Name)
	for _, val := range n.Values {
		ret += f.formatNode(val)
	}
	return ret
}

func (f *Formatter) formatMapping(n *ast.MappingNode) string {
	var ret string
	if n.IsFlowStyle {
		ret = f.origin(n.Start)
	} else {
		ret += f.formatCommentGroup(n.Comment)
	}
	for _, value := range n.Values {
		if value.CollectEntry != nil {
			ret += f.origin(value.CollectEntry)
		}
		ret += f.formatMappingValue(value)
	}
	if n.IsFlowStyle {
		ret += f.origin(n.End)
		ret += f.formatCommentGroup(n.Comment)
	}
	return ret
}

func (f *Formatter) formatTag(n *ast.TagNode) string {
	return f.origin(n.Start) + f.formatNode(n.Value)
}

func (f *Formatter) formatMappingKey(n *ast.MappingKeyNode) string {
	return f.origin(n.Start) + f.formatNode(n.Value)
}

func (f *Formatter) formatSequence(n *ast.SequenceNode) string {
	var ret string
	if n.IsFlowStyle {
		ret = f.origin(n.Start)
	} else {
		// add head comment.
		ret += f.formatCommentGroup(n.Comment)
	}
	for _, entry := range n.Entries {
		ret += f.formatNode(entry)
	}
	if n.IsFlowStyle {
		ret += f.origin(n.End)
		ret += f.formatCommentGroup(n.Comment)
	}
	ret += f.formatCommentGroup(n.FootComment)
	return ret
}

func (f *Formatter) formatSequenceEntry(n *ast.SequenceEntryNode) string {
	return f.formatCommentGroup(n.HeadComment) + f.origin(n.Start) + f.formatCommentGroup(n.LineComment) + f.formatNode(n.Value)
}

func (f *Formatter) formatAnchor(n *ast.AnchorNode) string {
	return f.origin(n.Start) + f.formatNode(n.Name) + f.formatNode(n.Value)
}

func (f *Formatter) formatAlias(n *ast.AliasNode) string {
	if f.anchorNodeMap != nil {
		anchorName := n.Value.GetToken().Value
		node := f.anchorNodeMap[anchorName]
		if node != nil {
			formatted := f.formatNode(node)
			// If formatted text contains newline characters, indentation needs to be considered.
			if strings.Contains(formatted, "\n") {
				// If the first character is not a newline, the first line should be output without indentation.
				isIgnoredFirstLine := !strings.HasPrefix(formatted, "\n")
				formatted = f.addIndentSpace(n.GetToken().Position.IndentNum, formatted, isIgnoredFirstLine)
			}
			return formatted
		}
	}
	return f.origin(n.Start) + f.formatNode(n.Value)
}

func (f *Formatter) formatNode(n ast.Node) string {
	switch nn := n.(type) {
	case *ast.DocumentNode:
		return f.formatDocument(nn)
	case *ast.NullNode:
		return f.formatNull(nn)
	case *ast.BoolNode:
		return f.formatBool(nn)
	case *ast.IntegerNode:
		return f.formatInteger(nn)
	case *ast.FloatNode:
		return f.formatFloat(nn)
	case *ast.StringNode:
		return f.formatString(nn)
	case *ast.InfinityNode:
		return f.formatInfinity(nn)
	case *ast.NanNode:
		return f.formatNan(nn)
	case *ast.LiteralNode:
		return f.formatLiteral(nn)
	case *ast.DirectiveNode:
		return f.formatDirective(nn)
	case *ast.TagNode:
		return f.formatTag(nn)
	case *ast.MappingNode:
		return f.formatMapping(nn)
	case *ast.MappingKeyNode:
		return f.formatMappingKey(nn)
	case *ast.MappingValueNode:
		return f.formatMappingValue(nn)
	case *ast.MergeKeyNode:
		return f.formatMergeKey(nn)
	case *ast.SequenceNode:
		return f.formatSequence(nn)
	case *ast.SequenceEntryNode:
		return f.formatSequenceEntry(nn)
	case *ast.AnchorNode:
		return f.formatAnchor(nn)
	case *ast.AliasNode:
		return f.formatAlias(nn)
	}
	return ""
}

func (f *Formatter) formatCommentGroup(g *ast.CommentGroupNode) string {
	if g == nil {
		return ""
	}
	var ret string
	for _, cm := range g.Comments {
		ret += f.formatComment(cm)
	}
	return ret
}

func (f *Formatter) formatComment(n *ast.CommentNode) string {
	if n == nil {
		return ""
	}
	return n.Token.Origin
}

// nolint: unused
func (f *Formatter) formatIndent(col int) string {
	if col <= 1 {
		return ""
	}
	return strings.Repeat(" ", col-1)
}

func (f *Formatter) trimNewLineCharPrefix(v string) string {
	return strings.TrimLeftFunc(v, func(r rune) bool {
		return r == '\n' || r == '\r'
	})
}

func (f *Formatter) trimSpacePrefix(v string) string {
	return strings.TrimLeftFunc(v, func(r rune) bool {
		return r == ' '
	})
}

func (f *Formatter) trimIndentSpace(trimIndentNum int, v string) string {
	if trimIndentNum == 0 {
		return v
	}
	lines := strings.Split(normalizeNewLineChars(v), "\n")
	out := make([]string, 0, len(lines))
	for _, line := range lines {
		var cnt int
		out = append(out, strings.TrimLeftFunc(line, func(r rune) bool {
			cnt++
			return r == ' ' && cnt <= trimIndentNum
		}))
	}
	return strings.Join(out, "\n")
}

func (f *Formatter) addIndentSpace(indentNum int, v string, isIgnoredFirstLine bool) string {
	if indentNum == 0 {
		return v
	}
	indent := strings.Repeat(" ", indentNum)
	lines := strings.Split(normalizeNewLineChars(v), "\n")
	out := make([]string, 0, len(lines))
	for idx, line := range lines {
		if line == "" || (isIgnoredFirstLine && idx == 0) {
			out = append(out, line)
			continue
		}
		out = append(out, indent+line)
	}
	return strings.Join(out, "\n")
}

// normalizeNewLineChars normalize CRLF and CR to LF.
func normalizeNewLineChars(v string) string {
	return strings.ReplaceAll(strings.ReplaceAll(v, "\r\n", "\n"), "\r", "\n")
}
