package jsbundler

// Top-level source segmentation. segmentTopLevel splits a module into contiguous
// chunks at top-level declaration boundaries; the Go Solid compiler uses it to
// find component declarations for solid-refresh instrumentation (wrapComponents
// in compile_solid_gen.go).
//
// Correctness contract: join(chunks) == src, always. A chunk always begins at a
// top-level declaration keyword and contains only whole top-level statements. The
// splitter is deliberately conservative — when the lexer is unsure it simply
// doesn't cut, producing fewer/larger chunks (still correct).

import "strings"

// declKeywords begin a top-level declaration. A line that starts (at brace depth
// 0, outside any string/comment/template/regex) with one of these — as a whole
// word — is a chunk boundary. `export` covers `export default`, `export const`,
// `export function`, and re-exports; `async` covers `async function`.
var declKeywords = []string{
	"import", "export", "const", "let", "var", "function",
	"async", "class", "type", "interface", "enum", "declare", "abstract",
}

// regexPrefixKeywords are the identifiers after which a `/` begins a regex
// literal rather than a division (e.g. `return /x/`), needed so the lexer keeps
// an accurate brace depth through regexes that contain braces or quotes.
var regexPrefixKeywords = map[string]bool{
	"return": true, "typeof": true, "instanceof": true, "in": true, "of": true,
	"new": true, "delete": true, "void": true, "do": true, "else": true,
	"yield": true, "await": true, "case": true,
}

// segmentTopLevel splits src into chunks whose concatenation is exactly src.
// Returns a single chunk (the whole source) when there is nothing safe to split.
func segmentTopLevel(src string) []string {
	cuts := topLevelCuts(src)
	if len(cuts) <= 1 {
		return []string{src}
	}
	chunks := make([]string, 0, len(cuts))
	for i := range cuts {
		end := len(src)
		if i+1 < len(cuts) {
			end = cuts[i+1]
		}
		chunks = append(chunks, src[cuts[i]:end])
	}
	// Defensive: the construction above is lossless, but never return a
	// non-lossless split — a single chunk is always safe.
	if strings.Join(chunks, "") != src {
		return []string{src}
	}
	return chunks
}

// lexical states
const (
	stNormal = iota
	stLineComment
	stBlockComment
	stSingle // '...'
	stDouble // "..."
	stTemplate
	stRegex
)

// topLevelCuts returns the sorted byte offsets at which chunks begin. Always
// includes 0. A cut is placed at the start of any line that begins in column 0
// (no leading whitespace) with a declaration keyword, provided the lexer is in a
// clean state there — i.e. not inside a block comment, template body, or `${}`
// interpolation that spans into this line.
//
// Column 0 is the top-level signal: these files indent everything inside a
// function/JSX, so a keyword in column 0 is a top-level declaration. That lets
// the lexer ignore brace depth and JSX entirely (JSX and nested statements are
// always indented) — it need only track string/comment/template state so a
// keyword *inside* a multi-line string or comment isn't mistaken for a boundary.
// Regexes and single/double strings can't span lines, so any mis-lex of them
// self-heals at the newline before the next candidate line.
func topLevelCuts(src string) []int {
	cuts := []int{0}
	n := len(src)

	state := stNormal
	depth := 0 // only tracked to match `${ ... }` interpolation braces
	// tmplStack holds the interpolation brace depth captured at each `${` so the
	// matching `}` resumes the template body instead of being counted as a plain
	// brace. Non-empty ⇒ we're inside an interpolation (line not a clean start).
	var tmplStack []int
	var prevSig byte // last significant byte, for regex-vs-division

	addCut := func(off int) {
		if off > cuts[len(cuts)-1] {
			cuts = append(cuts, off)
		}
	}
	// A newline just moved us to lineStart; if the lexer is clean there and the
	// line begins in column 0 with a declaration keyword, it's a chunk boundary.
	checkCut := func(lineStart int) {
		if state == stNormal && len(tmplStack) == 0 && startsDeclKeyword(src, lineStart) {
			addCut(lineStart)
		}
	}

	checkCut(0)

	for i := 0; 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
				}
			case ')', ']':
				if depth > 0 {
					depth--
				}
				prevSig = c
			case '\n':
				checkCut(i + 1)
			case ' ', '\t', '\r':
				// insignificant; leave prevSig
			default:
				prevSig = c
			}

		case stLineComment:
			if c == '\n' {
				state = stNormal
				checkCut(i + 1)
			}

		case stBlockComment:
			if c == '*' && i+1 < n && src[i+1] == '/' {
				state = stNormal
				i++
			}
			// a newline inside a block comment is not a clean start: no checkCut

		case stSingle:
			if c == '\\' {
				i++
			} else if c == '\'' {
				state = stNormal
				prevSig = c
			} else if c == '\n' {
				state = stNormal // strings can't span lines; recover
				checkCut(i + 1)
			}

		case stDouble:
			if c == '\\' {
				i++
			} else if c == '"' {
				state = stNormal
				prevSig = c
			} else if c == '\n' {
				state = stNormal
				checkCut(i + 1)
			}

		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++
			}
			// templates may span lines; the continuation is not a clean start

		case stRegex:
			if c == '\\' {
				i++
			} else if c == '[' {
				for i++; i < n; i++ { // character class: skip to `]`
					if src[i] == '\\' {
						i++
						continue
					}
					if src[i] == ']' {
						break
					}
				}
			} else if c == '/' {
				state = stNormal
				prevSig = c
			} else if c == '\n' {
				state = stNormal // regexes can't span lines; recover
				checkCut(i + 1)
			}
		}
	}

	return cuts
}

// startsDeclKeyword reports whether src[i:] begins with a declaration keyword as
// a whole word (the next character is not part of an identifier).
func startsDeclKeyword(src string, i int) bool {
	for _, kw := range declKeywords {
		if strings.HasPrefix(src[i:], kw) {
			j := i + len(kw)
			if j >= len(src) || !isIdentPart(src[j]) {
				return true
			}
		}
	}
	return false
}

// regexAllowed reports whether a `/` at position i begins a regex literal (as
// opposed to a division operator), from the preceding significant byte and, when
// that byte ends an identifier, whether the identifier is a regex-prefix keyword.
func regexAllowed(src string, i int, prevSig byte) bool {
	if prevSig == 0 {
		return true // start of input
	}
	if isIdentPart(prevSig) {
		// value context (identifier/number) unless the word is a keyword like
		// `return` after which a regex is expected.
		word := trailingWord(src, i)
		return regexPrefixKeywords[word]
	}
	switch prevSig {
	case ')', ']', '}':
		return false // end of a value/call/index
	default:
		// after operators, punctuation, `(`, `,`, `=`, etc. → regex expected
		return true
	}
}

// trailingWord returns the identifier word ending just before the run of
// whitespace that precedes position i (used to classify the token before a `/`).
func trailingWord(src string, i int) string {
	j := i
	for j > 0 && (src[j-1] == ' ' || src[j-1] == '\t' || src[j-1] == '\r' || src[j-1] == '\n') {
		j--
	}
	end := j
	for j > 0 && isIdentPart(src[j-1]) {
		j--
	}
	return src[j:end]
}

func isIdentPart(b byte) bool {
	return b == '_' || b == '$' ||
		(b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || (b >= '0' && b <= '9')
}
