/***************************************************************************
                               xyce.cpp
                             ----------------
    begin                : Fri Jan 16 2015
    copyright            : (C) 2015 by Vadim Kuznetsov
    email                : ra3xdh@gmail.com
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/


#include "xyce.h"
#include "components/equation.h"
#include "main.h"
#include "misc.h"
#include "node.h"
#include "wire.h"

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <QScopedPointer>

#include <iostream>

/*!
  \file xyce.cpp
  \brief Implementation of the Xyce class
*/

/*!
 * \brief Xyce::Xyce Class constructor
 * \param schematic Schematic that need to be simulated with Ngspice.
 * \param parent Parent object
 */
Xyce::Xyce(Schematic* schematic, QObject *parent) :
    AbstractSpiceKernel(schematic, parent),
    a_Noisesim(false),
    a_simulationsQueue(),
    a_netlistQueue()
{
    a_simulator_cmd = QucsSettings.XyceExecutable;
}

/*!
 * \brief Xyce::determineUsedSimulations Determine simulation used
 *        in schematic and add them into a_simulationsQueue list
 */
void Xyce::determineUsedSimulations(QStringList *sim_lst)
{

    for(Component *pc : a_schematic->a_DocComps) {
       if(pc->isSimulation && pc->isActive == COMP_IS_ACTIVE) {
           QString sim_typ = pc->Model;
           if (sim_typ==".AC") a_simulationsQueue.append("ac");
           if (sim_typ==".NOISE") a_simulationsQueue.append("noise");
           if (sim_typ==".TR") a_simulationsQueue.append("tran");
           if (sim_typ==".HB") a_simulationsQueue.append("hb");
           if (sim_typ==".SP") a_simulationsQueue.append("sp");
           if (sim_typ==".SENS_XYCE") a_simulationsQueue.append("sens");
           if (sim_typ==".SENS_TR_XYCE") a_simulationsQueue.append("sens_tr");
           if (sim_typ==".XYCESCR") a_simulationsQueue.append(pc->Name); // May be >= XYCE scripts
           if ((sim_typ==".SW")&&
               (pc->Props.at(0)->Value.startsWith("DC"))) a_simulationsQueue.append("dc");
       }
    }

    if (sim_lst != nullptr) {
        *sim_lst = a_simulationsQueue;
    }
}

/*!
 * \brief Xyce::getLabelledNets gets all of the named nets in the schematic,
 * where a named net is either a node or wire with a label.
 * \param[in] dialect SpiceDialect whether or not this is for Xyce/NGSpice
 * \param[out] QSet of named nets
 */
QSet<QString> Xyce::getLabelledNets(spicecompat::SpiceDialect dialect/*=spicecompat::SPICEXyce*/)
{
    if (!a_DC_OP_only) {
        return AbstractSpiceKernel::getLabelledNets(dialect);
    }

    // NOTE: DCOP is a special case since Xyce doesn't have a "print all"
    // Add all remaining nodes, because XYCE has no equivalent for PRINT ALL
    QSet<QString> namedNets;
    for (Node* pn : *a_schematic->a_Nodes) {
        if (pn->Name == "gnd") continue;
        namedNets.insert(pn->Name);
    }
    for (Component *pc : a_schematic->a_DocComps) {
        // Probes
        if (pc->isProbe) {
            namedNets.insert(pc->getProbeVariable(dialect));
        }
        // Add DC sources
        if ((pc->Model == "S4Q_V") || (pc->Model == "Vdc")) {
            namedNets.insert("I("+pc->Name+")");
        }
    }
    return namedNets;
}

/*! \brief Xyce::getActiveLabelledNets gets all of the named nets that is connected to
 * an active or shorted component, in the schematic, where a named net is either a node or wire with a label.
 * \param[in] dialect SpiceDialect whether or not this is for Xyce/NGSpice
 * \param[out] QSet of named active nets
 */
QSet<QString> Xyce::getActiveLabelledNets(spicecompat::SpiceDialect dialect/*=spicecompat::SPICEXyce*/)
{

    if (!a_DC_OP_only) {
        return AbstractSpiceKernel::getActiveLabelledNets(dialect);
    }

    // NOTE: DCOP is a special case since Xyce doesn't have a "print all"
    // Add all node names reachable from active (non-open) components,
    QSet<QString>activeNets;
    for (Component *pc : a_schematic->a_DocComps) {
        // the probe/sources are only valid if active
        if (pc->isActive != COMP_IS_ACTIVE) continue;
        // Probes
        if (pc->isProbe) {
            activeNets.insert(pc->getProbeVariable(dialect));
        }
        // DC sources
        if ((pc->Model == "S4Q_V") || (pc->Model == "Vdc")) {
            activeNets.insert("I("+pc->Name+")");
        }

        // add every node name
        for (Port *pp : pc->Ports) {
            Node *pn = pp->Connection;
            if (!pn || pn->Name == "gnd") continue;
            activeNets.insert(pn->Name);
        }
    }
    return activeNets;
}

/*!
 * \brief Xyce::createNetlist
 * \param[out] stream QTextStream that associated with spice netlist file
 * \param[in] simulations The list of simulations that need to included in netlist.
 * \param[out] vars The list of output variables and node names.
 * \param[out] outputs The list of spice output raw text files.
 */
void Xyce::createNetlist(
        QTextStream& stream,
        QStringList& simulations,
        QStringList& vars,
        QStringList& outputs)
{
    QString s;
    bool hasParSweep = false;

    stream << "* Qucs " << PACKAGE_VERSION << "  " << a_schematic->getDocName() << "\n";
    stream<<collectSpiceLibs(a_schematic); // collect libraries on the top of netlist

    if(!prepareSpiceNetlist(stream)) return; // Unable to perform spice simulation

    startNetlist(stream, spicecompat::SPICEXyce);

    // set variable names for named nodes and wires
    QSet<QString> validNets = getValidNets(spicecompat::SPICEXyce);
    vars = QStringList(validNets.begin(), validNets.end());
    vars.sort();

    //execute simulations

    //QFileInfo inf(a_schematic->getDocName());
    //QString basenam = inf.baseName();
    QString basenam = "spice4qucs";

    QString nods;
    nods.clear();
    for (auto& nod :vars) {
        if (!nod.startsWith("I(")) {
            nods += QStringLiteral("v(%1) ").arg(nod);
        } else {
            nods += nod + " ";
        }
    }

    if (a_DC_OP_only) {
        stream<<".OP\n";
        stream<<QStringLiteral(".PRINT dc format=noindex file=spice4qucs.cir.dc_op_xyce %1\n").arg(nods);
        outputs.append("spice4qucs.cir.dc_op_xyce");
        return;
    }

    // In case we want to save the netlist, the simulations list might be empty,
    // guard against that case.
    QString sim = simulations.isEmpty() ? QString() : simulations.first();
    QStringList spar_vars;
    for(Component *pc : a_schematic->a_DocComps) { // Xyce can run
       if(pc->isSimulation && pc->isActive == COMP_IS_ACTIVE) {                        // only one simulations per time.
           // if we don't have any active simulations, skip the netlisting here
           if (sim.isEmpty()) continue;

           QString sim_typ = pc->Model;              // Multiple simulations are forbidden.
           QString s = pc->getSpiceNetlist(spicecompat::SPICEXyce);
           if ((sim_typ==".AC")&&(sim=="ac")) stream<<s;
           if ((sim_typ==".NOISE")&&(sim=="noise")) stream<<s;
           if ((sim_typ==".SENS_XYCE")&&(sim=="sens")) stream<<s;
           if ((sim_typ==".SENS_TR_XYCE")&&(sim=="sens_tr")) stream<<s;
           if ((sim_typ==".SP")&&(sim=="sp")) {
               spar_vars = pc->getExtraVariables();
               stream<<s;
           }
           if (sim==pc->Name) stream<<s; // Xyce scripts
           if ((sim_typ==".TR")&&(sim=="tran")){
               stream<<s;
               // find Fourier tran
               for(Component *pc1 : a_schematic->a_DocComps) {
                   if (pc1->Model==".FOURIER") {
                       if (pc1->Props.at(0)->Value==pc->Name) {
                           QString s1 = pc1->getSpiceNetlist(spicecompat::SPICEXyce);
                           outputs.append("spice4qucs.tran.cir.four0");
                           stream<<s1;
                       }
                   }
               }
           }
           if ((sim_typ==".HB")&&(sim=="hb")) stream<<s;
           if (sim_typ==".SW") {
               QString SwpSim = pc->Props.at(0)->Value;
               if (SwpSim.startsWith("DC")&&(sim=="dc")) stream<<s;
               else if (SwpSim.startsWith("AC")&&(sim=="ac")) {
                   stream<<s;
                   hasParSweep = true;
               } else if (SwpSim.startsWith("SP")&&(sim=="sp")) {
                   stream<<s;
                   hasParSweep = true;
               } else if (SwpSim.startsWith("TR")&&(sim=="tran")) {
                   stream<<s;
                   hasParSweep = true;
               } else if (SwpSim.startsWith("HB")&&(sim=="hb")) {
                   stream<<s;
                   hasParSweep = true;
               } else if (SwpSim.startsWith("SENS")&&(sim=="sens")) {
                   stream<<s;
                   hasParSweep = true;
               } else if (SwpSim.startsWith("TSENS")&&(sim=="sens_tr")) {
                   stream<<s;
                   hasParSweep = true;
               } else if (SwpSim.startsWith("SW")&&(sim=="dc")) {
                   for(Component *pc1 : a_schematic->a_DocComps) {
                       if ((pc1->Name==SwpSim)&&(pc1->Props.at(0)->Value.startsWith("DC"))) {
                           stream<<s;
                           hasParSweep = true;
                       }
                   }
               }
           }
           if ((sim_typ==".DC")) stream<<s;
       }
    }

    // In the case we have no simulations, end the netlisting here
    if (sim.isEmpty()) {
        stream<<".END\n";
        return;
    }

    if (sim.startsWith("XYCESCR")) {
        for(Component *pc : a_schematic->a_DocComps) {
            if (pc->isSimulation)
                if (sim == pc->Name)
                    outputs.append(pc->Props.at(2)->Value.split(';'));
        }
        stream<<".END\n";
        return;
    }

    QString filename;
    if (hasParSweep) filename = QStringLiteral("%1.%2._swp.plot").arg(basenam).arg(sim);
    else filename = QStringLiteral("%1.%2.plot").arg(basenam).arg(sim);
    filename.remove(QRegularExpression("\\s")); // XYCE don't support spaces and quotes
    QString write_str;
    if (sim=="hb") {
        // write_str = QStringLiteral(".PRINT  %1 file=%2 %3\n").arg(sim).arg(filename).arg(nods);
        write_str = QStringLiteral(".PRINT  %1 %2\n").arg(sim).arg(nods);
        outputs.append("spice4qucs.hb.cir.HB.FD.prn");
    } else if (sim=="noise") {
        filename += "_std";
        write_str = QStringLiteral(".PRINT noise file=%1 inoise onoise\n").arg(filename);
        outputs.append(filename);
    } else if (sim=="sens") {
        write_str.clear();
        outputs.append("spice4qucs.sens.cir.SENS.prn");
    } else if (sim=="sens_tr") {
        write_str.clear();
        outputs.append("spice4qucs.sens_tr.cir.SENS.prn");
        outputs.append("spice4qucs.sens_tr.cir.TRADJ.prn");
    } else if (sim=="sp") {
        write_str = ".PRINT ac format=std file=spice4qucs_sparam.prn ";
        if (hasParSweep) {
            for (const auto &v: spar_vars) { // Bug in Xyce; cannot print Z-par if
                 // .STEP is activated; otherwise simulation error
                if ( !v.startsWith("z(")) write_str += QStringLiteral("%1 ").arg(v);
            }
        } else {
            write_str += spar_vars.join(" ");
        }
        write_str += "\n";
        outputs.append("spice4qucs_sparam.prn");
    } else {
        write_str = QStringLiteral(".PRINT  %1 format=raw file=%2 %3\n").arg(sim).arg(filename).arg(nods);
        outputs.append(filename);
    }
    stream<<write_str;

    stream<<".END\n";
}

/*!
 * \brief Xyce::slotSimulate Execute Xyce simulator and perform all
 *        simulations from the simulationQueue list
 */
void Xyce::slotSimulate()
{

    QStringList incompat;
    bool checker_error = false;
    if (!checkSchematic(incompat)) {
        QString s = incompat.join("; ");
        a_output.append("There were SPICE-incompatible components. Simulator cannot proceed.");
        a_output.append("Incompatible components are: " + s + "\n");
        checker_error = true;
    }

    if (!checkGround()) {
        a_output.append("No Ground found. Please add at least one ground!\n");
        checker_error = true;
    }

    if (!checkDCSimulation()) {
        a_output.append("Only DC simulation found in the schematic. It has no effect!"
                      " Add TRAN, AC, or Sweep simulation to proceed.\n");
        checker_error = true;
    }

    if (checker_error) {
        if (a_console != nullptr)
            a_console->insertPlainText(a_output);
        //emit finished();
        emit errors(QProcess::FailedToStart);
        return;
    }

    a_netlistQueue.clear();
    a_output_files.clear();

    if (a_DC_OP_only) {
        a_simulationsQueue.append("dc");
    } else  determineUsedSimulations();

    QFile::remove(a_workdir+"spice4qucs.sens_tr.cir.SENS.prn");
    QFile::remove(a_workdir+"spice4qucs.sens_tr.cir.TRADJ.prn");

    for (const QString& sim : a_simulationsQueue) {
        QStringList sim_lst;
        sim_lst.clear();
        sim_lst.append(sim);
        QString tmp_path = QDir::toNativeSeparators(a_workdir+"/spice4qucs."+sim+".cir");
        a_netlistQueue.append(tmp_path);
        QFile spice_file(tmp_path);
        if (spice_file.open(QFile::WriteOnly)) {
            QTextStream stream(&spice_file);
            createNetlist(stream,sim_lst,a_vars,a_output_files);
            spice_file.close();
        }
    }

    a_output.clear();
    emit started();
    nextSimulation();

}

/*!
 * \brief Xyce::SaveNetlist Save netlist into specified file without
 *        execution of simulator.
 * \param[in] filename The name of file in which netlist is saved
 * \param[in] netlist2Console Whether netlist to console instead to file
 */
void Xyce::SaveNetlist(QString filename, bool netlist2Console)
{
    determineUsedSimulations();

    QScopedPointer<QString> netlistString;
    QScopedPointer<QTextStream> netlistStream;
    QScopedPointer<QFile> netlistFile;

    if (netlist2Console)
    {
        netlistString.reset(new QString);
        netlistStream.reset(new QTextStream(netlistString.get()));
    }
    else
    {
        netlistFile.reset(new QFile(filename));

        if (netlistFile->open(QFile::WriteOnly))
        {
            netlistStream.reset(new QTextStream(netlistFile.get()));
        }
    }

    createNetlist(*netlistStream, a_simulationsQueue, a_vars, a_output_files);

    if (netlist2Console)
    {
        std::cout << netlistString->toUtf8().constData() << std::endl;
    }
}

/*!
 * \brief Xyce::slotFinished Simulator finished handler. End simulation or
 *        execute the next simulation from queue.
 */
void Xyce::slotFinished()
{
    a_output += a_simProcess->readAllStandardOutput();;

    if (a_Noisesim) {
        QFile logfile(a_workdir + QDir::separator() + "spice4qucs.noise_log");
        if (logfile.open(QIODevice::WriteOnly)) {
            QTextStream ts(&logfile);
            ts<<a_output;
            logfile.close();
        }
        a_Noisesim = false;
        a_output_files.append("spice4qucs.noise_log");
    }

    // In case of power sweeps, modify the .res file to convert the voltage sweep into a power sweep
    overwriteResFileWithPowerValues();

    if (a_netlistQueue.isEmpty()) {
        emit finished();
        emit progress(100);
        return;
    } else {
        nextSimulation();
    }
}

bool Xyce::waitEndOfSimulation()
{
    bool ok = false;
    while (!a_netlistQueue.isEmpty()) {
        ok = a_simProcess->waitForFinished(10000);
    }
    ok = a_simProcess->waitForFinished(10000);
    return ok;
}

/*!
 * \brief Xyce::slotProcessOutput Process Xyce output and report progress.
 */
void Xyce::slotProcessOutput()
{
    //***** Percent complete: 85.4987 %
    QString s = a_simProcess->readAllStandardOutput();
    if (s.contains("Percent complete:")) {
        int percent = round(s.section(' ',3,3,QString::SectionSkipEmpty).toFloat());
        emit progress(percent);
    }
    a_output += s;
    if (a_console != nullptr) {
        a_console->insertPlainText(s);
        a_console->moveCursor(QTextCursor::End);
    }
}

/*!
 * \brief Xyce::nextSimulation Execute the next simulation from queue.
 */
void Xyce::nextSimulation()
{
    if (!a_netlistQueue.isEmpty()) {
        QString file = a_netlistQueue.takeFirst();
        if (file.endsWith(".noise.cir")) a_Noisesim = true;
        a_simProcess->setWorkingDirectory(a_workdir);
        QString cmd = QStringLiteral("%1 %2 \"%3\"").arg(a_simulator_cmd,a_simulator_parameters,file);
        QStringList cmd_args = misc::parseCmdArgs(cmd);
        QString xyce_cmd = cmd_args.at(0);
        cmd_args.removeAt(0);
        a_simProcess->start(xyce_cmd,cmd_args);
    } else {
        a_output += "No simulation found. Please add at least one simulation!\n"
                  "Navigate to the \"simulations\" group in the components panel (left)"
                  " and drag simulation to the schematic sheet. Then define its parameters.\n"
                  "Exiting...\n";
        emit progress(100);
        emit finished(); // nothing to simulate
    }
}

void Xyce::setParallel(bool par)
{
    if (par) {
        QString xyce_par = QucsSettings.XyceParExecutable;
        xyce_par.replace("%p",QString::number(QucsSettings.NProcs));
        a_simulator_cmd = xyce_par;
        a_simulator_parameters = a_simulator_parameters + QStringLiteral(" -a ");
    } else {
        a_simulator_cmd = "\"" + QucsSettings.XyceExecutable + "\"";
        a_simulator_parameters = a_simulator_parameters + " -a ";
    }
}

void Xyce::overwriteResFileWithPowerValues()
{
  for (Component *pc : a_schematic->a_DocComps) {
    if (pc->Model != ".SW" || pc->isActive != COMP_IS_ACTIVE) continue;

    // Resolve the swept parameter to a schematic component
    // The Param field may contain either a component name (e.g. "P1")
    // or a symbolic .PARAM variable name shared by one or more Pac sources
    // (e.g. "Pin" where P1 has P="Pin").
    QString par = pc->getProperty("Param")->Value.trimmed();
    Component *target = a_schematic->getComponentByName(par);
    bool is_pac = (target != nullptr && target->Model == "Pac");
    if (!is_pac) {
      for (Component *c : *a_schematic->a_Components) {
        if (c->Model == "Pac" &&
            c->getProperty("P")->Value.trimmed() == par) {
          target = c;
          is_pac = true;
          break;
        }
      }
    }

    // Skip if the sweep target is not an AC power source
    if (!is_pac) continue;

    // Collect the original sweep values in dBm from the sweep component
    QString type = pc->getProperty("Type")->Value;
    QStringList dbm_values;

    if (type == "list" || type == "const") {
      QString list_str = pc->getProperty("Values")->Value;
      list_str.remove('[').remove(']');
      dbm_values = list_str.split(';');
    } else {
      // Enumerate lin/log points from Start/Stop/Points properties
      double start, stop, points, fac;
      QString unit;
      misc::str2num(pc->getProperty("Start")->Value, start, unit, fac); start *= fac;
      misc::str2num(pc->getProperty("Stop")->Value,  stop,  unit, fac); stop  *= fac;
      misc::str2num(pc->getProperty("Points")->Value, points, unit, fac); points *= fac;
      double step = (stop - start) / (points - 1);
      for (int i = 0; i < (int)points; ++i)
        dbm_values << QString::number(start + i * step, 'g', 8);
        }

    // Get the type of simulation and adjust the name of the .res file according to that
    QString swp_sim;
    Component *sim_pc = a_schematic->getComponentByName(pc->getProperty("Sim")->Value);
    if (sim_pc != nullptr) {
      QString sim_typ = sim_pc->Model;
      if (sim_typ == ".TR"){   swp_sim = "tran";
      } else if (sim_typ == ".AC"){
          swp_sim = "ac";
      } else if (sim_typ == ".SP"){
        swp_sim = "sp";
      } else if (sim_typ == ".HB"){
        swp_sim = "hb";
      } else if (sim_typ == ".DC"){
        swp_sim = "dc";
      } else {
        swp_sim = sim_typ.toLower().remove('.');
      }
    }

    // Overwrite the .res file with the original dBm values so that
    // convertToQucsData() uses power as the sweep axis in the dataset
    QString res_path = QDir::toNativeSeparators(
        a_workdir + QDir::separator() + "spice4qucs." + swp_sim + ".cir.res");

    QFile res_file(res_path);
    if (res_file.open(QFile::WriteOnly)) {
      QTextStream rs(&res_file);
      rs << "STEP                " << target->Name << ":P\n";
      for (int i = 0; i < dbm_values.count(); ++i)
        rs << QString::number(i) << "          " << dbm_values.at(i).trimmed() << "\n";
            res_file.close();
    }
    break;
  }
}
