package dwarf

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

// ReadString reads a null-terminated string from data.
func ReadString(data *bytes.Buffer) (string, error) {
	str, err := data.ReadString(0x0)
	if err != nil {
		return "", err
	}

	return str[:len(str)-1], nil
}

// ReadUintRaw reads an integer of ptrSize bytes, with the specified byte order, from reader.
func ReadUintRaw(reader io.Reader, order binary.ByteOrder, ptrSize int) (uint64, error) {
	switch ptrSize {
	case 2:
		var n uint16
		if err := binary.Read(reader, order, &n); err != nil {
			return 0, err
		}
		return uint64(n), nil
	case 4:
		var n uint32
		if err := binary.Read(reader, order, &n); err != nil {
			return 0, err
		}
		return uint64(n), nil
	case 8:
		var n uint64
		if err := binary.Read(reader, order, &n); err != nil {
			return 0, err
		}
		return n, nil
	}
	return 0, fmt.Errorf("pointer size %d not supported", ptrSize)
}

// WriteUint writes an integer of ptrSize bytes to writer, in the specified byte order.
func WriteUint(writer io.Writer, order binary.ByteOrder, ptrSize int, data uint64) error {
	switch ptrSize {
	case 4:
		return binary.Write(writer, order, uint32(data))
	case 8:
		return binary.Write(writer, order, data)
	}
	return fmt.Errorf("pointer size %d not supported", ptrSize)
}

// ReadDwarfLengthVersion reads a DWARF length field followed by a version field
func ReadDwarfLengthVersion(data []byte) (length uint64, dwarf64 bool, version uint8, byteOrder binary.ByteOrder) {
	if len(data) < 4 {
		return 0, false, 0, binary.LittleEndian
	}

	lengthfield := binary.LittleEndian.Uint32(data)
	voff := 4
	if lengthfield == ^uint32(0) {
		dwarf64 = true
		voff = 12
	}

	if voff+1 >= len(data) {
		return 0, false, 0, binary.LittleEndian
	}

	byteOrder = binary.LittleEndian
	x, y := data[voff], data[voff+1]
	switch {
	default:
		fallthrough
	case x == 0 && y == 0:
		version = 0
		byteOrder = binary.LittleEndian
	case x == 0:
		version = y
		byteOrder = binary.BigEndian
	case y == 0:
		version = x
		byteOrder = binary.LittleEndian
	}

	if dwarf64 {
		length = byteOrder.Uint64(data[4:])
	} else {
		length = uint64(byteOrder.Uint32(data))
	}

	return length, dwarf64, version, byteOrder
}

const (
	_DW_UT_compile = 0x1 + iota
	_DW_UT_type
	_DW_UT_partial
	_DW_UT_skeleton
	_DW_UT_split_compile
	_DW_UT_split_type
)

// ReadUnitVersions reads the DWARF version of each unit in a debug_info section and returns them as a map.
func ReadUnitVersions(data []byte) map[dwarf.Offset]uint8 {
	r := make(map[dwarf.Offset]uint8)
	off := dwarf.Offset(0)
	for len(data) > 0 {
		length, dwarf64, version, _ := ReadDwarfLengthVersion(data)

		data = data[4:]
		off += 4
		secoffsz := 4
		if dwarf64 {
			off += 8
			secoffsz = 8
			data = data[8:]
		}

		var headerSize int

		switch version {
		case 2, 3, 4:
			headerSize = 3 + secoffsz
		default: // 5 and later?
			unitType := data[2]

			switch unitType {
			case _DW_UT_compile, _DW_UT_partial:
				headerSize = 5 + secoffsz

			case _DW_UT_skeleton, _DW_UT_split_compile:
				headerSize = 4 + secoffsz + 8

			case _DW_UT_type, _DW_UT_split_type:
				headerSize = 4 + secoffsz + 8 + secoffsz
			}
		}

		r[off+dwarf.Offset(headerSize)] = version

		data = data[length:] // skip contents
		off += dwarf.Offset(length)
	}
	return r
}
