package pkgpath

import (
	"debug/elf"
	"debug/macho"
	"fmt"
	"io"
	"os"
	"runtime"
)

func newReader(r io.ReaderAt) (exeReader, error) {
	var file exeReader
	var err error
	file, err = newELFReader(r)
	if err == nil {
		return file, nil
	}
	file, err = newMachOReader(r)
	if err == nil {
		return file, nil
	}
	return nil, fmt.Errorf("unknown file format")
}

func findExecutableMainFile() (string, error) {
	exec, err := osExecutable()
	if err != nil {
		return "", err
	}
	r, err := os.Open(exec)
	if err != nil {
		return "", err
	}
	defer r.Close()

	rd, err := newReader(r)
	if err != nil {
		return "", err
	}

	file, _, err := findSelfFuncEntry(rd, "main.main")
	if err != nil {
		return "", err
	}
	return file, nil
}

func findSelfFuncEntry(file exeReader, name string) (string, int, error) {
	pc, err := file.FuncPC(name)
	if err != nil {
		return "", 0, fmt.Errorf("FuncPC: %v", err)
	}

	fn := runtime.FuncForPC(uintptr(pc))
	if fn == nil {
		return "", 0, fmt.Errorf("function not found")
	}
	filename, line := fn.FileLine(fn.Entry())
	return filename, line, nil
}

type exeReader interface {
	FuncPC(name string) (uint64, error)
}

type machoReader struct {
	file *macho.File
}

func newMachOReader(r io.ReaderAt) (*machoReader, error) {
	file, err := macho.NewFile(r)
	if err != nil {
		return nil, err
	}
	return &machoReader{file: file}, nil
}

func (r *machoReader) FuncPC(name string) (uint64, error) {
	for _, sym := range r.file.Symtab.Syms {
		if sym.Name == name {
			return sym.Value, nil
		}
	}

	return 0, fmt.Errorf("symbol not found")
}

type elfReader struct {
	file *elf.File
}

func newELFReader(r io.ReaderAt) (*elfReader, error) {
	file, err := elf.NewFile(r)
	if err != nil {
		return nil, err
	}
	return &elfReader{file: file}, nil
}

func (r *elfReader) FuncPC(name string) (uint64, error) {
	syms, err := r.file.Symbols()
	if err != nil {
		return 0, err
	}
	for _, sym := range syms {
		if sym.Name == name {
			return sym.Value, nil
		}
	}

	return 0, fmt.Errorf("symbol not found")
}
