package scanner

import (
	"maps"
	"math"
	"strconv"
	"strings"
	"unicode/utf16"
	"unicode/utf8"

	"github.com/microsoft/typescript-go/internal/core"
	"github.com/microsoft/typescript-go/internal/debug"
	"github.com/microsoft/typescript-go/internal/diagnostics"
	"github.com/microsoft/typescript-go/internal/stringutil"
)

type regularExpressionFlags int32

const (
	regularExpressionFlagsNone           regularExpressionFlags = 0
	regularExpressionFlagsHasIndices     regularExpressionFlags = 1 << 0 // d
	regularExpressionFlagsGlobal         regularExpressionFlags = 1 << 1 // g
	regularExpressionFlagsIgnoreCase     regularExpressionFlags = 1 << 2 // i
	regularExpressionFlagsMultiline      regularExpressionFlags = 1 << 3 // m
	regularExpressionFlagsDotAll         regularExpressionFlags = 1 << 4 // s
	regularExpressionFlagsUnicode        regularExpressionFlags = 1 << 5 // u
	regularExpressionFlagsUnicodeSets    regularExpressionFlags = 1 << 6 // v
	regularExpressionFlagsSticky         regularExpressionFlags = 1 << 7 // y
	regularExpressionFlagsAnyUnicodeMode regularExpressionFlags = regularExpressionFlagsUnicode | regularExpressionFlagsUnicodeSets
	regularExpressionFlagsModifiers      regularExpressionFlags = regularExpressionFlagsIgnoreCase | regularExpressionFlagsMultiline | regularExpressionFlagsDotAll
)

var charCodeToRegExpFlag = map[rune]regularExpressionFlags{
	'd': regularExpressionFlagsHasIndices,
	'g': regularExpressionFlagsGlobal,
	'i': regularExpressionFlagsIgnoreCase,
	'm': regularExpressionFlagsMultiline,
	's': regularExpressionFlagsDotAll,
	'u': regularExpressionFlagsUnicode,
	'v': regularExpressionFlagsUnicodeSets,
	'y': regularExpressionFlagsSticky,
}

var regExpFlagToFirstAvailableLanguageVersion = map[regularExpressionFlags]core.ScriptTarget{
	regularExpressionFlagsHasIndices:  core.ScriptTargetES2022,
	regularExpressionFlagsDotAll:      core.ScriptTargetES2018,
	regularExpressionFlagsUnicodeSets: core.ScriptTargetES2024,
}

func (s *Scanner) checkRegularExpressionFlagAvailability(flag regularExpressionFlags, pos int, size int) {
	if availableFrom, ok := regExpFlagToFirstAvailableLanguageVersion[flag]; ok && s.languageVersion() < availableFrom {
		s.errorAt(diagnostics.This_regular_expression_flag_is_only_available_when_targeting_0_or_later, pos, size, strings.ToLower(availableFrom.String()))
	}
}

type classSetExpressionType int

const (
	classSetExpressionTypeUnknown classSetExpressionType = iota
	classSetExpressionTypeClassUnion
	classSetExpressionTypeClassIntersection
	classSetExpressionTypeClassSubtraction
)

type groupNameReference struct {
	pos  int
	end  int
	name string
}

type decimalEscapeValue struct {
	pos   int
	end   int
	value int
}

type regExpParser struct {
	scanner         *Scanner
	end             int
	regExpFlags     regularExpressionFlags
	anyUnicodeMode  bool
	unicodeSetsMode bool
	annexB          bool

	anyUnicodeModeOrNonAnnexB bool
	namedCaptureGroups        bool

	// See scanClassSetExpression.
	mayContainStrings bool
	// The number of all (named and unnamed) capturing groups defined in the regex.
	numberOfCapturingGroups int
	// All named capturing groups defined in the regex.
	groupSpecifiers map[string]bool
	// All references to named capturing groups in the regex.
	groupNameReferences []groupNameReference
	// All numeric backreferences within the regex.
	decimalEscapes []decimalEscapeValue
	// A stack of scopes for named capturing groups. See scanGroupName.
	namedCapturingGroups []map[string]bool

	// pendingLowSurrogate holds the low surrogate to emit on the next
	// scanSourceCharacter call when Corsa has to split a non-BMP rune into
	// UTF-16 surrogate code units in non-unicode mode. Strada did not need
	// this bookkeeping because its source text was already indexed as UTF-16.
	pendingLowSurrogate rune
}

func (p *regExpParser) pos() int {
	return p.scanner.pos
}

func (p *regExpParser) setPos(v int) {
	p.scanner.pos = v
}

func (p *regExpParser) incPos(n int) {
	p.scanner.pos += n
}

func (p *regExpParser) char() rune {
	return p.scanner.char()
}

func (p *regExpParser) charAt(pos int) rune {
	return p.scanner.charAt(pos - p.pos())
}

func (p *regExpParser) error(msg *diagnostics.Message, pos int, length int, args ...any) {
	p.scanner.errorAt(msg, pos, length, args...)
}

func (p *regExpParser) text() string {
	return p.scanner.text
}

func compareDecimalStrings(a string, b string) int {
	a = strings.TrimLeft(a, "0")
	b = strings.TrimLeft(b, "0")
	if a == "" {
		a = "0"
	}
	if b == "" {
		b = "0"
	}
	if len(a) != len(b) {
		if len(a) < len(b) {
			return -1
		}
		return 1
	}
	return strings.Compare(a, b)
}

// Disjunction ::= Alternative ('|' Alternative)*
func (p *regExpParser) scanDisjunction(isInGroup bool) {
	for {
		p.namedCapturingGroups = append(p.namedCapturingGroups, make(map[string]bool))
		p.scanAlternative(isInGroup)
		p.namedCapturingGroups = p.namedCapturingGroups[:len(p.namedCapturingGroups)-1]
		if p.char() != '|' {
			return
		}
		p.incPos(1)
	}
}

// Alternative ::= Term*
// Term ::=
//
//	| Assertion
//	| Atom Quantifier?
//
// Assertion ::=
//
//	| '^'
//	| '$'
//	| '\b'
//	| '\B'
//	| '(?=' Disjunction ')'
//	| '(?!' Disjunction ')'
//	| '(?<=' Disjunction ')'
//	| '(?<!' Disjunction ')'
//
// Quantifier ::= QuantifierPrefix '?'?
// QuantifierPrefix ::=
//
//	| '*'
//	| '+'
//	| '?'
//	| '{' DecimalDigits (',' DecimalDigits?)? '}'
//
// Atom ::=
//
//	| PatternCharacter
//	| '.'
//	| '\' AtomEscape
//	| CharacterClass
//	| '(?<' RegExpIdentifierName '>' Disjunction ')'
//	| '(?' RegularExpressionFlags ('-' RegularExpressionFlags)? ':' Disjunction ')'
//
// CharacterClass ::= unicodeMode
//
//	? '[' ClassRanges ']'
//	: '[' ClassSetExpression ']'
func (p *regExpParser) scanAlternative(isInGroup bool) {
	isPreviousTermQuantifiable := false
	for p.pos() < p.end {
		start := p.pos()
		ch := p.char()
		switch ch {
		case '^', '$':
			p.incPos(1)
			isPreviousTermQuantifiable = false
		case '\\':
			p.incPos(1)
			switch p.char() {
			case 'b', 'B':
				p.incPos(1)
				isPreviousTermQuantifiable = false
			default:
				p.scanAtomEscape()
				isPreviousTermQuantifiable = true
			}
		case '(':
			p.incPos(1)
			if p.char() == '?' {
				p.incPos(1)
				switch p.char() {
				case '=', '!':
					p.incPos(1)
					// In Annex B, `(?=Disjunction)` and `(?!Disjunction)` are quantifiable
					isPreviousTermQuantifiable = !p.anyUnicodeModeOrNonAnnexB
				case '<':
					groupNameStart := p.pos()
					p.incPos(1)
					switch p.char() {
					case '=', '!':
						p.incPos(1)
						isPreviousTermQuantifiable = false
					default:
						p.scanGroupName(false /*isReference*/)
						p.scanExpectedChar('>')
						if p.scanner.languageVersion() < core.ScriptTargetES2018 {
							p.error(diagnostics.Named_capturing_groups_are_only_available_when_targeting_ES2018_or_later, groupNameStart, p.pos()-groupNameStart)
						}
						p.numberOfCapturingGroups++
						isPreviousTermQuantifiable = true
					}
				default:
					flagsStart := p.pos()
					setFlags := p.scanPatternModifiers(regularExpressionFlagsNone)
					if p.char() == '-' {
						p.incPos(1)
						p.scanPatternModifiers(setFlags)
						if p.pos() == flagsStart+1 {
							p.error(diagnostics.Subpattern_flags_must_be_present_when_there_is_a_minus_sign, flagsStart, p.pos()-flagsStart)
						}
					}
					p.scanExpectedChar(':')
					isPreviousTermQuantifiable = true
				}
			} else {
				p.numberOfCapturingGroups++
				isPreviousTermQuantifiable = true
			}
			p.scanDisjunction(true /*isInGroup*/)
			p.scanExpectedChar(')')
		case '{':
			p.incPos(1)
			digitsStart := p.pos()
			p.scanDigits()
			minStr := p.scanner.tokenValue
			if !p.anyUnicodeModeOrNonAnnexB && minStr == "" {
				isPreviousTermQuantifiable = true
				continue
			}
			if p.char() == ',' {
				p.incPos(1)
				p.scanDigits()
				maxStr := p.scanner.tokenValue
				if minStr == "" {
					if maxStr != "" || p.char() == '}' {
						p.error(diagnostics.Incomplete_quantifier_Digit_expected, digitsStart, 0)
					} else {
						p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, start, 1, string(ch))
						isPreviousTermQuantifiable = true
						continue
					}
				} else if maxStr != "" {
					if compareDecimalStrings(minStr, maxStr) > 0 && (p.anyUnicodeModeOrNonAnnexB || p.char() == '}') {
						p.error(diagnostics.Numbers_out_of_order_in_quantifier, digitsStart, p.pos()-digitsStart)
					}
				}
			} else if minStr == "" {
				if p.anyUnicodeModeOrNonAnnexB {
					p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, start, 1, string(ch))
				}
				isPreviousTermQuantifiable = true
				continue
			}
			if p.char() != '}' {
				if p.anyUnicodeModeOrNonAnnexB {
					p.error(diagnostics.X_0_expected, p.pos(), 0, "}")
					p.incPos(-1)
				} else {
					isPreviousTermQuantifiable = true
					continue
				}
			}
			fallthrough
		case '*', '+', '?':
			p.incPos(1)
			if p.char() == '?' {
				// Non-greedy
				p.incPos(1)
			}
			if !isPreviousTermQuantifiable {
				p.error(diagnostics.There_is_nothing_available_for_repetition, start, p.pos()-start)
			}
			isPreviousTermQuantifiable = false
		case '.':
			p.incPos(1)
			isPreviousTermQuantifiable = true
		case '[':
			p.incPos(1)
			if p.unicodeSetsMode {
				p.scanClassSetExpression()
			} else {
				p.scanClassRanges()
				p.pendingLowSurrogate = 0
			}
			p.scanExpectedChar(']')
			isPreviousTermQuantifiable = true
		case ')':
			if isInGroup {
				return
			}
			fallthrough
		case ']', '}':
			if p.anyUnicodeModeOrNonAnnexB || ch == ')' {
				p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, p.pos(), 1, string(ch))
			}
			p.incPos(1)
			isPreviousTermQuantifiable = true
		case '/', '|':
			return
		default:
			p.scanSourceCharacter()
			isPreviousTermQuantifiable = true
		}
	}
}

func (p *regExpParser) scanPatternModifiers(currFlags regularExpressionFlags) regularExpressionFlags {
	for p.pos() < p.end {
		ch, size := utf8.DecodeRuneInString(p.text()[p.pos():])
		if ch == utf8.RuneError || !IsIdentifierPart(ch) {
			break
		}
		flag, ok := charCodeToRegExpFlag[ch]
		if !ok {
			p.error(diagnostics.Unknown_regular_expression_flag, p.pos(), size)
		} else if currFlags&flag != 0 {
			p.error(diagnostics.Duplicate_regular_expression_flag, p.pos(), size)
		} else if flag&regularExpressionFlagsModifiers == 0 {
			p.error(diagnostics.This_regular_expression_flag_cannot_be_toggled_within_a_subpattern, p.pos(), size)
		} else {
			currFlags |= flag
			p.scanner.checkRegularExpressionFlagAvailability(flag, p.pos(), size)
		}
		p.incPos(size)
	}
	return currFlags
}

// AtomEscape ::=
//
//	| DecimalEscape
//	| CharacterClassEscape
//	| CharacterEscape
//	| 'k<' RegExpIdentifierName '>'
func (p *regExpParser) scanAtomEscape() {
	debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '\\')
	switch p.char() {
	case 'k':
		p.incPos(1)
		if p.char() == '<' {
			p.incPos(1)
			p.scanGroupName(true /*isReference*/)
			p.scanExpectedChar('>')
		} else if p.anyUnicodeModeOrNonAnnexB || p.namedCaptureGroups {
			p.error(diagnostics.X_k_must_be_followed_by_a_capturing_group_name_enclosed_in_angle_brackets, p.pos()-2, 2)
		}
	case 'q':
		if p.unicodeSetsMode {
			p.incPos(1)
			p.error(diagnostics.X_q_is_only_available_inside_character_class, p.pos()-2, 2)
			return
		}
		fallthrough
	default:
		if !p.scanCharacterClassEscape() && !p.scanDecimalEscape() {
			// Regex literals cannot contain line breaks here, so a character escape must consume something.
			debug.Assert(p.scanCharacterEscape(true /*atomEscape*/) != "")
		}
	}
}

// DecimalEscape ::= [1-9] [0-9]*
func (p *regExpParser) scanDecimalEscape() bool {
	debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '\\')
	ch := p.char()
	if ch >= '1' && ch <= '9' {
		start := p.pos()
		p.scanDigits()
		val, err := strconv.Atoi(p.scanner.tokenValue)
		if err != nil {
			val = math.MaxInt
		}
		p.decimalEscapes = append(p.decimalEscapes, decimalEscapeValue{pos: start, end: p.pos(), value: val})
		return true
	}
	return false
}

// CharacterEscape ::=
//
//	| `c` ControlLetter
//	| IdentityEscape
//	| (Other sequences handled by `scanEscapeSequence`)
//
// IdentityEscape ::=
//
//	| '^' | '$' | '/' | '\' | '.' | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|'
//	| [~AnyUnicodeMode] (any other non-identifier characters)
func (p *regExpParser) scanCharacterEscape(atomEscape bool) string {
	debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '\\')
	ch := p.char()
	switch ch {
	case -1:
		p.error(diagnostics.Undetermined_character_escape, p.pos()-1, 1)
		return "\\"
	case 'c':
		p.incPos(1)
		ch = p.char()
		if stringutil.IsASCIILetter(ch) {
			p.incPos(1)
			return string(ch & 0x1f)
		}
		if p.anyUnicodeModeOrNonAnnexB {
			p.error(diagnostics.X_c_must_be_followed_by_an_ASCII_letter, p.pos()-2, 2)
		} else if atomEscape {
			p.incPos(-1)
			return "\\"
		}
		return string(ch)
	case '^', '$', '/', '\\', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '|':
		p.incPos(1)
		return string(ch)
	default:
		p.incPos(-1) // back up to include the backslash for scanEscapeSequence
		flags := EscapeSequenceScanningFlagsRegularExpression
		if p.annexB {
			flags |= EscapeSequenceScanningFlagsAnnexB
		}
		if p.anyUnicodeMode {
			flags |= EscapeSequenceScanningFlagsAnyUnicodeMode
		}
		if atomEscape {
			flags |= EscapeSequenceScanningFlagsAtomEscape
		}
		return p.scanner.scanEscapeSequence(flags)
	}
}

func (p *regExpParser) scanGroupName(isReference bool) {
	debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '<')
	p.scanner.tokenStart = p.pos()
	p.scanner.scanIdentifier(0)
	if p.pos() == p.scanner.tokenStart {
		p.error(diagnostics.Expected_a_capturing_group_name, p.pos(), 0)
	} else if isReference {
		p.groupNameReferences = append(p.groupNameReferences, groupNameReference{pos: p.scanner.tokenStart, end: p.pos(), name: p.scanner.tokenValue})
	} else if p.namedCapturingGroupsContains(p.scanner.tokenValue) {
		p.error(diagnostics.Named_capturing_groups_with_the_same_name_must_be_mutually_exclusive_to_each_other, p.scanner.tokenStart, p.pos()-p.scanner.tokenStart)
	} else {
		if len(p.namedCapturingGroups) > 0 {
			p.namedCapturingGroups[len(p.namedCapturingGroups)-1][p.scanner.tokenValue] = true
		}
		p.groupSpecifiers[p.scanner.tokenValue] = true
	}
}

func (p *regExpParser) namedCapturingGroupsContains(name string) bool {
	for _, group := range p.namedCapturingGroups {
		if group[name] {
			return true
		}
	}
	return false
}

func (p *regExpParser) isClassContentExit(ch rune) bool {
	return ch == ']' || p.pos() >= p.end
}

// ClassRanges ::= '^'? (ClassAtom ('-' ClassAtom)?)*
func (p *regExpParser) scanClassRanges() {
	debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '[')
	p.pendingLowSurrogate = 0
	if p.char() == '^' {
		p.incPos(1)
	}
	for p.pos() < p.end {
		ch := p.char()
		if p.isClassContentExit(ch) {
			return
		}
		minStart := p.pos()
		minCharacter := p.scanClassAtom()
		if p.char() == '-' {
			p.incPos(1)
			ch = p.char()
			if p.isClassContentExit(ch) {
				return
			}
			if minCharacter == "" && p.anyUnicodeModeOrNonAnnexB {
				p.error(diagnostics.A_character_class_range_must_not_be_bounded_by_another_character_class, minStart, p.pos()-1-minStart)
			}
			maxStart := p.pos()
			maxCharacter := p.scanClassAtom()
			if maxCharacter == "" && p.anyUnicodeModeOrNonAnnexB {
				p.error(diagnostics.A_character_class_range_must_not_be_bounded_by_another_character_class, maxStart, p.pos()-maxStart)
				continue
			}
			if minCharacter == "" {
				continue
			}
			minCharacterValue, minSize := stringutil.DecodeJSStringRune(minCharacter)
			maxCharacterValue, maxSize := stringutil.DecodeJSStringRune(maxCharacter)
			if len(minCharacter) == minSize && len(maxCharacter) == maxSize && minCharacterValue > maxCharacterValue {
				p.error(diagnostics.Range_out_of_order_in_character_class, minStart, p.pos()-minStart)
			}
		}
	}
}

// Static Semantics: MayContainStrings
//     ClassUnion: ClassSetOperands.some(ClassSetOperand => ClassSetOperand.MayContainStrings)
//     ClassIntersection: ClassSetOperands.every(ClassSetOperand => ClassSetOperand.MayContainStrings)
//     ClassSubtraction: ClassSetOperands[0].MayContainStrings
//     ClassSetOperand:
//         || ClassStringDisjunctionContents.MayContainStrings
//         || CharacterClassEscape.UnicodePropertyValueExpression.LoneUnicodePropertyNameOrValue.MayContainStrings
//     ClassStringDisjunctionContents: ClassStrings.some(ClassString => ClassString.ClassSetCharacters.length !== 1)
//     LoneUnicodePropertyNameOrValue: isBinaryUnicodePropertyOfStrings(LoneUnicodePropertyNameOrValue)

// ClassSetExpression ::= '^'? (ClassUnion | ClassIntersection | ClassSubtraction)
// ClassUnion ::= (ClassSetRange | ClassSetOperand)*
// ClassIntersection ::= ClassSetOperand ('&&' ClassSetOperand)+
// ClassSubtraction ::= ClassSetOperand ('--' ClassSetOperand)+
// ClassSetRange ::= ClassSetCharacter '-' ClassSetCharacter
func (p *regExpParser) scanClassSetExpression() {
	debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '[')
	isCharacterComplement := false
	if p.char() == '^' {
		p.incPos(1)
		isCharacterComplement = true
	}
	expressionMayContainStrings := false
	ch := p.char()
	if p.isClassContentExit(ch) {
		return
	}
	start := p.pos()
	var operand string
	twoChars := ""
	if p.pos()+1 < p.end {
		twoChars = p.text()[p.pos() : p.pos()+2]
	}
	switch twoChars {
	case "--", "&&":
		p.error(diagnostics.Expected_a_class_set_operand, p.pos(), 0)
		p.mayContainStrings = false
	default:
		operand = p.scanClassSetOperand()
	}
	switch p.char() {
	case '-':
		if p.pos()+1 < p.end && p.charAt(p.pos()+1) == '-' {
			if isCharacterComplement && p.mayContainStrings {
				p.error(diagnostics.Anything_that_would_possibly_match_more_than_a_single_character_is_invalid_inside_a_negated_character_class, start, p.pos()-start)
			}
			expressionMayContainStrings = p.mayContainStrings
			p.scanClassSetSubExpression(classSetExpressionTypeClassSubtraction)
			p.mayContainStrings = !isCharacterComplement && expressionMayContainStrings
			return
		}
	case '&':
		if p.pos()+1 < p.end && p.charAt(p.pos()+1) == '&' {
			p.scanClassSetSubExpression(classSetExpressionTypeClassIntersection)
			if isCharacterComplement && p.mayContainStrings {
				p.error(diagnostics.Anything_that_would_possibly_match_more_than_a_single_character_is_invalid_inside_a_negated_character_class, start, p.pos()-start)
			}
			expressionMayContainStrings = p.mayContainStrings
			p.mayContainStrings = !isCharacterComplement && expressionMayContainStrings
			return
		} else {
			p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, p.pos(), 1, string(ch))
		}
	default:
		if isCharacterComplement && p.mayContainStrings {
			p.error(diagnostics.Anything_that_would_possibly_match_more_than_a_single_character_is_invalid_inside_a_negated_character_class, start, p.pos()-start)
		}
		expressionMayContainStrings = p.mayContainStrings
	}
	for p.pos() < p.end {
		ch = p.char()
		switch ch {
		case '-':
			p.incPos(1)
			ch = p.char()
			if p.isClassContentExit(ch) {
				p.mayContainStrings = !isCharacterComplement && expressionMayContainStrings
				return
			}
			if ch == '-' {
				p.incPos(1)
				p.error(diagnostics.Operators_must_not_be_mixed_within_a_character_class_Wrap_it_in_a_nested_class_instead, p.pos()-2, 2)
				start = p.pos() - 2
				operand = p.text()[start:p.pos()]
				continue
			} else {
				if operand == "" {
					p.error(diagnostics.A_character_class_range_must_not_be_bounded_by_another_character_class, start, p.pos()-1-start)
				}
				secondStart := p.pos()
				secondOperand := p.scanClassSetOperand()
				if isCharacterComplement && p.mayContainStrings {
					p.error(diagnostics.Anything_that_would_possibly_match_more_than_a_single_character_is_invalid_inside_a_negated_character_class, secondStart, p.pos()-secondStart)
				}
				expressionMayContainStrings = expressionMayContainStrings || p.mayContainStrings
				if secondOperand == "" {
					p.error(diagnostics.A_character_class_range_must_not_be_bounded_by_another_character_class, secondStart, p.pos()-secondStart)
				} else if operand != "" {
					minCharacterValue, minSize := stringutil.DecodeJSStringRune(operand)
					maxCharacterValue, maxSize := stringutil.DecodeJSStringRune(secondOperand)
					if len(operand) == minSize && len(secondOperand) == maxSize && minCharacterValue > maxCharacterValue {
						p.error(diagnostics.Range_out_of_order_in_character_class, start, p.pos()-start)
					}
				}
			}
		case '&':
			start = p.pos()
			p.incPos(1)
			if p.char() == '&' {
				p.incPos(1)
				p.error(diagnostics.Operators_must_not_be_mixed_within_a_character_class_Wrap_it_in_a_nested_class_instead, p.pos()-2, 2)
				if p.char() == '&' {
					p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, p.pos(), 1, string(ch))
					p.incPos(1)
				}
			} else {
				p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, p.pos()-1, 1, string(ch))
			}
			operand = p.text()[start:p.pos()]
			continue
		}
		if p.isClassContentExit(p.char()) {
			break
		}
		start = p.pos()
		twoChars = ""
		if p.pos()+1 < p.end {
			twoChars = p.text()[p.pos() : p.pos()+2]
		}
		switch twoChars {
		case "--", "&&":
			p.error(diagnostics.Operators_must_not_be_mixed_within_a_character_class_Wrap_it_in_a_nested_class_instead, p.pos(), 2)
			p.incPos(2)
			operand = p.text()[start:p.pos()]
		default:
			operand = p.scanClassSetOperand()
		}
	}
	p.mayContainStrings = !isCharacterComplement && expressionMayContainStrings
}

func (p *regExpParser) scanClassSetSubExpression(expressionType classSetExpressionType) {
	expressionMayContainStrings := p.mayContainStrings
	for p.pos() < p.end {
		ch := p.char()
		if p.isClassContentExit(ch) {
			break
		}
		switch ch {
		case '-':
			p.incPos(1)
			if p.char() == '-' {
				p.incPos(1)
				if expressionType != classSetExpressionTypeClassSubtraction {
					p.error(diagnostics.Operators_must_not_be_mixed_within_a_character_class_Wrap_it_in_a_nested_class_instead, p.pos()-2, 2)
				}
			} else {
				p.error(diagnostics.Operators_must_not_be_mixed_within_a_character_class_Wrap_it_in_a_nested_class_instead, p.pos()-1, 1)
			}
		case '&':
			p.incPos(1)
			if p.char() == '&' {
				p.incPos(1)
				if expressionType != classSetExpressionTypeClassIntersection {
					p.error(diagnostics.Operators_must_not_be_mixed_within_a_character_class_Wrap_it_in_a_nested_class_instead, p.pos()-2, 2)
				}
				if p.char() == '&' {
					p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, p.pos(), 1, string(ch))
					p.incPos(1)
				}
			} else {
				p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, p.pos()-1, 1, string(ch))
			}
		default:
			switch expressionType {
			case classSetExpressionTypeClassSubtraction:
				p.error(diagnostics.X_0_expected, p.pos(), 0, "--")
			case classSetExpressionTypeClassIntersection:
				p.error(diagnostics.X_0_expected, p.pos(), 0, "&&")
			}
		}
		ch = p.char()
		if p.isClassContentExit(ch) {
			p.error(diagnostics.Expected_a_class_set_operand, p.pos(), 0)
			break
		}
		p.scanClassSetOperand()
		if expressionType == classSetExpressionTypeClassIntersection {
			expressionMayContainStrings = expressionMayContainStrings && p.mayContainStrings
		}
	}
	p.mayContainStrings = expressionMayContainStrings
}

// ClassSetOperand ::=
//
//	| '[' ClassSetExpression ']'
//	| '\' CharacterClassEscape
//	| '\q{' ClassStringDisjunctionContents '}'
//	| ClassSetCharacter
func (p *regExpParser) scanClassSetOperand() string {
	p.mayContainStrings = false
	switch p.char() {
	case '[':
		p.incPos(1)
		p.scanClassSetExpression()
		p.scanExpectedChar(']')
		return ""
	case '\\':
		p.incPos(1)
		if p.scanCharacterClassEscape() {
			return ""
		} else if p.char() == 'q' {
			p.incPos(1)
			if p.char() == '{' {
				p.incPos(1)
				p.scanClassStringDisjunctionContents()
				p.scanExpectedChar('}')
				return ""
			} else {
				p.error(diagnostics.X_q_must_be_followed_by_string_alternatives_enclosed_in_braces, p.pos()-2, 2)
				return "q"
			}
		}
		p.incPos(-1)
		fallthrough
	default:
		return p.scanClassSetCharacter()
	}
}

// ClassStringDisjunctionContents ::= ClassSetCharacter* ('|' ClassSetCharacter*)*
func (p *regExpParser) scanClassStringDisjunctionContents() {
	debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '{')
	characterCount := 0
	for p.pos() < p.end {
		ch := p.char()
		switch ch {
		case '}':
			if characterCount != 1 {
				p.mayContainStrings = true
			}
			return
		case '|':
			if characterCount != 1 {
				p.mayContainStrings = true
			}
			p.incPos(1)
			characterCount = 0
		default:
			p.scanClassSetCharacter()
			characterCount++
		}
	}
}

// ClassSetCharacter ::=
//
//	| SourceCharacter -- ClassSetSyntaxCharacter -- ClassSetReservedDoublePunctuator
//	| '\' (CharacterEscape | ClassSetReservedPunctuator | 'b')
func (p *regExpParser) scanClassSetCharacter() string {
	ch := p.char()
	if ch == '\\' {
		p.incPos(1)
		innerCh := p.char()
		switch innerCh {
		case 'b':
			p.incPos(1)
			return "\b"
		case '&', '-', '!', '#', '%', ',', ':', ';', '<', '=', '>', '@', '`', '~':
			p.incPos(1)
			return string(innerCh)
		default:
			return p.scanCharacterEscape(false /*atomEscape*/)
		}
	} else if p.pos()+1 < p.end && ch == p.charAt(p.pos()+1) {
		switch ch {
		case '&', '!', '#', '%', '*', '+', ',', '.', ':', ';', '<', '=', '>', '?', '@', '`', '~':
			p.error(diagnostics.A_character_class_must_not_contain_a_reserved_double_punctuator_Did_you_mean_to_escape_it_with_backslash, p.pos(), 2)
			p.incPos(2)
			return p.text()[p.pos()-2 : p.pos()]
		}
	}
	switch ch {
	case '/', '(', ')', '[', ']', '{', '}', '-', '|':
		p.error(diagnostics.Unexpected_0_Did_you_mean_to_escape_it_with_backslash, p.pos(), 1, string(ch))
		p.incPos(1)
		return string(ch)
	}
	return p.scanSourceCharacter()
}

// ClassAtom ::=
//
//	| SourceCharacter but not one of '\' or ']'
//	| '\' ClassEscape
//
// ClassEscape ::=
//
//	| 'b'
//	| '-'
//	| CharacterClassEscape
//	| CharacterEscape
func (p *regExpParser) scanClassAtom() string {
	if p.char() == '\\' {
		p.incPos(1)
		ch := p.char()
		switch ch {
		case 'b':
			p.incPos(1)
			return "\b"
		case '-':
			p.incPos(1)
			return string(ch)
		default:
			if p.scanCharacterClassEscape() {
				return ""
			}
			return p.scanCharacterEscape(false /*atomEscape*/)
		}
	} else {
		return p.scanSourceCharacter()
	}
}

// CharacterClassEscape ::=
//
//	| 'd' | 'D' | 's' | 'S' | 'w' | 'W'
//	| [+AnyUnicodeMode] ('P' | 'p') '{' UnicodePropertyValueExpression '}'
func (p *regExpParser) scanCharacterClassEscape() bool {
	debug.Assert(p.pos() > 0 && p.text()[p.pos()-1] == '\\')
	isCharacterComplement := false
	start := p.pos() - 1
	ch := p.char()
	switch ch {
	case 'd', 'D', 's', 'S', 'w', 'W':
		p.incPos(1)
		return true
	case 'P':
		isCharacterComplement = true
		fallthrough
	case 'p':
		p.incPos(1)
		if p.char() == '{' {
			p.incPos(1)
			propertyNameOrValueStart := p.pos()
			propertyNameOrValue := p.scanWordCharacters()
			if p.char() == '=' {
				propertyName := nonBinaryUnicodeProperties[propertyNameOrValue]
				if p.pos() == propertyNameOrValueStart {
					p.error(diagnostics.Expected_a_Unicode_property_name, p.pos(), 0)
				} else if propertyName == "" {
					p.error(diagnostics.Unknown_Unicode_property_name, propertyNameOrValueStart, p.pos()-propertyNameOrValueStart)
					suggestion := p.getSpellingSuggestionForUnicodePropertyName(propertyNameOrValue)
					if suggestion != "" {
						p.error(diagnostics.Did_you_mean_0, propertyNameOrValueStart, p.pos()-propertyNameOrValueStart, suggestion)
					}
				}
				p.incPos(1)
				propertyValueStart := p.pos()
				propertyValue := p.scanWordCharacters()
				if p.pos() == propertyValueStart {
					p.error(diagnostics.Expected_a_Unicode_property_value, p.pos(), 0)
				} else if propertyName != "" {
					values := valuesOfNonBinaryUnicodeProperties[propertyName]
					if values != nil && !values.Has(propertyValue) {
						p.error(diagnostics.Unknown_Unicode_property_value, propertyValueStart, p.pos()-propertyValueStart)
						suggestion := p.getSpellingSuggestionForUnicodePropertyValue(propertyName, propertyValue)
						if suggestion != "" {
							p.error(diagnostics.Did_you_mean_0, propertyValueStart, p.pos()-propertyValueStart, suggestion)
						}
					}
				}
			} else {
				if p.pos() == propertyNameOrValueStart {
					p.error(diagnostics.Expected_a_Unicode_property_name_or_value, p.pos(), 0)
				} else if binaryUnicodePropertiesOfStrings.Has(propertyNameOrValue) {
					if !p.unicodeSetsMode {
						p.error(diagnostics.Any_Unicode_property_that_would_possibly_match_more_than_a_single_character_is_only_available_when_the_Unicode_Sets_v_flag_is_set, propertyNameOrValueStart, p.pos()-propertyNameOrValueStart)
					} else if isCharacterComplement {
						p.error(diagnostics.Anything_that_would_possibly_match_more_than_a_single_character_is_invalid_inside_a_negated_character_class, propertyNameOrValueStart, p.pos()-propertyNameOrValueStart)
					} else {
						p.mayContainStrings = true
					}
				} else if !valuesOfNonBinaryUnicodeProperties["General_Category"].Has(propertyNameOrValue) && !binaryUnicodeProperties.Has(propertyNameOrValue) {
					p.error(diagnostics.Unknown_Unicode_property_name_or_value, propertyNameOrValueStart, p.pos()-propertyNameOrValueStart)
					suggestion := p.getSpellingSuggestionForUnicodePropertyNameOrValue(propertyNameOrValue)
					if suggestion != "" {
						p.error(diagnostics.Did_you_mean_0, propertyNameOrValueStart, p.pos()-propertyNameOrValueStart, suggestion)
					}
				}
			}
			p.scanExpectedChar('}')
			if !p.anyUnicodeMode {
				p.error(diagnostics.Unicode_property_value_expressions_are_only_available_when_the_Unicode_u_flag_or_the_Unicode_Sets_v_flag_is_set, start, p.pos()-start)
			}
		} else if p.anyUnicodeModeOrNonAnnexB {
			p.error(diagnostics.X_0_must_be_followed_by_a_Unicode_property_value_expression_enclosed_in_braces, p.pos()-2, 2, string(ch))
		} else {
			p.incPos(-1)
			return false
		}
		return true
	}
	return false
}

func (p *regExpParser) getSpellingSuggestionForUnicodePropertyName(name string) string {
	return core.GetSpellingSuggestionForStrings(name, maps.Keys(nonBinaryUnicodeProperties))
}

func (p *regExpParser) getSpellingSuggestionForUnicodePropertyValue(propertyName string, value string) string {
	values := valuesOfNonBinaryUnicodeProperties[propertyName]
	if values == nil {
		return ""
	}
	return core.GetSpellingSuggestionForStrings(value, maps.Keys(values.Keys()))
}

func (p *regExpParser) getSpellingSuggestionForUnicodePropertyNameOrValue(name string) string {
	return core.GetSpellingSuggestionForStrings(name, core.ConcatenateSeq(
		maps.Keys(valuesOfNonBinaryUnicodeProperties["General_Category"].Keys()),
		maps.Keys(binaryUnicodeProperties.Keys()),
		maps.Keys(binaryUnicodePropertiesOfStrings.Keys()),
	))
}

func (p *regExpParser) scanWordCharacters() string {
	start := p.pos()
	for p.pos() < p.end {
		ch := p.char()
		if !isWordCharacter(ch) {
			break
		}
		p.incPos(1)
	}
	return p.text()[start:p.pos()]
}

func (p *regExpParser) scanSourceCharacter() string {
	if p.pos() >= p.end {
		return ""
	}
	if !p.anyUnicodeMode {
		if p.pendingLowSurrogate != 0 {
			// Second of two surrogate code units for the same non-BMP character.
			// Now advance past the full UTF-8 sequence (the high surrogate call did not advance).
			_, size := utf8.DecodeRuneInString(p.text()[p.pos():])
			p.incPos(size)
			low := p.pendingLowSurrogate
			p.pendingLowSurrogate = 0
			return stringutil.EncodeJSStringRune(low)
		}
		ch, size := utf8.DecodeRuneInString(p.text()[p.pos():])
		if ch == utf8.RuneError || size == 0 {
			// Not a valid rune; consume one raw byte.
			p.incPos(1)
			return string(p.text()[p.pos()-1])
		}
		if utf16.RuneLen(ch) == 2 {
			// Non-BMP character: emit the high surrogate first WITHOUT advancing.
			// The low surrogate will be emitted on the next call, which also advances.
			high, low := stringutil.CodePointToSurrogatePair(ch)
			p.pendingLowSurrogate = low
			return stringutil.EncodeJSStringRune(high)
		}
		p.incPos(size)
		return string(ch)
	}
	ch, size := utf8.DecodeRuneInString(p.text()[p.pos():])
	if size == 0 {
		return ""
	}
	if ch == utf8.RuneError {
		// Invalid UTF-8; consume the byte to avoid infinite loops.
		p.incPos(size)
		return ""
	}
	p.incPos(size)
	return string(ch)
}

func (p *regExpParser) scanExpectedChar(ch rune) {
	if p.char() == ch {
		p.incPos(1)
	} else {
		p.error(diagnostics.X_0_expected, p.pos(), 0, string(ch))
	}
}

func (p *regExpParser) scanDigits() {
	start := p.pos()
	for p.pos() < p.end && stringutil.IsDigit(p.char()) {
		p.incPos(1)
	}
	p.scanner.tokenValue = p.text()[start:p.pos()]
}

func (p *regExpParser) run() {
	// Regular expressions are checked more strictly when either in 'u' or 'v' mode, or
	// when not using the looser interpretation of the syntax from ECMA-262 Annex B.
	p.anyUnicodeModeOrNonAnnexB = p.anyUnicodeMode || !p.annexB

	p.scanDisjunction(false /*isInGroup*/)

	for _, reference := range p.groupNameReferences {
		if !p.groupSpecifiers[reference.name] {
			p.error(diagnostics.There_is_no_capturing_group_named_0_in_this_regular_expression, reference.pos, reference.end-reference.pos, reference.name)
			if len(p.groupSpecifiers) > 0 {
				suggestion := core.GetSpellingSuggestionForStrings(reference.name, maps.Keys(p.groupSpecifiers))
				if suggestion != "" {
					p.error(diagnostics.Did_you_mean_0, reference.pos, reference.end-reference.pos, suggestion)
				}
			}
		}
	}
	for _, escape := range p.decimalEscapes {
		// Although a DecimalEscape with a value greater than the number of capturing groups
		// is treated as either a LegacyOctalEscapeSequence or an IdentityEscape in Annex B,
		// an error is nevertheless reported since it's most likely a mistake.
		if escape.value > p.numberOfCapturingGroups {
			if p.numberOfCapturingGroups > 0 {
				p.error(diagnostics.This_backreference_refers_to_a_group_that_does_not_exist_There_are_only_0_capturing_groups_in_this_regular_expression, escape.pos, escape.end-escape.pos, p.numberOfCapturingGroups)
			} else {
				p.error(diagnostics.This_backreference_refers_to_a_group_that_does_not_exist_There_are_no_capturing_groups_in_this_regular_expression, escape.pos, escape.end-escape.pos)
			}
		}
	}
}
