package winpwn

import (
	"errors"
	"strings"
)

// Export describes one entry of a PE's export table (EAT), the analogue of
// pwntools' libc.symbols[...] entries.
type Export struct {
	// Name is empty when the function is exported by ordinal only.
	Name    string
	Ordinal uint16
	// RVA is the function's address. It is zero when ForwardTarget is set:
	// the export doesn't point at code in this module at all, it forwards
	// to a function in another DLL.
	RVA uint32
	// ForwardTarget is "DLLNAME.FuncName" when this export is a forwarder
	// (e.g. api-ms-win-core-*.dll entries that forward into kernelbase.dll).
	// Use ParseForwardTarget to split it.
	ForwardTarget string
}

// ParseForwardTarget splits a forwarder string ("KERNELBASE.CreateFileW")
// into the target DLL and function name.
func ParseForwardTarget(forward string) (dll string, fn string) {
	idx := strings.LastIndex(forward, ".")
	if idx == -1 {
		return "", forward
	}
	return forward[:idx], forward[idx+1:]
}

// exportDirectory mirrors winnt.h's IMAGE_EXPORT_DIRECTORY.
type exportDirectory struct {
	Characteristics       uint32
	TimeDateStamp         uint32
	MajorVersion          uint16
	MinorVersion          uint16
	Name                  uint32
	Base                  uint32
	NumberOfFunctions     uint32
	NumberOfNames         uint32
	AddressOfFunctions    uint32
	AddressOfNames        uint32
	AddressOfNameOrdinals uint32
}

// ListExports walks the full Export Address Table, the analogue of
// pwntools' libc.symbols when you need every entry rather than a single
// lookup. Functions exported by ordinal only (no name) come back with
// Name == "". Works against a live-process-backed PEFile exactly as well
// as a disk-backed one (see RVAToFileOffset).
func (p *PEFile) ListExports() ([]Export, error) {
	h, err := p.header()
	if err != nil {
		return nil, err
	}

	dir := h.dataDirectory[0]
	if dir.VirtualAddress == 0 {
		return nil, errors.New("export table not found")
	}
	exportOffset := p.RVAToFileOffset(dir.VirtualAddress)
	if exportOffset == 0 {
		return nil, errors.New("failed to map export directory RVA to file offset")
	}

	var expDir exportDirectory
	if err := p.readStructAt(exportOffset, &expDir); err != nil {
		return nil, err
	}

	funcsOffset := p.RVAToFileOffset(expDir.AddressOfFunctions)
	namesOffset := p.RVAToFileOffset(expDir.AddressOfNames)
	ordinalsOffset := p.RVAToFileOffset(expDir.AddressOfNameOrdinals)

	// Build ordinal-index -> name from the name table before walking
	// AddressOfFunctions, since not every function slot has a name.
	nameByOrdinalIndex := make(map[uint16]string, expDir.NumberOfNames)
	for i := uint32(0); i < expDir.NumberOfNames; i++ {
		var nameRVA uint32
		if err := p.readStructAt(namesOffset+int64(i*4), &nameRVA); err != nil {
			return nil, err
		}
		name, err := p.readCString(p.RVAToFileOffset(nameRVA))
		if err != nil {
			return nil, err
		}

		var ordinalIndex uint16
		if err := p.readStructAt(ordinalsOffset+int64(i*2), &ordinalIndex); err != nil {
			return nil, err
		}
		nameByOrdinalIndex[ordinalIndex] = name
	}

	// A forwarder's "RVA" doesn't point at code: it points back inside the
	// export directory itself, at an ASCII "DLL.Func" string.
	forwarderLo := dir.VirtualAddress
	forwarderHi := dir.VirtualAddress + dir.Size

	exports := make([]Export, 0, expDir.NumberOfFunctions)
	for i := uint32(0); i < expDir.NumberOfFunctions; i++ {
		var rva uint32
		if err := p.readStructAt(funcsOffset+int64(i*4), &rva); err != nil {
			return nil, err
		}
		if rva == 0 {
			continue // unused ordinal slot
		}

		e := Export{
			Name:    nameByOrdinalIndex[uint16(i)],
			Ordinal: uint16(expDir.Base + i),
		}
		if rva >= forwarderLo && rva < forwarderHi {
			fwd, err := p.readCString(p.RVAToFileOffset(rva))
			if err != nil {
				return nil, err
			}
			e.ForwardTarget = fwd
		} else {
			e.RVA = rva
		}
		exports = append(exports, e)
	}

	return exports, nil
}

// GetExport looks up a single export by name and reports whether it
// forwards to another DLL, unlike GetProcAddress which returns a bare RVA.
func (p *PEFile) GetExport(name string) (*Export, error) {
	exports, err := p.ListExports()
	if err != nil {
		return nil, err
	}
	for i := range exports {
		if exports[i].Name == name {
			return &exports[i], nil
		}
	}
	return nil, errors.New("function not found in export table: " + name)
}
