#include "Avr8.hpp"

#include <cassert>
#include <bitset>
#include <limits>
#include <thread>
#include <algorithm>

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

#include "src/Exceptions/InvalidConfig.hpp"
#include "Exceptions/DebugWirePhysicalInterfaceError.hpp"
#include "src/Targets/TargetRegister.hpp"

namespace Targets::Microchip::Avr::Avr8Bit
{
    using namespace Exceptions;

    Avr8::Avr8(const TargetConfig& targetConfig)
        : targetConfig(Avr8TargetConfig(targetConfig))
        , targetDescriptionFile(TargetDescription::TargetDescriptionFile(this->targetConfig.name))
        , signature(this->targetDescriptionFile.getTargetSignature())
        , name(this->targetDescriptionFile.getTargetName())
        , family(this->targetDescriptionFile.getFamily())
        , targetParameters(this->targetDescriptionFile.getTargetParameters())
        , supportedPhysicalInterfaces(this->targetDescriptionFile.getSupportedPhysicalInterfaces())
        , padDescriptorsByName(this->targetDescriptionFile.getPadDescriptorsMappedByName())
        , targetVariantsById(this->targetDescriptionFile.getVariantsMappedById())
        , stackPointerRegisterDescriptor(
            TargetRegisterDescriptor(
                TargetRegisterType::STACK_POINTER,
                this->targetParameters.stackPointerRegisterLowAddress.value(),
                this->targetParameters.stackPointerRegisterSize.value(),
                TargetMemoryType::OTHER,
                "SP",
                "CPU",
                "Stack Pointer Register",
                TargetRegisterAccess(true, true)
            )
        )
        , statusRegisterDescriptor(
            TargetRegisterDescriptor(
                TargetRegisterType::STATUS_REGISTER,
                this->targetParameters.statusRegisterStartAddress.value(),
                this->targetParameters.statusRegisterSize.value(),
                TargetMemoryType::OTHER,
                "SREG",
                "CPU",
                "Status Register",
                TargetRegisterAccess(true, true)
            )
        )
        , fuseEnableStrategy(this->targetDescriptionFile.getFuseEnableStrategy().value_or(FuseEnableStrategy::CLEAR))
    {
        if (!this->supportedPhysicalInterfaces.contains(this->targetConfig.physicalInterface)) {
            /*
             * The user has selected a physical interface that does not appear to be supported by the selected
             * target.
             *
             * Bloom's target description files provide a list of supported physical interfaces for each target
             * (which is how this->supportedPhysicalInterfaces is populated), but it's possible that this list may
             * be wrong/incomplete. For this reason, we don't throw an exception here. Instead, we just present the
             * user with a warning and a list of physical interfaces known to be supported by their selected target.
             */
            const auto physicalInterfaceNames = getPhysicalInterfaceNames();

            const auto supportedPhysicalInterfaceList = std::accumulate(
                this->supportedPhysicalInterfaces.begin(),
                this->supportedPhysicalInterfaces.end(),
                std::string(),
                [&physicalInterfaceNames] (const std::string& string, PhysicalInterface physicalInterface) {
                    if (physicalInterface == PhysicalInterface::ISP) {
                        /*
                         * Don't include the ISP interface in the list of supported interfaces, as doing so may
                         * mislead the user into thinking the ISP interface can be used for debugging operations.
                         */
                        return string;
                    }

                    return string + "\n - " + physicalInterfaceNames.at(physicalInterface);
                }
            );

            Logger::warning(
                "\nThe selected target (" + this->name + ") does not support the selected physical interface ("
                    + physicalInterfaceNames.at(this->targetConfig.physicalInterface) + "). Target activation "
                    "will likely fail. The target supports the following physical interfaces: \n"
                    + supportedPhysicalInterfaceList + "\n\nFor physical interface configuration values, see "
                    + Services::PathService::homeDomainName() + "/docs/configuration/avr8-physical-interfaces. \n\n"
                    + "If this information is incorrect, please report this to Bloom developers via "
                    + Services::PathService::homeDomainName() + "/report-issue.\n"
            );
        }

        if (
            this->targetConfig.manageOcdenFuseBit
            && this->targetConfig.physicalInterface != PhysicalInterface::JTAG
        ) {
            Logger::warning(
                "The 'manageOcdenFuseBit' parameter only applies to JTAG targets. It will be ignored in this session."
            );
        }

        this->loadTargetRegisterDescriptors();
        this->loadTargetMemoryDescriptors();
    }

    bool Avr8::supportsDebugTool(DebugTool* debugTool) {
        return debugTool->getAvr8DebugInterface(
            this->targetConfig,
            this->family,
            this->targetParameters,
            this->targetRegisterDescriptorsById
        ) != nullptr;
    }

    void Avr8::setDebugTool(DebugTool* debugTool) {
        this->targetPowerManagementInterface = debugTool->getTargetPowerManagementInterface();
        this->avr8DebugInterface = debugTool->getAvr8DebugInterface(
            this->targetConfig,
            this->family,
            this->targetParameters,
            this->targetRegisterDescriptorsById
        );

        this->avrIspInterface = debugTool->getAvrIspInterface(
            this->targetConfig
        );

        if (this->avrIspInterface != nullptr) {
            this->avrIspInterface->configure(this->targetConfig);
        }

        if (
            this->targetConfig.manageDwenFuseBit
            && this->avrIspInterface == nullptr
            && this->targetConfig.physicalInterface == PhysicalInterface::DEBUG_WIRE
        ) {
            Logger::warning(
                "The connected debug tool (or associated driver) does not provide any ISP interface. "
                    "Bloom will be unable to manage the DWEN fuse bit."
            );
        }
    }

    void Avr8::activate() {
        if (this->isActivated()) {
            return;
        }

        this->avr8DebugInterface->init();

        try {
            this->avr8DebugInterface->activate();

        } catch (const Exceptions::DebugWirePhysicalInterfaceError& debugWireException) {
            // We failed to activate the debugWire physical interface. DWEN fuse bit may need updating.

            if (!this->targetConfig.manageDwenFuseBit) {
                throw TargetOperationFailure(
                    "Failed to activate debugWire physical interface - check target connection and DWEN fuse "
                        "bit. Bloom can manage the DWEN fuse bit automatically. For instructions on enabling this "
                        "function, see " + Services::PathService::homeDomainName() + "/docs/debugging-avr-debugwire"
                );
            }

            try {
                Logger::warning(
                    "Failed to activate the debugWire physical interface - attempting to access target via "
                        "the ISP interface, for DWEN fuse bit inspection."
                );
                this->updateDwenFuseBit(true);

                // If the debug tool provides a TargetPowerManagementInterface, attempt to cycle the target power
                if (
                    this->targetPowerManagementInterface != nullptr
                    && this->targetConfig.cycleTargetPowerPostDwenUpdate
                ) {
                    Logger::info("Cycling target power");

                    Logger::debug("Disabling target power");
                    this->targetPowerManagementInterface->disableTargetPower();

                    Logger::debug(
                        "Holding power off for ~" + std::to_string(this->targetConfig.targetPowerCycleDelay.count())
                            + " ms"
                    );
                    std::this_thread::sleep_for(this->targetConfig.targetPowerCycleDelay);

                    Logger::debug("Enabling target power");
                    this->targetPowerManagementInterface->enableTargetPower();

                    Logger::debug(
                        "Waiting ~" + std::to_string(this->targetConfig.targetPowerCycleDelay.count())
                            + " ms for target power-up"
                    );
                    std::this_thread::sleep_for(this->targetConfig.targetPowerCycleDelay);
                }

            } catch (const Exception& exception) {
                throw Exception(
                    "Failed to access/update DWEN fuse bit via ISP interface - " + exception.getMessage()
                );
            }

            Logger::info("Retrying debugWire physical interface activation");
            this->avr8DebugInterface->activate();
        }

        if (
            this->targetConfig.physicalInterface == PhysicalInterface::JTAG
            && this->targetConfig.manageOcdenFuseBit
        ) {
            Logger::debug("Attempting OCDEN fuse bit management");
            this->updateOcdenFuseBit(true);
        }

        this->activated = true;

        /*
         * Validate the target signature.
         *
         * The signature obtained from the device should match what we loaded from the target description file.
         */
        const auto targetSignature = this->avr8DebugInterface->getDeviceId();

        if (targetSignature != this->signature) {
            throw Exception(
                "Failed to validate connected target - target signature mismatch.\nThe target signature"
                " (\"" + targetSignature.toHex() + "\") does not match the AVR8 target description signature (\""
                + this->signature.toHex() + "\"). This will likely be due to an incorrect target name in the "
                + "configuration file (bloom.yaml)."
            );
        }

        this->avr8DebugInterface->reset();
    }

    void Avr8::deactivate() {
        try {
            this->stop();
            this->clearAllBreakpoints();

            if (
                this->targetConfig.physicalInterface == PhysicalInterface::JTAG
                && this->targetConfig.manageOcdenFuseBit
            ) {
                Logger::debug("Attempting OCDEN fuse bit management");
                this->updateOcdenFuseBit(false);

            } else {
                this->avr8DebugInterface->deactivate();
            }

            this->activated = false;

        } catch (const Exception& exception) {
            Logger::error("Failed to deactivate AVR8 target - " + exception.getMessage());
        }
    }

    TargetDescriptor Avr8::getDescriptor() {
        auto descriptor = TargetDescriptor(
            this->signature.toHex(),
            this->name,
            "Microchip",
            this->targetMemoryDescriptorsByType,
            this->targetRegisterDescriptorsById,
            this->getBreakpointResources(),
            {},
            Targets::TargetMemoryType::FLASH
        );

        std::transform(
            this->targetVariantsById.begin(),
            this->targetVariantsById.end(),
            std::back_inserter(descriptor.variants),
            [] (auto& variantToIdPair) {
                return variantToIdPair.second;
            }
        );

        return descriptor;
    }

    void Avr8::run(std::optional<TargetMemoryAddress> toAddress) {
        if (toAddress.has_value()) {
            return this->avr8DebugInterface->runTo(*toAddress);
        }

        this->avr8DebugInterface->run();
    }

    void Avr8::stop() {
        this->avr8DebugInterface->stop();
    }

    void Avr8::step() {
        this->avr8DebugInterface->step();
    }

    void Avr8::reset() {
        this->avr8DebugInterface->reset();
    }

    void Avr8::setSoftwareBreakpoint(TargetMemoryAddress address) {
        this->avr8DebugInterface->setSoftwareBreakpoint(address);
    }

    void Avr8::removeSoftwareBreakpoint(TargetMemoryAddress address) {
        this->avr8DebugInterface->clearSoftwareBreakpoint(address);
    }

    void Avr8::setHardwareBreakpoint(TargetMemoryAddress address) {
        this->avr8DebugInterface->setHardwareBreakpoint(address);
    }

    void Avr8::removeHardwareBreakpoint(TargetMemoryAddress address) {
        this->avr8DebugInterface->clearHardwareBreakpoint(address);
    }

    void Avr8::clearAllBreakpoints() {
        this->avr8DebugInterface->clearAllBreakpoints();
    }

    void Avr8::writeRegisters(TargetRegisters registers) {
        this->avr8DebugInterface->writeRegisters(registers);
    }

    TargetRegisters Avr8::readRegisters(const Targets::TargetRegisterDescriptorIds& descriptorIds) {
        return this->avr8DebugInterface->readRegisters(descriptorIds);
    }

    TargetMemoryBuffer Avr8::readMemory(
        TargetMemoryType memoryType,
        std::uint32_t startAddress,
        std::uint32_t bytes,
        const std::set<Targets::TargetMemoryAddressRange>& excludedAddressRanges
    ) {
        return this->avr8DebugInterface->readMemory(memoryType, startAddress, bytes, excludedAddressRanges);
    }

    void Avr8::writeMemory(TargetMemoryType memoryType, std::uint32_t startAddress, const TargetMemoryBuffer& buffer) {
        if (memoryType == TargetMemoryType::FLASH && !this->programmingModeEnabled()) {
            throw Exception("Attempted Flash memory write with no active programming session.");
        }

        this->avr8DebugInterface->writeMemory(memoryType, startAddress, buffer);
    }

    void Avr8::eraseMemory(TargetMemoryType memoryType) {
        if (memoryType == TargetMemoryType::FLASH) {
            if (this->targetConfig.physicalInterface == PhysicalInterface::DEBUG_WIRE) {
                // debugWire targets do not need to be erased
                return;
            }

            if (!this->programmingModeEnabled()) {
                throw Exception("Attempted Flash memory erase with no active programming session.");
            }

            /*
             * For JTAG and UPDI targets, we must perform a chip erase. This means we could end up erasing EEPROM,
             * unless the EESAVE fuse bit has been programmed.
             *
             * If configured to do so, we will ensure that the EESAVE fuse bit has been programmed before we perform
             * the chip erase. The fuse will be restored to its original value at the end of the programming session.
             */
            if (
                this->targetConfig.physicalInterface == PhysicalInterface::JTAG
                || this->targetConfig.physicalInterface == PhysicalInterface::UPDI
            ) {
                if (this->targetConfig.preserveEeprom) {
                    Logger::debug("Inspecting EESAVE fuse bit");
                    this->activeProgrammingSession->managingEesaveFuseBit = this->updateEesaveFuseBit(true);

                } else {
                    Logger::warning(
                        "Performing chip-erase with preserveEeprom disabled. All EEPROM data will be lost!"
                    );
                }

                return this->avr8DebugInterface->eraseChip();
            }

            return this->avr8DebugInterface->eraseProgramMemory();
        }

        /*
         * Debug tools do not have to support the erasing of RAM or EEPROM memory. We just implement this as a
         * write operation.
         */
        this->writeMemory(
            memoryType,
            memoryType == TargetMemoryType::RAM
                ? this->targetParameters.ramStartAddress.value()
                : this->targetParameters.eepromStartAddress.value(),
            TargetMemoryBuffer(
                memoryType == TargetMemoryType::RAM
                    ? this->targetParameters.ramSize.value()
                    : this->targetParameters.eepromSize.value(),
                0xFF
            )
        );
    }

    TargetState Avr8::getState() {
        return this->avr8DebugInterface->getTargetState();
    }

    std::uint32_t Avr8::getProgramCounter() {
        return this->avr8DebugInterface->getProgramCounter();
    }

    void Avr8::setProgramCounter(std::uint32_t programCounter) {
        this->avr8DebugInterface->setProgramCounter(programCounter);
    }

    std::uint32_t Avr8::getStackPointer() {
        const auto stackPointerRegister = this->readRegisters(
            {this->stackPointerRegisterDescriptor.id}
        ).front();

        std::uint32_t stackPointer = 0;
        for (std::size_t i = 0; i < stackPointerRegister.size() && i < 4; i++) {
            stackPointer = (stackPointer << (8 * i)) | stackPointerRegister.value[i];
        }

        return stackPointer;
    }

    std::map<int, TargetPinState> Avr8::getPinStates(int variantId) {
        const auto targetVariantIt = this->targetVariantsById.find(variantId);

        if (targetVariantIt == this->targetVariantsById.end()) {
            throw Exception("Invalid target variant ID");
        }

        std::map<int, TargetPinState> output;
        const auto& variant = targetVariantIt->second;

        /*
         * To prevent the number of memory reads we perform here, we cache the data and map it by start address.
         *
         * This way, we only perform 3 memory reads for a target variant with 3 ports - one per port (instead of one
         * per pin).
         *
         * We may be able to make this more efficient by combining reads for ports with aligned memory addresses. This
         * will be considered when the need for it becomes apparent.
         */
        std::map<std::uint16_t, TargetMemoryBuffer> cachedMemoryByStartAddress;
        const auto readMemoryBitset = [this, &cachedMemoryByStartAddress] (std::uint16_t startAddress) {
            auto cachedByteIt = cachedMemoryByStartAddress.find(startAddress);

            if (cachedByteIt == cachedMemoryByStartAddress.end()) {
                cachedByteIt = cachedMemoryByStartAddress.insert(
                    std::pair(
                        startAddress,
                        this->readMemory(TargetMemoryType::RAM, startAddress, 1)
                    )
                ).first;
            }

            return std::bitset<std::numeric_limits<unsigned char>::digits>(
                cachedByteIt->second.at(0)
            );
        };

        for (const auto& [pinNumber, pinDescriptor] : variant.pinDescriptorsByNumber) {
            const auto padIt = this->padDescriptorsByName.find(pinDescriptor.padName);

            if (padIt != this->padDescriptorsByName.end()) {
                const auto& pad = padIt->second;

                if (!pad.gpioPinNumber.has_value()) {
                    continue;
                }

                auto pinState = TargetPinState();

                if (pad.gpioDdrAddress.has_value()) {
                    const auto ddrValue = readMemoryBitset(pad.gpioDdrAddress.value());

                    pinState.ioDirection = ddrValue.test(pad.gpioPinNumber.value()) ?
                        TargetPinState::IoDirection::OUTPUT : TargetPinState::IoDirection::INPUT;

                    if (pinState.ioDirection == TargetPinState::IoDirection::OUTPUT
                        && pad.gpioPortAddress.has_value()
                    ) {
                        const auto portRegisterValueBitset = readMemoryBitset(pad.gpioPortAddress.value());
                        pinState.ioState = portRegisterValueBitset.test(pad.gpioPinNumber.value()) ?
                            TargetPinState::IoState::HIGH : TargetPinState::IoState::LOW;

                    } else if (pinState.ioDirection == TargetPinState::IoDirection::INPUT
                        && pad.gpioPortInputAddress.has_value()
                    ) {
                        const auto portInputRegisterValue = readMemoryBitset(pad.gpioPortInputAddress.value());
                        pinState.ioState = portInputRegisterValue.test(pad.gpioPinNumber.value()) ?
                            TargetPinState::IoState::HIGH : TargetPinState::IoState::LOW;
                    }
                }

                output.insert(std::pair(pinNumber, pinState));
            }
        }

        return output;
    }

    void Avr8::setPinState(const TargetPinDescriptor& pinDescriptor, const TargetPinState& state) {
        const auto targetVariantIt = this->targetVariantsById.find(pinDescriptor.variantId);

        if (targetVariantIt == this->targetVariantsById.end()) {
            throw Exception("Invalid target variant ID");
        }

        const auto padDescriptorIt = this->padDescriptorsByName.find(pinDescriptor.padName);

        if (padDescriptorIt == this->padDescriptorsByName.end()) {
            throw Exception("Unknown pad");
        }

        if (!state.ioDirection.has_value()) {
            throw Exception("Missing IO direction state");
        }

        const auto& variant = targetVariantIt->second;
        const auto& padDescriptor = padDescriptorIt->second;
        auto ioState = state.ioState;

        if (state.ioDirection == TargetPinState::IoDirection::INPUT) {
            // When setting the direction to INPUT, we must always set the IO pin state to LOW
            ioState = TargetPinState::IoState::LOW;
        }

        if (
            !padDescriptor.gpioDdrAddress.has_value()
            || !padDescriptor.gpioPortAddress.has_value()
            || !padDescriptor.gpioPinNumber.has_value()
        ) {
            throw Exception("Inadequate pad descriptor");
        }

        const auto pinNumber = padDescriptor.gpioPinNumber.value();
        const auto ddrAddress = padDescriptor.gpioDdrAddress.value();
        const auto ddrValue = this->readMemory(TargetMemoryType::RAM, ddrAddress, 1);

        if (ddrValue.empty()) {
            throw Exception("Failed to read DDR value");
        }

        auto ddrValueBitset = std::bitset<std::numeric_limits<unsigned char>::digits>(ddrValue.front());
        if (ddrValueBitset.test(pinNumber) != (state.ioDirection == TargetPinState::IoDirection::OUTPUT)) {
            // DDR needs updating
            ddrValueBitset.set(pinNumber, (state.ioDirection == TargetPinState::IoDirection::OUTPUT));

            this->writeMemory(
                TargetMemoryType::RAM,
                ddrAddress,
                {static_cast<unsigned char>(ddrValueBitset.to_ulong())}
            );
        }

        if (ioState.has_value()) {
            const auto portRegisterAddress = padDescriptor.gpioPortAddress.value();
            const auto portRegisterValue = this->readMemory(TargetMemoryType::RAM, portRegisterAddress, 1);

            if (portRegisterValue.empty()) {
                throw Exception("Failed to read PORT register value");
            }

            auto portRegisterValueBitset = std::bitset<std::numeric_limits<unsigned char>::digits>(
                portRegisterValue.front()
            );

            if (portRegisterValueBitset.test(pinNumber) != (ioState == TargetPinState::IoState::HIGH)) {
                // PORT set register needs updating
                portRegisterValueBitset.set(pinNumber, (ioState == TargetPinState::IoState::HIGH));

                this->writeMemory(
                    TargetMemoryType::RAM,
                    portRegisterAddress,
                    {static_cast<unsigned char>(portRegisterValueBitset.to_ulong())}
                );
            }
        }
    }

    void Avr8::enableProgrammingMode() {
        if (this->activeProgrammingSession.has_value()) {
            return;
        }

        this->avr8DebugInterface->enableProgrammingMode();
        this->activeProgrammingSession = ProgrammingSession();
    }

    void Avr8::disableProgrammingMode() {
        if (!this->activeProgrammingSession.has_value()) {
            return;
        }

        if (this->activeProgrammingSession->managingEesaveFuseBit) {
            this->updateEesaveFuseBit(false);
        }

        this->avr8DebugInterface->disableProgrammingMode();
        this->activeProgrammingSession.reset();
    }

    bool Avr8::programmingModeEnabled() {
        return this->activeProgrammingSession.has_value();
    }

    void Avr8::loadTargetRegisterDescriptors() {
        this->targetRegisterDescriptorsById = this->targetDescriptionFile.getRegisterDescriptorsMappedById();

        /*
         * All AVR8 targets possess 32 general purpose CPU registers. These are not described in the TDF, so we
         * construct the descriptors for them here.
         */
        const auto gpRegisterStartAddress = this->targetParameters.gpRegisterStartAddress.value_or(0);
        for (std::uint8_t i = 0; i <= 31; i++) {
            auto generalPurposeRegisterDescriptor = TargetRegisterDescriptor(
                TargetRegisterType::GENERAL_PURPOSE_REGISTER,
                gpRegisterStartAddress + i,
                1,
                TargetMemoryType::OTHER,
                "r" + std::to_string(i),
                "CPU General Purpose",
                std::nullopt,
                TargetRegisterAccess(true, true)
            );

            this->targetRegisterDescriptorsById.emplace(
                generalPurposeRegisterDescriptor.id,
                std::move(generalPurposeRegisterDescriptor)
            );
        }

        this->targetRegisterDescriptorsById.emplace(
            this->stackPointerRegisterDescriptor.id,
            this->stackPointerRegisterDescriptor
        );
        this->targetRegisterDescriptorsById.emplace(
            this->statusRegisterDescriptor.id,
            this->statusRegisterDescriptor
        );
    }

    void Avr8::loadTargetMemoryDescriptors() {
        const auto ramStartAddress = this->targetParameters.ramStartAddress.value();
        const auto flashStartAddress = this->targetParameters.flashStartAddress.value();

        this->targetMemoryDescriptorsByType.insert(std::pair(
            TargetMemoryType::RAM,
            TargetMemoryDescriptor(
                TargetMemoryType::RAM,
                TargetMemoryAddressRange(
                    ramStartAddress,
                    ramStartAddress + this->targetParameters.ramSize.value() - 1
                ),
                TargetMemoryAccess(true, true, true)
            )
        ));

        this->targetMemoryDescriptorsByType.insert(std::pair(
            TargetMemoryType::FLASH,
            TargetMemoryDescriptor(
                TargetMemoryType::FLASH,
                TargetMemoryAddressRange(
                    flashStartAddress,
                    flashStartAddress + this->targetParameters.flashSize.value() - 1
                ),
                TargetMemoryAccess(true, true, false),
                this->targetParameters.flashPageSize
            )
        ));

        if (this->targetParameters.eepromStartAddress.has_value() && this->targetParameters.eepromSize.has_value()) {
            const auto eepromStartAddress = this->targetParameters.eepromStartAddress.value();

            this->targetMemoryDescriptorsByType.insert(std::pair(
                TargetMemoryType::EEPROM,
                TargetMemoryDescriptor(
                    TargetMemoryType::EEPROM,
                    TargetMemoryAddressRange(
                        eepromStartAddress,
                        eepromStartAddress + this->targetParameters.eepromSize.value() - 1
                    ),
                    TargetMemoryAccess(true, true, true)
                )
            ));
        }
    }

    BreakpointResources Avr8::getBreakpointResources() {
        auto maxHardwareBreakpoints = static_cast<std::uint16_t>(0);

        switch (this->targetConfig.physicalInterface) {
            case PhysicalInterface::JTAG: {
                maxHardwareBreakpoints = this->family == Family::XMEGA ? 2 : 3;
                break;
            }
            case PhysicalInterface::PDI: {
                maxHardwareBreakpoints = 2;
                break;
            }
            case PhysicalInterface::UPDI: {
                maxHardwareBreakpoints = 1;
                break;
            }
            default: {
                break;
            }
        }

        return BreakpointResources(
            maxHardwareBreakpoints,
            std::nullopt,
            std::min(
                static_cast<std::uint16_t>(this->targetConfig.reserveSteppingBreakpoint ? 1 : 0),
                maxHardwareBreakpoints
            )
        );
    }

    bool Avr8::isFuseEnabled(const FuseBitsDescriptor& descriptor, unsigned char fuseByteValue) const {
        const auto programmedValue = static_cast<unsigned char>(
            this->fuseEnableStrategy == FuseEnableStrategy::SET
                ? (0xFF & descriptor.bitMask)
                : 0
        );

        return (fuseByteValue & descriptor.bitMask) == programmedValue;
    }

    unsigned char Avr8::setFuseEnabled(
        const FuseBitsDescriptor& descriptor,
        unsigned char fuseByteValue,
        bool enabled
    ) const {
        return static_cast<unsigned char>(
            this->fuseEnableStrategy == FuseEnableStrategy::SET
                ? enabled ? (fuseByteValue | descriptor.bitMask) : fuseByteValue & ~(descriptor.bitMask)
                : enabled ? fuseByteValue & ~(descriptor.bitMask) : (fuseByteValue | descriptor.bitMask)
        );
    }

    void Avr8::updateDwenFuseBit(bool enable) {
        if (this->avrIspInterface == nullptr) {
            throw Exception(
                "Debug tool or driver does not provide access to an ISP interface - please confirm that the "
                    "debug tool supports ISP and then report this issue via " + Services::PathService::homeDomainName()
                    + "/report-issue"
            );
        }

        if (!this->supportedPhysicalInterfaces.contains(PhysicalInterface::DEBUG_WIRE)) {
            throw Exception(
                "Target does not support debugWire physical interface - check target configuration or "
                    "report this issue via " + Services::PathService::homeDomainName() + "/report-issue"
            );
        }

        const auto dwenFuseBitsDescriptor = this->targetDescriptionFile.getDwenFuseBitsDescriptor();
        const auto spienFuseBitsDescriptor = this->targetDescriptionFile.getSpienFuseBitsDescriptor();

        if (!dwenFuseBitsDescriptor.has_value()) {
            throw Exception("Could not find DWEN bit field in TDF.");
        }

        if (!spienFuseBitsDescriptor.has_value()) {
            throw Exception("Could not find SPIEN bit field in TDF.");
        }

        Logger::debug("Extracting ISP parameters from TDF");
        this->avrIspInterface->setIspParameters(this->targetDescriptionFile.getIspParameters());

        Logger::info("Initiating ISP interface");
        this->avrIspInterface->activate();

        /*
         * It is crucial that we understand the potential consequences of this operation.
         *
         * AVR fuses are used to control certain functions within the AVR (including the debugWire interface). Care
         * must be taken when updating these fuse bytes, as an incorrect value could render the AVR inaccessible to
         * standard programmers.
         *
         * For example, consider the SPI enable (SPIEN) fuse bit. This fuse bit is used to enable/disable the SPI for
         * serial programming. If the SPIEN fuse bit is cleared, most programming tools will not be able to gain access
         * to the target via the SPI. This isn't too bad, if there is some other way for the programming tool to gain
         * access (such as the debugWire interface). But now consider the DWEN fuse bit (which is used to enable/disable
         * the debugWire interface). What if both the SPIEN *and* the DWEN fuse bits are cleared? Both interfaces will
         * be disabled. Effectively, the AVR will be bricked, and the only course for recovery would be to use
         * high-voltage programming.
         *
         * When updating the DWEN fuse, Bloom relies on data from the target description file (TDF). But there is no
         * guarantee that this data is correct. For this reason, we perform additional checks in an attempt to reduce
         * the likelihood of bricking the target:
         *
         *  - Confirm target signature match - We read the AVR signature from the connected target and compare it to
         *    what we have in the TDF. The operation will be aborted if the signatures do not match.
         *
         *  - SPIEN fuse bit check - we can be certain that the SPIEN fuse bit is set, because we couldn't have gotten
         *    this far (post ISP activation) if it wasn't. We use this axiom to verify the validity of the data in the
         *    TDF. If the SPIEN fuse bit appears to be cleared, we can be fairly certain that the data we have on the
         *    SPIEN fuse bit is incorrect. From this, we assume that the data for the DWEN fuse bit is also incorrect,
         *    and abort the operation.
         *
         *  - Lock bits check - we read the lock bit byte from the target and confirm that all lock bits are cleared.
         *    If any lock bits are set, we abort the operation.
         *
         *  - DWEN fuse bit check - if the DWEN fuse bit is already set to the desired value, then there is no need
         *    to update it. But we may be checking the wrong bit (if the TDF data is incorrect) - either way, we will
         *    abort the operation.
         *
         * The precautions described above may reduce the likelihood of Bloom bricking the connected target, but there
         * is still a chance that all of the checks pass, and we still brick the device. Now would be a good time to
         * remind the user of liabilities in regard to Bloom and its contributors.
         */
        Logger::warning(
            "Updating the DWEN fuse bit is a potentially dangerous operation. Bloom is provided \"AS IS\", "
                "without warranty of any kind. You are using Bloom at your own risk. In no event shall the copyright "
                "owner or contributors be liable for any damage caused as a result of using Bloom. For more details, "
                "see the Bloom license at " + Services::PathService::homeDomainName() + "/license"
        );

        try {
            Logger::info("Reading target signature via ISP");
            const auto ispDeviceSignature = this->avrIspInterface->getDeviceId();

            if (ispDeviceSignature != this->signature) {
                throw Exception(
                    "AVR target signature mismatch - expected signature \"" + this->signature.toHex()
                        + "\" but got \"" + ispDeviceSignature.toHex() + "\". Please check target configuration."
                );
            }

            Logger::info("Target signature confirmed: " + ispDeviceSignature.toHex());

            const auto dwenFuseByte = this->avrIspInterface->readFuse(dwenFuseBitsDescriptor->fuseType).value;
            const auto spienFuseByte = (spienFuseBitsDescriptor->fuseType == dwenFuseBitsDescriptor->fuseType)
                ? dwenFuseByte
                : this->avrIspInterface->readFuse(spienFuseBitsDescriptor->fuseType).value;

            /*
             * Keep in mind that, for AVR fuses and lock bits, a set bit (0b1) means the fuse/lock is cleared, and a
             * cleared bit (0b0), means the fuse/lock is set.
             */

            if (!this->isFuseEnabled(*spienFuseBitsDescriptor, spienFuseByte)) {
                /*
                 * If we get here, something is very wrong. The SPIEN (SPI enable) fuse bit appears to be cleared, but
                 * this is not possible because we're connected to the target via the SPI (the ISP interface uses a
                 * physical SPI between the debug tool and the target).
                 *
                 * This could be (and likely is) caused by incorrect data for the SPIEN fuse bit, in the TDF (which was
                 * used to construct the spienFuseBitsDescriptor). And if the data for the SPIEN fuse bit is incorrect,
                 * then what's to say the data for the DWEN fuse bit (dwenFuseBitsDescriptor) is any better?
                 *
                 * We must assume the worst and abort the operation. Otherwise, we risk bricking the user's hardware.
                 */
                throw Exception(
                    "Invalid SPIEN fuse bit value - suspected inaccuracies in TDF data. Please report this to "
                        "Bloom developers as a matter of urgency, via " + Services::PathService::homeDomainName()
                        + "/report-issue"
                );
            }

            Logger::info("Current SPIEN fuse bit value confirmed");

            if (this->isFuseEnabled(*dwenFuseBitsDescriptor, dwenFuseByte) == enable) {
                /*
                 * The DWEN fuse appears to already be set to the desired value. This may be a result of incorrect data
                 * in the TDF, but we're not taking any chances.
                 *
                 * We don't throw an exception here, because we don't know if this is due to an error, or if the fuse
                 * bit is simply already set to the desired value.
                 */
                Logger::debug("DWEN fuse bit already set to desired value - aborting update operation");

                this->avrIspInterface->deactivate();
                return;
            }

            const auto lockBitByte = this->avrIspInterface->readLockBitByte();
            if (lockBitByte != 0xFF) {
                /*
                 * There is at least one lock bit that is set. Setting the DWEN fuse bit with the lock bits set may
                 * brick the device. We must abort.
                 */
                throw Exception(
                    "At least one lock bit has been set - updating the DWEN fuse bit could potentially brick "
                        "the target."
                );
            }

            Logger::info("Cleared lock bits confirmed");

            const auto newFuse = Fuse(
                dwenFuseBitsDescriptor->fuseType,
                this->setFuseEnabled(*dwenFuseBitsDescriptor, dwenFuseByte, enable)
            );

            Logger::warning("Updating DWEN fuse bit");
            this->avrIspInterface->programFuse(newFuse);

            Logger::debug("Verifying DWEN fuse bit");
            if (this->avrIspInterface->readFuse(dwenFuseBitsDescriptor->fuseType).value != newFuse.value) {
                throw Exception("Failed to update DWEN fuse bit - post-update verification failed");
            }

            Logger::info("DWEN fuse bit successfully updated");

            this->avrIspInterface->deactivate();

        } catch (const Exception& exception) {
            this->avrIspInterface->deactivate();
            throw exception;
        }
    }

    void Avr8::updateOcdenFuseBit(bool enable) {
        using Services::PathService;
        using Services::StringService;

        if (!this->supportedPhysicalInterfaces.contains(PhysicalInterface::JTAG)) {
            throw Exception(
                "Target does not support JTAG physical interface - check target configuration or "
                    "report this issue via " + PathService::homeDomainName() + "/report-issue"
            );
        }

        const auto targetSignature = this->avr8DebugInterface->getDeviceId();
        const auto tdSignature = this->targetDescriptionFile.getTargetSignature();

        if (targetSignature != tdSignature) {
            throw Exception(
                "Failed to validate connected target - target signature mismatch.\nThe target signature"
                    " (\"" + targetSignature.toHex() + "\") does not match the AVR8 target description signature (\""
                    + tdSignature.toHex() + "\"). This will likely be due to an incorrect target name in the "
                    + "configuration file (bloom.yaml)."
            );
        }

        const auto ocdenFuseBitsDescriptor = this->targetDescriptionFile.getOcdenFuseBitsDescriptor();
        const auto jtagenFuseBitsDescriptor = this->targetDescriptionFile.getJtagenFuseBitsDescriptor();

        if (!ocdenFuseBitsDescriptor.has_value()) {
            throw Exception("Could not find OCDEN bit field in TDF.");
        }

        if (!jtagenFuseBitsDescriptor.has_value()) {
            throw Exception("Could not find JTAGEN bit field in TDF.");
        }

        const auto ocdenFuseByteValue = this->avr8DebugInterface->readMemory(
            TargetMemoryType::FUSES,
            ocdenFuseBitsDescriptor->byteAddress,
            1
        ).at(0);
        const auto jtagenFuseByteValue = jtagenFuseBitsDescriptor->byteAddress == ocdenFuseBitsDescriptor->byteAddress
            ? ocdenFuseByteValue
            : this->avr8DebugInterface->readMemory(
                TargetMemoryType::FUSES,
                jtagenFuseBitsDescriptor->byteAddress,
                1
            ).at(0)
        ;

        Logger::debug("OCDEN fuse byte value (before update): 0x" + StringService::toHex(ocdenFuseByteValue));

        if (!this->isFuseEnabled(*jtagenFuseBitsDescriptor, jtagenFuseByteValue)) {
            /*
             * If we get here, something has gone wrong. The JTAGEN fuse should always be programmed by this point.
             * We wouldn't have been able to activate the JTAG physical interface if the fuse wasn't programmed.
             *
             * This means the data we have on the JTAGEN fuse bit, from the TDF, is likely incorrect. And if that's
             * the case, we cannot rely on the data for the OCDEN fuse bit being any better.
             */
            throw Exception(
                "Invalid JTAGEN fuse bit value - suspected inaccuracies in TDF data. Please report this to "
                "Bloom developers as a matter of urgency, via " + PathService::homeDomainName() + "/report-issue"
            );
        }

        if (this->isFuseEnabled(*ocdenFuseBitsDescriptor, ocdenFuseByteValue) == enable) {
            Logger::debug("OCDEN fuse bit already set to desired value - aborting update operation");
            return;
        }

        const auto newValue = this->setFuseEnabled(*ocdenFuseBitsDescriptor, ocdenFuseByteValue, enable);

        Logger::debug("New OCDEN fuse byte value (to be written): 0x" + StringService::toHex(newValue));

        Logger::warning("Updating OCDEN fuse bit");
        this->avr8DebugInterface->writeMemory(
            TargetMemoryType::FUSES,
            ocdenFuseBitsDescriptor->byteAddress,
            {newValue}
        );

        Logger::debug("Verifying OCDEN fuse bit");
        const auto postUpdateOcdenByteValue = this->avr8DebugInterface->readMemory(
            TargetMemoryType::FUSES,
            ocdenFuseBitsDescriptor->byteAddress,
            1
        ).at(0);

        if (postUpdateOcdenByteValue != newValue) {
            throw Exception("Failed to update OCDEN fuse bit - post-update verification failed");
        }

        Logger::info("OCDEN fuse bit updated");

        this->disableProgrammingMode();
    }

    bool Avr8::updateEesaveFuseBit(bool enable) {
        using Services::StringService;

        const auto eesaveFuseBitsDescriptor = this->targetDescriptionFile.getEesaveFuseBitsDescriptor();

        if (!eesaveFuseBitsDescriptor.has_value()) {
            throw Exception("Could not find EESAVE bit field in TDF.");
        }

        const auto eesaveFuseByteValue = this->avr8DebugInterface->readMemory(
            TargetMemoryType::FUSES,
            eesaveFuseBitsDescriptor->byteAddress,
            1
        ).at(0);

        Logger::debug("EESAVE fuse byte value (before update): 0x" + StringService::toHex(eesaveFuseByteValue));

        if (this->isFuseEnabled(*eesaveFuseBitsDescriptor, eesaveFuseByteValue) == enable) {
            Logger::debug("EESAVE fuse bit already set to desired value - aborting update operation");
            return false;
        }

        const auto newValue = this->setFuseEnabled(*eesaveFuseBitsDescriptor, eesaveFuseByteValue, enable);

        Logger::debug("New EESAVE fuse byte value (to be written): 0x" + StringService::toHex(newValue));

        Logger::warning("Updating EESAVE fuse bit");
        this->avr8DebugInterface->writeMemory(
            TargetMemoryType::FUSES,
            eesaveFuseBitsDescriptor->byteAddress,
            {newValue}
        );

        Logger::debug("Verifying EESAVE fuse bit");
        const auto postUpdateEesaveByteValue = this->avr8DebugInterface->readMemory(
            TargetMemoryType::FUSES,
            eesaveFuseBitsDescriptor->byteAddress,
            1
        ).at(0);

        if (postUpdateEesaveByteValue != newValue) {
            throw Exception("Failed to update EESAVE fuse bit - post-update verification failed");
        }

        Logger::info("EESAVE fuse bit updated");

        return true;
    }

    ProgramMemorySection Avr8::getProgramMemorySectionFromAddress(std::uint32_t address) {
        return this->targetParameters.bootSectionStartAddress.has_value()
            && address >= this->targetParameters.bootSectionStartAddress.value()
            ? ProgramMemorySection::BOOT
            : ProgramMemorySection::APPLICATION;
    }
}
