/****************************************************************************
 * apps/system/trace/trace.c
 *
 * 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <sys/ioctl.h>
#include <nuttx/note/notectl_driver.h>

#include "trace.h"

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

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

/****************************************************************************
 * Name: notectl_enable
 ****************************************************************************/

static bool notectl_enable(int flag, int notectlfd)
{
  struct note_filter_mode_s mode;
  int oldflag;

  ioctl(notectlfd, NOTECTL_GETMODE, (unsigned long)&mode);

  oldflag = (mode.flag & NOTE_FILTER_MODE_FLAG_ENABLE) != 0;
  if (flag == oldflag)
    {
      /* Already set */

      return false;
    }

  if (flag)
    {
      mode.flag |= NOTE_FILTER_MODE_FLAG_ENABLE;
    }
  else
    {
      mode.flag &= ~NOTE_FILTER_MODE_FLAG_ENABLE;
    }

  ioctl(notectlfd, NOTECTL_SETMODE, (unsigned long)&mode);

  return true;
}

/****************************************************************************
 * Name: trace_cmd_start
 ****************************************************************************/

static int trace_cmd_start(int index, int argc, FAR char **argv,
                           int notectlfd)
{
  FAR char *endptr;
  int duration = 0;
  bool cont = false;

  /* Usage: trace start [-c][<duration>] */

  if (index < argc)
    {
      if (strcmp(argv[index], "-c") == 0)
        {
          cont = true;
          index++;
        }
    }

  if (index < argc)
    {
      duration = strtoul(argv[index], &endptr, 0);
      if (!duration || endptr == argv[index] || *endptr != '\0')
        {
          fprintf(stderr,
                  "trace start: invalid argument '%s'\n", argv[index]);
          return ERROR;
        }

      index++;
    }

  /* Clear the trace buffer */

  if (!cont)
    {
      trace_dump_clear();
    }

  /* Start tracing */

  notectl_enable(true, notectlfd);

  if (duration > 0)
    {
      /* If <duration> is given, stop tracing after specified seconds. */

      sleep(duration);
      notectl_enable(false, notectlfd);
    }

  return index;
}

/****************************************************************************
 * Name: trace_cmd_dump
 ****************************************************************************/

#ifdef CONFIG_DRIVER_NOTERAM
static int trace_cmd_dump(int index, int argc, FAR char **argv,
                          int notectlfd)
{
  FAR FILE *out = stdout;
  int ret;
  bool changed = false;
  bool cont = false;

  /* Usage: trace dump [-c][<filename>] */

  if (index < argc)
    {
      if (strcmp(argv[index], "-c") == 0)
        {
          cont = true;
          index++;
        }
    }

  /* If <filename> is '-' or not given, trace dump is displayed
   * to stdout.
   */

  if (index < argc)
    {
      if (strcmp(argv[index], "-") != 0)
        {
          /* If <filename> is given, open the file stream for output. */

          out = fopen(argv[index], "w");
          if (out == NULL)
            {
              fprintf(stderr,
                      "trace dump: cannot open '%s'\n", argv[index]);
              return ERROR;
            }
        }

      index++;
    }

  /* Stop the tracing before dump */

  if (!cont)
    {
      changed = notectl_enable(false, notectlfd);
    }

  /* Dump the trace data */

  ret = trace_dump(out);

  if (changed)
    {
      notectl_enable(true, notectlfd);
    }

  /* If needed, close the file stream for dump. */

  if (out != stdout)
    {
      fclose(out);
    }

  if (ret < 0)
    {
      fprintf(stderr,
              "trace dump: dump failed\n");
      return ERROR;
    }

  return index;
}
#endif

/****************************************************************************
 * Name: trace_cmd_cmd
 ****************************************************************************/

#ifdef CONFIG_SYSTEM_SYSTEM
static int trace_cmd_cmd(int index, int argc, FAR char **argv, int notectlfd)
{
  char command[CONFIG_NSH_LINELEN];
  bool changed;
  bool cont = false;

  /* Usage: trace cmd [-c] <command> [<args>...] */

  if (index < argc)
    {
      if (strcmp(argv[index], "-c") == 0)
        {
          cont = true;
          index++;
        }
    }

  if (index >= argc)
    {
      /* <command> parameter is mandatory. */

      fprintf(stderr,
              "trace cmd: no argument\n");
      return ERROR;
    }

  memset(command, 0, sizeof(command));
  while (index < argc)
    {
      strcat(command, argv[index]);
      strcat(command, " ");
      index++;
    }

  /* Clear the trace buffer */

  if (!cont)
    {
      trace_dump_clear();
    }

  /* Execute the command with tracing */

  changed = notectl_enable(true, notectlfd);

  system(command);

  if (changed)
    {
      notectl_enable(false, notectlfd);
    }

  return index;
}
#endif

/****************************************************************************
 * Name: trace_cmd_mode
 ****************************************************************************/

static int trace_cmd_mode(int index, int argc, FAR char **argv,
                          int notectlfd)
{
  struct note_filter_mode_s mode;
  bool owmode;
  bool enable;
  bool modified = false;
#ifdef CONFIG_SCHED_INSTRUMENTATION_SYSCALL
  struct note_filter_syscall_s filter_syscall;
#endif
#ifdef CONFIG_SCHED_INSTRUMENTATION_IRQHANDLER
  struct note_filter_irq_s filter_irq;
#endif

#if defined(CONFIG_SCHED_INSTRUMENTATION_SYSCALL) ||\
    defined(CONFIG_SCHED_INSTRUMENTATION_IRQHANDLER)
  int i;
  int count;
#endif

  /* Usage: trace mode [{+|-}{o|s|a|i}...] */

  /* Get current trace mode */

  ioctl(notectlfd, NOTECTL_GETMODE, (unsigned long)&mode);
  owmode = trace_dump_get_overwrite();

  /* Parse the mode setting parameters */

  while (index < argc)
    {
      if (argv[index][0] != '-' && argv[index][0] != '+')
        {
          break;
        }

      enable = (argv[index][0] == '+');

      switch (argv[index][1])
        {
#ifdef CONFIG_DRIVER_NOTERAM
          case 'o':   /* Overwrite mode */
            owmode = enable;
            break;
#endif

#ifdef CONFIG_SCHED_INSTRUMENTATION_SWITCH
          case 'w':   /* Switch trace */
            if (enable)
              {
                mode.flag |= NOTE_FILTER_MODE_FLAG_SWITCH;
              }
            else
              {
                mode.flag &= ~NOTE_FILTER_MODE_FLAG_SWITCH;
              }
            break;
#endif

#ifdef CONFIG_SCHED_INSTRUMENTATION_SYSCALL
          case 's':   /* Syscall trace */
            if (enable)
              {
                mode.flag |= NOTE_FILTER_MODE_FLAG_SYSCALL;
              }
            else
              {
                mode.flag &= ~NOTE_FILTER_MODE_FLAG_SYSCALL;
              }
            break;

          case 'a':   /* Record syscall arguments */
            if (enable)
              {
                mode.flag |= NOTE_FILTER_MODE_FLAG_SYSCALL_ARGS;
              }
            else
              {
                mode.flag &= ~NOTE_FILTER_MODE_FLAG_SYSCALL_ARGS;
              }
            break;
#endif

#ifdef CONFIG_SCHED_INSTRUMENTATION_IRQHANDLER
          case 'i':   /* IRQ trace */
            if (enable)
              {
                mode.flag |= NOTE_FILTER_MODE_FLAG_IRQ;
              }
            else
              {
                mode.flag &= ~NOTE_FILTER_MODE_FLAG_IRQ;
              }
            break;
#endif

#ifdef CONFIG_SCHED_INSTRUMENTATION_DUMP
          case 'd':   /* Dump trace */
            if (enable)
              {
                mode.flag |= NOTE_FILTER_MODE_FLAG_DUMP;
              }
            else
              {
                mode.flag &= ~NOTE_FILTER_MODE_FLAG_DUMP;
              }
            break;
#endif

          default:
            fprintf(stderr,
                    "trace mode: invalid option '%s'\n", argv[index]);
            return ERROR;
        }

      index++;
      modified = true;
    }

  if (modified)
    {
      /* Update trace mode */

      ioctl(notectlfd, NOTECTL_SETMODE, (unsigned long)&mode);
      trace_dump_set_overwrite(owmode);

      return index;
    }

  /* If no parameter, display current trace mode setting. */

  printf("Task trace mode:\n");
  printf(" Trace                   : %s\n",
         (mode.flag & NOTE_FILTER_MODE_FLAG_ENABLE) ?
          "enabled" : "disabled");

#ifdef CONFIG_DRIVER_NOTERAM
  printf(" Overwrite               : %s\n",
         owmode ? "on  (+o)" : "off (-o)");
#endif

#ifdef CONFIG_SCHED_INSTRUMENTATION_SYSCALL
  ioctl(notectlfd, NOTECTL_GETSYSCALLFILTER,
        (unsigned long)&filter_syscall);
  for (count = i = 0; i < SYS_nsyscalls; i++)
    {
      if (NOTE_FILTER_SYSCALLMASK_ISSET(i, &filter_syscall))
        {
          count++;
        }
    }

  printf(" Syscall trace           : %s\n",
         mode.flag & NOTE_FILTER_MODE_FLAG_SYSCALL ?
          "on  (+s)" : "off (-s)");
  if (mode.flag & NOTE_FILTER_MODE_FLAG_SYSCALL)
    {
      printf("  Filtered Syscalls      : %d\n", count);
    }

  printf(" Syscall trace with args : %s\n",
         mode.flag & NOTE_FILTER_MODE_FLAG_SYSCALL_ARGS ?
          "on  (+a)" : "off (-a)");
#endif

#ifdef CONFIG_SCHED_INSTRUMENTATION_IRQHANDLER
  ioctl(notectlfd, NOTECTL_GETIRQFILTER, (unsigned long)&filter_irq);
  for (count = i = 0; i < NR_IRQS; i++)
    {
      if (NOTE_FILTER_IRQMASK_ISSET(i, &filter_irq))
        {
          count++;
        }
    }

  printf(" IRQ trace               : %s\n",
         mode.flag & NOTE_FILTER_MODE_FLAG_IRQ ?
          "on  (+i)" : "off (-i)");
  if (mode.flag & NOTE_FILTER_MODE_FLAG_IRQ)
    {
      printf("  Filtered IRQs          : %d\n", count);
    }
#endif

  return index;
}

/****************************************************************************
 * Name: trace_cmd_switch
 ****************************************************************************/

#ifdef CONFIG_SCHED_INSTRUMENTATION_SWITCH
static int trace_cmd_switch(int index, int argc, FAR char **argv,
                            int notectlfd)
{
  bool enable;
  struct note_filter_mode_s mode;

  /* Usage: trace switch [+|-] */

  /* Get current filter setting */

  ioctl(notectlfd, NOTECTL_GETMODE, (unsigned long)&mode);

  /* Parse the setting parameters */

  if (argv[index][0] == '-' || argv[index][0] == '+')
    {
      enable = (argv[index][0] == '+');
      if (enable ==
          ((mode.flag & NOTE_FILTER_MODE_FLAG_SWITCH) != 0))
        {
          /* Already set */

          return false;
        }

      if (enable)
        {
          mode.flag |= NOTE_FILTER_MODE_FLAG_SWITCH;
        }
      else
        {
          mode.flag &= ~NOTE_FILTER_MODE_FLAG_SWITCH;
        }

      ioctl(notectlfd, NOTECTL_SETMODE, (unsigned long)&mode);

      index++;
    }

  return index;
}
#endif

/****************************************************************************
 * Name: trace_cmd_syscall
 ****************************************************************************/

#ifdef CONFIG_SCHED_INSTRUMENTATION_SYSCALL
static int trace_cmd_syscall(int index, int argc, FAR char **argv,
                             int notectlfd)
{
  bool enable;
  bool modified = false;
  int syscallno;
  FAR struct note_filter_syscall_s filter_syscall;
  int n;
  int count;

  /* Usage: trace syscall [{+|-}<syscallname>...] */

  /* Get current syscall filter setting */

  ioctl(notectlfd, NOTECTL_GETSYSCALLFILTER,
        (unsigned long)&filter_syscall);

  /* Parse the setting parameters */

  while (index < argc)
    {
      if (argv[index][0] != '-' && argv[index][0] != '+')
        {
          break;
        }

      enable = (argv[index][0] == '+');

      /* Check whether the given pattern matches for each syscall names */

      for (syscallno = 0; syscallno < SYS_nsyscalls; syscallno++)
        {
          if (fnmatch(&argv[index][1], g_funcnames[syscallno], 0))
            {
              continue;
            }

          /* If matches, update the masked syscall number list */

          if (enable)
            {
              NOTE_FILTER_SYSCALLMASK_SET(syscallno, &filter_syscall);
            }
          else
            {
              NOTE_FILTER_SYSCALLMASK_CLR(syscallno, &filter_syscall);
            }
        }

      index++;
      modified = true;
    }

  if (modified)
    {
      /* Update current syscall filter setting */

      ioctl(notectlfd, NOTECTL_SETSYSCALLFILTER,
            (unsigned long)&filter_syscall);
    }
  else
    {
      /* If no parameter, display current setting. */

      for (count = n = 0; n < SYS_nsyscalls; n++)
        {
          if (NOTE_FILTER_SYSCALLMASK_ISSET(n, &filter_syscall))
            {
              count++;
            }
        }

      printf("Filtered Syscalls: %d\n", count);

      for (n = 0; n < SYS_nsyscalls; n++)
        {
          if (NOTE_FILTER_SYSCALLMASK_ISSET(n, &filter_syscall))
            {
              printf("  %s\n", g_funcnames[n]);
            }
        }
    }

  return index;
}
#endif

/****************************************************************************
 * Name: trace_cmd_irq
 ****************************************************************************/

#ifdef CONFIG_SCHED_INSTRUMENTATION_IRQHANDLER
static int trace_cmd_irq(int index, int argc, FAR char **argv, int notectlfd)
{
  bool enable;
  bool modified = false;
  int irqno;
  FAR char *endptr;
  struct note_filter_irq_s filter_irq;
  int n;
  int count;

  /* Usage: trace irq [{+|-}<irqnum>...] */

  /* Get current irq filter setting */

  ioctl(notectlfd, NOTECTL_GETIRQFILTER, (unsigned long)&filter_irq);

  /* Parse the setting parameters */

  while (index < argc)
    {
      if (argv[index][0] != '-' && argv[index][0] != '+')
        {
          break;
        }

      enable = (argv[index][0] == '+');

      if (argv[index][1] == '*')
        {
          /* Mask or unmask all IRQs */

          if (enable)
            {
              for (n = 0; n < NR_IRQS; n++)
                {
                  NOTE_FILTER_IRQMASK_SET(n, &filter_irq);
                }
            }
          else
            {
              NOTE_FILTER_IRQMASK_ZERO(&filter_irq);
            }
        }
      else
        {
          /* Get IRQ number */

          irqno = strtoul(&argv[index][1], &endptr, 0);
          if (endptr == &argv[index][1] || *endptr != '\0' ||
              irqno >= NR_IRQS)
            {
              fprintf(stderr,
                      "trace irq: invalid argument '%s'\n", argv[index]);
              return ERROR;
            }

          /* Update the masked IRQ number list */

          if (enable)
            {
              NOTE_FILTER_IRQMASK_SET(irqno, &filter_irq);
            }
          else
            {
              NOTE_FILTER_IRQMASK_CLR(irqno, &filter_irq);
            }
        }

      index++;
      modified = true;
    }

  if (modified)
    {
      /* Update current irq filter setting */

      ioctl(notectlfd, NOTECTL_SETIRQFILTER, (unsigned long)&filter_irq);
    }
  else
    {
      /* If no parameter, display current setting. */

      for (count = n = 0; n < NR_IRQS; n++)
        {
          if (NOTE_FILTER_IRQMASK_ISSET(n, &filter_irq))
            {
              count++;
            }
        }

      printf("Filtered IRQs: %d\n", count);

      for (n = 0; n < NR_IRQS; n++)
        {
          if (NOTE_FILTER_IRQMASK_ISSET(n, &filter_irq))
            {
              printf("  %d\n", n);
            }
        }
    }

  return index;
}
#endif

/****************************************************************************
 * Name: trace_cmd_print
 ****************************************************************************/

#ifdef CONFIG_SCHED_INSTRUMENTATION_DUMP
static int trace_cmd_print(int index, int argc, FAR char **argv,
                           int notectlfd)
{
  bool enable;
  struct note_filter_mode_s mode;

  /* Usage: trace print [+|-] */

  /* Get current filter setting */

  ioctl(notectlfd, NOTECTL_GETMODE, (unsigned long)&mode);

  /* Parse the setting parameters */

  if (argv[index][0] == '-' || argv[index][0] == '+')
    {
      enable = (argv[index][0] == '+');
      if (enable ==
          ((mode.flag & NOTE_FILTER_MODE_FLAG_DUMP) != 0))
        {
          /* Already set */

          return false;
        }

      if (enable)
        {
          mode.flag |= NOTE_FILTER_MODE_FLAG_DUMP;
        }
      else
        {
          mode.flag &= ~NOTE_FILTER_MODE_FLAG_DUMP;
        }

      ioctl(notectlfd, NOTECTL_SETMODE, (unsigned long)&mode);

      index++;
    }

  return index;
}
#endif

/****************************************************************************
 * Name: show_usage
 ****************************************************************************/

static void show_usage(void)
{
  fprintf(stderr,
          "\nUsage: trace <subcommand>...\n"
          "Subcommand:\n"
          "  start [-c][<duration>]          :"
                                " Start task tracing\n"
          "  stop                            :"
                                " Stop task tracing\n"
#ifdef CONFIG_SYSTEM_SYSTEM
          "  cmd [-c] <command> [<args>...]  :"
                                " Get the trace while running <command>\n"
#endif
#ifdef CONFIG_DRIVER_NOTERAM
          "  dump [-c][<filename>]           :"
                                " Output the trace result\n"
#endif
          "  mode [{+|-}{o|w|s|a|i|d}...]        :"
                                " Set task trace options\n"
#ifdef CONFIG_SCHED_INSTRUMENTATION_SWITCH
          "  switch [+|-] :"
                                " Configure switch trace filter\n"
#endif
#ifdef CONFIG_SCHED_INSTRUMENTATION_SYSCALL
          "  syscall [{+|-}<syscallname>...] :"
                                " Configure syscall trace filter\n"
#endif
#ifdef CONFIG_SCHED_INSTRUMENTATION_IRQHANDLER
          "  irq [{+|-}<irqnum>...]          :"
                                " Configure IRQ trace filter\n"
#endif
#ifdef CONFIG_SCHED_INSTRUMENTATION_DUMP
          "  print [+|-] :"
                                " Configure dump trace filter\n"
#endif
         );
}

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

int main(int argc, FAR char *argv[])
{
  int notectlfd;
  int exitcode = EXIT_FAILURE;
  int i;

  /* Open note control device */

  notectlfd = open("/dev/notectl", 0);
  if (notectlfd < 0)
    {
      fprintf(stderr,
              "trace: cannot open /dev/notectl\n");
      goto errout;
    }

  if (argc == 1)
    {
      /* No arguments - show current mode */

      trace_cmd_mode(0, 0, NULL, notectlfd);
      goto exit_with_close;
    }

  /* Parse command line arguments */

  i = 1;
  while (i < argc)
    {
      if (strcmp(argv[i], "start") == 0)
        {
          i = trace_cmd_start(i + 1, argc, argv, notectlfd);
        }
      else if (strcmp(argv[i], "stop") == 0)
        {
          i++;
          notectl_enable(false, notectlfd);
        }
#ifdef CONFIG_DRIVER_NOTERAM
      else if (strcmp(argv[i], "dump") == 0)
        {
          i = trace_cmd_dump(i + 1, argc, argv, notectlfd);
        }
#endif
#ifdef CONFIG_SYSTEM_SYSTEM
      else if (strcmp(argv[i], "cmd") == 0)
        {
          i = trace_cmd_cmd(i + 1, argc, argv, notectlfd);
        }
#endif
      else if (strcmp(argv[i], "mode") == 0)
        {
          i = trace_cmd_mode(i + 1, argc, argv, notectlfd);
        }
#ifdef CONFIG_SCHED_INSTRUMENTATION_SWITCH
      else if (strcmp(argv[i], "switch") == 0)
        {
          i = trace_cmd_switch(i + 1, argc, argv, notectlfd);
        }
#endif
#ifdef CONFIG_SCHED_INSTRUMENTATION_SYSCALL
      else if (strcmp(argv[i], "syscall") == 0)
        {
          i = trace_cmd_syscall(i + 1, argc, argv, notectlfd);
        }
#endif
#ifdef CONFIG_SCHED_INSTRUMENTATION_IRQHANDLER
      else if (strcmp(argv[i], "irq") == 0)
        {
          i = trace_cmd_irq(i + 1, argc, argv, notectlfd);
        }
#endif
#ifdef CONFIG_SCHED_INSTRUMENTATION_DUMP
      else if (strcmp(argv[i], "print") == 0)
        {
          i = trace_cmd_print(i + 1, argc, argv, notectlfd);
        }
#endif
      else
        {
          show_usage();
          goto errout_with_close;
        }

      if (i < 0)
        {
          goto errout_with_close;
        }
    }

exit_with_close:
  exitcode = EXIT_SUCCESS;

  /* Close note */

errout_with_close:
  close(notectlfd);

errout:
  return exitcode;
}
