package stacksize

// This file implements parsing DWARF call frame information and interpreting
// the CFI bytecode, or enough of it for most practical code.

import (
	"bytes"
	"debug/elf"
	"encoding/binary"
	"fmt"
	"io"
)

// dwarfCIE represents one DWARF Call Frame Information structure.
type dwarfCIE struct {
	bytecode            []byte
	codeAlignmentFactor uint64
}

// parseFrames parses all call frame information from a .debug_frame section and
// provides the passed in symbols map with frame size information.
func parseFrames(f *elf.File, data []byte, symbols map[uint64]*CallNode) error {
	if f.Class != elf.ELFCLASS32 {
		// TODO: ELF64
		return fmt.Errorf("expected ELF32")
	}
	cies := make(map[uint32]*dwarfCIE)

	// Read each entity.
	r := bytes.NewBuffer(data)
	for {
		start := len(data) - r.Len()
		var length uint32
		err := binary.Read(r, binary.LittleEndian, &length)
		if err == io.EOF {
			return nil
		}
		if err != nil {
			return err
		}
		var cie uint32
		err = binary.Read(r, binary.LittleEndian, &cie)
		if err != nil {
			return err
		}
		if cie == 0xffffffff {
			// This is a CIE.
			var fields struct {
				Version      uint8
				Augmentation uint8
				AddressSize  uint8
				SegmentSize  uint8
			}
			err = binary.Read(r, binary.LittleEndian, &fields)
			if err != nil {
				return err
			}
			if fields.Version != 4 {
				return fmt.Errorf("unimplemented: .debug_frame version %d", fields.Version)
			}
			if fields.Augmentation != 0 {
				return fmt.Errorf("unimplemented: .debug_frame with augmentation")
			}
			if fields.SegmentSize != 0 {
				return fmt.Errorf("unimplemented: .debug_frame with segment size")
			}
			codeAlignmentFactor, err := readULEB128(r)
			if err != nil {
				return err
			}
			_, err = readSLEB128(r) // data alignment factor
			if err != nil {
				return err
			}
			_, err = readULEB128(r) // return address register
			if err != nil {
				return err
			}
			rest := (start + int(length) + 4) - (len(data) - r.Len())
			bytecode := r.Next(rest)
			cies[uint32(start)] = &dwarfCIE{
				codeAlignmentFactor: codeAlignmentFactor,
				bytecode:            bytecode,
			}
		} else {
			// This is a FDE.
			var fields struct {
				InitialLocation uint32
				AddressRange    uint32
			}
			err = binary.Read(r, binary.LittleEndian, &fields)
			if err != nil {
				return err
			}
			if _, ok := cies[cie]; !ok {
				return fmt.Errorf("could not find CIE 0x%x in .debug_frame section", cie)
			}
			frame := frameInfo{
				cie:    cies[cie],
				start:  uint64(fields.InitialLocation),
				loc:    uint64(fields.InitialLocation),
				length: uint64(fields.AddressRange),
			}
			rest := (start + int(length) + 4) - (len(data) - r.Len())
			bytecode := r.Next(rest)

			if frame.start == 0 {
				// Not sure where these come from but they don't seem to be
				// important.
				continue
			}

			_, err = frame.exec(frame.cie.bytecode)
			if err != nil {
				return err
			}
			entries, err := frame.exec(bytecode)
			if err != nil {
				return err
			}
			var maxFrameSize uint64
			for _, entry := range entries {
				switch f.Machine {
				case elf.EM_ARM:
					if entry.cfaRegister != 13 { // r13 or sp
						// something other than a stack pointer (on ARM)
						return fmt.Errorf("%08x..%08x: unknown CFA register number %d", frame.start, frame.start+frame.length, entry.cfaRegister)
					}
				default:
					return fmt.Errorf("unknown architecture: %s", f.Machine)
				}
				if entry.cfaOffset > maxFrameSize {
					maxFrameSize = entry.cfaOffset
				}
			}
			node := symbols[frame.start]
			if node.Size != frame.length {
				return fmt.Errorf("%s: symtab gives symbol length %d while DWARF gives symbol length %d", node, node.Size, frame.length)
			}
			node.FrameSize = maxFrameSize
			node.FrameSizeType = Bounded
			if debugPrint {
				fmt.Printf("%08x..%08x: frame size %4d %s\n", frame.start, frame.start+frame.length, maxFrameSize, node)
			}
		}
	}
}

// frameInfo contains the state of executing call frame information bytecode.
type frameInfo struct {
	cie         *dwarfCIE
	start       uint64
	loc         uint64
	length      uint64
	cfaRegister uint64
	cfaOffset   uint64
}

// frameInfoLine represents one line in the frame table (.debug_frame) at one
// point in the execution of the bytecode.
type frameInfoLine struct {
	loc         uint64
	cfaRegister uint64
	cfaOffset   uint64
}

func (fi *frameInfo) newLine() frameInfoLine {
	return frameInfoLine{
		loc:         fi.loc,
		cfaRegister: fi.cfaRegister,
		cfaOffset:   fi.cfaOffset,
	}
}

// exec executes the given bytecode in the CFI. Most CFI bytecode is actually
// very simple and provides a way to determine the maximum call frame size.
//
// The frame size often changes multiple times in a function, for example the
// frame size may be adjusted in the prologue and epilogue. Each frameInfoLine
// may contain such a change.
func (fi *frameInfo) exec(bytecode []byte) ([]frameInfoLine, error) {
	var entries []frameInfoLine
	r := bytes.NewBuffer(bytecode)
	for {
		op, err := r.ReadByte()
		if err != nil {
			if err == io.EOF {
				entries = append(entries, fi.newLine())
				return entries, nil
			}
			return nil, err
		}
		// For details on the various opcodes, see:
		// http://dwarfstd.org/doc/DWARF5.pdf (page 239)
		highBits := op >> 6 // high order 2 bits
		lowBits := op & 0x1f
		switch highBits {
		case 1: // DW_CFA_advance_loc
			fi.loc += uint64(lowBits) * fi.cie.codeAlignmentFactor
			entries = append(entries, fi.newLine())
		case 2: // DW_CFA_offset
			// This indicates where a register is saved on the stack in the
			// prologue. We can ignore that for our purposes.
			_, err := readULEB128(r)
			if err != nil {
				return nil, err
			}
		case 0:
			switch lowBits {
			case 0: // DW_CFA_nop
				// no operation
			case 0x02: // DW_CFA_advance_loc1
				// Very similar to DW_CFA_advance_loc but allows for a slightly
				// larger range.
				offset, err := r.ReadByte()
				if err != nil {
					return nil, err
				}
				fi.loc += uint64(offset) * fi.cie.codeAlignmentFactor
				entries = append(entries, fi.newLine())
				// TODO: DW_CFA_advance_loc2 etc
			case 0x05: // DW_CFA_offset_extended
				// Semantics are the same as DW_CFA_offset, but the encoding is
				// different. Ignore it just like DW_CFA_offset.
				_, err := readULEB128(r) // ULEB128 register
				if err != nil {
					return nil, err
				}
				_, err = readULEB128(r) // ULEB128 offset
				if err != nil {
					return nil, err
				}
			case 0x07: // DW_CFA_undefined
				// Marks a single register as undefined. This is used to stop
				// unwinding in tinygo_startTask using:
				//     .cfi_undefined lr
				// Ignore this directive.
				_, err := readULEB128(r)
				if err != nil {
					return nil, err
				}
			case 0x0c: // DW_CFA_def_cfa
				register, err := readULEB128(r)
				if err != nil {
					return nil, err
				}
				offset, err := readULEB128(r)
				if err != nil {
					return nil, err
				}
				fi.cfaRegister = register
				fi.cfaOffset = offset
			case 0x0e: // DW_CFA_def_cfa_offset
				offset, err := readULEB128(r)
				if err != nil {
					return nil, err
				}
				fi.cfaOffset = offset
			default:
				return nil, fmt.Errorf("could not decode .debug_frame bytecode op 0x%x (for address 0x%x)", op, fi.loc)
			}
		default:
			return nil, fmt.Errorf("could not decode .debug_frame bytecode op 0x%x (for address 0x%x)", op, fi.loc)
		}
	}
}

// Source: https://en.wikipedia.org/wiki/LEB128#Decode_unsigned_integer
func readULEB128(r *bytes.Buffer) (result uint64, err error) {
	// TODO: guard against overflowing 64-bit integers.
	var shift uint8
	for {
		b, err := r.ReadByte()
		if err != nil {
			return 0, err
		}
		result |= uint64(b&0x7f) << shift
		if b&0x80 == 0 {
			break
		}
		shift += 7
	}
	return
}

// Source: https://en.wikipedia.org/wiki/LEB128#Decode_signed_integer
func readSLEB128(r *bytes.Buffer) (result int64, err error) {
	var shift uint8

	var b byte
	var rawResult uint64
	for {
		b, err = r.ReadByte()
		if err != nil {
			return 0, err
		}
		rawResult |= uint64(b&0x7f) << shift
		shift += 7
		if b&0x80 == 0 {
			break
		}
	}

	// sign bit of byte is second high order bit (0x40)
	if shift < 64 && b&0x40 != 0 {
		// sign extend
		rawResult |= ^uint64(0) << shift
	}
	result = int64(rawResult)

	return
}
