#include "vcd_lib.h"

#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include "buffer.h"

size_t vcd_sizeof_signal_type(vcd_signal_type_t type) {
  switch (type) {
  case VCD_SIGNAL_TYPE_BOOL:
    return sizeof(int);
  case VCD_SIGNAL_TYPE_INT:
    return sizeof(int);
  case VCD_SIGNAL_TYPE_FLOAT:
    return sizeof(float);
  }
}

typedef struct vcd_signal {
  char *name;
  vcd_signal_type_t type;
  buffer_t *samples;
} vcd_signal_t;

vcd_signal_t *vcd_signal_alloc(const char *name,
                               vcd_signal_type_t type,
                               size_t initial_buffer_size) {
  vcd_signal_t *res = malloc_checked(sizeof *res);
  res->name = strdup_checked(name);
  res->type = type;
  res->samples =
    buffer_alloc(initial_buffer_size * vcd_sizeof_signal_type(type));
  return res;
}

void vcd_signal_free(vcd_signal_t *signal) {
  assert (signal);

  buffer_free(signal->samples);
  free(signal->name);
  free(signal);
}

void vcd_add_samples(vcd_signal_t *signal, void *samples, size_t count) {
  assert (signal);
  assert (samples);
  buffer_write(signal->samples,
               samples,
               count * vcd_sizeof_signal_type(signal->type));
}

const char *vcd_time_unit_repr(vcd_time_unit_t u) {
  const char *table[] = { "s", "ms", "us", "ns", "ps", "fs" };
  assert (VCD_TIME_UNIT_S <= u && u <= VCD_TIME_UNIT_FS);
  return table[u];
}

typedef struct vcd_file {
  char *filename;
  buffer_t *signals;
  vcd_time_unit_t time_unit;
  size_t time_unit_factor;
} vcd_file_t;

vcd_file_t *vcd_file_alloc(const char *filename,
                           vcd_time_unit_t time_unit,
                           size_t time_unit_factor) {
  assert (filename);

  vcd_file_t *vcd = malloc_checked(sizeof *vcd);
  vcd->filename = strdup_checked(filename);
  vcd->time_unit = time_unit;
  vcd->time_unit_factor = time_unit_factor;
  vcd->signals = buffer_alloc(10 * sizeof(vcd_signal_t));

  return vcd;
}

void vcd_file_free(vcd_file_t *vcd) {
  assert (vcd);

  /* Free buffers. */
  buffer_foreach (vcd_signal_t *, psig, vcd->signals)
    vcd_signal_free(*psig);
  buffer_free(vcd->signals);

  free(vcd->filename);
  free(vcd);
}

vcd_signal_t *vcd_file_lookup_signal(const vcd_file_t *vcd, const char *name) {
  assert (vcd);
  assert (name);

  vcd_signal_t *res = NULL;

  buffer_foreach (vcd_signal_t *, psig, vcd->signals) {
    if (strcmp((*psig)->name, name) == 0) {
      res = *psig;
      break;
    }
  }

  return res;
}

bool vcd_file_add_signal(const vcd_file_t *vcd, vcd_signal_t *signal) {
  assert (vcd);
  assert (signal);

  if (vcd_file_lookup_signal(vcd, signal->name))
    return false;

  buffer_write(vcd->signals, &signal, sizeof signal);
  return true;
}

bool vcd_file_write(vcd_file_t *vcd) {
  FILE *f = fopen(vcd->filename, "w");

  if (!f)
    return false;

  time_t current_time;
  time(&current_time);

  fprintf(f, "$version Generated by vcd.c $end\n");
  fprintf(f, "$date %s $end\n", ctime(&current_time));
  fprintf(f, "$timescale %zu %s $end\n",
          vcd->time_unit_factor,
          vcd_time_unit_repr(vcd->time_unit));

  /* Dump signal declarations. */
  fprintf(f, "$scope module Top $end\n");
  buffer_foreach (vcd_signal_t *, psig, vcd->signals) {
    fprintf(f, "$var ");
    switch ((*psig)->type) {
    case VCD_SIGNAL_TYPE_BOOL:
      fprintf(f, "wire 1");
      break;
    case VCD_SIGNAL_TYPE_INT:
      fprintf(f, "integer %zu", 8 * sizeof(int));
      break;
    case VCD_SIGNAL_TYPE_FLOAT:
      fprintf(f, "real 32");
      break;
    }
    fprintf(f, " %p %s $end\n", (*psig), (*psig)->name);
  }
  fprintf(f, "$upscope $end\n");
  fprintf(f, "$enddefinitions\n");

  /* Dump samples. */
  fprintf(f, "$dumpvars\n");

  /* We maintain a pointer to the current sample in each buffer. */
  size_t signal_count = vcd->signals->occupancy / sizeof(vcd_signal_t *);
  unsigned char **psamples = calloc(signal_count, sizeof(unsigned char *));
  assert (psamples);
  for (size_t i = 0; i < signal_count; i++)
    psamples[i] = ((vcd_signal_t **)vcd->signals->data)[i]->samples->data;

  /* We dump  */
  bool active = true;
  for (size_t step = 0; active; step++) {
    fprintf(f, "#%zu\n", step);
    active = false;
    for (size_t i = 0; i < signal_count; i++) {
      vcd_signal_t *sig = ((vcd_signal_t **)vcd->signals->data)[i];
      if (psamples[i] < sig->samples->data + sig->samples->occupancy) {
        active = true;
        switch (sig->type) {
        case VCD_SIGNAL_TYPE_BOOL:
          fprintf(f, "%d%p\n", (*(int *)psamples[i] ? 1 : 0), sig);
          psamples[i] += sizeof(int);
          break;
        case VCD_SIGNAL_TYPE_INT:
          fprintf(f, "r%d %p\n", *(int *)psamples, sig);
          psamples[i] += sizeof(int);
          break;
        case VCD_SIGNAL_TYPE_FLOAT:
          fprintf(f, "r%.16g %p\n", *(float *)psamples, sig);
          psamples[i] += sizeof(float);
          break;
        }
      }
    }
  }

  free(psamples);

  fclose(f);
  return true;
}
