/**
 * This module helps to decide whether an appropriate execinfo implementation
 * is available in the underling C runtime or in an external library. In the
 * latter case exactly one of the following version identifiers should be
 * set at the time of building druntime.
 *
 * Possible external execinfo version IDs based on possible backtrace output
 * formats:
 * $(TABLE
 * $(THEAD Version ID, Backtrace format)
 * $(TROW $(B ExtExecinfo_BSDFmt), 0x00000000 <_D6module4funcAFZv+0x78> at module)
 * $(TROW $(B ExtExecinfo_DarwinFmt), 1  module    0x00000000 D6module4funcAFZv + 0)
 * $(TROW $(B ExtExecinfo_GNUFmt), module(_D6module4funcAFZv) [0x00000000] $(B or)
 * module(_D6module4funcAFZv+0x78) [0x00000000] $(B or) module(_D6module4funcAFZv-0x78) [0x00000000])
 * $(TROW $(B ExtExecinfo_SolarisFmt), object'symbol+offset [pc])
 * )
 *
 * The code also ensures that at most one format is selected (either by automatic
 * C runtime detection or by $(B ExtExecinfo_) version IDs) and stores the
 * corresponding values in $(LREF BacktraceFmt).
 *
 * With $(LREF getMangledSymbolName) we can get the original mangled symbol name
 * from `backtrace_symbols` output of any supported version.
 *
 * Copyright: Copyright Digital Mars 2019.
 * License:   $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
 * Source: $(DRUNTIMESRC core/internal/_execinfo.d)
 */

module core.internal.execinfo;

version (OSX)
    version = Darwin;
else version (iOS)
    version = Darwin;
else version (TVOS)
    version = Darwin;
else version (WatchOS)
    version = Darwin;

version (ExtExecinfo_BSDFmt)
    version = _extExecinfo;
else version (ExtExecinfo_DarwinFmt)
    version = _extExecinfo;
else version (ExtExecinfo_GNUFmt)
    version = _extExecinfo;
else version (ExtExecinfo_SolarisFmt)
    version = _extExecinfo;

version (linux)
{
    version (CRuntime_Glibc)
        import _execinfo = core.sys.linux.execinfo;
    else version (CRuntime_UClibc)
        import _execinfo = core.sys.linux.execinfo;
    else version (_extExecinfo)
        import _execinfo = core.sys.linux.execinfo;
}
else version (Darwin)
    import _execinfo = core.sys.darwin.execinfo;
else version (FreeBSD)
    import _execinfo = core.sys.freebsd.execinfo;
else version (NetBSD)
    import _execinfo = core.sys.netbsd.execinfo;
else version (DragonFlyBSD)
    import _execinfo = core.sys.dragonflybsd.execinfo;
else version (Solaris)
    import _execinfo = core.sys.solaris.execinfo;

/// Indicates the availability of backtrace functions
enum bool hasExecinfo = is(_execinfo == module);

static if (hasExecinfo)
{
    /// Always points to the platform's backtrace function.
    alias backtrace = _execinfo.backtrace;

    /// Always points to the platform's backtrace_symbols function. The
    /// supported output format can be obtained by testing
    /// $(LREF BacktraceFmt) enum values.
    alias backtrace_symbols = _execinfo.backtrace_symbols;

    /// Always points to the platform's backtrace_symbols_fd function.
    alias backtrace_symbols_fd = _execinfo.backtrace_symbols_fd;
}

// Inspect possible backtrace formats
private
{
    version (FreeBSD)
        enum _BTFmt_BSD = true;
    else version (DragonFlyBSD)
        enum _BTFmt_BSD = true;
    else version (NetBSD)
        enum _BTFmt_BSD = true;
    else version (ExtExecinfo_BSDFmt)
        enum _BTFmt_BSD = true;
    else
        enum _BTFmt_BSD = false;

    version (Darwin)
        enum _BTFmt_Darwin = true;
    else version (ExtExecinfo_DarwinFmt)
        enum _BTFmt_Darwin = true;
    else
        enum _BTFmt_Darwin = false;

    version (CRuntime_Glibc)
        enum _BTFmt_GNU = true;
    else version (CRuntime_UClibc)
        enum _BTFmt_GNU = true;
    else version (ExtExecinfo_GNUFmt)
        enum _BTFmt_GNU = true;
    else
        enum _BTFmt_GNU = false;

    version (Solaris)
        enum _BTFmt_Solaris = true;
    else version (ExtExecinfo_SolarisFmt)
        enum _BTFmt_Solaris = true;
    else
        enum _BTFmt_Solaris = false;
}

/**
 * Indicates the backtrace format of the actual execinfo implementation.
 * At most one of the values is allowed to be set to `true` the
 * others should be `false`.
 */
enum BacktraceFmt : bool
{
    /// 0x00000000 <_D6module4funcAFZv+0x78> at module
    BSD = _BTFmt_BSD,

    /// 1  module    0x00000000 D6module4funcAFZv + 0
    Darwin = _BTFmt_Darwin,

    /// module(_D6module4funcAFZv) [0x00000000]
    /// $(B or) module(_D6module4funcAFZv+0x78) [0x00000000]
    /// $(B or) module(_D6module4funcAFZv-0x78) [0x00000000]
    GNU = _BTFmt_GNU,

    /// object'symbol+offset [pc]
    Solaris = _BTFmt_Solaris
}

private bool atMostOneBTFmt()
{
    size_t trueCnt = 0;

    foreach (fmt; __traits(allMembers, BacktraceFmt))
        if (__traits(getMember, BacktraceFmt, fmt)) ++trueCnt;

    return trueCnt <= 1;
}

static assert(atMostOneBTFmt, "Cannot be set more than one BacktraceFmt at the same time.");

/**
  * Takes a `backtrace_symbols` output and identifies the mangled symbol
  * name in it. Optionally, also sets the begin and end indices of the symbol name in
  * the input buffer.
  *
  * Params:
  *  btBuf = The input buffer containing the output of `backtrace_symbols`
  *  symBeg = Output parameter indexing the first character of the symbol's name
  *  symEnd = Output parameter indexing the first character after the symbol's name
  *
  * Returns:
  *  The name of the symbol
  */
static if (hasExecinfo)
const(char)[] getMangledSymbolName(const(char)[] btBuf, out size_t symBeg,
        out size_t symEnd) @nogc nothrow
{
    static if (BacktraceFmt.Darwin)
    {
        for (size_t i = 0, n = 0; i < btBuf.length; i++)
        {
            if (' ' == btBuf[i])
            {
                n++;
                while (i < btBuf.length && ' ' == btBuf[i])
                    i++;
                if (3 > n)
                    continue;

                symBeg = i;
                while (i < btBuf.length && ' ' != btBuf[i])
                    i++;
                symEnd = i;
                break;
            }
        }
    }
    else
    {
        static if (BacktraceFmt.GNU)
        {
            enum bChar = '(';
            enum eChar = ')';
        }
        else static if (BacktraceFmt.BSD)
        {
            enum bChar = '<';
            enum eChar = '>';
        }
        else static if (BacktraceFmt.Solaris)
        {
            enum bChar = '\'';
            enum eChar = '+';
        }

        foreach (i; 0 .. btBuf.length)
        {
            if (btBuf[i] == bChar)
            {
                foreach (j; i+1 .. btBuf.length)
                {
                    const e = btBuf[j];
                    if (e == eChar || e == '+' || e == '-')
                    {
                        symBeg = i + 1;
                        symEnd = j;
                        break;
                    }
                }
                break;
            }
        }
    }

    assert(symBeg <= symEnd);
    assert(symEnd < btBuf.length);

    return btBuf[symBeg .. symEnd];
}

/// ditto
static if (hasExecinfo)
const(char)[] getMangledSymbolName(const(char)[] btBuf) @nogc nothrow
{
    size_t symBeg, symEnd;
    return getMangledSymbolName(btBuf, symBeg, symEnd);
}

@nogc nothrow unittest
{
    size_t symBeg, symEnd;

    static if (BacktraceFmt.BSD)
    {
        enum bufBSD = "0x00000000 <_D6module4funcAFZv+0x78> at module";
        auto resBSD = getMangledSymbolName(bufBSD, symBeg, symEnd);
        assert("_D6module4funcAFZv" == resBSD);
        assert(12 == symBeg);
        assert(30 == symEnd);
    }
    else static if (BacktraceFmt.Darwin)
    {
        enum bufDarwin = "1  module    0x00000000 D6module4funcAFZv + 0";
        auto resDarwin = getMangledSymbolName(bufDarwin, symBeg, symEnd);
        assert("D6module4funcAFZv" == resDarwin);
        assert(24 == symBeg);
        assert(41 == symEnd);
    }
    else static if (BacktraceFmt.GNU)
    {
        enum bufGNU0 = "module(_D6module4funcAFZv) [0x00000000]";
        auto resGNU0 = getMangledSymbolName(bufGNU0, symBeg, symEnd);
        assert("_D6module4funcAFZv" == resGNU0);
        assert(7 == symBeg);
        assert(25 == symEnd);

        enum bufGNU1 = "module(_D6module4funcAFZv+0x78) [0x00000000]";
        auto resGNU1 = getMangledSymbolName(bufGNU1, symBeg, symEnd);
        assert("_D6module4funcAFZv" == resGNU1);
        assert(7 == symBeg);
        assert(25 == symEnd);

        enum bufGNU2 = "/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main-0x78) [0x00000000]";
        auto resGNU2 = getMangledSymbolName(bufGNU2, symBeg, symEnd);
        assert("__libc_start_main" == resGNU2);
        assert(32 == symBeg);
        assert(49 == symEnd);
    }
    else static if (BacktraceFmt.Solaris)
    {
        enum bufSolaris = "object'symbol+offset [pc]";
        auto resSolaris = getMangledSymbolName(bufSolaris, symBeg, symEnd);
        assert("symbol" == resSolaris);
        assert(7 == symBeg);
        assert(13 == symEnd);
    }
    else
        assert(!__traits(compiles, getMangledSymbolName));
}
