// Copyright 2020-2025 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package parser

import (
	"github.com/bufbuild/protocompile/experimental/ast"
	"github.com/bufbuild/protocompile/experimental/internal/lexer"
	"github.com/bufbuild/protocompile/experimental/internal/taxa"
	"github.com/bufbuild/protocompile/experimental/report"
	"github.com/bufbuild/protocompile/experimental/seq"
	"github.com/bufbuild/protocompile/experimental/source"
	"github.com/bufbuild/protocompile/experimental/token"
	"github.com/bufbuild/protocompile/experimental/token/keyword"
	"github.com/bufbuild/protocompile/internal/ext/slicesx"
)

// lex is a combined lexer for Protobuf and CEL.
var lex = lexer.Lexer{
	OnKeyword: func(k keyword.Keyword) lexer.OnKeyword {
		switch k {
		case keyword.Comment:
			return lexer.LineComment
		case keyword.LComment, keyword.RComment:
			return lexer.BlockComment
		case keyword.LParen, keyword.LBracket, keyword.LBrace,
			keyword.RParen, keyword.RBracket, keyword.RBrace:
			return lexer.BracketKeyword
		default:
			if k.IsProtobuf() || k.IsCEL() {
				return lexer.SoftKeyword
			}
			return lexer.DiscardKeyword
		}
	},

	IsAffix: func(affix string, kind token.Kind, suffix bool) bool {
		switch kind {
		case token.Number:
			return suffix && slicesx.Among(affix, "u", "U")
		case token.String:
			return !suffix && slicesx.Among(affix, "r", "b", "rb")
		default:
			return false
		}
	},

	NumberCanStartWithDot: true,
	OldStyleOctal:         true,
	RequireASCIIIdent:     true,
	EscapeExtended:        true,
	EscapeAsk:             true,
	EscapeOctal:           true,
	EscapePartialX:        true,
	EscapeUppercaseX:      true,
	EscapeOldStyleUnicode: true,
}

// Parse lexes and parses the Protobuf file tracked by ctx.
//
// Diagnostics generated by this process are written to errs. Returns whether
// parsing succeeded without errors.
//
// Parse will freeze the stream in ctx when it is done.
func Parse(path string, source *source.File, r *report.Report) (file *ast.File, ok bool) {
	prior := len(r.Diagnostics)

	r.SaveOptions(func() {
		if path == "google/protobuf/descriptor.proto" {
			// descriptor.proto contains required fields, which we warn against.
			// However, that would cause literally every project ever to have
			// warnings, and in general, any warnings we add should not ding
			// the worst WKT file of them all.
			r.SuppressWarnings = true
		}

		file = ast.New(path, lex.Lex(source, r))
		parse(file, r)

		defer file.Stream().Freeze()
	})

	ok = true
	for _, d := range r.Diagnostics[prior:] {
		if d.Level() >= report.Error {
			ok = false
			break
		}
	}

	return file, ok
}

// parse implements the core parser loop.
func parse(file *ast.File, errs *report.Report) {
	p := &parser{
		Nodes:  file.Nodes(),
		Report: errs,
	}

	defer p.CatchICE(false, nil)

	c := file.Stream().Cursor()

	var mark token.CursorMark
	for !c.Done() {
		ensureProgress(c, &mark)
		node := parseDecl(p, c, taxa.TopLevel)
		if !node.IsZero() {
			seq.Append(file.Decls(), node)
		}
	}

	p.parseComplete = true
	legalizeFile(p, file)
}

// ensureProgress is used to make sure that the parser makes progress on each
// loop iteration. See mustProgress in lex_state.go for the lexer equivalent.
func ensureProgress(c *token.Cursor, m *token.CursorMark) {
	next := c.Mark()
	if *m == next {
		panic("protocompile/parser: parser failed to make progress; this is a bug in protocompile")
	}
	*m = next
}
