package asm

import (
	"crypto/sha1"
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"errors"
	"fmt"
	"hash"
	"io"
	"math"
	"sort"
	"strings"

	"github.com/cilium/ebpf/internal"
	"github.com/cilium/ebpf/internal/platform"
	"github.com/cilium/ebpf/internal/sys"
)

// InstructionSize is the size of a BPF instruction in bytes
const InstructionSize = 8

// RawInstructionOffset is an offset in units of raw BPF instructions.
type RawInstructionOffset uint64

var ErrUnreferencedSymbol = errors.New("unreferenced symbol")
var ErrUnsatisfiedMapReference = errors.New("unsatisfied map reference")
var ErrUnsatisfiedProgramReference = errors.New("unsatisfied program reference")

// Bytes returns the offset of an instruction in bytes.
func (rio RawInstructionOffset) Bytes() uint64 {
	return uint64(rio) * InstructionSize
}

// Instruction is a single eBPF instruction.
type Instruction struct {
	OpCode   OpCode
	Dst      Register
	Src      Register
	Offset   int16
	Constant int64

	// Metadata contains optional metadata about this instruction.
	Metadata Metadata
}

// Width returns how many raw BPF instructions the Instruction occupies within
// an instruction stream. For example, an Instruction encoding a 64-bit value
// will typically occupy 2 raw instructions, while a 32-bit constant can be
// encoded in a single raw instruction.
func (ins *Instruction) Width() RawInstructionOffset {
	return RawInstructionOffset(ins.OpCode.rawInstructions())
}

// Unmarshal decodes a BPF instruction.
func (ins *Instruction) Unmarshal(r io.Reader, bo binary.ByteOrder, platform string) error {
	data := make([]byte, InstructionSize)
	if _, err := io.ReadFull(r, data); err != nil {
		return err
	}

	ins.OpCode = OpCode(data[0])

	regs := data[1]
	switch bo {
	case binary.LittleEndian:
		ins.Dst, ins.Src = Register(regs&0xF), Register(regs>>4)
	case binary.BigEndian:
		ins.Dst, ins.Src = Register(regs>>4), Register(regs&0xf)
	}

	ins.Offset = int16(bo.Uint16(data[2:4]))

	// Convert to int32 before widening to int64
	// to ensure the signed bit is carried over.
	ins.Constant = int64(int32(bo.Uint32(data[4:8])))

	if ins.IsBuiltinCall() {
		if ins.Constant >= 0 {
			// Leave negative constants from the instruction stream
			// unchanged. These are sometimes used as placeholders for later
			// patching.
			// This relies on not having a valid platform tag with a high bit set.
			fn, err := BuiltinFuncForPlatform(platform, uint32(ins.Constant))
			if err != nil {
				return err
			}
			ins.Constant = int64(fn)
		}
	} else if ins.OpCode.Class().IsALU() {
		switch ins.OpCode.ALUOp() {
		case Div:
			if ins.Offset == 1 {
				ins.OpCode = ins.OpCode.SetALUOp(SDiv)
				ins.Offset = 0
			}
		case Mod:
			if ins.Offset == 1 {
				ins.OpCode = ins.OpCode.SetALUOp(SMod)
				ins.Offset = 0
			}
		case Mov:
			switch ins.Offset {
			case 8:
				ins.OpCode = ins.OpCode.SetALUOp(MovSX8)
				ins.Offset = 0
			case 16:
				ins.OpCode = ins.OpCode.SetALUOp(MovSX16)
				ins.Offset = 0
			case 32:
				ins.OpCode = ins.OpCode.SetALUOp(MovSX32)
				ins.Offset = 0
			}
		}
	} else if ins.OpCode.Class() == StXClass &&
		ins.OpCode.Mode() == AtomicMode {
		// For atomic ops, part of the opcode is stored in the
		// constant field. Shift over 8 bytes so we can OR with the actual opcode and
		// apply `atomicMask` to avoid merging unknown bits that may be added in the future.
		ins.OpCode |= (OpCode((ins.Constant << 8)) & atomicMask)
	}

	if !ins.OpCode.IsDWordLoad() {
		return nil
	}

	// Pull another instruction from the stream to retrieve the second
	// half of the 64-bit immediate value.
	if _, err := io.ReadFull(r, data); err != nil {
		// No Wrap, to avoid io.EOF clash
		return errors.New("64bit immediate is missing second half")
	}

	// Require that all fields other than the value are zero.
	if bo.Uint32(data[0:4]) != 0 {
		return errors.New("64bit immediate has non-zero fields")
	}

	cons1 := uint32(ins.Constant)
	cons2 := int32(bo.Uint32(data[4:8]))
	ins.Constant = int64(cons2)<<32 | int64(cons1)

	return nil
}

// Marshal encodes a BPF instruction.
func (ins Instruction) Marshal(w io.Writer, bo binary.ByteOrder) (uint64, error) {
	if ins.OpCode == InvalidOpCode {
		return 0, errors.New("invalid opcode")
	}

	isDWordLoad := ins.OpCode.IsDWordLoad()

	cons := int32(ins.Constant)
	if isDWordLoad {
		// Encode least significant 32bit first for 64bit operations.
		cons = int32(uint32(ins.Constant))
	}

	regs, err := newBPFRegisters(ins.Dst, ins.Src, bo)
	if err != nil {
		return 0, fmt.Errorf("can't marshal registers: %s", err)
	}

	if ins.IsBuiltinCall() {
		fn := BuiltinFunc(ins.Constant)
		plat, value := platform.DecodeConstant(fn)
		if plat != platform.Native {
			return 0, fmt.Errorf("function %s (%s): %w", fn, plat, internal.ErrNotSupportedOnOS)
		}
		cons = int32(value)
	} else if ins.OpCode.Class().IsALU() {
		newOffset := int16(0)
		switch ins.OpCode.ALUOp() {
		case SDiv:
			ins.OpCode = ins.OpCode.SetALUOp(Div)
			newOffset = 1
		case SMod:
			ins.OpCode = ins.OpCode.SetALUOp(Mod)
			newOffset = 1
		case MovSX8:
			ins.OpCode = ins.OpCode.SetALUOp(Mov)
			newOffset = 8
		case MovSX16:
			ins.OpCode = ins.OpCode.SetALUOp(Mov)
			newOffset = 16
		case MovSX32:
			ins.OpCode = ins.OpCode.SetALUOp(Mov)
			newOffset = 32
		}
		if newOffset != 0 && ins.Offset != 0 {
			return 0, fmt.Errorf("extended ALU opcodes should have an .Offset of 0: %s", ins)
		}
		ins.Offset = newOffset
	} else if atomic := ins.OpCode.AtomicOp(); atomic != InvalidAtomic {
		ins.OpCode = ins.OpCode &^ atomicMask
		ins.Constant = int64(atomic >> 8)
	}

	op, err := ins.OpCode.bpfOpCode()
	if err != nil {
		return 0, err
	}

	data := make([]byte, InstructionSize)
	data[0] = op
	data[1] = byte(regs)
	bo.PutUint16(data[2:4], uint16(ins.Offset))
	bo.PutUint32(data[4:8], uint32(cons))
	if _, err := w.Write(data); err != nil {
		return 0, err
	}

	if !isDWordLoad {
		return InstructionSize, nil
	}

	// The first half of the second part of a double-wide instruction
	// must be zero. The second half carries the value.
	bo.PutUint32(data[0:4], 0)
	bo.PutUint32(data[4:8], uint32(ins.Constant>>32))
	if _, err := w.Write(data); err != nil {
		return 0, err
	}

	return 2 * InstructionSize, nil
}

// AssociateMap associates a Map with this Instruction.
//
// Implicitly clears the Instruction's Reference field.
//
// Returns an error if the Instruction is not a map load.
func (ins *Instruction) AssociateMap(m FDer) error {
	if !ins.IsLoadFromMap() {
		return errors.New("not a load from a map")
	}

	ins.Metadata.Set(referenceMeta{}, nil)
	ins.Metadata.Set(mapMeta{}, m)

	return nil
}

func (ins *Instruction) encodeMapFD(fd int) {
	// Preserve the offset value for direct map loads.
	offset := uint64(ins.Constant) & (math.MaxUint32 << 32)
	rawFd := uint64(uint32(fd))
	ins.Constant = int64(offset | rawFd)
}

// mapFd returns the map file descriptor stored in the 32 least significant
// bits of ins' Constant field.
func (ins *Instruction) mapFd() int {
	return int(int32(ins.Constant))
}

// RewriteMapOffset changes the offset of a direct load from a map.
//
// Returns an error if the instruction is not a direct load.
func (ins *Instruction) RewriteMapOffset(offset uint32) error {
	if !ins.OpCode.IsDWordLoad() {
		return fmt.Errorf("%s is not a 64 bit load", ins.OpCode)
	}

	if ins.Src != PseudoMapValue {
		return errors.New("not a direct load from a map")
	}

	fd := uint64(ins.Constant) & math.MaxUint32
	ins.Constant = int64(uint64(offset)<<32 | fd)
	return nil
}

func (ins *Instruction) mapOffset() uint32 {
	return uint32(uint64(ins.Constant) >> 32)
}

// IsLoadFromMap returns true if the instruction loads from a map.
//
// This covers both loading the map pointer and direct map value loads.
func (ins *Instruction) IsLoadFromMap() bool {
	return ins.OpCode == LoadImmOp(DWord) && (ins.Src == PseudoMapFD || ins.Src == PseudoMapValue)
}

// IsFunctionCall returns true if the instruction calls another BPF function.
//
// This is not the same thing as a BPF helper call.
func (ins *Instruction) IsFunctionCall() bool {
	return ins.OpCode.JumpOp() == Call && ins.Src == PseudoCall
}

// IsKfuncCall returns true if the instruction calls a kfunc.
//
// This is not the same thing as a BPF helper call.
func (ins *Instruction) IsKfuncCall() bool {
	return ins.OpCode.JumpOp() == Call && ins.Src == PseudoKfuncCall
}

// IsLoadOfFunctionPointer returns true if the instruction loads a function pointer.
func (ins *Instruction) IsLoadOfFunctionPointer() bool {
	return ins.OpCode.IsDWordLoad() && ins.Src == PseudoFunc
}

// IsFunctionReference returns true if the instruction references another BPF
// function, either by invoking a Call jump operation or by loading a function
// pointer.
func (ins *Instruction) IsFunctionReference() bool {
	return ins.IsFunctionCall() || ins.IsLoadOfFunctionPointer()
}

// IsBuiltinCall returns true if the instruction is a built-in call, i.e. BPF helper call.
func (ins *Instruction) IsBuiltinCall() bool {
	return ins.OpCode.JumpOp() == Call && ins.Src == R0 && ins.Dst == R0
}

// IsConstantLoad returns true if the instruction loads a constant of the
// given size.
func (ins *Instruction) IsConstantLoad(size Size) bool {
	return ins.OpCode == LoadImmOp(size) && ins.Src == R0 && ins.Offset == 0
}

// Format implements fmt.Formatter.
func (ins Instruction) Format(f fmt.State, c rune) {
	if c != 'v' {
		fmt.Fprintf(f, "{UNRECOGNIZED: %c}", c)
		return
	}

	op := ins.OpCode

	if op == InvalidOpCode {
		fmt.Fprint(f, "INVALID")
		return
	}

	// Omit trailing space for Exit
	if op.JumpOp() == Exit {
		fmt.Fprint(f, op)
		return
	}

	if ins.IsLoadFromMap() {
		fd := ins.mapFd()
		m := ins.Map()
		switch ins.Src {
		case PseudoMapFD:
			if m != nil {
				fmt.Fprintf(f, "LoadMapPtr dst: %s map: %s", ins.Dst, m)
			} else {
				fmt.Fprintf(f, "LoadMapPtr dst: %s fd: %d", ins.Dst, fd)
			}

		case PseudoMapValue:
			if m != nil {
				fmt.Fprintf(f, "LoadMapValue dst: %s, map: %s off: %d", ins.Dst, m, ins.mapOffset())
			} else {
				fmt.Fprintf(f, "LoadMapValue dst: %s, fd: %d off: %d", ins.Dst, fd, ins.mapOffset())
			}
		}

		goto ref
	}

	switch cls := op.Class(); {
	case cls.isLoadOrStore():
		fmt.Fprintf(f, "%v ", op)
		switch op.Mode() {
		case ImmMode:
			fmt.Fprintf(f, "dst: %s imm: %d", ins.Dst, ins.Constant)
		case AbsMode:
			fmt.Fprintf(f, "imm: %d", ins.Constant)
		case IndMode:
			fmt.Fprintf(f, "dst: %s src: %s imm: %d", ins.Dst, ins.Src, ins.Constant)
		case MemMode, MemSXMode:
			fmt.Fprintf(f, "dst: %s src: %s off: %d imm: %d", ins.Dst, ins.Src, ins.Offset, ins.Constant)
		case AtomicMode:
			fmt.Fprintf(f, "dst: %s src: %s off: %d", ins.Dst, ins.Src, ins.Offset)
		}

	case cls.IsALU():
		fmt.Fprintf(f, "%v", op)
		if op == Swap.Op(ImmSource) {
			fmt.Fprintf(f, "%d", ins.Constant)
		}

		fmt.Fprintf(f, " dst: %s ", ins.Dst)
		switch {
		case op.ALUOp() == Swap:
			break
		case op.Source() == ImmSource:
			fmt.Fprintf(f, "imm: %d", ins.Constant)
		default:
			fmt.Fprintf(f, "src: %s", ins.Src)
		}

	case cls.IsJump():
		switch jop := op.JumpOp(); jop {
		case Call:
			fmt.Fprintf(f, "%v ", op)
			switch ins.Src {
			case PseudoCall:
				// bpf-to-bpf call
				fmt.Fprint(f, ins.Constant)
			case PseudoKfuncCall:
				// kfunc call
				fmt.Fprintf(f, "Kfunc(%d)", ins.Constant)
			default:
				fmt.Fprint(f, BuiltinFunc(ins.Constant))
			}

		case Ja:
			fmt.Fprintf(f, "%v ", op)
			if ins.OpCode.Class() == Jump32Class {
				fmt.Fprintf(f, "imm: %d", ins.Constant)
			} else {
				fmt.Fprintf(f, "off: %d", ins.Offset)
			}

		case JCOND:
			switch ins.Src {
			case PseudoMayGoto:
				fmt.Fprintf(f, "JCond may_goto off: %d", ins.Offset)
			default:
				fmt.Fprintf(f, "%v", op)
			}

		default:
			fmt.Fprintf(f, "%v ", op)
			fmt.Fprintf(f, "dst: %s off: %d ", ins.Dst, ins.Offset)
			if op.Source() == ImmSource {
				fmt.Fprintf(f, "imm: %d", ins.Constant)
			} else {
				fmt.Fprintf(f, "src: %s", ins.Src)
			}
		}
	default:
		fmt.Fprintf(f, "%v ", op)
	}

ref:
	if ins.Reference() != "" {
		fmt.Fprintf(f, " <%s>", ins.Reference())
	}
}

func (ins Instruction) equal(other Instruction) bool {
	return ins.OpCode == other.OpCode &&
		ins.Dst == other.Dst &&
		ins.Src == other.Src &&
		ins.Offset == other.Offset &&
		ins.Constant == other.Constant
}

// Size returns the amount of bytes ins would occupy in binary form.
func (ins Instruction) Size() uint64 {
	return uint64(InstructionSize * ins.OpCode.rawInstructions())
}

// WithMetadata sets the given Metadata on the Instruction. e.g. to copy
// Metadata from another Instruction when replacing it.
func (ins Instruction) WithMetadata(meta Metadata) Instruction {
	ins.Metadata = meta
	return ins
}

type symbolMeta struct{}

// WithSymbol marks the Instruction as a Symbol, which other Instructions
// can point to using corresponding calls to WithReference.
func (ins Instruction) WithSymbol(name string) Instruction {
	ins.Metadata.Set(symbolMeta{}, name)
	return ins
}

// Symbol returns the value ins has been marked with using WithSymbol,
// otherwise returns an empty string. A symbol is often an Instruction
// at the start of a function body.
func (ins Instruction) Symbol() string {
	sym, _ := ins.Metadata.Get(symbolMeta{}).(string)
	return sym
}

type referenceMeta struct{}

// WithReference makes ins reference another Symbol or map by name.
func (ins Instruction) WithReference(ref string) Instruction {
	ins.Metadata.Set(referenceMeta{}, ref)
	return ins
}

// Reference returns the Symbol or map name referenced by ins, if any.
func (ins Instruction) Reference() string {
	ref, _ := ins.Metadata.Get(referenceMeta{}).(string)
	return ref
}

type mapMeta struct{}

// Map returns the Map referenced by ins, if any.
// An Instruction will contain a Map if e.g. it references an existing,
// pinned map that was opened during ELF loading.
func (ins Instruction) Map() FDer {
	fd, _ := ins.Metadata.Get(mapMeta{}).(FDer)
	return fd
}

type sourceMeta struct{}

// WithSource adds source information about the Instruction.
func (ins Instruction) WithSource(src fmt.Stringer) Instruction {
	ins.Metadata.Set(sourceMeta{}, src)
	return ins
}

// Source returns source information about the Instruction. The field is
// present when the compiler emits BTF line info about the Instruction and
// usually contains the line of source code responsible for it.
func (ins Instruction) Source() fmt.Stringer {
	str, _ := ins.Metadata.Get(sourceMeta{}).(fmt.Stringer)
	return str
}

// A Comment can be passed to Instruction.WithSource to add a comment
// to an instruction.
type Comment string

func (s Comment) String() string {
	return string(s)
}

// FDer represents a resource tied to an underlying file descriptor.
// Used as a stand-in for e.g. ebpf.Map since that type cannot be
// imported here and FD() is the only method we rely on.
type FDer interface {
	FD() int
}

// Instructions is an eBPF program.
type Instructions []Instruction

// AppendInstructions decodes [Instruction] from r and appends them to insns.
func AppendInstructions(insns Instructions, r io.Reader, bo binary.ByteOrder, platform string) (Instructions, error) {
	var offset uint64
	for {
		var ins Instruction
		err := ins.Unmarshal(r, bo, platform)
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			return nil, fmt.Errorf("offset %d: %w", offset, err)
		}

		insns = append(insns, ins)
		offset += ins.Size()
	}

	return insns, nil
}

// Name returns the name of the function insns belongs to, if any.
func (insns Instructions) Name() string {
	if len(insns) == 0 {
		return ""
	}
	return insns[0].Symbol()
}

func (insns Instructions) String() string {
	return fmt.Sprint(insns)
}

// Size returns the amount of bytes insns would occupy in binary form.
func (insns Instructions) Size() uint64 {
	var sum uint64
	for _, ins := range insns {
		sum += ins.Size()
	}
	return sum
}

// AssociateMap updates all Instructions that Reference the given symbol
// to point to an existing Map m instead.
//
// Returns ErrUnreferencedSymbol error if no references to symbol are found
// in insns. If symbol is anything else than the symbol name of map (e.g.
// a bpf2bpf subprogram), an error is returned.
func (insns Instructions) AssociateMap(symbol string, m FDer) error {
	if symbol == "" {
		return errors.New("empty symbol")
	}

	var found bool
	for i := range insns {
		ins := &insns[i]
		if ins.Reference() != symbol {
			continue
		}

		if err := ins.AssociateMap(m); err != nil {
			return err
		}

		found = true
	}

	if !found {
		return fmt.Errorf("symbol %s: %w", symbol, ErrUnreferencedSymbol)
	}

	return nil
}

// SymbolOffsets returns the set of symbols and their offset in
// the instructions.
func (insns Instructions) SymbolOffsets() (map[string]int, error) {
	offsets := make(map[string]int)

	for i, ins := range insns {
		if ins.Symbol() == "" {
			continue
		}

		if _, ok := offsets[ins.Symbol()]; ok {
			return nil, fmt.Errorf("duplicate symbol %s", ins.Symbol())
		}

		offsets[ins.Symbol()] = i
	}

	return offsets, nil
}

// FunctionReferences returns a set of symbol names these Instructions make
// bpf-to-bpf calls to.
func (insns Instructions) FunctionReferences() []string {
	calls := make(map[string]struct{})
	for _, ins := range insns {
		if ins.Constant != -1 {
			// BPF-to-BPF calls have -1 constants.
			continue
		}

		if ins.Reference() == "" {
			continue
		}

		if !ins.IsFunctionReference() {
			continue
		}

		calls[ins.Reference()] = struct{}{}
	}

	result := make([]string, 0, len(calls))
	for call := range calls {
		result = append(result, call)
	}

	sort.Strings(result)
	return result
}

// ReferenceOffsets returns the set of references and their offset in
// the instructions.
func (insns Instructions) ReferenceOffsets() map[string][]int {
	offsets := make(map[string][]int)

	for i, ins := range insns {
		if ins.Reference() == "" {
			continue
		}

		offsets[ins.Reference()] = append(offsets[ins.Reference()], i)
	}

	return offsets
}

// Format implements fmt.Formatter.
//
// You can control indentation of symbols by
// specifying a width. Setting a precision controls the indentation of
// instructions.
// The default character is a tab, which can be overridden by specifying
// the ' ' space flag.
func (insns Instructions) Format(f fmt.State, c rune) {
	if c != 's' && c != 'v' {
		fmt.Fprintf(f, "{UNKNOWN FORMAT '%c'}", c)
		return
	}

	// Precision is better in this case, because it allows
	// specifying 0 padding easily.
	padding, ok := f.Precision()
	if !ok {
		padding = 1
	}

	indent := strings.Repeat("\t", padding)
	if f.Flag(' ') {
		indent = strings.Repeat(" ", padding)
	}

	symPadding, ok := f.Width()
	if !ok {
		symPadding = padding - 1
	}
	if symPadding < 0 {
		symPadding = 0
	}

	symIndent := strings.Repeat("\t", symPadding)
	if f.Flag(' ') {
		symIndent = strings.Repeat(" ", symPadding)
	}

	// Guess how many digits we need at most, by assuming that all instructions
	// are double wide.
	highestOffset := len(insns) * 2
	offsetWidth := int(math.Ceil(math.Log10(float64(highestOffset))))

	iter := insns.Iterate()
	for iter.Next() {
		if iter.Ins.Symbol() != "" {
			fmt.Fprintf(f, "%s%s:\n", symIndent, iter.Ins.Symbol())
		}
		if src := iter.Ins.Source(); src != nil {
			line := strings.TrimSpace(src.String())
			if line != "" {
				fmt.Fprintf(f, "%s%*s; %s\n", indent, offsetWidth, " ", line)
			}
		}
		fmt.Fprintf(f, "%s%*d: %v\n", indent, offsetWidth, iter.Offset, iter.Ins)
	}
}

// Marshal encodes a BPF program into the kernel format.
//
// insns may be modified if there are unresolved jumps or bpf2bpf calls.
//
// Returns ErrUnsatisfiedProgramReference if there is a Reference Instruction
// without a matching Symbol Instruction within insns.
func (insns Instructions) Marshal(w io.Writer, bo binary.ByteOrder) error {
	if err := insns.encodeFunctionReferences(); err != nil {
		return err
	}

	if err := insns.encodeMapPointers(); err != nil {
		return err
	}

	for i, ins := range insns {
		if _, err := ins.Marshal(w, bo); err != nil {
			return fmt.Errorf("instruction %d: %w", i, err)
		}
	}
	return nil
}

// Tag calculates the kernel tag for a series of instructions.
//
// It mirrors bpf_prog_calc_tag in the kernel and so can be compared
// to ProgramInfo.Tag to figure out whether a loaded program matches
// certain instructions.
//
// Deprecated: The value produced by this method no longer matches tags produced
// by the kernel since Linux 6.18. Use [Instructions.HasTag] instead.
func (insns Instructions) Tag(bo binary.ByteOrder) (string, error) {
	// We cannot determine which hashing function to use without probing the kernel.
	// So use the legacy SHA-1 implementation and deprecate this method.
	return insns.tagSha1(bo)
}

// HasTag returns true if the given tag matches the kernel tag of insns.
func (insns Instructions) HasTag(tag string, bo binary.ByteOrder) (bool, error) {
	sha256Tag, err := insns.tagSha256(bo)
	if err != nil {
		return false, fmt.Errorf("hashing sha256: %w", err)
	}
	if tag == sha256Tag {
		return true, nil
	}

	sha1Tag, err := insns.tagSha1(bo)
	if err != nil {
		return false, fmt.Errorf("hashing sha1: %w", err)
	}

	return tag == sha1Tag, nil
}

// tagSha1 calculates the kernel tag for a series of instructions.
//
// It mirrors bpf_prog_calc_tag in kernels up to v6.18 and can be compared to
// ProgramInfo.Tag to figure out whether a loaded Program matches insns.
func (insns Instructions) tagSha1(bo binary.ByteOrder) (string, error) {
	h := sha1.New()
	if err := insns.hash(h, bo); err != nil {
		return "", err
	}
	return hex.EncodeToString(h.Sum(nil)[:sys.BPF_TAG_SIZE]), nil
}

// tagSha256 calculates the kernel tag for a series of instructions.
//
// It mirrors bpf_prog_calc_tag in the kernel and can be compared to
// ProgramInfo.Tag to figure out whether a loaded Program matches insns.
func (insns Instructions) tagSha256(bo binary.ByteOrder) (string, error) {
	h := sha256.New()
	if err := insns.hash(h, bo); err != nil {
		return "", err
	}
	return hex.EncodeToString(h.Sum(nil)[:sys.BPF_TAG_SIZE]), nil
}

// hash calculates the hash of the instruction stream. Map load instructions
// are zeroed out, since these contain map file descriptors or pointers to
// maps, which will be different from load to load and would make the hash
// non-deterministic.
func (insns Instructions) hash(h hash.Hash, bo binary.ByteOrder) error {
	for i, ins := range insns {
		if ins.IsLoadFromMap() {
			ins.Constant = 0
		}
		_, err := ins.Marshal(h, bo)
		if err != nil {
			return fmt.Errorf("instruction %d: %w", i, err)
		}
	}
	return nil
}

// encodeFunctionReferences populates the Offset (or Constant, depending on
// the instruction type) field of instructions with a Reference field to point
// to the offset of the corresponding instruction with a matching Symbol field.
//
// Only Reference Instructions that are either jumps or BPF function references
// (calls or function pointer loads) are populated.
//
// Returns ErrUnsatisfiedProgramReference if there is a Reference Instruction
// without at least one corresponding Symbol Instruction within insns.
func (insns Instructions) encodeFunctionReferences() error {
	// Index the offsets of instructions tagged as a symbol.
	symbolOffsets := make(map[string]RawInstructionOffset)
	iter := insns.Iterate()
	for iter.Next() {
		ins := iter.Ins

		if ins.Symbol() == "" {
			continue
		}

		if _, ok := symbolOffsets[ins.Symbol()]; ok {
			return fmt.Errorf("duplicate symbol %s", ins.Symbol())
		}

		symbolOffsets[ins.Symbol()] = iter.Offset
	}

	// Find all instructions tagged as references to other symbols.
	// Depending on the instruction type, populate their constant or offset
	// fields to point to the symbol they refer to within the insn stream.
	iter = insns.Iterate()
	for iter.Next() {
		i := iter.Index
		offset := iter.Offset
		ins := iter.Ins

		if ins.Reference() == "" {
			continue
		}

		switch {
		case ins.IsFunctionReference() && ins.Constant == -1,
			ins.OpCode == Ja.opCode(Jump32Class, ImmSource) && ins.Constant == -1:
			symOffset, ok := symbolOffsets[ins.Reference()]
			if !ok {
				return fmt.Errorf("%s at insn %d: symbol %q: %w", ins.OpCode, i, ins.Reference(), ErrUnsatisfiedProgramReference)
			}

			ins.Constant = int64(symOffset - offset - 1)

		case ins.OpCode.Class().IsJump() && ins.Offset == -1:
			symOffset, ok := symbolOffsets[ins.Reference()]
			if !ok {
				return fmt.Errorf("%s at insn %d: symbol %q: %w", ins.OpCode, i, ins.Reference(), ErrUnsatisfiedProgramReference)
			}

			ins.Offset = int16(symOffset - offset - 1)
		}
	}

	return nil
}

// encodeMapPointers finds all Map Instructions and encodes their FDs
// into their Constant fields.
func (insns Instructions) encodeMapPointers() error {
	iter := insns.Iterate()
	for iter.Next() {
		ins := iter.Ins

		if !ins.IsLoadFromMap() {
			continue
		}

		m := ins.Map()
		if m == nil {
			continue
		}

		fd := m.FD()
		if fd < 0 {
			return fmt.Errorf("map %s: %w", m, sys.ErrClosedFd)
		}

		ins.encodeMapFD(m.FD())
	}

	return nil
}

// Iterate allows iterating a BPF program while keeping track of
// various offsets.
//
// Modifying the instruction slice will lead to undefined behaviour.
func (insns Instructions) Iterate() *InstructionIterator {
	return &InstructionIterator{insns: insns}
}

// InstructionIterator iterates over a BPF program.
type InstructionIterator struct {
	insns Instructions
	// The instruction in question.
	Ins *Instruction
	// The index of the instruction in the original instruction slice.
	Index int
	// The offset of the instruction in raw BPF instructions. This accounts
	// for double-wide instructions.
	Offset RawInstructionOffset
}

// Next returns true as long as there are any instructions remaining.
func (iter *InstructionIterator) Next() bool {
	if len(iter.insns) == 0 {
		return false
	}

	if iter.Ins != nil {
		iter.Index++
		iter.Offset += RawInstructionOffset(iter.Ins.OpCode.rawInstructions())
	}
	iter.Ins = &iter.insns[0]
	iter.insns = iter.insns[1:]
	return true
}

type bpfRegisters uint8

func newBPFRegisters(dst, src Register, bo binary.ByteOrder) (bpfRegisters, error) {
	switch bo {
	case binary.LittleEndian:
		return bpfRegisters((src << 4) | (dst & 0xF)), nil
	case binary.BigEndian:
		return bpfRegisters((dst << 4) | (src & 0xF)), nil
	default:
		return 0, fmt.Errorf("unrecognized ByteOrder %T", bo)
	}
}
