/**
 * @file graph_io.cpp
 * @brief Implements graph file load/save wrapper methods for the Graph class
 *        (load pipeline coordination, parser thread termination, format writers).
 * @author Dimitris B. Kalamaras
 * @copyright
 *   Copyright (C) 2005-2026 by Dimitris B. Kalamaras.
 *   This file is part of SocNetV (Social Network Visualizer).
 * @license
 *   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, version 3 or later.
 *   For more details, see <http://www.gnu.org/licenses/>.
 * @see https://socnetv.org
 */

#include "graph.h"
#include <QDebug>
#include <QFile>
#include <QFileInfo>
#include <QRegularExpression>
#include <QTextStream>
#include <QDir>
#include <QStringEncoder>
#include "graph/io/graph_parse_sink_graph.h"
#include <memory>

/**
 * @brief Loads a graph from a given file.
 *
 * It creates a new Parser object, moves it to a another thread,
 * connects signals and slots and calls its run() method.
 *
 * @param fileName
 * @param codecName
 * @param m_showLabels
 * @param maxWidth
 * @param maxHeight
 * @param fileFormat
 * @param sm_two_mode
 * @return
 */
void Graph::loadFile(const QString fileName,
                     const QString codecName,
                     const int fileFormat,
                     const QString delimiter,
                     const int sm_two_mode,
                     const bool sm_has_labels)
{

    qDebug() << "Loading the file:" << fileName;

    qDebug() << "First, clearing current relations...";
    relationsClear();

    qDebug() << "Next, creating new file_parser -- we are on thread:" << this->thread();
    file_parser = new Parser();

    qDebug() << " moving parser to her own new thread...";
    file_parser->moveToThread(&file_parserThread);

    qDebug() << "file_parser thread now: " << file_parser->thread();

    qDebug() << "connecting file_parser signals...";

    connect(&file_parserThread, &QThread::finished,
            file_parser, &QObject::deleteLater);

    // NEW: GUI uses sink-backed mutation (Parser owns the sink for thread-safe lifetime)
    file_parser->setOwnedParseSink(
        std::make_unique<SocNetV::IO::GraphParseSinkGraph>(*this));

    connect(file_parser, &Parser::finished,
            this, &Graph::graphLoadedTerminateParserThreads);

    qDebug() << "Starting parser thread...";
    file_parserThread.start();

    qDebug() << "Calling the file_parser to load the file...";
    file_parser->load(
        fileName,
        codecName,
        initVertexSize,
        initVertexColor,
        initVertexShape,
        initVertexNumberColor,
        initVertexNumberSize,
        initVertexLabelColor,
        initVertexLabelSize,
        initEdgeColor,
        canvasWidth,
        canvasHeight,
        fileFormat,
        delimiter,
        sm_two_mode,
        sm_has_labels);
}

/**
 * @brief Graph::graphLoadedTerminateParserThreads
 * @param reason
 */
void Graph::graphLoadedTerminateParserThreads(QString reason)
{
    qDebug() << "Terminating parser threads - reason " << reason
             << " Checking if file_parserThread is running...";
    if (file_parserThread.isRunning())
    {
        qDebug() << "deleting file_parser pointer";
        delete file_parser;
        file_parser = 0; // see why here: https://goo.gl/tQxpGA

        qDebug() << "file_parserThread running."
                    "Calling file_parserThread.quit();";
        file_parserThread.quit();
    }
}

/**
 * @brief Stores loaded file name, graph name, sets edge direction type and signals MW to update the UI
 *
 * Called from Parser when file parsing ends.
 *
 * @param fileType
 * @param fileName
 * @param netName
 * @param totalNodes
 * @param totalLinks
 * @param edgeDirType
 * @param elapsedTime
 * @param message
 */
void Graph::graphFileLoaded(const int &fileType,
                            const QString &fileName,
                            const QString &netName,
                            const int &totalNodes,
                            const int &totalLinks,
                            const int &edgeDirType,
                            const qint64 &elapsedTime,
                            const QString &message)
{
    if (fileType == FileType::UNRECOGNIZED)
    {
        emit signalGraphLoaded(fileType, QString(), QString(), 0, 0, 0, elapsedTime, message);
        return;
    }

    qDebug() << "Loaded file OK."
             << "type:" << fileType
             << "filename:" << fileName
             << "nodes:" << totalNodes
             << "links (parser count):" << totalLinks
             << "edgeDirType:" << edgeDirType
             << "isDirected (currently):" << isDirected();

    setFileName(fileName);
    if (!netName.isEmpty())
        setName(netName);
    else
        setName((fileName.split("/").last()).split("/").first());

    if (edgeDirType == EdgeType::Directed)
        this->setDirected(true);
    else
        this->setDirected(false);

    setFileFormat(fileType);
    setModStatus(ModStatus::SavedUnchanged);

    qDebug() << "graphFileLoaded: after setDirected/setUndirected:"
             << "isDirected" << isDirected()
             << "isUndirected" << isUndirected()
             << "m_totalEdges" << m_totalEdges
             << "calculatedEdges" << calculatedEdges;

    // Do not trust totalLinks from the parser.
    //
    // The parser accumulates totalLinks naively per section: each *Arcs line
    // adds 1, each *Edges line adds 2. In mixed Pajek files where the same
    // node pair appears in both *Arcs and *Edges sections, those pairs are
    // counted twice, inflating the total. (See issue #183.)
    //
    // edgesEnabled() counts from the actual adjacency (outEdgesCount() per
    // vertex) which is always correct regardless of file format or section
    // overlap. calculatedEdges is false at this point (invalidated by each
    // edgeCreate() call during parsing via setModStatus(EdgeCount)), so
    // edgesEnabled() will perform a fresh recount here.
    int actualLinks = edgesEnabled();

    if (actualLinks != totalLinks)
    {
        qDebug() << "WARNING: Parser totalLinks" << totalLinks
                 << "differs from actual adjacency count" << actualLinks
                 << "- using actual count."
                 << "This typically indicates overlapping *Arcs/*Edges pairs"
                 << "in a mixed Pajek file (see issue #183).";
    }

    emit signalGraphLoaded(fileType,
                           fileName,
                           getName(),
                           totalNodes,
                           actualLinks,
                           graphDensity(),
                           elapsedTime,
                           message);
}

/**
 * @brief Saves the current graph to a file.
 *
 * Checks the requested file type and calls the corresponding saveGraphTo...() method
 *
 * @param fileName
 * @param fileType
 *
 */
void Graph::saveToFile(const QString &fileName,
                       const int &fileType,
                       const bool &saveEdgeWeights,
                       const bool &saveZeroWeightEdges)
{
    qDebug() << "Saving current graph to file named:" << fileName;
    bool saved = false;
    switch (fileType)
    {
    case FileType::PAJEK:
    {
        saved = saveToPajekFormat(fileName, getName(), canvasWidth, canvasHeight);
        break;
    }
    case FileType::ADJACENCY:
    {
        saved = saveToAdjacencyFormat(fileName, saveEdgeWeights);
        break;
    }
    case FileType::GRAPHVIZ:
    {
        saved = saveToDotFormat(fileName);
        break;
    }
    case FileType::GRAPHML:
    {
        saved = saveToGraphMLFormat(fileName, saveZeroWeightEdges);
        break;
    }
    case FileType::UCINET:
    {
        saved = saveToUCINETDLFormat(fileName);
        break;
    }
    case FileType::EDGELIST_WEIGHTED:
    {
        saved = saveToEdgeListWeightedFormat(fileName);
        break;
    }
    case FileType::EDGELIST_SIMPLE:
    {
        saved = saveToEdgeListSimpleFormat(fileName);
        break;
    }
    default:
    {
        setFileFormat(FileType::UNRECOGNIZED);
        break;
    }
    };
    if (saved)
    {
        setModStatus(ModStatus::SavedUnchanged);
    }
    else
    {
        emit signalGraphSavedStatus(FileType::UNRECOGNIZED);
    }
}

/**
 * @brief Save the current graph to a Pajek (.paj) file.
 *
 * Writes:
 *  - `*Network <name>`
 *  - `*Vertices N` with labels, colors, positions, and shapes (as available)
 *  - Edges either as:
 *      - single-relation: `*Arcs` + `*Edges` sections (legacy behavior), or
 *      - multi-relation: one `*Matrix` block per relation.
 *
 * Relation header export (multi-relation):
 *  - Canonical matrix header syntax:
 *      - `*Matrix :k`
 *      - `*Matrix :k "Label"`
 *  - If the relation label is empty/unlabeled, no quotes are emitted.
 *  - Labels are normalized to avoid polluted forms like `9 'star'`:
 *      - strips wrapping quotes
 *      - strips a leading index token (e.g. `9`, `9:`)
 *      - escapes internal quotes per Pajek convention (`"` -> `""`)
 *
 * Notes:
 *  - Numeric output is written using the C locale for stable decimal formatting.
 *  - `maxWidth`/`maxHeight` are used to scale vertex coordinates to Pajek's
 *    normalized coordinate space. If 0, current canvas dimensions are used.
 *  - The active relation is restored before returning.
 *  - On success, updates the graph’s file name and file format metadata.
 *
 * @param fileName Output file path (should end in .paj).
 * @param networkName Optional exported network name. If empty, uses graph name.
 *                    If "unnamed", derives from file name without extension.
 * @param maxWidth  Canvas width used to normalize X coordinates (0 => use current).
 * @param maxHeight Canvas height used to normalize Y coordinates (0 => use current).
 *
 * @return true on success, false if the file cannot be opened/written.
 */
bool Graph::saveToPajekFormat(const QString &fileName,
                              QString networkName,
                              int maxWidth,
                              int maxHeight)
{
    qDebug() << "Saving graph to Pajek-formatted file:" << fileName;

    qreal weight = 0;
    QFileInfo fileInfo(fileName);
    QString fileNameNoPath = fileInfo.fileName();

    networkName = (networkName == "") ? getName().toHtmlEscaped() : networkName;
    networkName = (networkName == "unnamed")
                      ? fileNameNoPath.toHtmlEscaped().left(fileNameNoPath.lastIndexOf('.'))
                      : networkName;

    maxWidth = (maxWidth == 0) ? canvasWidth : maxWidth;
    maxHeight = (maxHeight == 0) ? canvasHeight : maxHeight;

    QFile f(fileName);
    if (!f.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        qDebug() << "Could not open (for writing) file:" << fileName;
        progressStatus(tr("Error. Could not write to ") + fileName);
        return false;
    }

    QTextStream t(&f);
    // Keep numeric output stable-ish across locales.
    t.setLocale(QLocale::c());
    t.setRealNumberNotation(QTextStream::SmartNotation);
    t.setRealNumberPrecision(12);

    t << "*Network " << networkName << "\n";

    // ---- Vertices (once) ----
    t << "*Vertices " << vertices() << "\n";
    for (VList::const_iterator it = m_graph.cbegin(); it != m_graph.cend(); ++it)
    {
        t << (*it)->number() << " " << "\"" << (*it)->label() << "\"";
        t << " ic " << (*it)->colorToPajek();
        t << "\t\t" << (*it)->x() / (maxWidth) << " \t" << (*it)->y() / (maxHeight);
        t << "\t" << (*it)->shape();
        t << "\n";
    }

    // ---- Relations ----
    const int relCount = relations();
    const int originalRel = relationCurrent();

    /**
     *  @brief Returns a canonical label for Pajek *Matrix headers (no surrounding quotes).
     *
     * Pajek *Matrix Header Export Contract
     *
     * EXPORT IS STRICT AND CANONICAL.
     *
     * Multirelational headers MUST be written in exactly one of these forms:
     *
     *   *Matrix :k
     *   *Matrix :k "Label"
     *
     * Where:
     *   - k is the 1-based relation index.
     *   - No leading index is embedded in the label.
     *   - No empty quotes are emitted.
     *   - No alternate forms like "*Matrix k:", "*Matrix k: """, etc.
     *
     * Relation labels are normalized before export:
     *   - Strip wrapping quotes.
     *   - Strip leading index pollution (e.g. "9 'star'" → "star").
     *   - Treat empty or index-only labels as unlabeled.
     *   - Escape internal double quotes as "" (Pajek rule).
     *
     * IMPORT remains tolerant. EXPORT remains canonical.
     */
    auto canonicalPajekRelationName = [](QString s, int k) -> QString
    {
        s = s.trimmed();
        const QString kStr = QString::number(k);

        auto stripWrapQuotes = [](QString x) -> QString
        {
            x = x.trimmed();
            if (x.size() >= 2)
            {
                const QChar a = x.front();
                const QChar b = x.back();
                if ((a == '"' && b == '"') || (a == '\'' && b == '\''))
                    x = x.mid(1, x.size() - 2).trimmed();
            }
            return x;
        };

        s = stripWrapQuotes(s);

        // Remove leading ":k"
        if (s.startsWith(':'))
            s = s.mid(1).trimmed();

        // Remove leading "k" or "k:"
        if (s.startsWith(kStr))
        {
            s = s.mid(kStr.size()).trimmed();
            if (s.startsWith(':'))
                s = s.mid(1).trimmed();
            s = stripWrapQuotes(s);
        }

        // Unlabeled cases
        if (s.isEmpty() || s == kStr)
            return QString();

        // Escape internal quotes per Pajek rules
        s.replace("\"", "\"\"");

        return s.trimmed();
    };

    // Multirelational: write Pajek *Matrix blocks per relation (like Padgett, etc.)
    if (relCount > 1)
    {
        for (int r = 0; r < relCount; ++r)
        {
            relationSet(r, false /*updateUI*/);

            const int k = r + 1;
            const QString relLabel = canonicalPajekRelationName(relationCurrentName(), k);

            // Canonical header:
            //   *Matrix :k
            //   *Matrix :k "label"
            t << "*Matrix :" << k;
            if (!relLabel.isEmpty())
                t << " \"" << relLabel << "\"";
            t << "\n";

            // Emit N x N matrix
            for (VList::const_iterator it = m_graph.cbegin(); it != m_graph.cend(); ++it)
            {
                const int i = (*it)->number();

                bool first = true;
                for (VList::const_iterator jt = m_graph.cbegin(); jt != m_graph.cend(); ++jt)
                {
                    const int j = (*jt)->number();

                    if (isDirected())
                        weight = edgeExists(i, j); // directed lookup
                    else
                        weight = edgeExists(i, j, true); // undirected lookup

                    // Pajek expects space-separated row entries
                    if (!first)
                        t << " ";
                    first = false;

                    // Print 0 for no edge. Otherwise print numeric weight.
                    // (Unweighted relations usually store 1.)
                    if (weight == 0)
                        t << "0";
                    else
                        t << weight;
                }
                t << "\n";
            }
        }
    }
    else
    {
        // Single relation: keep existing arcs/edges output to minimize behavior change.
        t << "*Arcs \n";
        for (VList::const_iterator it = m_graph.cbegin(); it != m_graph.cend(); ++it)
        {
            for (VList::const_iterator jt = m_graph.begin(); jt != m_graph.end(); jt++)
            {
                if ((weight = edgeExists((*it)->number(), (*jt)->number())) != 0 &&
                    (edgeExists((*jt)->number(), (*it)->number())) != weight)
                {
                    t << (*it)->number() << " " << (*jt)->number() << " " << weight;
                    t << " c " << (*it)->outLinkColor((*jt)->number());
                    t << "\n";
                }
            }
        }

        t << "*Edges \n";
        for (VList::const_iterator it = m_graph.cbegin(); it != m_graph.cend(); ++it)
        {
            for (VList::const_iterator jt = m_graph.begin(); jt != m_graph.end(); jt++)
            {
                if ((weight = edgeExists((*it)->number(), (*jt)->number(), true)) != 0)
                {
                    if ((*it)->number() > (*jt)->number())
                        continue;
                    t << (*it)->number() << " " << (*jt)->number() << " " << weight;
                    t << " c " << (*it)->outLinkColor((*jt)->number());
                    t << "\n";
                }
            }
        }
    }

    // Restore original relation (important if caller expects it)
    if (originalRel >= 0 && originalRel < relCount)
        relationSet(originalRel, false);

    f.close();

    setFileName(fileName);
    setFileFormat(FileType::PAJEK);

    progressStatus(tr("File %1 saved").arg(fileNameNoPath));
    return true;
}

/**
 * @brief Saves the active graph to an adjacency-formatted file
 *
 * @param fileName
 *
 * @return bool
 */
bool Graph::saveToAdjacencyFormat(const QString &fileName,
                                  const bool &saveEdgeWeights)
{

    qDebug() << "Saving graph to adjacency-formatted file:" << fileName;

    QFile file(fileName);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        qDebug() << "Could not open (for writing) file:" << fileName;
        progressStatus(tr("Error. Could not write to ") + fileName);
        return false;
    }
    QTextStream outText(&file);

    writeMatrixAdjacencyTo(outText, saveEdgeWeights);

    file.close();

    // Store the filename
    setFileName(fileName);

    // Store the file format
    setFileFormat(FileType::ADJACENCY);

    QString fileNameNoPath = fileName.split("/").last();
    progressStatus(QString(tr("Adjacency matrix-formatted network saved into file %1")).arg(fileNameNoPath));
    return true;
}

/**
 * @brief Saves the active graph (current relation) to a GraphViz DOT file.
 *
 * Produces a valid digraph or graph block. Node identifiers are n<num> to
 * avoid issues with labels that contain spaces or special characters. All
 * standard visual properties (label, color, shape) and custom node/edge
 * attributes are written as DOT attribute lists so the file round-trips
 * through parseAsDot() without data loss.
 *
 * Only the currently active relation is exported. If the graph has multiple
 * relations the caller is responsible for warning the user before invoking
 * this method.
 *
 * @param fileName  Destination file path.
 * @return true on success, false if the file could not be opened.
 */
bool Graph::saveToDotFormat(const QString &fileName)
{
    qDebug() << "Saving graph to GraphViz DOT file:" << fileName;

    QFile f(fileName);
    if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
        qDebug() << "Could not open (for writing) file:" << fileName;
        progressStatus(tr("Error. Could not write to ") + fileName);
        return false;
    }

    QTextStream t(&f);
    t.setLocale(QLocale::c());
    t.setRealNumberNotation(QTextStream::SmartNotation);
    t.setRealNumberPrecision(6);

    const QString graphKeyword = isDirected() ? "digraph" : "graph";
    const QString edgeOp      = isDirected() ? " -> " : " -- ";

    QString netName = getName();
    netName.replace('"', '\'');

    t << graphKeyword << " \"" << netName << "\" {\n";
    t << "    graph [label=\"" << netName << "\"];\n\n";

    // Sanitise a custom attribute key to a valid DOT identifier.
    auto sanitiseKey = [](const QString &k) -> QString {
        QString s = k;
        s.replace(QRegularExpression("[^A-Za-z0-9_]"), "_");
        if (!s.isEmpty() && s[0].isDigit())
            s.prepend('_');
        return s;
    };

    // Escape a value string for DOT double-quoted literals.
    auto escapeVal = [](const QString &v) -> QString {
        QString s = v;
        s.replace('\\', "\\\\");
        s.replace('"', "\\\"");
        return s;
    };

    const bool hasVertexAttrs = !graphHasVertexCustomAttributes().isEmpty();
    const bool hasEdgeAttrs   = !graphHasEdgeCustomAttributes().isEmpty();
    const int  activeRel      = relationCurrent();

    // ---- Nodes ----------------------------------------------------------------
    for (VList::const_iterator it = m_graph.cbegin(); it != m_graph.cend(); ++it) {
        GraphVertex *v = *it;
        t << "    n" << v->number()
          << " [label=\""  << escapeVal(v->label())  << "\""
          << ", color=\""  << escapeVal(v->color())  << "\""
          << ", shape=\""  << escapeVal(v->shape())  << "\""
          << ", pos=\""    << v->x() << "," << v->y() << "\"";

        if (hasVertexAttrs) {
            const QHash<QString, QString> attrs = v->customAttributes();
            for (auto ai = attrs.constBegin(); ai != attrs.constEnd(); ++ai)
                t << ", " << sanitiseKey(ai.key()) << "=\"" << escapeVal(ai.value()) << "\"";
        }

        t << "];\n";
    }

    t << "\n";

    // ---- Edges (active relation only) -----------------------------------------
    for (VList::const_iterator it = m_graph.cbegin(); it != m_graph.cend(); ++it) {
        GraphVertex *sv = *it;
        const H_edges &oe = sv->outEdges();

        for (auto ei = oe.constBegin(); ei != oe.constEnd(); ++ei) {
            const int    tgt     = ei.key();
            const int    relIdx  = ei.value().first;
            const qreal  weight  = ei.value().second.first;
            const bool   enabled = ei.value().second.second;

            if (!enabled || relIdx != activeRel)
                continue;

            // For undirected graphs each edge is stored in both directions;
            // only emit the lower-numbered endpoint to avoid duplicates.
            if (!isDirected() && sv->number() > tgt)
                continue;

            t << "    n" << sv->number() << edgeOp << "n" << tgt
              << " [weight=" << weight
              << ", label=\""  << escapeVal(sv->outEdgeLabel(tgt))  << "\""
              << ", color=\""  << escapeVal(sv->outLinkColor(tgt))  << "\"";

            if (hasEdgeAttrs) {
                const QHash<QString, QString> eAttrs = sv->outEdgeCustomAttributes(tgt);
                for (auto ai = eAttrs.constBegin(); ai != eAttrs.constEnd(); ++ai)
                    t << ", " << sanitiseKey(ai.key()) << "=\"" << escapeVal(ai.value()) << "\"";
            }

            t << "];\n";
        }
    }

    t << "}\n";
    f.close();

    setFileName(fileName);
    setFileFormat(FileType::GRAPHVIZ);
    progressStatus(tr("File %1 saved").arg(QFileInfo(fileName).fileName()));
    return true;
}

/**
 * @brief Saves the current graph to a GraphML-formatted file
 *
 * @param fileName
 * @param networkName
 * @param maxWidth
 * @param maxHeight
 *
 * @return bool
 */
bool Graph::saveToGraphMLFormat(const QString &fileName,
                                const bool &saveZeroWeightEdges,
                                QString networkName,
                                int maxWidth,
                                int maxHeight)
{

    qDebug() << "Saving graph to GraphML-formatted file:" << fileName;

    qreal weight = 0;
    int source = 0, target = 0, edgeCount = 0, m_size = 1, m_labelSize;
    QString m_color, m_labelColor, m_label;
    bool openToken;

    QFileInfo fileInfo(fileName);
    QString fileNameNoPath = fileInfo.fileName();

    QString saveDirPath = fileInfo.canonicalPath();

    QString iconsSubDir = fileInfo.baseName() + "_" + fileInfo.suffix() + "_images";
    QString iconsDirPath = saveDirPath + "/" + iconsSubDir;

    QDir saveDir(saveDirPath);
    qreal rel_coord_x = 0;
    qreal rel_coord_y = 0;

    // Check if there are nodes with custom icons in the network
    if (graphHasVertexCustomIcons())
    {
        qDebug() << "Custom node icons exist."
                 << "Creating images subdir" << iconsDirPath;
        // There are custom node icons in this net.
        // We need to save these custom icons to a folder
        // Create a subdir inside the directory where the actual network file
        // is about to be saved. All custom icons will be copied one-by-one there.
        if (saveDir.mkpath(iconsDirPath))
        {
            qDebug() << "created icons subdir"
                     << iconsDirPath;
        }
        else
        {
            qDebug() << "ERROR creating subdir!";
        }
    }
    else
    {
        qDebug() << "No custom node icons. Nothing to do";
    }

    QString iconPath = QString();
    QString iconFileName = QString();
    QString copyIconFileNamePath = QString();

    // Init custom attributes lists and temp hashes
    QStringList vertexCustomAttributesList = graphHasVertexCustomAttributes();
    QHash<QString, QString> m_vertexCustomAttributes = QHash<QString, QString>();
    QStringList edgeCustomAttributesList = graphHasEdgeCustomAttributes();
    QHash<QString, QString> m_edgeCustomAttributes = QHash<QString, QString>();

    networkName = (networkName == "") ? getName().toHtmlEscaped() : networkName;
    networkName = (networkName == "unnamed") ? fileNameNoPath.toHtmlEscaped().left(fileNameNoPath.lastIndexOf('.')) : networkName;
    qDebug() << "file:" << fileName.toUtf8() << "networkName" << networkName;

    maxWidth = (maxWidth == 0) ? (int)canvasWidth : maxWidth;
    maxHeight = (maxHeight == 0) ? (int)canvasHeight : maxHeight;

    QFile f(fileName);
    if (!f.open(QIODevice::WriteOnly | QIODevice::Text))
    {
        qDebug() << "Could not open (for writing) file:" << fileName;
        progressStatus(tr("Error. Could not write to ") + fileName);
        return false;
    }
    QTextStream outText(&f);
    QString outTextEncoding = QStringEncoder(outText.encoding()).name();

    qDebug() << "Using default codec for saving stream:" << outTextEncoding;

    qDebug() << " writing xml version...";
    outText << "<?xml version=\"1.0\" encoding=\"" << outTextEncoding << "\"?> \n";
    outText << " <!-- Created by SocNetV " << VERSION << " -->\n";
    outText << "<graphml xmlns=\"http://graphml.graphdrawing.org/xmlns\" "
               "      xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance \" "
               "      xsi:schemaLocation=\"http://graphml.graphdrawing.org/xmlns "
               "      http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd\">"
               "\n";

    qDebug() << "writing keys...";

    outText << "  <key id=\"d0\" for=\"node\" attr.name=\"label\" attr.type=\"string\"> \n"
               "    <default>"
               "</default> \n"
               "  </key> \n";

    outText << "  <key id=\"d1\" for=\"node\" attr.name=\"x_coordinate\" attr.type=\"double\"> \n"
               "    <default>"
            << "0.0" << "</default> \n"
                        "  </key> \n";

    outText << "  <key id=\"d2\" for=\"node\" attr.name=\"y_coordinate\" attr.type=\"double\"> \n"
               "    <default>"
            << "0.0" << "</default> \n"
                        "  </key> \n";
    outText << "  <key id=\"d3\" for=\"node\" attr.name=\"size\" attr.type=\"double\"> \n"
               "    <default>"
            << initVertexSize << "</default> \n"
                                 "  </key> \n";

    outText << "  <key id=\"d4\" for=\"node\" attr.name=\"color\" attr.type=\"string\"> \n"
               "    <default>"
            << initVertexColor << "</default> \n"
                                  "  </key> \n";

    outText << "  <key id=\"d5\" for=\"node\" attr.name=\"shape\" attr.type=\"string\"> \n"
               "    <default>"
            << initVertexShape << "</default> \n"
                                  "  </key> \n";

    // Check if there are nodes with custom icons in this network
    if (graphHasVertexCustomIcons())
    {
        // There are custom icons, so we will copy the default custom icon
        // to the subdir we created earlier
        iconPath = initVertexIconPath;
        iconFileName = QFileInfo(iconPath).fileName();
        copyIconFileNamePath = iconsDirPath + "/" + iconFileName;
        if (!QFile(copyIconFileNamePath).exists())
        {
            if (QFile::copy(iconPath, copyIconFileNamePath))
            {
                qDebug() << "default iconFile saved to:" << copyIconFileNamePath;
            }
            else
            {
                qDebug() << "ERROR saving default iconFile to:" << copyIconFileNamePath;
            }
        }
        else
        {
            qDebug() << "default iconFile already exists in:" << copyIconFileNamePath;
        }
        // And we write a new key (id 51) in our graphml for this default custom icon
        outText << "  <key id=\"d51\" for=\"node\" attr.name=\"custom-icon\" attr.type=\"string\"> \n"
                   "    <default>"
                << iconsSubDir + "/" + iconFileName << "</default> \n"
                                                       "  </key> \n";
    } // end check if custom icons exist

    outText << "  <key id=\"d6\" for=\"node\" attr.name=\"label.color\" attr.type=\"string\"> \n"
               "    <default>"
            << initVertexLabelColor << "</default> \n"
                                       "  </key> \n";
    outText << "  <key id=\"d7\" for=\"node\" attr.name=\"label.size\" attr.type=\"string\"> \n"
               "    <default>"
            << initVertexLabelSize << "</default> \n"
                                      "  </key> \n";
    outText << "  <key id=\"d8\" for=\"edge\" attr.name=\"weight\" attr.type=\"double\"> \n"
               "    <default>1.0</default> \n"
               "  </key> \n";

    outText << "  <key id=\"d9\" for=\"edge\" attr.name=\"color\" attr.type=\"string\"> \n"
               "    <default>"
            << initEdgeColor << "</default> \n"
                                "  </key> \n";

    outText << "  <key id=\"d10\" for=\"edge\" attr.name=\"label\" attr.type=\"string\"> \n"
               "    <default>"
            << "" << "</default> \n"
                     "  </key> \n";

    // Save custom node attribute key definitions, if any.
    if (!vertexCustomAttributesList.isEmpty())
    {
        qDebug() << "saving defaults for vertexCustomAttributesList:" << vertexCustomAttributesList;
        QString customVertexAttrId;
        for (qsizetype i = 0; i < vertexCustomAttributesList.size(); ++i)
        {
            customVertexAttrId = 'd' + QString::number(1000 + i);
            qDebug() << "customVertexAttrId:" << customVertexAttrId
                     << "customVertexAttr" << vertexCustomAttributesList.at(i);
            outText << "  <key id=\"" + customVertexAttrId + "\" for=\"node\" attr.name=\"" + vertexCustomAttributesList.at(i) + "\" attr.type=\"string\"> \n"
                       "    <default></default> \n"
                       "  </key> \n";
        }
    }

    // Save custom edge attribute key definitions, if any (IDs: d2000+).
    if (!edgeCustomAttributesList.isEmpty())
    {
        qDebug() << "saving defaults for edgeCustomAttributesList:" << edgeCustomAttributesList;
        QString customEdgeAttrId;
        for (qsizetype i = 0; i < edgeCustomAttributesList.size(); ++i)
        {
            customEdgeAttrId = 'd' + QString::number(2000 + i);
            qDebug() << "customEdgeAttrId:" << customEdgeAttrId
                     << "customEdgeAttr" << edgeCustomAttributesList.at(i);
            outText << "  <key id=\"" + customEdgeAttrId + "\" for=\"edge\" attr.name=\"" + edgeCustomAttributesList.at(i) + "\" attr.type=\"string\"> \n"
                       "    <default></default> \n"
                       "  </key> \n";
        }
    }

    VList::const_iterator it;
    VList::const_iterator jt;
    QString relationName;
    int relationPrevious = relationCurrent();
    for (int i = 0; i < relations(); ++i)
    {
        relationName = (m_relationsList.at(i).simplified()).remove("\"");
        relationSet(i, false);
        qDebug() << "writing graph tag. Relation:" << relationName;

        if (isUndirected())
            outText << "  <graph id=\""
                    << ((relations() == 1) ? networkName : relationName)
                    << "\" edgedefault=\"undirected\"> \n";
        else
            outText << "  <graph id=\""
                    << ((relations() == 1) ? networkName : relationName)
                    << "\" edgedefault=\"directed\"> \n";

        qDebug() << "writing nodes data...";
        for (it = m_graph.cbegin(); it != m_graph.cend(); ++it)
        {
            if (!(*it)->isEnabled())
                continue;
            qDebug() << "Node id:" << (*it)->number();
            outText << "    <node id=\"" << (*it)->number() << "\"> \n";
            m_color = (*it)->color();
            m_size = (*it)->size();
            m_labelSize = (*it)->labelSize();
            m_labelColor = (*it)->labelColor();
            m_label = (*it)->label();
            m_label = htmlEscaped(m_label);
            m_vertexCustomAttributes = (*it)->customAttributes();
            outText << "      <data key=\"d0\">" << m_label << "</data>\n";

            rel_coord_x = (*it)->x() / (maxWidth);
            rel_coord_y = (*it)->y() / (maxHeight);

            //            qDebug()<<"Rel coordinates: "
            //                   << rel_coord_x
            //                   << ","
            //                   << rel_coord_y;

            outText << "      <data key=\"d1\">" << rel_coord_x << "</data>\n";
            outText << "      <data key=\"d2\">" << rel_coord_y << "</data>\n";

            if (initVertexSize != m_size)
            {
                outText << "      <data key=\"d3\">" << m_size << "</data>\n";
            }

            if (QString::compare(initVertexColor, m_color, Qt::CaseInsensitive) != 0)
            {
                outText << "      <data key=\"d4\">" << m_color << "</data>\n";
            }

            outText << "      <data key=\"d5\">" << (*it)->shape() << "</data>\n";

            if ((*it)->shape() == "custom")
            {
                iconPath = (*it)->shapeIconPath();
                iconFileName = QFileInfo(iconPath).fileName();
                copyIconFileNamePath = iconsDirPath + "/" + iconFileName;
                if (!QFile(copyIconFileNamePath).exists())
                {
                    if (QFile::copy(iconPath, copyIconFileNamePath))
                    {
                        qDebug() << "iconFile for node:" << (*it)->number()
                                 << "saved to:" << copyIconFileNamePath;
                    }
                    else
                    {
                        qDebug() << "ERROR saving iconFile for" << (*it)->number()
                                 << "saved to: " << copyIconFileNamePath;
                    }
                }
                else
                {
                    qDebug() << "iconFile for node:" << (*it)->number()
                             << "already exists in:" << copyIconFileNamePath;
                }
                outText << "      <data key=\"d51\">" << iconsSubDir + "/" + iconFileName << "</data>\n";
            }

            if (QString::compare(initVertexLabelColor, m_labelColor, Qt::CaseInsensitive) != 0)
            {
                outText << "      <data key=\"d6\">" << m_labelColor << "</data>\n";
            }

            if (initVertexLabelSize != m_labelSize)
            {
                outText << "      <data key=\"d7\">" << m_labelSize << "</data>\n";
            }

            qDebug() << "m_vertexCustomAttributes:" << m_vertexCustomAttributes;
            if (!m_vertexCustomAttributes.isEmpty())
            {
                QString customVertexAttrId;
                for (auto cit = m_vertexCustomAttributes.cbegin(), end = m_vertexCustomAttributes.cend(); cit != end; ++cit)
                {
                    int attrIndex = vertexCustomAttributesList.indexOf(cit.key());
                    if (attrIndex < 0)
                        continue; // key not in global list, skip
                    customVertexAttrId = 'd' + QString::number(1000 + attrIndex);
                    outText << "      <data key=\"" + customVertexAttrId + "\">" << cit.value() << "</data>\n";
                }
            }

            outText << "    </node>\n";
        }

        qDebug() << "writing edges data...";
        edgeCount = 0;
        if (isDirected())
        {
            for (it = m_graph.cbegin(); it != m_graph.cend(); ++it)
            {
                for (jt = m_graph.cbegin(); jt != m_graph.cend(); jt++)
                {
                    source = (*it)->number();
                    target = (*jt)->number();
                    m_label = "";

                    // Check if user opted to save zero-weight edges
                    if (saveZeroWeightEdges)
                    {
                        weight = this->edgeExistsVirtual(source, target);
                    }
                    else
                    {
                        weight = this->edgeExists(source, target);
                    }

                    if ((!saveZeroWeightEdges && weight != 0) || (saveZeroWeightEdges && weight != RAND_MAX))
                    {
                        ++edgeCount;
                        m_color = (*it)->outLinkColor(target);
                        m_label = edgeLabel(source, target);
                        m_label = htmlEscaped(m_label);
                        //                        qDebug()<< "edge no:"
                        //                                << edgeCount
                        //                                << "from n1=" << source << "to n2=" << target
                        //                                << "with weight" << weight
                        //                                << "and color" << m_color.toUtf8() ;
                        outText << "    <edge id=\"" << "e" + QString::number(edgeCount)
                                << "\" directed=\"" << "true" << "\" source=\"" << source
                                << "\" target=\"" << target << "\"";

                        openToken = true;
                        if (weight != 0 || (saveZeroWeightEdges && weight != RAND_MAX))
                        {
                            outText << "> \n";
                            outText << "      <data key=\"d8\">" << weight << "</data>" << " \n";
                            openToken = false;
                        }
                        if (QString::compare(initEdgeColor, m_color, Qt::CaseInsensitive) != 0)
                        {
                            if (openToken)
                                outText << "> \n";
                            outText << "      <data key=\"d9\">" << m_color << "</data>" << " \n";
                            openToken = false;
                        }
                        if (!m_label.isEmpty())
                        {
                            if (openToken)
                                outText << "> \n";
                            outText << "      <data key=\"d10\">" << m_label << "</data>" << " \n";
                            openToken = false;
                        }
                        if (!edgeCustomAttributesList.isEmpty())
                        {
                            m_edgeCustomAttributes = edgeCustomAttributes(source, target);
                            if (!m_edgeCustomAttributes.isEmpty())
                            {
                                if (openToken)
                                    outText << "> \n";
                                openToken = false;
                                QString customEdgeAttrId;
                                for (auto cit = m_edgeCustomAttributes.cbegin(); cit != m_edgeCustomAttributes.cend(); ++cit)
                                {
                                    int attrIndex = edgeCustomAttributesList.indexOf(cit.key());
                                    if (attrIndex < 0)
                                        continue;
                                    customEdgeAttrId = 'd' + QString::number(2000 + attrIndex);
                                    outText << "      <data key=\"" + customEdgeAttrId + "\">" << cit.value() << "</data>\n";
                                }
                            }
                        }

                        if (openToken)
                            outText << "/> \n";
                        else
                            outText << "    </edge>\n";
                    }
                }
            }
        }
        else
        {
            for (it = m_graph.cbegin(); it != m_graph.cend(); ++it)
            {
                for (jt = it; jt != m_graph.end(); jt++)
                {
                    source = (*it)->number();
                    target = (*jt)->number();
                    m_label = "";

                    // Check if user opted to save zero-weight edges
                    if (saveZeroWeightEdges)
                    {
                        weight = this->edgeExistsVirtual(source, target);
                    }
                    else
                    {
                        weight = this->edgeExists(source, target);
                    }

                    if ((!saveZeroWeightEdges && weight != 0) || (saveZeroWeightEdges && weight != RAND_MAX))
                    {
                        ++edgeCount;
                        m_color = (*it)->outLinkColor(target);
                        m_label = edgeLabel(source, target);
                        m_label = htmlEscaped(m_label);
                        outText << "    <edge id=\"" << "e" + QString::number(edgeCount)
                                << "\" directed=\"" << "false" << "\" source=\"" << source
                                << "\" target=\"" << target << "\"";

                        openToken = true;
                        if (weight != 0 || (saveZeroWeightEdges && weight != RAND_MAX))
                        {
                            outText << "> \n";
                            outText << "      <data key=\"d8\">" << weight << "</data>" << " \n";
                            openToken = false;
                        }
                        if (QString::compare(initEdgeColor, m_color, Qt::CaseInsensitive) != 0)
                        {
                            if (openToken)
                                outText << "> \n";
                            outText << "      <data key=\"d9\">" << m_color << "</data>" << " \n";
                            openToken = false;
                        }
                        if (!m_label.isEmpty())
                        {
                            if (openToken)
                                outText << "> \n";
                            outText << "      <data key=\"d10\">" << m_label << "</data>" << " \n";
                            openToken = false;
                        }
                        if (!edgeCustomAttributesList.isEmpty())
                        {
                            m_edgeCustomAttributes = edgeCustomAttributes(source, target);
                            if (!m_edgeCustomAttributes.isEmpty())
                            {
                                if (openToken)
                                    outText << "> \n";
                                openToken = false;
                                QString customEdgeAttrId;
                                for (auto cit = m_edgeCustomAttributes.cbegin(); cit != m_edgeCustomAttributes.cend(); ++cit)
                                {
                                    int attrIndex = edgeCustomAttributesList.indexOf(cit.key());
                                    if (attrIndex < 0)
                                        continue;
                                    customEdgeAttrId = 'd' + QString::number(2000 + attrIndex);
                                    outText << "      <data key=\"" + customEdgeAttrId + "\">" << cit.value() << "</data>\n";
                                }
                            }
                        }
                        if (openToken)
                            outText << "/> \n";
                        else
                            outText << "    </edge>\n";
                    }
                }
            }
        }

        outText << "  </graph>\n";
    }
    outText << "</graphml>\n";

    f.close();
    relationSet(relationPrevious, false);

    // Store the filename
    setFileName(fileName);

    // Store the file format
    setFileFormat(FileType::GRAPHML);

    progressStatus(tr("File %1 saved").arg(fileNameNoPath));

    return true;
}

/**
 * @brief Saves the current graph to a UCINET DL file (FULLMATRIX format).
 *
 * Emits one header block followed by all relation matrices back-to-back in the
 * DATA section.  The parser advances its relation counter whenever the source
 * row counter exceeds N, so no separator is needed between matrices.
 *
 * Single relation: N=<n>, no NM or LEVEL LABELS.
 * Multi-relation:  N=<n> NM=<m>, LEVEL LABELS block with relation names.
 *
 * Node labels are always written in ROW LABELS / COLUMN LABELS sections.
 * Edge weights are written as-is (0 for absent edges).
 *
 * @param fileName  Destination file path.
 * @return true on success, false if the file cannot be opened.
 */
bool Graph::saveToUCINETDLFormat(const QString &fileName)
{
    qDebug() << "Saving graph to UCINET DL file:" << fileName;

    QFile f(fileName);
    if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
        qDebug() << "Could not open (for writing) file:" << fileName;
        progressStatus(tr("Error. Could not write to ") + fileName);
        return false;
    }

    QTextStream t(&f);
    t.setLocale(QLocale::c());
    t.setRealNumberNotation(QTextStream::SmartNotation);
    t.setRealNumberPrecision(12);

    const int n    = vertices();
    const int nm   = relations();
    const int orig = relationCurrent();

    t << "DL\n";
    if (nm > 1)
        t << "N=" << n << " NM=" << nm << "\n";
    else
        t << "N=" << n << "\n";

    t << "FORMAT = FULLMATRIX DIAGONAL PRESENT\n";

    // LEVEL LABELS (relation names) — only for multi-relation
    if (nm > 1) {
        t << "LEVEL LABELS:\n";
        for (int r = 0; r < nm; ++r) {
            relationSet(r, false);
            t << relationCurrentName() << "\n";
        }
        relationSet(orig, false);
    }

    // ROW LABELS (node labels)
    t << "ROW LABELS:\n";
    for (VList::const_iterator it = m_graph.cbegin(); it != m_graph.cend(); ++it)
        t << (*it)->label() << "\n";

    // COLUMN LABELS (same as row for square matrices)
    t << "COLUMN LABELS:\n";
    for (VList::const_iterator it = m_graph.cbegin(); it != m_graph.cend(); ++it)
        t << (*it)->label() << "\n";

    // DATA: all NM matrices back-to-back
    t << "DATA:\n";
    for (int r = 0; r < nm; ++r) {
        relationSet(r, false);
        for (VList::const_iterator it = m_graph.cbegin(); it != m_graph.cend(); ++it) {
            const int i = (*it)->number();
            bool first = true;
            for (VList::const_iterator jt = m_graph.cbegin(); jt != m_graph.cend(); ++jt) {
                const int j = (*jt)->number();
                qreal w = 0;
                if (isDirected())
                    w = edgeExists(i, j);
                else
                    w = edgeExists(i, j, true);
                if (!first)
                    t << " ";
                first = false;
                if (w == 0)
                    t << "0";
                else
                    t << w;
            }
            t << "\n";
        }
    }

    f.close();

    if (orig >= 0 && orig < nm)
        relationSet(orig, false);

    setFileName(fileName);
    setFileFormat(FileType::UCINET);
    progressStatus(tr("File %1 saved").arg(QFileInfo(fileName).fileName()));
    return true;
}

/**
 * @brief Saves the active relation to a weighted edge list file.
 *
 * Format: one edge per line — "source target weight" — using the node label
 * as identifier (spaces in labels replaced with underscores).  A comment
 * header line describes the columns.
 *
 * Only the currently active relation is exported.
 *
 * @param fileName  Destination file path.
 * @return true on success, false if the file cannot be opened.
 */
bool Graph::saveToEdgeListWeightedFormat(const QString &fileName)
{
    qDebug() << "Saving graph to weighted edge list file:" << fileName;

    QFile f(fileName);
    if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
        qDebug() << "Could not open (for writing) file:" << fileName;
        progressStatus(tr("Error. Could not write to ") + fileName);
        return false;
    }

    QTextStream t(&f);
    t.setLocale(QLocale::c());
    t.setRealNumberNotation(QTextStream::SmartNotation);
    t.setRealNumberPrecision(12);

    // Sanitise a node label for use as an edge-list identifier.
    auto sanitiseLabel = [](const QString &lbl) -> QString {
        QString s = lbl.trimmed();
        s.replace(' ', '_');
        return s;
    };

    t << "# source target weight\n";

    const int activeRel = relationCurrent();

    for (VList::const_iterator it = m_graph.cbegin(); it != m_graph.cend(); ++it) {
        GraphVertex *sv = *it;
        const H_edges &oe = sv->outEdges();
        for (auto ei = oe.constBegin(); ei != oe.constEnd(); ++ei) {
            const int   tgt     = ei.key();
            const int   relIdx  = ei.value().first;
            const qreal weight  = ei.value().second.first;
            const bool  enabled = ei.value().second.second;

            if (!enabled || relIdx != activeRel)
                continue;
            if (!isDirected() && sv->number() > tgt)
                continue;

            t << sanitiseLabel(sv->label()) << " "
              << sanitiseLabel(vertexLabel(tgt)) << " "
              << weight << "\n";
        }
    }

    f.close();

    setFileName(fileName);
    setFileFormat(FileType::EDGELIST_WEIGHTED);
    progressStatus(tr("File %1 saved").arg(QFileInfo(fileName).fileName()));
    return true;
}

/**
 * @brief Saves the active relation to a simple (unweighted) edge list file.
 *
 * Format: one edge per line — "source target" — using the node label as
 * identifier (spaces in labels replaced with underscores).  A comment header
 * line describes the columns.
 *
 * Only the currently active relation is exported.
 *
 * @param fileName  Destination file path.
 * @return true on success, false if the file cannot be opened.
 */
bool Graph::saveToEdgeListSimpleFormat(const QString &fileName)
{
    qDebug() << "Saving graph to simple edge list file:" << fileName;

    QFile f(fileName);
    if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) {
        qDebug() << "Could not open (for writing) file:" << fileName;
        progressStatus(tr("Error. Could not write to ") + fileName);
        return false;
    }

    QTextStream t(&f);

    auto sanitiseLabel = [](const QString &lbl) -> QString {
        QString s = lbl.trimmed();
        s.replace(' ', '_');
        return s;
    };

    t << "# source target\n";

    const int activeRel = relationCurrent();

    for (VList::const_iterator it = m_graph.cbegin(); it != m_graph.cend(); ++it) {
        GraphVertex *sv = *it;
        const H_edges &oe = sv->outEdges();
        for (auto ei = oe.constBegin(); ei != oe.constEnd(); ++ei) {
            const int  tgt     = ei.key();
            const int  relIdx  = ei.value().first;
            const bool enabled = ei.value().second.second;

            if (!enabled || relIdx != activeRel)
                continue;
            if (!isDirected() && sv->number() > tgt)
                continue;

            t << sanitiseLabel(sv->label()) << " "
              << sanitiseLabel(vertexLabel(tgt)) << "\n";
        }
    }

    f.close();

    setFileName(fileName);
    setFileFormat(FileType::EDGELIST_SIMPLE);
    progressStatus(tr("File %1 saved").arg(QFileInfo(fileName).fileName()));
    return true;
}
