package bindnode

import (
	"fmt"
	"reflect"
	"strings"

	"github.com/ipld/go-ipld-prime/datamodel"
	"github.com/ipld/go-ipld-prime/node/basicnode"
	"github.com/ipld/go-ipld-prime/schema"
)

func reprNode(node datamodel.Node) datamodel.Node {
	if node, ok := node.(schema.TypedNode); ok {
		return node.Representation()
	}
	// datamodel.Absent and datamodel.Null are not typed.
	// TODO: is this a problem? surely a typed struct's fields are always
	// typed, even when absent or null.
	return node
}

func reprStrategy(typ schema.Type) interface{} {
	// Can't use an interface check, as each method has a different result type.
	// TODO: consider inlining this type switch at each call site,
	// as the call sites need the underlying schema.Type too.
	switch typ := typ.(type) {
	case *schema.TypeStruct:
		return typ.RepresentationStrategy()
	case *schema.TypeUnion:
		return typ.RepresentationStrategy()
	case *schema.TypeEnum:
		return typ.RepresentationStrategy()
	}
	return nil
}

type _prototypeRepr _prototype

func (w *_prototypeRepr) NewBuilder() datamodel.NodeBuilder {
	return &_builderRepr{_assemblerRepr{
		cfg:        w.cfg,
		schemaType: w.schemaType,
		val:        reflect.New(w.goType).Elem(),
	}}
}

type _nodeRepr _node

func (w *_nodeRepr) Kind() datamodel.Kind {
	switch reprStrategy(w.schemaType).(type) {
	case schema.StructRepresentation_Stringjoin:
		return datamodel.Kind_String
	case schema.StructRepresentation_Map:
		return datamodel.Kind_Map
	case schema.StructRepresentation_Tuple, schema.StructRepresentation_ListPairs:
		return datamodel.Kind_List
	case schema.UnionRepresentation_Keyed:
		return datamodel.Kind_Map
	case schema.UnionRepresentation_Kinded:
		haveIdx, _ := unionMember(w.val)
		if haveIdx < 0 {
			panic(fmt.Sprintf("bindnode: kinded union %s has no member", w.val.Type()))
		}
		mtyp := w.schemaType.(*schema.TypeUnion).Members()[haveIdx]
		return mtyp.RepresentationBehavior()
	case schema.UnionRepresentation_Stringprefix:
		return datamodel.Kind_String
	case schema.EnumRepresentation_Int:
		return datamodel.Kind_Int
	case schema.EnumRepresentation_String:
		return datamodel.Kind_String
	default:
		return (*_node)(w).Kind()
	}
}

func outboundMappedKey(stg schema.StructRepresentation_Map, key string) string {
	// TODO: why doesn't stg just allow us to "get" by the key string?
	field := schema.SpawnStructField(key, "", false, false)
	mappedKey := stg.GetFieldKey(field)
	return mappedKey
}

func inboundMappedKey(typ *schema.TypeStruct, stg schema.StructRepresentation_Map, key string) string {
	// TODO: can't do a "reverse" lookup... needs better API probably.
	fields := typ.Fields()
	for _, field := range fields {
		mappedKey := stg.GetFieldKey(field)
		if key == mappedKey {
			return field.Name()
		}
	}
	return key // fallback to the same key
}

func outboundMappedType(stg schema.UnionRepresentation_Keyed, key string) string {
	// TODO: why doesn't stg just allow us to "get" by the key string?
	typ := schema.SpawnBool(key)
	mappedKey := stg.GetDiscriminant(typ)
	return mappedKey
}

func inboundMappedType(typ *schema.TypeUnion, stg schema.UnionRepresentation_Keyed, key string) string {
	// TODO: can't do a "reverse" lookup... needs better API probably.
	for _, member := range typ.Members() {
		mappedKey := stg.GetDiscriminant(member)
		if key == mappedKey {
			// println(key, "rev-mapped to", field.Name())
			return member.Name()
		}
	}
	// println(key, "had no mapping")
	return key // fallback to the same key
}

// asKinded can be called on a kinded union node to obtain a node
// representing one of its members, identified by kind.
func (w *_nodeRepr) asKinded(stg schema.UnionRepresentation_Kinded, kind datamodel.Kind) *_nodeRepr {
	name := stg.GetMember(kind)
	members := w.schemaType.(*schema.TypeUnion).Members()
	for i, member := range members {
		if member.Name() != name {
			continue
		}
		w2 := *w
		w2.val = w.val.Field(i).Elem()
		w2.schemaType = member
		return &w2
	}
	panic("bindnode TODO: GetMember result is missing?")
}

func (w *_nodeRepr) LookupByString(key string) (datamodel.Node, error) {
	if stg, ok := reprStrategy(w.schemaType).(schema.UnionRepresentation_Kinded); ok {
		w = w.asKinded(stg, datamodel.Kind_Map)
	}
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.StructRepresentation_Map:
		revKey := inboundMappedKey(w.schemaType.(*schema.TypeStruct), stg, key)
		v, err := (*_node)(w).LookupByString(revKey)
		if err != nil {
			return nil, err
		}
		return reprNode(v), nil
	case schema.UnionRepresentation_Keyed:
		revKey := inboundMappedType(w.schemaType.(*schema.TypeUnion), stg, key)
		v, err := (*_node)(w).LookupByString(revKey)
		if err != nil {
			return nil, err
		}
		return reprNode(v), nil
	default:
		v, err := (*_node)(w).LookupByString(key)
		if err != nil {
			return nil, err
		}
		return reprNode(v), nil
	}
}

func (w *_nodeRepr) LookupByIndex(idx int64) (datamodel.Node, error) {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Kinded:
		return w.asKinded(stg, datamodel.Kind_List).LookupByIndex(idx)
	case schema.StructRepresentation_Tuple:
		fields := w.schemaType.(*schema.TypeStruct).Fields()
		if idx < 0 || int(idx) >= len(fields) {
			return nil, datamodel.ErrNotExists{Segment: datamodel.PathSegmentOfInt(idx)}
		}
		field := fields[idx]
		v, err := (*_node)(w).LookupByString(field.Name())
		if err != nil {
			return nil, err
		}
		return reprNode(v), nil
	case schema.StructRepresentation_ListPairs:
		fields := w.schemaType.(*schema.TypeStruct).Fields()
		if idx < 0 || int(idx) >= len(fields) {
			return nil, datamodel.ErrNotExists{Segment: datamodel.PathSegmentOfInt(idx)}
		}
		var curField int64
		for _, field := range fields {
			value, err := (*_node)(w).LookupByString(field.Name())
			if err != nil {
				return nil, err
			}
			if value.IsAbsent() {
				continue
			}
			if curField == idx {
				return buildListpairsField(basicnode.NewString(field.Name()), reprNode(value))
			}
			curField++
		}
		return nil, datamodel.ErrNotExists{Segment: datamodel.PathSegmentOfInt(idx)}
	default:
		v, err := (*_node)(w).LookupByIndex(idx)
		if err != nil {
			return nil, err
		}
		return reprNode(v), nil
	}
}

func (w *_nodeRepr) LookupBySegment(seg datamodel.PathSegment) (datamodel.Node, error) {
	switch w.Kind() {
	case datamodel.Kind_Map:
		return w.LookupByString(seg.String())
	case datamodel.Kind_List:
		idx, err := seg.Index()
		if err != nil {
			return nil, err
		}
		return w.LookupByIndex(idx)
	}
	return nil, datamodel.ErrWrongKind{
		TypeName:        w.schemaType.Name(),
		MethodName:      "LookupBySegment",
		AppropriateKind: datamodel.KindSet_Recursive,
		ActualKind:      w.Kind(),
	}
}

func (w *_nodeRepr) LookupByNode(key datamodel.Node) (datamodel.Node, error) {
	switch w.Kind() {
	case datamodel.Kind_Map:
		s, err := key.AsString()
		if err != nil {
			return nil, err
		}
		return w.LookupByString(s)
	case datamodel.Kind_List:
		i, err := key.AsInt()
		if err != nil {
			return nil, err
		}
		return w.LookupByIndex(i)
	}
	return nil, datamodel.ErrWrongKind{
		TypeName:        w.schemaType.Name(),
		MethodName:      "LookupByNode",
		AppropriateKind: datamodel.KindSet_Recursive,
		ActualKind:      w.Kind(),
	}
}

func (w *_nodeRepr) MapIterator() datamodel.MapIterator {
	// TODO: we can try to reuse reprStrategy here and elsewhere
	if stg, ok := reprStrategy(w.schemaType).(schema.UnionRepresentation_Kinded); ok {
		w = w.asKinded(stg, datamodel.Kind_Map)
	}
	switch reprStrategy(w.schemaType).(type) {
	case schema.StructRepresentation_Map:
		itr := (*_node)(w).MapIterator().(*_structIterator)
		// When we reach the last non-absent field, we should stop.
		itr.reprEnd = int(w.lengthMinusTrailingAbsents())
		return (*_structIteratorRepr)(itr)
	case schema.UnionRepresentation_Keyed:
		itr := (*_node)(w).MapIterator().(*_unionIterator)
		return (*_unionIteratorRepr)(itr)
	default:
		iter, _ := (*_node)(w).MapIterator().(*_mapIterator)
		if iter == nil {
			return nil
		}
		return (*_mapIteratorRepr)(iter)
	}
}

type _mapIteratorRepr _mapIterator

func (w *_mapIteratorRepr) Next() (key, value datamodel.Node, _ error) {
	k, v, err := (*_mapIterator)(w).Next()
	if err != nil {
		return nil, nil, err
	}
	return reprNode(k), reprNode(v), nil
}

func (w *_mapIteratorRepr) Done() bool {
	return w.nextIndex >= w.keysVal.Len()
}

func (w *_nodeRepr) ListIterator() datamodel.ListIterator {
	if stg, ok := reprStrategy(w.schemaType).(schema.UnionRepresentation_Kinded); ok {
		w = w.asKinded(stg, datamodel.Kind_List)
	}
	switch reprStrategy(w.schemaType).(type) {
	case schema.StructRepresentation_Tuple:
		typ := w.schemaType.(*schema.TypeStruct)
		iter := _tupleIteratorRepr{cfg: w.cfg, schemaType: typ, fields: typ.Fields(), val: w.val}
		iter.reprEnd = int(w.lengthMinusTrailingAbsents())
		return &iter
	case schema.StructRepresentation_ListPairs:
		typ := w.schemaType.(*schema.TypeStruct)
		iter := _listpairsIteratorRepr{cfg: w.cfg, schemaType: typ, fields: typ.Fields(), val: w.val}
		iter.reprEnd = int(w.lengthMinusTrailingAbsents())
		return &iter
	default:
		iter, _ := (*_node)(w).ListIterator().(*_listIterator)
		if iter == nil {
			return nil
		}
		return (*_listIteratorRepr)(iter)
	}
}

type _listIteratorRepr _listIterator

func (w *_listIteratorRepr) Next() (index int64, value datamodel.Node, _ error) {
	idx, v, err := (*_listIterator)(w).Next()
	if err != nil {
		return idx, nil, err
	}
	return idx, reprNode(v), nil
}

func (w *_listIteratorRepr) Done() bool {
	return w.nextIndex >= w.val.Len()
}

func (w *_nodeRepr) lengthMinusAbsents() int64 {
	fields := w.schemaType.(*schema.TypeStruct).Fields()
	n := int64(len(fields))
	for i, field := range fields {
		if field.IsOptional() && w.val.Field(i).IsNil() {
			n--
		}
	}
	return n
}

type _tupleIteratorRepr struct {
	// TODO: support embedded fields?
	cfg        *config
	schemaType *schema.TypeStruct
	fields     []schema.StructField
	val        reflect.Value // non-pointer
	nextIndex  int

	// these are only used in repr.go
	reprEnd int
}

func (w *_tupleIteratorRepr) Next() (index int64, value datamodel.Node, _ error) {
	for {
		idx := w.nextIndex
		_, value, err := (*_structIterator)(w).Next()
		if err != nil {
			return 0, nil, err
		}
		if w.nextIndex <= w.reprEnd {
			return int64(idx), reprNode(value), nil
		}
	}
}

func (w *_tupleIteratorRepr) Done() bool {
	return w.nextIndex >= w.reprEnd
}

type _listpairsIteratorRepr struct {
	cfg        *config
	schemaType *schema.TypeStruct
	fields     []schema.StructField
	val        reflect.Value // non-pointer
	nextIndex  int

	// these are only used in repr.go
	reprEnd int
}

func (w *_listpairsIteratorRepr) Next() (index int64, value datamodel.Node, _ error) {
	for {
		if w.Done() {
			return 0, nil, datamodel.ErrIteratorOverread{}
		}
		idx := w.nextIndex
		key, value, err := (*_structIterator)(w).Next()
		if err != nil {
			return 0, nil, err
		}
		if value.IsAbsent() || w.nextIndex > w.reprEnd {
			continue
		}
		field, err := buildListpairsField(key, reprNode(value))
		if err != nil {
			return 0, nil, err
		}
		return int64(idx), field, nil
	}
}

func (w *_listpairsIteratorRepr) Done() bool {
	return w.nextIndex >= w.reprEnd
}

func buildListpairsField(key, value datamodel.Node) (datamodel.Node, error) {
	nb := basicnode.Prototype.List.NewBuilder()
	la, err := nb.BeginList(2)
	if err != nil {
		return nil, err
	}
	if err := la.AssembleValue().AssignNode(key); err != nil {
		return nil, err
	}
	if err := la.AssembleValue().AssignNode(value); err != nil {
		return nil, err
	}
	if err := la.Finish(); err != nil {
		return nil, err
	}
	return nb.Build(), nil
}

func (w *_nodeRepr) lengthMinusTrailingAbsents() int64 {
	fields := w.schemaType.(*schema.TypeStruct).Fields()
	for i := len(fields) - 1; i >= 0; i-- {
		field := fields[i]
		if !field.IsOptional() || !w.val.Field(i).IsNil() {
			return int64(i + 1)
		}
	}
	return 0
}

func (w *_nodeRepr) Length() int64 {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.StructRepresentation_Stringjoin:
		return -1
	case schema.StructRepresentation_Map:
		return w.lengthMinusAbsents()
	case schema.StructRepresentation_Tuple:
		return w.lengthMinusTrailingAbsents()
	case schema.StructRepresentation_ListPairs:
		return w.lengthMinusAbsents()
	case schema.UnionRepresentation_Keyed:
		return (*_node)(w).Length()
	case schema.UnionRepresentation_Kinded:
		w = w.asKinded(stg, w.Kind())
		return (*_node)(w).Length()
	default:
		return (*_node)(w).Length()
	}
}

func (w *_nodeRepr) IsAbsent() bool {
	if reprStrategy(w.schemaType) == nil {
		return (*_node)(w).IsAbsent()
	}
	return false
}

func (w *_nodeRepr) IsNull() bool {
	if reprStrategy(w.schemaType) == nil {
		return (*_node)(w).IsNull()
	}
	return false
}

func (w *_nodeRepr) AsBool() (bool, error) {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Kinded:
		return w.asKinded(stg, datamodel.Kind_Bool).AsBool()
	default:
		return (*_node)(w).AsBool()
	}
}

func (w *_nodeRepr) AsInt() (int64, error) {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Kinded:
		return w.asKinded(stg, datamodel.Kind_Int).AsInt()
	case schema.EnumRepresentation_Int:
		kind := w.val.Kind()
		if kind == reflect.String {
			s, err := (*_node)(w).AsString()
			if err != nil {
				return 0, err
			}
			mapped, ok := stg[s]
			if !ok {
				// We assume that the schema strategy is correct,
				// so we can only fail if the stored string isn't a valid member.
				return 0, fmt.Errorf("AsInt: %q is not a valid member of enum %s", s, w.schemaType.Name())
			}
			// TODO: the strategy type should probably use int64 rather than int
			return int64(mapped), nil
		}
		var i int
		// TODO: check for overflows
		if kindInt[kind] {
			i = int(w.val.Int())
		} else if kindUint[kind] {
			i = int(w.val.Uint())
		} else {
			return 0, fmt.Errorf("AsInt: unexpected kind: %s", kind)
		}
		for _, reprInt := range stg {
			if reprInt == i {
				return int64(i), nil
			}
		}
		// We assume that the schema strategy is correct,
		// so we can only fail if the stored string isn't a valid member.
		return 0, fmt.Errorf("AsInt: %d is not a valid member of enum %s", i, w.schemaType.Name())
	default:
		return (*_node)(w).AsInt()
	}
}

func (w *_nodeRepr) AsFloat() (float64, error) {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Kinded:
		return w.asKinded(stg, datamodel.Kind_Float).AsFloat()
	default:
		return (*_node)(w).AsFloat()
	}
}

func (w *_nodeRepr) AsString() (string, error) {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.StructRepresentation_Stringjoin:
		var b strings.Builder
		itr := (*_node)(w).MapIterator()
		first := true
		for !itr.Done() {
			_, v, err := itr.Next()
			if err != nil {
				return "", err
			}
			s, err := reprNode(v).AsString()
			if err != nil {
				return "", err
			}
			if first {
				first = false
			} else {
				b.WriteString(stg.GetDelim())
			}
			b.WriteString(s)
		}
		return b.String(), nil
	case schema.UnionRepresentation_Stringprefix:
		haveIdx, mval := unionMember(w.val)
		mtyp := w.schemaType.(*schema.TypeUnion).Members()[haveIdx]

		w2 := *w
		w2.val = mval
		w2.schemaType = mtyp
		s, err := w2.AsString()
		if err != nil {
			return "", err
		}

		name := stg.GetDiscriminant(mtyp)
		return name + stg.GetDelim() + s, nil
	case schema.UnionRepresentation_Kinded:
		return w.asKinded(stg, datamodel.Kind_String).AsString()
	case schema.EnumRepresentation_String:
		s, err := (*_node)(w).AsString()
		if err != nil {
			return "", err
		}
		if mapped := stg[s]; mapped != "" {
			return mapped, nil
		}
		members := w.schemaType.(*schema.TypeEnum).Members()
		for _, member := range members {
			if s == member {
				return s, nil
			}
		}
		for k, v := range stg {
			// a programming error? we may have the enum string value rather than the type
			if v == s {
				return "", fmt.Errorf("AsString: %q is not a valid member of enum %s (bindnode works at the type level; did you mean %q?)", s, w.schemaType.Name(), k)
			}
		}
		return "", fmt.Errorf("AsString: %q is not a valid member of enum %s", s, w.schemaType.Name())
	default:
		return (*_node)(w).AsString()
	}
}

func (w *_nodeRepr) AsBytes() ([]byte, error) {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Kinded:
		return w.asKinded(stg, datamodel.Kind_Bytes).AsBytes()
	default:
		return (*_node)(w).AsBytes()
	}
}

func (w *_nodeRepr) AsLink() (datamodel.Link, error) {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Kinded:
		return w.asKinded(stg, datamodel.Kind_Link).AsLink()
	default:
		return (*_node)(w).AsLink()
	}
}

func (w *_nodeRepr) Prototype() datamodel.NodePrototype {
	return (*_prototypeRepr)((*_node)(w).Prototype().(*_prototype))
}

type _builderRepr struct {
	_assemblerRepr
}

// TODO: returning a repr node here is probably good, but there's a gotcha: one
// can go from a typed node to a repr node via the Representation method, but
// not the other way. That's probably why codegen returns a typed node here.
// The solution might be to add a way to go from the repr node to its parent
// typed node.

func (w *_builderRepr) Build() datamodel.Node {
	// TODO: see the notes above.
	// return &_nodeRepr{schemaType: w.schemaType, val: w.val}
	return &_node{cfg: w.cfg, schemaType: w.schemaType, val: w.val}
}

func (w *_builderRepr) Reset() {
	panic("bindnode TODO: Reset")
}

type _assemblerRepr struct {
	cfg        *config
	schemaType schema.Type
	val        reflect.Value // non-pointer
	finish     func() error

	nullable bool
}

func assemblerRepr(am datamodel.NodeAssembler) datamodel.NodeAssembler {
	switch am := am.(type) {
	case *_assembler:
		return (*_assemblerRepr)(am)
	case _errorAssembler:
		return am
	default:
		panic(fmt.Sprintf("unexpected NodeAssembler type: %T", am))
	}
}

func (w *_assemblerRepr) asKinded(stg schema.UnionRepresentation_Kinded, kind datamodel.Kind) datamodel.NodeAssembler {
	name := stg.GetMember(kind)
	members := w.schemaType.(*schema.TypeUnion).Members()
	kindSet := make([]datamodel.Kind, 0, len(members))
	for idx, member := range members {
		if member.Name() != name {
			kindSet = append(kindSet, member.RepresentationBehavior())
			continue
		}
		w2 := *w
		goType := w.val.Field(idx).Type().Elem()
		valPtr := reflect.New(goType)
		w2.val = valPtr.Elem()
		w2.schemaType = member

		// Layer a new finish func on top, to set Index/Value.
		w2.finish = func() error {
			unionSetMember(w.val, idx, valPtr)
			if w.finish != nil {
				if err := w.finish(); err != nil {
					return err
				}
			}
			return nil
		}
		return &w2
	}
	return _errorAssembler{datamodel.ErrWrongKind{
		TypeName:        w.schemaType.Name() + ".Repr",
		MethodName:      "", // TODO: we could fill it via runtime.Callers
		AppropriateKind: datamodel.KindSet(kindSet),
		ActualKind:      kind,
	}}
}

func (w *_assemblerRepr) BeginMap(sizeHint int64) (datamodel.MapAssembler, error) {
	if stg, ok := reprStrategy(w.schemaType).(schema.UnionRepresentation_Kinded); ok {
		return w.asKinded(stg, datamodel.Kind_Map).BeginMap(sizeHint)
	}
	asm, err := (*_assembler)(w).BeginMap(sizeHint)
	if err != nil {
		return nil, err
	}
	switch asm := asm.(type) {
	case *_structAssembler:
		return (*_structAssemblerRepr)(asm), nil
	case *_mapAssembler:
		return (*_mapAssemblerRepr)(asm), nil
	case *_unionAssembler:
		return (*_unionAssemblerRepr)(asm), nil
	case *basicMapAssembler:
		return asm, nil
	default:
		return nil, fmt.Errorf("bindnode BeginMap TODO: %T", asm)
	}
}

func (w *_assemblerRepr) BeginList(sizeHint int64) (datamodel.ListAssembler, error) {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Kinded:
		return w.asKinded(stg, datamodel.Kind_List).BeginList(sizeHint)
	case schema.StructRepresentation_Tuple, schema.StructRepresentation_ListPairs:
		asm, err := (*_assembler)(w).BeginMap(sizeHint)
		if err != nil {
			return nil, err
		}
		return (*_listStructAssemblerRepr)(asm.(*_structAssembler)), nil
	default:
		asm, err := (*_assembler)(w).BeginList(sizeHint)
		if err != nil {
			return nil, err
		}
		if _, ok := asm.(*basicListAssembler); ok {
			return asm, nil
		}
		return (*_listAssemblerRepr)(asm.(*_listAssembler)), nil
	}
}

func (w *_assemblerRepr) AssignNull() error {
	return (*_assembler)(w).AssignNull()
}

func (w *_assemblerRepr) AssignBool(b bool) error {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Kinded:
		return w.asKinded(stg, datamodel.Kind_Bool).AssignBool(b)
	default:
		return (*_assembler)(w).AssignBool(b)
	}
}

func (w *_assemblerRepr) assignUInt(uin datamodel.UintNode) error {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Kinded:
		return w.asKinded(stg, datamodel.Kind_Int).(*_assemblerRepr).assignUInt(uin)
	case schema.EnumRepresentation_Int:
		uin, err := uin.AsUint()
		if err != nil {
			return err
		}
		return fmt.Errorf("AssignInt: %d is not a valid member of enum %s", uin, w.schemaType.Name())
	default:
		return (*_assembler)(w).assignUInt(uin)
	}
}

func (w *_assemblerRepr) AssignInt(i int64) error {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Kinded:
		return w.asKinded(stg, datamodel.Kind_Int).AssignInt(i)
	case schema.EnumRepresentation_Int:
		for member, reprInt := range stg {
			if int64(reprInt) != i {
				continue
			}
			val := (*_assembler)(w).createNonPtrVal()
			kind := val.Kind()
			if kind == reflect.String {
				// Reuse AssignString so we don't have to repeat ten lines.
				return (*_assembler)(w).AssignString(member)
			}
			// Short-cut to storing the repr int directly, akin to node.go's AssignInt.
			if kindInt[kind] {
				val.SetInt(i)
			} else if kindUint[kind] {
				if i < 0 {
					// TODO: write a test
					return fmt.Errorf("bindnode: cannot assign negative integer to %s", w.val.Type())
				}
				val.SetUint(uint64(i))
			} else {
				return fmt.Errorf("AsInt: unexpected kind: %s", val.Kind())
			}
			if w.finish != nil {
				if err := w.finish(); err != nil {
					return err
				}
			}
			return nil
		}
		return fmt.Errorf("AssignInt: %d is not a valid member of enum %s", i, w.schemaType.Name())
	default:
		return (*_assembler)(w).AssignInt(i)
	}
}

func (w *_assemblerRepr) AssignFloat(f float64) error {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Kinded:
		return w.asKinded(stg, datamodel.Kind_Float).AssignFloat(f)
	default:
		return (*_assembler)(w).AssignFloat(f)
	}
}

func (w *_assemblerRepr) AssignString(s string) error {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.StructRepresentation_Stringjoin:
		fields := w.schemaType.(*schema.TypeStruct).Fields()
		parts := strings.Split(s, stg.GetDelim())
		if len(parts) != len(fields) {
			return fmt.Errorf("bindnode TODO: len mismatch")
		}
		mapAsm, err := (*_assembler)(w).BeginMap(-1)
		if err != nil {
			return err
		}
		for i, field := range fields {
			entryAsm, err := mapAsm.AssembleEntry(field.Name())
			if err != nil {
				return err
			}
			entryAsm = assemblerRepr(entryAsm)
			if err := entryAsm.AssignString(parts[i]); err != nil {
				return err
			}
		}
		return mapAsm.Finish()
	case schema.UnionRepresentation_Kinded:
		return w.asKinded(stg, datamodel.Kind_String).AssignString(s)
	case schema.UnionRepresentation_Stringprefix:
		hasDelim := stg.GetDelim() != ""

		var prefix, remainder string
		if hasDelim {
			parts := strings.SplitN(s, stg.GetDelim(), 2)
			if len(parts) != 2 {
				return fmt.Errorf("schema rejects data: the union type %s expects delimiter %q, and it was not found in the data %q", w.schemaType.Name(), stg.GetDelim(), s)
			}
			prefix, remainder = parts[0], parts[1]
		}

		members := w.schemaType.(*schema.TypeUnion).Members()
		for idx, member := range members {
			descrm := stg.GetDiscriminant(member)
			if hasDelim {
				if stg.GetDiscriminant(member) != prefix {
					continue
				}
			} else {
				if !strings.HasPrefix(s, descrm) {
					continue
				}
				remainder = s[len(descrm):]
			}

			// TODO: DRY: this has much in common with the asKinded method; it differs only in that we picked idx already in a different way.
			w2 := *w
			goType := w.val.Field(idx).Type().Elem()
			valPtr := reflect.New(goType)
			w2.val = valPtr.Elem()
			w2.schemaType = member
			w2.finish = func() error {
				unionSetMember(w.val, idx, valPtr)
				if w.finish != nil {
					if err := w.finish(); err != nil {
						return err
					}
				}
				return nil
			}
			return w2.AssignString(remainder)
		}
		return fmt.Errorf("schema rejects data: the union type %s requires a known prefix, and it was not found in the data %q", w.schemaType.Name(), s)
	case schema.EnumRepresentation_String:
		// Note that we need to do a reverse lookup.
		for member, mapped := range stg {
			if mapped == s {
				return (*_assembler)(w).AssignString(member)
			}
		}
		members := w.schemaType.(*schema.TypeEnum).Members()
		for _, member := range members {
			if s == member {
				return (*_assembler)(w).AssignString(member)
			}
		}
		return fmt.Errorf("AssignString: %q is not a valid member of enum %s", s, w.schemaType.Name())
	case schema.EnumRepresentation_Int:
		return datamodel.ErrWrongKind{
			TypeName:        w.schemaType.Name(),
			MethodName:      "AssignString",
			AppropriateKind: datamodel.KindSet_JustInt,
			ActualKind:      datamodel.Kind_String,
		}
	default:
		return (*_assembler)(w).AssignString(s)
	}
}

func (w *_assemblerRepr) AssignBytes(p []byte) error {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Kinded:
		return w.asKinded(stg, datamodel.Kind_Bytes).AssignBytes(p)
	default:
		return (*_assembler)(w).AssignBytes(p)
	}
}

func (w *_assemblerRepr) AssignLink(link datamodel.Link) error {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Kinded:
		return w.asKinded(stg, datamodel.Kind_Link).AssignLink(link)
	default:
		return (*_assembler)(w).AssignLink(link)
	}
}

func (w *_assemblerRepr) AssignNode(node datamodel.Node) error {
	// TODO: attempt to take a shortcut, like assembler.AssignNode
	if uintNode, ok := node.(datamodel.UintNode); ok {
		return w.assignUInt(uintNode)
	}
	return datamodel.Copy(node, w)
}

func (w *_assemblerRepr) Prototype() datamodel.NodePrototype {
	panic("bindnode TODO: Assembler.Prototype")
}

type _structAssemblerRepr _structAssembler

func (w *_structAssemblerRepr) AssembleKey() datamodel.NodeAssembler {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.StructRepresentation_Map:
		return (*_structAssembler)(w).AssembleKey()
	case schema.StructRepresentation_Stringjoin,
		schema.StructRepresentation_StringPairs:
		// TODO: perhaps the ErrorWrongKind type should also be extended to explicitly describe whether the method was applied on bare DM, type-level, or repr-level.
		return _errorAssembler{datamodel.ErrWrongKind{
			TypeName:        w.schemaType.Name() + ".Repr",
			MethodName:      "AssembleKey",
			AppropriateKind: datamodel.KindSet_JustMap,
			ActualKind:      datamodel.Kind_String,
		}}
	case schema.StructRepresentation_Tuple, schema.StructRepresentation_ListPairs:
		return _errorAssembler{datamodel.ErrWrongKind{
			TypeName:        w.schemaType.Name() + ".Repr",
			MethodName:      "AssembleKey",
			AppropriateKind: datamodel.KindSet_JustMap,
			ActualKind:      datamodel.Kind_List,
		}}
	default:
		return _errorAssembler{fmt.Errorf("bindnode AssembleKey TODO: %T", stg)}
	}
}

func (w *_structAssemblerRepr) AssembleValue() datamodel.NodeAssembler {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.StructRepresentation_Map:
		key := w.curKey.val.String()
		revKey := inboundMappedKey(w.schemaType, stg, key)
		w.curKey.val.SetString(revKey)

		valAsm := (*_structAssembler)(w).AssembleValue()
		valAsm = assemblerRepr(valAsm)
		return valAsm
	default:
		return _errorAssembler{fmt.Errorf("bindnode AssembleValue TODO: %T", stg)}
	}
}

func (w *_structAssemblerRepr) AssembleEntry(k string) (datamodel.NodeAssembler, error) {
	if err := w.AssembleKey().AssignString(k); err != nil {
		return nil, err
	}
	am := w.AssembleValue()
	return am, nil
}

func (w *_structAssemblerRepr) Finish() error {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.StructRepresentation_Map:
		err := (*_structAssembler)(w).Finish()
		if err, ok := err.(schema.ErrMissingRequiredField); ok {
			for i, name := range err.Missing {
				serial := outboundMappedKey(stg, name)
				if serial != name {
					err.Missing[i] += fmt.Sprintf(" (serial:%q)", serial)
				}
			}
		}
		return err
	default:
		return fmt.Errorf("bindnode Finish TODO: %T", stg)
	}
}

func (w *_structAssemblerRepr) KeyPrototype() datamodel.NodePrototype {
	panic("bindnode TODO")
}

func (w *_structAssemblerRepr) ValuePrototype(k string) datamodel.NodePrototype {
	panic("bindnode TODO: struct ValuePrototype")
}

type _mapAssemblerRepr _mapAssembler

func (w *_mapAssemblerRepr) AssembleKey() datamodel.NodeAssembler {
	asm := (*_mapAssembler)(w).AssembleKey()
	return (*_assemblerRepr)(asm.(*_assembler))
}

func (w *_mapAssemblerRepr) AssembleValue() datamodel.NodeAssembler {
	asm := (*_mapAssembler)(w).AssembleValue()
	return (*_assemblerRepr)(asm.(*_assembler))
}

func (w *_mapAssemblerRepr) AssembleEntry(k string) (datamodel.NodeAssembler, error) {
	if err := w.AssembleKey().AssignString(k); err != nil {
		return nil, err
	}
	am := w.AssembleValue()
	return am, nil
}

func (w *_mapAssemblerRepr) Finish() error {
	return (*_mapAssembler)(w).Finish()
}

func (w *_mapAssemblerRepr) KeyPrototype() datamodel.NodePrototype {
	panic("bindnode TODO")
}

func (w *_mapAssemblerRepr) ValuePrototype(k string) datamodel.NodePrototype {
	panic("bindnode TODO: struct ValuePrototype")
}

type _listStructAssemblerRepr _structAssembler

func (w *_listStructAssemblerRepr) AssembleValue() datamodel.NodeAssembler {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.StructRepresentation_Tuple:
		fields := w.schemaType.Fields()
		if w.nextIndex >= len(fields) {
			return _errorAssembler{datamodel.ErrNotExists{
				Segment: datamodel.PathSegmentOfInt(int64(w.nextIndex)),
			}}
		}
		field := fields[w.nextIndex]
		w.doneFields[w.nextIndex] = true
		w.nextIndex++

		entryAsm, err := (*_structAssembler)(w).AssembleEntry(field.Name())
		if err != nil {
			return _errorAssembler{err}
		}
		entryAsm = assemblerRepr(entryAsm)
		return entryAsm
	case schema.StructRepresentation_ListPairs:
		return &_listpairsFieldAssemblerRepr{parent: (*_structAssembler)(w)}
	default:
		return _errorAssembler{fmt.Errorf("bindnode AssembleValue TODO: %T", stg)}
	}
}

func (w *_listStructAssemblerRepr) Finish() error {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.StructRepresentation_Tuple, schema.StructRepresentation_ListPairs:
		return (*_structAssembler)(w).Finish()
	default:
		return fmt.Errorf("bindnode Finish TODO: %T", stg)
	}
}

func (w *_listStructAssemblerRepr) ValuePrototype(idx int64) datamodel.NodePrototype {
	panic("bindnode TODO: list ValuePrototype")
}

type _listpairsFieldAssemblerRepr struct {
	parent *_structAssembler
}

func (w _listpairsFieldAssemblerRepr) BeginMap(int64) (datamodel.MapAssembler, error) {
	return nil, datamodel.ErrWrongKind{
		TypeName:        w.parent.schemaType.Name(),
		MethodName:      "BeginMap",
		AppropriateKind: datamodel.KindSet_JustList,
		ActualKind:      datamodel.Kind_Map,
	}
}
func (w *_listpairsFieldAssemblerRepr) BeginList(int64) (datamodel.ListAssembler, error) {
	return &_listpairsFieldListAssemblerRepr{parent: w.parent}, nil
}
func (w _listpairsFieldAssemblerRepr) AssignNull() error {
	return datamodel.ErrWrongKind{
		TypeName:        w.parent.schemaType.Name(),
		MethodName:      "AssignNull",
		AppropriateKind: datamodel.KindSet_JustList,
		ActualKind:      datamodel.Kind_Map,
	}
}
func (w _listpairsFieldAssemblerRepr) AssignBool(bool) error {
	return datamodel.ErrWrongKind{
		TypeName:        w.parent.schemaType.Name(),
		MethodName:      "AssignBool",
		AppropriateKind: datamodel.KindSet_JustList,
		ActualKind:      datamodel.Kind_Map,
	}
}
func (w _listpairsFieldAssemblerRepr) AssignInt(int64) error {
	return datamodel.ErrWrongKind{
		TypeName:        w.parent.schemaType.Name(),
		MethodName:      "AssignInt",
		AppropriateKind: datamodel.KindSet_JustList,
		ActualKind:      datamodel.Kind_Map,
	}
}
func (w _listpairsFieldAssemblerRepr) AssignFloat(float64) error {
	return datamodel.ErrWrongKind{
		TypeName:        w.parent.schemaType.Name(),
		MethodName:      "AssignFloat",
		AppropriateKind: datamodel.KindSet_JustList,
		ActualKind:      datamodel.Kind_Map,
	}
}
func (w _listpairsFieldAssemblerRepr) AssignString(string) error {
	return datamodel.ErrWrongKind{
		TypeName:        w.parent.schemaType.Name(),
		MethodName:      "AssignString",
		AppropriateKind: datamodel.KindSet_JustList,
		ActualKind:      datamodel.Kind_Map,
	}
}
func (w _listpairsFieldAssemblerRepr) AssignBytes([]byte) error {
	return datamodel.ErrWrongKind{
		TypeName:        w.parent.schemaType.Name(),
		MethodName:      "AssignBytes",
		AppropriateKind: datamodel.KindSet_JustList,
		ActualKind:      datamodel.Kind_Map,
	}
}
func (w _listpairsFieldAssemblerRepr) AssignLink(datamodel.Link) error {
	return datamodel.ErrWrongKind{
		TypeName:        w.parent.schemaType.Name(),
		MethodName:      "AssignLink",
		AppropriateKind: datamodel.KindSet_JustList,
		ActualKind:      datamodel.Kind_Map,
	}
}

func (w *_listpairsFieldAssemblerRepr) AssignNode(n datamodel.Node) error {
	return datamodel.Copy(n, w)
}

func (w _listpairsFieldAssemblerRepr) Prototype() datamodel.NodePrototype {
	panic("bindnode TODO: listpairs field Prototype")
}

type _listpairsFieldListAssemblerRepr struct {
	parent *_structAssembler
	idx    int
}

func (w *_listpairsFieldListAssemblerRepr) AssembleValue() datamodel.NodeAssembler {
	w.idx++
	switch w.idx {
	case 1:
		return w.parent.AssembleKey()
	case 2:
		asm := w.parent.AssembleValue()
		return assemblerRepr(asm.(*_assembler))
	default:
		return _errorAssembler{fmt.Errorf("bindnode: too many values in listpairs field")}
	}
}

func (w *_listpairsFieldListAssemblerRepr) Finish() error {
	return nil
}

func (w *_listpairsFieldListAssemblerRepr) ValuePrototype(idx int64) datamodel.NodePrototype {
	panic("bindnode TODO: listpairs field ValuePrototype")
}

// Note that lists do not have any representation strategy right now.
type _listAssemblerRepr _listAssembler

func (w *_listAssemblerRepr) AssembleValue() datamodel.NodeAssembler {
	asm := (*_listAssembler)(w).AssembleValue()
	return (*_assemblerRepr)(asm.(*_assembler))
}

func (w *_listAssemblerRepr) Finish() error {
	return (*_listAssembler)(w).Finish()
}

func (w *_listAssemblerRepr) ValuePrototype(idx int64) datamodel.NodePrototype {
	panic("bindnode TODO: list ValuePrototype")
}

type _unionAssemblerRepr _unionAssembler

func (w *_unionAssemblerRepr) AssembleKey() datamodel.NodeAssembler {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Keyed:
		return (*_unionAssembler)(w).AssembleKey()
	default:
		return _errorAssembler{fmt.Errorf("bindnode AssembleKey TODO: %T", stg)}
	}
}

func (w *_unionAssemblerRepr) AssembleValue() datamodel.NodeAssembler {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Keyed:
		key := w.curKey.val.String()
		revKey := inboundMappedType(w.schemaType, stg, key)
		w.curKey.val.SetString(revKey)

		valAsm := (*_unionAssembler)(w).AssembleValue()
		valAsm = assemblerRepr(valAsm)
		return valAsm
	default:
		return _errorAssembler{fmt.Errorf("bindnode AssembleValue TODO: %T", stg)}
	}
}

func (w *_unionAssemblerRepr) AssembleEntry(k string) (datamodel.NodeAssembler, error) {
	if err := w.AssembleKey().AssignString(k); err != nil {
		return nil, err
	}
	am := w.AssembleValue()
	return am, nil
}

func (w *_unionAssemblerRepr) Finish() error {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Keyed:
		return (*_unionAssembler)(w).Finish()
	default:
		return fmt.Errorf("bindnode Finish TODO: %T", stg)
	}
}

func (w *_unionAssemblerRepr) KeyPrototype() datamodel.NodePrototype {
	panic("bindnode TODO")
}

func (w *_unionAssemblerRepr) ValuePrototype(k string) datamodel.NodePrototype {
	panic("bindnode TODO: union ValuePrototype")
}

type _structIteratorRepr _structIterator

func (w *_structIteratorRepr) Next() (key, value datamodel.Node, _ error) {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.StructRepresentation_Map:
		for {
			key, value, err := (*_structIterator)(w).Next()
			if err != nil {
				return nil, nil, err
			}
			if value.IsAbsent() {
				continue
			}
			keyStr, _ := key.AsString()
			mappedKey := outboundMappedKey(stg, keyStr)
			if mappedKey != keyStr {
				key = basicnode.NewString(mappedKey)
			}
			return key, reprNode(value), nil
		}
	default:
		return nil, nil, fmt.Errorf("bindnode Next TODO: %T", stg)
	}
}

func (w *_structIteratorRepr) Done() bool {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.StructRepresentation_Map:
		// TODO: the fact that repr map iterators skip absents should be
		// documented somewhere
		return w.nextIndex >= w.reprEnd
	default:
		panic(fmt.Sprintf("bindnode Done TODO: %T", stg))
	}
}

type _unionIteratorRepr _unionIterator

func (w *_unionIteratorRepr) Next() (key, value datamodel.Node, _ error) {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Keyed:
		key, value, err := (*_unionIterator)(w).Next()
		if err != nil {
			return nil, nil, err
		}
		keyStr, _ := key.AsString()
		mappedKey := outboundMappedType(stg, keyStr)
		if mappedKey != keyStr {
			key = basicnode.NewString(mappedKey)
		}
		return key, reprNode(value), nil
	default:
		return nil, nil, fmt.Errorf("bindnode Next TODO: %T", stg)
	}
}

func (w *_unionIteratorRepr) Done() bool {
	switch stg := reprStrategy(w.schemaType).(type) {
	case schema.UnionRepresentation_Keyed:
		return (*_unionIterator)(w).Done()
	default:
		panic(fmt.Sprintf("bindnode Done TODO: %T", stg))
	}
}
