/*
 * This file is part of the OSDI component of NGSPICE.
 * Copyright© 2022 SemiMod GmbH.
 * 
 * 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/iferrmsg.h"
#include "ngspice/memory.h"
#include "ngspice/ngspice.h"
#include "ngspice/typedefs.h"

#include "osdi.h"
#include "osdidefs.h"

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>

#define NUM_SIM_PARAMS 10
char *sim_params[NUM_SIM_PARAMS + 1] = {
    "iniLim", "gmin", "gdev", "tnom", 
    "simulatorVersion", "sourceScaleFactor", 
    "epsmin", "reltol", "vntol", "abstol", 
    NULL};
char *sim_params_str[1] = {NULL};

double sim_param_vals[NUM_SIM_PARAMS] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

/* values returned by $simparam*/
OsdiSimParas get_simparams(const CKTcircuit *ckt) {
  double simulatorVersion = strtod(PACKAGE_VERSION, NULL);
  double gdev = ckt->CKTgmin;
  double sourceScaleFactor = ckt->CKTsrcFact;
  double gmin = ((ckt->CKTgmin) > (ckt->CKTdiagGmin)) ? (ckt->CKTgmin)
                                                      : (ckt->CKTdiagGmin);
  double initializeLimiting = (ckt->CKTmode & MODEINITJCT) ? 1 : 0;

  double sim_param_vals_[NUM_SIM_PARAMS] = {
      // Verilog-A tnom is in degrees Celsius
      initializeLimiting, gmin, gdev, ckt->CKTnomTemp-CONSTCtoK, simulatorVersion, sourceScaleFactor, 
      ckt->CKTepsmin, ckt->CKTreltol, ckt->CKTvoltTol, ckt->CKTabstol };
  memcpy(&sim_param_vals, &sim_param_vals_, sizeof(double) * NUM_SIM_PARAMS);
  OsdiSimParas sim_params_ = {.names = sim_params,
                              .vals = (double *)&sim_param_vals,
                              .names_str = sim_params_str,
                              .vals_str = NULL};
  return sim_params_;
}

static void eval(const OsdiDescriptor *descr, const GENinstance *gen_inst,
                 void *inst, OsdiExtraInstData *extra_inst_data,
                 const void *model, const OsdiSimInfo *sim_info) {

  OsdiNgspiceHandle handle =
      (OsdiNgspiceHandle){.kind = 3, .name = gen_inst->GENname};
  /* TODO initial conditions? */
  extra_inst_data->eval_flags = descr->eval(&handle, inst, model, sim_info);
}

static void load(CKTcircuit *ckt, const GENinstance *gen_inst, void *model,
                 void *inst, OsdiExtraInstData *extra_inst_data, bool is_tran,
                 bool is_init_tran, const OsdiDescriptor *descr) {

  NG_IGNORE(extra_inst_data);

  double dump;
  if (is_tran) {
    /* load dc matrix and capacitances (charge derivative multiplied with
     * CKTag[0]) */
    descr->load_jacobian_tran(inst, model, ckt->CKTag[0]);

    /* load static rhs and dynamic linearized rhs (SUM Vb * dIa/dVb)*/
    descr->load_spice_rhs_tran(inst, model, ckt->CKTrhs, ckt->CKTrhsOld,
                               ckt->CKTag[0]);

    uint32_t *node_mapping =
        (uint32_t *)(((char *)inst) + descr->node_mapping_offset);

    /* use numeric integration to obtain the remainer of the RHS*/
    int state = gen_inst->GENstate + (int)descr->num_states;
    for (uint32_t i = 0; i < descr->num_nodes; i++) {
      if (descr->nodes[i].react_residual_off != UINT32_MAX) {

        double residual_react =
            *((double *)(((char *)inst) + descr->nodes[i].react_residual_off));

        /* store charges in state vector*/
        ckt->CKTstate0[state] = residual_react;
        if (is_init_tran) {
          ckt->CKTstate1[state] = residual_react;
        }

        /* we only care about the numeric integration itself not ceq/geq
        because those are already calculated by load_jacobian_tran and
        load_spice_rhs_tran*/
        NIintegrate(ckt, &dump, &dump, 0, state);

        /* add the numeric derivative to the rhs */
        ckt->CKTrhs[node_mapping[i]] -= ckt->CKTstate0[state + 1];

        if (is_init_tran) {
          ckt->CKTstate1[state + 1] = ckt->CKTstate0[state + 1];
        }

        state += 2;
      }
    }
  } else {
    /* copy internal derivatives into global matrix */
    descr->load_jacobian_resist(inst, model);

    /* calculate spice RHS from internal currents and store into global RHS
     */
    descr->load_spice_rhs_dc(inst, model, ckt->CKTrhs, ckt->CKTrhsOld);
  }
}

extern int OSDIload(GENmodel *inModel, CKTcircuit *ckt) {
  GENmodel *gen_model;
  GENinstance *gen_inst;

  bool is_init_smsig = ckt->CKTmode & MODEINITSMSIG;
  bool is_dc = ckt->CKTmode & (MODEDCOP | MODEDCTRANCURVE);
  bool is_ac = ckt->CKTmode & (MODEAC | MODEINITSMSIG);
  bool is_tran = ckt->CKTmode & (MODETRAN);
  bool is_tran_op = ckt->CKTmode & (MODETRANOP);
  bool is_init_tran = ckt->CKTmode & MODEINITTRAN;
  bool is_init_junc = ckt->CKTmode & MODEINITJCT;

  OsdiSimInfo sim_info = {
      .paras = get_simparams(ckt),
      .abstime = is_tran ? ckt->CKTtime : 0.0,
      .prev_solve = ckt->CKTrhsOld,
      .prev_state = ckt->CKTstates[0],
      .next_state = ckt->CKTstates[0],
      .flags = CALC_RESIST_JACOBIAN,
  };

  sim_info.flags |= CALC_OP;

  if (is_dc) {
    sim_info.flags |= ANALYSIS_DC | ANALYSIS_STATIC;
  }

  if (!is_init_smsig) {
    sim_info.flags |= CALC_RESIST_RESIDUAL | ENABLE_LIM | CALC_RESIST_LIM_RHS;
  }

  if (is_tran) {
    sim_info.flags |= CALC_REACT_JACOBIAN | CALC_REACT_RESIDUAL |
                      CALC_REACT_LIM_RHS | ANALYSIS_TRAN;
  }

  if (is_tran_op) {
    sim_info.flags |= ANALYSIS_TRAN;
  }

  if (is_ac) {
    sim_info.flags |= CALC_REACT_JACOBIAN | ANALYSIS_AC;
  }

  if (is_init_tran) {
    sim_info.flags |= ANALYSIS_IC | ANALYSIS_STATIC;
  }

  if (is_init_junc) {
    sim_info.flags |= INIT_LIM;
  }

  if (ckt->CKTmode & MODEACNOISE) {
    sim_info.flags |= ANALYSIS_NOISE;
  }
  sim_info.flags |= CALC_NOISE;

  /* bypass: let a device reuse its last operating point when its terminal
   * voltages barely changed (consistent with native bypassing devices).
   * Not during init/predictor steps, where a full evaluation is required. */
  if (ckt->CKTbypass &&
      !(ckt->CKTmode & (MODEINITPRED | MODEINITJCT | MODEINITTRAN |
                        MODEINITFIX | MODEINITSMSIG))) {
    sim_info.flags |= ENABLE_BYPASS;
  }

  OsdiRegistryEntry *entry = osdi_reg_entry_model(inModel);
  const OsdiDescriptor *descr = entry->descriptor;
  uint32_t eval_flags = 0;

#ifdef USE_OMP
  int ret = OK;

  /* use openmp 3.0 tasks to parallelize linked list transveral */
#pragma omp parallel
#pragma omp single
  {
    for (gen_model = inModel; gen_model; gen_model = gen_model->GENnextModel) {
      void *model = osdi_model_data(gen_model);

      for (gen_inst = gen_model->GENinstances; gen_inst;
           gen_inst = gen_inst->GENnextInstance) {

        void *inst = osdi_instance_data(entry, gen_inst);

        OsdiExtraInstData *extra_inst_data =
            osdi_extra_instance_data(entry, gen_inst);

#pragma omp task firstprivate(gen_inst, inst, extra_inst_data, model)
        eval(descr, gen_inst, inst, extra_inst_data, model, &sim_info);
      }
    }
  }

  /* init small signal analysis does not require loading values into
   * matrix/rhs*/
  if (is_init_smsig) {
    return ret;
  }

  for (gen_model = inModel; gen_model; gen_model = gen_model->GENnextModel) {
    void *model = osdi_model_data(gen_model);

    for (gen_inst = gen_model->GENinstances; gen_inst;
         gen_inst = gen_inst->GENnextInstance) {
      void *inst = osdi_instance_data(entry, gen_inst);
      OsdiExtraInstData *extra_inst_data =
          osdi_extra_instance_data(entry, gen_inst);
      load(ckt, gen_inst, model, inst, extra_inst_data, is_tran, is_init_tran,
           descr);
      eval_flags |= extra_inst_data->eval_flags;
    }
  }
#else
  for (gen_model = inModel; gen_model; gen_model = gen_model->GENnextModel) {
    void *model = osdi_model_data(gen_model);

    for (gen_inst = gen_model->GENinstances; gen_inst;
         gen_inst = gen_inst->GENnextInstance) {
      void *inst = osdi_instance_data(entry, gen_inst);

      OsdiExtraInstData *extra_inst_data =
          osdi_extra_instance_data(entry, gen_inst);
      eval(descr, gen_inst, inst, extra_inst_data, model, &sim_info);

      /* init small signal analysis does not require loading values into
       * matrix/rhs*/
      if (!is_init_smsig) {
        load(ckt, gen_inst, model, inst, extra_inst_data, is_tran, is_init_tran,
             descr);
        eval_flags |= extra_inst_data->eval_flags;
      }
    }
  }
#endif

  /* call to $fatal in Verilog-A abort simulation!*/
  if (eval_flags & EVAL_RET_FLAG_FATAL) {
    return E_PANIC;
  }

  if (eval_flags & EVAL_RET_FLAG_LIM) {
    ckt->CKTnoncon++;
    ckt->CKTtroubleElt = gen_inst;
  }

  if (eval_flags & EVAL_RET_FLAG_STOP) {
    return E_PAUSE;
  }

  return OK;
}
