package dwarfbuilder

import (
	"bytes"
	"debug/dwarf"
	"encoding/binary"

	"github.com/go-delve/delve/pkg/dwarf/godwarf"
	"github.com/go-delve/delve/pkg/dwarf/leb128"
)

// Form represents a DWARF form kind (see Figure 20, page 160 and following,
// DWARF v4)
type Form uint16

const (
	DW_FORM_addr         Form = 0x01 // address
	DW_FORM_block2       Form = 0x03 // block
	DW_FORM_block4       Form = 0x04 // block
	DW_FORM_data2        Form = 0x05 // constant
	DW_FORM_data4        Form = 0x06 // constant
	DW_FORM_data8        Form = 0x07 // constant
	DW_FORM_string       Form = 0x08 // string
	DW_FORM_block        Form = 0x09 // block
	DW_FORM_block1       Form = 0x0a // block
	DW_FORM_data1        Form = 0x0b // constant
	DW_FORM_flag         Form = 0x0c // flag
	DW_FORM_sdata        Form = 0x0d // constant
	DW_FORM_strp         Form = 0x0e // string
	DW_FORM_udata        Form = 0x0f // constant
	DW_FORM_ref_addr     Form = 0x10 // reference
	DW_FORM_ref1         Form = 0x11 // reference
	DW_FORM_ref2         Form = 0x12 // reference
	DW_FORM_ref4         Form = 0x13 // reference
	DW_FORM_ref8         Form = 0x14 // reference
	DW_FORM_ref_udata    Form = 0x15 // reference
	DW_FORM_indirect     Form = 0x16 // (see Section 7.5.3)
	DW_FORM_sec_offset   Form = 0x17 // lineptr, loclistptr, macptr, rangelistptr
	DW_FORM_exprloc      Form = 0x18 // exprloc
	DW_FORM_flag_present Form = 0x19 // flag
	DW_FORM_ref_sig8     Form = 0x20 // reference
)

// Encoding represents a DWARF base type encoding (see section 7.8, page 168
// and following, DWARF v4).
type Encoding uint16

const (
	DW_ATE_address         Encoding = 0x01
	DW_ATE_boolean         Encoding = 0x02
	DW_ATE_complex_float   Encoding = 0x03
	DW_ATE_float           Encoding = 0x04
	DW_ATE_signed          Encoding = 0x05
	DW_ATE_signed_char     Encoding = 0x06
	DW_ATE_unsigned        Encoding = 0x07
	DW_ATE_unsigned_char   Encoding = 0x08
	DW_ATE_imaginary_float Encoding = 0x09
	DW_ATE_packed_decimal  Encoding = 0x0a
	DW_ATE_numeric_string  Encoding = 0x0b
	DW_ATE_edited          Encoding = 0x0c
	DW_ATE_signed_fixed    Encoding = 0x0d
	DW_ATE_unsigned_fixed  Encoding = 0x0e
	DW_ATE_decimal_float   Encoding = 0x0f
	DW_ATE_UTF             Encoding = 0x10
	DW_ATE_lo_user         Encoding = 0x80
	DW_ATE_hi_user         Encoding = 0xff
)

// Address represents a machine address.
type Address uint64

type tagDescr struct {
	tag dwarf.Tag

	attr     []dwarf.Attr
	form     []Form
	children bool
}

type tagState struct {
	off dwarf.Offset
	tagDescr
}

// TagOpen starts a new DIE, call TagClose after adding all attributes and
// children elements.
func (b *Builder) TagOpen(tag dwarf.Tag, name string) dwarf.Offset {
	if len(b.tagStack) > 0 {
		b.tagStack[len(b.tagStack)-1].children = true
	}
	ts := &tagState{off: dwarf.Offset(b.info.Len())}
	ts.tag = tag
	b.info.WriteByte(0)
	b.tagStack = append(b.tagStack, ts)
	if name != "" {
		b.Attr(dwarf.AttrName, name)
	}

	return ts.off
}

// SetHasChildren sets the current DIE as having children (even if none are added).
func (b *Builder) SetHasChildren() {
	if len(b.tagStack) == 0 {
		panic("NoChildren with no open tags")
	}
	b.tagStack[len(b.tagStack)-1].children = true
}

// TagClose closes the current DIE.
func (b *Builder) TagClose() {
	if len(b.tagStack) == 0 {
		panic("TagClose with no open tags")
	}
	tag := b.tagStack[len(b.tagStack)-1]
	abbrev := b.abbrevFor(tag.tagDescr)
	b.info.Bytes()[tag.off] = abbrev
	if tag.children {
		b.info.WriteByte(0)
	}
	b.tagStack = b.tagStack[:len(b.tagStack)-1]
}

// Attr adds an attribute to the current DIE.
func (b *Builder) Attr(attr dwarf.Attr, val any) dwarf.Offset {
	if len(b.tagStack) == 0 {
		panic("Attr with no open tags")
	}
	off := dwarf.Offset(b.info.Len())
	tag := b.tagStack[len(b.tagStack)-1]
	if tag.children {
		panic("Can't add attributes after adding children")
	}

	tag.attr = append(tag.attr, attr)

	switch x := val.(type) {
	case string:
		tag.form = append(tag.form, DW_FORM_string)
		b.info.WriteString(x)
		b.info.WriteByte(0)
	case uint8:
		tag.form = append(tag.form, DW_FORM_data1)
		binary.Write(&b.info, binary.LittleEndian, x)
	case uint16:
		tag.form = append(tag.form, DW_FORM_data2)
		binary.Write(&b.info, binary.LittleEndian, x)
	case uint64:
		tag.form = append(tag.form, DW_FORM_data8)
		binary.Write(&b.info, binary.LittleEndian, x)
	case Address:
		tag.form = append(tag.form, DW_FORM_addr)
		binary.Write(&b.info, binary.LittleEndian, x)
	case dwarf.Offset:
		tag.form = append(tag.form, DW_FORM_ref_addr)
		binary.Write(&b.info, binary.LittleEndian, x)
	case []byte:
		tag.form = append(tag.form, DW_FORM_block4)
		binary.Write(&b.info, binary.LittleEndian, uint32(len(x)))
		b.info.Write(x)
	case []LocEntry:
		tag.form = append(tag.form, DW_FORM_sec_offset)
		binary.Write(&b.info, binary.LittleEndian, uint32(b.loc.Len()))

		// base address
		binary.Write(&b.loc, binary.LittleEndian, ^uint64(0))
		binary.Write(&b.loc, binary.LittleEndian, uint64(0))

		for _, locentry := range x {
			binary.Write(&b.loc, binary.LittleEndian, locentry.Lowpc)
			binary.Write(&b.loc, binary.LittleEndian, locentry.Highpc)
			binary.Write(&b.loc, binary.LittleEndian, uint16(len(locentry.Loc)))
			b.loc.Write(locentry.Loc)
		}

		// end of loclist
		binary.Write(&b.loc, binary.LittleEndian, uint64(0))
		binary.Write(&b.loc, binary.LittleEndian, uint64(0))
	default:
		panic("unknown value type")
	}

	return off
}

// PatchOffset writes the offset 'patch' at offset patchedOffset.
func (b *Builder) PatchOffset(patchedOffset, patch dwarf.Offset) {
	infoBytes := b.info.Bytes()
	buf := new(bytes.Buffer)
	binary.Write(buf, binary.LittleEndian, patch)
	copy(infoBytes[patchedOffset:], buf.Bytes())
}

func sameTagDescr(a, b tagDescr) bool {
	if a.tag != b.tag {
		return false
	}
	if len(a.attr) != len(b.attr) {
		return false
	}
	if a.children != b.children {
		return false
	}
	for i := range a.attr {
		if a.attr[i] != b.attr[i] {
			return false
		}
		if a.form[i] != b.form[i] {
			return false
		}
	}
	return true
}

// abbrevFor returns an abbrev for the given entry description. If no abbrev
// for tag already exist a new one is created.
func (b *Builder) abbrevFor(tag tagDescr) byte {
	for abbrev, descr := range b.abbrevs {
		if sameTagDescr(descr, tag) {
			return byte(abbrev + 1)
		}
	}

	b.abbrevs = append(b.abbrevs, tag)
	return byte(len(b.abbrevs))
}

func (b *Builder) makeAbbrevTable() []byte {
	var abbrev bytes.Buffer

	for i := range b.abbrevs {
		leb128.EncodeUnsigned(&abbrev, uint64(i+1))
		leb128.EncodeUnsigned(&abbrev, uint64(b.abbrevs[i].tag))
		if b.abbrevs[i].children {
			abbrev.WriteByte(0x01)
		} else {
			abbrev.WriteByte(0x00)
		}
		for j := range b.abbrevs[i].attr {
			leb128.EncodeUnsigned(&abbrev, uint64(b.abbrevs[i].attr[j]))
			leb128.EncodeUnsigned(&abbrev, uint64(b.abbrevs[i].form[j]))
		}
		leb128.EncodeUnsigned(&abbrev, 0)
		leb128.EncodeUnsigned(&abbrev, 0)
	}

	leb128.EncodeUnsigned(&abbrev, uint64(0))

	return abbrev.Bytes()
}

func (b *Builder) AddCompileUnit(name string, lowPC uint64) dwarf.Offset {
	r := b.TagOpen(dwarf.TagCompileUnit, name)
	b.Attr(dwarf.AttrLowpc, lowPC)
	return r
}

// AddSubprogram adds a subprogram declaration to debug_info, must call
// TagClose after adding all local variables and parameters.
// Will write an abbrev corresponding to a DW_TAG_subprogram, followed by a
// DW_AT_lowpc and a DW_AT_highpc.
func (b *Builder) AddSubprogram(fnname string, lowpc, highpc uint64) dwarf.Offset {
	r := b.TagOpen(dwarf.TagSubprogram, fnname)
	b.Attr(dwarf.AttrLowpc, Address(lowpc))
	b.Attr(dwarf.AttrHighpc, Address(highpc))
	return r
}

// AddVariable adds a new variable entry to debug_info.
// Will write a DW_TAG_variable, followed by a DW_AT_type and a
// DW_AT_location.
func (b *Builder) AddVariable(varname string, typ dwarf.Offset, loc any) dwarf.Offset {
	r := b.TagOpen(dwarf.TagVariable, varname)
	b.Attr(dwarf.AttrType, typ)
	b.Attr(dwarf.AttrLocation, loc)
	b.TagClose()
	return r
}

// AddBaseType adds a new base type entry to debug_info.
// Will write a DW_TAG_base_type, followed by a DW_AT_encoding and a
// DW_AT_byte_size.
func (b *Builder) AddBaseType(typename string, encoding Encoding, byteSz uint16) dwarf.Offset {
	r := b.TagOpen(dwarf.TagBaseType, typename)
	b.Attr(dwarf.AttrEncoding, uint16(encoding))
	b.Attr(dwarf.AttrByteSize, byteSz)
	b.TagClose()
	return r
}

// AddStructType adds a new structure type to debug_info. Call TagClose to
// finish adding fields.
// Will write a DW_TAG_struct_type, followed by a DW_AT_byte_size.
func (b *Builder) AddStructType(typename string, byteSz uint16) dwarf.Offset {
	r := b.TagOpen(dwarf.TagStructType, typename)
	b.Attr(dwarf.AttrByteSize, byteSz)
	return r
}

// AddMember adds a new member entry to debug_info.
// Writes a DW_TAG_member followed by DW_AT_type and DW_AT_data_member_loc.
func (b *Builder) AddMember(fieldname string, typ dwarf.Offset, memberLoc []byte) dwarf.Offset {
	r := b.TagOpen(dwarf.TagMember, fieldname)
	b.Attr(dwarf.AttrType, typ)
	b.Attr(dwarf.AttrDataMemberLoc, memberLoc)
	b.TagClose()
	return r
}

// AddPointerType adds a new pointer type to debug_info.
func (b *Builder) AddPointerType(typename string, typ dwarf.Offset) dwarf.Offset {
	r := b.TagOpen(dwarf.TagPointerType, typename)
	b.Attr(dwarf.AttrType, typ)
	b.Attr(godwarf.AttrGoKind, uint8(22))
	b.TagClose()
	return r
}
