package builder

import (
	"fmt"
	"sort"

	"github.com/golang/protobuf/proto"
	dpb "github.com/golang/protobuf/protoc-gen-go/descriptor"

	"github.com/jhump/protoreflect/desc"
	"github.com/jhump/protoreflect/desc/internal"
)

// EnumBuilder is a builder used to construct a desc.EnumDescriptor.
//
// To create a new EnumBuilder, use NewEnum.
type EnumBuilder struct {
	baseBuilder

	Options        *dpb.EnumOptions
	ReservedRanges []*dpb.EnumDescriptorProto_EnumReservedRange
	ReservedNames  []string

	values  []*EnumValueBuilder
	symbols map[string]*EnumValueBuilder
}

// NewEnum creates a new EnumBuilder for an enum with the given name. Since the
// new message has no parent element, it also has no package name (e.g. it is in
// the unnamed package, until it is assigned to a file builder that defines a
// package name).
func NewEnum(name string) *EnumBuilder {
	return &EnumBuilder{
		baseBuilder: baseBuilderWithName(name),
		symbols:     map[string]*EnumValueBuilder{},
	}
}

// FromEnum returns an EnumBuilder that is effectively a copy of the given
// descriptor.
//
// Note that it is not just the given enum that is copied but its entire file.
// So the caller can get the parent element of the returned builder and the
// result would be a builder that is effectively a copy of the enum descriptor's
// parent.
//
// This means that enum builders created from descriptors do not need to be
// explicitly assigned to a file in order to preserve the original enum's
// package name.
func FromEnum(ed *desc.EnumDescriptor) (*EnumBuilder, error) {
	if fb, err := FromFile(ed.GetFile()); err != nil {
		return nil, err
	} else if eb, ok := fb.findFullyQualifiedElement(ed.GetFullyQualifiedName()).(*EnumBuilder); ok {
		return eb, nil
	} else {
		return nil, fmt.Errorf("could not find enum %s after converting file %q to builder", ed.GetFullyQualifiedName(), ed.GetFile().GetName())
	}
}

func fromEnum(ed *desc.EnumDescriptor, localEnums map[*desc.EnumDescriptor]*EnumBuilder) (*EnumBuilder, error) {
	eb := NewEnum(ed.GetName())
	eb.Options = ed.GetEnumOptions()
	eb.ReservedRanges = ed.AsEnumDescriptorProto().GetReservedRange()
	eb.ReservedNames = ed.AsEnumDescriptorProto().GetReservedName()
	setComments(&eb.comments, ed.GetSourceInfo())

	localEnums[ed] = eb

	for _, evd := range ed.GetValues() {
		if evb, err := fromEnumValue(evd); err != nil {
			return nil, err
		} else if err := eb.TryAddValue(evb); err != nil {
			return nil, err
		}
	}

	return eb, nil
}

// SetName changes this enum's name, returning the enum builder for method
// chaining. If the given new name is not valid (e.g. TrySetName would have
// returned an error) then this method will panic.
func (eb *EnumBuilder) SetName(newName string) *EnumBuilder {
	if err := eb.TrySetName(newName); err != nil {
		panic(err)
	}
	return eb
}

// TrySetName changes this enum's name. It will return an error if the given new
// name is not a valid protobuf identifier or if the parent builder already has
// an element with the given name.
func (eb *EnumBuilder) TrySetName(newName string) error {
	return eb.baseBuilder.setName(eb, newName)
}

// SetComments sets the comments associated with the enum. This method returns
// the enum builder, for method chaining.
func (eb *EnumBuilder) SetComments(c Comments) *EnumBuilder {
	eb.comments = c
	return eb
}

// GetChildren returns any builders assigned to this enum builder. These will be
// the enum's values.
func (eb *EnumBuilder) GetChildren() []Builder {
	var ch []Builder
	for _, evb := range eb.values {
		ch = append(ch, evb)
	}
	return ch
}

func (eb *EnumBuilder) findChild(name string) Builder {
	return eb.symbols[name]
}

func (eb *EnumBuilder) removeChild(b Builder) {
	if p, ok := b.GetParent().(*EnumBuilder); !ok || p != eb {
		return
	}
	eb.values = deleteBuilder(b.GetName(), eb.values).([]*EnumValueBuilder)
	delete(eb.symbols, b.GetName())
	b.setParent(nil)
}

func (eb *EnumBuilder) renamedChild(b Builder, oldName string) error {
	if p, ok := b.GetParent().(*EnumBuilder); !ok || p != eb {
		return nil
	}

	if err := eb.addSymbol(b.(*EnumValueBuilder)); err != nil {
		return err
	}
	delete(eb.symbols, oldName)
	return nil
}

func (eb *EnumBuilder) addSymbol(b *EnumValueBuilder) error {
	if _, ok := eb.symbols[b.GetName()]; ok {
		return fmt.Errorf("enum %s already contains value named %q", GetFullyQualifiedName(eb), b.GetName())
	}
	eb.symbols[b.GetName()] = b
	return nil
}

// SetOptions sets the enum options for this enum and returns the enum, for
// method chaining.
func (eb *EnumBuilder) SetOptions(options *dpb.EnumOptions) *EnumBuilder {
	eb.Options = options
	return eb
}

// GetValue returns the enum value with the given name. If no such value exists
// in the enum, nil is returned.
func (eb *EnumBuilder) GetValue(name string) *EnumValueBuilder {
	return eb.symbols[name]
}

// RemoveValue removes the enum value with the given name. If no such value
// exists in the enum, this is a no-op. This returns the enum builder, for
// method chaining.
func (eb *EnumBuilder) RemoveValue(name string) *EnumBuilder {
	eb.TryRemoveValue(name)
	return eb
}

// TryRemoveValue removes the enum value with the given name and returns false
// if the enum has no such value.
func (eb *EnumBuilder) TryRemoveValue(name string) bool {
	if evb, ok := eb.symbols[name]; ok {
		eb.removeChild(evb)
		return true
	}
	return false
}

// AddValue adds the given enum value to this enum. If an error prevents the
// value from being added, this method panics. This returns the enum builder,
// for method chaining.
func (eb *EnumBuilder) AddValue(evb *EnumValueBuilder) *EnumBuilder {
	if err := eb.TryAddValue(evb); err != nil {
		panic(err)
	}
	return eb
}

// TryAddValue adds the given enum value to this enum, returning any error that
// prevents the value from being added (such as a name collision with another
// value already added to the enum).
func (eb *EnumBuilder) TryAddValue(evb *EnumValueBuilder) error {
	if err := eb.addSymbol(evb); err != nil {
		return err
	}
	Unlink(evb)
	evb.setParent(eb)
	eb.values = append(eb.values, evb)
	return nil
}

// AddReservedRange adds the given reserved range to this message. The range is
// inclusive of both the start and end, just like defining a range in proto IDL
// source. This returns the message, for method chaining.
func (eb *EnumBuilder) AddReservedRange(start, end int32) *EnumBuilder {
	rr := &dpb.EnumDescriptorProto_EnumReservedRange{
		Start: proto.Int32(start),
		End:   proto.Int32(end),
	}
	eb.ReservedRanges = append(eb.ReservedRanges, rr)
	return eb
}

// SetReservedRanges replaces all of this enum's reserved ranges with the
// given slice of ranges. This returns the enum, for method chaining.
func (eb *EnumBuilder) SetReservedRanges(ranges []*dpb.EnumDescriptorProto_EnumReservedRange) *EnumBuilder {
	eb.ReservedRanges = ranges
	return eb
}

// AddReservedName adds the given name to the list of reserved value names for
// this enum. This returns the enum, for method chaining.
func (eb *EnumBuilder) AddReservedName(name string) *EnumBuilder {
	eb.ReservedNames = append(eb.ReservedNames, name)
	return eb
}

// SetReservedNames replaces all of this enum's reserved value names with the
// given slice of names. This returns the enum, for method chaining.
func (eb *EnumBuilder) SetReservedNames(names []string) *EnumBuilder {
	eb.ReservedNames = names
	return eb
}

func (eb *EnumBuilder) buildProto(path []int32, sourceInfo *dpb.SourceCodeInfo) (*dpb.EnumDescriptorProto, error) {
	addCommentsTo(sourceInfo, path, &eb.comments)

	var needNumbersAssigned []*dpb.EnumValueDescriptorProto
	values := make([]*dpb.EnumValueDescriptorProto, 0, len(eb.values))
	for _, evb := range eb.values {
		path := append(path, internal.Enum_valuesTag, int32(len(values)))
		evp, err := evb.buildProto(path, sourceInfo)
		if err != nil {
			return nil, err
		}
		values = append(values, evp)
		if !evb.numberSet {
			needNumbersAssigned = append(needNumbersAssigned, evp)
		}
	}

	if len(needNumbersAssigned) > 0 {
		tags := make([]int, len(values)-len(needNumbersAssigned))
		for i, ev := range values {
			tag := ev.GetNumber()
			if tag != 0 {
				tags[i] = int(tag)
			}
		}
		sort.Ints(tags)
		t := 0
		ti := sort.Search(len(tags), func(i int) bool {
			return tags[i] >= 0
		})
		if ti < len(tags) {
			tags = tags[ti:]
		}
		for len(needNumbersAssigned) > 0 {
			for len(tags) > 0 && t == tags[0] {
				t++
				tags = tags[1:]
			}
			needNumbersAssigned[0].Number = proto.Int32(int32(t))
			needNumbersAssigned = needNumbersAssigned[1:]
			t++
		}
	}

	return &dpb.EnumDescriptorProto{
		Name:          proto.String(eb.name),
		Options:       eb.Options,
		Value:         values,
		ReservedRange: eb.ReservedRanges,
		ReservedName:  eb.ReservedNames,
	}, nil
}

// Build constructs an enum descriptor based on the contents of this enum
// builder. If there are any problems constructing the descriptor, including
// resolving symbols referenced by the builder or failing to meet certain
// validation rules, an error is returned.
func (eb *EnumBuilder) Build() (*desc.EnumDescriptor, error) {
	ed, err := eb.BuildDescriptor()
	if err != nil {
		return nil, err
	}
	return ed.(*desc.EnumDescriptor), nil
}

// BuildDescriptor constructs an enum descriptor based on the contents of this
// enum builder. Most usages will prefer Build() instead, whose return type
// is a concrete descriptor type. This method is present to satisfy the Builder
// interface.
func (eb *EnumBuilder) BuildDescriptor() (desc.Descriptor, error) {
	return doBuild(eb, BuilderOptions{})
}

// EnumValueBuilder is a builder used to construct a desc.EnumValueDescriptor.
// A enum value builder *must* be added to an enum before calling its Build()
// method.
//
// To create a new EnumValueBuilder, use NewEnumValue.
type EnumValueBuilder struct {
	baseBuilder

	number    int32
	numberSet bool
	Options   *dpb.EnumValueOptions
}

// NewEnumValue creates a new EnumValueBuilder for an enum value with the given
// name. The return value's numeric value will not be set, which means it will
// be auto-assigned when the descriptor is built, unless explicitly set with a
// call to SetNumber.
func NewEnumValue(name string) *EnumValueBuilder {
	return &EnumValueBuilder{baseBuilder: baseBuilderWithName(name)}
}

// FromEnumValue returns an EnumValueBuilder that is effectively a copy of the
// given descriptor.
//
// Note that it is not just the given enum value that is copied but its entire
// file. So the caller can get the parent element of the returned builder and
// the result would be a builder that is effectively a copy of the enum value
// descriptor's parent enum.
//
// This means that enum value builders created from descriptors do not need to
// be explicitly assigned to a file in order to preserve the original enum
// value's package name.
func FromEnumValue(evd *desc.EnumValueDescriptor) (*EnumValueBuilder, error) {
	if fb, err := FromFile(evd.GetFile()); err != nil {
		return nil, err
	} else if evb, ok := fb.findFullyQualifiedElement(evd.GetFullyQualifiedName()).(*EnumValueBuilder); ok {
		return evb, nil
	} else {
		return nil, fmt.Errorf("could not find enum value %s after converting file %q to builder", evd.GetFullyQualifiedName(), evd.GetFile().GetName())
	}
}

func fromEnumValue(evd *desc.EnumValueDescriptor) (*EnumValueBuilder, error) {
	evb := NewEnumValue(evd.GetName())
	evb.Options = evd.GetEnumValueOptions()
	evb.number = evd.GetNumber()
	evb.numberSet = true
	setComments(&evb.comments, evd.GetSourceInfo())

	return evb, nil
}

// SetName changes this enum value's name, returning the enum value builder for
// method chaining. If the given new name is not valid (e.g. TrySetName would
// have returned an error) then this method will panic.
func (evb *EnumValueBuilder) SetName(newName string) *EnumValueBuilder {
	if err := evb.TrySetName(newName); err != nil {
		panic(err)
	}
	return evb
}

// TrySetName changes this enum value's name. It will return an error if the
// given new name is not a valid protobuf identifier or if the parent enum
// builder already has an enum value with the given name.
func (evb *EnumValueBuilder) TrySetName(newName string) error {
	return evb.baseBuilder.setName(evb, newName)
}

// SetComments sets the comments associated with the enum value. This method
// returns the enum value builder, for method chaining.
func (evb *EnumValueBuilder) SetComments(c Comments) *EnumValueBuilder {
	evb.comments = c
	return evb
}

// GetChildren returns nil, since enum values cannot have child elements. It is
// present to satisfy the Builder interface.
func (evb *EnumValueBuilder) GetChildren() []Builder {
	// enum values do not have children
	return nil
}

func (evb *EnumValueBuilder) findChild(name string) Builder {
	// enum values do not have children
	return nil
}

func (evb *EnumValueBuilder) removeChild(b Builder) {
	// enum values do not have children
}

func (evb *EnumValueBuilder) renamedChild(b Builder, oldName string) error {
	// enum values do not have children
	return nil
}

// SetOptions sets the enum value options for this enum value and returns the
// enum value, for method chaining.
func (evb *EnumValueBuilder) SetOptions(options *dpb.EnumValueOptions) *EnumValueBuilder {
	evb.Options = options
	return evb
}

// GetNumber returns the enum value's numeric value. If the number has not been
// set this returns zero.
func (evb *EnumValueBuilder) GetNumber() int32 {
	return evb.number
}

// HasNumber returns whether or not the enum value's numeric value has been set.
// If it has not been set, it is auto-assigned when the descriptor is built.
func (evb *EnumValueBuilder) HasNumber() bool {
	return evb.numberSet
}

// ClearNumber clears this enum value's numeric value and then returns the enum
// value builder, for method chaining. After being cleared, the number will be
// auto-assigned when the descriptor is built, unless explicitly set by a
// subsequent call to SetNumber.
func (evb *EnumValueBuilder) ClearNumber() *EnumValueBuilder {
	evb.number = 0
	evb.numberSet = false
	return evb
}

// SetNumber changes the numeric value for this enum value and then returns the
// enum value, for method chaining.
func (evb *EnumValueBuilder) SetNumber(number int32) *EnumValueBuilder {
	evb.number = number
	evb.numberSet = true
	return evb
}

func (evb *EnumValueBuilder) buildProto(path []int32, sourceInfo *dpb.SourceCodeInfo) (*dpb.EnumValueDescriptorProto, error) {
	addCommentsTo(sourceInfo, path, &evb.comments)

	return &dpb.EnumValueDescriptorProto{
		Name:    proto.String(evb.name),
		Number:  proto.Int32(evb.number),
		Options: evb.Options,
	}, nil
}

// Build constructs an enum value descriptor based on the contents of this enum
// value builder. If there are any problems constructing the descriptor,
// including resolving symbols referenced by the builder or failing to meet
// certain validation rules, an error is returned.
func (evb *EnumValueBuilder) Build() (*desc.EnumValueDescriptor, error) {
	evd, err := evb.BuildDescriptor()
	if err != nil {
		return nil, err
	}
	return evd.(*desc.EnumValueDescriptor), nil
}

// BuildDescriptor constructs an enum value descriptor based on the contents of
// this enum value builder. Most usages will prefer Build() instead, whose
// return type is a concrete descriptor type. This method is present to satisfy
// the Builder interface.
func (evb *EnumValueBuilder) BuildDescriptor() (desc.Descriptor, error) {
	return doBuild(evb, BuilderOptions{})
}
