package bundler

// Go-native Solid JSX compiler (replaces babel-preset-solid running in goja).
//
// Pipeline: esbuild strips TS types (JSX preserved), then this package parses the
// JSX trees out of the JS and rewrites each into Solid's dom-expressions runtime
// output (template cloning + fine-grained _$insert/_$effect/_$createComponent).
// JS expressions inside `{...}` are captured as opaque strings and, where they
// may contain nested JSX, recompiled recursively — so we never need a full JS
// parser, only a JSX-aware scanner.
//
// This file is the PARSER (JSX text -> tree). Codegen lives in compile_solid_gen.go.

import (
	"fmt"
	"strings"
)

type jsxKind int

const (
	jsxElement   jsxKind = iota // lowercase tag -> real DOM element (templated)
	jsxComponent                // Capitalized/dotted tag -> _$createComponent
	jsxFragment                 // <>...</>
	jsxText                     // literal character data between tags
	jsxExpr                     // {expr} — raw JS, may itself contain JSX
)

type attrKind int

const (
	attrStatic attrKind = iota // name="literal" or bare boolean
	attrExpr                   // name={expr}
	attrSpread                 // {...expr}
)

type jsxAttr struct {
	kind  attrKind
	name  string
	value string // literal string value (attrStatic with a value)
	expr  string // JS expression (attrExpr) or spread source (attrSpread)
	boolt bool   // bare boolean attribute (attrStatic, no `=`)
}

type jsxNode struct {
	kind     jsxKind
	tag      string
	attrs    []jsxAttr
	children []jsxNode
	text     string // jsxText
	expr     string // jsxExpr (raw, may contain nested JSX)

	marker bool // codegen: this dynamic child needs a `<!>` insert anchor
}

// parseJSX parses a JSX element/fragment beginning at src[i] == '<'. It returns
// the node and the index just past the element's closing `>`.
func parseJSX(src string, i int) (jsxNode, int, error) {
	n := len(src)
	if i >= n || src[i] != '<' {
		return jsxNode{}, 0, fmt.Errorf("parseJSX: expected '<' at %d", i)
	}
	i++ // consume '<'

	// Fragment: <> ... </>
	if i < n && src[i] == '>' {
		i++
		children, ci, err := parseChildren(src, i)
		if err != nil {
			return jsxNode{}, 0, err
		}
		i, err = consumeCloseTag(src, ci)
		if err != nil {
			return jsxNode{}, 0, err
		}
		return jsxNode{kind: jsxFragment, children: children}, i, nil
	}

	// Tag name.
	start := i
	for i < n && isTagChar(src[i]) {
		i++
	}
	if i == start {
		return jsxNode{}, 0, fmt.Errorf("parseJSX: empty tag name at %d", start)
	}
	node := jsxNode{tag: src[start:i]}
	if isComponentTag(node.tag) {
		node.kind = jsxComponent
	} else {
		node.kind = jsxElement
	}

	attrs, ai, selfClose, err := parseAttrs(src, i)
	if err != nil {
		return jsxNode{}, 0, err
	}
	node.attrs = attrs
	i = ai
	if selfClose {
		return node, i, nil
	}

	children, ci, err := parseChildren(src, i)
	if err != nil {
		return jsxNode{}, 0, err
	}
	node.children = children
	i, err = consumeCloseTag(src, ci)
	if err != nil {
		return jsxNode{}, 0, err
	}
	return node, i, nil
}

// parseAttrs parses attributes after the tag name until `>` or `/>`. It returns
// the attrs, the index past the terminator, and whether the tag self-closed.
func parseAttrs(src string, i int) ([]jsxAttr, int, bool, error) {
	n := len(src)
	var attrs []jsxAttr
	for i < n {
		for i < n && isSpace(src[i]) {
			i++
		}
		if i >= n {
			break
		}
		switch {
		case src[i] == '>':
			return attrs, i + 1, false, nil
		case src[i] == '/' && i+1 < n && src[i+1] == '>':
			return attrs, i + 2, true, nil
		case src[i] == '{': // {...spread}
			expr, ni := captureBraces(src, i)
			e := strings.TrimSpace(expr)
			e = strings.TrimSpace(strings.TrimPrefix(e, "..."))
			attrs = append(attrs, jsxAttr{kind: attrSpread, expr: e})
			i = ni
		default:
			ns := i
			for i < n && isAttrNameChar(src[i]) {
				i++
			}
			if i == ns {
				return nil, 0, false, fmt.Errorf("parseAttrs: unexpected %q at %d", src[i], i)
			}
			name := src[ns:i]
			for i < n && isSpace(src[i]) {
				i++
			}
			if i < n && src[i] == '=' {
				i++
				for i < n && isSpace(src[i]) {
					i++
				}
				if i >= n {
					return nil, 0, false, fmt.Errorf("parseAttrs: attr value expected")
				}
				if src[i] == '{' {
					expr, ni := captureBraces(src, i)
					attrs = append(attrs, jsxAttr{kind: attrExpr, name: name, expr: strings.TrimSpace(expr)})
					i = ni
				} else if src[i] == '"' || src[i] == '\'' {
					q := src[i]
					i++
					vs := i
					for i < n && src[i] != q {
						i++
					}
					attrs = append(attrs, jsxAttr{kind: attrStatic, name: name, value: src[vs:i]})
					if i < n {
						i++ // closing quote
					}
				} else {
					return nil, 0, false, fmt.Errorf("parseAttrs: bad attr value at %d", i)
				}
			} else {
				attrs = append(attrs, jsxAttr{kind: attrStatic, name: name, boolt: true})
			}
		}
	}
	return nil, 0, false, fmt.Errorf("parseAttrs: unterminated tag")
}

// parseChildren parses child nodes until the matching `</`. It returns the nodes
// and the index at the `<` of the closing tag. Adjacent character data becomes a
// single jsxText node (JSX whitespace normalization happens in codegen).
func parseChildren(src string, i int) ([]jsxNode, int, error) {
	n := len(src)
	var nodes []jsxNode
	var text strings.Builder
	flush := func() {
		if text.Len() > 0 {
			nodes = append(nodes, jsxNode{kind: jsxText, text: text.String()})
			text.Reset()
		}
	}
	for i < n {
		c := src[i]
		switch {
		case c == '<' && i+1 < n && src[i+1] == '/':
			flush()
			return nodes, i, nil
		case c == '<':
			flush()
			child, ni, err := parseJSX(src, i)
			if err != nil {
				return nil, 0, err
			}
			nodes = append(nodes, child)
			i = ni
		case c == '{':
			flush()
			expr, ni := captureBraces(src, i)
			nodes = append(nodes, jsxNode{kind: jsxExpr, expr: strings.TrimSpace(expr)})
			i = ni
		default:
			text.WriteByte(c)
			i++
		}
	}
	return nil, 0, fmt.Errorf("parseChildren: unterminated (missing close tag)")
}

// consumeCloseTag consumes `</name>` (or `</>`) starting at src[i] == '<'.
func consumeCloseTag(src string, i int) (int, error) {
	n := len(src)
	if i+1 >= n || src[i] != '<' || src[i+1] != '/' {
		return 0, fmt.Errorf("consumeCloseTag: expected '</' at %d", i)
	}
	i += 2
	for i < n && src[i] != '>' {
		i++
	}
	if i >= n {
		return 0, fmt.Errorf("consumeCloseTag: unterminated close tag")
	}
	return i + 1, nil // past '>'
}

// captureBraces returns the text between a `{` at src[i] and its matching `}`
// (exclusive), and the index just past that `}`. It tracks strings, template
// literals (with `${}` interpolation), comments, and regex literals so braces
// inside them don't miscount — the same lexer the segmenter uses.
func captureBraces(src string, i int) (inner string, next int) {
	n := len(src)
	start := i + 1
	i++ // skip opening '{'
	depth := 0
	state := stNormal
	var tmplStack []int
	var prevSig byte
	for ; i < n; i++ {
		c := src[i]
		switch state {
		case stNormal:
			switch c {
			case '/':
				if i+1 < n && src[i+1] == '/' {
					state = stLineComment
					i++
					continue
				}
				if i+1 < n && src[i+1] == '*' {
					state = stBlockComment
					i++
					continue
				}
				if regexAllowed(src, i, prevSig) {
					state = stRegex
					prevSig = c
					continue
				}
				prevSig = c
			case '\'':
				state = stSingle
				prevSig = c
			case '"':
				state = stDouble
				prevSig = c
			case '`':
				state = stTemplate
				prevSig = c
			case '{', '(', '[':
				depth++
				prevSig = c
			case '}':
				if len(tmplStack) > 0 && depth == tmplStack[len(tmplStack)-1] {
					tmplStack = tmplStack[:len(tmplStack)-1]
					depth--
					state = stTemplate
				} else if depth > 0 {
					depth--
					prevSig = c
				} else {
					return src[start:i], i + 1 // matching close of the outer '{'
				}
			case ')', ']':
				if depth > 0 {
					depth--
				}
				prevSig = c
			case '<':
				// A `<` in expression position (and followed by a tag/fragment
				// start) opens nested JSX, not a less-than: skip the whole element
				// via parseJSX so its `</tag>` slashes and `{}` don't desync the JS
				// lexer. Otherwise it's the comparison operator.
				if regexAllowed(src, i, prevSig) && i+1 < n && (isASCIILetter(src[i+1]) || src[i+1] == '>') {
					if _, ni, err := parseJSX(src, i); err == nil {
						i = ni - 1 // loop's i++ lands just past the element
						prevSig = '>'
						continue
					}
				}
				prevSig = c
			default:
				if !isSpace(c) {
					prevSig = c
				}
			}
		case stLineComment:
			if c == '\n' {
				state = stNormal
			}
		case stBlockComment:
			if c == '*' && i+1 < n && src[i+1] == '/' {
				state = stNormal
				i++
			}
		case stSingle:
			if c == '\\' {
				i++
			} else if c == '\'' {
				state = stNormal
				prevSig = c
			}
		case stDouble:
			if c == '\\' {
				i++
			} else if c == '"' {
				state = stNormal
				prevSig = c
			}
		case stTemplate:
			if c == '\\' {
				i++
			} else if c == '`' {
				state = stNormal
				prevSig = c
			} else if c == '$' && i+1 < n && src[i+1] == '{' {
				depth++
				tmplStack = append(tmplStack, depth)
				state = stNormal
				i++
			}
		case stRegex:
			if c == '\\' {
				i++
			} else if c == '[' {
				for i++; i < n; i++ {
					if src[i] == '\\' {
						i++
						continue
					}
					if src[i] == ']' {
						break
					}
				}
			} else if c == '/' {
				state = stNormal
				prevSig = c
			}
		}
	}
	return src[start:], n // unterminated
}

func isComponentTag(tag string) bool {
	if tag == "" {
		return false
	}
	if strings.ContainsAny(tag, ".") {
		return true // member expression component, e.g. <Foo.Bar>
	}
	c := tag[0]
	return c >= 'A' && c <= 'Z'
}

func isTagChar(b byte) bool {
	return isASCIILetter(b) || (b >= '0' && b <= '9') || b == '-' || b == '.' || b == ':' || b == '_'
}

func isAttrNameChar(b byte) bool {
	return isASCIILetter(b) || (b >= '0' && b <= '9') || b == '-' || b == ':' || b == '_'
}

func isSpace(b byte) bool {
	return b == ' ' || b == '\t' || b == '\n' || b == '\r'
}
