// Copyright 2020-2026 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 main

import (
	"bytes"
	"errors"
	"fmt"
	"go/format"
	"iter"
	"os"
	"runtime/debug"
	"strings"

	"google.golang.org/protobuf/reflect/protoreflect"

	"github.com/bufbuild/protocompile/internal/cases"
	"github.com/bufbuild/protocompile/internal/ext/flagx"
	"github.com/bufbuild/protocompile/internal/ext/stringsx"
	"github.com/bufbuild/protocompile/wellknownimports"
)

// convertPath converts a fully-qualified Protobuf path from descriptor.proto
// into a nice variable name.
func convertPath(name protoreflect.FullName) string {
	path := string(name)
	path = strings.TrimPrefix(path, "google.protobuf.")

	buf := new(strings.Builder)
	prev := ""
	for c := range strings.SplitSeq(path, ".") {
		if c == "VisibilityFeature" {
			continue
		}

		if c == "DescriptorProto" {
			c = "Message"
		}
		c, _ = strings.CutSuffix(c, "DescriptorProto")
		c = cases.Pascal.Convert(c)
		if _, _, c2 := stringsx.CutOverlap(prev, c); c2 != "" {
			c = c2
		}

		if buf.Len() > 0 {
			buf.WriteByte('_')
		}
		buf.WriteString(c)
		prev = c
	}

	return buf.String()
}

func walk[D protoreflect.Descriptor](d protoreflect.FileDescriptor) iter.Seq[D] {
	return func(y func(D) bool) {
		yield := func(d protoreflect.Descriptor) bool {
			v, ok := d.(D)
			return !ok || y(v)
		}

		var rec func(protoreflect.Descriptor) bool
		rec = func(d protoreflect.Descriptor) bool {
			switch d := d.(type) {
			case protoreflect.FileDescriptor:
				return yield(d) && each(d.Enums(), rec) && each(d.Messages(), rec)
			case protoreflect.MessageDescriptor:
				return yield(d) && each(d.Enums(), rec) &&
					each(d.Fields(), yield) &&
					each(d.Messages(), rec)
			case protoreflect.EnumDescriptor:
				return yield(d) && each(d.Values(), rec)
			default:
				return yield(d)
			}
		}

		rec(d)
	}
}

func each[T protoreflect.Descriptor, L interface {
	Len() int
	Get(int) T
}](l L, yield func(protoreflect.Descriptor) bool) bool {
	for i := range l.Len() {
		if !yield(l.Get(i)) {
			return false
		}
	}
	return true
}

func main() {
	flagx.Main(func() error {
		files := wellknownimports.Files()
		descpb, _ := files.FindFileByPath("google/protobuf/descriptor.proto")
		anypb, _ := files.FindFileByPath("google/protobuf/any.proto")

		info, ok := debug.ReadBuildInfo()
		if !ok {
			return errors.New("debug: could not read build info")
		}

		out := new(bytes.Buffer)
		fmt.Fprintf(out, "// Code generated by %s. DO NOT EDIT.\n\n", info.Path)
		fmt.Fprintf(out, "package %s\n\n", os.Getenv("GOPACKAGE"))

		fmt.Fprintf(out, "const (\n")
		for field := range walk[protoreflect.FieldDescriptor](descpb) {
			fmt.Fprintf(out, "// Field number for %v.\n", field.FullName())
			fmt.Fprintf(out, "\t%s = %v\n\n", convertPath(field.FullName()), field.Number())
		}
		for field := range walk[protoreflect.FieldDescriptor](anypb) {
			fmt.Fprintf(out, "// Field number for %v.\n", field.FullName())
			fmt.Fprintf(out, "\t%s = %v\n\n", convertPath(field.FullName()), field.Number())
		}
		fmt.Fprintf(out, ")\n\n")

		fmt.Fprintf(out, "const (\n")
		for value := range walk[protoreflect.EnumValueDescriptor](descpb) {
			fmt.Fprintf(out, "// Enum value for %v.\n", value.FullName())

			// Use nice, enum-relative names.
			name := value.Parent().FullName() + "." + protoreflect.FullName(value.Name())
			fmt.Fprintf(out, "\t%s = %v\n\n", convertPath(name), value.Number())
		}
		fmt.Fprintf(out, ")\n\n")

		src, err := format.Source(out.Bytes())
		if err != nil {
			return err
		}

		return os.WriteFile("generated.go", src, 0666) //nolint:gosec
	})
}
