/* vector-asc.c
 *
 * Wiretap Library
 * Copyright (c) 1998 by Gilbert Ramirez <gram@alumni.rice.edu>
 *
 * Support for Vector ASC log file format generated by several CAN tools from Vector Informatik
 * Copyright (c) 2026 by Miklos Marton <martonmiklosqdev@gmail.com>
 *
 * Heavily inspired by the ASC parser of the python-can:
 * https://github.com/hardbyte/python-can/blob/main/can/io/asc.py
 * and from the peak-trc.c decoder implementation
 *
 * Based on Vector Specification CAN, Log & Trigger ASC Logging Format Version 1.3.5 dated 2014-10-07
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include <config.h>
#include <file_wrappers.h>
#include <epan/dissectors/packet-socketcan.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <time.h>
#include <glib.h>
#include <locale.h>

#include "vector-asc.h"
#include "socketcan.h"

//#define VECTOR_ASC_DEBUG
#ifdef VECTOR_ASC_DEBUG
#include <stdio.h>
#define vector_asc_debug_printf(...) printf(__VA_ARGS__)
#else
#define vector_asc_debug_printf(...) (void)0
#endif

#define ASC_MAGIC "date"
#define VECTOR_ASC_MAX_LINE_SIZE 512

static const gchar *packet_pattern = "\\d+\\.\\d+\\s+(\\d+\\s+(\\w+\\s+(Tx|Rx)|ErrorFrame)|CANFD)";
static const gchar *base_pattern = "base\\s+(hex|dec)(\\s+timestamps\\s+(absolute|relative))?";

typedef enum {
  TimeStamp,
  Channel,
  CanID,
  Direction,
  Brs,
  Esi,
  DataMarker,
  Dlc,
  DataLength,
  DataBytes
} vector_asc_data_column_t;

typedef enum{
  Hex,
  Dec
} vector_asc_base_t;

typedef enum{
  Absolute,
  Relative
} vector_asc_timestamp_t;

typedef enum {
  Date,
  Base,
  Version,
  Packets
} vector_asc_processing_state_t;

typedef struct {
  uint8_t major, minor, patch;
} vector_asc_version_t;

typedef struct {
  vector_asc_processing_state_t state;
  vector_asc_timestamp_t timestamps_type;
  vector_asc_base_t base;
  time_t trace_start;
  time_t trace_start_nsec;
  GRegex* data_matcher;
  vector_asc_version_t version;
} vector_asc_state_t;

static int vector_asc_file_type_subtype = -1;

static GRegex *month_regexes[16] = {0};
static const gchar *month_numbers[16] = {
    "01", "02", "03", "04", "05", "06", "07", "08",
    "09", "10", "11", "12", "03", "05", "10", "12"
};

static const gchar *month_patterns[16] = {
    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
    "Sep", "Oct", "Nov", "Dec", "Mär", "Mai", "Okt", "Dez"
};

void init_month_regexes(void)
{
  for (int i = 0; i < 16; ++i) {
    month_regexes[i] = g_regex_new(month_patterns[i], 0, 0, NULL);
  }
}

void free_month_regexes(void)
{
  for (int i = 0; i < 16; ++i) {
    //g_free(month_regexes[i]);
  }
}

void register_vector_asc(void);

static wtap_open_return_val
vector_asc_parse(wtap* wth, vector_asc_state_t* state, gint64* offset, int* err, char** err_info)
{
  gint64 seek_off = 0;
  char line_buffer[VECTOR_ASC_MAX_LINE_SIZE];

  vector_asc_debug_printf("%s: Trying vector_asc file decoder\n", G_STRFUNC);

  /* Initial start time until we find it in the header */
  state->trace_start = time(NULL);
  state->trace_start_nsec = 0;
  state->state = Date;
  state->timestamps_type = Absolute;

  bool reprocess_line = false;

  /* Bail if the end of the file is here and it hasn't been determined it's a Vector ASC file */
  while (!file_eof(wth->fh))
  {
    seek_off = file_tell(wth->fh);
    vector_asc_debug_printf("%s: Starting parser at offset %" PRIi64 "\n", G_STRFUNC, seek_off);

    if (!reprocess_line)
    {
      if (file_gets(line_buffer, VECTOR_ASC_MAX_LINE_SIZE, wth->fh) == NULL)
      {
        /* Error reading file, bail out */
        *err = file_error(wth->fh, err_info);
        if (*err != 0 && *err != WTAP_ERR_SHORT_READ)
          return WTAP_OPEN_ERROR;

        return WTAP_OPEN_NOT_MINE;
      }
    } else {
      reprocess_line = false;
    }

    g_strstrip(line_buffer);
    size_t line_len = strlen(line_buffer);

    /* Ignore empty lines */
    if (line_len == 0)
      continue;

    if (state->state == Date) {
      if (strncmp(line_buffer, ASC_MAGIC, strlen(ASC_MAGIC)) != 0) {
        return WTAP_OPEN_NOT_MINE;
      } else {
        // replace month names with 2 digit numbers
        gchar *datetime_string = g_strdup(&line_buffer[5]);
        for (gsize i = 0; i < 16; i++) {
          gchar *old = datetime_string;
          datetime_string = g_regex_replace_literal(
              month_regexes[i],
              datetime_string, -1, 0,
              month_numbers[i],
              0, NULL
              );
          g_free(old);
        }
        // date <WeekDay> <Month> <Date> <Fulltime> <Year>
        // Starting with CANalyzer/CANoe v8.2 SP2 the <Fulltime> is stored in milliseconds.
        bool parsed = false;
        int year, month, day, hour, min, sec, usec;
        char ampm[4];
        // Try the %m %d %H:%M:%S.%f %Y	format
        int matches = sscanf(datetime_string, "%d %02d %02d:%02d:%02d.%d %04d", &month, &day, &hour, &min, &sec, &usec, &year);
        if (matches == 7) {
          parsed = true;
          state->trace_start_nsec = usec * 1000;
        }

        if (!parsed) {
          // fallback try on the %m %d %H:%M:%S %Y format
          matches = sscanf(datetime_string, "%d %02d %02d:%02d:%02d %04d", &month, &day, &hour, &min, &sec, &year);
          if (matches == 6) {
            parsed = true;
          }
        }

        if (!parsed) {
          // try the 8 Apr 2025 3:49:52pm format
          matches = sscanf(datetime_string, "%d %02d %04d %d:%d:%d%02s", &day, &month, &year, &hour, &min, &sec, ampm);
          if (matches == 7) {
            if (strncmp(ampm, "pm", 2) == 0) {
              hour += 12;
            }
            parsed = true;
          }
        }

        if (parsed) {
          struct tm t = {0};
          t.tm_year = year - 1900;
          t.tm_mon = month - 1;
          t.tm_mday = day;
          t.tm_hour = hour;
          t.tm_min = min;
          t.tm_sec = sec;
          state->trace_start = mktime(&t);
        }

        g_free(datetime_string);

        state->state = Base;
        continue;
      }
    } else if (state->state == Base) {
      if (strncmp(line_buffer, "base ", 5) == 0) {
        GMatchInfo *match_info;
        GRegex *base_regex = g_regex_new(base_pattern, G_REGEX_CASELESS, 0, NULL);
        if (g_regex_match(base_regex, line_buffer, 0, &match_info)) {
          const gchar *base_str = g_match_info_fetch(match_info, 1);
          const gchar *time_str = g_match_info_fetch(match_info, 3);

          if (g_strcmp0(base_str, "hex") == 0)
            state->base = Hex;
          else
            state->base = Dec;

          if (time_str && g_strcmp0(time_str, "relative") == 0) {
            state->timestamps_type = Relative;
          }

          g_free((gchar *)base_str);
          g_free((gchar *)time_str);
        }

        g_match_info_free(match_info);
        g_regex_unref(base_regex);
        base_regex = NULL;
      }
      state->state = Packets;
    } else if (state->state == Version) {
      // Starting with CANalyzer/CANoe v7.0 a version number is written after the header in form of a comment:
      // '// version <major>.<minor>.<patch>'
      if (strncmp(line_buffer, "// version", 10) == 0) {
      } else {
        // version not following the header, maybe pre 7.0 file -> reprocess the line as Packet
        state->state = Packets;
        reprocess_line = true;
        continue;
      }
    } else if (state->state == Packets) {
      /* seek until the first data packet */
      if (state->data_matcher == NULL) {
        GError *error = NULL;
        state->data_matcher = g_regex_new(packet_pattern, (GRegexCompileFlags)(G_REGEX_OPTIMIZE | G_REGEX_RAW), (GRegexMatchFlags)0, NULL);
        if (!state->data_matcher) {
          g_error_free(error);
          return WTAP_OPEN_ERROR;
        }
      }

      GMatchInfo *match_info;
      g_regex_match(state->data_matcher, line_buffer, 0, &match_info);
      if (g_match_info_matches(match_info)) {
        g_match_info_free(match_info);

        *offset = seek_off;
        return WTAP_OPEN_MINE;
      }
    }
  }

  return WTAP_OPEN_NOT_MINE;
}

static bool vector_asc_process_classic_can_frame(char *token, wtap_can_msg_t *vector_msg, wtap* wth)
{
  vector_asc_state_t *state = (vector_asc_state_t*)wtap_socketcan_get_private_data(wth);
  if (strcmp(token, "errorframe") == 0) {
    vector_msg->type = MSG_TYPE_ERR;
  } else {
    int columnIndex = CanID;
    int dataCounter = 0;
    uint32_t tmp = 0;
    while (token != NULL)
    {
      switch ((vector_asc_data_column_t)columnIndex) {
        case TimeStamp:
        case Channel:
          // just to make the compler warning free, timestamp and channel had been processed already in vector_asc_read_packet
          break ;
        case CanID:
          // extended frames marked by an x suffix in the CAN ID column
          if (state->base == Hex) {
            if (token[strlen(token) - 1] == 'x') {
              sscanf(token, "%Xx", &vector_msg->id);
              vector_msg->type = MSG_TYPE_EXT;
            } else {
              sscanf(token, "%X", &vector_msg->id);
              vector_msg->type = MSG_TYPE_STD;
            }
          } else {
            if (token[strlen(token) - 1] == 'x') {
              sscanf(token, "%ux", &vector_msg->id);
              vector_msg->type = MSG_TYPE_EXT;
            } else {
              sscanf(token, "%u", &vector_msg->id);
              vector_msg->type = MSG_TYPE_STD;
            }
          }
          columnIndex = Direction;
          break;
        case Direction:
          columnIndex = DataMarker;
          break;
        case DataMarker:
          if (token[0] == 'r') {
            vector_msg->type = MSG_TYPE_STD_RTR;
          }
          columnIndex = Dlc;
          break;
        case Dlc:
          sscanf(token, "%hhu", &vector_msg->data.length);
          columnIndex = DataBytes;
          break;
        case DataBytes:
          // databytes are generated into multiple columns
          if (dataCounter < 8 && dataCounter < vector_msg->data.length) {
            sscanf(token, "%02X", &tmp);
            vector_msg->data.data[dataCounter] = tmp;
          }
          dataCounter++;
          break;
        case Brs:
        case Esi:
        case DataLength:
          // CAN FD columns
          break;
      }
      token = strtok(NULL, " ");
    }
  }
  return true;
}

static bool vector_asc_process_fd_can_frame(char *token, wtap_can_msg_t *vector_msg, wtap* wth)
{
  (void)wth;
  if (strcmp(token, "errorframe") == 0) {
    vector_msg->type = MSG_TYPE_ERR;
  } else {
    int columnIndex = CanID;
    int dataCounter = 0;
    uint32_t tmp = 0;
    while (token != NULL)
    {
      switch ((vector_asc_data_column_t)columnIndex) {
        case TimeStamp:
        case Channel:
          // timestamp and channel had been processed already in vector_asc_read_packet
          break;
        case CanID:
          // extended frames marked by an x suffix in the CAN ID column
          //  CAN FD representation only in hex. (See 3.12 Symbols - <ID> -> special column
          if (token[strlen(token) - 1] == 'x') {
            sscanf(token, "%Xx", &vector_msg->id);
            vector_msg->type = MSG_TYPE_EXT_FD;
          } else {
            sscanf(token, "%X", &vector_msg->id);
            vector_msg->type = MSG_TYPE_STD_FD;
          }
          columnIndex = Direction;
          break;
        case Direction:
          columnIndex = DataMarker;
          break;
        case Brs:
          break;
        case Esi:
          break;
        case DataLength:
          break;
        case DataMarker:
          if (token[0] == 'r') {
            vector_msg->type = MSG_TYPE_STD_RTR;
          }
          columnIndex = Dlc;
          break;
        case Dlc:
          sscanf(token, "%hhu", &vector_msg->data.length);
          columnIndex = DataBytes;
          break;
        case DataBytes:
          if (dataCounter < 8 && dataCounter < vector_msg->data.length) {
            sscanf(token, "%02X", &tmp);
            vector_msg->data.data[dataCounter] = tmp;
          }
          dataCounter++;
          break;
      }
      token = strtok(NULL, " ");
    }
  }
  /*
            can_id_str, frame_name_or_brs, rest_of_message = rest_of_message.split(
                None, 2
            )

            if frame_name_or_brs.isdigit():
                brs = frame_name_or_brs
                esi, dlc_str, data_length_str, data = rest_of_message.split(None, 3)
            else:
                brs, esi, dlc_str, data_length_str, data = rest_of_message.split(
                    None, 4
                )

            self._extract_can_id(can_id_str, msg_kwargs)
            msg_kwargs["bitrate_switch"] = brs == "1"
            msg_kwargs["error_state_indicator"] = esi == "1"
            dlc = int(dlc_str, self._converted_base)
            data_length = int(data_length_str)
            if data_length == 0:
                # CAN remote Frame
                msg_kwargs["is_remote_frame"] = True
                msg_kwargs["dlc"] = dlc
            else:
                if dlc2len(dlc) != data_length:
                    logger.warning(
                        "DLC vs Data Length mismatch %d[%d] != %d",
                        dlc,
                        dlc2len(dlc),
                        data_length,
                    )
                msg_kwargs["dlc"] = data_length

            self._process_data_string(data, data_length, msg_kwargs)

        return Message(**msg_kwargs)*/
  return true;
}

static bool
vector_asc_read_packet(wtap *wth, FILE_T fh, vector_asc_state_t* state,
                       wtap_rec *rec, int *err, gchar **err_info,
                       gint64 *data_offset)
{
  wtap_can_msg_t vector_msg = {0};
  char line_buffer[VECTOR_ASC_MAX_LINE_SIZE];

  while (!file_eof(fh))
  {
    if (data_offset)
      *data_offset = file_tell(fh);

    /* Read a line */
    if (file_gets(line_buffer, VECTOR_ASC_MAX_LINE_SIZE, fh) == NULL)
    {
      *err = file_error(fh, err_info);
      return false;
    }

    if ((strcmp(line_buffer, "Begin Triggerblock") == 0) ||
        (strcmp(line_buffer, "End Triggerblock") == 0)) {
      continue;
    }

    GMatchInfo *match_info;
    double message_timestamp_s = 0.0;
    bool isFd = false;

    vector_msg.interface_id = WTAP_SOCKETCAN_INVALID_INTERFACE_ID;

    g_regex_match(state->data_matcher, line_buffer, 0, &match_info);
    if (g_match_info_matches(match_info)) {
      g_match_info_free(match_info);

      char *column = strtok(line_buffer, " ");
      int columnIndex = TimeStamp;
      while (column != NULL)
      {
        switch ((vector_asc_data_column_t)columnIndex) {
          case TimeStamp:
            message_timestamp_s = g_ascii_strtod(column, NULL);
            if (state->timestamps_type == Absolute) {
              message_timestamp_s += state->trace_start + (state->trace_start_nsec / 1e9);
              int delta_secs = (int)(message_timestamp_s);
              int delta_nsecs = (int)((message_timestamp_s - delta_secs) * 1e9);

              vector_msg.ts.secs = delta_secs;
              vector_msg.ts.nsecs = delta_nsecs;
            } else {
              vector_msg.ts.secs = (time_t)(message_timestamp_s);
              vector_msg.ts.nsecs = (int)((message_timestamp_s - (vector_msg.ts.secs)) * 1e9);
            }
            columnIndex = Channel;
            break;
          case Channel:
            if ((strcmp(column, "Comment:") == 0) ||
                (strcmp(column, "GPS") == 0) ||
                (strcmp(column, "SV:") == 0) ||
                (strcmp(column, "log") == 0)) {
              continue;
            }

            if (strcmp(column, "CANFD") == 0) {
              isFd = true;
              columnIndex = Channel;
            } else if (strcmp(column, "CAN") == 0) {
              columnIndex = Channel;
            } else {
              char* interface_name = wmem_strdup_printf(NULL, "interface%s", column);
              vector_msg.interface_id = wtap_socketcan_find_or_create_new_interface(wth, interface_name);
              wmem_free(NULL, interface_name);
              columnIndex = CanID;
            }
            break;
          case CanID:
            if (isFd) {
              vector_asc_process_fd_can_frame(column, &vector_msg, wth);
            } else {
              vector_asc_process_classic_can_frame(column, &vector_msg, wth);
            }
            break;
          default:
            break;
        }
        column = strtok(NULL, " ");
      }
    }

    return wtap_socketcan_gen_packet(wth, rec, &vector_msg, "vector-asc", err, err_info);
  }

  return false;
}

static bool
vector_asc_read(wtap   *wth, wtap_rec *rec, int *err, char **err_info,
                int64_t *data_offset)
{
  vector_asc_state_t* state = (vector_asc_state_t*)wtap_socketcan_get_private_data(wth);
  vector_asc_debug_printf("%s: Try reading at offset %" PRIi64 "\n", G_STRFUNC, file_tell(wth->fh));
  return vector_asc_read_packet(wth, wth->fh, state, rec, err, err_info, data_offset);
}

static bool vector_asc_seek_read(wtap *wth, int64_t seek_off, wtap_rec *rec, int *err, char **err_info)
{
  vector_asc_debug_printf("%s: Read at offset %" PRIi64 "\n", G_STRFUNC, seek_off);
  vector_asc_state_t* state = (vector_asc_state_t*)wtap_socketcan_get_private_data(wth);

  if (file_seek(wth->random_fh, seek_off, SEEK_SET, err) == -1)
  {
    *err      = errno;
    *err_info = g_strdup(g_strerror(errno));

    return false;
  }

  return vector_asc_read_packet(wth, wth->random_fh, state, rec, err, err_info, NULL);
}

static void
clean_asc_state(vector_asc_state_t* state)
{
  if (state != NULL)
  {
    if (state->data_matcher != NULL)
      g_regex_unref(state->data_matcher);

    g_free(state);
  }
}


static void vector_asc_close(void* tap_data)
{
  clean_asc_state((vector_asc_state_t*)tap_data);
  free_month_regexes();
}

wtap_open_return_val
vector_asc_open(wtap* wth, int* err, char** err_info)
{
  gint64 data_offset = 0;
  vector_asc_state_t* asc_state = g_new0(vector_asc_state_t, 1);

  /* wth->priv stores a pointer to the general file properties.
         * It it updated when the header data is parsed */
  wth->priv = asc_state;

  init_month_regexes();

  wtap_open_return_val open_val = vector_asc_parse(wth, asc_state, &data_offset, err, err_info);
  if (open_val != WTAP_OPEN_MINE)
  {
    clean_asc_state(asc_state);
    wth->priv = NULL;
    return open_val;
  }

  vector_asc_debug_printf("%s: This is our file\n", G_STRFUNC);

  /* Go to the start of the real packet data since header is now done */
  if (file_seek(wth->fh, data_offset, SEEK_SET, err) == -1)
  {
    *err = errno;
    *err_info = g_strdup(g_strerror(errno));
    return WTAP_OPEN_ERROR;
  }

  wtap_set_as_socketcan(wth, vector_asc_file_type_subtype, WTAP_TSPREC_USEC, asc_state, vector_asc_close);
  wth->subtype_read = vector_asc_read;
  wth->subtype_seek_read = vector_asc_seek_read;

  return WTAP_OPEN_MINE;
}

static const struct supported_block_type vector_asc_blocks_supported[] = {
    /*
     * We support packet blocks, with no comments or other options.
     */
    { WTAP_BLOCK_PACKET, MULTIPLE_BLOCKS_SUPPORTED, NO_OPTIONS_SUPPORTED }
};

static const struct file_type_subtype_info vector_asc_info = {
    "Vector ASC CAN log file", "vector-asc", "asc", NULL,
    false, BLOCKS_SUPPORTED(vector_asc_blocks_supported),
    NULL, NULL, NULL
};

void register_vector_asc(void)
{
  vector_asc_file_type_subtype = wtap_register_file_type_subtype(&vector_asc_info);
}

/*
 * Editor modelines  -  https://www.wireshark.org/tools/modelines.html
 *
 * Local variables:
 * c-basic-offset: 4
 * tab-width: 8
 * indent-tabs-mode: nil
 * End:
 *
 * vi: set shiftwidth=4 tabstop=8 expandtab:
 * :indentSize=4:tabSize=8:noTabs=true:
 */
