// 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 reporter contains the types used for reporting errors from
// protocompile operations. It contains error types as well as interfaces
// for reporting and handling errors and warnings.
package reporter

import (
	"sync"

	"github.com/bufbuild/protocompile/ast"
)

// ErrorReporter is responsible for reporting the given error. If the reporter
// returns a non-nil error, parsing/linking will abort with that error. If the
// reporter returns nil, parsing will continue, allowing the parser to try to
// report as many syntax and/or link errors as it can find.
type ErrorReporter func(err ErrorWithPos) error

// WarningReporter is responsible for reporting the given warning. This is used
// for indicating non-error messages to the calling program for things that do
// not cause the parse to fail but are considered bad practice. Though they are
// just warnings, the details are supplied to the reporter via an error type.
type WarningReporter func(ErrorWithPos)

// Reporter is a type that handles reporting both errors and warnings.
// A reporter does not need to be thread-safe. Safe concurrent access is
// managed by a Handler.
type Reporter interface {
	// Error is called when the given error is encountered and needs to be
	// reported to the calling program. This signature matches ErrorReporter
	// because it has the same semantics. If this function returns non-nil
	// then the operation will abort immediately with the given error. But
	// if it returns nil, the operation will continue, reporting more errors
	// as they are encountered. If the reporter never returns non-nil then
	// the operation will eventually fail with ErrInvalidSource.
	Error(ErrorWithPos) error
	// Warning is called when the given warnings is encountered and needs to be
	// reported to the calling program. Despite the argument being an error
	// type, a warning will never cause the operation to abort or fail (unless
	// the reporter's implementation of this method panics).
	Warning(ErrorWithPos)
}

// NewReporter creates a new reporter that invokes the given functions on error
// or warning.
func NewReporter(errs ErrorReporter, warnings WarningReporter) Reporter {
	return reporterFuncs{errs: errs, warnings: warnings}
}

type reporterFuncs struct {
	errs     ErrorReporter
	warnings WarningReporter
}

func (r reporterFuncs) Error(err ErrorWithPos) error {
	if r.errs == nil {
		return err
	}
	return r.errs(err)
}

func (r reporterFuncs) Warning(err ErrorWithPos) {
	if r.warnings != nil {
		r.warnings(err)
	}
}

// Handler is used by protocompile operations for handling errors and warnings.
// This type is thread-safe. It uses a mutex to serialize calls to its reporter
// so that reporter instances do not have to be thread-safe (unless re-used
// across multiple handlers).
type Handler struct {
	parent       *Handler
	mu           sync.Mutex
	reporter     Reporter
	errsReported bool
	err          error
}

// NewHandler creates a new Handler that reports errors and warnings using the
// given reporter.
func NewHandler(rep Reporter) *Handler {
	if rep == nil {
		rep = NewReporter(nil, nil)
	}
	return &Handler{reporter: rep}
}

// SubHandler returns a "child" of h. Use of a child handler is the same as use
// of the parent, except that the Error() and ReporterError() functions only
// report non-nil for errors that were reported using the child handler. So
// errors reported directly to the parent or to a different child handler won't
// be returned. This is useful for making concurrent access to the handler more
// deterministic: if a child handler is only used from one goroutine, its view
// of reported errors is consistent and unimpacted by concurrent operations.
func (h *Handler) SubHandler() *Handler {
	return &Handler{parent: h}
}

// HandleError handles the given error. If the given err is an ErrorWithPos, it
// is reported, and this function returns the error returned by the reporter. If
// the given err is NOT an ErrorWithPos, the current operation will abort
// immediately.
//
// If the handler has already aborted (by returning a non-nil error from a prior
// call to HandleError or HandleErrorf), that same error is returned and the
// given error is not reported.
func (h *Handler) HandleError(err error) error {
	if h.parent != nil {
		_, isErrWithPos := err.(ErrorWithPos)
		err = h.parent.HandleError(err)

		// update child state
		h.mu.Lock()
		defer h.mu.Unlock()
		if isErrWithPos {
			h.errsReported = true
		}
		h.err = err
		return err
	}

	h.mu.Lock()
	defer h.mu.Unlock()

	if h.err != nil {
		return h.err
	}
	if ewp, ok := err.(ErrorWithPos); ok {
		h.errsReported = true
		err = h.reporter.Error(ewp)
	}
	h.err = err
	return err
}

// HandleErrorWithPos handles an error with the given source position.
//
// If the handler has already aborted (by returning a non-nil error from a prior
// call to HandleError or HandleErrorf), that same error is returned and the
// given error is not reported.
func (h *Handler) HandleErrorWithPos(span ast.SourceSpan, err error) error {
	return h.HandleError(Error(span, err))
}

// HandleErrorf handles an error with the given source position, creating the
// error using the given message format and arguments.
//
// If the handler has already aborted (by returning a non-nil error from a call
// to HandleError or HandleErrorf), that same error is returned and the given
// error is not reported.
func (h *Handler) HandleErrorf(span ast.SourceSpan, format string, args ...any) error {
	return h.HandleError(Errorf(span, format, args...))
}

// HandleWarning handles the given warning. This will delegate to the handler's
// configured reporter.
func (h *Handler) HandleWarning(err ErrorWithPos) {
	if h.parent != nil {
		h.parent.HandleWarning(err)
		return
	}

	// even though we aren't touching mutable fields, we acquire lock anyway so
	// that underlying reporter does not have to be thread-safe
	h.mu.Lock()
	defer h.mu.Unlock()

	h.reporter.Warning(err)
}

// HandleWarningWithPos handles a warning with the given source position. This will
// delegate to the handler's configured reporter.
func (h *Handler) HandleWarningWithPos(span ast.SourceSpan, err error) {
	h.HandleWarning(Error(span, err))
}

// HandleWarningf handles a warning with the given source position, creating the
// actual error value using the given message format and arguments.
func (h *Handler) HandleWarningf(span ast.SourceSpan, format string, args ...any) {
	h.HandleWarning(Errorf(span, format, args...))
}

// Error returns the handler result. If any errors have been reported then this
// returns a non-nil error. If the reporter never returned a non-nil error then
// ErrInvalidSource is returned. Otherwise, this returns the error returned by
// the  handler's reporter (the same value returned by ReporterError).
func (h *Handler) Error() error {
	h.mu.Lock()
	defer h.mu.Unlock()

	if h.errsReported && h.err == nil {
		return ErrInvalidSource
	}
	return h.err
}

// ReporterError returns the error returned by the handler's reporter. If
// the reporter has either not been invoked (no errors handled) or has not
// returned any non-nil value, then this returns nil.
func (h *Handler) ReporterError() error {
	h.mu.Lock()
	defer h.mu.Unlock()

	return h.err
}
