/*
 * Copyright (C) 2020-2021 Aloysius Indrayanto
 *
 * This file is part of the eCxx library, see LICENSE file for the license details.
 */

/*
 * ----------------------------------------------------------------------------------
 * The code in this file is copied (with a few modifications) from:
 *     core_esp8266_postmortem.cpp - output of debug info on sketch crash
 *     gdb_hooks.cpp               - default (no-op) hooks for GDB stub library
 * ----------------------------------------------------------------------------------
 * Copyright (C) 2015-2018 Ivan Grokhotkov. All rights reserved.
 *
 * This file is part of the esp8266 core for Arduino environment.
 *
 * This library is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any later
 * version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License along
 * with this library; if not, write to the Free Software Foundation, Inc., 51
 * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 * ----------------------------------------------------------------------------------
 */


#include <user_interface.h>

#include "CoreSys.h"
#include "StackThunk.h"


extern "C" {


//
// Function prototypes with "C" linkage
//
extern void __real_system_restart_local() __ECXX_ATTR_NORETURN;


//
// Forward declaration of functions with "C" linkage
//
extern void __wrap_system_restart_local();

extern void __unhandled_exception(const char* str) __ECXX_ATTR_NORETURN;
extern void __assert_func(const char* file, int line, const char* func, const char* what) __ECXX_ATTR_NORETURN;
extern void __panic_func(const char* file, int line, const char* func) __ECXX_ATTR_NORETURN;


//
// External variables from "SysHeap.cpp"
//
extern void*         umm_last_fail_alloc_addr;
extern eCxx::int32_t umm_last_fail_alloc_size;

#ifdef DEBUG_ESP_OOM
extern const char*   umm_last_fail_alloc_file;
extern eCxx::int32_t umm_last_fail_alloc_line;
#endif


//
// Debugging information
//
static const char*   panicFile          = nullptr;
static eCxx::int32_t panicLine          = 0;
static const char*   panicFunc          = nullptr;
static const char*   panicWhat          = nullptr;

static bool          abortCalled        = false;
static const char*   unhandledException = nullptr;

static uint32_t      stacksmashAddress  = 0;


// Using numbers different from "REASON_*" in "user_interface.h"
constexpr eCxx::int32_t REASON_USER_STACK_SMASH     = 253;
constexpr eCxx::int32_t REASON_USER_SWEXCEPTION_RST = 254;

static eCxx::int32_t userResetReason = REASON_DEFAULT_RST;


//
// Helper functions
//
static inline void uart0WriteChar(char c)
{
    while( ( (USS(HW_UART_0) >> USTXC) & 0xFF ) >= 0x7F ) {}

    if(c == '\n') USF(HW_UART_0) = '\r';
    USF(HW_UART_0) = c;
}


static inline void uart1WriteChar(char c)
{
    while( ( (USS(HW_UART_1) >> USTXC) & 0xFF ) >= 0x7E ) {}

    if(c == '\n') USF(HW_UART_1) = '\r';
    USF(HW_UART_1) = c;
}


static inline void uartWriteChar(char c)
{
    uart0WriteChar(c);
    uart1WriteChar(c);
}


static void printfP(const char *fmt, ...)
{
    char destStr[256];

    va_list argPtr;

    va_start(argPtr, fmt);
    ::vsnprintf_P(destStr, sizeof(destStr), fmt, argPtr);
    va_end(argPtr);

    const char *c = destStr;
    while(*c) ets_uart_putc1(*c++);
}


static void printCutHere()
{
    ets_putc('\n');
    ets_putc('\n');

    for(eCxx::uint32_t i = 0; i < 15; ++i) ets_putc('-');

    printfP( PGM_STR(" CUT HERE FOR EXCEPTION DECODER ").p );

    for(eCxx::uint32_t i = 0; i < 15; ++i) ets_putc('-');

    ets_putc('\n');
}


static void printStack(eCxx::uint32_t start, uint32_t end)
{
    for(eCxx::uint32_t pos = start; pos < end; pos += 0x10) {
        const eCxx::uint32_t* values = reinterpret_cast<eCxx::uint32_t*>(pos);
        const bool            isSF   = (values[2] == pos + 0x10); // Stack frames usually have SP saved as the second word
        printfP(
            PGM_STR("%08x : %08x %08x %08x %08x %c\n").p,
            pos,
            values[0],
            values[1],
            values[2],
            values[3],
            isSF ? '<' : ' '
        );
    }
}


static void raiseException() __ECXX_ATTR_NORETURN;
static void raiseException()
{
    if( gdb_present() ) __asm__ __volatile__ ( "syscall \n\t" ); // Triggers GDB when enabled

    userResetReason = REASON_USER_SWEXCEPTION_RST;
    printfP( PGM_STR("\nUser exception (panic/abort/assert)").p );

    __wrap_system_restart_local();

    for(;;); // Prevents warning from __ECXX_ATTR_NORETURN
}


static void customCrashCallback(rst_info* rstInfo, eCxx::uint32_t stack, eCxx::uint32_t stackEnd)
{
    eCxx::ignoreUnused(rstInfo );
    eCxx::ignoreUnused(stack   );
    eCxx::ignoreUnused(stackEnd);
}


//
// Debugging functions
//
extern void abort()
{
    abortCalled = true;

    raiseException();
}


extern void __unhandled_exception(const char* str)
{
    unhandledException = str;

    raiseException();
}


extern void __assert_func(const char* file, int line, const char* func, const char* what)
{
    panicFile = file;
    panicLine = line;
    panicFunc = func;
    panicWhat = what;

    gdb_do_break();
    raiseException();
}


extern void __panic_func(const char* file, int line, const char* func)
{
    panicFile = file;
    panicLine = line;
    panicFunc = func;
    panicWhat = nullptr;

    gdb_do_break();
    raiseException();
}


::uintptr_t __stack_chk_guard = 0x08675309 ^ RANDOM_REG32;

extern void __stack_chk_fail() __ECXX_ATTR_NORETURN;
extern void __stack_chk_fail()
{
    userResetReason = REASON_USER_STACK_SMASH;
    printfP( PGM_STR("\nPANIC: Stack overrun").p );

    stacksmashAddress = reinterpret_cast<uint32_t>( __builtin_return_address(0) );

    if( gdb_present() ) __asm__ __volatile__ ( "syscall \n\t" ); // Triggers GDB when enabled

    __wrap_system_restart_local();

    for(;;); // This function does not return
}


extern void __wrap_system_restart_local()
{
    register eCxx::uint32_t sp asm("a1");
             eCxx::uint32_t spDump = sp;

    rst_info rstInfo;
    memset(&rstInfo, 0, sizeof(rstInfo));

    if(userResetReason == REASON_DEFAULT_RST) {
        system_rtc_mem_read( 0, &rstInfo, sizeof(rstInfo) );
        if(rstInfo.reason != REASON_SOFT_WDT_RST  &&
           rstInfo.reason != REASON_EXCEPTION_RST &&
           rstInfo.reason != REASON_WDT_RST
        ) {
            rstInfo.reason = REASON_DEFAULT_RST;
        }
    }
    else {
        rstInfo.reason = userResetReason;
    }

    ets_install_putc1(&uartWriteChar);

    printCutHere();

    if(panicLine) {
        printfP( PGM_STR("\nPanic %s:%d %s").p, panicFile, panicLine, panicFunc );
        if(panicWhat) printfP( PGM_STR(": Assertion '%s' failed").p, panicWhat );
        ets_putc('\n');
    }
    else if(unhandledException) {
        printfP( PGM_STR("\nUnhandled C++ exception: %s\n").p, unhandledException );
    }
    else if(abortCalled) {
        printfP( PGM_STR("\nAbort called\n").p );
    }
    else if(rstInfo.reason == REASON_EXCEPTION_RST) {
        // On division by zero, the GCC divide routine in ROM jumps to the address below and
        // executes ILL (00 00 00); in this case, print the exception as 6 (IntegerDivZero)
        const bool divByZero = (rstInfo.exccause == 0) && (rstInfo.epc1 == 0x4000DCE5);
        printfP(
            PGM_STR("\nException (%d):\nepc1=0x%08x epc2=0x%08x epc3=0x%08x excvaddr=0x%08x depc=0x%08x\n").p,
            divByZero ? 6 : rstInfo.exccause, rstInfo.epc1, rstInfo.epc2, rstInfo.epc3, rstInfo.excvaddr, rstInfo.depc
        );
    }
    else if(rstInfo.reason == REASON_SOFT_WDT_RST) {
        printfP( PGM_STR("\nSoft WDT reset\n").p );
    }
    else if(rstInfo.reason == REASON_USER_STACK_SMASH) {
        printfP( PGM_STR("\nStack overflow detected\n").p );
        printfP( PGM_STR("\nException (%d):\nepc1=0x%08x epc2=0x%08x epc3=0x%08x excvaddr=0x%08x depc=0x%08x\n").p,
            5 /* Alloca exception (the closest thing to stack fault) */, stacksmashAddress, 0, 0, 0, 0
        );
    }
    else {
        printfP( PGM_STR("\nGeneric reset\n").p );
    }

    // Amount of stack taken by interrupt or exception handler and everything up to
    // __wrap_system_restart_local)_; determined empirically, it might break
    eCxx::uint32_t offset = 0;

         if(rstInfo.reason == REASON_SOFT_WDT_RST ) offset = 0x01A0;
    else if(rstInfo.reason == REASON_EXCEPTION_RST) offset = 0x0190;
    else if(rstInfo.reason == REASON_WDT_RST      ) offset = 0x0010;

    eCxx::uint32_t contStackStart = reinterpret_cast<eCxx::uint32_t>(g_ContData->stack   );
    eCxx::uint32_t contStackEnd   = reinterpret_cast<eCxx::uint32_t>(g_ContData->stackEnd);
    eCxx::uint32_t stackEnd;

    if( spDump > stack_thunk_get_stack_bot() && spDump <= stack_thunk_get_stack_top() ) {
        // Dump the BearSSL second stack and then reset SP back to the main cont stack
        printfP(
            PGM_STR("\nctx: stack_thunk\nsp: %08x end: %08x offset: %04x\n").p,
                spDump, stack_thunk_get_stack_top(), offset
        );
        printfP( PGM_STR("\n>>>stack>>>\n").p );
        printStack( spDump + offset, stack_thunk_get_stack_top() );
        printfP( PGM_STR("<<<stack<<<\n").p );
        // No offset needed anymore, the exception info was stored in the BearSSL stack
        offset = 0;
        spDump = stack_thunk_get_cont_sp();
    }

    if(spDump > contStackStart && spDump < contStackEnd) {
        printfP( PGM_STR("\nctx: cont\n").p );
        stackEnd = contStackEnd;
    }
    else {
        printfP( PGM_STR("\nctx: sys\n").p );
        stackEnd = 0x3FFFFFF0;
    }
    printfP( PGM_STR("sp: %08x end: %08x offset: %04x\n").p, spDump, stackEnd, offset );

    printfP( PGM_STR("\n>>>stack>>>\n").p );
    printStack(spDump + offset, stackEnd);
    printfP( PGM_STR("<<<stack<<<\n").p );

    // Use capital 0X formatting to ensure the standard EspExceptionDecoder does not match the address
    if(umm_last_fail_alloc_addr) {
#ifdef DEBUG_ESP_OOM
        printfP( PGM_STR("\nlast failed alloc call: %08X(%d)@%s:%d\n").p,
            reinterpret_cast<eCxx::uint32_t>(umm_last_fail_alloc_addr), umm_last_fail_alloc_size,
            umm_last_fail_alloc_file, umm_last_fail_alloc_line
        );
#else
        printfP(
            PGM_STR("\nlast failed alloc call: %08X(%d)\n").p,
            reinterpret_cast<eCxx::uint32_t>(umm_last_fail_alloc_addr), umm_last_fail_alloc_size
        );
#endif
    }

    printCutHere();

    if(unhandledException && umm_last_fail_alloc_addr) {
        // Outside from the cut-her" zone, print the correct caller address so that idf-monitor.py
        // will be able to decode and show exact location in the source code
        printfP(
            PGM_STR("\nlast failed alloc caller: 0x%08x\n").p,
            reinterpret_cast<eCxx::uint32_t>(umm_last_fail_alloc_addr)
        );
    }

    customCrashCallback(&rstInfo, spDump + offset, stackEnd);

    ets_delay_us(10000);
    __real_system_restart_local();
}


//
// GDB hooks stub
//
#if __GNUC__ >= 8

extern void ICACHE_RAM_ATTR gdb_init                     (                                    ) {               }
extern void ICACHE_RAM_ATTR gdb_do_break                 (                                    ) {               }
extern bool ICACHE_RAM_ATTR gdb_present                  (                                    ) { return false; }

extern bool ICACHE_RAM_ATTR gdbstub_has_putc1_control    (                                    ) { return false; }
extern void ICACHE_RAM_ATTR gdbstub_set_putc1_callback   (void (*)(char)                      ) {               }
extern bool ICACHE_RAM_ATTR gdbstub_has_uart_isr_control (                                    ) { return false; }
extern void ICACHE_RAM_ATTR gdbstub_set_uart_isr_callback(void (*)(void*,eCxx::uint8_t), void*) {               }
extern void ICACHE_RAM_ATTR gdbstub_write_char           (char                                ) {               }
extern void ICACHE_RAM_ATTR gdbstub_write                (const char*, size_t                 ) {               }

#else

static bool ICACHE_RAM_ATTR __gdb_no_op()
{ return false; }

extern void gdb_init                     (                                    ) __ECXX_ATTR_ALIAS("__gdb_no_op");
extern void gdb_do_break                 (                                    ) __ECXX_ATTR_ALIAS("__gdb_no_op");
extern bool gdb_present                  (                                    ) __ECXX_ATTR_ALIAS("__gdb_no_op");

extern bool gdbstub_has_putc1_control    (                                    ) __ECXX_ATTR_ALIAS("__gdb_no_op");
extern void gdbstub_set_putc1_callback   (void (*)(char)                      ) __ECXX_ATTR_ALIAS("__gdb_no_op");
extern bool gdbstub_has_uart_isr_control (                                    ) __ECXX_ATTR_ALIAS("__gdb_no_op");
extern void gdbstub_set_uart_isr_callback(void (*)(void*,eCxx::uint8_t), void*) __ECXX_ATTR_ALIAS("__gdb_no_op");
extern void gdbstub_write_char           (char                                ) __ECXX_ATTR_ALIAS("__gdb_no_op");
extern void gdbstub_write                (const char*, size_t                 ) __ECXX_ATTR_ALIAS("__gdb_no_op");

#endif


} // extern "C"
