#include "TargetDescriptionFile.hpp"

#include <QFile>

#include "src/Services/PathService.hpp"
#include "src/Services/StringService.hpp"

#include "src/Targets/TargetMemory.hpp"
#include "src/Targets/TargetPinoutDescriptor.hpp"
#include "src/Helpers/BiMap.hpp"
#include "src/Logger/Logger.hpp"

#include "Exceptions/TargetDescriptionParsingFailureException.hpp"
#include "Exceptions/InvalidTargetDescriptionDataException.hpp"

namespace Targets::TargetDescription
{
    using namespace Exceptions;
    using namespace ::Exceptions;

    using Services::StringService;

    TargetDescriptionFile::TargetDescriptionFile(const std::string& xmlFilePath) {
        this->init(xmlFilePath);
    }

    TargetDescriptionFile::TargetDescriptionFile(const QDomDocument& xml) {
        this->init(xml);
    }

    const std::string& TargetDescriptionFile::getName() const {
        return this->getDeviceAttribute("name");
    }

    TargetFamily TargetDescriptionFile::getFamily() const {
        const auto& familyName = this->getDeviceAttribute("family");

        if (familyName == "AVR8") {
            return TargetFamily::AVR_8;
        }

        if (familyName == "RISCV") {
            return TargetFamily::RISC_V;
        }

        throw InvalidTargetDescriptionDataException{"Failed to resolve target family - invalid family name"};
    }

    std::optional<std::string> TargetDescriptionFile::tryGetVendorName() const {
        return this->tryGetDeviceAttribute("vendor");
    }

    const std::string& TargetDescriptionFile::getVendorName() const {
        return this->getDeviceAttribute("vendor");
    }

    std::optional<std::reference_wrapper<const PropertyGroup>> TargetDescriptionFile::tryGetPropertyGroup(
        std::string_view keyStr
    ) const {
        const auto keys = StringService::split(keyStr, '.');

        const auto firstSubgroupIt = this->propertyGroupsByKey.find(*keys.begin());
        return firstSubgroupIt != this->propertyGroupsByKey.end()
            ? keys.size() > 1
                ? firstSubgroupIt->second.tryGetSubgroup(keys | std::ranges::views::drop(1))
                : std::optional{std::cref(firstSubgroupIt->second)}
            : std::nullopt;
    }

    const PropertyGroup& TargetDescriptionFile::getPropertyGroup(std::string_view keyStr) const {
        const auto propertyGroup = this->tryGetPropertyGroup(keyStr);

        if (!propertyGroup.has_value()) {
            throw InvalidTargetDescriptionDataException{
                "Failed to get property group \"" + std::string{keyStr} + "\" from TDF - property group not found"
            };
        }

        return propertyGroup->get();
    }

    std::optional<std::reference_wrapper<const Property>> TargetDescriptionFile::tryGetProperty(
        std::string_view groupKey,
        std::string_view propertyKey
    ) const {
        const auto propertyGroup = this->tryGetPropertyGroup(groupKey);

        if (!propertyGroup.has_value()) {
            return std::nullopt;
        }

        return propertyGroup->get().tryGetProperty(propertyKey);
    }

    const Property& TargetDescriptionFile::getProperty(std::string_view groupKey, std::string_view propertyKey) const {
        const auto property = this->tryGetProperty(groupKey, propertyKey);

        if (!property.has_value()) {
            throw InvalidTargetDescriptionDataException{
                "Failed to get property \"" + std::string{propertyKey} + "\" from group \"" + std::string{groupKey}
                    + "\", from TDF - property/group not found"
            };
        }

        return property->get();
    }

    std::optional<std::reference_wrapper<const AddressSpace>> TargetDescriptionFile::tryGetAddressSpace(
        std::string_view key
    ) const {
        const auto addressSpaceIt = this->addressSpacesByKey.find(key);
        return addressSpaceIt != this->addressSpacesByKey.end()
            ? std::optional{std::cref(addressSpaceIt->second)}
            : std::nullopt;
    }

    const AddressSpace& TargetDescriptionFile::getAddressSpace(std::string_view key) const {
        const auto addressSpace = this->tryGetAddressSpace(key);

        if (!addressSpace.has_value()) {
            throw InvalidTargetDescriptionDataException{
                "Failed to get address space \"" + std::string{key} + "\" from TDF - address space not found"
            };
        }

        return addressSpace->get();
    }

    std::optional<std::reference_wrapper<const MemorySegment>> TargetDescriptionFile::tryGetMemorySegment(
        std::string_view addressSpaceKey,
        std::string_view segmentKey
    ) const {
        const auto addressSpace = this->tryGetAddressSpace(addressSpaceKey);

        if (!addressSpace.has_value()) {
            return std::nullopt;
        }

        return addressSpace->get().tryGetMemorySegment(segmentKey);
    }

    const MemorySegment& TargetDescriptionFile::getMemorySegment(
        std::string_view addressSpaceKey,
        std::string_view segmentKey
    ) const {
        const auto segment = this->tryGetMemorySegment(addressSpaceKey, segmentKey);

        if (!segment.has_value()) {
            throw InvalidTargetDescriptionDataException{
                "Failed to get memory segment \"" + std::string{segmentKey} + "\" from address space \""
                    + std::string{addressSpaceKey} + "\" from TDF"
            };
        }

        return segment->get();
    }

    std::set<TargetPhysicalInterface> TargetDescriptionFile::getPhysicalInterfaces() const {
        static const auto physicalInterfacesByValue = BiMap<std::string, TargetPhysicalInterface>{
            {"updi", TargetPhysicalInterface::UPDI},
            {"debug_wire", TargetPhysicalInterface::DEBUG_WIRE},
            {"jtag", TargetPhysicalInterface::JTAG},
            {"pdi", TargetPhysicalInterface::PDI},
            {"isp", TargetPhysicalInterface::ISP},
            {"sdi", TargetPhysicalInterface::SDI},
        };

        auto output = std::set<TargetPhysicalInterface>{};

        for (const auto& physicalInterface : this->physicalInterfaces) {
            const auto interface = physicalInterfacesByValue.valueAt(physicalInterface.value);

            if (interface.has_value()) {
                output.insert(*interface);
            }
        }

        return output;
    }

    std::optional<std::reference_wrapper<const Module>> TargetDescriptionFile::tryGetModule(
        std::string_view key
    ) const {
        const auto moduleIt = this->modulesByKey.find(key);
        return moduleIt != this->modulesByKey.end()
            ? std::optional{std::cref(moduleIt->second)}
            : std::nullopt;
    }

    const Module& TargetDescriptionFile::getModule(std::string_view key) const {
        const auto module = this->tryGetModule(key);

        if (!module.has_value()) {
            throw InvalidTargetDescriptionDataException{
                "Failed to get module \"" + std::string{key} + "\" from TDF - module not found"
            };
        }

        return module->get();
    }

    std::optional<std::reference_wrapper<const Peripheral>> TargetDescriptionFile::tryGetPeripheral(
        std::string_view key
    ) const {
        const auto peripheralIt = this->peripheralsByKey.find(key);
        return peripheralIt != this->peripheralsByKey.end()
            ? std::optional{std::cref(peripheralIt->second)}
            : std::nullopt;
    }

    const Peripheral& TargetDescriptionFile::getPeripheral(std::string_view key) const {
        const auto peripheral = this->tryGetPeripheral(key);

        if (!peripheral.has_value()) {
            throw InvalidTargetDescriptionDataException{
                "Failed to get peripheral \"" + std::string{key} + "\" from TDF - peripheral not found"
            };
        }

        return peripheral->get();
    }

    std::set<const Peripheral*> TargetDescriptionFile::getModulePeripherals(const std::string& moduleKey) const {
        auto output = std::set<const Peripheral*>{};

        for (const auto& [peripheralKey, peripheral] : this->peripheralsByKey) {
            if (peripheral.moduleKey == moduleKey) {
                output.insert(&peripheral);
            }
        }

        return output;
    }

    std::set<const Peripheral*> TargetDescriptionFile::getGpioPeripherals() const {
        return this->getModulePeripherals("gpio_port");
    }

    std::optional<TargetMemorySegmentDescriptor> TargetDescriptionFile::tryGetTargetMemorySegmentDescriptor(
        std::string_view addressSpaceKey,
        std::string_view segmentKey
    ) const {
        const auto addressSpace = this->tryGetAddressSpace(addressSpaceKey);

        if (!addressSpace.has_value()) {
            return std::nullopt;
        }

        const auto segment = addressSpace->get().tryGetMemorySegment(segmentKey);

        if (!segment.has_value()) {
            return std::nullopt;
        }

        return TargetDescriptionFile::targetMemorySegmentDescriptorFromMemorySegment(
            segment->get(),
            addressSpace->get()
        );
    }

    TargetMemorySegmentDescriptor TargetDescriptionFile::getTargetMemorySegmentDescriptor(
        std::string_view addressSpaceKey,
        std::string_view segmentKey
    ) const {
        const auto& addressSpace = this->getAddressSpace(addressSpaceKey);
        return TargetDescriptionFile::targetMemorySegmentDescriptorFromMemorySegment(
            addressSpace.getMemorySegment(segmentKey),
            addressSpace
        );
    }

    std::optional<TargetPeripheralDescriptor> TargetDescriptionFile::tryGetTargetPeripheralDescriptor(
        std::string_view key
    ) const {
        const auto peripheral = this->tryGetPeripheral(key);

        if (!peripheral.has_value()) {
            return std::nullopt;
        }

        return TargetDescriptionFile::targetPeripheralDescriptorFromPeripheral(
            peripheral->get(),
            this->getModule(peripheral->get().moduleKey)
        );
    }

    TargetPeripheralDescriptor TargetDescriptionFile::getTargetPeripheralDescriptor(std::string_view key) const {
        const auto& peripheral = this->getPeripheral(key);

        return TargetDescriptionFile::targetPeripheralDescriptorFromPeripheral(
            peripheral,
            this->getModule(peripheral.moduleKey)
        );
    }

    std::map<
        std::string,
        TargetAddressSpaceDescriptor
    > TargetDescriptionFile::targetAddressSpaceDescriptorsByKey() const {
        auto output = std::map<std::string, TargetAddressSpaceDescriptor>{};

        for (const auto& [key, addressSpace] : this->addressSpacesByKey) {
            output.emplace(key, TargetDescriptionFile::targetAddressSpaceDescriptorFromAddressSpace(addressSpace));
        }

        return output;
    }

    std::map<std::string, TargetPeripheralDescriptor> TargetDescriptionFile::targetPeripheralDescriptorsByKey() const {
        auto output = std::map<std::string, TargetPeripheralDescriptor>{};

        for (const auto& [key, peripheral] : this->peripheralsByKey) {
            output.emplace(
                key,
                TargetDescriptionFile::targetPeripheralDescriptorFromPeripheral(
                    peripheral,
                    this->getModule(peripheral.moduleKey)
                )
            );
        }

        return output;
    }

    std::map<std::string, TargetPadDescriptor> TargetDescriptionFile::targetPadDescriptorsByKey() const {
        auto output = std::map<std::string, TargetPadDescriptor>{};

        const auto gpioPeripheralSignalPadKeys = this->getGpioPeripheralSignalPadKeys();
        for (const auto& [key, pad] : this->padsByKey) {
            output.emplace(
                key,
                TargetDescriptionFile::targetPadDescriptorFromPad(pad, gpioPeripheralSignalPadKeys)
            );
        }

        return output;
    }

    std::map<std::string, TargetPinoutDescriptor> TargetDescriptionFile::targetPinoutDescriptorsByKey() const {
        auto output = std::map<std::string, TargetPinoutDescriptor>{};

        for (const auto& [key, pinout] : this->pinoutsByKey) {
            output.emplace(
                key,
                TargetDescriptionFile::targetPinoutDescriptorFromPinout(pinout)
            );
        }

        return output;
    }

    std::map<std::string, TargetVariantDescriptor> TargetDescriptionFile::targetVariantDescriptorsByKey() const {
        auto output = std::map<std::string, TargetVariantDescriptor>{};

        for (const auto& [key, variant] : this->variantsByKey) {
            output.emplace(
                key,
                TargetDescriptionFile::targetVariantDescriptorFromVariant(variant)
            );
        }

        return output;
    }

    std::vector<TargetPadDescriptor> TargetDescriptionFile::targetPadDescriptors() const {
        auto output = std::vector<TargetPadDescriptor>{};

        const auto gpioPeripheralSignalPadKeys = this->getGpioPeripheralSignalPadKeys();
        for (const auto& [key, pad] : this->padsByKey) {
            output.emplace_back(TargetDescriptionFile::targetPadDescriptorFromPad(pad, gpioPeripheralSignalPadKeys));
        }

        return output;
    }

    std::vector<TargetPeripheralDescriptor> TargetDescriptionFile::gpioPortPeripheralDescriptors() const {
        auto output = std::vector<TargetPeripheralDescriptor>{};

        const auto& gpioPortModule = this->getModule("gpio_port");

        for (const auto& [peripheralKey, peripheral] : this->peripheralsByKey) {
            if (peripheral.moduleKey != gpioPortModule.key) {
                continue;
            }

            output.emplace_back(
                TargetDescriptionFile::targetPeripheralDescriptorFromPeripheral(peripheral, gpioPortModule)
            );
        }

        return output;
    }

    void TargetDescriptionFile::init(const std::string& xmlFilePath) {
        auto file = QFile{QString::fromStdString(xmlFilePath)};
        if (!file.exists()) {
            throw InternalFatalErrorException{"Failed to load target description file - file not found"};
        }

        file.open(QIODevice::ReadOnly);
        auto document = QDomDocument{};
        if (!document.setContent(file.readAll())) {
            throw TargetDescriptionParsingFailureException{};
        }

        this->init(document);
    }

    void TargetDescriptionFile::init(const QDomDocument& document) {
        const auto deviceElement = document.documentElement();
        if (deviceElement.nodeName() != "device") {
            throw TargetDescriptionParsingFailureException{"Root \"device\" element not found."};
        }

        const auto deviceAttributes = deviceElement.attributes();
        for (auto i = 0; i < deviceAttributes.length(); ++i) {
            const auto deviceAttribute = deviceAttributes.item(i);
            this->deviceAttributesByName.emplace(
                deviceAttribute.nodeName().toStdString(),
                deviceAttribute.nodeValue().toStdString()
            );
        }

        for (
            auto element = deviceElement.firstChildElement("property-groups").firstChildElement("property-group");
            !element.isNull();
            element = element.nextSiblingElement("property-group")
        ) {
            auto propertyGroup = TargetDescriptionFile::propertyGroupFromXml(element);
            this->propertyGroupsByKey.emplace(propertyGroup.key, std::move(propertyGroup));
        }

        for (
            auto element = deviceElement.firstChildElement("address-spaces").firstChildElement("address-space");
            !element.isNull();
            element = element.nextSiblingElement("address-space")
        ) {
            auto addressSpace = TargetDescriptionFile::addressSpaceFromXml(element);
            this->addressSpacesByKey.emplace(addressSpace.key, std::move(addressSpace));
        }

        for (
            auto element = deviceElement.firstChildElement("physical-interfaces")
                .firstChildElement("physical-interface");
            !element.isNull();
            element = element.nextSiblingElement("physical-interface")
        ) {
            this->physicalInterfaces.emplace_back(TargetDescriptionFile::physicalInterfaceFromXml(element));
        }

        for (
            auto element = deviceElement.firstChildElement("modules").firstChildElement("module");
            !element.isNull();
            element = element.nextSiblingElement("module")
        ) {
            auto module = TargetDescriptionFile::moduleFromXml(element);
            this->modulesByKey.emplace(module.key, std::move(module));
        }

        for (
            auto element = deviceElement.firstChildElement("peripherals").firstChildElement("peripheral");
            !element.isNull();
            element = element.nextSiblingElement("peripheral")
        ) {
            auto peripheral = TargetDescriptionFile::peripheralFromXml(element);
            this->peripheralsByKey.emplace(peripheral.key, std::move(peripheral));
        }

        for (
            auto element = deviceElement.firstChildElement("pads").firstChildElement("pad");
            !element.isNull();
            element = element.nextSiblingElement("pad")
        ) {
            auto pad = TargetDescriptionFile::padFromXml(element);
            this->padsByKey.emplace(pad.key, std::move(pad));
        }

        for (
            auto element = deviceElement.firstChildElement("pinouts").firstChildElement("pinout");
            !element.isNull();
            element = element.nextSiblingElement("pinout")
        ) {
            auto pinout = TargetDescriptionFile::pinoutFromXml(element);
            this->pinoutsByKey.emplace(pinout.key, std::move(pinout));
        }

        for (
            auto element = deviceElement.firstChildElement("variants").firstChildElement("variant");
            !element.isNull();
            element = element.nextSiblingElement("variant")
        ) {
            auto variant = TargetDescriptionFile::variantFromXml(element);
            this->variantsByKey.emplace(variant.key, std::move(variant));
        }
    }

    std::optional<std::reference_wrapper<const std::string>> TargetDescriptionFile::tryGetDeviceAttribute(
        const std::string& attributeName
    ) const {
        const auto attributeIt = this->deviceAttributesByName.find(attributeName);

        if (attributeIt == this->deviceAttributesByName.end()) {
            return std::nullopt;
        }

        return std::cref(attributeIt->second);
    }

    const std::string& TargetDescriptionFile::getDeviceAttribute(const std::string& attributeName) const {
        const auto attribute = this->tryGetDeviceAttribute(attributeName);

        if (!attribute.has_value()) {
            throw InvalidTargetDescriptionDataException{"Missing target device attribute (\"" + attributeName + "\")"};
        }

        return attribute->get();
    }

    std::set<std::string> TargetDescriptionFile::getGpioPeripheralSignalPadKeys() const {
        auto output = std::set<std::string>{};
        for (const auto* peripheral : this->getGpioPeripherals()) {
            for (const auto& signal : peripheral->sigs) {
                output.insert(signal.padKey);
            }
        }

        return output;
    }

    std::optional<std::string> TargetDescriptionFile::tryGetAttribute(
        const QDomElement& element,
        const QString& attributeName
    ) {
        return element.hasAttribute(attributeName)
            ? std::optional{element.attribute(attributeName).toStdString()}
            : std::nullopt;
    }

    std::string TargetDescriptionFile::getAttribute(const QDomElement& element, const QString& attributeName) {
        const auto attribute = TargetDescriptionFile::tryGetAttribute(element, attributeName);

        if (!attribute.has_value()) {
            throw InvalidTargetDescriptionDataException{
                "Failed to fetch attribute from TDF element \"" + element.nodeName().toStdString()
                    + "\" - attribute \"" + attributeName.toStdString() + "\" not found"
            };
        }

        return *attribute;
    }

    PropertyGroup TargetDescriptionFile::propertyGroupFromXml(const QDomElement& xmlElement) {
        auto output = PropertyGroup{TargetDescriptionFile::getAttribute(xmlElement, "key"), {}, {}};

        for (
            auto element = xmlElement.firstChildElement("property");
            !element.isNull();
            element = element.nextSiblingElement("property")
        ) {
            auto property = TargetDescriptionFile::propertyFromXml(element);
            output.propertiesByKey.emplace(property.key, std::move(property));
        }

        for (
            auto element = xmlElement.firstChildElement("property-group");
            !element.isNull();
            element = element.nextSiblingElement("property-group")
        ) {
            auto subgroup = TargetDescriptionFile::propertyGroupFromXml(element);
            output.subgroupsByKey.emplace(subgroup.key, std::move(subgroup));
        }

        return output;
    }

    Property TargetDescriptionFile::propertyFromXml(const QDomElement& xmlElement) {
        return {
            TargetDescriptionFile::getAttribute(xmlElement, "key"),
            TargetDescriptionFile::getAttribute(xmlElement, "value")
        };
    }

    AddressSpace TargetDescriptionFile::addressSpaceFromXml(const QDomElement& xmlElement) {
        static const auto endiannessByName = BiMap<std::string, TargetMemoryEndianness>{
            {"big", TargetMemoryEndianness::BIG},
            {"little", TargetMemoryEndianness::LITTLE},
        };

        const auto endiannessName = TargetDescriptionFile::tryGetAttribute(xmlElement, "endianness");

        auto endianness = std::optional<TargetMemoryEndianness>{};
        if (endiannessName.has_value()) {
            endianness = endiannessByName.valueAt(*endiannessName);

            if (!endianness.has_value()) {
                throw InvalidTargetDescriptionDataException{
                    "Failed to extract address space from TDF - invalid endianness name \"" + *endiannessName + "\""
                };
            }
        }

        const auto unitSize = TargetDescriptionFile::tryGetAttribute(xmlElement, "unit-size");

        auto output = AddressSpace{
            TargetDescriptionFile::getAttribute(xmlElement, "key"),
            StringService::toUint32(TargetDescriptionFile::getAttribute(xmlElement, "start")),
            StringService::toUint32(TargetDescriptionFile::getAttribute(xmlElement, "size")),
            unitSize.has_value() ? StringService::toUint8(*unitSize) : std::uint8_t{1},
            endianness,
            {}
        };

        for (
            auto element = xmlElement.firstChildElement("memory-segment");
            !element.isNull();
            element = element.nextSiblingElement("memory-segment")
        ) {
            auto section = TargetDescriptionFile::memorySegmentFromXml(element);
            output.memorySegmentsByKey.emplace(section.key, std::move(section));
        }

        return output;
    }

    MemorySegment TargetDescriptionFile::memorySegmentFromXml(const QDomElement& xmlElement) {
        static const auto typesByName = BiMap<std::string, TargetMemorySegmentType>{
            {"gp_registers", TargetMemorySegmentType::GENERAL_PURPOSE_REGISTERS},
            {"registers", TargetMemorySegmentType::REGISTERS},
            {"aliased", TargetMemorySegmentType::ALIASED},
            {"eeprom", TargetMemorySegmentType::EEPROM},
            {"flash", TargetMemorySegmentType::FLASH},
            {"fuses", TargetMemorySegmentType::FUSES},
            {"io", TargetMemorySegmentType::IO},
            {"ram", TargetMemorySegmentType::RAM},
            {"lockbits", TargetMemorySegmentType::LOCKBITS},
            {"osccal", TargetMemorySegmentType::OSCCAL},
            {"production_signatures", TargetMemorySegmentType::PRODUCTION_SIGNATURES},
            {"signatures", TargetMemorySegmentType::SIGNATURES},
            {"user_signatures", TargetMemorySegmentType::USER_SIGNATURES},
        };

        const auto typeName = TargetDescriptionFile::getAttribute(xmlElement, "type");

        const auto type = typesByName.valueAt(typeName);
        if (!type.has_value()) {
            throw InvalidTargetDescriptionDataException{
                "Failed to extract memory segment from TDF - invalid memory segment type name \"" + typeName + "\""
            };
        }

        const auto pageSize = TargetDescriptionFile::tryGetAttribute(xmlElement, "page-size");
        const auto accessString = TargetDescriptionFile::tryGetAttribute(xmlElement, "access");

        auto output = MemorySegment{
            TargetDescriptionFile::getAttribute(xmlElement, "key"),
            TargetDescriptionFile::getAttribute(xmlElement, "name"),
            *type,
            StringService::toUint32(TargetDescriptionFile::getAttribute(xmlElement, "start")),
            StringService::toUint32(TargetDescriptionFile::getAttribute(xmlElement, "size")),
            TargetDescriptionFile::getAttribute(xmlElement, "executable") == "1",
            TargetMemoryAccess{
                !accessString.has_value() || accessString->find('R') != std::string::npos,
                !accessString.has_value() || accessString->find('W') != std::string::npos
            },
            pageSize.has_value()
                ? std::optional{StringService::toUint32(*pageSize)}
                : std::nullopt,
            {}
        };

        for (
            auto element = xmlElement.firstChildElement("section");
            !element.isNull();
            element = element.nextSiblingElement("section")
        ) {
            auto section = TargetDescriptionFile::memorySegmentSectionFromXml(element);
            output.sectionsByKey.emplace(section.key, std::move(section));
        }

        return output;
    }

    MemorySegmentSection TargetDescriptionFile::memorySegmentSectionFromXml(const QDomElement& xmlElement) {
        auto output = MemorySegmentSection{
            TargetDescriptionFile::getAttribute(xmlElement, "key"),
            TargetDescriptionFile::getAttribute(xmlElement, "name"),
            StringService::toUint32(TargetDescriptionFile::getAttribute(xmlElement, "start")),
            StringService::toUint32(TargetDescriptionFile::getAttribute(xmlElement, "size")),
            {}
        };

        for (
            auto element = xmlElement.firstChildElement("section");
            !element.isNull();
            element = element.nextSiblingElement("section")
        ) {
            auto section = TargetDescriptionFile::memorySegmentSectionFromXml(element);
            output.subSectionsByKey.emplace(section.key, std::move(section));
        }

        return output;
    }

    PhysicalInterface TargetDescriptionFile::physicalInterfaceFromXml(const QDomElement& xmlElement) {
        return {
            TargetDescriptionFile::getAttribute(xmlElement, "value")
        };
    }

    Module TargetDescriptionFile::moduleFromXml(const QDomElement& xmlElement) {
        auto output = Module{
            TargetDescriptionFile::getAttribute(xmlElement, "key"),
            TargetDescriptionFile::getAttribute(xmlElement, "name"),
            TargetDescriptionFile::getAttribute(xmlElement, "description"),
            {}
        };

        for (
            auto element = xmlElement.firstChildElement("register-group");
            !element.isNull();
            element = element.nextSiblingElement("register-group")
        ) {
            auto registerGroup = TargetDescriptionFile::registerGroupFromXml(element);
            output.registerGroupsByKey.emplace(registerGroup.key, std::move(registerGroup));
        }

        return output;
    }

    RegisterGroup TargetDescriptionFile::registerGroupFromXml(const QDomElement& xmlElement) {
        const auto offset = TargetDescriptionFile::tryGetAttribute(xmlElement, "offset");

        auto output = RegisterGroup{
            TargetDescriptionFile::getAttribute(xmlElement, "key"),
            TargetDescriptionFile::getAttribute(xmlElement, "name"),
            offset.has_value() ? std::optional{StringService::toUint32(*offset)} : std::nullopt,
            {},
            {},
            {}
        };

        for (
            auto element = xmlElement.firstChildElement("register");
            !element.isNull();
            element = element.nextSiblingElement("register")
        ) {
            auto reg = TargetDescriptionFile::registerFromXml(element);
            output.registersByKey.emplace(reg.key, std::move(reg));
        }

        for (
            auto element = xmlElement.firstChildElement("register-group");
            !element.isNull();
            element = element.nextSiblingElement("register-group")
        ) {
            auto registerGroup = TargetDescriptionFile::registerGroupFromXml(element);
            output.subgroupsByKey.emplace(registerGroup.key, std::move(registerGroup));
        }

        for (
            auto element = xmlElement.firstChildElement("register-group-reference");
            !element.isNull();
            element = element.nextSiblingElement("register-group-reference")
        ) {
            auto registerGroupReference = TargetDescriptionFile::registerGroupReferenceFromXml(element);
            output.subgroupReferencesByKey.emplace(registerGroupReference.key, std::move(registerGroupReference));
        }

        return output;
    }

    RegisterGroupReference TargetDescriptionFile::registerGroupReferenceFromXml(const QDomElement& xmlElement) {
        return {
            TargetDescriptionFile::getAttribute(xmlElement, "key"),
            TargetDescriptionFile::getAttribute(xmlElement, "name"),
            TargetDescriptionFile::getAttribute(xmlElement, "register-group-key"),
            StringService::toUint32(TargetDescriptionFile::getAttribute(xmlElement, "offset")),
            TargetDescriptionFile::tryGetAttribute(xmlElement, "description")
        };
    }

    Register TargetDescriptionFile::registerFromXml(const QDomElement& xmlElement) {
        const auto initialValue = TargetDescriptionFile::tryGetAttribute(xmlElement, "initial-value");
        const auto alternative = TargetDescriptionFile::tryGetAttribute(xmlElement, "alternative");
        const auto accessString = TargetDescriptionFile::tryGetAttribute(xmlElement, "access");

        auto output = Register{
            TargetDescriptionFile::getAttribute(xmlElement, "key"),
            TargetDescriptionFile::getAttribute(xmlElement, "name"),
            TargetDescriptionFile::tryGetAttribute(xmlElement, "description"),
            StringService::toUint32(TargetDescriptionFile::getAttribute(xmlElement, "offset")),
            StringService::toUint16(TargetDescriptionFile::getAttribute(xmlElement, "size")),
            initialValue.has_value() ? std::optional{StringService::toUint64(*initialValue)} : std::nullopt,
            accessString.has_value()
                ? std::optional{TargetRegisterAccess{
                    accessString->find('R') != std::string::npos,
                    accessString->find('W') != std::string::npos
                }}
                : std::nullopt,
            alternative.has_value() ? std::optional{*alternative == "true"} : std::nullopt,
            {}
        };

        for (
            auto element = xmlElement.firstChildElement("bit-field");
            !element.isNull();
            element = element.nextSiblingElement("bit-field")
        ) {
            auto bitField = TargetDescriptionFile::bitFieldFromXml(element);
            output.bitFieldsByKey.emplace(bitField.key, std::move(bitField));
        }

        return output;
    }

    BitField TargetDescriptionFile::bitFieldFromXml(const QDomElement& xmlElement) {
        return {
            TargetDescriptionFile::getAttribute(xmlElement, "key"),
            TargetDescriptionFile::getAttribute(xmlElement, "name"),
            TargetDescriptionFile::tryGetAttribute(xmlElement, "description"),
            StringService::toUint64(TargetDescriptionFile::getAttribute(xmlElement, "mask")),
            TargetDescriptionFile::tryGetAttribute(xmlElement, "access")
        };
    }

    Peripheral TargetDescriptionFile::peripheralFromXml(const QDomElement& xmlElement) {
        const auto offset = TargetDescriptionFile::tryGetAttribute(xmlElement, "offset");

        auto output = Peripheral{
            TargetDescriptionFile::getAttribute(xmlElement, "key"),
            TargetDescriptionFile::getAttribute(xmlElement, "name"),
            TargetDescriptionFile::getAttribute(xmlElement, "module-key"),
            {},
            {}
        };

        for (
            auto element = xmlElement.firstChildElement("register-group-instance");
            !element.isNull();
            element = element.nextSiblingElement("register-group-instance")
        ) {
            auto registerGroupInstance = TargetDescriptionFile::registerGroupInstanceFromXml(element);
            output.registerGroupInstancesByKey.emplace(
                registerGroupInstance.key.value_or(registerGroupInstance.registerGroupKey),
                std::move(registerGroupInstance)
            );
        }

        for (
            auto element = xmlElement.firstChildElement("signals").firstChildElement("signal");
            !element.isNull();
            element = element.nextSiblingElement("signal")
        ) {
            output.sigs.emplace_back(TargetDescriptionFile::signalFromXml(element));
        }

        return output;
    }

    RegisterGroupInstance TargetDescriptionFile::registerGroupInstanceFromXml(const QDomElement& xmlElement) {
        return {
            TargetDescriptionFile::tryGetAttribute(xmlElement, "key"),
            TargetDescriptionFile::tryGetAttribute(xmlElement, "name"),
            TargetDescriptionFile::getAttribute(xmlElement, "register-group-key"),
            TargetDescriptionFile::getAttribute(xmlElement, "address-space-key"),
            StringService::toUint32(TargetDescriptionFile::getAttribute(xmlElement, "offset")),
            TargetDescriptionFile::tryGetAttribute(xmlElement, "description")
        };
    }

    Signal TargetDescriptionFile::signalFromXml(const QDomElement& xmlElement) {
        const auto alternative = TargetDescriptionFile::tryGetAttribute(xmlElement, "alternative");
        const auto index = TargetDescriptionFile::tryGetAttribute(xmlElement, "index");

        return {
            TargetDescriptionFile::getAttribute(xmlElement, "name"),
            TargetDescriptionFile::getAttribute(xmlElement, "pad-key"),
            alternative.has_value() ? std::optional{*alternative == "true"} : std::nullopt,
            index.has_value() ? std::optional{StringService::toUint64(*index)} : std::nullopt,
            TargetDescriptionFile::tryGetAttribute(xmlElement, "function"),
            TargetDescriptionFile::tryGetAttribute(xmlElement, "field")
        };
    }

    Pad TargetDescriptionFile::padFromXml(const QDomElement& xmlElement) {
        return {
            TargetDescriptionFile::getAttribute(xmlElement, "key"),
            TargetDescriptionFile::getAttribute(xmlElement, "name")
        };
    }

    Pinout TargetDescriptionFile::pinoutFromXml(const QDomElement& xmlElement) {
        static const auto typesByName = BiMap<std::string, TargetPinoutType>{
            {"soic", TargetPinoutType::SOIC},
            {"ssop", TargetPinoutType::SSOP},
            {"dip", TargetPinoutType::DIP},
            {"qfn", TargetPinoutType::QFN},
            {"mlf", TargetPinoutType::MLF},
            {"drqfn", TargetPinoutType::DUAL_ROW_QFN},
            {"qfp", TargetPinoutType::QFP},
            {"bga", TargetPinoutType::BGA},
        };

        const auto typeName = TargetDescriptionFile::getAttribute(xmlElement, "type");

        const auto type = typesByName.valueAt(typeName);
        if (!type.has_value()) {
            throw InvalidTargetDescriptionDataException{
                "Failed to extract pinout from TDF - invalid pinout type name \"" + typeName + "\""
            };
        }

        auto output = Pinout{
            TargetDescriptionFile::getAttribute(xmlElement, "key"),
            TargetDescriptionFile::getAttribute(xmlElement, "name"),
            *type,
            TargetDescriptionFile::tryGetAttribute(xmlElement, "function"),
            {}
        };

        for (
            auto element = xmlElement.firstChildElement("pin");
            !element.isNull();
            element = element.nextSiblingElement("pin")
        ) {
            output.pins.emplace_back(TargetDescriptionFile::pinFromXml(element));
        }

        return output;
    }

    Pin TargetDescriptionFile::pinFromXml(const QDomElement& xmlElement) {
        return {
            TargetDescriptionFile::getAttribute(xmlElement, "position"),
            TargetDescriptionFile::tryGetAttribute(xmlElement, "pad-key")
        };
    }

    Variant TargetDescriptionFile::variantFromXml(const QDomElement& xmlElement) {
        auto output = Variant{
            TargetDescriptionFile::getAttribute(xmlElement, "key"),
            TargetDescriptionFile::getAttribute(xmlElement, "name"),
            TargetDescriptionFile::getAttribute(xmlElement, "pinout-key"),
            {}
        };

        for (
            auto element = xmlElement.firstChildElement("property-groups").firstChildElement("property-group");
            !element.isNull();
            element = element.nextSiblingElement("property-group")
        ) {
            auto propertyGroup = TargetDescriptionFile::propertyGroupFromXml(element);
            output.propertyGroupsByKey.emplace(propertyGroup.key, std::move(propertyGroup));
        }

        return output;
    }

    TargetAddressSpaceDescriptor TargetDescriptionFile::targetAddressSpaceDescriptorFromAddressSpace(
        const AddressSpace& addressSpace
    ) {
        auto output = TargetAddressSpaceDescriptor{
            addressSpace.key,
            TargetMemoryAddressRange{
                addressSpace.startAddress,
                addressSpace.startAddress + (addressSpace.size / addressSpace.unitSize) - 1
            },
            addressSpace.endianness.value_or(TargetMemoryEndianness::LITTLE),
            {},
            addressSpace.unitSize
        };

        for (const auto& [key, memorySegment] : addressSpace.memorySegmentsByKey) {
            output.segmentDescriptorsByKey.emplace(
                key,
                TargetDescriptionFile::targetMemorySegmentDescriptorFromMemorySegment(memorySegment, addressSpace)
            );
        }

        return output;
    }

    TargetMemorySegmentDescriptor TargetDescriptionFile::targetMemorySegmentDescriptorFromMemorySegment(
        const MemorySegment& memorySegment,
        const AddressSpace& addressSpace
    ) {
        return {
            addressSpace.key,
            memorySegment.key,
            memorySegment.name,
            memorySegment.type,
            TargetMemoryAddressRange{
                memorySegment.startAddress,
                memorySegment.startAddress + (memorySegment.size / addressSpace.unitSize) - 1
            },
            addressSpace.unitSize,
            memorySegment.executable,
            memorySegment.access,
            memorySegment.access,
            false,
            memorySegment.pageSize
        };
    }

    TargetPeripheralDescriptor TargetDescriptionFile::targetPeripheralDescriptorFromPeripheral(
        const Peripheral& peripheral,
        const Module& peripheralModule
    ) {
        auto output = TargetPeripheralDescriptor{
            peripheral.key,
            peripheral.name,
            peripheralModule.description,
            {},
            {}
        };

        for (const auto& [key, registerGroupInstance] : peripheral.registerGroupInstancesByKey) {
            output.registerGroupDescriptorsByKey.emplace(
                key,
                TargetDescriptionFile::targetRegisterGroupDescriptorFromRegisterGroup(
                    peripheralModule.getRegisterGroup(registerGroupInstance.registerGroupKey),
                    peripheralModule,
                    peripheral.key,
                    registerGroupInstance.addressSpaceKey,
                    registerGroupInstance.offset,
                    std::nullopt,
                    registerGroupInstance.description,
                    registerGroupInstance.key,
                    registerGroupInstance.name
                )
            );
        }

        for (const auto& signal : peripheral.sigs) {
            output.signalDescriptors.emplace_back(
                TargetDescriptionFile::targetPeripheralSignalDescriptorFromSignal(signal)
            );
        }

        return output;
    }

    TargetPeripheralSignalDescriptor TargetDescriptionFile::targetPeripheralSignalDescriptorFromSignal(
        const Signal& signal
    ) {
        return {
            signal.padKey,
            signal.index
        };
    }

    TargetRegisterGroupDescriptor TargetDescriptionFile::targetRegisterGroupDescriptorFromRegisterGroup(
        const RegisterGroup& registerGroup,
        const Module& peripheralModule,
        const std::string& peripheralKey,
        const std::string& addressSpaceKey,
        TargetMemoryAddress baseAddress,
        const std::optional<std::string>& parentGroupAbsoluteKey,
        const std::optional<std::string>& description,
        const std::optional<std::string>& keyOverride,
        const std::optional<std::string>& nameOverride
    ) {
        const auto& key = keyOverride.has_value() ? *keyOverride : registerGroup.key;
        const auto& name = nameOverride.has_value() ? *nameOverride : registerGroup.name;
        const auto absoluteKey = parentGroupAbsoluteKey.has_value()
            ? *parentGroupAbsoluteKey + "." + key
            : key;

        auto output = TargetRegisterGroupDescriptor{
            key,
            absoluteKey,
            name,
            peripheralKey,
            addressSpaceKey,
            description,
            {},
            {}
        };

        for (const auto& [key, subgroup] : registerGroup.subgroupsByKey) {
            output.subgroupDescriptorsByKey.emplace(
                key,
                TargetDescriptionFile::targetRegisterGroupDescriptorFromRegisterGroup(
                    subgroup,
                    peripheralModule,
                    peripheralKey,
                    addressSpaceKey,
                    baseAddress + registerGroup.offset.value_or(0),
                    absoluteKey
                )
            );
        }

        for (const auto& [key, subgroupReference] : registerGroup.subgroupReferencesByKey) {
            const auto& referencedGroup = peripheralModule.getRegisterGroup(subgroupReference.registerGroupKey);
            output.subgroupDescriptorsByKey.emplace(
                key,
                TargetDescriptionFile::targetRegisterGroupDescriptorFromRegisterGroup(
                    referencedGroup,
                    peripheralModule,
                    peripheralKey,
                    addressSpaceKey,
                    baseAddress + registerGroup.offset.value_or(0) + subgroupReference.offset,
                    absoluteKey,
                    subgroupReference.description,
                    subgroupReference.key,
                    subgroupReference.name
                )
            );
        }

        for (const auto& [key, reg] : registerGroup.registersByKey) {
            output.registerDescriptorsByKey.emplace(
                key,
                TargetDescriptionFile::targetRegisterDescriptorFromRegister(
                    reg,
                    absoluteKey,
                    peripheralKey,
                    addressSpaceKey,
                    baseAddress + registerGroup.offset.value_or(0)
                )
            );
        }

        return output;
    }

    TargetRegisterDescriptor TargetDescriptionFile::targetRegisterDescriptorFromRegister(
        const Register& reg,
        const std::string& absoluteGroupKey,
        const std::string& peripheralKey,
        const std::string& addressSpaceKey,
        TargetMemoryAddress baseAddress
    ) {
        auto output = TargetRegisterDescriptor{
            reg.key,
            reg.name,
            absoluteGroupKey,
            peripheralKey,
            addressSpaceKey,
            baseAddress + reg.offset,
            reg.size,
            TargetRegisterType::OTHER,
            reg.access.value_or(TargetRegisterAccess{true, true}),
            reg.description,
            {}
        };

        for (const auto& [key, bitField] : reg.bitFieldsByKey) {
            output.bitFieldDescriptorsByKey.emplace(
                key,
                TargetDescriptionFile::targetBitFieldDescriptorFromBitField(bitField)
            );
        }

        return output;
    }

    TargetBitFieldDescriptor TargetDescriptionFile::targetBitFieldDescriptorFromBitField(const BitField& bitField) {
        return {
            bitField.key,
            bitField.name,
            bitField.mask,
            bitField.description
        };
    }

    TargetPadDescriptor TargetDescriptionFile::targetPadDescriptorFromPad(
        const Pad& pad,
        const std::set<std::string>& gpioPeripheralSignalPadKeys
    ) {
        const auto resolvePadType = [&gpioPeripheralSignalPadKeys] (const Pad& pad) -> TargetPadType {
            if (gpioPeripheralSignalPadKeys.contains(pad.key)) {
                return TargetPadType::GPIO;
            }

            const auto padNameLower = StringService::asciiToLower(pad.name);

            if (
                padNameLower.find("vcc") == 0
                || padNameLower.find("avcc") == 0
                || padNameLower.find("aref") == 0
                || padNameLower.find("avdd") == 0
                || padNameLower.find("vdd") == 0
            ) {
                return TargetPadType::VCC;
            }

            if (padNameLower.find("gnd") == 0 || padNameLower.find("agnd") == 0 || padNameLower.find("vss") == 0) {
                return TargetPadType::GND;
            }

            return TargetPadType::OTHER;
        };

        return {
            pad.key,
            pad.name,
            resolvePadType(pad)
        };
    }

    TargetPinoutDescriptor TargetDescriptionFile::targetPinoutDescriptorFromPinout(const Pinout& pinout) {
        auto output = TargetPinoutDescriptor{
            pinout.key,
            pinout.name,
            pinout.type,
            {}
        };

        for (const auto& pin : pinout.pins) {
            output.pinDescriptors.emplace_back(TargetDescriptionFile::targetPinDescriptorFromPin(pin));
        }

        return output;
    }

    TargetPinDescriptor TargetDescriptionFile::targetPinDescriptorFromPin(const Pin& pin) {
        return {
            pin.position,
            pin.padKey
        };
    }

    TargetVariantDescriptor TargetDescriptionFile::targetVariantDescriptorFromVariant(const Variant& variant) {
        return {
            variant.key,
            variant.name,
            variant.pinoutKey
        };
    }
}
