/****************************************************************************
 * sched/misc/assert.c
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

#include <nuttx/arch.h>
#include <nuttx/board.h>
#include <nuttx/coredump.h>
#include <nuttx/fs/fs.h>
#include <nuttx/init.h>
#include <nuttx/irq.h>
#include <nuttx/tls.h>
#include <nuttx/signal.h>
#ifdef CONFIG_ARCH_LEDS
#  include <arch/board/board.h>
#endif
#include <nuttx/panic_notifier.h>
#include <nuttx/reboot_notifier.h>
#include <nuttx/syslog/syslog.h>
#include <nuttx/usb/usbdev_trace.h>

#include <assert.h>
#include <debug.h>
#include <stdio.h>
#include <stdint.h>
#include <sys/utsname.h>

#include "irq/irq.h"
#include "sched/sched.h"
#include "group/group.h"

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

#define DEADLOCK_MAX 8

#ifndef CONFIG_BOARD_RESET_ON_ASSERT
#  define CONFIG_BOARD_RESET_ON_ASSERT 0
#endif

/* Check if an interrupt stack size is configured */

#ifndef CONFIG_ARCH_INTERRUPTSTACK
#  define CONFIG_ARCH_INTERRUPTSTACK 0
#endif

/* USB trace dumping */

#ifndef CONFIG_USBDEV_TRACE
#  undef CONFIG_ARCH_USBDUMP
#endif

#define DUMP_PTR(p, x) ((uintptr_t)(&(p)[(x)]) < stack_top ? (p)[(x)] : 0)
#define DUMP_STRIDE    (sizeof(FAR void *) * 8)

#if UINTPTR_MAX <= UINT32_MAX
#  define DUMP_FORMAT " %08" PRIxPTR ""
#elif UINTPTR_MAX <= UINT64_MAX
#  define DUMP_FORMAT " %016" PRIxPTR ""
#endif

/* Architecture can overwrite the default XCPTCONTEXT alignment */

#ifndef XCPTCONTEXT_ALIGN
#  define XCPTCONTEXT_ALIGN 16
#endif

/****************************************************************************
 * Private Data
 ****************************************************************************/

static uintptr_t
g_last_regs[XCPTCONTEXT_REGS] aligned_data(XCPTCONTEXT_ALIGN);
static FAR const char * const g_policy[4] =
{
  "FIFO", "RR", "SPORADIC"
};

static FAR const char * const g_ttypenames[4] =
{
  "Task",
  "pthread",
  "Kthread",
  "Invalid"
};

static bool g_fatal_assert = false;

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name: assert_tracecallback
 ****************************************************************************/

#ifdef CONFIG_ARCH_USBDUMP
static int usbtrace_syslog(FAR const char *fmt, ...)
{
  va_list ap;

  /* Let vsyslog do the real work */

  va_start(ap, fmt);
  vsyslog(LOG_EMERG, fmt, ap);
  va_end(ap);
  return OK;
}

static int assert_tracecallback(FAR struct usbtrace_s *trace, FAR void *arg)
{
  usbtrace_trprintf(usbtrace_syslog, trace->event, trace->value);
  return 0;
}
#endif

#ifdef CONFIG_ARCH_STACKDUMP

/****************************************************************************
 * Name: stack_dump
 ****************************************************************************/

static void stack_dump(uintptr_t sp, uintptr_t stack_top)
{
  uintptr_t stack;

  for (stack = sp; stack <= stack_top; stack += DUMP_STRIDE)
    {
      FAR uintptr_t *ptr = (FAR uintptr_t *)stack;

      _alert("%p:"DUMP_FORMAT DUMP_FORMAT DUMP_FORMAT DUMP_FORMAT
             DUMP_FORMAT DUMP_FORMAT DUMP_FORMAT DUMP_FORMAT "\n",
             (FAR void *)stack, DUMP_PTR(ptr, 0), DUMP_PTR(ptr , 1),
             DUMP_PTR(ptr, 2), DUMP_PTR(ptr, 3), DUMP_PTR(ptr, 4),
             DUMP_PTR(ptr, 5), DUMP_PTR(ptr , 6), DUMP_PTR(ptr, 7));
    }
}

/****************************************************************************
 * Name: dump_stack
 ****************************************************************************/

static void dump_stack(FAR const char *tag, uintptr_t sp,
                       uintptr_t base, size_t size, size_t used)
{
  uintptr_t top = base + size;

  _alert("%s Stack:\n", tag);
  _alert("  base: %p\n", (FAR void *)base);
  _alert("  size: %08zu\n", size);

  if (sp != 0)
    {
      _alert("    sp: %p\n", (FAR void *)sp);

      /* Get more information */

      if (sp - DUMP_STRIDE >= base)
        {
          sp -= DUMP_STRIDE;
        }

      stack_dump(sp, top);
    }
  else
    {
#ifdef CONFIG_STACK_COLORATION
      size_t remain = size - used;

      base += remain;
      size -= remain;
#endif

#if CONFIG_ARCH_STACKDUMP_MAX_LENGTH > 0
      if (size > CONFIG_ARCH_STACKDUMP_MAX_LENGTH)
        {
          size = CONFIG_ARCH_STACKDUMP_MAX_LENGTH;
        }
#endif

      stack_dump(base, base + size);
    }
}

/****************************************************************************
 * Name: dump_stacks
 ****************************************************************************/

static void dump_stacks(FAR struct tcb_s *rtcb, uintptr_t sp)
{
#if CONFIG_ARCH_INTERRUPTSTACK > 0
  uintptr_t intstack_base = up_get_intstackbase(this_cpu());
  size_t intstack_size = CONFIG_ARCH_INTERRUPTSTACK;
  uintptr_t intstack_top = intstack_base + intstack_size;
  uintptr_t intstack_sp = 0;
#endif
#ifdef CONFIG_ARCH_KERNEL_STACK
  uintptr_t kernelstack_base = (uintptr_t)rtcb->xcp.kstack;
  size_t kernelstack_size = CONFIG_ARCH_KERNEL_STACKSIZE;
  uintptr_t kernelstack_top = kernelstack_base + kernelstack_size;
  uintptr_t kernelstack_sp = 0;
#endif
  uintptr_t tcbstack_base = (uintptr_t)rtcb->stack_base_ptr;
  size_t tcbstack_size = (size_t)rtcb->adj_stack_size;
  uintptr_t tcbstack_top = tcbstack_base + tcbstack_size;
  uintptr_t tcbstack_sp = 0;
  bool force = false;

#if CONFIG_ARCH_INTERRUPTSTACK > 0
  if (sp >= intstack_base && sp < intstack_top)
    {
      intstack_sp = sp;
    }
  else
#endif
#ifdef CONFIG_ARCH_KERNEL_STACK
  if (sp >= kernelstack_base && sp < kernelstack_top)
    {
      kernelstack_sp = sp;
    }
  else
#endif
  if (sp >= tcbstack_base && sp < tcbstack_top)
    {
      tcbstack_sp = sp;
    }
  else
    {
      force = true;
      _alert("ERROR: Stack pointer is not within the stack\n");
    }

#if CONFIG_ARCH_INTERRUPTSTACK > 0
  if (intstack_sp != 0 || force)
    {
      dump_stack("IRQ",
                 intstack_sp,
                 intstack_base,
                 intstack_size,
#ifdef CONFIG_STACK_COLORATION
                 up_check_intstack(this_cpu())
#else
                 0
#endif
                 );

      tcbstack_sp = up_getusrsp((FAR void *)up_current_regs());
      if (tcbstack_sp < tcbstack_base || tcbstack_sp >= tcbstack_top)
        {
          tcbstack_sp = 0;
          force = true;
        }
    }
#endif

#ifdef CONFIG_ARCH_KERNEL_STACK
  if (kernelstack_sp != 0 || force)
    {
      dump_stack("Kernel",
                 kernelstack_sp,
                 kernelstack_base,
                 kernelstack_size,
                 0
                );
    }
#endif

  if (tcbstack_sp != 0 || force)
    {
      dump_stack("User",
                 tcbstack_sp,
                 tcbstack_base,
                 tcbstack_size,
#ifdef CONFIG_STACK_COLORATION
                 up_check_tcbstack(rtcb)
#else
                 0
#endif
                 );
    }
}

#endif

/****************************************************************************
 * Name: dump_task
 ****************************************************************************/

static void dump_task(FAR struct tcb_s *tcb, FAR void *arg)
{
  char args[64] = "";
  char state[32];
  FAR char *s;
#ifdef CONFIG_STACK_COLORATION
  size_t stack_filled = 0;
  size_t stack_used;
#endif
#ifndef CONFIG_SCHED_CPULOAD_NONE
  struct cpuload_s cpuload;
  size_t fracpart = 0;
  size_t intpart = 0;
  size_t tmp;

  clock_cpuload(tcb->pid, &cpuload);

  if (cpuload.total > 0)
    {
      tmp      = (1000 * cpuload.active) / cpuload.total;
      intpart  = tmp / 10;
      fracpart = tmp - 10 * intpart;
    }
#endif

#ifdef CONFIG_STACK_COLORATION
  stack_used = up_check_tcbstack(tcb);
  if (tcb->adj_stack_size > 0 && stack_used > 0)
    {
      /* Use fixed-point math with one decimal place */

      stack_filled = 10 * 100 * stack_used / tcb->adj_stack_size;
    }
#endif

  /* Stringify the argument vector */

  group_argvstr(tcb, args, sizeof(args));

  /* get the task_state */

  nxsched_get_stateinfo(tcb, state, sizeof(state));
  if ((s = strchr(state, ',')) != NULL)
    {
      *s = ' ';
    }

  /* Dump interesting properties of this task */

  _alert("   %4d %5d"
#ifdef CONFIG_SMP
         "  %4d"
#endif
         " %3d %-8s %-7s %c"
         " %-18s"
         " " SIGSET_FMT
         " %p"
         "   %7zu"
#ifdef CONFIG_STACK_COLORATION
         "   %7zu   %3zu.%1zu%%%c"
#endif
#ifndef CONFIG_SCHED_CPULOAD_NONE
         "   %3zu.%01zu%%"
#endif
         "   %s%s\n"
         , tcb->pid
         , tcb->group ? tcb->group->tg_pid : -1
#ifdef CONFIG_SMP
         , tcb->cpu
#endif
         , tcb->sched_priority
         , g_policy[(tcb->flags & TCB_FLAG_POLICY_MASK) >>
                    TCB_FLAG_POLICY_SHIFT]
         , g_ttypenames[(tcb->flags & TCB_FLAG_TTYPE_MASK)
                        >> TCB_FLAG_TTYPE_SHIFT]
         , tcb->flags & TCB_FLAG_EXIT_PROCESSING ? 'P' : '-'
         , state
         , SIGSET_ELEM(&tcb->sigprocmask)
         , tcb->stack_base_ptr
         , tcb->adj_stack_size
#ifdef CONFIG_STACK_COLORATION
         , up_check_tcbstack(tcb)
         , stack_filled / 10, stack_filled % 10
         , (stack_filled >= 10 * 80 ? '!' : ' ')
#endif
#ifndef CONFIG_SCHED_CPULOAD_NONE
         , intpart, fracpart
#endif
#if CONFIG_TASK_NAME_SIZE > 0
         , tcb->name
#else
         , "<noname>"
#endif
         , args
        );
}

/****************************************************************************
 * Name: dump_backtrace
 ****************************************************************************/

#ifdef CONFIG_SCHED_BACKTRACE
static void dump_backtrace(FAR struct tcb_s *tcb, FAR void *arg)
{
  sched_dumpstack(tcb->pid);
}
#endif

/****************************************************************************
 * Name: dump_filelist
 ****************************************************************************/

#ifdef CONFIG_DUMP_ON_EXIT
static void dump_filelist(FAR struct tcb_s *tcb, FAR void *arg)
{
  FAR struct filelist *filelist = &tcb->group->tg_filelist;
  files_dumplist(filelist);
}
#endif

/****************************************************************************
 * Name: dump_tasks
 ****************************************************************************/

static void dump_tasks(void)
{
#if CONFIG_ARCH_INTERRUPTSTACK > 0
  int cpu;
#endif

  /* Dump interesting properties of each task in the crash environment */

  _alert("   PID GROUP"
#ifdef CONFIG_SMP
         "   CPU"
#endif
         " PRI POLICY   TYPE    NPX"
         " STATE   EVENT"
         "      SIGMASK        "
         "  STACKBASE"
         "  STACKSIZE"
#ifdef CONFIG_STACK_COLORATION
         "      USED   FILLED "
#endif
#ifndef CONFIG_SCHED_CPULOAD_NONE
         "      CPU"
#endif
         "   COMMAND\n");

#if CONFIG_ARCH_INTERRUPTSTACK > 0
  for (cpu = 0; cpu < CONFIG_SMP_NCPUS; cpu++)
    {
#  ifdef CONFIG_STACK_COLORATION
      size_t stack_used = up_check_intstack(cpu);
      size_t stack_filled = 0;

      if (stack_used > 0)
        {
          /* Use fixed-point math with one decimal place */

          stack_filled = 10 * 100 *
                         stack_used / CONFIG_ARCH_INTERRUPTSTACK;
        }
#  endif

      _alert("  ----   ---"
#  ifdef CONFIG_SMP
             "  %4d"
#  endif
             " --- --------"
             " ------- ---"
             " ------- ----------"
             " ----------------"
             " %p"
             "   %7u"
#  ifdef CONFIG_STACK_COLORATION
             "   %7zu   %3zu.%1zu%%%c"
#  endif
#  ifndef CONFIG_SCHED_CPULOAD_NONE
             "     ----"
#  endif
             "   irq\n"
#ifdef CONFIG_SMP
             , cpu
#endif
             , (FAR void *)up_get_intstackbase(cpu)
             , CONFIG_ARCH_INTERRUPTSTACK
#  ifdef CONFIG_STACK_COLORATION
             , stack_used
             , stack_filled / 10, stack_filled % 10,
             (stack_filled >= 10 * 80 ? '!' : ' ')
#  endif
            );
    }
#endif

  nxsched_foreach(dump_task, NULL);

#ifdef CONFIG_SCHED_BACKTRACE
  nxsched_foreach(dump_backtrace, NULL);
#endif

#ifdef CONFIG_DUMP_ON_EXIT
  nxsched_foreach(dump_filelist, NULL);
#endif
}

/****************************************************************************
 * Name: dump_deadlock
 ****************************************************************************/

#ifdef CONFIG_ARCH_DEADLOCKDUMP
static void dump_deadlock(void)
{
  pid_t deadlock[DEADLOCK_MAX];
  size_t i = nxsched_collect_deadlock(deadlock, DEADLOCK_MAX);

  if (i > 0)
    {
      _alert("Deadlock detected\n");
      while (i-- > 0)
        {
#ifdef CONFIG_SCHED_BACKTRACE
          sched_dumpstack(deadlock[i]);
#else
          _alert("deadlock pid: %d\n", deadlock[i]);
#endif
        }
    }
}
#endif

/****************************************************************************
 * Name: pause_all_cpu
 ****************************************************************************/

#ifdef CONFIG_SMP
static void pause_all_cpu(void)
{
  int cpu;

  for (cpu = 0; cpu < CONFIG_SMP_NCPUS; cpu++)
    {
      if (cpu != this_cpu())
        {
          up_cpu_pause(cpu);
        }
    }
}
#endif

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: _assert
 ****************************************************************************/

void _assert(FAR const char *filename, int linenum,
             FAR const char *msg, FAR void *regs)
{
  const bool os_ready = OSINIT_OS_READY();
  FAR struct tcb_s *rtcb = running_task();
#if CONFIG_TASK_NAME_SIZE > 0
  FAR struct tcb_s *ptcb = NULL;
#endif
  struct panic_notifier_s notifier_data;
  struct utsname name;
  irqstate_t flags;
  bool fatal = true;

#if CONFIG_TASK_NAME_SIZE > 0
  if (rtcb->group && !(rtcb->flags & TCB_FLAG_TTYPE_KERNEL))
    {
      ptcb = nxsched_get_tcb(rtcb->group->tg_pid);
    }
#endif

  flags = 0; /* suppress GCC warning */
  if (os_ready)
    {
      flags = enter_critical_section();
    }

  if (g_fatal_assert)
    {
      goto reset;
    }

  if (os_ready && fatal)
    {
#ifdef CONFIG_SMP
      pause_all_cpu();
#endif
    }

  /* try to save current context if regs is null */

  if (regs == NULL)
    {
      up_saveusercontext(g_last_regs);
      regs = g_last_regs;
    }
  else
    {
      memcpy(g_last_regs, regs, sizeof(g_last_regs));
    }

#if CONFIG_BOARD_RESET_ON_ASSERT < 2
  if (!up_interrupt_context() &&
      (rtcb->flags & TCB_FLAG_TTYPE_MASK) != TCB_FLAG_TTYPE_KERNEL)
    {
      fatal = false;
    }
  else
#endif
    {
      g_fatal_assert = true;
    }

  notifier_data.rtcb = rtcb;
  notifier_data.regs = regs;
  notifier_data.filename = filename;
  notifier_data.linenum = linenum;
  notifier_data.msg = msg;
  panic_notifier_call_chain(fatal ? PANIC_KERNEL : PANIC_TASK,
                            &notifier_data);
#ifdef CONFIG_ARCH_LEDS
  board_autoled_on(LED_ASSERTION);
#endif

  /* Flush any buffered SYSLOG data (from prior to the assertion) */

  syslog_flush();

  uname(&name);
  _alert("Current Version: %s %s %s %s %s\n",
         name.sysname, name.nodename,
         name.release, name.version, name.machine);

  _alert("Assertion failed %s: at file: %s:%d task"
#ifdef CONFIG_SMP
         "(CPU%d)"
#endif
         ": "
#if CONFIG_TASK_NAME_SIZE > 0
         "%s "
         "process: %s "
#endif
         "%p\n",
         msg ? msg : "",
         filename ? filename : "", linenum,
#ifdef CONFIG_SMP
         this_cpu(),
#endif
#if CONFIG_TASK_NAME_SIZE > 0
         rtcb->name,
         ptcb ? ptcb->name : "Kernel",
#endif
         rtcb->entry.main);

  /* Register dump */

  up_dump_register(regs);

#ifdef CONFIG_ARCH_STACKDUMP
  dump_stacks(rtcb, up_getusrsp(regs));
#endif

  /* Show back trace */

#ifdef CONFIG_SCHED_BACKTRACE
  sched_dumpstack(rtcb->pid);
#endif

  /* Flush any buffered SYSLOG data */

  syslog_flush();

  if (fatal)
    {
      dump_tasks();

#ifdef CONFIG_ARCH_DEADLOCKDUMP
      /* Deadlock Dump */

      dump_deadlock();
#endif

#ifdef CONFIG_ARCH_USBDUMP
      /* Dump USB trace data */

      usbtrace_enumerate(assert_tracecallback, NULL);
#endif

#ifdef CONFIG_BOARD_CRASHDUMP
      board_crashdump(up_getsp(), rtcb, filename, linenum, msg, regs);
#endif

#if defined(CONFIG_BOARD_COREDUMP_SYSLOG) || \
    defined(CONFIG_BOARD_COREDUMP_BLKDEV)
      /* Dump core information */

#  ifdef CONFIG_BOARD_COREDUMP_FULL
      coredump_dump(INVALID_PROCESS_ID);
#  else
      coredump_dump(rtcb->pid);
#  endif
#endif

      /* Flush any buffered SYSLOG data */

      syslog_flush();
      panic_notifier_call_chain(PANIC_KERNEL_FINAL, &notifier_data);

      reboot_notifier_call_chain(SYS_HALT, NULL);

reset:
#if CONFIG_BOARD_RESET_ON_ASSERT >= 1
      board_reset(CONFIG_BOARD_ASSERT_RESET_VALUE);
#else
      for (; ; )
        {
#ifdef CONFIG_ARCH_LEDS
          /* FLASH LEDs a 2Hz */

          board_autoled_on(LED_PANIC);
          up_mdelay(250);
          board_autoled_off(LED_PANIC);
#endif
          up_mdelay(250);
        }
#endif
    }

  if (os_ready)
    {
      leave_critical_section(flags);
    }
}