package proc

import (
	"go/constant"
	"unsafe"
)

// delve counterpart to runtime.moduledata
type moduleData struct {
	types, etypes uintptr
	typemapVar    *Variable
}

func (dbp *Process) loadModuleData() (err error) {
	dbp.loadModuleDataOnce.Do(func() {
		scope := &EvalScope{Thread: dbp.CurrentThread, PC: 0, CFA: 0}
		var md *Variable
		md, err = scope.packageVarAddr("runtime.firstmoduledata")
		if err != nil {
			return
		}

		for md.Addr != 0 {
			var typesVar, etypesVar, nextVar, typemapVar *Variable
			var types, etypes uint64

			if typesVar, err = md.structMember("types"); err != nil {
				return
			}
			if etypesVar, err = md.structMember("etypes"); err != nil {
				return
			}
			if nextVar, err = md.structMember("next"); err != nil {
				return
			}
			if typemapVar, err = md.structMember("typemap"); err != nil {
				return
			}
			if types, err = typesVar.asUint(); err != nil {
				return
			}
			if etypes, err = etypesVar.asUint(); err != nil {
				return
			}

			dbp.moduleData = append(dbp.moduleData, moduleData{uintptr(types), uintptr(etypes), typemapVar})

			md = nextVar.maybeDereference()
			if md.Unreadable != nil {
				err = md.Unreadable
				return
			}
		}
	})

	return
}

func (dbp *Process) resolveTypeOff(typeAddr uintptr, off uintptr) (*Variable, error) {
	// See runtime.(*_type).typeOff in $GOROOT/src/runtime/type.go
	if err := dbp.loadModuleData(); err != nil {
		return nil, err
	}

	var md *moduleData
	for i := range dbp.moduleData {
		if typeAddr >= dbp.moduleData[i].types && typeAddr < dbp.moduleData[i].etypes {
			md = &dbp.moduleData[i]
		}
	}

	rtyp, err := dbp.findType("runtime._type")
	if err != nil {
		return nil, err
	}

	if md == nil {
		v, err := dbp.reflectOffsMapAccess(off)
		if err != nil {
			return nil, err
		}
		v.loadValue(LoadConfig{false, 1, 0, 0, -1})
		addr, _ := constant.Int64Val(v.Value)
		return v.newVariable(v.Name, uintptr(addr), rtyp), nil
	}

	if t, _ := md.typemapVar.mapAccess(newConstant(constant.MakeUint64(uint64(off)), dbp.CurrentThread)); t != nil {
		return t, nil
	}

	res := md.types + uintptr(off)

	return dbp.CurrentThread.newVariable("", res, rtyp), nil
}

func (dbp *Process) resolveNameOff(typeAddr uintptr, off uintptr) (name, tag string, pkgpathoff int32, err error) {
	// See runtime.resolveNameOff in $GOROOT/src/runtime/type.go
	if err = dbp.loadModuleData(); err != nil {
		return "", "", 0, err
	}

	for _, md := range dbp.moduleData {
		if typeAddr >= md.types && typeAddr < md.etypes {
			return dbp.loadName(md.types + off)
		}
	}

	v, err := dbp.reflectOffsMapAccess(off)
	if err != nil {
		return "", "", 0, err
	}

	resv := v.maybeDereference()
	if resv.Unreadable != nil {
		return "", "", 0, resv.Unreadable
	}

	return dbp.loadName(resv.Addr)
}

func (dbp *Process) reflectOffsMapAccess(off uintptr) (*Variable, error) {
	scope := &EvalScope{Thread: dbp.CurrentThread, PC: 0, CFA: 0}
	reflectOffs, err := scope.packageVarAddr("runtime.reflectOffs")
	if err != nil {
		return nil, err
	}

	reflectOffsm, err := reflectOffs.structMember("m")
	if err != nil {
		return nil, err
	}

	return reflectOffsm.mapAccess(newConstant(constant.MakeUint64(uint64(off)), dbp.CurrentThread))
}

const (
	// flags for the name struct (see 'type name struct' in $GOROOT/src/reflect/type.go)
	nameflagExported = 1 << 0
	nameflagHasTag   = 1 << 1
	nameflagHasPkg   = 1 << 2
)

func (dbp *Process) loadName(addr uintptr) (name, tag string, pkgpathoff int32, err error) {
	off := addr
	namedata, err := dbp.CurrentThread.readMemory(off, 3)
	off += 3
	if err != nil {
		return "", "", 0, err
	}

	namelen := uint16(namedata[1]<<8) | uint16(namedata[2])

	rawstr, err := dbp.CurrentThread.readMemory(off, int(namelen))
	off += uintptr(namelen)
	if err != nil {
		return "", "", 0, err
	}

	name = string(rawstr)

	if namedata[0]&nameflagHasTag != 0 {
		taglendata, err := dbp.CurrentThread.readMemory(off, 2)
		off += 2
		if err != nil {
			return "", "", 0, err
		}
		taglen := uint16(taglendata[0]<<8) | uint16(taglendata[1])

		rawstr, err := dbp.CurrentThread.readMemory(off, int(taglen))
		off += uintptr(taglen)
		if err != nil {
			return "", "", 0, err
		}

		tag = string(rawstr)
	}

	if namedata[0]&nameflagHasPkg != 0 {
		pkgdata, err := dbp.CurrentThread.readMemory(off, 4)
		if err != nil {
			return "", "", 0, err
		}

		// see func pkgPath in $GOROOT/src/reflect/type.go
		copy((*[4]byte)(unsafe.Pointer(&pkgpathoff))[:], pkgdata)
	}

	return name, tag, pkgpathoff, nil
}
