// Copyright 2026 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package app

import (
	"fmt"
	"reflect"
	"sync"
)

var typeInitFunctions = []func(){}
var typeInitMutex = sync.Mutex{}

func LocalTypeInit(localIdInitialiser func(govalue any) TypeId,
	publicIdInitialiser func(domainValue any, id []byte, idVersion Version) (TypeId, error)) {
	typeInitMutex.Lock()
	if NewLocalTypeId == nil {
		NewLocalTypeId = localIdInitialiser
		for _, initFunction := range typeInitFunctions {
			initFunction()
		}
		typeInitFunctions = []func(){}
	}
	typeInitMutex.Unlock()
}

var NewLocalTypeId func(govalue any) TypeId

var typeIds = map[string]TypeId{}
var typeIdsMutex = &sync.RWMutex{}

func MustNewId(domain Domain, id ident, idVersion Version) TypeId {
	typeid, err := NewTypeId(domain, id, idVersion)

	if err != nil {
		panic(fmt.Sprintf("Can't create new type id: %s", err))
	}

	return typeid
}

func NewTypeId(domain Domain, id ident, idVersion Version) (TypeId, error) {

	base, err := NewId(domain, id /*idVersion*/)

	if err != nil {
		return nil, err
	}

	typeId := &typeId{base}
	value := typeId.String()

	typeIdsMutex.RLock()
	if registeredId, ok := typeIds[value]; ok {
		typeIdsMutex.RUnlock()
		return registeredId, nil
	}

	typeIdsMutex.RUnlock()
	typeIdsMutex.Lock()

	if registeredId, ok := typeIds[value]; ok {
		typeIdsMutex.Unlock()
		return registeredId, nil
	}

	typeIds[value] = typeId

	typeIdsMutex.Unlock()
	return typeId, nil
}

type TypeId interface {
	Id
	AssignableTo(typeId TypeId) bool
	TypeInfo() TypeInfo
}

type TypeInfo interface {
	Name() string
	LocalType() reflect.Type
}

type Typed interface {
	TypeId() TypeId
}

type typeId struct {
	Id
}

func (id *typeId) AssignableTo(typeId TypeId) bool {
	return id.CompareTo(typeId) == 0
}

func (id *typeId) TypeInfo() TypeInfo {
	return nil
}

func RegisterId(typeid TypeId) {
	typeIdsMutex.Lock()
	typeIds[typeid.String()] = typeid
	typeIdsMutex.Unlock()
}

type TypeArray []TypeId

func (a TypeArray) Len() int {
	return len(a)
}

func (a TypeArray) String() string {
	argTypes := make([]string, a.Len())
	for i, argType := range a {
		argTypes[i] = argType.String()
	}

	return fmt.Sprintf("%s", argTypes)
}

func (a TypeArray) Type(at any) TypeId {
	return a[at.(int)]
}

type TypeMap map[string]TypeId

func (m TypeMap) Len() int {
	return len(m)
}

func (m TypeMap) String() string {
	argTypes := make([]string, m.Len())
	var i = 0
	for argName, argType := range m {
		argTypes[i] = fmt.Sprintf("%s=%s", argName, argType.String())
		i++
	}

	return fmt.Sprintf("%s", argTypes)
}

func (m TypeMap) Type(at any) TypeId {
	return m[at.(string)]
}

type ArgTypes interface {
	Len() int
	String() string
	Type(at any) TypeId
}

type Args interface {
	Arg(key any) any
	Len() int
}

type ArgArray []any

func (a ArgArray) Arg(at any) any {
	return a[at.(int)]
}

func (a ArgArray) Len() int {
	return len(a)
}

type ArgMap map[string]any

func (m ArgMap) Arg(at any) any {
	return m[at.(string)]
}

func (m ArgMap) Len() int {
	return len(m)
}

// LocalTypeDomain is the domain used to build type IDs for Go types via
// TypeIdOf / newIdFor. It is initialized at package init to a local named
// domain so TypeIds always have a non-nil domain and a stable string
// representation.
var LocalTypeDomain Domain = func() Domain {
	d, err := NewNamedDomain[string]("local.type")
	if err != nil {
		// This only fails if domain construction itself is broken, in which
		// case the whole package would be unusable. Panic here so the
		// problem surfaces during init rather than later as a nil deref.
		panic(fmt.Sprintf("failed to init LocalTypeDomain: %v", err))
	}
	return d
}()
var Trace = false

var typeids = map[reflect.Type]TypeId{}
var typeidsMutex = &sync.RWMutex{}

func TypeIdOf(govalue any) TypeId {
	var err error
	var gotype reflect.Type
	var ok bool

	if typeid, ok := govalue.(*GoTypeId); ok {
		return typeid
	}

	if gotype, ok = govalue.(reflect.Type); !ok {
		gotype = reflect.TypeOf(govalue)
	}

	if gotype == nil {
		return nil
	}

	if gotype.Kind() == reflect.Pointer {
		gotype = gotype.Elem()
	}

	typeidsMutex.RLock()
	id, ok := typeids[gotype]
	typeidsMutex.RUnlock()

	if !ok {
		typeidsMutex.Lock()
		id, err = newIdFor(gotype)

		if err != nil {
			typeidsMutex.Unlock()
			panic(fmt.Sprintf("Can't create type id for:%+v\n", err))
		}
		typeids[gotype] = id
		typeidsMutex.Unlock()
	}

	return id
}

type GoTypeId struct {
	Id
	typeInfo TypeInfo
}

func (ti *GoTypeId) TypeInfo() TypeInfo {
	return ti.typeInfo
}

func (ti *GoTypeId) AssignableTo(typeId TypeId) bool {
	if assignableId, ok := typeId.(*GoTypeId); ok {
		thisType := ti.typeInfo.LocalType()
		assignableType := assignableId.typeInfo.LocalType()

		if Trace {
			fmt.Printf("Go Type Assign %v->%v (%v)\n", thisType, assignableType,
				thisType.AssignableTo(assignableType) ||
					(thisType.Kind() == reflect.Pointer && (assignableType.Kind() != reflect.Pointer &&
						thisType.AssignableTo(reflect.PointerTo(assignableType)))) ||
					((assignableType.Kind() == reflect.Pointer || assignableType.Kind() == reflect.Interface) &&
						(thisType.Kind() != reflect.Pointer && reflect.PointerTo(thisType).AssignableTo(assignableType))))
		}

		if thisType.AssignableTo(assignableType) {
			return true
		}

		if thisType.Kind() == reflect.Pointer {
			return (assignableType.Kind() != reflect.Pointer &&
				thisType.AssignableTo(reflect.PointerTo(assignableType)))
		}

		if assignableType.Kind() == reflect.Pointer || assignableType.Kind() == reflect.Interface {
			return (thisType.Kind() != reflect.Pointer &&
				reflect.PointerTo(thisType).AssignableTo(assignableType))
		}
	}

	return false
}

func newIdFor(gotype reflect.Type) (TypeId, error) {

	typeName := gotype.Name()

	if pkgPath := gotype.PkgPath(); len(pkgPath) > 0 {
		typeName = fmt.Sprintf("%s.%s", pkgPath, typeName)
	}

	base, err := NewId(LocalTypeDomain, typeName)

	if err != nil {
		return nil, err
	}

	id := &GoTypeId{base, NewTypeInfo(gotype)}

	RegisterId(id)

	return id, nil
}

type typeInfo struct {
	localType reflect.Type
}

func NewTypeInfo(localType reflect.Type) TypeInfo {
	return &typeInfo{localType}
}

func (info *typeInfo) Name() string {
	if info.localType != nil {
		return info.localType.Name()
	}

	return ""
}

func (info *typeInfo) LocalType() reflect.Type {
	return info.localType
}

func TypeIdValues(typeIds any) []TypeId {
	s := reflect.ValueOf(typeIds)

	if s.Kind() == reflect.Pointer {
		s = s.Elem()
	}

	var values []TypeId
	for i := 0; i < s.NumField(); i++ {
		if value, ok := reflect.TypeAssert[TypeId](s.Field(i)); ok {
			values = append(values, value)
		}
	}
	return values
}
