package line

import (
	"bytes"
	"encoding/binary"
	"path"
	"strings"

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

// DebugLinePrologue prologue of .debug_line data.
type DebugLinePrologue struct {
	UnitLength     uint32
	Version        uint16
	Length         uint32
	MinInstrLength uint8
	MaxOpPerInstr  uint8
	InitialIsStmt  uint8
	LineBase       int8
	LineRange      uint8
	OpcodeBase     uint8
	StdOpLengths   []uint8
}

// DebugLineInfo info of .debug_line data.
type DebugLineInfo struct {
	Prologue     *DebugLinePrologue
	IncludeDirs  []string
	FileNames    []*FileEntry
	Instructions []byte
	Lookup       map[string]*FileEntry

	Logf func(string, ...any)

	// stateMachineCache[pc] is a state machine stopped at pc
	stateMachineCache map[uint64]*StateMachine

	// lastMachineCache[pc] is a state machine stopped at an address after pc
	lastMachineCache map[uint64]*StateMachine

	// debugLineStr is the contents of the .debug_line_str section.
	debugLineStr []byte

	// staticBase is the address at which the executable is loaded, 0 for non-PIEs
	staticBase uint64

	// if normalizeBackslash is true all backslashes (\) will be converted into forward slashes (/)
	normalizeBackslash bool
	ptrSize            int
	endSeqIsValid      bool
}

// FileEntry file entry in File Name Table.
type FileEntry struct {
	Path        string
	DirIdx      uint64
	LastModTime uint64
	Length      uint64
}

type DebugLines []*DebugLineInfo

// ParseAll parses all debug_line segments found in data
func ParseAll(data []byte, debugLineStr []byte, logfn func(string, ...any), staticBase uint64, normalizeBackslash bool, ptrSize int) DebugLines {
	var (
		lines = make(DebugLines, 0)
		buf   = bytes.NewBuffer(data)
	)

	// We have to parse multiple file name tables here.
	for buf.Len() > 0 {
		lines = append(lines, Parse("", buf, debugLineStr, logfn, staticBase, normalizeBackslash, ptrSize))
	}

	return lines
}

// Parse parses a single debug_line segment from buf. Compdir is the
// DW_AT_comp_dir attribute of the associated compile unit.
func Parse(compdir string, buf *bytes.Buffer, debugLineStr []byte, logfn func(string, ...any), staticBase uint64, normalizeBackslash bool, ptrSize int) *DebugLineInfo {
	dbl := new(DebugLineInfo)
	dbl.Logf = logfn
	if logfn == nil {
		dbl.Logf = func(string, ...any) {}
	}
	dbl.staticBase = staticBase
	dbl.ptrSize = ptrSize
	dbl.Lookup = make(map[string]*FileEntry)
	dbl.IncludeDirs = append(dbl.IncludeDirs, compdir)

	dbl.stateMachineCache = make(map[uint64]*StateMachine)
	dbl.lastMachineCache = make(map[uint64]*StateMachine)
	dbl.normalizeBackslash = normalizeBackslash
	dbl.debugLineStr = debugLineStr

	parseDebugLinePrologue(dbl, buf)
	if dbl.Prologue.Version >= 5 {
		if !parseIncludeDirs5(dbl, buf) {
			return nil
		}
		if !parseFileEntries5(dbl, buf) {
			return nil
		}
	} else {
		if !parseIncludeDirs2(dbl, buf) {
			return nil
		}
		if !parseFileEntries2(dbl, buf) {
			return nil
		}
	}

	// Instructions size calculation breakdown:
	//   - dbl.Prologue.UnitLength is the length of the entire unit, not including the 4 bytes to represent that length.
	//   - dbl.Prologue.Length is the length of the prologue not including unit length, version or prologue length itself.
	//   - So you have UnitLength - PrologueLength - (version_length_bytes(2) + prologue_length_bytes(4)).
	verDelta := uint32(6)
	if dbl.Prologue.Version >= 5 {
		verDelta = uint32(8)
	}
	dbl.Instructions = buf.Next(int(dbl.Prologue.UnitLength - dbl.Prologue.Length - verDelta))

	return dbl
}

func parseDebugLinePrologue(dbl *DebugLineInfo, buf *bytes.Buffer) {
	p := new(DebugLinePrologue)

	p.UnitLength = binary.LittleEndian.Uint32(buf.Next(4))
	p.Version = binary.LittleEndian.Uint16(buf.Next(2))
	if p.Version >= 5 {
		dbl.ptrSize = int(buf.Next(1)[0])  // address_size
		dbl.ptrSize += int(buf.Next(1)[0]) // segment_selector_size
	}

	p.Length = binary.LittleEndian.Uint32(buf.Next(4))
	p.MinInstrLength = buf.Next(1)[0]
	if p.Version >= 4 {
		p.MaxOpPerInstr = buf.Next(1)[0]
	} else {
		p.MaxOpPerInstr = 1
	}
	p.InitialIsStmt = buf.Next(1)[0]
	p.LineBase = int8(buf.Next(1)[0])
	p.LineRange = buf.Next(1)[0]
	p.OpcodeBase = buf.Next(1)[0]

	p.StdOpLengths = make([]uint8, p.OpcodeBase-1)
	binary.Read(buf, binary.LittleEndian, &p.StdOpLengths)

	dbl.Prologue = p
}

// parseIncludeDirs2 parses the directory table for DWARF version 2 through 4.
func parseIncludeDirs2(info *DebugLineInfo, buf *bytes.Buffer) bool {
	for {
		str, err := dwarf.ReadString(buf)
		if err != nil {
			if info.Logf != nil {
				info.Logf("error reading string: %v", err)
			}
			return false
		}
		if str == "" {
			break
		}

		info.IncludeDirs = append(info.IncludeDirs, str)
	}
	return true
}

// parseIncludeDirs5 parses the directory table for DWARF version 5.
func parseIncludeDirs5(info *DebugLineInfo, buf *bytes.Buffer) bool {
	dirEntryFormReader := readEntryFormat(buf, info.Logf)
	if dirEntryFormReader == nil {
		return false
	}
	dirCount, _ := leb128.DecodeUnsigned(buf)
	info.IncludeDirs = make([]string, 0, dirCount)
	for range dirCount {
		dirEntryFormReader.reset()
		for dirEntryFormReader.next(buf) {
			switch dirEntryFormReader.contentType {
			case _DW_LNCT_path:
				switch dirEntryFormReader.formCode {
				case _DW_FORM_string:
					info.IncludeDirs = append(info.IncludeDirs, dirEntryFormReader.str)
				case _DW_FORM_line_strp:
					buf := bytes.NewBuffer(info.debugLineStr[dirEntryFormReader.u64:])
					dir, _ := dwarf.ReadString(buf)
					info.IncludeDirs = append(info.IncludeDirs, dir)
				default:
					info.Logf("unsupported string form %#x", dirEntryFormReader.formCode)
				}
			case _DW_LNCT_directory_index:
			case _DW_LNCT_timestamp:
			case _DW_LNCT_size:
			case _DW_LNCT_MD5:
			}
		}
		if dirEntryFormReader.err != nil {
			if info.Logf != nil {
				info.Logf("error reading directory entries table: %v", dirEntryFormReader.err)
			}
			return false
		}
	}
	return true
}

// parseFileEntries2 parses the file table for DWARF 2 through 4
func parseFileEntries2(info *DebugLineInfo, buf *bytes.Buffer) bool {
	for {
		entry := readFileEntry(info, buf, true)
		if entry == nil {
			return false
		}
		if entry.Path == "" {
			break
		}

		info.FileNames = append(info.FileNames, entry)
		info.Lookup[entry.Path] = entry
	}
	return true
}

func readFileEntry(info *DebugLineInfo, buf *bytes.Buffer, exitOnEmptyPath bool) *FileEntry {
	entry := new(FileEntry)

	var err error
	entry.Path, err = dwarf.ReadString(buf)
	if err != nil {
		if info.Logf != nil {
			info.Logf("error reading file entry: %v", err)
		}
		return nil
	}
	if entry.Path == "" && exitOnEmptyPath {
		return entry
	}

	if info.normalizeBackslash {
		entry.Path = strings.ReplaceAll(entry.Path, "\\", "/")
	}

	entry.DirIdx, _ = leb128.DecodeUnsigned(buf)
	entry.LastModTime, _ = leb128.DecodeUnsigned(buf)
	entry.Length, _ = leb128.DecodeUnsigned(buf)
	if !pathIsAbs(entry.Path) {
		if entry.DirIdx < uint64(len(info.IncludeDirs)) {
			entry.Path = path.Join(info.IncludeDirs[entry.DirIdx], entry.Path)
		}
	}

	return entry
}

// pathIsAbs returns true if this is an absolute path.
// We can not use path.IsAbs because it will not recognize windows paths as
// absolute. We also can not use filepath.Abs because we want this
// processing to be independent of the host operating system (we could be
// reading an executable file produced on windows on a unix machine or vice
// versa).
func pathIsAbs(s string) bool {
	if len(s) >= 1 && s[0] == '/' {
		return true
	}
	if len(s) >= 2 && s[1] == ':' && (('a' <= s[0] && s[0] <= 'z') || ('A' <= s[0] && s[0] <= 'Z')) {
		return true
	}
	return false
}

// parseFileEntries5 parses the file table for DWARF 5
func parseFileEntries5(info *DebugLineInfo, buf *bytes.Buffer) bool {
	fileEntryFormReader := readEntryFormat(buf, info.Logf)
	if fileEntryFormReader == nil {
		return false
	}
	fileCount, _ := leb128.DecodeUnsigned(buf)
	info.FileNames = make([]*FileEntry, 0, fileCount)
	for i := 0; i < int(fileCount); i++ {
		var (
			p      string
			diridx int

			entry = new(FileEntry)
		)

		fileEntryFormReader.reset()

		for fileEntryFormReader.next(buf) {
			diridx = -1

			switch fileEntryFormReader.contentType {
			case _DW_LNCT_path:
				switch fileEntryFormReader.formCode {
				case _DW_FORM_string:
					p = fileEntryFormReader.str
				case _DW_FORM_line_strp:
					buf := bytes.NewBuffer(info.debugLineStr[fileEntryFormReader.u64:])
					p, _ = dwarf.ReadString(buf)
				default:
					info.Logf("unsupported string form %#x", fileEntryFormReader.formCode)
				}
			case _DW_LNCT_directory_index:
				diridx = int(fileEntryFormReader.u64)
			case _DW_LNCT_timestamp:
				entry.LastModTime = fileEntryFormReader.u64
			case _DW_LNCT_size:
				entry.Length = fileEntryFormReader.u64
			case _DW_LNCT_MD5:
				// not implemented
			}
		}
		if fileEntryFormReader.err != nil {
			if info.Logf != nil {
				info.Logf("error reading file entries table: %v", fileEntryFormReader.err)
			}
			return false
		}

		if diridx >= 0 && diridx < len(info.IncludeDirs) {
			p = path.Join(info.IncludeDirs[diridx], p)
		}
		if info.normalizeBackslash {
			p = strings.ReplaceAll(p, "\\", "/")
		}
		entry.Path = p
		info.FileNames = append(info.FileNames, entry)
		info.Lookup[entry.Path] = entry
	}
	return true
}
