package schema

import (
	"fmt"

	"github.com/ipld/go-ipld-prime/datamodel"
)

// Everything in this file is __a temporary hack__ and will be __removed__.
//
// These methods will only hang around until more of the "ast" packages are finished;
// thereafter, building schema.Type and schema.TypeSystem values will only be
// possible through first constructing a schema AST, and *then* using Reify(),
// which will validate things correctly, cycle-check, cross-link, etc.
//
// (Meanwhile, we're using these methods in the codegen prototypes.)

// These methods use Type objects as parameters when pointing to other things,
//  but this is... turning out consistently problematic.
//   Even when we're doing this hacky direct-call doesn't-need-to-be-serializable temp stuff,
//    as written, this doesn't actually let us express cyclic things viably!
//   The same initialization questions are also going to come up again when we try to make
//    concrete values in the output of codegen.
// Maybe it's actually just a bad idea to have our reified Type types use Type pointers at all.
//  (I will never get tired of the tongue twisters, evidently.)
//  I'm not actually using that much, and it's always avoidable (it's trivial to replace with a map lookup bouncing through a 'ts' variable somewhere).
//  And having the AST gen'd types be... just... the thing... sounds nice.  It could save a lot of work.
//   (It would mean the golang types don't tell you whether the values have been checked for global properties or not, but, eh.)
//   (It's not really compatible with "Prototype and Type are the same thing for codegen'd stuff", either (or, we need more interfaces, and to *really* lean into them), but maybe that's okay.)

func SpawnTypeSystem(types ...Type) (*TypeSystem, []error) {
	ts := TypeSystem{}
	ts.Init()
	for _, typ := range types {
		ts.Accumulate(typ)
	}
	if errs := ts.ValidateGraph(); errs != nil {
		return nil, errs
	}
	return &ts, nil
}
func MustTypeSystem(types ...Type) *TypeSystem {
	if ts, err := SpawnTypeSystem(types...); err != nil {
		panic(err)
	} else {
		return ts
	}
}

func SpawnString(name TypeName) *TypeString {
	return &TypeString{typeBase{name, nil}}
}

func SpawnBool(name TypeName) *TypeBool {
	return &TypeBool{typeBase{name, nil}}
}

func SpawnInt(name TypeName) *TypeInt {
	return &TypeInt{typeBase{name, nil}}
}

func SpawnFloat(name TypeName) *TypeFloat {
	return &TypeFloat{typeBase{name, nil}}
}

func SpawnBytes(name TypeName) *TypeBytes {
	return &TypeBytes{typeBase{name, nil}}
}

func SpawnLink(name TypeName) *TypeLink {
	return &TypeLink{typeBase{name, nil}, "", false}
}

func SpawnLinkReference(name TypeName, pointsTo TypeName) *TypeLink {
	return &TypeLink{typeBase{name, nil}, pointsTo, true}
}

func SpawnList(name TypeName, valueType TypeName, nullable bool) *TypeList {
	return &TypeList{typeBase{name, nil}, false, valueType, nullable}
}

func SpawnMap(name TypeName, keyType TypeName, valueType TypeName, nullable bool) *TypeMap {
	return &TypeMap{typeBase{name, nil}, false, keyType, valueType, nullable}
}

func SpawnAny(name TypeName) *TypeAny {
	return &TypeAny{typeBase{name, nil}}
}

func SpawnStruct(name TypeName, fields []StructField, repr StructRepresentation) *TypeStruct {
	v := &TypeStruct{
		typeBase{name, nil},
		fields,
		make(map[string]StructField, len(fields)),
		repr,
	}
	for i := range fields {
		fields[i].parent = v
		v.fieldsMap[fields[i].name] = fields[i]
	}
	switch repr.(type) {
	case StructRepresentation_Stringjoin:
		for _, f := range fields {
			if f.IsMaybe() {
				panic("neither nullable nor optional is supported on struct stringjoin representation")
			}
		}
	case nil:
		v.representation = SpawnStructRepresentationMap(nil)
	}
	return v
}
func SpawnStructField(name string, typ TypeName, optional bool, nullable bool) StructField {
	return StructField{nil /*populated later*/, name, typ, optional, nullable}
}
func SpawnStructRepresentationMap(renames map[string]string) StructRepresentation_Map {
	return StructRepresentation_Map{renames, nil}
}
func SpawnStructRepresentationMap2(renames map[string]string, implicits map[string]ImplicitValue) StructRepresentation_Map {
	return StructRepresentation_Map{renames, implicits}
}
func SpawnStructRepresentationTuple() StructRepresentation_Tuple {
	return StructRepresentation_Tuple{}
}
func SpawnStructRepresentationListPairs() StructRepresentation_ListPairs {
	return StructRepresentation_ListPairs{}
}
func SpawnStructRepresentationStringjoin(delim string) StructRepresentation_Stringjoin {
	return StructRepresentation_Stringjoin{delim}
}

func SpawnUnion(name TypeName, members []TypeName, repr UnionRepresentation) *TypeUnion {
	return &TypeUnion{typeBase{name, nil}, members, repr}
}
func SpawnUnionRepresentationKeyed(table map[string]TypeName) UnionRepresentation_Keyed {
	return UnionRepresentation_Keyed{table}
}
func SpawnUnionRepresentationKinded(table map[datamodel.Kind]TypeName) UnionRepresentation_Kinded {
	return UnionRepresentation_Kinded{table}
}
func SpawnUnionRepresentationStringprefix(delim string, table map[string]TypeName) UnionRepresentation_Stringprefix {
	return UnionRepresentation_Stringprefix{delim, table}
}
func SpawnUnionRepresentationInline(discriminantKey string, table map[string]TypeName) UnionRepresentation_Inline {
	return UnionRepresentation_Inline{discriminantKey, table}
}

func SpawnEnum(name TypeName, members []string, repr EnumRepresentation) *TypeEnum {
	return &TypeEnum{typeBase{name, nil}, members, repr}
}

// Utility function adding default basic types to schema type system
func SpawnDefaultBasicTypes(ts *TypeSystem) {
	ts.Accumulate(SpawnBool("Bool"))
	ts.Accumulate(SpawnInt("Int"))
	ts.Accumulate(SpawnFloat("Float"))
	ts.Accumulate(SpawnString("String"))
	ts.Accumulate(SpawnBytes("Bytes"))

	ts.Accumulate(SpawnAny("Any"))

	ts.Accumulate(SpawnMap("Map", "String", "Any", false))
	ts.Accumulate(SpawnList("List", "Any", false))

	// Should be &Any, really.
	ts.Accumulate(SpawnLink("Link"))

	// TODO: schema package lacks support?
	// ts.Accumulate(schema.SpawnUnit("Null", NullRepr))
}

// Clone creates a copy of a type that is not in the original's universe (so it can be used elsewhere safely)
func Clone(typ Type) Type {
	switch kindedType := typ.(type) {
	case *TypeBool:
		return SpawnBool(kindedType.Name())
	case *TypeString:
		return SpawnString(kindedType.Name())
	case *TypeBytes:
		return SpawnBytes(kindedType.Name())
	case *TypeInt:
		return SpawnInt(kindedType.Name())
	case *TypeFloat:
		return SpawnFloat(kindedType.Name())
	case *TypeAny:
		return SpawnAny(kindedType.Name())
	case *TypeMap:
		return SpawnMap(kindedType.Name(),
			kindedType.KeyType().Name(),
			kindedType.ValueType().Name(),
			kindedType.ValueIsNullable())
	case *TypeList:
		return SpawnList(kindedType.Name(), kindedType.ValueType().Name(), kindedType.ValueIsNullable())
	case *TypeLink:
		if kindedType.HasReferencedType() {
			return SpawnLinkReference(kindedType.Name(), kindedType.ReferencedType().Name())
		} else {
			return SpawnLink(kindedType.Name())
		}
	case *TypeUnion:
		members := kindedType.Members()
		memberNames := make([]TypeName, 0, len(members))
		for _, member := range members {
			memberNames = append(memberNames, member.Name())
		}
		return SpawnUnion(kindedType.Name(), memberNames, kindedType.RepresentationStrategy())
	case *TypeStruct:
		oldFields := kindedType.Fields()
		newFields := make([]StructField, 0, len(oldFields))
		for _, oldField := range oldFields {
			newFields = append(newFields, SpawnStructField(oldField.Name(), oldField.Type().Name(), oldField.IsOptional(), oldField.IsNullable()))
		}
		return SpawnStruct(kindedType.Name(), newFields, kindedType.RepresentationStrategy())
	case *TypeEnum:
		return SpawnEnum(kindedType.Name(), kindedType.Members(), kindedType.RepresentationStrategy())
	default:
		panic("unexpected type, don't know how to clone")
	}
}

func MergeTypeSystem(target *TypeSystem, source *TypeSystem, ignoreDups bool) {
	for _, name := range source.Names() {
		typ := Clone(source.TypeByName(name))
		if ignoreDups {
			accumulateWithRecovery(target, typ)
		} else {
			target.Accumulate(typ)
		}
	}
}

func accumulateWithRecovery(ts *TypeSystem, typ Type) {
	defer func() {
		_ = recover()
	}()
	ts.Accumulate(typ)
}

// The methods relating to TypeSystem are also mutation-heavy and placeholdery.

func (ts *TypeSystem) Init() {
	ts.namedTypes = make(map[TypeName]Type)
}
func (ts *TypeSystem) Accumulate(typ Type) {
	typ._Type(ts)
	name := typ.Name()
	if _, ok := ts.namedTypes[name]; ok {
		panic(fmt.Sprintf("duplicate type name: %s", name))
	}
	ts.namedTypes[name] = typ
	ts.names = append(ts.names, name)
}
func (ts TypeSystem) GetTypes() map[TypeName]Type {
	return ts.namedTypes
}
func (ts TypeSystem) TypeByName(n string) Type {
	return ts.namedTypes[n]
}
func (ts TypeSystem) Names() []TypeName {
	return ts.names
}

// ValidateGraph checks that all type names referenced are defined.
//
// It does not do any other validations of individual type's sensibleness
// (that should've happened when they were created
// (although also note many of those validates are NYI,
// and are roadmapped for after we research self-hosting)).
func (ts TypeSystem) ValidateGraph() []error {
	var ee []error
	for tn, t := range ts.namedTypes {
		switch t2 := t.(type) {
		case *TypeBool,
			*TypeInt,
			*TypeFloat,
			*TypeString,
			*TypeBytes,
			*TypeEnum:
			continue // nothing to check: these are leaf nodes and refer to no other types.
		case *TypeLink:
			if !t2.hasReferencedType {
				continue
			}
			if _, ok := ts.namedTypes[t2.referencedType]; !ok {
				ee = append(ee, fmt.Errorf("type %s refers to missing type %s (as link reference type)", tn, t2.referencedType))
			}
		case *TypeStruct:
			for _, f := range t2.fields {
				if _, ok := ts.namedTypes[f.typ]; !ok {
					ee = append(ee, fmt.Errorf("type %s refers to missing type %s (in field %q)", tn, f.typ, f.name))
				}
			}
		case *TypeMap:
			if _, ok := ts.namedTypes[t2.keyType]; !ok {
				ee = append(ee, fmt.Errorf("type %s refers to missing type %s (as key type)", tn, t2.keyType))
			}
			if _, ok := ts.namedTypes[t2.valueType]; !ok {
				ee = append(ee, fmt.Errorf("type %s refers to missing type %s (as value type)", tn, t2.valueType))
			}
		case *TypeList:
			if _, ok := ts.namedTypes[t2.valueType]; !ok {
				ee = append(ee, fmt.Errorf("type %s refers to missing type %s (as value type)", tn, t2.valueType))
			}
		case *TypeUnion:
			for _, mn := range t2.members {
				if _, ok := ts.namedTypes[mn]; !ok {
					ee = append(ee, fmt.Errorf("type %s refers to missing type %s (as a member)", tn, mn))
				}
			}
		}
	}
	return ee
}
