<?php
namespace Targets\TargetDescriptionFiles\Services\Xml;

use DOMDocument;
use DOMNode;
use DOMElement;
use DOMNodeList;
use RuntimeException;
use Targets\TargetDescriptionFiles\Avr8\Services\ValidationService;
use Targets\TargetDescriptionFiles\Pad;
use Targets\TargetDescriptionFiles\Services\StringService;
use Targets\TargetDescriptionFiles\Avr8\Avr8TargetDescriptionFile;
use Targets\TargetDescriptionFiles\Avr8\AvrFamily;
use Targets\TargetDescriptionFiles\TargetFamily;
use Targets\TargetDescriptionFiles\PropertyGroup;
use Targets\TargetDescriptionFiles\Property;
use Targets\TargetDescriptionFiles\AddressSpace;
use Targets\TargetDescriptionFiles\PhysicalInterface;
use Targets\TargetDescriptionFiles\MemorySegment;
use Targets\TargetDescriptionFiles\MemorySegmentType;
use Targets\TargetDescriptionFiles\MemorySegmentSection;
use Targets\TargetDescriptionFiles\Peripheral;
use Targets\TargetDescriptionFiles\RegisterGroupInstance;
use Targets\TargetDescriptionFiles\Signal;
use Targets\TargetDescriptionFiles\Module;
use Targets\TargetDescriptionFiles\RegisterGroup;
use Targets\TargetDescriptionFiles\RegisterGroupReference;
use Targets\TargetDescriptionFiles\Register;
use Targets\TargetDescriptionFiles\BitField;
use Targets\TargetDescriptionFiles\Pinout;
use Targets\TargetDescriptionFiles\PinoutType;
use Targets\TargetDescriptionFiles\Pin;
use Targets\TargetDescriptionFiles\Variant;
use Targets\TargetPhysicalInterface;

require_once __DIR__ . '/StringService.php';
require_once __DIR__ . '/../Avr8/Avr8TargetDescriptionFile.php';

/**
 * This AtdfService provides ATDF -> TDF conversion.
 *
 * We use this to convert ATDFs of new Microchip AVR targets to Bloom's TDF format.
 * See the ConvertAtdfToTdf.php script for more.
 *
 * NOTE: This service class is only intended for performing a first-pass conversion - it will do most of the work, but
 * may not cover every corner, so the generated TDF may require some manual touch-ups. It's best to run the generated
 * TDF through validation (@see ValidationService;), to identify any issues that require attention.
 */
class AtdfService
{
    private StringService $stringService;

    public function __construct(?StringService $stringService = null)
    {
        $this->stringService = $stringService ?? new StringService();
    }

    /**
     * Searches for ATDF files in the given directory, recursively.
     *
     * Rudimentary implementation - filters files by .atdf file extension.
     *
     * @param string $directoryPath
     *
     * @return \SplFileInfo[]
     *  An SplFileInfo instance for each ATDF file.
     */
    public function findFiles(string $directoryPath): array
    {
        $output = [];

        $directory = new \DirectoryIterator($directoryPath);
        foreach ($directory as $entry) {
            if ($entry->isFile() && strtolower($entry->getExtension()) === 'atdf') {
                $output[] = clone $entry;

            } elseif ($entry->isDir() && !$entry->isDot()) {
                $output = array_merge($output, $this->findFiles($entry->getPathname()));
            }
        }

        return $output;
    }

    /**
     * Processes an ATDF XML document and generates a TDF object.
     *
     * @param DOMDocument $document
     *  The ATDF XML document to process.
     *
     * @return Avr8TargetDescriptionFile
     *  The generated TDF object.
     *
     * @throws \Exception
     */
    public function toTdf(DOMDocument $document): Avr8TargetDescriptionFile
    {
        $tdf = new Avr8TargetDescriptionFile();

        $deviceElements = $this->getElementsFromXPath('devices/device', $document);
        if ($deviceElements->count() !== 1) {
            throw new RuntimeException('Unexpected number of device elements');
        }
        $deviceElement = $deviceElements->item(0);

        $deviceAttributesByName = $this->getNodeAttributesByName($deviceElement);
        $tdf->deviceAttributesByName = [
            'name' => $deviceAttributesByName['name'] ?? null,
            'family' => TargetFamily::AVR_8->value,
            'architecture' => $deviceAttributesByName['architecture'] ?? null,
            'configuration-value' => isset($deviceAttributesByName['name'])
                ? strtolower($deviceAttributesByName['name'])
                : null,
            'avr-family' => $this->resolveAvrFamily(
                $deviceAttributesByName['family'] ?? '',
                $deviceAttributesByName['name'] ?? ''
            )->value ?? null,
        ];

        $propertyGroupElements = $this->getDeviceElementsFromXPath(
            'property-groups/property-group',
            $document
        );
        foreach ($propertyGroupElements as $element) {
            $tdf->propertyGroups[] = $this->propertyGroupFromElement($element);
        }

        $addressSpaceElements = $this->getDeviceElementsFromXPath(
            'address-spaces/address-space',
            $document
        );
        foreach ($addressSpaceElements as $element) {
            $tdf->addressSpaces[] = $this->addressSpaceFromElement($element, $tdf);
        }

        $interfaceElements = $this->getDeviceElementsFromXPath(
            'interfaces/interface',
            $document
        );
        foreach ($interfaceElements as $element) {
            $tdf->physicalInterfaces[] = $this->physicalInterfaceFromElement($element);
        }

        $peripheralModuleElements = $this->getDeviceElementsFromXPath(
            'peripherals/module',
            $document
        );
        foreach ($peripheralModuleElements as $peripheralModuleElement) {
            foreach ($peripheralModuleElement->getElementsByTagName('instance') as $element) {
                $tdf->peripherals[] = $this->peripheralFromElement(
                    $element,
                    $this->getNodeAttributesByName($peripheralModuleElement)
                );
            }
        }

        $moduleElements = $this->getElementsFromXPath(
            'modules/module',
            $document
        );
        foreach ($moduleElements as $element) {
            $tdf->modules[] = $this->moduleFromElement($element);
        }

        $padsByKey = [];
        $pinoutElements = $this->getElementsFromXPath(
            'pinouts/pinout',
            $document
        );
        foreach ($pinoutElements as $element) {
            $tdf->pinouts[] = $this->pinoutFromElement($element);

            foreach ($element->childNodes as $childNode) {
                if (!$childNode instanceof DOMElement) {
                    continue;
                }

                if ($childNode->nodeName === 'pin') {
                    $pad = $this->padFromPinElement($childNode);
                    if ($pad->key === null || $pad->key === 'nc') {
                        continue;
                    }

                    $padsByKey[$pad->key] = $pad;
                }
            }
        }

        $tdf->pads = array_values($padsByKey);

        // Sort pads by key
        uasort($tdf->pads, function (Pad $padA, Pad $padB): bool {
            return $padA->key > $padB->key;
        });

        $variantElements = $this->getElementsFromXPath(
            'variants/variant',
            $document
        );
        foreach ($variantElements as $element) {
            $tdf->variants[] = $this->variantFromElement($element);
        }

        $moduleKeysToRenameByName = [
            'port' => 'gpio_port'
        ];

        foreach ($tdf->modules as $module) {
            if (array_key_exists(strtolower($module->name), $moduleKeysToRenameByName)) {
                $tdf->renameModuleKey($module, $moduleKeysToRenameByName[strtolower($module->name)]);
            }
        }

        // Rename GPIO port signals
        foreach ($tdf->peripherals as $peripheral) {
            if ($peripheral->moduleKey !== 'gpio_port') {
                continue;
            }

            foreach ($peripheral->signals as $signal) {
                $signal->name = strtoupper((string) $signal->padKey);
            }
        }

        if (in_array(TargetPhysicalInterface::UPDI, $tdf->getSupportedDebugPhysicalInterfaces())) {
            /*
             * ATDFs for UPDI-enabled targets do not typically possess an `ocd_base_addr` property in the updi_interface
             * property group. Bloom needs this property to configure EDBG debug tools, so we add it here.
             *
             * The value of the property (0x00000F80) seems to be the same across all UPDI-enabled targets, so maybe
             * that's why it wasn't included in the ATDFs.
             */
            if (
                ($updiInterfacePropertyGroup = $tdf->getPropertyGroup('updi_interface')) !== null
                && $updiInterfacePropertyGroup->getProperty('ocd_base_addr') === null
            ) {
                $updiInterfacePropertyGroup->properties[] = new Property('ocd_base_addr', '0x00000F80');
            }
        }

        if ($tdf->getAvrFamily() === AvrFamily::XMEGA) {
            /*
             * ATDFs for XMEGA targets do not typically possess a `signature_offset` property in the pdi_interface
             * property group. Bloom needs this property to configure EDBG debug tools, so we add it here.
             *
             * The value of the property (0x00000090) seems to be the same across all XMEGA targets, so maybe that's
             * why it wasn't included in the ATDFs.
             */
            if (
                ($pdiInterfacePropertyGroup = $tdf->getPropertyGroup('pdi_interface')) !== null
                && $pdiInterfacePropertyGroup->getProperty('signature_offset') === null
            ) {
                $pdiInterfacePropertyGroup->properties[] = new Property('signature_offset', '0x00000090');
            }
        }

        return $tdf;
    }

    private function getElementsFromXPath(string $expression, DOMDocument $document): DOMNodeList
    {
        $elements = (new \DOMXPath($document))->query('/avr-tools-device-file/' . $expression);

        if ($elements === false) {
            throw new \Exception('Failed to evaluate XPath expression - malformed expression');
        }

        return $elements;
    }

    private function getImmediateChildElements(DOMElement $element, string $name): array
    {
        $output = [];
        foreach ($element->childNodes as $childNode) {
            if (!$childNode instanceof DOMElement || $childNode->nodeName !== $name) {
                continue;
            }

            $output[] = $childNode;
        }

        return $output;
    }

    private function getDeviceElementsFromXPath(string $expression, DOMDocument $document): DOMNodeList
    {
        return $this->getElementsFromXPath('devices/device[1]/' . $expression, $document);
    }

    private function getNodeAttributesByName(DOMNode $node): array
    {
        $output = [];
        foreach ($node->attributes as $attribute) {
            /** @var \DOMAttr $attribute */
            $output[$attribute->name] = $attribute->value;
        }

        return $output;
    }

    private function resolveAvrFamily(string $atdfFamilyName, string $atdfTargetName): ?AvrFamily
    {
        $atdfFamilyName = strtolower(trim($atdfFamilyName));
        $atdfTargetName = strtolower(trim($atdfTargetName));

        if (str_contains($atdfFamilyName, 'xmega') !== false) {
            return AvrFamily::XMEGA;
        }

        if (str_contains($atdfFamilyName, 'tiny') !== false) {
            return AvrFamily::TINY;
        }

        if (str_contains($atdfFamilyName, 'mega') !== false) {
            return AvrFamily::MEGA;
        }

        if (strlen($atdfTargetName) >= 7 && str_starts_with($atdfTargetName, 'avr')) {
            if ($atdfFamilyName === 'avr da' || substr($atdfTargetName, 5, 2) === 'da') {
                return AvrFamily::DA;
            }

            if ($atdfFamilyName === 'avr db' || substr($atdfTargetName, 5, 2) === 'db') {
                return AvrFamily::DB;
            }

            if ($atdfFamilyName === 'avr dd' || substr($atdfTargetName, 5, 2) === 'dd') {
                return AvrFamily::DD;
            }

            if ($atdfFamilyName === 'avr du' || substr($atdfTargetName, 5, 2) === 'du') {
                return AvrFamily::DU;
            }

            if ($atdfFamilyName === 'avr ea' || substr($atdfTargetName, 5, 2) === 'ea') {
                return AvrFamily::EA;
            }

            if ($atdfFamilyName === 'avr eb' || substr($atdfTargetName, 5, 2) === 'eb') {
                return AvrFamily::EB;
            }
        }

        return null;
    }

    private function propertyGroupFromElement(DOMElement $element): PropertyGroup
    {
        $attributes = $this->getNodeAttributesByName($element);

        $output = new PropertyGroup(isset($attributes['name']) ? strtolower($attributes['name']) : null, [], []);

        foreach ($element->childNodes as $childNode) {
            if (!$childNode instanceof DOMElement) {
                continue;
            }

            if ($childNode->nodeName === 'property') {
                $output->properties[] = $this->propertyFromElement($childNode);
            }
        }

        return $output;
    }

    private function propertyFromElement(DOMElement $element): Property
    {
        $attributes = $this->getNodeAttributesByName($element);
        return new Property(
            isset($attributes['name']) ? strtolower($attributes['name']) : null,
            $attributes['value'] ?? null,
        );
    }

    private function addressSpaceFromElement(DOMElement $element, Avr8TargetDescriptionFile $tdf): AddressSpace
    {
        $attributes = $this->getNodeAttributesByName($element);

        $output = new AddressSpace(
            isset($attributes['id']) ? strtolower($attributes['id']) : null,
            $this->stringService->tryStringToInt($attributes['start'] ?? null),
            $this->stringService->tryStringToInt($attributes['size'] ?? null),
            null,
            $attributes['endianness'] ?? null,
        );

        foreach ($element->childNodes as $childNode) {
            if (!$childNode instanceof DOMElement) {
                continue;
            }

            if ($childNode->nodeName === 'memory-segment') {
                $output->memorySegments[] = $this->memorySegmentFromElement($childNode);
            }
        }

        if ($tdf->getAvrFamily() === AvrFamily::MEGA && $output->key === 'prog') {
            /*
             * ATDFs for some MEGA targets contain boot section **options** as memory segments in the `prog` address
             * space. By "options", I mean the target can be configured with one of these boot sections, via the
             * setting of some fuse/register.
             *
             * So these are not actual memory segments - they're just potential sections of the program memory segment.
             * For this reason, we convert them into property groups and discard the segments.
             */
            $bootSectionPropertyGroups = [];
            $filteredSegments = [];

            foreach ($output->memorySegments as $segment) {
                if (!str_starts_with($segment->key, 'boot_section_')) {
                    $filteredSegments[] = $segment;
                    continue;
                }

                $bootSectionPropertyGroups[] = new PropertyGroup(
                    $segment->key,
                    [],
                    [
                        new Property(
                            'start_address',
                            '0x' . str_pad(
                                strtoupper(dechex($segment->addressRange->startAddress)),
                                8,
                                '0',
                                STR_PAD_LEFT
                            )
                        ),
                        new Property('size', $segment->size()),
                        new Property('page_size', $segment->pageSize),
                    ]
                );
            }

            if (!empty($bootSectionPropertyGroups)) {
                $tdf->propertyGroups[] = new PropertyGroup(
                    'boot_section_options',
                    $bootSectionPropertyGroups,
                    []
                );

                $output->memorySegments = $filteredSegments;
            }
        }

        /*
         * Bloom will look up certain memory segments using specific keys, so we must ensure that these segments hold
         * the correct key.
         *
         * While we're at it, we also rename some other keys and segment names, for the sake of consistency.
         */
        $segmentKeysToRename = [
            'flash' => 'internal_program_memory',
            'progmem' => 'internal_program_memory',
            'internal_sram' => 'internal_ram',
            'iram' => 'internal_ram',
            'external_sram' => 'external_ram',
            'eeprom' => 'internal_eeprom',
            'registers' => 'gp_registers',
        ];

        $segmentNamesToRename = [
            'boot_section' => 'Boot Section',
            'io' => 'Input/Output',
            'mapped_io' => 'Mapped Input/Output',
            'mapped_eeprom' => 'Mapped EEPROM',
            'internal_ram' => 'Internal RAM',
            'external_ram' => 'External RAM',
            'internal_program_memory' => 'Internal FLASH',
            'internal_eeprom' => 'Internal EEPROM',
            'gp_registers' => 'General Purpose Registers',
            'signatures' => 'Signatures',
            'fuses' => 'Fuses',
            'lockbits' => 'Lockbits',
            'user_signatures' => 'User Signatures',
            'prod_signatures' => 'Production Signatures',
        ];

        foreach ($output->memorySegments as $segment) {
            if (array_key_exists($segment->key, $segmentKeysToRename)) {
                $segment->key = $segmentKeysToRename[$segment->key];
            }

            if (array_key_exists($segment->key, $segmentNamesToRename)) {
                $segment->name = $segmentNamesToRename[$segment->key];
            }
        }

        /*
         * Like Bloom's TDFs, ATDFs have sections within memory segments, but ATDFs do not have a specific element for
         * sections. They just use overlapping `<memory-segment>` elements to describe sections.
         *
         * Bloom's TDFs have a dedicated element for sections, which reside in memory segment elements and can be
         * nested.
         *
         * So now, we must convert all overlapping memory segments to sections.
         */

        $convertedSegmentKeys = [];

        foreach ($output->memorySegments as $segment) {
            if (in_array($segment->key, $convertedSegmentKeys)) {
                continue;
            }

            foreach ($output->memorySegments as $otherSegment) {
                if ($otherSegment->key === $segment->key) {
                    continue;
                }

                if ($segment->contains($otherSegment)) {
                    $convertedSegmentKeys[] = $otherSegment->key;

                    $section = new MemorySegmentSection(
                        $otherSegment->key,
                        $otherSegment->name,
                        $otherSegment->addressRange->startAddress,
                        $otherSegment->size(),
                        null,
                        []
                    );

                    /*
                     * Find the appropriate parent section, if there is one. Otherwise, just store the new section in
                     * the segment.
                     */
                    if (
                        $otherSegment->addressRange !== null
                        && ($parentSection = $segment->getInnermostSectionContainingAddressRange(
                            $otherSegment->addressRange
                        )) !== null
                    ) {
                        $parentSection->subSections[] = $section;

                    } else {
                        $segment->sections[] = $section;
                    }
                }
            }
        }

        // Remove the converted memory segments
        $output->memorySegments = array_filter(
            $output->memorySegments,
            fn (MemorySegment $segment): bool => !in_array($segment->key, $convertedSegmentKeys)
        );

        /*
         * ATDFs for XMEGA targets have a separate memory segment for the application and boot section of program
         * memory, but they have no segment for the entire program memory. This is inconsistent with the other ATDFs
         * and Bloom expects all TDFs to contain one internal_program_memory segment.
         *
         * For these reasons, we overwrite the program memory address space with a better structured memory segment.
         */
        if (
            $output->key === 'prog'
            && $tdf->getAvrFamily() === AvrFamily::XMEGA
            && ($appSectionSegment = $output->getMemorySegment('app_section')) !== null
            && ($bootSectionSegment = $output->getMemorySegment('boot_section')) !== null
            && ($appTableSection = $appSectionSegment->getSection('apptable_section')) !== null
        ) {
            $output->memorySegments = [
                new MemorySegment(
                    'internal_program_memory',
                    'Internal FLASH',
                    MemorySegmentType::FLASH,
                    $appSectionSegment->addressRange->startAddress,
                    $appSectionSegment->size() + $bootSectionSegment->size(),
                    null,
                    $appSectionSegment->pageSize,
                    $appSectionSegment->access,
                    [
                        new MemorySegmentSection(
                            'app_section',
                            'Application Section',
                            $appSectionSegment->addressRange->startAddress,
                            $appSectionSegment->size(),
                            null,
                            [
                                new MemorySegmentSection(
                                    'app_table_section',
                                    'Application Table Section',
                                    $appTableSection->addressRange->startAddress,
                                    $appTableSection->size(),
                                    null,
                                    []
                                )
                            ]
                        ),
                        new MemorySegmentSection(
                            'boot_section',
                            'Boot Loader Section',
                            $bootSectionSegment->addressRange->startAddress,
                            $bootSectionSegment->size(),
                            null,
                            []
                        )
                    ],
                    $appSectionSegment->executable
                )
            ];
        }

        return $output;
    }

    private function memorySegmentFromElement(DOMElement $element): MemorySegment
    {
        // Some segments tend to have invalid page sizes.
        $ignorePageSizeForSegments = [
            'signatures',
            'prod_signatures',
            'user_signatures',
            'lockbits',
            'fuses',
        ];

        $attributes = $this->getNodeAttributesByName($element);

        $segmentKey = isset($attributes['name']) ? strtolower($attributes['name']) : null;
        return new MemorySegment(
            $segmentKey,
            $attributes['name'] ?? null,
            $this->resolveMemorySegmentType(
                $attributes['type'] ?? '',
                $attributes['name'] ?? ''
            ),
            $this->stringService->tryStringToInt($attributes['start'] ?? null),
            $this->stringService->tryStringToInt($attributes['size'] ?? null),
            null,
            !in_array($segmentKey, $ignorePageSizeForSegments)
                ? $this->stringService->tryStringToInt($attributes['pagesize'] ?? null)
                : null,
            $attributes['rw'] ?? null,
            [],
            isset($attributes['exec']) && $attributes['exec']
        );
    }

    private function resolveMemorySegmentType(string $atdfTypeName, string $atdfSegmentName): ?MemorySegmentType
    {
        $atdfTypeName = strtolower($atdfTypeName);
        $atdfSegmentName = strtolower($atdfSegmentName);

        if (
            $atdfTypeName === 'aliased'
            || ($atdfTypeName === 'other' && str_starts_with($atdfSegmentName, 'mapped_'))
        ) {
            return MemorySegmentType::ALIASED;
        }

        if ($atdfTypeName === 'production_signatures' || $atdfSegmentName === 'prod_signatures') {
            return MemorySegmentType::PRODUCTION_SIGNATURES;
        }

        return match ($atdfTypeName) {
            'gp_registers', 'regs' => MemorySegmentType::GENERAL_PURPOSE_REGISTERS,
            'eeprom' => MemorySegmentType::EEPROM,
            'flash' => MemorySegmentType::FLASH,
            'fuses' => MemorySegmentType::FUSES,
            'io' => MemorySegmentType::IO,
            'ram' => MemorySegmentType::RAM,
            'lockbits' => MemorySegmentType::LOCKBITS,
            'osccal' => MemorySegmentType::OSCCAL,
            'signatures' => MemorySegmentType::SIGNATURES,
            'user_signatures' => MemorySegmentType::USER_SIGNATURES,
            default => null
        };
    }

    private function physicalInterfaceFromElement(DOMElement $element): PhysicalInterface
    {
        $physicalInterfacesByName = [
            'isp' => TargetPhysicalInterface::ISP,
            'debugwire' => TargetPhysicalInterface::DEBUG_WIRE,
            'updi' => TargetPhysicalInterface::UPDI,
            'pdi' => TargetPhysicalInterface::PDI,
            'jtag' => TargetPhysicalInterface::JTAG,
        ];

        $attributes = $this->getNodeAttributesByName($element);
        return new PhysicalInterface(
            $physicalInterfacesByName[strtolower($attributes['name'] ?? '')]?->value
                ?? (!empty($attributes['type']) ? strtolower($attributes['type']) : null),
            []
        );
    }

    private function peripheralFromElement(DOMElement $element, array $moduleAttributes): Peripheral
    {
        $attributes = $this->getNodeAttributesByName($element);

        $output = new Peripheral(
            isset($attributes['name']) ? strtolower($attributes['name']) : null,
            $attributes['name'] ?? null,
            strtolower(
                isset($moduleAttributes['id']) && isset($moduleAttributes['name'])
                    ? $moduleAttributes['id'] . '_' . $moduleAttributes['name']
                    : $moduleAttributes['id'] ?? $moduleAttributes['name'] ?? ''
            ),
            [],
            []
        );

        foreach ($element->childNodes as $childNode) {
            if (!$childNode instanceof DOMElement) {
                continue;
            }

            if ($childNode->nodeName === 'register-group') {
                $output->registerGroupInstances[] = $this->registerGroupInstanceFromElement($childNode);
            }
        }

        if (count($output->registerGroupInstances) === 1) {
            /*
             * ATDF peripherals sometimes override the register group keys and names, when there's only a single
             * register group in the peripheral.
             *
             * This is not necessary for Bloom's TDFs, so we tidy it up here by clearing the `key` and `name`
             * attributes where we can.
             */
            foreach ($output->registerGroupInstances as $instance) {
                if ($output->key == 'fuse' && $instance->key !== $instance->registerGroupKey) {
                    continue;
                }

                $instance->key = null;
                $instance->name = null;
            }
        }

        $signalElement = $element->getElementsByTagName('signals')->item(0);
        if ($signalElement instanceof DOMElement) {
            foreach ($signalElement->childNodes as $childNode) {
                if (!$childNode instanceof DOMElement) {
                    continue;
                }

                if ($childNode->nodeName === 'signal') {
                    $output->signals[] = $this->signalFromElement($childNode);
                }
            }
        }

        return $output;
    }

    private function registerGroupInstanceFromElement(DOMElement $element): RegisterGroupInstance
    {
        $attributes = $this->getNodeAttributesByName($element);

        return new RegisterGroupInstance(
            isset($attributes['name']) ? strtolower($attributes['name']) : null,
            $attributes['name'] ?? null,
            isset($attributes['name-in-module'])
                ? strtolower($attributes['name-in-module'])
                : null,
            isset($attributes['address-space'])
                ? strtolower($attributes['address-space'])
                : null,
            $this->stringService->tryStringToInt($attributes['offset'] ?? null),
            $attributes['caption'] ?? null
        );
    }

    private function signalFromElement(DOMElement $element): Signal
    {
        $attributes = $this->getNodeAttributesByName($element);

        $padKey = isset($attributes['pad']) ? strtolower(trim($attributes['pad'])) : null;
        $function = $attributes['function'] ?? null;
        return new Signal(
            isset($attributes['group'])
                ? trim($attributes['group'])
                : (!empty($padKey) ? strtoupper($padKey) : null),
            $padKey,
            stristr((string) $function, 'default') !== false
                ? false
                : (stristr((string) $function, '_alt') !== false ? true : null),
            $this->stringService->tryStringToInt($attributes['index'] ?? null),
            $function,
            $attributes['field'] ?? null
        );
    }

    private function moduleFromElement(DOMElement $element): Module
    {
        $attributes = $this->getNodeAttributesByName($element);

        $output = new Module(
            strtolower(
                isset($attributes['id']) && isset($attributes['name'])
                    ? $attributes['id'] . '_' . $attributes['name']
                    : $attributes['id'] ?? $attributes['name'] ?? ''
            ),
            $attributes['name'] ?? null,
            $attributes['caption'] ?? null,
            []
        );

        foreach ($element->childNodes as $childNode) {
            if (!$childNode instanceof DOMElement) {
                continue;
            }

            if ($childNode->nodeName === 'register-group') {
                $output->registerGroups = array_merge(
                    $output->registerGroups,
                    $this->registerGroupFromElement($childNode)
                );
            }
        }

        return $output;
    }

    private function registerGroupFromElement(DOMElement $element): array
    {
        $attributes = $this->getNodeAttributesByName($element);

        $modeElements = $this->getImmediateChildElements($element, 'mode');
        if (!empty($modeElements)) {
            $parentGroup = new RegisterGroup(
                isset($attributes['name']) ? strtolower($attributes['name']) : null,
                $attributes['name'] ?? null,
                $this->stringService->tryStringToInt($attributes['offset'] ?? null),
                [],
                [],
                []
            );

            /** @var RegisterGroup[] $modeGroupsByName */
            $modeGroupsByName = [];

            foreach ($modeElements as $modeElement) {
                $modeAttributes = $this->getNodeAttributesByName($modeElement);
                $modeGroup = new RegisterGroup(
                    isset($modeAttributes['name']) && $parentGroup->key !== null
                        ? $parentGroup->key . '_' . strtolower($modeAttributes['name'])
                        : null,
                    $modeAttributes['name'] ?? null,
                    null,
                    [],
                    [],
                    []
                );
                $modeGroupsByName[$modeGroup->name] = $modeGroup;

                $parentGroup->subgroupReferences[] = new RegisterGroupReference(
                    $modeGroup->key,
                    $modeGroup->name,
                    $modeGroup->key,
                    0,
                    null
                );
            }

            foreach ($element->childNodes as $childNode) {
                if (!$childNode instanceof DOMElement) {
                    continue;
                }

                if ($childNode->nodeName === 'register') {
                    $registerAttributes = $this->getNodeAttributesByName($childNode);
                    if (
                        isset($registerAttributes['modes'])
                        && array_key_exists($registerAttributes['modes'], $modeGroupsByName)
                    ) {
                        $modeGroupsByName[$registerAttributes['modes']]->registers[] = $this->registerFromElement(
                            $childNode
                        );

                    } else {
                        $parentGroup->registers[] = $this->registerFromElement($childNode);
                    }

                    continue;
                }

                if ($childNode->nodeName === 'register-group') {
                    $subgroupAttributes = $this->getNodeAttributesByName($childNode);
                    if (
                        isset($subgroupAttributes['modes'])
                        && array_key_exists($subgroupAttributes['modes'], $modeGroupsByName)
                    ) {
                        if (isset($subgroupAttributes['name-in-module'])) {
                            $modeGroupsByName[$subgroupAttributes['modes']]->subgroupReferences[] =
                                $this->registerGroupReferenceFromElement(
                                    $childNode
                                );

                        } else {
                            $modeGroupsByName[$subgroupAttributes['modes']]->subgroups = array_merge(
                                $modeGroupsByName[$subgroupAttributes['modes']]->subgroups,
                                $this->registerGroupFromElement($childNode)
                            );
                        }

                    } else {
                        if (isset($subgroupAttributes['name-in-module'])) {
                            $parentGroup->subgroupReferences[] = $this->registerGroupReferenceFromElement($childNode);

                        } else {
                            $parentGroup->subgroups = array_merge(
                                $parentGroup->subgroups,
                                $this->registerGroupFromElement($childNode)
                            );
                        }
                    }
                }
            }

            return array_merge(array_values($modeGroupsByName), [$parentGroup]);
        }

        $output = new RegisterGroup(
            isset($attributes['name']) ? strtolower($attributes['name']) : null,
            $attributes['name'] ?? null,
            $this->stringService->tryStringToInt($attributes['offset'] ?? null),
            [],
            [],
            []
        );

        foreach ($element->childNodes as $childNode) {
            if (!$childNode instanceof DOMElement) {
                continue;
            }

            if ($childNode->nodeName === 'register') {
                $output->registers[] = $this->registerFromElement($childNode);
                continue;
            }

            if ($childNode->nodeName === 'register-group') {
                if (isset($this->getNodeAttributesByName($childNode)['name-in-module'])) {
                    $output->subgroupReferences[] = $this->registerGroupReferenceFromElement($childNode);

                } else {
                    $output->subgroups = array_merge($output->subgroups, $this->registerGroupFromElement($childNode));
                }
            }
        }

        return [$output];
    }

    private function registerGroupReferenceFromElement(DOMElement $element): RegisterGroupReference
    {
        $attributes = $this->getNodeAttributesByName($element);

        return new RegisterGroupReference(
            isset($attributes['name']) ? strtolower($attributes['name']) : null,
            $attributes['name'] ?? null,
            isset($attributes['name-in-module']) ? strtolower($attributes['name-in-module']) : null,
            $this->stringService->tryStringToInt($attributes['offset'] ?? null),
            $attributes['caption'] ?? null
        );
    }

    private function registerFromElement(DOMElement $element): Register
    {
        $attributes = $this->getNodeAttributesByName($element);

        $output = new Register(
            isset($attributes['name']) ? strtolower($attributes['name']) : null,
            $attributes['name'] ?? null,
            $attributes['caption'] ?? null,
            $this->stringService->tryStringToInt($attributes['offset'] ?? null),
            $this->stringService->tryStringToInt($attributes['size'] ?? null),
            $this->stringService->tryStringToInt($attributes['intval'] ?? null),
            $attributes['rw'] ?? null,
            null,
            []
        );

        foreach ($element->childNodes as $childNode) {
            if (!$childNode instanceof DOMElement) {
                continue;
            }

            if ($childNode->nodeName === 'bitfield') {
                $output->bitFields[] = $this->bitFieldFromElement($childNode);
            }
        }

        return $output;
    }

    private function bitFieldFromElement(DOMElement $element): BitField
    {
        $attributes = $this->getNodeAttributesByName($element);

        return new BitField(
            isset($attributes['name']) ? strtolower($attributes['name']) : null,
            $attributes['name'] ?? null,
            $attributes['caption'] ?? null,
            $this->stringService->tryStringToInt($attributes['mask'] ?? null),
            $attributes['rw'] ?? null
        );
    }

    private function padFromPinElement(DOMElement $element): ?Pad
    {
        $attributes = $this->getNodeAttributesByName($element);

        return new Pad(
            isset($attributes['pad']) ? strtolower(trim($attributes['pad'])) : null,
            trim($attributes['pad'] ?? '')
        );
    }

    private function pinoutFromElement(DOMElement $element): Pinout
    {
        $attributes = $this->getNodeAttributesByName($element);

        $output = new Pinout(
            isset($attributes['name']) ? strtolower($attributes['name']) : null,
            $attributes['name'] ?? null,
            null,
            $attributes['function'] ?? null,
            []
        );

        if (stristr($output->name, PinoutType::DIP->value) !== false) {
            $output->type = PinoutType::DIP;

        } elseif (stristr($output->name, PinoutType::SOIC->value) !== false) {
            $output->type = PinoutType::SOIC;

        } elseif (stristr($output->name, PinoutType::SSOP->value) !== false) {
            $output->type = PinoutType::SSOP;

        } elseif (stristr($output->name, PinoutType::DUAL_ROW_QFN->value) !== false) {
            $output->type = PinoutType::DUAL_ROW_QFN;

        }  elseif (stristr($output->name, PinoutType::QFN->value) !== false) {
            $output->type = PinoutType::QFN;

        }  elseif (stristr($output->name, PinoutType::MLF->value) !== false) {
            $output->type = PinoutType::MLF;

        } elseif (stristr($output->name, PinoutType::QFP->value) !== false) {
            $output->type = PinoutType::QFP;

        }  elseif (stristr($output->name, PinoutType::BGA->value) !== false) {
            $output->type = PinoutType::BGA;
        }

        foreach ($element->childNodes as $childNode) {
            if (!$childNode instanceof DOMElement) {
                continue;
            }

            if ($childNode->nodeName === 'pin') {
                $output->pins[] = $this->pinFromElement($childNode);
            }
        }

        return $output;
    }

    private function pinFromElement(DOMElement $element): Pin
    {
        $attributes = $this->getNodeAttributesByName($element);

        return new Pin(
            $attributes['position'] ?? null,
            ($pad = strtolower(trim($attributes['pad'] ?? ''))) !== 'nc' ? $pad : null
        );
    }

    private function variantFromElement(DOMElement $element): Variant
    {
        $attributes = $this->getNodeAttributesByName($element);

        return new Variant(
            str_replace(
                ['-', '/'],
                '_',
                strtolower($attributes['ordercode'] ?? $attributes['name'] ?? '')
            ),
            $attributes['ordercode'] ?? $attributes['name'] ?? null,
            isset($attributes['pinout']) ? strtolower($attributes['pinout']) : null,
            []
        );
    }
}
