package checker

import (
	"maps"

	"github.com/microsoft/typescript-go/internal/ast"
	"github.com/microsoft/typescript-go/internal/tracing"
)

// Tracer records types and trace events during type checking. A nil *Tracer
// is a valid no-op, so call sites can use `if tr := c.tracer; tr != nil` to
// gate work that only matters under --generateTrace.
type Tracer struct {
	tracing      *tracing.Tracing
	recorder     tracing.Tracer
	checkerIndex int
}

// NewTracer creates a Tracer for the given checker index that records both
// type-creation events and trace events through the provided tracing session.
func NewTracer(tr *tracing.Tracing, checkerIndex int) *Tracer {
	return &Tracer{tracing: tr, recorder: tr.NewTypeTracer(checkerIndex), checkerIndex: checkerIndex}
}

func (t *Tracer) RecordType(typ *Type) {
	t.recorder.RecordType(wrapType(typ))
}

func (t *Tracer) Push(phase tracing.Phase, name string, args map[string]any, separateBeginAndEnd bool) func() {
	if !separateBeginAndEnd {
		return t.tracing.Push(phase, name, t.copyWithCheckerIndex(args), separateBeginAndEnd)
	}

	args, restore := t.temporarilyAddCheckerIndex(args)
	pop := t.tracing.Push(phase, name, args, separateBeginAndEnd)
	restore()

	return func() {
		_, restoreEndArgs := t.temporarilyAddCheckerIndex(args)
		defer restoreEndArgs()
		pop()
	}
}

func (t *Tracer) Instant(phase tracing.Phase, name string, args map[string]any) {
	t.tracing.Instant(phase, name, t.copyWithCheckerIndex(args))
}

func (t *Tracer) copyWithCheckerIndex(args map[string]any) map[string]any {
	withCheckerIndex := make(map[string]any, len(args)+1)
	maps.Copy(withCheckerIndex, args)
	withCheckerIndex["checkerId"] = t.checkerIndex
	return withCheckerIndex
}

func (t *Tracer) temporarilyAddCheckerIndex(args map[string]any) (map[string]any, func()) {
	if args == nil {
		args = map[string]any{}
	}

	previous, hadPrevious := args["checkerId"]
	args["checkerId"] = t.checkerIndex

	return args, func() {
		if hadPrevious {
			args["checkerId"] = previous
		} else {
			delete(args, "checkerId")
		}
	}
}

// tracedTypeAdapter adapts a Type to the tracing.TracedType interface
type tracedTypeAdapter struct {
	t       *Type
	checker *Checker
}

var _ tracing.TracedType = (*tracedTypeAdapter)(nil)

func (a *tracedTypeAdapter) Id() uint32 {
	return uint32(a.t.id)
}

func (a *tracedTypeAdapter) FormatFlags() []string {
	return FormatTypeFlags(a.t.flags)
}

func (a *tracedTypeAdapter) IsConditional() bool {
	return a.t.flags&TypeFlagsConditional != 0
}

func (a *tracedTypeAdapter) Symbol() *ast.Symbol {
	return a.t.symbol
}

func (a *tracedTypeAdapter) AliasSymbol() *ast.Symbol {
	if a.t.alias == nil {
		return nil
	}
	return a.t.alias.Symbol()
}

func (a *tracedTypeAdapter) AliasTypeArguments() []tracing.TracedType {
	if a.t.alias == nil {
		return nil
	}
	return wrapTypes(a.t.alias.TypeArguments())
}

func (a *tracedTypeAdapter) IntrinsicName() string {
	if a.t.flags&TypeFlagsIntrinsic == 0 {
		return ""
	}
	data, ok := a.t.data.(*IntrinsicType)
	if !ok {
		return ""
	}
	return data.intrinsicName
}

func (a *tracedTypeAdapter) UnionTypes() []tracing.TracedType {
	if a.t.flags&TypeFlagsUnion == 0 {
		return nil
	}
	return wrapTypes(a.t.AsUnionType().types)
}

func (a *tracedTypeAdapter) IntersectionTypes() []tracing.TracedType {
	if a.t.flags&TypeFlagsIntersection == 0 {
		return nil
	}
	return wrapTypes(a.t.AsIntersectionType().types)
}

func (a *tracedTypeAdapter) IndexType() tracing.TracedType {
	if a.t.flags&TypeFlagsIndex == 0 {
		return nil
	}
	t := a.t.AsIndexType().target
	if t == nil {
		return nil
	}
	return wrapType(t)
}

func (a *tracedTypeAdapter) IndexedAccessObjectType() tracing.TracedType {
	if a.t.flags&TypeFlagsIndexedAccess == 0 {
		return nil
	}
	t := a.t.AsIndexedAccessType().objectType
	if t == nil {
		return nil
	}
	return wrapType(t)
}

func (a *tracedTypeAdapter) IndexedAccessIndexType() tracing.TracedType {
	if a.t.flags&TypeFlagsIndexedAccess == 0 {
		return nil
	}
	t := a.t.AsIndexedAccessType().indexType
	if t == nil {
		return nil
	}
	return wrapType(t)
}

func (a *tracedTypeAdapter) ConditionalCheckType() tracing.TracedType {
	if a.t.flags&TypeFlagsConditional == 0 {
		return nil
	}
	t := a.t.AsConditionalType().checkType
	if t == nil {
		return nil
	}
	return wrapType(t)
}

func (a *tracedTypeAdapter) ConditionalExtendsType() tracing.TracedType {
	if a.t.flags&TypeFlagsConditional == 0 {
		return nil
	}
	t := a.t.AsConditionalType().extendsType
	if t == nil {
		return nil
	}
	return wrapType(t)
}

func (a *tracedTypeAdapter) ConditionalTrueType() tracing.TracedType {
	if a.t.flags&TypeFlagsConditional == 0 {
		return nil
	}
	t := a.t.AsConditionalType().resolvedTrueType
	if t == nil {
		return nil
	}
	return wrapType(t)
}

func (a *tracedTypeAdapter) ConditionalFalseType() tracing.TracedType {
	if a.t.flags&TypeFlagsConditional == 0 {
		return nil
	}
	t := a.t.AsConditionalType().resolvedFalseType
	if t == nil {
		return nil
	}
	return wrapType(t)
}

func (a *tracedTypeAdapter) SubstitutionBaseType() tracing.TracedType {
	if a.t.flags&TypeFlagsSubstitution == 0 {
		return nil
	}
	t := a.t.AsSubstitutionType().baseType
	if t == nil {
		return nil
	}
	return wrapType(t)
}

func (a *tracedTypeAdapter) SubstitutionConstraintType() tracing.TracedType {
	if a.t.flags&TypeFlagsSubstitution == 0 {
		return nil
	}
	t := a.t.AsSubstitutionType().constraint
	if t == nil {
		return nil
	}
	return wrapType(t)
}

func (a *tracedTypeAdapter) ReferenceTarget() tracing.TracedType {
	if a.t.flags&TypeFlagsObject == 0 || a.t.objectFlags&ObjectFlagsReference == 0 {
		return nil
	}
	t := a.t.AsTypeReference().target
	if t == nil {
		return nil
	}
	return wrapType(t)
}

func (a *tracedTypeAdapter) ReferenceTypeArguments() []tracing.TracedType {
	if a.t.flags&TypeFlagsObject == 0 || a.t.objectFlags&ObjectFlagsReference == 0 {
		return nil
	}
	return wrapTypes(a.t.AsTypeReference().resolvedTypeArguments)
}

func (a *tracedTypeAdapter) ReferenceNode() *ast.Node {
	if a.t.flags&TypeFlagsObject == 0 || a.t.objectFlags&ObjectFlagsReference == 0 {
		return nil
	}
	return a.t.AsTypeReference().node
}

func (a *tracedTypeAdapter) ReverseMappedSourceType() tracing.TracedType {
	if a.t.flags&TypeFlagsObject == 0 || a.t.objectFlags&ObjectFlagsReverseMapped == 0 {
		return nil
	}
	t := a.t.AsReverseMappedType().source
	if t == nil {
		return nil
	}
	return wrapType(t)
}

func (a *tracedTypeAdapter) ReverseMappedMappedType() tracing.TracedType {
	if a.t.flags&TypeFlagsObject == 0 || a.t.objectFlags&ObjectFlagsReverseMapped == 0 {
		return nil
	}
	t := a.t.AsReverseMappedType().mappedType
	if t == nil {
		return nil
	}
	return wrapType(t)
}

func (a *tracedTypeAdapter) ReverseMappedConstraintType() tracing.TracedType {
	if a.t.flags&TypeFlagsObject == 0 || a.t.objectFlags&ObjectFlagsReverseMapped == 0 {
		return nil
	}
	t := a.t.AsReverseMappedType().constraintType
	if t == nil {
		return nil
	}
	return wrapType(t)
}

func (a *tracedTypeAdapter) EvolvingArrayElementType() tracing.TracedType {
	if a.t.flags&TypeFlagsObject == 0 || a.t.objectFlags&ObjectFlagsEvolvingArray == 0 {
		return nil
	}
	t := a.t.AsEvolvingArrayType().elementType
	if t == nil {
		return nil
	}
	return wrapType(t)
}

func (a *tracedTypeAdapter) EvolvingArrayFinalType() tracing.TracedType {
	if a.t.flags&TypeFlagsObject == 0 || a.t.objectFlags&ObjectFlagsEvolvingArray == 0 {
		return nil
	}
	t := a.t.AsEvolvingArrayType().finalArrayType
	if t == nil {
		return nil
	}
	return wrapType(t)
}

func (a *tracedTypeAdapter) IsTuple() bool {
	return a.t.objectFlags&ObjectFlagsTuple != 0
}

func (a *tracedTypeAdapter) Pattern() *ast.Node {
	if a.checker == nil {
		return nil
	}
	return a.checker.patternForType[a.t]
}

func (a *tracedTypeAdapter) RecursionIdentity() any {
	return getRecursionIdentity(a.t).value
}

func (a *tracedTypeAdapter) Display() string {
	// Compute display text for types where it's valuable for trace analysis.
	// TypeScript only does this for Anonymous|Literal types, but we extend to
	// unions, intersections, and template literals since they often lack
	// firstDeclaration and the display text helps identify them.
	// Incomplete types during tracing can cause panics, which we intentionally
	// suppress (returning ""), matching TypeScript's try/catch around typeToString.
	if a.checker == nil {
		return ""
	}
	if a.t.objectFlags&ObjectFlagsAnonymous != 0 ||
		a.t.flags&(TypeFlagsLiteral|TypeFlagsTemplateLiteral|TypeFlagsUnion|TypeFlagsIntersection) != 0 {
		defer func() {
			_ = recover()
		}()
		return a.checker.TypeToString(a.t)
	}
	return ""
}

func wrapType(t *Type) tracing.TracedType {
	if t == nil {
		return nil
	}
	return &tracedTypeAdapter{t: t, checker: t.checker}
}

func wrapTypes(types []*Type) []tracing.TracedType {
	if len(types) == 0 {
		return nil
	}
	result := make([]tracing.TracedType, len(types))
	for i, t := range types {
		result[i] = wrapType(t)
	}
	return result
}
