package fakereflect

import (
	"fmt"
	"go/types"
	"reflect"
)

type TypeAndCanAddr struct {
	Type    types.Type
	canAddr bool
}

type StructField struct {
	Index     []int
	Name      string
	Anonymous bool
	Tag       reflect.StructTag
	f         *types.Var
	Type      TypeAndCanAddr
}

func (sf StructField) IsExported() bool { return sf.f.Exported() }

func (t TypeAndCanAddr) Field(i int) StructField {
	st := t.Type.Underlying().(*types.Struct)
	f := st.Field(i)
	return StructField{
		f:         f,
		Index:     []int{i},
		Name:      f.Name(),
		Anonymous: f.Anonymous(),
		Tag:       reflect.StructTag(st.Tag(i)),
		Type: TypeAndCanAddr{
			Type:    f.Type(),
			canAddr: t.canAddr,
		},
	}
}

func (t TypeAndCanAddr) FieldByIndex(index []int) StructField {
	f := t.Field(index[0])
	for _, idx := range index[1:] {
		f = f.Type.Field(idx)
	}
	f.Index = index
	return f
}

func PtrTo(t TypeAndCanAddr) TypeAndCanAddr {
	// Note that we don't care about canAddr here because it's irrelevant to all uses of PtrTo
	return TypeAndCanAddr{Type: types.NewPointer(t.Type)}
}

func (t TypeAndCanAddr) CanAddr() bool { return t.canAddr }

func (t TypeAndCanAddr) Implements(ityp *types.Interface) bool {
	return types.Implements(t.Type, ityp)
}

func (t TypeAndCanAddr) IsSlice() bool {
	_, ok := t.Type.Underlying().(*types.Slice)
	return ok
}

func (t TypeAndCanAddr) IsArray() bool {
	_, ok := t.Type.Underlying().(*types.Array)
	return ok
}

func (t TypeAndCanAddr) IsPtr() bool {
	_, ok := t.Type.Underlying().(*types.Pointer)
	return ok
}

func (t TypeAndCanAddr) IsInterface() bool {
	_, ok := t.Type.Underlying().(*types.Interface)
	return ok
}

func (t TypeAndCanAddr) IsStruct() bool {
	_, ok := t.Type.Underlying().(*types.Struct)
	return ok
}

func (t TypeAndCanAddr) Name() string {
	named, ok := types.Unalias(t.Type).(*types.Named)
	if !ok {
		return ""
	}
	return named.Obj().Name()
}

func (t TypeAndCanAddr) NumField() int {
	return t.Type.Underlying().(*types.Struct).NumFields()
}

func (t TypeAndCanAddr) String() string {
	return t.Type.String()
}

func (t TypeAndCanAddr) Key() TypeAndCanAddr {
	return TypeAndCanAddr{Type: t.Type.Underlying().(*types.Map).Key()}
}

func (t TypeAndCanAddr) Elem() TypeAndCanAddr {
	switch typ := t.Type.Underlying().(type) {
	case *types.Pointer:
		return TypeAndCanAddr{
			Type:    typ.Elem(),
			canAddr: true,
		}
	case *types.Slice:
		return TypeAndCanAddr{
			Type:    typ.Elem(),
			canAddr: true,
		}
	case *types.Array:
		return TypeAndCanAddr{
			Type:    typ.Elem(),
			canAddr: t.canAddr,
		}
	case *types.Map:
		return TypeAndCanAddr{
			Type:    typ.Elem(),
			canAddr: false,
		}
	default:
		panic(fmt.Sprintf("unhandled type %T", typ))
	}
}
