package genaip

import (
	"fmt"

	"google.golang.org/genproto/googleapis/api/annotations"
	"google.golang.org/protobuf/compiler/protogen"
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protoreflect"
	"google.golang.org/protobuf/reflect/protoregistry"
)

// PluginName is the name of the AIP Go protobuf compiler plugin.
const PluginName = "protoc-gen-go-aip"

const generatedFilenameSuffix = "_aip.go"

type Config struct {
	IncludeResourceDefinitions bool
}

// Run the AIP Go protobuf compiler plugin.
func Run(gen *protogen.Plugin, config Config) error {
	var files protoregistry.Files
	for _, file := range gen.Files {
		if err := files.RegisterFile(file.Desc); err != nil {
			return err
		}
	}
	for _, file := range gen.Files {
		if !file.Generate {
			continue
		}
		g := newGeneratedFile(gen, file)
		g.Skip()
		var rangeErr error
		rangeResourcesInFile(
			file.Desc,
			func(resource *annotations.ResourceDescriptor, extension protoreflect.ExtensionType) bool {
				if !config.IncludeResourceDefinitions && extension == annotations.E_ResourceDefinition {
					return true
				}
				g.Unskip()
				if err := (resourceNameCodeGenerator{
					resource: resource,
					files:    &files,
					file:     file,
				}).GenerateCode(g); err != nil {
					rangeErr = err
					return false
				}
				return true
			},
		)
		if rangeErr != nil {
			return rangeErr
		}
	}
	return nil
}

func newGeneratedFile(gen *protogen.Plugin, file *protogen.File) *protogen.GeneratedFile {
	g := gen.NewGeneratedFile(file.GeneratedFilenamePrefix+generatedFilenameSuffix, file.GoImportPath)
	g.P("// Code generated by ", PluginName, ". DO NOT EDIT.")
	g.P("//")
	g.P("// versions:")
	g.P("// \t", PluginName, " ", PluginVersion)
	g.P("// \tprotoc ", getProtocVersion(gen))
	g.P("// source: ", file.Desc.Path())
	g.P()
	g.P("package ", file.GoPackageName)
	return g
}

func getProtocVersion(gen *protogen.Plugin) string {
	if v := gen.Request.GetCompilerVersion(); v != nil {
		return fmt.Sprintf("v%v.%v.%v", v.GetMajor(), v.GetMinor(), v.GetPatch())
	}
	return "(unknown)"
}

func rangeResourcesInFile(
	file protoreflect.FileDescriptor,
	fn func(resource *annotations.ResourceDescriptor, extension protoreflect.ExtensionType) bool,
) {
	for _, resource := range proto.GetExtension(
		file.Options(), annotations.E_ResourceDefinition,
	).([]*annotations.ResourceDescriptor) {
		if !fn(resource, annotations.E_ResourceDefinition) {
			return
		}
	}
	for i := 0; i < file.Messages().Len(); i++ {
		resource := proto.GetExtension(
			file.Messages().Get(i).Options(), annotations.E_Resource,
		).(*annotations.ResourceDescriptor)
		if resource == nil {
			continue
		}
		if !fn(resource, annotations.E_Resource) {
			return
		}
	}
}
