//go:build ebpf
// +build ebpf

package ebpf

// #include "./trace_probe/function_vals.bpf.h"
import "C"
import (
	_ "embed"
	"encoding/binary"
	"errors"
	"reflect"
	"runtime"
	"sync"
	"unsafe"

	"github.com/go-delve/delve/pkg/dwarf/godwarf"
	"github.com/go-delve/delve/pkg/dwarf/op"

	bpf "github.com/aquasecurity/libbpfgo"
	"github.com/aquasecurity/libbpfgo/helpers"
)

//go:embed trace_probe/trace.o
var TraceProbeBytes []byte

const FakeAddressBase = 0xbeed000000000000

type EBPFContext struct {
	bpfModule  *bpf.Module
	bpfProg    *bpf.BPFProg
	bpfEvents  chan []byte
	bpfRingBuf *bpf.RingBuffer
	bpfArgMap  *bpf.BPFMap

	parsedBpfEvents []RawUProbeParams
	m               sync.Mutex
}

func (ctx *EBPFContext) Close() {
	if ctx.bpfModule != nil {
		ctx.bpfModule.Close()
	}
}

func (ctx *EBPFContext) AttachUprobe(pid int, name string, offset uint32) error {
	if ctx.bpfProg == nil {
		return errors.New("no eBPF program loaded")
	}
	_, err := ctx.bpfProg.AttachUprobe(pid, name, offset)
	return err
}

func (ctx *EBPFContext) UpdateArgMap(key uint64, goidOffset int64, args []UProbeArgMap, gAddrOffset uint64) error {
	if ctx.bpfArgMap == nil {
		return errors.New("eBPF map not loaded")
	}
	params := createFunctionParameterList(key, goidOffset, args)
	params.g_addr_offset = C.longlong(gAddrOffset)
	return ctx.bpfArgMap.Update(unsafe.Pointer(&key), unsafe.Pointer(&params))
}

func (ctx *EBPFContext) GetBufferedTracepoints() []RawUProbeParams {
	ctx.m.Lock()
	defer ctx.m.Unlock()

	if len(ctx.parsedBpfEvents) == 0 {
		return make([]RawUProbeParams, 0)
	}

	events := make([]RawUProbeParams, len(ctx.parsedBpfEvents))
	copy(events, ctx.parsedBpfEvents)
	ctx.parsedBpfEvents = ctx.parsedBpfEvents[:0]
	return events
}

func SymbolToOffset(file, symbol string) (uint32, error) {
	return helpers.SymbolToOffset(file, symbol)
}

func LoadEBPFTracingProgram() (*EBPFContext, error) {
	var ctx EBPFContext
	var err error

	ctx.bpfModule, err = bpf.NewModuleFromBuffer(TraceProbeBytes, "trace.o")
	if err != nil {
		return nil, err
	}

	ctx.bpfModule.BPFLoadObject()
	prog, err := ctx.bpfModule.GetProgram("uprobe__dlv_trace")
	if err != nil {
		return nil, err
	}
	ctx.bpfProg = prog

	ctx.bpfEvents = make(chan []byte)
	ctx.bpfRingBuf, err = ctx.bpfModule.InitRingBuf("events", ctx.bpfEvents)
	if err != nil {
		return nil, err
	}
	ctx.bpfRingBuf.Start()

	ctx.bpfArgMap, err = ctx.bpfModule.GetMap("arg_map")
	if err != nil {
		return nil, err
	}

	// TODO(derekparker): This should eventually be moved to a more generalized place.
	go func() {
		for {
			b, ok := <-ctx.bpfEvents
			if !ok {
				return
			}

			parsed := ParseFunctionParameterList(b)

			ctx.m.Lock()
			ctx.parsedBpfEvents = append(ctx.parsedBpfEvents, parsed)
			ctx.m.Unlock()
		}
	}()

	return &ctx, nil
}

func ParseFunctionParameterList(rawParamBytes []byte) RawUProbeParams {
	params := (*C.function_parameter_list_t)(unsafe.Pointer(&rawParamBytes[0]))

	defer runtime.KeepAlive(params) // Ensure the param is not garbage collected.

	var rawParams RawUProbeParams
	rawParams.FnAddr = int(params.fn_addr)
	rawParams.GoroutineID = int(params.goroutine_id)

	for i := 0; i < int(params.n_parameters); i++ {
		iparam := &RawUProbeParam{}
		data := make([]byte, 0x60)
		ret := params.params[i]
		iparam.Kind = reflect.Kind(ret.kind)

		val := C.GoBytes(unsafe.Pointer(&ret.val), C.int(ret.size))
		rawDerefValue := C.GoBytes(unsafe.Pointer(&ret.deref_val[0]), 0x30)
		copy(data, val)
		copy(data[0x30:], rawDerefValue)
		iparam.Data = data

		pieces := make([]op.Piece, 0, 2)
		pieces = append(pieces, op.Piece{Size: 0x30, Kind: op.AddrPiece, Val: FakeAddressBase})
		pieces = append(pieces, op.Piece{Size: 0x30, Kind: op.AddrPiece, Val: FakeAddressBase + 0x30})
		iparam.Pieces = pieces

		iparam.Addr = FakeAddressBase

		switch iparam.Kind {
		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
			iparam.RealType = &godwarf.IntType{BasicType: godwarf.BasicType{CommonType: godwarf.CommonType{ByteSize: 8}}}
		case reflect.String:
			strLen := binary.LittleEndian.Uint64(val[8:])
			iparam.Base = FakeAddressBase + 0x30
			iparam.Len = int64(strLen)
		}

		rawParams.InputParams = append(rawParams.InputParams, iparam)
	}

	return rawParams
}

func createFunctionParameterList(entry uint64, goidOffset int64, args []UProbeArgMap) C.function_parameter_list_t {
	var params C.function_parameter_list_t
	params.goid_offset = C.uint(goidOffset)
	params.n_parameters = C.uint(len(args))
	params.fn_addr = C.uint(entry)
	for i, arg := range args {
		var param C.function_parameter_t
		param.size = C.uint(arg.Size)
		param.offset = C.uint(arg.Offset)
		param.kind = C.uint(arg.Kind)
		if arg.InReg {
			param.in_reg = true
			param.n_pieces = C.int(len(arg.Pieces))
			for i := range arg.Pieces {
				if i > 5 {
					break
				}
				param.reg_nums[i] = C.int(arg.Pieces[i])
			}
		}
		params.params[i] = param
	}
	return params
}
