/* 
 * This file is part of the OSDI component of NGSPICE.
 * Copyright© 2022 SemiMod GmbH.
 * Copyright© 2023 Pascal Kuthe.
 * 
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. 
 *
 * Author: Pascal Kuthe <pascal.kuthe@semimod.de>
 */

#include "ngspice/config.h"
#include "ngspice/hash.h"
#include "ngspice/memory.h"
#include "ngspice/stringutil.h"
#include "osdidefs.h"

#include <stddef.h>
#include <sys/stat.h>

#include "osdi.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#if (!defined HAS_WINGUI) && (!defined __MINGW32__) && (!defined _MSC_VER)

#include <dlfcn.h> /* to load libraries*/
#define OPENLIB(path) dlopen(path, RTLD_NOW | RTLD_LOCAL)
#define GET_SYM(lib, sym) dlsym(lib, sym)
#define FREE_DLERR_MSG(msg)

#else /* ifdef HAS_WINGUI */

#include <windows.h>
#include <shlwapi.h>
#define OPENLIB(path) LoadLibrary(path)
#define GET_SYM(lib, sym) ((void *)GetProcAddress(lib, sym))
char *dlerror(void);
#define FREE_DLERR_MSG(msg) free_dlerr_msg(msg)
static void free_dlerr_msg(char *msg);

#endif /* ifndef HAS_WINGUI */

char *inputdir = NULL;

/* Returns true if path is an absolute path and false if it is a
 * relative path. No check is done for the existance of the path. */
inline static bool is_absolute_pathname(const char *path) {
#ifdef _WIN32
  return !PathIsRelativeA(path);
#else
  return path[0] == DIR_TERM;
#endif
} /* end of funciton is_absolute_pathname */

/*-------------------------------------------------------------------------*
  Look up the variable sourcepath and try everything in the list in order
  if the file isn't in . and it isn't an abs path name.
  *-------------------------------------------------------------------------*/

static char *resolve_path(const char *name) {
  struct stat st;

#if defined(_WIN32)

  /* If variable 'mingwpath' is set: convert mingw /d/... to d:/... */
  if (cp_getvar("mingwpath", CP_BOOL, NULL, 0) && name[0] == DIR_TERM_LINUX &&
      isalpha_c(name[1]) && name[2] == DIR_TERM_LINUX) {
    DS_CREATE(ds, 100);
    if (ds_cat_str(&ds, name) != 0) {
      fprintf(stderr, "Unable to copy string while resolving path");
      controlled_exit(EXIT_FAILURE);
    }
    char *const buf = ds_get_buf(&ds);
    buf[0] = buf[1];
    buf[1] = ':';
    char *const resolved_path = resolve_path(buf);
    ds_free(&ds);
    return resolved_path;
  }

#endif

  /* just try it */
  if (stat(name, &st) == 0)
    return copy(name);

#if !defined(EXT_ASC) && (defined(__MINGW32__) || defined(_MSC_VER))
  wchar_t wname[BSIZE_SP];
  if (MultiByteToWideChar(CP_UTF8, 0, name, -1, wname,
                          2 * (int)strlen(name) + 1) == 0) {
    fprintf(stderr, "UTF-8 to UTF-16 conversion failed with 0x%x\n",
            GetLastError());
    fprintf(stderr, "%s could not be converted\n", name);
    return NULL;
  }
  if (_waccess(wname, 0) == 0)
    return copy(name);
#endif

  return (char *)NULL;
} /* end of function inp_pathresolve */

static char *resolve_input_path(const char *name) {
  /* if name is an absolute path name,
   *   or if we haven't anything to prepend anyway
   */
  if (is_absolute_pathname(name)) {
    return resolve_path(name);
  }

  if (name[0] == '~' && name[1] == '/') {
    char *const y = cp_tildexpand(name);
    if (y) {
      char *const r = resolve_path(y);
      txfree(y);
      return r;
    }
  }

  /* Check for NGSPICE_OSDI_DIR. If available, add file name
     and try to resolve the path */
  char* lbuffer = getenv("NGSPICE_OSDI_DIR");
  if (lbuffer && *lbuffer) {
      DS_CREATE(ds, 100);
      int rc_ds = 0;
      rc_ds |= ds_cat_str(&ds, lbuffer);  /* copy the dir name */
      const size_t n = ds_get_length(&ds); /* end of copied dir name */

      /* Append a directory separator if not present already */
      const char ch_last = n > 0 ? lbuffer[n - 1] : '\0';
      if (ch_last != DIR_TERM
#ifdef _WIN32
          && ch_last != DIR_TERM_LINUX
#endif
          ) {
          rc_ds |= ds_cat_char(&ds, DIR_TERM);
      }
      rc_ds |= ds_cat_str(&ds, name); /* append the file name */

      if (rc_ds != 0) {
          (void)fprintf(cp_err, "Error: Unable to build \"dir\" path name "
              "in inp_pathresolve_at");
          controlled_exit(EXIT_FAILURE);
      }

      char* const r = resolve_path(ds_get_buf(&ds));
      ds_free(&ds);
      if (r)
          return r;
  }

  /*
   * If called from a script inputdir != NULL so try relativ to that dir
   * Otherwise try relativ to the current workdir and relativ to the
   * executables path
   */

  if (inputdir) {
      DS_CREATE(ds, 100);
      int rc_ds = 0;
      rc_ds |= ds_cat_str(&ds, inputdir);  /* copy the dir name */
      const size_t n = ds_get_length(&ds); /* end of copied dir name */

      /* Append a directory separator if not present already */
      const char ch_last = n > 0 ? inputdir[n - 1] : '\0';
      if (ch_last != DIR_TERM
#ifdef _WIN32
          && ch_last != DIR_TERM_LINUX
#endif
          ) {
          rc_ds |= ds_cat_char(&ds, DIR_TERM);
      }
      rc_ds |= ds_cat_str(&ds, name); /* append the file name */

      if (rc_ds != 0) {
          (void)fprintf(cp_err, "Error: Unable to build \"dir\" path name "
              "in inp_pathresolve_at");
          controlled_exit(EXIT_FAILURE);
      }

      char* const r = resolve_path(ds_get_buf(&ds));
      ds_free(&ds);
      if (r)
          return r;
  }

  if (Spice_Exec_Path && *Spice_Exec_Path) {
      DS_CREATE(ds, 100);
      int rc_ds = 0;
      rc_ds |= ds_cat_str(&ds, Spice_Exec_Path);  /* copy the dir name */
      const size_t n = ds_get_length(&ds); /* end of copied dir name */

      /* Append a directory separator if not present already */
      const char ch_last = n > 0 ? Spice_Exec_Path[n - 1] : '\0';
      if (ch_last != DIR_TERM
#ifdef _WIN32
          && ch_last != DIR_TERM_LINUX
#endif
          ) {
          rc_ds |= ds_cat_char(&ds, DIR_TERM);
      }
      rc_ds |= ds_cat_str(&ds, name); /* append the file name */

      if (rc_ds != 0) {
          (void)fprintf(cp_err, "Unable to build \"dir\" path name "
              "in inp_pathresolve_at");
          controlled_exit(EXIT_FAILURE);
      }

      char* const r = resolve_path(ds_get_buf(&ds));
      ds_free(&ds);
      if (r)
          return r;
  }
  /* no inputdir, or not found relative to inputdir:
   search relative to current working directory */
  DS_CREATE(ds, 100);
  if (ds_cat_printf(&ds, ".%c%s", DIR_TERM, name) != 0) {
      (void)fprintf(cp_err,
          "Unable to build \".\" path name in inp_pathresolve_at");
      controlled_exit(EXIT_FAILURE);
  }
  char* const r = resolve_path(ds_get_buf(&ds));
  ds_free(&ds);
  if (r != (char*)NULL) {
      return r;
  }

  return NULL;
} /* end of function inp_pathresolve_at */

static size_t pad_to_align(size_t alignment, size_t size) {
  size_t padding = alignment - size % alignment;
  if (padding == alignment) {
    return size;
  }
  return padding + size;
}
/**
 * Calculates the offset that the OSDI instance data has from the beginning of
 * the instance data allocated by ngspice. This offset is non trivial because
 * ngspice must store the terminal pointers before the remaining instance
 * data. As a result the offset is not constant and a variable amount of
 * padding must be inserted to ensure correct alignment.
 */
static size_t calc_osdi_instance_data_off(const OsdiDescriptor *descr) {
  size_t res = sizeof(GENinstance) /* generic data */
               + descr->num_terminals * sizeof(int);
  // KLU pointers
#ifdef KLU
  res = pad_to_align(alignof(double *), res);
  res += ((size_t)descr->num_jacobian_entries) * 2 * sizeof(double *);
#endif
  // noise values
  res = pad_to_align(alignof(double), res);
  res += NSTATVARS * (descr->num_noise_src + 1) * sizeof(double);
  return pad_to_align(alignof(max_align_t), res);
}

#ifdef KLU
static size_t calc_osdi_instance_matrix_off(const OsdiDescriptor *descr) {
  size_t res = sizeof(GENinstance) /* generic data */
               + descr->num_terminals * sizeof(int);
  return pad_to_align(alignof(double *), res);
}
#endif

static size_t calc_osdi_noise_off(const OsdiDescriptor *descr) {
  size_t res = sizeof(GENinstance) /* generic data */
               + descr->num_terminals * sizeof(int);
  // KLU pointers
#ifdef KLU
  res = pad_to_align(alignof(double *), res);
  res += ((size_t)descr->num_jacobian_entries) * 2 * sizeof(double *);
#endif
  // noise values
  res = pad_to_align(alignof(double), res);
  return res;
}

#define INVALID_OBJECT                                                         \
  (OsdiObjectFile) { .num_entries = -1 }

#define EMPTY_OBJECT                                                           \
  (OsdiObjectFile) {NULL, 0}

#define ERR_AND_RET                                                            \
  error = dlerror();                                                           \
  fprintf(stderr, "Error opening osdi lib \"%s\": %s\n", path, error);                  \
  FREE_DLERR_MSG(error);                                                       \
  return INVALID_OBJECT;

#define STRINGIFY(x) #x
#define TOSTRING(x) STRINGIFY(x)

#define GET_CONST(name, ty)                                                    \
  sym = GET_SYM(handle, STRINGIFY(name));                                      \
  if (!sym) {                                                                  \
    ERR_AND_RET                                                                \
  }                                                                            \
  const ty name = *((ty *)sym);

#define GET_PTR(name, ty)                                                      \
  sym = GET_SYM(handle, STRINGIFY(name));                                      \
  if (!sym) {                                                                  \
    ERR_AND_RET                                                                \
  }                                                                            \
  const ty *name = (ty *)sym;

#define GET_PTR_NOCHECK(name, ty)                                              \
  sym = GET_SYM(handle, STRINGIFY(name));                                      \
  const ty *name = (ty *)sym;

#define INIT_CALLBACK(name, ty)                                                \
  sym = GET_SYM(handle, STRINGIFY(name));                                      \
  if (sym) {                                                                   \
    ty *slot = (ty *)sym;                                                      \
    *slot = name;                                                              \
  }

#define IS_LIM_FUN(fun_name, num_args_, val)                                   \
  if (strcmp(lim_table[i].name, fun_name) == 0) {                              \
    if (lim_table[i].num_args == num_args_) {                                  \
      lim_table[i].func_ptr = (void *)val;                                     \
      continue;                                                                \
    } else {                                                                   \
      expected_args = num_args_;                                               \
    }                                                                          \
  }

static NGHASHPTR known_object_files = NULL;
#define DUMMYDATA ((void *)42)
/**
 * Loads an object file from the hard drive with the platform equivalent of
 * dlopen. This function checks that the OSDI version of the object file is
 * valid and then writes all data into the `registry`.
 * If any errors occur an appropriate message is written to errMsg
 *
 * @param PATH path A path to the shared object file
 * @param uint32_t* len The amount of entries already written into `registry`
 * @param uint32_t* capacity The amount of space available in `registry`
 * before reallocation is required
 * @returns -1 on error, 0 otherwise
 */
extern OsdiObjectFile load_object_file(const char *input) {
  void *handle;
  char *error;
  const void *sym;
  /* ensure the hashtable exists */
  if (!known_object_files) {
    known_object_files = nghash_init_pointer(8);
  }
  const char *path = resolve_input_path(input);
  if (!path) {
    fprintf(stderr, "Error opening osdi lib \"%s\": No such file or directory!\n",
           input);
    return INVALID_OBJECT;
  }

  handle = OPENLIB(path);
  if (!handle) {
    ERR_AND_RET
  }

  /* Keep track of loaded shared object files to avoid loading the same model
   * multiple times. We use the handle as a key because the same SO will always
   * return the SAME pointer as long as dlclose is not called.
   * nghash_insert returns NULL if the key (handle) was not already in the table
   * and the data (DUMMYDATA) that was previously inserted (!= NULL) otherwise*/
  if (nghash_insert(known_object_files, handle, DUMMYDATA)) {
    txfree(path);
    return EMPTY_OBJECT;
  }

  // Try to get OSDI_DESCRIPTOR_SIZE
  GET_PTR_NOCHECK(OSDI_DESCRIPTOR_SIZE, uint32_t);
  size_t descriptor_size;

  // Get major and minor OSDI version  
  GET_CONST(OSDI_VERSION_MAJOR, uint32_t);
  GET_CONST(OSDI_VERSION_MINOR, uint32_t);

  if (OSDI_DESCRIPTOR_SIZE) {
    // This must be openvaf-reloaded 
    // Must be version>=0.4
    if (!(
      (OSDI_VERSION_MAJOR == 0 && OSDI_VERSION_MINOR >= 4) ||
      OSDI_VERSION_MAJOR >= 1
    )) {
      printf("NGSPICE supports OpenVAF-reloaded OSDI version >= 0.4 but \"%s\" uses v%d.%d!",
        path, OSDI_VERSION_MAJOR, OSDI_VERSION_MINOR);
      txfree(path); 
      return INVALID_OBJECT;
    }
    descriptor_size = *OSDI_DESCRIPTOR_SIZE;
  } else {
    // Original OpenVAF, must be version==0.3
    if (OSDI_VERSION_MAJOR != OSDI_VERSION_MAJOR_CURR ||
      OSDI_VERSION_MINOR != OSDI_VERSION_MINOR_CURR) {
      printf("NGSPICE only supports OSDI v%d.%d but \"%s\" uses v%d.%d!",
        OSDI_VERSION_MAJOR_CURR, OSDI_VERSION_MINOR_CURR, path,
        OSDI_VERSION_MAJOR, OSDI_VERSION_MINOR);
      txfree(path);
      return INVALID_OBJECT;
    }
    descriptor_size = sizeof(OsdiDescriptor);
  }
  
  GET_CONST(OSDI_NUM_DESCRIPTORS, uint32_t);
  GET_PTR(OSDI_DESCRIPTORS, OsdiDescriptor);

  INIT_CALLBACK(osdi_log, osdi_log_ptr)

  uint32_t lim_table_len = 0;
  sym = GET_SYM(handle, "OSDI_LIM_TABLE_LEN");
  if (sym) {
    lim_table_len = *((uint32_t *)sym);
  }

  sym = GET_SYM(handle, "OSDI_LIM_TABLE");
  OsdiLimFunction *lim_table = NULL;
  if (sym) {
    lim_table = (OsdiLimFunction *)sym;
  } else {
    lim_table_len = 0;
  }

  for (uint32_t i = 0; i < lim_table_len; i++) {
    int expected_args = -1;
    IS_LIM_FUN("pnjlim", 2, osdi_pnjlim)
    IS_LIM_FUN("limvds", 0, osdi_limvds)
    IS_LIM_FUN("fetlim", 1, osdi_fetlim)
    IS_LIM_FUN("limitlog", 1, osdi_limitlog)
    if (expected_args == -1) {
      printf("warning(osdi): unknown $limit function \"%s\"", lim_table[i].name);
    } else {
      printf("warning(osdi): unexpected number of arguments %i (expected %i) "
             "for \"%s\", ignoring...",
             lim_table[i].num_args, expected_args, lim_table[i].name);
    }
  }

  OsdiRegistryEntry *dst = TMALLOC(OsdiRegistryEntry, OSDI_NUM_DESCRIPTORS);
  
  char* desc_ptr = (char*)OSDI_DESCRIPTORS;
  for (uint32_t i = 0; i < OSDI_NUM_DESCRIPTORS; i++) {
    const OsdiDescriptor *descr = (OsdiDescriptor*)desc_ptr;
    desc_ptr += descriptor_size;
     
    uint32_t dt = descr->num_params + descr->num_opvars;
    bool has_m = false;
    uint32_t temp = descr->num_params + descr->num_opvars + 1;
    for (uint32_t param_id = 0; param_id < descr->num_params; param_id++) {
      OsdiParamOpvar *param = &descr->param_opvar[param_id];
      for (uint32_t j = 0; j < 1 + param->num_alias; j++) {
        char *name = param->name[j];
        if (!strcmp(name, "m")) {
          has_m = true;
        } else if (!strcmp(name, "dt")) {
          dt = UINT32_MAX;
        } else if (!strcasecmp(name, "dtemp") || !strcasecmp(name, "dt")) {
          dt = param_id;
        } else if (!strcmp(name, "temp")) {
          temp = UINT32_MAX;
        } else if (!strcasecmp(name, "temp") ||
                   !strcasecmp(name, "temperature")) {
          temp = param_id;
        }
      }
    }

    size_t inst_off = calc_osdi_instance_data_off(descr);
    size_t noise_off = calc_osdi_noise_off(descr);
    dst[i] = (OsdiRegistryEntry){
        .descriptor = descr,
        .inst_offset = (uint32_t)inst_off,
        .noise_offset = (uint32_t)noise_off,
        .dt = dt,
        .temp = temp,
        .has_m = has_m,

#ifdef KLU
        .matrix_ptr_offset = (uint32_t)calc_osdi_instance_matrix_off(descr),
#endif

    };
  }

  txfree(path);
  return (OsdiObjectFile){
      .entrys = dst,
      .num_entries = (int)OSDI_NUM_DESCRIPTORS,
  };
}

#ifdef KLU
inline size_t osdi_instance_matrix_ptr_off(const OsdiRegistryEntry *entry) {
  return entry->matrix_ptr_offset;
}
inline double **osdi_instance_matrix_ptr(const OsdiRegistryEntry *entry,
                                         GENinstance *inst) {
  return (double **)(((char *)inst) + osdi_instance_matrix_ptr_off(entry));
}
#endif

inline double *osdi_noise_data(const OsdiRegistryEntry *entry,
                                         GENinstance *inst) {
  return (double *)(((char *)inst) + entry->noise_offset);
}

inline size_t osdi_instance_data_off(const OsdiRegistryEntry *entry) {
  return entry->inst_offset;
}

inline void *osdi_instance_data(const OsdiRegistryEntry *entry,
                                GENinstance *inst) {
  return (void *)(((char *)inst) + osdi_instance_data_off(entry));
}

inline OsdiExtraInstData *
osdi_extra_instance_data(const OsdiRegistryEntry *entry, GENinstance *inst) {
  OsdiDescriptor *descr = (OsdiDescriptor *)entry->descriptor;
  return (OsdiExtraInstData *)(((char *)inst) + entry->inst_offset +
                               descr->instance_size);
}

inline size_t osdi_model_data_off(void) {
  return offsetof(OsdiModelData, data);
}

inline void *osdi_model_data(GENmodel *model) {
  return (void *)&((OsdiModelData *)model)->data;
}

inline void *osdi_model_data_from_inst(GENinstance *inst) {
  return osdi_model_data(inst->GENmodPtr);
}

inline OsdiRegistryEntry *osdi_reg_entry_model(const GENmodel *model) {
  return (OsdiRegistryEntry *)ft_sim->devices[model->GENmodType]
      ->registry_entry;
}

inline OsdiRegistryEntry *osdi_reg_entry_inst(const GENinstance *inst) {
  return osdi_reg_entry_model(inst->GENmodPtr);
}

#if defined(__MINGW32__) || defined(HAS_WINGUI) || defined(_MSC_VER)

/* For reporting error message if formatting fails */
static const char errstr_fmt[] =
    "Unable to find message in dlerr(). System code = %lu";
static char errstr[sizeof errstr_fmt - 3 + 3 * sizeof(unsigned long)];

#if !defined (XSPICE)
char *dlerror(void) {
  LPVOID lpMsgBuf;

  DWORD rc = FormatMessage(
      FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
          FORMAT_MESSAGE_IGNORE_INSERTS,
      NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
      (LPTSTR)&lpMsgBuf, 0, NULL);

  if (rc == 0) { /* FormatMessage failed */
    (void)sprintf(errstr, errstr_fmt, (unsigned long)GetLastError());
    return errstr;
  }

  return lpMsgBuf; /* Return the formatted message */
} /* end of function dlerror */
#endif

/* Free message related to dynamic loading */
static void free_dlerr_msg(char *msg) {
  if (msg != errstr) { /* msg is an allocation */
    LocalFree(msg);
  }
} /* end of function free_dlerr_msg */

#endif /* Windows emulation of dlerr */
