package pgtype

import errors "golang.org/x/xerrors"

// EnumType represents a enum type. While it implements Value, this is only in service of its type conversion duties
// when registered as a data type in a ConnType. It should not be used directly as a Value.
type EnumType struct {
	value  string
	status Status

	typeName   string            // PostgreSQL type name
	members    []string          // enum members
	membersMap map[string]string // map to quickly lookup member and reuse string instead of allocating
}

// NewEnumType initializes a new EnumType. It retains a read-only reference to members. members must not be changed.
func NewEnumType(typeName string, members []string) *EnumType {
	et := &EnumType{typeName: typeName, members: members}
	et.membersMap = make(map[string]string, len(members))
	for _, m := range members {
		et.membersMap[m] = m
	}
	return et
}

func (et *EnumType) NewTypeValue() Value {
	return &EnumType{
		value:  et.value,
		status: et.status,

		typeName:   et.typeName,
		members:    et.members,
		membersMap: et.membersMap,
	}
}

func (et *EnumType) TypeName() string {
	return et.typeName
}

func (et *EnumType) Members() []string {
	return et.members
}

// Set assigns src to dst. Set purposely does not check that src is a member. This allows continued error free
// operation in the event the PostgreSQL enum type is modified during a connection.
func (dst *EnumType) Set(src interface{}) error {
	if src == nil {
		dst.status = Null
		return nil
	}

	if value, ok := src.(interface{ Get() interface{} }); ok {
		value2 := value.Get()
		if value2 != value {
			return dst.Set(value2)
		}
	}

	switch value := src.(type) {
	case string:
		dst.value = value
		dst.status = Present
	case *string:
		if value == nil {
			dst.status = Null
		} else {
			dst.value = *value
			dst.status = Present
		}
	case []byte:
		if value == nil {
			dst.status = Null
		} else {
			dst.value = string(value)
			dst.status = Present
		}
	default:
		if originalSrc, ok := underlyingStringType(src); ok {
			return dst.Set(originalSrc)
		}
		return errors.Errorf("cannot convert %v to enum %s", value, dst.typeName)
	}

	return nil
}

func (dst EnumType) Get() interface{} {
	switch dst.status {
	case Present:
		return dst.value
	case Null:
		return nil
	default:
		return dst.status
	}
}

func (src *EnumType) AssignTo(dst interface{}) error {
	switch src.status {
	case Present:
		switch v := dst.(type) {
		case *string:
			*v = src.value
			return nil
		case *[]byte:
			*v = make([]byte, len(src.value))
			copy(*v, src.value)
			return nil
		default:
			if nextDst, retry := GetAssignToDstType(dst); retry {
				return src.AssignTo(nextDst)
			}
			return errors.Errorf("unable to assign to %T", dst)
		}
	case Null:
		return NullAssignTo(dst)
	}

	return errors.Errorf("cannot decode %#v into %T", src, dst)
}

func (EnumType) PreferredResultFormat() int16 {
	return TextFormatCode
}

func (dst *EnumType) DecodeText(ci *ConnInfo, src []byte) error {
	if src == nil {
		dst.status = Null
		return nil
	}

	// Lookup the string in membersMap to avoid an allocation.
	if s, found := dst.membersMap[string(src)]; found {
		dst.value = s
	} else {
		// If an enum type is modified after the initial connection it is possible to receive an unexpected value.
		// Gracefully handle this situation. Purposely NOT modifying members and membersMap to allow for sharing members
		// and membersMap between connections.
		dst.value = string(src)
	}
	dst.status = Present

	return nil
}

func (dst *EnumType) DecodeBinary(ci *ConnInfo, src []byte) error {
	return dst.DecodeText(ci, src)
}

func (EnumType) PreferredParamFormat() int16 {
	return TextFormatCode
}

func (src EnumType) EncodeText(ci *ConnInfo, buf []byte) ([]byte, error) {
	switch src.status {
	case Null:
		return nil, nil
	case Undefined:
		return nil, errUndefined
	}

	return append(buf, src.value...), nil
}

func (src EnumType) EncodeBinary(ci *ConnInfo, buf []byte) ([]byte, error) {
	return src.EncodeText(ci, buf)
}
