package pgs

import (
	"errors"
	"fmt"
	"reflect"

	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/runtime/protoimpl"
)

// An Extension is a custom option annotation that can be applied to an Entity to provide additional
// semantic details and metadata about the Entity.
type Extension interface {
	Field

	// ParentEntity returns the ParentEntity where the Extension is defined
	DefinedIn() ParentEntity

	// Extendee returns the Message that the Extension is extending
	Extendee() Message

	setExtendee(m Message)
}

type ext struct {
	field

	parent   ParentEntity
	extendee Message
	fqn      string
}

func (e *ext) FullyQualifiedName() string { return e.fqn }
func (e *ext) Syntax() Syntax             { return e.parent.Syntax() }
func (e *ext) Package() Package           { return e.parent.Package() }
func (e *ext) File() File                 { return e.parent.File() }
func (e *ext) BuildTarget() bool          { return e.parent.BuildTarget() }
func (e *ext) DefinedIn() ParentEntity    { return e.parent }
func (e *ext) Extendee() Message          { return e.extendee }
func (e *ext) Message() Message           { return nil }
func (e *ext) InOneOf() bool              { return false }
func (e *ext) OneOf() OneOf               { return nil }
func (e *ext) setMessage(m Message)       {} // noop
func (e *ext) setOneOf(o OneOf)           {} // noop
func (e *ext) setExtendee(m Message)      { e.extendee = m }

func (e *ext) accept(v Visitor) (err error) {
	if v == nil {
		return
	}

	_, err = v.VisitExtension(e)
	return
}

var extractor extExtractor

func init() { extractor = protoExtExtractor{} }

type extExtractor interface {
	HasExtension(proto.Message, *protoimpl.ExtensionInfo) bool
	GetExtension(proto.Message, *protoimpl.ExtensionInfo) interface{}
}

type protoExtExtractor struct{}

func (e protoExtExtractor) HasExtension(pb proto.Message, ext *protoimpl.ExtensionInfo) bool {
	return proto.HasExtension(pb, ext)
}

func (e protoExtExtractor) GetExtension(pb proto.Message, ext *protoimpl.ExtensionInfo) interface{} {
	return proto.GetExtension(pb, ext)
}

func extension(opts proto.Message, e *protoimpl.ExtensionInfo, out interface{}) (bool, error) {
	if opts == nil || reflect.ValueOf(opts).IsNil() {
		return false, nil
	}

	if e == nil {
		return false, errors.New("nil *protoimpl.ExtensionInfo parameter provided")
	}

	if out == nil {
		return false, errors.New("nil extension output parameter provided")
	}

	o := reflect.ValueOf(out)
	if o.Kind() != reflect.Ptr {
		return false, errors.New("out parameter must be a pointer type")
	}

	if !extractor.HasExtension(opts, e) {
		return false, nil
	}

	val := extractor.GetExtension(opts, e)
	if val == nil {
		return false, errors.New("extracted extension value is nil")
	}

	v := reflect.ValueOf(val)
	for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
		v = v.Elem()
	}

	for o.Kind() == reflect.Ptr || o.Kind() == reflect.Interface {
		if o.Kind() == reflect.Ptr && o.IsNil() {
			o.Set(reflect.New(o.Type().Elem()))
		}
		o = o.Elem()
	}

	if v.Type().AssignableTo(o.Type()) {
		o.Set(v)
		return true, nil
	}

	return true, fmt.Errorf("cannot assign extension type %q to output type %q",
		v.Type().String(),
		o.Type().String())
}
