/****************************************************************************
 * libs/libbuiltin/libgcc/gcov.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 <gcov.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <syslog.h>
#include <sys/stat.h>
#include <sys/types.h>

#include <nuttx/streams.h>
#include <nuttx/reboot_notifier.h>

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

/* The GCOV 12 gcno/gcda format has slight change,
 * Please refer to gcov-io.h in the GCC 12 for
 * more details.
 */

#if __GNUC__ >= 12
#  define GCOV_12_FORMAT
#endif

#ifdef GCOV_12_FORMAT
#  define GCOV_UNIT_SIZE      4
#else
#  define GCOV_UNIT_SIZE      1
#endif

#ifdef GCOV_12_FORMAT
#  define GCOV_TAG_FUNCTION_LENGTH 12
#else
#  define GCOV_TAG_FUNCTION_LENGTH 3
#endif

#if __GNUC__ >= 14
#  define GCOV_COUNTERS 9u
#elif __GNUC__ >= 10
#  define GCOV_COUNTERS 8u
#elif __GNUC__ >= 8
#  define GCOV_COUNTERS 9u
#else
#  define GCOV_COUNTERS 10u
#endif

#define GCOV_DATA_MAGIC       (0x67636461)
#define GCOV_NOTE_MAGIC       (0x67636e6f)
#define GCOV_FILENAME_MAGIC   (0x6763666e)

#define GCOV_TAG_FUNCTION     (0x01000000)
#define GCOV_TAG_COUNTER_BASE (0x01a10000)

#define GCOV_TAG_FOR_COUNTER(count) \
  (GCOV_TAG_COUNTER_BASE + ((uint32_t)(count) << 17))

#define GCOV_PATH_MAX_TOKENS 64

/****************************************************************************
 * Private Types
 ****************************************************************************/

typedef uint64_t gcov_type;
typedef unsigned int gcov_unsigned_t;

/** Profiling data per object file
 *
 * This data is generated by gcc during compilation and doesn't change
 * at run-time with the exception of the next pointer.
 */

struct gcov_info
{
  unsigned int version;                /* Gcov version (same as GCC version) */
  FAR struct gcov_info *next;          /* List head for a singly-linked list */
  unsigned int stamp;                  /* Uniquifying time stamp */
#ifdef GCOV_12_FORMAT
  unsigned int checksum;               /* unique object checksum */
#endif
  FAR const char *filename;            /* Name of the associated gcda data file */
  void (*merge[GCOV_COUNTERS])(FAR gcov_type *, unsigned int);
  unsigned int n_functions;            /* number of instrumented functions */
  FAR struct gcov_fn_info **functions; /* function information */
};

/* Information about counters for a single function
 *
 * This data is generated by gcc during compilation and doesn't change
 * at run-time.
 */

struct gcov_ctr_info
{
  unsigned int num;      /* Number of counter values for this type */
  FAR gcov_type *values; /* Array of counter values for this type */
};

/* Profiling meta data per function
 *
 * This data is generated by gcc during compilation and doesn't change
 * at run-time.
 */

struct gcov_fn_info
{
  FAR const struct gcov_info *key; /* Comdat key */
  unsigned int ident;              /* Unique ident of function */
  unsigned int lineno_checksum;    /* Function lineno checksum */
  unsigned int cfg_checksum;       /* Function cfg checksum */
  struct gcov_ctr_info ctrs[1];    /* Instrumented counters */
};

struct dump_args
{
  FAR CODE int  (*mkdir)(FAR const char *);
  FAR struct lib_outstream_s *stream;
  FAR char *prefix;
  FAR char *tokens[GCOV_PATH_MAX_TOKENS];
  size_t    token_cnt;
  FAR char *path_buffer;
  int       strip;
};

typedef CODE int (*dump_fn_t)(FAR struct dump_args *,
                              FAR char *, FAR uint8_t *, size_t);

/****************************************************************************
 * Public Data
 ****************************************************************************/

FAR struct gcov_info *__gcov_info_start;
FAR struct gcov_info *__gcov_info_end;

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

static void dump_counter(FAR void *buffer, FAR size_t *off, uint64_t v)
{
  if (buffer)
    {
      *(FAR uint64_t *)((FAR uint8_t *)buffer + *off) = v;
    }

  *off += sizeof(uint64_t);
}

static void dump_unsigned(FAR void *buffer, FAR size_t *off, uint32_t v)
{
  if (buffer)
    {
      *(FAR uint32_t *)((FAR uint8_t *)buffer + *off) = v;
    }

  *off += sizeof(uint32_t);
}

static int get_path_tokens(FAR char *path,
                           FAR char **tokens, size_t max_tokens)
{
  size_t token_count = 0;
  FAR char *token;

  token = strtok(path, "/");
  while (token != NULL)
    {
      if (token_count >= max_tokens)
        {
          return -ENAMETOOLONG;
        }

      tokens[token_count++] = token;
      token = strtok(NULL, "/");
    }

  tokens[token_count] = NULL;
  return token_count;
}

static size_t gcov_convert(FAR uint8_t *buffer,
                           FAR const struct gcov_info *info)
{
  FAR struct gcov_fn_info *gfi;
  FAR struct gcov_ctr_info *gci;
  uint32_t functions;
  uint32_t counts;
  uint32_t value;
  size_t pos = 0;

  /* File header. */

  dump_unsigned(buffer, &pos, GCOV_DATA_MAGIC);
  dump_unsigned(buffer, &pos, info->version);
  dump_unsigned(buffer, &pos, info->stamp);

#ifdef GCOV_12_FORMAT
  dump_unsigned(buffer, &pos, info->checksum);
#endif

  /* Function headers. */

  for (functions = 0; functions < info->n_functions; functions++)
    {
      gfi = info->functions[functions];

      /* Function record. */

      dump_unsigned(buffer, &pos, GCOV_TAG_FUNCTION);
      dump_unsigned(buffer, &pos, GCOV_TAG_FUNCTION_LENGTH);
      dump_unsigned(buffer, &pos, gfi->ident);
      dump_unsigned(buffer, &pos, gfi->lineno_checksum);
      dump_unsigned(buffer, &pos, gfi->cfg_checksum);

      gci = gfi->ctrs;
      for (counts = 0; counts < GCOV_COUNTERS; counts++)
        {
          if (!info->merge[counts])
            {
              continue;
            }

          /* Counter record. */

          dump_unsigned(buffer, &pos, GCOV_TAG_FOR_COUNTER(counts));
          dump_unsigned(buffer, &pos, gci->num * 2 * GCOV_UNIT_SIZE);

          for (value = 0; value < gci->num; value++)
            {
              dump_counter(buffer, &pos, gci->values[value]);
            }

          gci++;
        }
    }

  return pos;
}

static int gcov_mkdir(FAR const char *path)
{
  if (access(path, F_OK) != 0)
    {
      return mkdir(path, 0777);
    }

  return OK;
}

#ifdef CONFIG_COVERAGE_GCOV_DUMP_REBOOT
static int gcov_reboot_notify(FAR struct notifier_block *nb,
                              unsigned long action, FAR void *data)
{
  __gcov_dump();
  return OK;
}
#endif

static int gcov_standard_dump(FAR struct dump_args *args,
                              FAR char *path, FAR uint8_t *data, size_t size)
{
  int written;
  int fd;

  fd = _NX_OPEN(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
  if (fd < 0)
    {
      return -errno;
    }

  written = _NX_WRITE(fd, data, size);
  if (written != size)
    {
      return -errno;
    }

  _NX_CLOSE(fd);
  return OK;
}

static int gcov_onefile_dump(FAR struct dump_args *args,
                             FAR char *path,
                             FAR uint8_t *data, size_t size)
{
  FAR struct lib_outstream_s *stream = args->stream;
  struct lib_hexdumpstream_s hexstream;
  uint16_t checksum = 0;
  int i;

  lib_hexdumpstream(&hexstream, stream);
  for (i = 0; i < size; i++)
    {
      checksum += ((FAR const uint8_t *)data)[i];
    }

  lib_sprintf(stream,
              "gcov start filename:%s size: %zuByte\n",
              path, size);

  lib_stream_puts(&hexstream, data, size);
  lib_stream_flush(&hexstream);

  lib_sprintf(stream,
              "gcov end filename:%s checksum: %#0x\n",
              path, checksum);
  lib_stream_flush(stream);

  return OK;
}

static int gcov_dump_foreach(dump_fn_t handler, FAR struct dump_args *args)
{
  FAR struct gcov_info *info;
  FAR char *filename;
  FAR uint8_t *data;
  int token_count;
  int size;
  int i;

  for (info = __gcov_info_start; info; info = info->next)
    {
      memset(args->path_buffer, 0x00, PATH_MAX);

      size = gcov_convert(NULL, info);
      data = lib_malloc(size);
      if (data == NULL)
        {
          syslog(LOG_ERR, "gcov alloc failed!");
          return -ERROR;
        }

      gcov_convert(data, info);

      filename = strdup(info->filename);
      if (filename == NULL)
        {
          syslog(LOG_ERR, "gcov alloc failed! skip %s", info->filename);
          lib_free(data);
          return -ERROR;
        }

      token_count = args->token_cnt +
                    get_path_tokens(filename,
                                    &args->tokens[args->token_cnt],
                                    GCOV_PATH_MAX_TOKENS);
      if (token_count < args->token_cnt)
        {
          syslog(LOG_ERR, "gcov get path tokens failed! skip %s",
                          info->filename);
          goto exit;
        }

      for (i = 0; i < token_count - 1; i++)
        {
          strcat(args->path_buffer, "/");
          strcat(args->path_buffer, args->tokens[i]);
          if (args->mkdir)
            {
              args->mkdir(args->path_buffer);
            }
        }

      strcat(args->path_buffer, "/");
      strcat(args->path_buffer, args->tokens[token_count - 1]);

      if (handler(args, args->path_buffer, data, size) < 0)
        {
          syslog(LOG_ERR, "gcov dump %s failed!\n", args->path_buffer);
          goto exit;
        }

      lib_free(filename);
      lib_free(data);
    }

  return OK;

exit:
  lib_free(filename);
  lib_free(data);

  return -ERROR;
}

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

void __gcov_init(FAR struct gcov_info *info)
{
#ifdef CONFIG_COVERAGE_GCOV_DUMP_REBOOT
  static struct notifier_block nb;
#endif
  char path[PATH_MAX] = CONFIG_COVERAGE_DEFAULT_PREFIX;
  static int inited = 0;
  struct tm *tm_info;
  time_t cur;

  if (!inited)
    {
      cur = time(NULL);
      tm_info = localtime(&cur);

      strftime(path + strlen(path),
               PATH_MAX,
               "/gcov_%Y%m%d_%H%M%S",
               tm_info);

      setenv("GCOV_PREFIX_STRIP", CONFIG_COVERAGE_DEFAULT_PREFIX_STRIP, 1);
      setenv("GCOV_PREFIX", path, 1);

#ifdef CONFIG_COVERAGE_GCOV_DUMP_REBOOT
      nb.notifier_call = gcov_reboot_notify;
      register_reboot_notifier(&nb);
#endif

      inited++;
    }

  info->next = __gcov_info_start;
  __gcov_info_start = info;
}

void __gcov_merge_add(FAR gcov_type *counters, unsigned int n_counters)
{
}

void __gcov_exit(void)
{
}

void __gcov_execve(void)
{
}

void __gcov_execl(void)
{
}

void __gcov_execv(void)
{
}

void __gcov_execle(void)
{
}

pid_t __gcov_fork(void)
{
  return fork();
}

void __gcov_dump(void)
{
  int ret = -1;
  FAR char *prefix;
  struct stat state;
  struct dump_args args =
  {
    0
  };

  prefix = getenv("GCOV_PREFIX");
  if (prefix == NULL)
    {
      syslog(LOG_ERR, "No path prefix specified");
    }

  args.path_buffer = lib_get_tempbuffer(PATH_MAX);
  if (args.path_buffer == NULL)
    {
      syslog(LOG_ERR, "gcov alloc failed!");
      return;
    }

  if (stat(prefix, &state) ||
      S_ISREG(state.st_mode) || S_ISCHR(state.st_mode))
    {
      struct lib_rawoutstream_s stream;
      int fd = _NX_OPEN(prefix, O_WRONLY | O_CREAT | O_TRUNC, 0666);
      if (fd < 0)
        {
          syslog(LOG_ERR, "open %s failed! ret: %d\n", prefix, ret);
          goto exit;
        }

      lib_rawoutstream(&stream, fd);
      args.stream = &stream.common;

      ret = gcov_dump_foreach(gcov_onefile_dump, &args);

      _NX_CLOSE(fd);
    }
  else if(S_ISDIR(state.st_mode))
    {
      args.mkdir = gcov_mkdir;
      args.strip = atoi(getenv("GCOV_PREFIX_STRIP"));
      args.token_cnt = get_path_tokens(prefix,
                                       args.tokens,
                                       GCOV_PATH_MAX_TOKENS);

      ret = gcov_dump_foreach(gcov_standard_dump, &args);
    }
  else
    {
      ret = -ENOTDIR;
    }

exit:

  if (ret < 0)
    {
      syslog(LOG_INFO, "Gcov dump failed\n");
    }
  else
    {
      syslog(LOG_INFO, "Gcov dump complete\n");
    }

  lib_put_tempbuffer(args.path_buffer);
}

void __gcov_reset(void)
{
  FAR struct gcov_info *info;
  FAR struct gcov_ctr_info *gci;
  uint32_t functions;
  uint32_t counts;

  for (info = __gcov_info_start; info; info = info->next)
    {
      for (functions = 0; functions < info->n_functions; functions++)
        {
          gci = info->functions[functions]->ctrs;

          for (counts = 0; counts < GCOV_COUNTERS; counts++)
            {
              if (!info->merge[counts])
                {
                  continue;
                }

              memset(gci->values, 0, gci->num * sizeof(gcov_type));
              gci++;
            }
        }
    }
}
