package core

import (
	"bytes"
	"debug/elf"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"os"
	"strings"

	"github.com/go-delve/delve/pkg/elfwriter"
	"github.com/go-delve/delve/pkg/proc"
	"github.com/go-delve/delve/pkg/proc/amd64util"
	"github.com/go-delve/delve/pkg/proc/linutil"
)

// Copied from golang.org/x/sys/unix.Timeval since it's not available on all
// systems.
type linuxCoreTimeval struct {
	Sec  int64
	Usec int64
}

// NT_FILE is file mapping information, e.g. program text mappings. Desc is a LinuxNTFile.
const _NT_FILE elf.NType = 0x46494c45 // "FILE".

// NT_X86_XSTATE is other registers, including AVX and such.
const _NT_X86_XSTATE elf.NType = 0x202 // Note type for notes containing X86 XSAVE area.

// NT_AUXV is the note type for notes containing a copy of the Auxv array
const _NT_AUXV elf.NType = 0x6

// NT_FPREGSET is the note type for floating point registers.
const _NT_FPREGSET elf.NType = 0x2

// Fetch architecture using exeELF.Machine from core file
// Refer https://man7.org/linux/man-pages/man5/elf.5.html
const (
	_EM_AARCH64          = 183
	_EM_X86_64           = 62
	_EM_RISCV            = 243
	_EM_LOONGARCH        = 258
	_ARM_FP_HEADER_START = 512
)

const elfErrorBadMagicNumber = "bad magic number"

func linuxThreadsFromNotes(p *process, notes []*note, machineType elf.Machine) proc.Thread {
	var currentThread proc.Thread
	var lastThread osThread

	for _, note := range notes {
		switch note.Type {
		case elf.NT_PRSTATUS:
			switch machineType {
			case _EM_X86_64:
				t := note.Desc.(*linuxPrStatusAMD64)
				lastThread = &linuxAMD64Thread{linutil.AMD64Registers{Regs: &t.Reg}, t}
			case _EM_AARCH64:
				t := note.Desc.(*linuxPrStatusARM64)
				lastThread = &linuxARM64Thread{linutil.ARM64Registers{Regs: &t.Reg}, t}
			case _EM_RISCV:
				t := note.Desc.(*linuxPrStatusRISCV64)
				lastThread = &linuxRISCV64Thread{linutil.RISCV64Registers{Regs: &t.Reg}, t}
			case _EM_LOONGARCH:
				t := note.Desc.(*linuxPrStatusLOONG64)
				lastThread = &linuxLOONG64Thread{linutil.LOONG64Registers{Regs: &t.Reg}, t}
			default:
				continue
			}
			p.Threads[lastThread.ThreadID()] = &thread{lastThread, p, proc.CommonThread{}}
			if currentThread == nil {
				currentThread = p.Threads[lastThread.ThreadID()]
			}
		case _NT_FPREGSET:
			switch th := lastThread.(type) {
			case *linuxARM64Thread:
				th.regs.Fpregs = note.Desc.(*linutil.ARM64PtraceFpRegs).Decode()
			case *linuxRISCV64Thread:
				th.regs.Fpregs = note.Desc.(*linutil.RISCV64PtraceFpRegs).Decode()
			case *linuxLOONG64Thread:
				th.regs.Fpregs = note.Desc.(*linutil.LOONG64PtraceFpRegs).Decode()
			}
		case _NT_X86_XSTATE:
			if lastThread != nil {
				lastThread.(*linuxAMD64Thread).regs.Fpregs = note.Desc.(*amd64util.AMD64Xstate).Decode()
			}
		case elf.NT_PRPSINFO:
			p.pid = int(note.Desc.(*linuxPrPsInfo).Pid)
		}
	}
	return currentThread
}

var supportedLinuxMachines = map[elf.Machine]string{
	_EM_X86_64:    "amd64",
	_EM_AARCH64:   "arm64",
	_EM_RISCV:     "riscv64",
	_EM_LOONGARCH: "loong64",
}

// readLinuxOrPlatformIndependentCore reads a core file from corePath
// corresponding to the executable at exePath. For details on the Linux ELF
// core format, see:
// https://www.gabriel.urdhr.fr/2015/05/29/core-file/,
// https://uhlo.blogspot.com/2012/05/brief-look-into-core-dumps.html,
// elf_core_dump in https://elixir.bootlin.com/linux/v4.20.17/source/fs/binfmt_elf.c,
// and, if absolutely desperate, readelf.c from the binutils source.
func readLinuxOrPlatformIndependentCore(corePath, exePath string) (*process, proc.Thread, error) {
	coreFile, err := elf.Open(corePath)
	if err != nil {
		if _, isfmterr := err.(*elf.FormatError); isfmterr && (strings.Contains(err.Error(), elfErrorBadMagicNumber) || strings.Contains(err.Error(), " at offset 0x0: too short")) {
			// Go >=1.11 and <1.11 produce different errors when reading a non-elf file.
			return nil, nil, ErrUnrecognizedFormat
		}
		return nil, nil, err
	}

	if coreFile.Type != elf.ET_CORE {
		return nil, nil, fmt.Errorf("%v is not a core file", coreFile)
	}

	machineType := coreFile.Machine
	notes, platformIndependentDelveCore, err := readNotes(coreFile, machineType)
	if err != nil {
		return nil, nil, err
	}

	exe, err := os.Open(exePath)
	if err != nil {
		return nil, nil, err
	}
	exeELF, err := elf.NewFile(exe)
	if err != nil {
		if !platformIndependentDelveCore {
			return nil, nil, err
		}
	} else {
		if exeELF.Machine != machineType {
			return nil, nil, fmt.Errorf("architecture mismatch between core file (%#x) and executable file (%#x)", machineType, exeELF.Machine)
		}
		if exeELF.Type != elf.ET_EXEC && exeELF.Type != elf.ET_DYN {
			return nil, nil, fmt.Errorf("%v is not an exe file", exeELF)
		}
	}

	memory := buildMemory(coreFile, exeELF, exe, notes)

	// TODO support 386
	var bi *proc.BinaryInfo
	if platformIndependentDelveCore {
		goos, goarch, err := platformFromNotes(notes)
		if err != nil {
			return nil, nil, err
		}
		bi = proc.NewBinaryInfo(goos, goarch)
	} else if goarch, ok := supportedLinuxMachines[machineType]; ok {
		bi = proc.NewBinaryInfo("linux", goarch)
	} else {
		return nil, nil, errors.New("unsupported machine type")
	}

	entryPoint := findEntryPoint(notes, bi.Arch.PtrSize())

	p := &process{
		mem:         memory,
		Threads:     map[int]*thread{},
		entryPoint:  entryPoint,
		bi:          bi,
		breakpoints: proc.NewBreakpointMap(),
	}

	if platformIndependentDelveCore {
		currentThread, err := threadsFromDelveNotes(p, notes)
		return p, currentThread, err
	}

	currentThread := linuxThreadsFromNotes(p, notes, machineType)
	return p, currentThread, nil
}

type linuxAMD64Thread struct {
	regs linutil.AMD64Registers
	t    *linuxPrStatusAMD64
}

type linuxARM64Thread struct {
	regs linutil.ARM64Registers
	t    *linuxPrStatusARM64
}

type linuxRISCV64Thread struct {
	regs linutil.RISCV64Registers
	t    *linuxPrStatusRISCV64
}

type linuxLOONG64Thread struct {
	regs linutil.LOONG64Registers
	t    *linuxPrStatusLOONG64
}

func (t *linuxAMD64Thread) Registers() (proc.Registers, error) {
	var r linutil.AMD64Registers
	r.Regs = t.regs.Regs
	r.Fpregs = t.regs.Fpregs
	return &r, nil
}

func (t *linuxARM64Thread) Registers() (proc.Registers, error) {
	var r linutil.ARM64Registers
	r.Regs = t.regs.Regs
	r.Fpregs = t.regs.Fpregs
	return &r, nil
}

func (t *linuxRISCV64Thread) Registers() (proc.Registers, error) {
	var r linutil.RISCV64Registers
	r.Regs = t.regs.Regs
	r.Fpregs = t.regs.Fpregs
	return &r, nil
}

func (t *linuxLOONG64Thread) Registers() (proc.Registers, error) {
	var r linutil.LOONG64Registers
	r.Regs = t.regs.Regs
	r.Fpregs = t.regs.Fpregs
	return &r, nil
}

func (t *linuxAMD64Thread) ThreadID() int {
	return int(t.t.Pid)
}

func (t *linuxARM64Thread) ThreadID() int {
	return int(t.t.Pid)
}

func (t *linuxRISCV64Thread) ThreadID() int {
	return int(t.t.Pid)
}

func (t *linuxLOONG64Thread) ThreadID() int {
	return int(t.t.Pid)
}

// Note is a note from the PT_NOTE prog.
// Relevant types:
// - NT_FILE: File mapping information, e.g. program text mappings. Desc is a LinuxNTFile.
// - NT_PRPSINFO: Information about a process, including PID and signal. Desc is a LinuxPrPsInfo.
// - NT_PRSTATUS: Information about a thread, including base registers, state, etc. Desc is a LinuxPrStatus.
// - NT_FPREGSET (Not implemented): x87 floating point registers.
// - NT_X86_XSTATE: Other registers, including AVX and such.
type note struct {
	Type elf.NType
	Name string
	Desc any // Decoded Desc from the
}

// readNotes reads all the notes from the notes prog in core.
func readNotes(core *elf.File, machineType elf.Machine) ([]*note, bool, error) {
	var notesProg *elf.Prog
	for _, prog := range core.Progs {
		if prog.Type == elf.PT_NOTE {
			notesProg = prog
			break
		}
	}

	r := notesProg.Open()
	hasDelveThread := false
	hasDelveHeader := false
	hasElfPrStatus := false
	notes := []*note{}
	for {
		note, err := readNote(r, machineType)
		if err == io.EOF {
			break
		}
		if err != nil {
			return nil, false, err
		}
		switch note.Type {
		case elfwriter.DelveHeaderNoteType:
			hasDelveHeader = true
		case elfwriter.DelveThreadNodeType:
			hasDelveThread = true
		case elf.NT_PRSTATUS:
			hasElfPrStatus = true
		}
		notes = append(notes, note)
	}

	return notes, hasDelveThread && hasDelveHeader && !hasElfPrStatus, nil
}

// readNote reads a single note from r, decoding the descriptor if possible.
func readNote(r io.ReadSeeker, machineType elf.Machine) (*note, error) {
	// Notes are laid out as described in the SysV ABI:
	// https://www.sco.com/developers/gabi/latest/ch5.pheader.html#note_section
	note := &note{}
	hdr := &elfNotesHdr{}

	err := binary.Read(r, binary.LittleEndian, hdr)
	if err != nil {
		return nil, err // don't wrap so readNotes sees EOF.
	}
	note.Type = elf.NType(hdr.Type)

	name := make([]byte, hdr.Namesz)
	if _, err := r.Read(name); err != nil {
		return nil, fmt.Errorf("reading name: %v", err)
	}
	note.Name = string(name)
	if err := skipPadding(r, 4); err != nil {
		return nil, fmt.Errorf("aligning after name: %v", err)
	}
	desc := make([]byte, hdr.Descsz)
	if _, err := r.Read(desc); err != nil {
		return nil, fmt.Errorf("reading desc: %v", err)
	}
	descReader := bytes.NewReader(desc)
	switch note.Type {
	case elf.NT_PRSTATUS:
		switch machineType {
		case _EM_X86_64:
			note.Desc = &linuxPrStatusAMD64{}
		case _EM_AARCH64:
			note.Desc = &linuxPrStatusARM64{}
		case _EM_RISCV:
			note.Desc = &linuxPrStatusRISCV64{}
		case _EM_LOONGARCH:
			note.Desc = &linuxPrStatusLOONG64{}
		default:
			return nil, errors.New("unsupported machine type")
		}
		if err := binary.Read(descReader, binary.LittleEndian, note.Desc); err != nil {
			return nil, fmt.Errorf("reading NT_PRSTATUS: %v", err)
		}
	case elf.NT_PRPSINFO:
		note.Desc = &linuxPrPsInfo{}
		if err := binary.Read(descReader, binary.LittleEndian, note.Desc); err != nil {
			return nil, fmt.Errorf("reading NT_PRPSINFO: %v", err)
		}
	case _NT_FILE:
		// No good documentation reference, but the structure is
		// simply a header, including entry count, followed by that
		// many entries, and then the file name of each entry,
		// null-delimited. Not reading the names here.
		data := &linuxNTFile{}
		if err := binary.Read(descReader, binary.LittleEndian, &data.linuxNTFileHdr); err != nil {
			return nil, fmt.Errorf("reading NT_FILE header: %v", err)
		}
		for i := 0; i < int(data.Count); i++ {
			entry := &linuxNTFileEntry{}
			if err := binary.Read(descReader, binary.LittleEndian, entry); err != nil {
				return nil, fmt.Errorf("reading NT_FILE entry %v: %v", i, err)
			}
			data.entries = append(data.entries, entry)
		}
		note.Desc = data
	case _NT_X86_XSTATE:
		if machineType == _EM_X86_64 {
			var fpregs amd64util.AMD64Xstate
			if err := amd64util.AMD64XstateRead(desc, true, &fpregs, 0); err != nil {
				return nil, err
			}
			note.Desc = &fpregs
		}
	case _NT_AUXV, elfwriter.DelveHeaderNoteType, elfwriter.DelveThreadNodeType:
		note.Desc = desc
	case _NT_FPREGSET:
		if machineType == _EM_AARCH64 {
			err = readFpregsetNote(note, &linutil.ARM64PtraceFpRegs{}, desc[:_ARM_FP_HEADER_START])
		} else if machineType == _EM_RISCV {
			err = readFpregsetNote(note, &linutil.RISCV64PtraceFpRegs{}, desc)
		} else if machineType == _EM_LOONGARCH {
			err = readFpregsetNote(note, &linutil.LOONG64PtraceFpRegs{}, desc)
		}
		if err != nil {
			return nil, err
		}
	}
	if err := skipPadding(r, 4); err != nil {
		return nil, fmt.Errorf("aligning after desc: %v", err)
	}
	return note, nil
}

func readFpregsetNote(note *note, fpregs interface{ Byte() []byte }, desc []byte) error {
	rdr := bytes.NewReader(desc)
	if err := binary.Read(rdr, binary.LittleEndian, fpregs.Byte()); err != nil {
		return err
	}
	note.Desc = fpregs
	return nil
}

// skipPadding moves r to the next multiple of pad.
func skipPadding(r io.ReadSeeker, pad int64) error {
	pos, err := r.Seek(0, io.SeekCurrent)
	if err != nil {
		return err
	}
	if pos%pad == 0 {
		return nil
	}
	if _, err := r.Seek(pad-(pos%pad), io.SeekCurrent); err != nil {
		return err
	}
	return nil
}

func buildMemory(core, exeELF *elf.File, exe io.ReaderAt, notes []*note) proc.MemoryReader {
	memory := &SplicedMemory{}

	// For now, assume all file mappings are to the exe.
	for _, note := range notes {
		if note.Type == _NT_FILE {
			fileNote := note.Desc.(*linuxNTFile)
			for _, entry := range fileNote.entries {
				r := &offsetReaderAt{
					reader: exe,
					offset: entry.Start - (entry.FileOfs * fileNote.PageSize),
				}
				memory.Add(r, entry.Start, entry.End-entry.Start)
			}
		}
	}

	// Load memory segments from exe and then from the core file,
	// allowing the corefile to overwrite previously loaded segments
	for _, elfFile := range []*elf.File{exeELF, core} {
		if elfFile == nil {
			continue
		}
		for _, prog := range elfFile.Progs {
			if prog.Type == elf.PT_LOAD {
				if prog.Filesz == 0 {
					continue
				}
				r := &offsetReaderAt{
					reader: prog.ReaderAt,
					offset: prog.Vaddr,
				}
				memory.Add(r, prog.Vaddr, prog.Filesz)
			}
		}
	}
	return memory
}

func findEntryPoint(notes []*note, ptrSize int) uint64 {
	for _, note := range notes {
		if note.Type == _NT_AUXV {
			return linutil.EntryPointFromAuxv(note.Desc.([]byte), ptrSize)
		}
	}
	return 0
}

// LinuxPrPsInfo has various structures from the ELF spec and the Linux kernel.
// AMD64 specific primarily because of unix.PtraceRegs, but also
// because some of the fields are word sized.
// See https://elixir.bootlin.com/linux/v4.20.17/source/include/uapi/linux/elfcore.h
type linuxPrPsInfo struct {
	State                uint8
	Sname                int8
	Zomb                 uint8
	Nice                 int8
	_                    [4]uint8
	Flag                 uint64
	Uid, Gid             uint32
	Pid, Ppid, Pgrp, Sid int32
	Fname                [16]uint8
	Args                 [80]uint8
}

// LinuxPrStatusAMD64 is a copy of the prstatus kernel struct.
type linuxPrStatusAMD64 struct {
	Siginfo                      linuxSiginfo
	Cursig                       uint16
	_                            [2]uint8
	Sigpend                      uint64
	Sighold                      uint64
	Pid, Ppid, Pgrp, Sid         int32
	Utime, Stime, CUtime, CStime linuxCoreTimeval
	Reg                          linutil.AMD64PtraceRegs
	Fpvalid                      int32
}

// LinuxPrStatusARM64 is a copy of the prstatus kernel struct.
type linuxPrStatusARM64 struct {
	Siginfo                      linuxSiginfo
	Cursig                       uint16
	_                            [2]uint8
	Sigpend                      uint64
	Sighold                      uint64
	Pid, Ppid, Pgrp, Sid         int32
	Utime, Stime, CUtime, CStime linuxCoreTimeval
	Reg                          linutil.ARM64PtraceRegs
	Fpvalid                      int32
}

// LinuxPrStatusRISCV64 is a copy of the prstatus kernel struct.
type linuxPrStatusRISCV64 struct {
	Siginfo                      linuxSiginfo
	Cursig                       uint16
	_                            [2]uint8
	Sigpend                      uint64
	Sighold                      uint64
	Pid, Ppid, Pgrp, Sid         int32
	Utime, Stime, CUtime, CStime linuxCoreTimeval
	Reg                          linutil.RISCV64PtraceRegs
	Fpvalid                      int32
}

// LinuxPrStatusLOONG64 is a copy of the prstatus kernel struct.
type linuxPrStatusLOONG64 struct {
	Siginfo                      linuxSiginfo
	Cursig                       uint16
	_                            [2]uint8
	Sigpend                      uint64
	Sighold                      uint64
	Pid, Ppid, Pgrp, Sid         int32
	Utime, Stime, CUtime, CStime linuxCoreTimeval
	Reg                          linutil.LOONG64PtraceRegs
	Fpvalid                      int32
}

// LinuxSiginfo is a copy of the
// siginfo kernel struct.
type linuxSiginfo struct {
	Signo int32
	Code  int32
	Errno int32
}

// LinuxNTFile contains information on mapped files.
type linuxNTFile struct {
	linuxNTFileHdr
	entries []*linuxNTFileEntry
}

// LinuxNTFileHdr is a header struct for NTFile.
type linuxNTFileHdr struct {
	Count    uint64
	PageSize uint64
}

// LinuxNTFileEntry is an entry of an NT_FILE note.
type linuxNTFileEntry struct {
	Start   uint64
	End     uint64
	FileOfs uint64
}

// elfNotesHdr is the ELF Notes header.
// Same size on 64 and 32-bit machines.
type elfNotesHdr struct {
	Namesz uint32
	Descsz uint32
	Type   uint32
}
