// SPDX-License-Identifier: LGPL-2.1-or-later

/***************************************************************************
 *   Copyright (c) 2002 Jürgen Riegel <juergen.riegel@web.de>              *
 *                                                                         *
 *   This file is part of the FreeCAD CAx development system.              *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU Library General Public License (LGPL)   *
 *   as published by the Free Software Foundation; either version 2 of     *
 *   the License, or (at your option) any later version.                   *
 *   for detail see the LICENCE text file.                                 *
 *                                                                         *
 *   FreeCAD is distributed in the hope that it will be useful,            *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU Library General Public License for more details.                  *
 *                                                                         *
 *   You should have received a copy of the GNU Library General Public     *
 *   License along with FreeCAD; if not, write to the Free Software        *
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
 *   USA                                                                   *
 *                                                                         *
 ***************************************************************************/


#include <algorithm>
#include <iostream>
#include <memory>
#include <xercesc/dom/DOM.hpp>
#include <xercesc/framework/LocalFileFormatTarget.hpp>
#include <xercesc/framework/LocalFileInputSource.hpp>
#include <xercesc/framework/MemBufFormatTarget.hpp>
#include <xercesc/framework/MemBufInputSource.hpp>
#include <xercesc/parsers/XercesDOMParser.hpp>
#include <xercesc/sax/EntityResolver.hpp>
#include <xercesc/sax/ErrorHandler.hpp>
#include <xercesc/sax/SAXParseException.hpp>
#include <sstream>
#include <string>
#include <utility>

#include <QFileInfo>
#include <QLockFile>
#include <QDir>

#include <FCConfig.h>

#ifdef FC_OS_LINUX
# include <unistd.h>
#endif

#include <boost/algorithm/string.hpp>
#include <fmt/printf.h>

#include "Parameter.h"
#include "Parameter.inl"
#include "Console.h"
#include "Exception.h"
#include "Tools.h"

FC_LOG_LEVEL_INIT("Parameter", true, true)

#ifndef XERCES_CPP_NAMESPACE_BEGIN
# define XERCES_CPP_NAMESPACE_QUALIFIER
using namespace XERCES_CPP_NAMESPACE;
#else
XERCES_CPP_NAMESPACE_USE
#endif
using namespace Base;


#include "XMLTools.h"

//**************************************************************************
//**************************************************************************
// private classes declaration:
// - DOMTreeErrorReporter
// - StrX
// - DOMPrintFilter
// - DOMPrintErrorHandler
// - XStr
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

class DOMTreeErrorReporter: public ErrorHandler
{
public:
    // -----------------------------------------------------------------------
    //  Implementation of the error handler interface
    // -----------------------------------------------------------------------
    void warning(const SAXParseException& toCatch) override;
    void error(const SAXParseException& toCatch) override;
    void fatalError(const SAXParseException& toCatch) override;
    void resetErrors() override;

    // -----------------------------------------------------------------------
    //  Getter methods
    // -----------------------------------------------------------------------
    bool getSawErrors() const;

    // -----------------------------------------------------------------------
    //  Private data members
    //
    //  fSawErrors
    //      This is set if we get any errors, and is queryable via a getter
    //      method. Its used by the main code to suppress output if there are
    //      errors.
    // -----------------------------------------------------------------------
    bool fSawErrors {false};
};


class DOMPrintFilter: public DOMLSSerializerFilter
{
public:
    /** @name Constructors */
    explicit DOMPrintFilter(ShowType whatToShow = DOMNodeFilter::SHOW_ALL);
    //@{

    /** @name Destructors */
    ~DOMPrintFilter() override = default;
    //@{

    /** @ interface from DOMWriterFilter */
    FilterAction acceptNode(const XERCES_CPP_NAMESPACE_QUALIFIER DOMNode* node) const override;
    //@{

    ShowType getWhatToShow() const override
    {
        return fWhatToShow;
    }

    // unimplemented copy ctor and assignment operator
    DOMPrintFilter(const DOMPrintFilter&) = delete;
    DOMPrintFilter(DOMPrintFilter&&) = delete;
    DOMPrintFilter& operator=(const DOMPrintFilter&) = delete;
    DOMPrintFilter& operator=(DOMPrintFilter&&) = delete;

    ShowType fWhatToShow;
};

class DOMPrintErrorHandler: public DOMErrorHandler
{
public:
    DOMPrintErrorHandler() = default;
    ~DOMPrintErrorHandler() override = default;

    /** @name The error handler interface */
    bool handleError(const DOMError& domError) override;
    void resetErrors()
    {}

    /* Unimplemented constructors and operators */
    DOMPrintErrorHandler(const DOMPrintErrorHandler&) = delete;
    DOMPrintErrorHandler(DOMPrintErrorHandler&&) = delete;
    void operator=(const DOMPrintErrorHandler&) = delete;
    void operator=(DOMPrintErrorHandler&&) = delete;
};


inline bool DOMTreeErrorReporter::getSawErrors() const
{
    return fSawErrors;
}


//**************************************************************************
//**************************************************************************
// ParameterManager
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


//**************************************************************************
// Construction/Destruction


/** Default construction
 */
ParameterGrp::ParameterGrp(
    XERCES_CPP_NAMESPACE_QUALIFIER DOMElement* GroupNode,
    const char* sName,
    ParameterGrp* Parent
)
    : _pGroupNode(GroupNode)
    , _Parent(Parent)
{
    if (sName) {
        _cName = sName;
    }
    if (_Parent) {
        _Manager = _Parent->_Manager;
    }
}


/** Destruction
 * complete destruction of the object
 */
ParameterGrp::~ParameterGrp()
{
    for (auto& v : _GroupMap) {
        v.second->_Parent = nullptr;
        v.second->_Manager = nullptr;
    }
    if (_Detached && _pGroupNode) {
        _pGroupNode->release();
    }
}

//**************************************************************************
// Access methods

void ParameterGrp::copyTo(const Base::Reference<ParameterGrp>& Grp)
{
    if (Grp == this) {
        return;
    }

    // delete previous content
    Grp->Clear(true);

    // copy all
    insertTo(Grp);
}

void ParameterGrp::insertTo(const Base::Reference<ParameterGrp>& Grp)
{
    if (Grp == this) {
        return;
    }

    // copy group
    std::vector<Base::Reference<ParameterGrp>> Grps = GetGroups();
    std::vector<Base::Reference<ParameterGrp>>::iterator It1;
    for (It1 = Grps.begin(); It1 != Grps.end(); ++It1) {
        (*It1)->insertTo(Grp->GetGroup((*It1)->GetGroupName()));
    }

    // copy strings
    std::vector<std::pair<std::string, std::string>> StringMap = GetASCIIMap();
    std::vector<std::pair<std::string, std::string>>::iterator It2;
    for (It2 = StringMap.begin(); It2 != StringMap.end(); ++It2) {
        Grp->SetASCII(It2->first.c_str(), It2->second.c_str());
    }

    // copy bool
    std::vector<std::pair<std::string, bool>> BoolMap = GetBoolMap();
    std::vector<std::pair<std::string, bool>>::iterator It3;
    for (It3 = BoolMap.begin(); It3 != BoolMap.end(); ++It3) {
        Grp->SetBool(It3->first.c_str(), It3->second);
    }

    // copy int
    std::vector<std::pair<std::string, long>> IntMap = GetIntMap();
    std::vector<std::pair<std::string, long>>::iterator It4;
    for (It4 = IntMap.begin(); It4 != IntMap.end(); ++It4) {
        Grp->SetInt(It4->first.c_str(), It4->second);
    }

    // copy float
    std::vector<std::pair<std::string, double>> FloatMap = GetFloatMap();
    std::vector<std::pair<std::string, double>>::iterator It5;
    for (It5 = FloatMap.begin(); It5 != FloatMap.end(); ++It5) {
        Grp->SetFloat(It5->first.c_str(), It5->second);
    }

    // copy uint
    std::vector<std::pair<std::string, unsigned long>> UIntMap = GetUnsignedMap();
    std::vector<std::pair<std::string, unsigned long>>::iterator It6;
    for (It6 = UIntMap.begin(); It6 != UIntMap.end(); ++It6) {
        Grp->SetUnsigned(It6->first.c_str(), It6->second);
    }
}

void ParameterGrp::exportTo(const char* FileName)
{
    auto Mngr = ParameterManager::Create();

    Mngr->CreateDocument();

    // copy all into the new document
    insertTo(Base::Reference<ParameterGrp>(Mngr));

    Mngr->SaveDocument(FileName);
}

void ParameterGrp::importFrom(const char* FileName)
{
    auto Mngr = ParameterManager::Create();

    if (Mngr->LoadDocument(FileName) != 1) {
        throw FileException("ParameterGrp::import() cannot load document", FileName);
    }

    Mngr->copyTo(Base::Reference<ParameterGrp>(this));
}

void ParameterGrp::insert(const char* FileName)
{
    auto Mngr = ParameterManager::Create();

    if (Mngr->LoadDocument(FileName) != 1) {
        throw FileException("ParameterGrp::import() cannot load document", FileName);
    }

    Mngr->insertTo(Base::Reference<ParameterGrp>(this));
}

void ParameterGrp::revert(const char* FileName)
{
    auto Mngr = ParameterManager::Create();

    if (Mngr->LoadDocument(FileName) != 1) {
        throw FileException("ParameterGrp::revert() cannot load document", FileName);
    }

    revert(Base::Reference<ParameterGrp>(Mngr));
}

void ParameterGrp::revert(const Base::Reference<ParameterGrp>& Grp)
{
    if (Grp == this) {
        return;
    }

    for (auto& grp : Grp->GetGroups()) {
        if (HasGroup(grp->GetGroupName())) {
            GetGroup(grp->GetGroupName())->revert(grp);
        }
    }

    for (const auto& v : Grp->GetASCIIMap()) {
        if (GetASCII(v.first.c_str(), v.second.c_str()) == v.second) {
            RemoveASCII(v.first.c_str());
        }
    }

    for (const auto& v : Grp->GetBoolMap()) {
        if (GetBool(v.first.c_str(), v.second) == v.second) {
            RemoveBool(v.first.c_str());
        }
    }

    for (const auto& v : Grp->GetIntMap()) {
        if (GetInt(v.first.c_str(), v.second) == v.second) {
            RemoveInt(v.first.c_str());
        }
    }

    for (const auto& v : Grp->GetUnsignedMap()) {
        if (GetUnsigned(v.first.c_str(), v.second) == v.second) {
            RemoveUnsigned(v.first.c_str());
        }
    }

    for (const auto& v : Grp->GetFloatMap()) {
        if (GetFloat(v.first.c_str(), v.second) == v.second) {
            RemoveFloat(v.first.c_str());
        }
    }
}

XERCES_CPP_NAMESPACE_QUALIFIER DOMElement* ParameterGrp::CreateElement(
    XERCES_CPP_NAMESPACE_QUALIFIER DOMElement* Start,
    const char* Type,
    const char* Name
)
{
    if (XMLString::compareString(Start->getNodeName(), XStrLiteral("FCParamGroup").unicodeForm()) != 0
        && XMLString::compareString(Start->getNodeName(), XStrLiteral("FCParameters").unicodeForm())
            != 0) {
        Base::Console().warning(
            "CreateElement: %s cannot have the element %s of type %s\n",
            StrX(Start->getNodeName()).c_str(),
            Name,
            Type
        );
        return nullptr;
    }

    if (_Detached && _Parent) {
        // re-attach the group
        _Parent->_GetGroup(_cName.c_str());
    }

    XERCES_CPP_NAMESPACE_QUALIFIER DOMDocument* pDocument = Start->getOwnerDocument();

    auto pcElem = pDocument->createElement(XStr(Type).unicodeForm());
    pcElem->setAttribute(XStrLiteral("Name").unicodeForm(), XStr(Name).unicodeForm());
    Start->appendChild(pcElem);

    return pcElem;
}

Base::Reference<ParameterGrp> ParameterGrp::GetGroup(const char* Name)
{
    if (!Name) {
        throw Base::ValueError("Empty group name");
    }

    Base::Reference<ParameterGrp> hGrp = this;
    std::vector<std::string> tokens;
    boost::split(tokens, Name, boost::is_any_of("/"));
    for (auto& token : tokens) {
        boost::trim(token);
        if (token.empty()) {
            continue;
        }
        hGrp = hGrp->_GetGroup(token.c_str());
        if (!hGrp) {
            // The group is clearing. Return some dummy group to avoid caller
            // crashing for backward compatibility.
            hGrp = new ParameterGrp();
            hGrp->_cName = Name;
            break;
        }
    }
    if (hGrp == this) {
        throw Base::ValueError("Empty group name");
    }
    return hGrp;
}

Base::Reference<ParameterGrp> ParameterGrp::_GetGroup(const char* Name)
{
    Base::Reference<ParameterGrp> rParamGrp;
    if (!_pGroupNode) {
        if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
            FC_WARN("Adding group " << Name << " in an orphan group " << _cName);
        }
        return rParamGrp;
    }
    if (_Clearing) {
        if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
            FC_WARN("Adding group " << Name << " while clearing " << GetPath());
        }
        return rParamGrp;
    }

    DOMElement* pcTemp {};

    // search if Group node already there
    pcTemp = FindElement(_pGroupNode, "FCParamGroup", Name);

    // already created?
    if (!(rParamGrp = _GroupMap[Name]).isValid()) {
        if (!pcTemp) {
            pcTemp = CreateElement(_pGroupNode, "FCParamGroup", Name);
        }
        // create and register handle
        rParamGrp = Base::Reference<ParameterGrp>(new ParameterGrp(pcTemp, Name, this));
        _GroupMap[Name] = rParamGrp;
    }
    else if (!pcTemp) {
        _pGroupNode->appendChild(rParamGrp->_pGroupNode);
        rParamGrp->_Detached = false;
        if (this->_Detached && this->_Parent) {
            // Re-attach the group. Note that this may fail if the parent is
            // clearing. That's why we check this->_Detached below.
            this->_Parent->_GetGroup(_cName.c_str());
        }
    }

    if (!pcTemp && !this->_Detached) {
        _Notify(ParamType::FCGroup, Name, Name);
    }

    return rParamGrp;
}

std::string ParameterGrp::GetPath() const
{
    std::string path;
    if (_Parent && _Parent != _Manager) {
        path = _Parent->GetPath();
    }
    if (!path.empty() && !_cName.empty()) {
        path += "/";
    }
    path += _cName;
    return path;
}

std::vector<Base::Reference<ParameterGrp>> ParameterGrp::GetGroups()
{
    Base::Reference<ParameterGrp> rParamGrp;
    std::vector<Base::Reference<ParameterGrp>> vrParamGrp;

    if (!_pGroupNode) {
        return vrParamGrp;
    }

    std::string Name;

    DOMElement* pcTemp = FindElement(_pGroupNode, "FCParamGroup");
    while (pcTemp) {
        Name = StrX(pcTemp->getAttributes()
                        ->getNamedItem(XStrLiteral("Name").unicodeForm())
                        ->getNodeValue())
                   .c_str();
        // already created?
        if (!(rParamGrp = _GroupMap[Name]).isValid()) {
            rParamGrp = Base::Reference<ParameterGrp>(new ParameterGrp(pcTemp, Name.c_str(), this));
            _GroupMap[Name] = rParamGrp;
        }
        vrParamGrp.push_back(rParamGrp);
        // go to next
        pcTemp = FindNextElement(pcTemp, "FCParamGroup");
    }

    return vrParamGrp;
}

/// test if this group is empty
bool ParameterGrp::IsEmpty() const
{
    return !(_pGroupNode && _pGroupNode->getFirstChild());
}

/// test if a special sub group is in this group
bool ParameterGrp::HasGroup(const char* Name) const
{
    if (_GroupMap.find(Name) != _GroupMap.end()) {
        return true;
    }

    if (_pGroupNode && FindElement(_pGroupNode, "FCParamGroup", Name) != nullptr) {
        return true;
    }

    return false;
}

const char* ParameterGrp::TypeName(ParamType Type)
{
    switch (Type) {
        case ParamType::FCBool:
            return "FCBool";
        case ParamType::FCInt:
            return "FCInt";
        case ParamType::FCUInt:
            return "FCUInt";
        case ParamType::FCText:
            return "FCText";
        case ParamType::FCFloat:
            return "FCFloat";
        case ParamType::FCGroup:
            return "FCParamGroup";
        default:
            return nullptr;
    }
}

ParameterGrp::ParamType ParameterGrp::TypeValue(const char* Name)
{
    if (Name) {
        if (boost::equals(Name, "FCBool")) {
            return ParamType::FCBool;
        }
        if (boost::equals(Name, "FCInt")) {
            return ParamType::FCInt;
        }
        if (boost::equals(Name, "FCUInt")) {
            return ParamType::FCUInt;
        }
        if (boost::equals(Name, "FCText")) {
            return ParamType::FCText;
        }
        if (boost::equals(Name, "FCFloat")) {
            return ParamType::FCFloat;
        }
        if (boost::equals(Name, "FCParamGroup")) {
            return ParamType::FCGroup;
        }
    }
    return ParamType::FCInvalid;
}

void ParameterGrp::SetAttribute(ParamType Type, const char* Name, const char* Value)
{
    switch (Type) {
        case ParamType::FCBool:
        case ParamType::FCInt:
        case ParamType::FCUInt:
        case ParamType::FCFloat:
            return _SetAttribute(Type, Name, Value);
        case ParamType::FCText:
            return SetASCII(Name, Value);
        case ParamType::FCGroup:
            RenameGrp(Name, Value);
            break;
        default:
            break;
    }
}

void ParameterGrp::RemoveAttribute(ParamType Type, const char* Name)
{
    switch (Type) {
        case ParamType::FCBool:
            return RemoveBool(Name);
        case ParamType::FCInt:
            return RemoveInt(Name);
        case ParamType::FCUInt:
            return RemoveUnsigned(Name);
        case ParamType::FCText:
            return RemoveASCII(Name);
        case ParamType::FCFloat:
            return RemoveFloat(Name);
        case ParamType::FCGroup:
            return RemoveGrp(Name);
        default:
            break;
    }
}

const char* ParameterGrp::GetAttribute(
    ParamType Type,
    const char* Name,
    std::string& Value,
    const char* Default
) const
{
    if (!_pGroupNode) {
        return Default;
    }

    const char* T = TypeName(Type);
    if (!T) {
        return Default;
    }

    DOMElement* pcElem = FindElement(_pGroupNode, T, Name);
    if (!pcElem) {
        return Default;
    }

    if (Type == ParamType::FCText) {
        Value = GetASCII(Name, Default);
    }
    else if (Type != ParamType::FCGroup) {
        Value = StrX(pcElem->getAttribute(XStrLiteral("Value").unicodeForm())).c_str();
    }
    return Value.c_str();
}

std::vector<std::pair<std::string, std::string>> ParameterGrp::GetAttributeMap(
    ParamType Type,
    const char* sFilter
) const
{
    std::vector<std::pair<std::string, std::string>> res;
    if (!_pGroupNode) {
        return res;
    }

    const char* T = TypeName(Type);
    if (!T) {
        return res;
    }

    std::string Name;

    DOMElement* pcTemp = FindElement(_pGroupNode, T);
    while (pcTemp) {
        Name = StrX(
                   static_cast<DOMElement*>(pcTemp)
                       ->getAttributes()
                       ->getNamedItem(XStrLiteral("Name").unicodeForm())
                       ->getNodeValue()
        )
                   .c_str();
        // check on filter condition
        if (!sFilter || Name.find(sFilter) != std::string::npos) {
            if (Type == ParamType::FCGroup) {
                res.emplace_back(Name, std::string());
            }
            else {
                res.emplace_back(
                    Name,
                    StrX(static_cast<DOMElement*>(pcTemp)->getAttribute(XStrLiteral("Value").unicodeForm()))
                        .c_str()
                );
            }
        }
        pcTemp = FindNextElement(pcTemp, T);
    }
    return res;
}

void ParameterGrp::_Notify(ParamType Type, const char* Name, const char* Value)
{
    if (_Manager) {
        _Manager->signalParamChanged(this, Type, Name, Value);
    }
}

void ParameterGrp::_SetAttribute(ParamType T, const char* Name, const char* Value)
{
    const char* Type = TypeName(T);
    if (!Type) {
        return;
    }
    if (!_pGroupNode) {
        if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
            FC_WARN("Setting attribute " << Type << ":" << Name << " in an orphan group " << _cName);
        }
        return;
    }
    if (_Clearing) {
        if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
            FC_WARN("Adding attribute " << Type << ":" << Name << " while clearing " << GetPath());
        }
        return;
    }

    // find or create the Element
    DOMElement* pcElem = FindOrCreateElement(_pGroupNode, Type, Name);
    if (pcElem) {
        XStr attr("Value");
        // set the value only if different
        if (strcmp(StrX(pcElem->getAttribute(attr.unicodeForm())).c_str(), Value) != 0) {
            pcElem->setAttribute(attr.unicodeForm(), XStr(Value).unicodeForm());
            // trigger observer
            _Notify(T, Name, Value);
        }
        // For backward compatibility, old observer gets notified regardless of
        // value changes or not.
        Notify(Name);
    }
}

bool ParameterGrp::GetBool(const char* Name, bool bPreset) const
{
    if (!_pGroupNode) {
        return bPreset;
    }

    // check if Element in group
    DOMElement* pcElem = FindElement(_pGroupNode, "FCBool", Name);
    // if not return preset
    if (!pcElem) {
        return bPreset;
    }

    // if yes check the value and return
    return (strcmp(StrX(pcElem->getAttribute(XStrLiteral("Value").unicodeForm())).c_str(), "1") == 0);
}

void ParameterGrp::SetBool(const char* Name, bool bValue)
{
    _SetAttribute(ParamType::FCBool, Name, bValue ? "1" : "0");
}

std::vector<bool> ParameterGrp::GetBools(const char* sFilter) const
{
    std::vector<bool> vrValues;
    if (!_pGroupNode) {
        return vrValues;
    }

    std::string Name;

    DOMElement* pcTemp = FindElement(_pGroupNode, "FCBool");
    while (pcTemp) {
        Name = StrX(pcTemp->getAttribute(XStrLiteral("Name").unicodeForm())).c_str();
        // check on filter condition
        if (!sFilter || Name.find(sFilter) != std::string::npos) {
            if (strcmp(StrX(pcTemp->getAttribute(XStrLiteral("Value").unicodeForm())).c_str(), "1")
                != 0) {
                vrValues.push_back(false);
            }
            else {
                vrValues.push_back(true);
            }
        }
        pcTemp = FindNextElement(pcTemp, "FCBool");
    }

    return vrValues;
}

std::vector<std::pair<std::string, bool>> ParameterGrp::GetBoolMap(const char* sFilter) const
{
    std::vector<std::pair<std::string, bool>> vrValues;
    if (!_pGroupNode) {
        return vrValues;
    }

    std::string Name;

    DOMElement* pcTemp = FindElement(_pGroupNode, "FCBool");
    while (pcTemp) {
        Name = StrX(pcTemp->getAttribute(XStrLiteral("Name").unicodeForm())).c_str();
        // check on filter condition
        if (!sFilter || Name.find(sFilter) != std::string::npos) {
            if (strcmp(StrX(pcTemp->getAttribute(XStrLiteral("Value").unicodeForm())).c_str(), "1")
                != 0) {
                vrValues.emplace_back(Name, false);
            }
            else {
                vrValues.emplace_back(Name, true);
            }
        }
        pcTemp = FindNextElement(pcTemp, "FCBool");
    }

    return vrValues;
}

long ParameterGrp::GetInt(const char* Name, long lPreset) const
{
    if (!_pGroupNode) {
        return lPreset;
    }

    // check if Element in group
    DOMElement* pcElem = FindElement(_pGroupNode, "FCInt", Name);
    // if not return preset
    if (!pcElem) {
        return lPreset;
    }
    // if yes check the value and return
    return atol(StrX(pcElem->getAttribute(XStrLiteral("Value").unicodeForm())).c_str());
}

void ParameterGrp::SetInt(const char* Name, long lValue)
{
    std::string buf = fmt::sprintf("%li", lValue);
    _SetAttribute(ParamType::FCInt, Name, buf.c_str());
}

std::vector<long> ParameterGrp::GetInts(const char* sFilter) const
{
    std::vector<long> vrValues;
    if (!_pGroupNode) {
        return vrValues;
    }

    std::string Name;

    DOMElement* pcTemp = FindElement(_pGroupNode, "FCInt");
    while (pcTemp) {
        Name = StrX(pcTemp->getAttribute(XStrLiteral("Name").unicodeForm())).c_str();
        // check on filter condition
        if (!sFilter || Name.find(sFilter) != std::string::npos) {
            vrValues.push_back(
                atol(StrX(pcTemp->getAttribute(XStrLiteral("Value").unicodeForm())).c_str())
            );
        }
        pcTemp = FindNextElement(pcTemp, "FCInt");
    }

    return vrValues;
}

std::vector<std::pair<std::string, long>> ParameterGrp::GetIntMap(const char* sFilter) const
{
    std::vector<std::pair<std::string, long>> vrValues;
    if (!_pGroupNode) {
        return vrValues;
    }

    std::string Name;

    DOMElement* pcTemp = FindElement(_pGroupNode, "FCInt");
    while (pcTemp) {
        Name = StrX(pcTemp->getAttribute(XStrLiteral("Name").unicodeForm())).c_str();
        // check on filter condition
        if (!sFilter || Name.find(sFilter) != std::string::npos) {
            vrValues.emplace_back(
                Name,
                (atol(StrX(pcTemp->getAttribute(XStrLiteral("Value").unicodeForm())).c_str()))
            );
        }
        pcTemp = FindNextElement(pcTemp, "FCInt");
    }

    return vrValues;
}

unsigned long ParameterGrp::GetUnsigned(const char* Name, unsigned long lPreset) const
{
    if (!_pGroupNode) {
        return lPreset;
    }

    // check if Element in group
    DOMElement* pcElem = FindElement(_pGroupNode, "FCUInt", Name);
    // if not return preset
    if (!pcElem) {
        return lPreset;
    }

    // if yes check the value and return
    const int base = 10;
    return strtoul(StrX(pcElem->getAttribute(XStrLiteral("Value").unicodeForm())).c_str(), nullptr, base);
}

void ParameterGrp::SetUnsigned(const char* Name, unsigned long lValue)
{
    std::string buf = fmt::sprintf("%lu", lValue);
    _SetAttribute(ParamType::FCUInt, Name, buf.c_str());
}

std::vector<unsigned long> ParameterGrp::GetUnsigneds(const char* sFilter) const
{
    std::vector<unsigned long> vrValues;
    if (!_pGroupNode) {
        return vrValues;
    }

    std::string Name;
    const int base = 10;

    DOMElement* pcTemp = FindElement(_pGroupNode, "FCUInt");
    while (pcTemp) {
        Name = StrX(pcTemp->getAttribute(XStrLiteral("Name").unicodeForm())).c_str();
        // check on filter condition
        if (!sFilter || Name.find(sFilter) != std::string::npos) {
            vrValues.push_back(strtoul(
                StrX(pcTemp->getAttribute(XStrLiteral("Value").unicodeForm())).c_str(),
                nullptr,
                base
            ));
        }
        pcTemp = FindNextElement(pcTemp, "FCUInt");
    }

    return vrValues;
}

std::vector<std::pair<std::string, unsigned long>> ParameterGrp::GetUnsignedMap(const char* sFilter) const
{
    std::vector<std::pair<std::string, unsigned long>> vrValues;
    if (!_pGroupNode) {
        return vrValues;
    }

    std::string Name;
    const int base = 10;

    DOMElement* pcTemp = FindElement(_pGroupNode, "FCUInt");
    while (pcTemp) {
        Name = StrX(pcTemp->getAttribute(XStrLiteral("Name").unicodeForm())).c_str();
        // check on filter condition
        if (!sFilter || Name.find(sFilter) != std::string::npos) {
            vrValues.emplace_back(
                Name,
                (strtoul(
                    StrX(pcTemp->getAttribute(XStrLiteral("Value").unicodeForm())).c_str(),
                    nullptr,
                    base
                ))
            );
        }
        pcTemp = FindNextElement(pcTemp, "FCUInt");
    }

    return vrValues;
}

double ParameterGrp::GetFloat(const char* Name, double dPreset) const
{
    if (!_pGroupNode) {
        return dPreset;
    }

    // check if Element in group
    DOMElement* pcElem = FindElement(_pGroupNode, "FCFloat", Name);
    // if not return preset
    if (!pcElem) {
        return dPreset;
    }
    // if yes check the value and return
    return atof(StrX(pcElem->getAttribute(XStrLiteral("Value").unicodeForm())).c_str());
}

void ParameterGrp::SetFloat(const char* Name, double dValue)
{
    // use %.12f instead of %f to handle values < 1.0e-6
    std::string buf = fmt::sprintf("%.12f", dValue);
    _SetAttribute(ParamType::FCFloat, Name, buf.c_str());
}

std::vector<double> ParameterGrp::GetFloats(const char* sFilter) const
{
    std::vector<double> vrValues;
    if (!_pGroupNode) {
        return vrValues;
    }

    std::string Name;

    DOMElement* pcTemp = FindElement(_pGroupNode, "FCFloat");
    while (pcTemp) {
        Name = StrX(pcTemp->getAttribute(XStrLiteral("Name").unicodeForm())).c_str();
        // check on filter condition
        if (!sFilter || Name.find(sFilter) != std::string::npos) {
            vrValues.push_back(
                atof(StrX(pcTemp->getAttribute(XStrLiteral("Value").unicodeForm())).c_str())
            );
        }
        pcTemp = FindNextElement(pcTemp, "FCFloat");
    }

    return vrValues;
}

std::vector<std::pair<std::string, double>> ParameterGrp::GetFloatMap(const char* sFilter) const
{
    std::vector<std::pair<std::string, double>> vrValues;
    if (!_pGroupNode) {
        return vrValues;
    }

    std::string Name;

    DOMElement* pcTemp = FindElement(_pGroupNode, "FCFloat");
    while (pcTemp) {
        Name = StrX(pcTemp->getAttribute(XStrLiteral("Name").unicodeForm())).c_str();
        // check on filter condition
        if (!sFilter || Name.find(sFilter) != std::string::npos) {
            vrValues.emplace_back(
                Name,
                (atof(StrX(pcTemp->getAttribute(XStrLiteral("Value").unicodeForm())).c_str()))
            );
        }
        pcTemp = FindNextElement(pcTemp, "FCFloat");
    }

    return vrValues;
}


void ParameterGrp::SetASCII(const char* Name, const char* sValue)
{
    if (!_pGroupNode) {
        if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
            FC_WARN("Setting attribute " << "FCText:" << Name << " in an orphan group " << _cName);
        }
        return;
    }
    if (_Clearing) {
        if (FC_LOG_INSTANCE.isEnabled(FC_LOGLEVEL_LOG)) {
            FC_WARN("Adding attribute " << "FCText:" << Name << " while clearing " << GetPath());
        }
        return;
    }

    bool isNew = false;
    DOMElement* pcElem = FindElement(_pGroupNode, "FCText", Name);
    if (!pcElem) {
        pcElem = CreateElement(_pGroupNode, "FCText", Name);
        isNew = true;
    }
    if (pcElem) {
        // and set the value
        DOMNode* pcElem2 = pcElem->getFirstChild();
        if (!pcElem2) {
            XERCES_CPP_NAMESPACE_QUALIFIER DOMDocument* pDocument = _pGroupNode->getOwnerDocument();
            DOMText* pText = pDocument->createTextNode(XUTF8Str(sValue).unicodeForm());
            pcElem->appendChild(pText);
            if (isNew || sValue[0] != 0) {
                _Notify(ParamType::FCText, Name, sValue);
            }
        }
        else if (strcmp(StrXUTF8(pcElem2->getNodeValue()).c_str(), sValue) != 0) {
            pcElem2->setNodeValue(XUTF8Str(sValue).unicodeForm());
            _Notify(ParamType::FCText, Name, sValue);
        }
        // trigger observer
        Notify(Name);
    }
}

std::string ParameterGrp::GetASCII(const char* Name, const char* pPreset) const
{
    if (!_pGroupNode) {
        return pPreset ? pPreset : "";
    }

    // check if Element in group
    DOMElement* pcElem = FindElement(_pGroupNode, "FCText", Name);
    // if not return preset
    if (!pcElem) {
        if (!pPreset) {
            return {};
        }
        return {pPreset};
    }
    // if yes check the value and return
    DOMNode* pcElem2 = pcElem->getFirstChild();
    if (pcElem2) {
        return {StrXUTF8(pcElem2->getNodeValue()).c_str()};
    }
    return {};
}

std::vector<std::string> ParameterGrp::GetASCIIs(const char* sFilter) const
{
    std::vector<std::string> vrValues;
    if (!_pGroupNode) {
        return vrValues;
    }

    std::string Name;

    DOMElement* pcTemp = FindElement(_pGroupNode, "FCText");
    while (pcTemp) {
        Name = StrXUTF8(pcTemp->getAttribute(XStrLiteral("Name").unicodeForm())).c_str();
        // check on filter condition
        if (!sFilter || Name.find(sFilter) != std::string::npos) {
            // retrieve the text element
            DOMNode* pcElem2 = pcTemp->getFirstChild();
            if (pcElem2) {
                vrValues.emplace_back(StrXUTF8(pcElem2->getNodeValue()).c_str());
            }
            else {
                vrValues.emplace_back("");  // For a string, an empty value is possible and allowed
            }
        }
        pcTemp = FindNextElement(pcTemp, "FCText");
    }

    return vrValues;
}

std::vector<std::pair<std::string, std::string>> ParameterGrp::GetASCIIMap(const char* sFilter) const
{
    std::vector<std::pair<std::string, std::string>> vrValues;
    if (!_pGroupNode) {
        return vrValues;
    }

    std::string Name;

    DOMElement* pcTemp = FindElement(_pGroupNode, "FCText");
    while (pcTemp) {
        Name = StrXUTF8(pcTemp->getAttribute(XStrLiteral("Name").unicodeForm())).c_str();
        // check on filter condition
        if (!sFilter || Name.find(sFilter) != std::string::npos) {
            // retrieve the text element
            DOMNode* pcElem2 = pcTemp->getFirstChild();
            if (pcElem2) {
                vrValues.emplace_back(Name, std::string(StrXUTF8(pcElem2->getNodeValue()).c_str()));
            }
            else {
                vrValues.emplace_back(
                    Name,
                    std::string()
                );  // For a string, an empty value is possible and allowed
            }
        }
        pcTemp = FindNextElement(pcTemp, "FCText");
    }

    return vrValues;
}

//**************************************************************************
// Access methods

void ParameterGrp::RemoveASCII(const char* Name)
{
    if (!_pGroupNode) {
        return;
    }

    // check if Element in group
    DOMElement* pcElem = FindElement(_pGroupNode, "FCText", Name);
    // if not return
    if (!pcElem) {
        return;
    }

    DOMNode* node = _pGroupNode->removeChild(pcElem);
    node->release();

    // trigger observer
    _Notify(ParamType::FCText, Name, nullptr);
    Notify(Name);
}

void ParameterGrp::RemoveBool(const char* Name)
{
    if (!_pGroupNode) {
        return;
    }

    // check if Element in group
    DOMElement* pcElem = FindElement(_pGroupNode, "FCBool", Name);
    // if not return
    if (!pcElem) {
        return;
    }

    DOMNode* node = _pGroupNode->removeChild(pcElem);
    node->release();

    // trigger observer
    _Notify(ParamType::FCBool, Name, nullptr);
    Notify(Name);
}


void ParameterGrp::RemoveFloat(const char* Name)
{
    if (!_pGroupNode) {
        return;
    }

    // check if Element in group
    DOMElement* pcElem = FindElement(_pGroupNode, "FCFloat", Name);
    // if not return
    if (!pcElem) {
        return;
    }

    DOMNode* node = _pGroupNode->removeChild(pcElem);
    node->release();

    // trigger observer
    _Notify(ParamType::FCFloat, Name, nullptr);
    Notify(Name);
}

void ParameterGrp::RemoveInt(const char* Name)
{
    if (!_pGroupNode) {
        return;
    }

    // check if Element in group
    DOMElement* pcElem = FindElement(_pGroupNode, "FCInt", Name);
    // if not return
    if (!pcElem) {
        return;
    }

    DOMNode* node = _pGroupNode->removeChild(pcElem);
    node->release();

    // trigger observer
    _Notify(ParamType::FCInt, Name, nullptr);
    Notify(Name);
}

void ParameterGrp::RemoveUnsigned(const char* Name)
{
    if (!_pGroupNode) {
        return;
    }

    // check if Element in group
    DOMElement* pcElem = FindElement(_pGroupNode, "FCUInt", Name);
    // if not return
    if (!pcElem) {
        return;
    }

    DOMNode* node = _pGroupNode->removeChild(pcElem);
    node->release();

    // trigger observer
    _Notify(ParamType::FCUInt, Name, nullptr);
    Notify(Name);
}

Base::Color ParameterGrp::GetColor(const char* Name, Base::Color lPreset) const
{
    auto packed = GetUnsigned(Name, lPreset.getPackedValue());

    return Color(static_cast<uint32_t>(packed));
}

void ParameterGrp::SetColor(const char* Name, Base::Color lValue)
{
    SetUnsigned(Name, lValue.getPackedValue());
}

std::vector<Base::Color> ParameterGrp::GetColors(const char* sFilter) const
{
    auto packed = GetUnsigneds(sFilter);
    std::vector<Base::Color> result;

    std::transform(
        packed.begin(),
        packed.end(),
        std::back_inserter(result),
        [](const unsigned long lValue) { return Color(static_cast<uint32_t>(lValue)); }
    );

    return result;
}

std::vector<std::pair<std::string, Base::Color>> ParameterGrp::GetColorMap(const char* sFilter) const
{
    auto packed = GetUnsignedMap(sFilter);
    std::vector<std::pair<std::string, Base::Color>> result;

    std::transform(
        packed.begin(),
        packed.end(),
        std::back_inserter(result),
        [](const std::pair<std::string, unsigned long>& lValue) {
            return std::make_pair(lValue.first, Color(static_cast<uint32_t>(lValue.second)));
        }
    );

    return result;
}

void ParameterGrp::RemoveColor(const char* Name)
{
    RemoveUnsigned(Name);
}

void ParameterGrp::RemoveGrp(const char* Name)
{
    if (!_pGroupNode) {
        return;
    }

    auto it = _GroupMap.find(Name);
    if (it == _GroupMap.end()) {
        return;
    }

    // If this or any of its children is referenced by an observer we do not
    // delete the handle, just in case the group is later added again, or else
    // those existing observer won't get any notification. BUT, we DO delete
    // the underlying xml elements, so that we don't save the empty group
    // later.
    it->second->Clear(false);
    if (!it->second->_Detached) {
        it->second->_Detached = true;
        _pGroupNode->removeChild(it->second->_pGroupNode);
    }
    if (it->second->ShouldRemove()) {
        it->second->_Parent = nullptr;
        it->second->_Manager = nullptr;
        _GroupMap.erase(it);
    }

    // trigger observer
    Notify(Name);
}

bool ParameterGrp::RenameGrp(const char* OldName, const char* NewName)
{
    if (!_pGroupNode) {
        return false;
    }

    auto it = _GroupMap.find(OldName);
    if (it == _GroupMap.end()) {
        return false;
    }
    auto jt = _GroupMap.find(NewName);
    if (jt != _GroupMap.end()) {
        return false;
    }

    // rename group handle
    _GroupMap[NewName] = _GroupMap[OldName];
    _GroupMap.erase(OldName);
    _GroupMap[NewName]->_cName = NewName;

    // check if Element in group
    DOMElement* pcElem = FindElement(_pGroupNode, "FCParamGroup", OldName);
    if (pcElem) {
        pcElem->setAttribute(XStrLiteral("Name").unicodeForm(), XStr(NewName).unicodeForm());
    }

    _Notify(ParamType::FCGroup, NewName, OldName);
    return true;
}

void ParameterGrp::Clear(bool notify)
{
    if (!_pGroupNode) {
        return;
    }

    Base::StateLocker guard(_Clearing);

    // early trigger notification of group removal when all its children
    // hierarchies are intact.
    _Notify(ParamType::FCGroup, nullptr, nullptr);

    // checking on references
    for (auto it = _GroupMap.begin(); it != _GroupMap.end();) {
        // If a group handle is referenced by some observer, then do not remove
        // it but clear it, so that any existing observer can still get
        // notification if the group is later on add back. We do remove the
        // underlying xml element from its parent so that we won't save this
        // empty group.
        it->second->Clear(notify);
        if (!it->second->_Detached) {
            it->second->_Detached = true;
            _pGroupNode->removeChild(it->second->_pGroupNode);
        }
        if (!it->second->ShouldRemove()) {
            ++it;
        }
        else {
            it->second->_Parent = nullptr;
            it->second->_Manager = nullptr;
            it = _GroupMap.erase(it);
        }
    }

    // Remove the rest of non-group nodes;
    std::vector<std::pair<ParamType, std::string>> params;
    for (DOMNode *child = _pGroupNode->getFirstChild(), *next = child; child != nullptr;
         child = next) {
        next = next->getNextSibling();
        ParamType type = TypeValue(StrX(child->getNodeName()).c_str());
        if (type != ParamType::FCInvalid && type != ParamType::FCGroup) {
            params.emplace_back(
                type,
                StrX(child->getAttributes()->getNamedItem(XStrLiteral("Name").unicodeForm())->getNodeValue())
                    .c_str()
            );
        }
        DOMNode* node = _pGroupNode->removeChild(child);
        node->release();
    }

    for (auto& v : params) {
        _Notify(v.first, v.second.c_str(), nullptr);
        if (notify) {
            Notify(v.second.c_str());
        }
    }

    // trigger observer
    Notify("");
}

//**************************************************************************
// Access methods

bool ParameterGrp::ShouldRemove() const
{
    if (this->getRefCount() > 1) {
        return false;
    }

    return std::all_of(_GroupMap.cbegin(), _GroupMap.cend(), [](const auto& it) {
        return it.second->ShouldRemove();
    });
}

XERCES_CPP_NAMESPACE_QUALIFIER DOMElement* ParameterGrp::FindElement(
    XERCES_CPP_NAMESPACE_QUALIFIER DOMElement* Start,
    const char* Type,
    const char* Name
) const
{
    if (XMLString::compareString(Start->getNodeName(), XStrLiteral("FCParamGroup").unicodeForm()) != 0
        && XMLString::compareString(Start->getNodeName(), XStrLiteral("FCParameters").unicodeForm())
            != 0) {
        Base::Console().warning(
            "FindElement: %s cannot have the element %s of type %s\n",
            StrX(Start->getNodeName()).c_str(),
            Name,
            Type
        );
        return nullptr;
    }
    const XStr xType(Type);
    const XStr xName(Name);
    for (DOMNode* clChild = Start->getFirstChild(); clChild != nullptr;
         clChild = clChild->getNextSibling()) {
        if (clChild->getNodeType() == DOMNode::ELEMENT_NODE) {
            // the right node Type
            if (!XMLString::compareString(xType.unicodeForm(), clChild->getNodeName())) {
                auto attrs = clChild->getAttributes();
                if (attrs->getLength() > 0) {
                    if (Name) {
                        DOMNode* attr = attrs->getNamedItem(XStrLiteral("Name").unicodeForm());
                        if (attr
                            && !XMLString::compareString(xName.unicodeForm(), attr->getNodeValue())) {
                            return dynamic_cast<DOMElement*>(clChild);
                        }
                    }
                    else {
                        return dynamic_cast<DOMElement*>(clChild);
                    }
                }
            }
        }
    }
    return nullptr;
}

XERCES_CPP_NAMESPACE_QUALIFIER DOMElement* ParameterGrp::FindNextElement(
    XERCES_CPP_NAMESPACE_QUALIFIER DOMNode* Prev,
    const char* Type
) const
{
    DOMNode* clChild = Prev;
    if (!clChild) {
        return nullptr;
    }

    const XStr xType(Type);
    while ((clChild = clChild->getNextSibling()) != nullptr) {
        if (clChild->getNodeType() == DOMNode::ELEMENT_NODE) {
            // the right node Type
            if (!XMLString::compareString(xType.unicodeForm(), clChild->getNodeName())) {
                return dynamic_cast<DOMElement*>(clChild);
            }
        }
    }
    return nullptr;
}

XERCES_CPP_NAMESPACE_QUALIFIER DOMElement* ParameterGrp::FindOrCreateElement(
    XERCES_CPP_NAMESPACE_QUALIFIER DOMElement* Start,
    const char* Type,
    const char* Name
)
{
    // first try to find it
    DOMElement* pcElem = FindElement(Start, Type, Name);
    if (pcElem) {
        return pcElem;
    }

    return CreateElement(Start, Type, Name);
}

XERCES_CPP_NAMESPACE_QUALIFIER DOMNode* ParameterGrp::FindAttribute(
    XERCES_CPP_NAMESPACE_QUALIFIER DOMNode* Node,
    const char* Name
) const
{
    DOMNamedNodeMap* attr = Node->getAttributes();
    if (attr) {
        return attr->getNamedItem(XStr(Name).unicodeForm());
    }
    return nullptr;
}

std::vector<std::pair<ParameterGrp::ParamType, std::string>> ParameterGrp::GetParameterNames(
    const char* sFilter
) const
{
    std::vector<std::pair<ParameterGrp::ParamType, std::string>> res;
    if (!_pGroupNode) {
        return res;
    }

    std::string Name;

    for (DOMNode* clChild = _pGroupNode->getFirstChild(); clChild != nullptr;
         clChild = clChild->getNextSibling()) {
        if (clChild->getNodeType() == DOMNode::ELEMENT_NODE) {
            StrX type(clChild->getNodeName());
            ParamType Type = TypeValue(type.c_str());
            if (Type != ParamType::FCInvalid && Type != ParamType::FCGroup) {
                if (clChild->getAttributes()->getLength() > 0) {
                    StrX name(clChild->getAttributes()
                                  ->getNamedItem(XStrLiteral("Name").unicodeForm())
                                  ->getNodeValue());
                    if (!sFilter || strstr(name.c_str(), sFilter)) {
                        res.emplace_back(Type, name.c_str());
                    }
                }
            }
        }
    }
    return res;
}

void ParameterGrp::NotifyAll()
{
    // get all ints and notify
    std::vector<std::pair<std::string, long>> IntMap = GetIntMap();
    for (const auto& it : IntMap) {
        Notify(it.first.c_str());
    }

    // get all booleans and notify
    std::vector<std::pair<std::string, bool>> BoolMap = GetBoolMap();
    for (const auto& it : BoolMap) {
        Notify(it.first.c_str());
    }

    // get all Floats and notify
    std::vector<std::pair<std::string, double>> FloatMap = GetFloatMap();
    for (const auto& it : FloatMap) {
        Notify(it.first.c_str());
    }

    // get all strings and notify
    std::vector<std::pair<std::string, std::string>> StringMap = GetASCIIMap();
    for (const auto& it : StringMap) {
        Notify(it.first.c_str());
    }

    // get all uints and notify
    std::vector<std::pair<std::string, unsigned long>> UIntMap = GetUnsignedMap();
    for (const auto& it : UIntMap) {
        Notify(it.first.c_str());
    }
}

void ParameterGrp::_Reset()
{
    _pGroupNode = nullptr;
    for (auto& v : _GroupMap) {
        v.second->_Reset();
    }
}

//**************************************************************************
//**************************************************************************
// ParameterSerializer
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ParameterSerializer::ParameterSerializer(std::string fn)
    : filename(std::move(fn))
{}

ParameterSerializer::~ParameterSerializer() = default;

void ParameterSerializer::SaveDocument(const ParameterManager& mgr)
{
    mgr.SaveDocument(filename.c_str());
}

int ParameterSerializer::LoadDocument(ParameterManager& mgr)
{
    return mgr.LoadDocument(filename.c_str());
}

bool ParameterSerializer::LoadOrCreateDocument(ParameterManager& mgr)
{
    return mgr.LoadOrCreateDocument(filename.c_str());
}

//**************************************************************************
//**************************************************************************
// ParameterManager
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

static XercesDOMParser::ValSchemes gValScheme = XercesDOMParser::Val_Auto;  // NOLINT

//**************************************************************************
// Construction/Destruction

/** Default construction
 */
ParameterManager::ParameterManager()
{
    _Manager = this;

    // initialize the XML system
    Init();

    // ---------------------------------------------------------------------------
    //  Local data
    //
    //  gXmlFile
    //      The path to the file to parser. Set via command line.
    //
    //  gDoNamespaces
    //      Indicates whether namespace processing should be done.
    //
    //  gDoSchema
    //      Indicates whether schema processing should be done.
    //
    //  gSchemaFullChecking
    //      Indicates whether full schema constraint checking should be done.
    //
    //  gDoCreate
    //      Indicates whether entity reference nodes needs to be created or not.
    //      Defaults to false.
    //
    //  gOutputEncoding
    //      The encoding we are to output in. If not set on the command line,
    //      then it is defaults to the encoding of the input XML file.
    //
    //  gMyEOLSequence
    //      The end of line sequence we are to output.
    //
    //  gSplitCdataSections
    //      Indicates whether split-cdata-sections is to be enabled or not.
    //
    //  gDiscardDefaultContent
    //      Indicates whether default content is discarded or not.
    //
    //  gUseFilter
    //      Indicates if user wants to plug in the DOMPrintFilter.
    //
    //  gValScheme
    //      Indicates what validation scheme to use. It defaults to 'auto', but
    //      can be set via the -v= command.
    //
    // ---------------------------------------------------------------------------

    // NOLINTBEGIN
    gIgnoreSave = false;
    gDoNamespaces = false;
    gDoSchema = false;
    gSchemaFullChecking = false;
    gDoCreate = true;

    gOutputEncoding = nullptr;
    gMyEOLSequence = nullptr;

    gSplitCdataSections = true;
    gDiscardDefaultContent = true;
    gUseFilter = true;
    gFormatPrettyPrint = true;
    // NOLINTEND
}

/** Destruction
 * complete destruction of the object
 */
ParameterManager::~ParameterManager()
{
    _Reset();
    delete _pDocument;
    delete paramSerializer;
}

Base::Reference<ParameterManager> ParameterManager::Create()
{
    return {new ParameterManager()};
}

void ParameterManager::Init()
{
    static bool Init = false;
    if (!Init) {
        try {
            XMLPlatformUtils::Initialize();
        }
        catch (const XMLException& toCatch) {
#if defined(FC_OS_LINUX) || defined(FC_OS_CYGWIN)
            std::ostringstream err;
#else
            std::stringstream err;
#endif
            char* pMsg = XMLString::transcode(toCatch.getMessage());
            err << "Error during Xerces-c Initialization.\n"
                << "  Exception message:" << pMsg;
            XMLString::release(&pMsg);
            throw XMLBaseException(err.str().c_str());
        }
        Init = true;
    }
}

void ParameterManager::Terminate()
{
    XMLTools::terminate();
    XMLPlatformUtils::Terminate();
}

//**************************************************************************
// Serializer handling

void ParameterManager::SetSerializer(ParameterSerializer* ps)
{
    if (paramSerializer != ps) {
        delete paramSerializer;
    }
    paramSerializer = ps;
}

bool ParameterManager::HasSerializer() const
{
    return (paramSerializer != nullptr);
}

const std::string& ParameterManager::GetSerializeFileName() const
{
    static const std::string _dummy;
    return paramSerializer ? paramSerializer->GetFileName() : _dummy;
}

int ParameterManager::LoadDocument()
{
    if (paramSerializer) {
        return paramSerializer->LoadDocument(*this);
    }

    return -1;
}

bool ParameterManager::LoadOrCreateDocument()
{
    if (paramSerializer) {
        return paramSerializer->LoadOrCreateDocument(*this);
    }

    return false;
}

void ParameterManager::SaveDocument() const
{
    if (paramSerializer) {
        paramSerializer->SaveDocument(*this);
    }
}

void ParameterManager::SetIgnoreSave(bool value)
{
    gIgnoreSave = value;
}

bool ParameterManager::IgnoreSave() const
{
    return gIgnoreSave;
}

namespace
{
QString getLockFile(const Base::FileInfo& file)
{
    QFileInfo fi(QDir::tempPath(), QString::fromStdString(file.fileName() + ".lock"));
    return fi.absoluteFilePath();
}

int getTimeout()
{
    const int timeout = 5000;
    return timeout;
}
}  // namespace

//**************************************************************************
// Document handling

bool ParameterManager::LoadOrCreateDocument(const char* sFileName)
{
    Base::FileInfo file(sFileName);
    if (file.exists()) {
        LoadDocument(sFileName);
        return false;
    }

    CreateDocument();
    return true;
}

int ParameterManager::LoadDocument(const char* sFileName)
{
    try {
        Base::FileInfo file(sFileName);
        QLockFile lock(getLockFile(file));
        if (!lock.tryLock(getTimeout())) {
            // Continue with empty config
            CreateDocument();
            SetIgnoreSave(true);
            std::cerr << "Failed to access file for reading: " << sFileName << std::endl;
            return 1;
        }
#if defined(FC_OS_WIN32)
        std::wstring name = file.toStdWString();
        // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
        LocalFileInputSource inputSource(reinterpret_cast<const XMLCh*>(name.c_str()));
#else
        LocalFileInputSource inputSource(XStr(file.filePath().c_str()).unicodeForm());
#endif
        return LoadDocument(inputSource);
    }
    catch (const Base::Exception& e) {
        std::cerr << e.what() << std::endl;
        throw;
    }
    catch (...) {
        std::cerr << "An error occurred during parsing\n " << std::endl;
        throw;
    }
}

class NoOpEntityResolver: public XERCES_CPP_NAMESPACE_QUALIFIER EntityResolver
{
public:
    InputSource* resolveEntity(const XMLCh* const publicId, const XMLCh* const systemId) override
    {
        (void)publicId;
        (void)systemId;
        return nullptr;  // Block all external entity resolution
    }
};

int ParameterManager::LoadDocument(const XERCES_CPP_NAMESPACE_QUALIFIER InputSource& inputSource)
{
    //
    //  Create our parser, then attach an error handler to the parser.
    //  The parser will call back to methods of the ErrorHandler if it
    //  discovers errors during the course of parsing the XML document.
    //
    auto parser = std::make_unique<XercesDOMParser>();
    auto entityBlocker = std::make_unique<NoOpEntityResolver>();
    parser->setValidationScheme(gValScheme);
    parser->setDoNamespaces(gDoNamespaces);
    parser->setDoSchema(gDoSchema);
    parser->setValidationSchemaFullChecking(gSchemaFullChecking);
    parser->setCreateEntityReferenceNodes(gDoCreate);
    parser->setDisableDefaultEntityResolution(true);
    parser->setEntityResolver(entityBlocker.get());

    auto errReporter = std::make_unique<DOMTreeErrorReporter>();
    parser->setErrorHandler(errReporter.get());

    //
    //  Parse the XML file, catching any XML exceptions that might propagate
    //  out of it.
    //
    bool errorsOccurred = false;
    try {
        parser->parse(inputSource);
    }
    catch (const XMLException& e) {
        std::cerr << "An error occurred during parsing\n   Message: " << StrX(e.getMessage()) << "\n";
        errorsOccurred = true;
    }
    catch (const DOMException& e) {
        std::cerr << "A DOM error occurred during parsing\n   DOMException code: " << e.code << "\n";
        errorsOccurred = true;
    }
    catch (...) {
        std::cerr << "An error occurred during parsing\n " << "\n";
        errorsOccurred = true;
    }

    if (errorsOccurred) {
        return 0;
    }

    _pDocument = parser->adoptDocument();

    if (!_pDocument) {
        throw XMLBaseException("Malformed Parameter document: Invalid document");
    }

    DOMElement* rootElem = _pDocument->getDocumentElement();
    if (!rootElem) {
        throw XMLBaseException("Malformed Parameter document: Root group not found");
    }

    _pGroupNode = FindElement(rootElem, "FCParamGroup", "Root");

    if (!_pGroupNode) {
        throw XMLBaseException("Malformed Parameter document: Root group not found");
    }

    return 1;
}

void ParameterManager::SaveDocument(const char* sFileName) const
{
    try {
        Base::FileInfo file(sFileName);
        QLockFile lock(getLockFile(file));
        if (!lock.tryLock(getTimeout())) {
            std::cerr << "Failed to access file for writing: " << sFileName << std::endl;
            return;
        }
        //
        // Plug in a format target to receive the resultant
        // XML stream from the serializer.
        //
        // LocalFileFormatTarget prints the resultant XML stream
        // to a file once it receives any thing from the serializer.
        //
        XMLFormatTarget* myFormTarget {};
#if defined(FC_OS_WIN32)
        std::wstring name = file.toStdWString();
        // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
        myFormTarget = new LocalFileFormatTarget(reinterpret_cast<const XMLCh*>(name.c_str()));
#else
        myFormTarget = new LocalFileFormatTarget(file.filePath().c_str());
#endif
        SaveDocument(myFormTarget);
        delete myFormTarget;
    }
    catch (XMLException& e) {
        std::cerr << "An error occurred during creation of output transcoder. Msg is:" << std::endl
                  << StrX(e.getMessage()) << std::endl;
    }
}

void ParameterManager::SaveDocument(XMLFormatTarget* pFormatTarget) const
{
    try {
        std::unique_ptr<DOMPrintFilter> myFilter;
        std::unique_ptr<DOMErrorHandler> myErrorHandler;

        // NOLINTBEGIN
        // get a serializer, an instance of DOMWriter
        XMLCh tempStr[100];
        XMLString::transcode("LS", tempStr, 99);
        DOMImplementation* impl = DOMImplementationRegistry::getDOMImplementation(tempStr);
        DOMLSSerializer* theSerializer = static_cast<DOMImplementationLS*>(impl)->createLSSerializer();
        // NOLINTEND

        // set user specified end of line sequence and output encoding
        theSerializer->setNewLine(gMyEOLSequence);


        //
        // do the serialization through DOMWriter::writeNode();
        //
        if (_pDocument) {
            DOMLSOutput* theOutput = static_cast<DOMImplementationLS*>(impl)->createLSOutput();
            theOutput->setEncoding(gOutputEncoding);

            if (gUseFilter) {
                myFilter = std::make_unique<DOMPrintFilter>(
                    DOMNodeFilter::SHOW_ELEMENT | DOMNodeFilter::SHOW_ATTRIBUTE
                    | DOMNodeFilter::SHOW_DOCUMENT_TYPE | DOMNodeFilter::SHOW_TEXT
                );
                theSerializer->setFilter(myFilter.get());
            }

            // plug in user's own error handler
            myErrorHandler = std::make_unique<DOMPrintErrorHandler>();
            DOMConfiguration* config = theSerializer->getDomConfig();

            // NOLINTBEGIN
            config->setParameter(XMLUni::fgDOMErrorHandler, myErrorHandler.get());

            // set feature if the serializer supports the feature/mode
            if (config->canSetParameter(XMLUni::fgDOMWRTSplitCdataSections, gSplitCdataSections)) {
                config->setParameter(XMLUni::fgDOMWRTSplitCdataSections, gSplitCdataSections);
            }

            if (config->canSetParameter(XMLUni::fgDOMWRTDiscardDefaultContent, gDiscardDefaultContent)) {
                config->setParameter(XMLUni::fgDOMWRTDiscardDefaultContent, gDiscardDefaultContent);
            }

            if (config->canSetParameter(XMLUni::fgDOMWRTFormatPrettyPrint, gFormatPrettyPrint)) {
                config->setParameter(XMLUni::fgDOMWRTFormatPrettyPrint, gFormatPrettyPrint);
            }
            // NOLINTEND

            theOutput->setByteStream(pFormatTarget);
            theSerializer->write(_pDocument, theOutput);

            theOutput->release();
        }

        theSerializer->release();
    }
    catch (XMLException& e) {
        std::cerr << "An error occurred during creation of output transcoder. Msg is:" << std::endl
                  << StrX(e.getMessage()) << std::endl;
    }
}

void ParameterManager::CreateDocument()
{
    // creating a document from screatch
    DOMImplementation* impl = DOMImplementationRegistry::getDOMImplementation(
        XStrLiteral("Core").unicodeForm()
    );
    delete _pDocument;
    _pDocument = impl->createDocument(
        nullptr,                                    // root element namespace URI.
        XStrLiteral("FCParameters").unicodeForm(),  // root element name
        nullptr
    );  // document type object (DTD).

    // creating the node for the root group
    DOMElement* rootElem = _pDocument->getDocumentElement();
    _pGroupNode = _pDocument->createElement(XStrLiteral("FCParamGroup").unicodeForm());
    _pGroupNode->setAttribute(XStrLiteral("Name").unicodeForm(), XStrLiteral("Root").unicodeForm());
    rootElem->appendChild(_pGroupNode);
}

void ParameterManager::CheckDocument() const
{
    if (!_pDocument) {
        return;
    }

    try {
        //
        // Plug in a format target to receive the resultant
        // XML stream from the serializer.
        //
        // LocalFileFormatTarget prints the resultant XML stream
        // to a file once it receives any thing from the serializer.
        //
        MemBufFormatTarget myFormTarget;
        SaveDocument(&myFormTarget);

        // Either use the file saved on disk or write the current XML into a buffer in memory
        // const char* xmlFile = "...";
        MemBufInputSource xmlFile(myFormTarget.getRawBuffer(), myFormTarget.getLen(), "(memory)");

        // Either load the XSD file from disk or use the built-in string
        // const char* xsdFile = "...";
        std::string xsdStr(xmlSchemeString);  // NOLINT
        // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
        MemBufInputSource xsdFile(
            reinterpret_cast<const XMLByte*>(xsdStr.c_str()),
            xsdStr.size(),
            "Parameter.xsd"
        );

        // See
        // http://apache-xml-project.6118.n7.nabble.com/validating-xml-with-xsd-schema-td17515.html
        //
        XercesDOMParser parser;
        Grammar* grammar = parser.loadGrammar(xsdFile, Grammar::SchemaGrammarType, true);
        if (!grammar) {
            Base::Console().error("Grammar file cannot be loaded.\n");
            return;
        }

        parser.setExternalNoNamespaceSchemaLocation("Parameter.xsd");
        // parser.setExitOnFirstFatalError(true);
        // parser.setValidationConstraintFatal(true);
        parser.cacheGrammarFromParse(true);
        parser.setValidationScheme(XercesDOMParser::Val_Auto);
        parser.setDoNamespaces(true);
        parser.setDoSchema(true);
        parser.setDisableDefaultEntityResolution(true);

        DOMTreeErrorReporter errHandler;
        parser.setErrorHandler(&errHandler);
        parser.parse(xmlFile);

        if (parser.getErrorCount() > 0) {
            Base::Console().error(
                "Unexpected XML structure detected: %zu errors\n",
                parser.getErrorCount()
            );
        }
    }
    catch (XMLException& e) {
        std::cerr << "An error occurred while checking document. Msg is:" << std::endl
                  << StrX(e.getMessage()) << std::endl;
    }
}


//**************************************************************************
//**************************************************************************
// DOMTreeErrorReporter
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

void DOMTreeErrorReporter::warning(const SAXParseException& /*exc*/)
{
    //
    // Ignore all warnings.
    //
}

void DOMTreeErrorReporter::error(const SAXParseException& toCatch)
{
    fSawErrors = true;
    std::cerr << "Error at file \"" << StrX(toCatch.getSystemId()) << "\", line "
              << toCatch.getLineNumber() << ", column " << toCatch.getColumnNumber()
              << "\n   Message: " << StrX(toCatch.getMessage()) << std::endl;
}

void DOMTreeErrorReporter::fatalError(const SAXParseException& toCatch)
{
    fSawErrors = true;
    std::cerr << "Fatal Error at file \"" << StrX(toCatch.getSystemId()) << "\", line "
              << toCatch.getLineNumber() << ", column " << toCatch.getColumnNumber()
              << "\n   Message: " << StrX(toCatch.getMessage()) << std::endl;
}

void DOMTreeErrorReporter::resetErrors()
{
    // No-op in this case
}


//**************************************************************************
//**************************************************************************
// DOMPrintFilter
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
DOMPrintFilter::DOMPrintFilter(ShowType whatToShow)
    : fWhatToShow(whatToShow)
{}

DOMPrintFilter::FilterAction DOMPrintFilter::acceptNode(const DOMNode* node) const
{
    if (XMLString::compareString(node->getNodeName(), XStrLiteral("FCParameters").unicodeForm())
        == 0) {
        // This node is supposed to have a single FCParamGroup and two text nodes.
        // Over time it can happen that the text nodes collect extra newlines.
        const DOMNodeList* children = node->getChildNodes();
        for (XMLSize_t i = 0; i < children->getLength(); i++) {
            DOMNode* child = children->item(i);
            if (child->getNodeType() == DOMNode::TEXT_NODE) {
                child->setNodeValue(XStrLiteral("\n").unicodeForm());
            }
        }
    }

    // clang-format off
    switch (node->getNodeType()) {
        case DOMNode::TEXT_NODE: {
            // Filter out text element if it is under a group node. Note text xml
            // element is plain text in between tags, and we do not store any text
            // there.
            auto parent = node->getParentNode();
            if (parent && XMLString::compareString(parent->getNodeName(),
                                                   XStrLiteral("FCParamGroup").unicodeForm()) == 0) {
                return DOMNodeFilter::FILTER_REJECT;
            }
            return DOMNodeFilter::FILTER_ACCEPT;
        }
        case DOMNode::DOCUMENT_TYPE_NODE:
        case DOMNode::DOCUMENT_NODE: {
            return DOMNodeFilter::FILTER_REJECT;  // no effect
        }
        default: {
            return DOMNodeFilter::FILTER_ACCEPT;
        }
    }
    // clang-format on
}

//**************************************************************************
//**************************************************************************
// DOMPrintErrorHandler
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++


bool DOMPrintErrorHandler::handleError(const DOMError& domError)
{
    // Display whatever error message passed from the serializer
    char* msg = XMLString::transcode(domError.getMessage());
    std::cout << msg << std::endl;
    XMLString::release(&msg);

    // Instructs the serializer to continue serialization if possible.
    return true;
}
