import os
import sys

import dicom_parser

# cspell: ignore Wundefined afterloader angio autorefraction autosequence boli bscan cadsr ciexy ctdi ctdivol decf
# cspell: ignore deidentification dermoscopic diconde dicos dvhroi exif fiducials fluence fnumber frameof gpsdop hicpp
# cspell: ignore hpgl imager inms inua inus iods isop ivol ivus keratoconus keratometer keratometric kerma lensometry
# cspell: ignore lookat meterset metersets mrfov multiway nmpet nmtomo nolintbegin nolintend octb octz oecf olink
# cspell: ignore postamble powerline prcs presaturation radiofluoroscopic relaxivity reprojection rgblut rtdvh rtroi
# cspell: ignore sddn sdhn sdvn softcopy soundpath stereometric stowrs tlhc tomo tomosynthesis tomotherapeutic toric
# cspell: ignore tractography xaxrf

g_root_dir = os.path.dirname(__file__)
g_test_dir = os.path.join(os.path.dirname(g_root_dir), "test", "tu", "dicom")
g_header_extension = ".hpp"
g_source_extension = ".cpp"
g_test = "Test"

g_attribute_class_name = "Attribute"
g_sop_class_name = "Sop"


def generate_cspell_ignore(max_line_length: int = 120) -> str:
    cspell_words = {
        "bscan", "frameof", "isop", "nmpet", "nmtomo", "rtdvh", "afterloader", "angio", "autorefraction",
        "autosequence", "boli", "cadsr", "ciexy", "ctdi", "ctdivol", "decf", "deidentification", "dermoscopic",
        "diconde", "dicos", "dvhroi", "exif", "fiducials", "fluence", "fnumber", "gpsdop", "hicpp", "hpgl", "imager",
        "inms", "inua", "inus", "ivol", "ivus", "keratoconus", "keratometer", "keratometric", "kerma", "lensometry",
        "lookat", "meterset", "metersets", "mrfov", "multiway", "nolintbegin", "nolintend", "octb", "octz", "oecf",
        "postamble", "powerline", "prcs", "presaturation", "radiofluoroscopic", "relaxivity", "reprojection", "rgblut",
        "rtroi", "sddn", "sdhn", "sdvn", "softcopy", "soundpath", "stereometric", "stowrs", "tlhc", "tomo",
        "tomosynthesis", "tomotherapeutic", "toric", "tractography", "Wundefined", "xaxrf", "olink", "iods"
    }

    cspell_preamble = "// cspell: ignore"
    cspell_lines = cspell_preamble
    cspell_line_length = len(cspell_lines)

    for cspell_word in sorted(cspell_words):
        if cspell_line_length + len(cspell_word) >= max_line_length:
            new_line = cspell_preamble + " " + cspell_word
            cspell_line_length = len(new_line)
            cspell_lines += "\n" + new_line
        else:
            new_word = " " + cspell_word
            cspell_line_length += len(new_word)
            cspell_lines += new_word

    return cspell_lines


g_cspell_ignore = generate_cspell_ignore()


def find_sight_root_dir() -> str:
    """Find the SIGHT root directory"""
    sight_root_dir = g_root_dir
    while not os.path.exists(os.path.join(sight_root_dir, ".sight")):
        sight_root_dir = os.path.dirname(sight_root_dir)
    return sight_root_dir


g_script_path = __file__.replace(find_sight_root_dir(), "").replace("\\", "/")


def generate_preamble(includes: str, header: bool = True, namespace: str = None) -> str:
    """Generate the preamble for the generated files"""
    preamble = f"// This file was automatically generated by the '{g_script_path}' script. DO NOT EDIT !\n"
    preamble += f"// Used arguments: '{dicom_parser.g_arguments}'\n\n"

    if header:
        preamble += "#pragma once\n\n"

    if len(includes) > 0:
        preamble += f"{includes}\n\n"

    preamble += f"{g_cspell_ignore}\n\n"

    preamble += "// NOLINTBEGIN\n\n"

    if namespace is None:
        preamble += "namespace sight::data::dicom::ut\n{\n\n"
    else:
        preamble += f"namespace sight::data::dicom::{namespace}\n{{\n\n"

    return preamble


def generate_footer(namespace: str = None) -> str:
    """Generate the footer for the generated files"""
    if namespace is None:
        string = "} // namespace sight::data::dicom::ut\n\n"
    else:
        string = f"}} // namespace sight::data::dicom::{namespace}\n\n"

    string += "// NOLINTEND\n"

    return string


def tag_to_uint16(tag: dicom_parser.Tag) -> dicom_parser.Tag:
    """Convert a tag to a uint16_t hex string"""
    return dicom_parser.Tag(f"0x{tag.group.replace('x', 'F')}", f"0x{tag.element.replace('x', 'F')}")


def write_attribute_hpp() -> None:
    """Write the attribute.hpp file."""
    with open(os.path.join(g_root_dir, g_attribute_class_name + g_header_extension), mode='w',
              encoding='utf-8') as attribute_hpp:
        # Preamble
        attribute_hpp.write(
            generate_preamble(
                includes=(
                    "#include \"data/config.hpp\"\n\n"
                    "#include <cstdint>\n"
                    "#include <string_view>"
                ),
                header=True,
                namespace=g_attribute_class_name.lower()
            )
        )

        # enum Keyword
        attribute_hpp.write(
            f"enum class Keyword : std::uint16_t\n"
            "{\n"
            f"    {dicom_parser.g_invalid} = 0,\n"
        )

        for tag in sorted(dicom_parser.g_filtered_attributes):
            attribute = dicom_parser.g_filtered_attributes[tag]
            attribute_hpp.write(f"    {attribute.keyword},\n")

        attribute_hpp.write(
            "    _SIZE\n"
            "};\n\n"
        )

        # enum VR
        attribute_hpp.write(
            "enum class VR : std::uint8_t\n"
            "{\n"
            f"    {dicom_parser.g_invalid} = 0,\n"
        )

        for vr in sorted(dicom_parser.g_vrs):
            attribute_hpp.write(f"    {vr},\n")

        attribute_hpp.write(
            "    _SIZE\n"
            "};\n\n"
        )

        # enum VM
        attribute_hpp.write(
            "enum class VM : std::uint8_t\n"
            "{\n"
            f"    {dicom_parser.g_invalid} = 0,\n"
        )

        for vm in sorted(dicom_parser.g_vms):
            attribute_hpp.write(f"    {vm},\n")

        attribute_hpp.write(
            "    _SIZE\n"
            "};\n\n"
        )

        # IAttribute class
        attribute_hpp.write(
            f"struct DATA_CLASS_API I{g_attribute_class_name}\n"
            "{\n"
            f"    constexpr I{g_attribute_class_name}(\n"
            f"        const Keyword& keyword,\n"
            "        const std::uint16_t& group,\n"
            "        const std::uint16_t& element,\n"
            "        const std::string_view& name,\n"
            "        const VR& vr,\n"
            "        const VM& vm\n"
            "    ) noexcept :\n"
            "        m_keyword(keyword),\n"
            "        m_group(group),\n"
            "        m_element(element),\n"
            "        m_name(name),\n"
            "        m_vr(vr),\n"
            "        m_vm(vm)\n"
            "    {\n"
            "    }\n\n"
            f"    constexpr operator Keyword() const noexcept\n"
            "    {\n"
            "        return m_keyword;\n"
            "    }\n\n"
            f"    constexpr auto operator==(const I{g_attribute_class_name}& other) const noexcept\n"
            "    {\n"
            "       return m_keyword == other.m_keyword;\n"
            "    }\n\n"
            f"    const Keyword& m_keyword;\n"
            "    const std::uint16_t& m_group;\n"
            "    const std::uint16_t& m_element;\n"
            "    const std::string_view& m_name;\n"
            "    const VR& m_vr;\n"
            "    const VM& m_vm;\n"
            "};\n\n"
        )

        # Attribute class
        attribute_hpp.write(
            f"template<Keyword>\n"
            f"struct DATA_CLASS_API {g_attribute_class_name} final : public I{g_attribute_class_name}\n"
            "{\n"
            f"    constexpr {g_attribute_class_name}() noexcept :\n"
            f"        I{g_attribute_class_name}(s_keyword, s_group, s_element, s_name, s_vr, s_vm)\n"
            "    {\n"
            "    }\n\n"
            f"    SIGHT_DATA_API static const Keyword s_keyword;\n"
            "    SIGHT_DATA_API static const std::uint16_t s_group;\n"
            "    SIGHT_DATA_API static const std::uint16_t s_element;\n"
            "    SIGHT_DATA_API static const std::string_view s_name;\n"
            "    SIGHT_DATA_API static const VR s_vr;\n"
            "    SIGHT_DATA_API static const VM s_vm;\n"
            "};\n\n"
        )

        # attribute(attribute::Keyword) and attribute(group, element) methods
        attribute_hpp.write(
            f"/// @return a static const reference to an {g_attribute_class_name}\n"
            "/// @{\n"
            f"SIGHT_DATA_API const I{g_attribute_class_name}& get(Keyword keyword);\n"
            f"SIGHT_DATA_API const I{g_attribute_class_name}& get(std::uint16_t group, std::uint16_t element);\n"
            "/// @}\n\n"
        )

        attribute_hpp.write(generate_footer(namespace=g_attribute_class_name.lower()))


def write_attribute_cpp() -> None:
    """Write the Attribute.cpp file."""
    with open(os.path.join(g_root_dir, g_attribute_class_name + g_source_extension), mode='w',
              encoding='utf-8') as attribute_cpp:
        attribute_cpp.write(
            generate_preamble(
                includes=(
                    f"#include \"data/dicom/{g_attribute_class_name + g_header_extension}\"\n"
                    "#include <core/exceptionmacros.hpp>"
                ),
                header=False,
                namespace=g_attribute_class_name.lower()
            )
        )

        # get<Keyword::xxx>()
        attribute_cpp.write(
            f"template<Keyword keyword>\n"
            f"inline static const {g_attribute_class_name}<keyword>& get()\n"
            "{\n"
            f"    static constexpr {g_attribute_class_name}<keyword> instance;\n"
            "    return instance;\n"
            "}\n"
        )

        attribute_cpp.write(
            "\n//------------------------------------------------------------------------------\n\n"
        )

        # Attribute<Keyword::xxx>::s_yyy = zzz
        for tag in sorted(dicom_parser.g_filtered_attributes):
            attribute = dicom_parser.g_filtered_attributes[tag]
            uint16_tag = tag_to_uint16(tag)

            attribute_cpp.write(
                f"template<> SIGHT_DATA_API const Keyword {g_attribute_class_name}<Keyword::{attribute.keyword}>::s_keyword = Keyword::{attribute.keyword};\n"
                f"template<> SIGHT_DATA_API const std::uint16_t {g_attribute_class_name}<Keyword::{attribute.keyword}>::s_group = {uint16_tag.group};\n"
                f"template<> SIGHT_DATA_API const std::uint16_t {g_attribute_class_name}<Keyword::{attribute.keyword}>::s_element = {uint16_tag.element};\n"
                f"template<> SIGHT_DATA_API const std::string_view {g_attribute_class_name}<Keyword::{attribute.keyword}>::s_name = \"{attribute.name}\";\n"
                f"template<> SIGHT_DATA_API const VR {g_attribute_class_name}<Keyword::{attribute.keyword}>::s_vr = VR::{attribute.vr};\n"
                f"template<> SIGHT_DATA_API const VM {g_attribute_class_name}<Keyword::{attribute.keyword}>::s_vm = VM::{attribute.vm};\n"
            )

        attribute_cpp.write(
            "\n//------------------------------------------------------------------------------\n\n"
        )

        # get(Keyword)
        attribute_cpp.write(
            f"const I{g_attribute_class_name}& get(Keyword keyword)\n"
            "{\n"
            "    switch(keyword)\n"
            "    {\n"
        )

        for tag in sorted(dicom_parser.g_filtered_attributes):
            attribute = dicom_parser.g_filtered_attributes[tag]
            attribute_cpp.write(
                f"        case Keyword::{attribute.keyword}: return get<Keyword::{attribute.keyword}>();\n"
            )

        attribute_cpp.write(
            "        default:\n"
            f"            SIGHT_THROW(\"Unknown {g_attribute_class_name} Keyword: '\" << std::uint16_t(keyword) << \"'.\");\n"
            "    }\n"
        )

        attribute_cpp.write(
            "}\n\n"
            "//------------------------------------------------------------------------------\n\n"
        )

        # get(group, element)
        attribute_cpp.write(
            f"const I{g_attribute_class_name}& get(std::uint16_t group, std::uint16_t element)\n"
            "{\n"
            "    // Special case for groups in the range 0x6000-0x60FF for attribute (60xx, xxxx)\n"
            "    if(group > 0x6000 && group <= 0x60FF)\n"
            "    {\n"
            "        group = 0x6000;\n"
            "    }\n\n"
            "    switch(group)\n"
            "    {\n"
        )

        # Order by group and then by element
        attribute_by_group_by_element = dict[str, dict[str, dicom_parser.Attribute]]()
        for tag in sorted(dicom_parser.g_filtered_attributes):
            attribute = dicom_parser.g_filtered_attributes[tag]
            uint16_tag = tag_to_uint16(tag)

            if uint16_tag.group not in attribute_by_group_by_element:
                attribute_by_group_by_element[uint16_tag.group] = dict[str, dicom_parser.Attribute]()

            attribute_by_group_by_element[uint16_tag.group][uint16_tag.element] = attribute

        # For each group
        for group in sorted(attribute_by_group_by_element):
            elements = attribute_by_group_by_element[group]

            attribute_cpp.write(
                f"        case {group}:\n"
                "        {\n"
                "            switch(element)\n"
                "            {\n"
            )

            for element in sorted(elements):
                attribute = elements[element]
                attribute_cpp.write(
                    f"            case {element}: return get<Keyword::{attribute.keyword}>();\n"
                )

            attribute_cpp.write(
                "            default:\n"
                "                SIGHT_THROW(\"Unknown element: '\" << element << \"'.\");\n"
                "            }\n"
                "        }\n\n"
            )

        attribute_cpp.write(
            "        default:\n"
            "            SIGHT_THROW(\"Unknown group: '\" << group << \"'.\");\n"
            "    }\n"
        )

        attribute_cpp.write(
            "}\n\n"
            "//------------------------------------------------------------------------------\n\n"
        )

        attribute_cpp.write(generate_footer(namespace=g_attribute_class_name.lower()))


def write_sop_hpp() -> None:
    """Write the sop.hpp file."""
    with open(os.path.join(g_root_dir, g_sop_class_name + g_header_extension), mode='w', encoding='utf-8') as sop_hpp:
        # Preamble
        sop_hpp.write(
            generate_preamble(
                includes=(
                    "#include \"data/config.hpp\"\n"
                    "#include <cstdint>\n"
                    "#include <string_view>"
                ),
                header=True,
                namespace=g_sop_class_name.lower()
            )
        )

        # enum Keyword
        sop_hpp.write(
            f"enum class Keyword : std::uint8_t\n"
            "{\n"
            f"    {dicom_parser.g_invalid} = 0,\n"
        )

        for uid in sorted(dicom_parser.g_filtered_sops):
            sop_hpp.write(f"    {uid.keyword},\n")

        sop_hpp.write(
            "    _SIZE\n"
            "};\n\n"
        )

        # ISop
        sop_hpp.write(
            f"struct DATA_CLASS_API I{g_sop_class_name}\n"
            "{\n"
            f"    constexpr I{g_sop_class_name}(\n"
            f"        const Keyword& keyword,\n"
            "        const std::string_view& name,\n"
            "        const std::string_view& iud,\n"
            "        const std::string_view& iod\n"
            "    ) noexcept :\n"
            "        m_keyword(keyword),\n"
            "        m_name(name),\n"
            "        m_uid(iud),\n"
            "        m_iod(iod)\n"
            "    {\n"
            "    }\n\n"
            f"    constexpr operator Keyword() const noexcept\n"
            "    {\n"
            "        return m_keyword;\n"
            "    }\n\n"
            f"    constexpr auto operator==(const I{g_sop_class_name}& other) const noexcept\n"
            "    {\n"
            "       return m_keyword == other.m_keyword;\n"
            "    };\n\n"
            f"    const Keyword& m_keyword;\n"
            "    const std::string_view& m_name;\n"
            "    const std::string_view& m_uid;\n"
            "    const std::string_view& m_iod;\n"
            "};\n\n"
        )

        # Sop
        sop_hpp.write(
            f"template<Keyword>\n"
            f"struct DATA_CLASS_API {g_sop_class_name} final : public I{g_sop_class_name}\n"
            "{\n"
            f"    constexpr {g_sop_class_name}() noexcept :\n"
            f"        I{g_sop_class_name}(s_keyword, s_name, s_uid, s_iod)\n"
            "    {\n"
            "    }\n\n"
            f"    SIGHT_DATA_API static const Keyword s_keyword;\n"
            "    SIGHT_DATA_API static const std::string_view s_name;\n"
            "    SIGHT_DATA_API static const std::string_view s_uid;\n"
            "    SIGHT_DATA_API static const std::string_view s_iod;\n"
            "};\n\n"
        )

        # get(Keyword) and keyword(uid)
        sop_hpp.write(
            f"/// @return a static const reference to an {g_sop_class_name}\n"
            "/// @{\n"
            f"SIGHT_DATA_API const I{g_sop_class_name}& get(Keyword keyword);\n"
            f"SIGHT_DATA_API const I{g_sop_class_name}& get(const std::string_view& uid);\n"
            "/// @}\n\n"
            "/// convert a uid directly to a keyword\n"
            "SIGHT_DATA_API const Keyword& keyword(const std::string_view& uid) noexcept;\n\n"
        )

        sop_hpp.write(generate_footer(namespace=g_sop_class_name.lower()))


def write_sop_cpp() -> None:
    """Write the Sop.cpp file."""
    with open(os.path.join(g_root_dir, g_sop_class_name + g_source_extension), mode='w', encoding='utf-8') as sop_cpp:
        sop_cpp.write(
            generate_preamble(
                includes=(
                    f"#include \"data/dicom/{g_sop_class_name + g_header_extension}\"\n"
                    "#include <core/exceptionmacros.hpp>\n"
                    "#include <unordered_map>"
                ),
                header=False,
                namespace=g_sop_class_name.lower()
            )
        )

        # get<Keyword::xxx>()
        sop_cpp.write(
            f"template<Keyword keyword>\n"
            f"inline static const {g_sop_class_name}<keyword>& get()\n"
            "{\n"
            f"    static constexpr {g_sop_class_name}<keyword> instance;\n"
            "    return instance;\n"
            "}\n"
        )

        sop_cpp.write(
            "\n//------------------------------------------------------------------------------\n\n"
        )

        # Sop<Keyword::xxx>::s_yyy = zzz
        for uid in sorted(dicom_parser.g_filtered_sops):
            sop = dicom_parser.g_filtered_sops[uid]
            iod = dicom_parser.g_filtered_iods[sop.iod.keyword]
            sop_cpp.write(
                f"template<> SIGHT_DATA_API const Keyword {g_sop_class_name}<Keyword::{uid.keyword}>::s_keyword = Keyword::{uid.keyword};\n"
                f"template<> SIGHT_DATA_API const std::string_view {g_sop_class_name}<Keyword::{uid.keyword}>::s_name = \"{uid.name}\";\n"
                f"template<> SIGHT_DATA_API const std::string_view {g_sop_class_name}<Keyword::{uid.keyword}>::s_uid = \"{uid.value}\";\n"
                f"template<> SIGHT_DATA_API const std::string_view {g_sop_class_name}<Keyword::{uid.keyword}>::s_iod = \"{iod.name}\";\n"
            )

        sop_cpp.write(
            "\n//------------------------------------------------------------------------------\n\n"
        )

        # get(Keyword)
        sop_cpp.write(
            f"const I{g_sop_class_name}& get(Keyword keyword)\n"
            "{\n"
            "    switch(keyword)\n"
            "    {\n"
        )

        for uid in sorted(dicom_parser.g_filtered_sops):
            sop_cpp.write(
                f"        case Keyword::{uid.keyword}: return get<Keyword::{uid.keyword}>();\n"
            )

        sop_cpp.write(
            "        default:\n"
            f"            SIGHT_THROW(\"Unknown {g_sop_class_name} Keyword: '\" << std::uint8_t(keyword) << \"'.\");\n"
            "    }\n"
        )

        sop_cpp.write(
            "}\n\n"
            "//------------------------------------------------------------------------------\n\n"
        )

        # get(uid)
        sop_cpp.write(
            f"const I{g_sop_class_name}& get(const std::string_view& uid)\n"
            "{\n"
            f"    static const std::unordered_map<std::string_view, const I{g_sop_class_name}&> {g_sop_class_name.upper()}_MAP {{\n"
        )

        for uid in sorted(dicom_parser.g_filtered_sops):
            sop_cpp.write(
                f"        {{{g_sop_class_name}<Keyword::{uid.keyword}>::s_uid, get<Keyword::{uid.keyword}>()}},\n"
            )

        sop_cpp.write(
            "    };\n\n"
            f"    return {g_sop_class_name.upper()}_MAP.at(uid);\n"
            "}\n\n"
            "//------------------------------------------------------------------------------\n\n"
        )

        sop_cpp.write(
            f"const Keyword& keyword(const std::string_view& uid) noexcept\n"
            "{\n"
            "    try\n"
            "    {\n"
            f"        return get(uid).m_keyword;\n"
            "    }\n"
            "    catch(...)\n"
            "    {\n"
            "        static const auto invalid = Keyword::INVALID;\n"
            "        return invalid;\n"
            "    }\n"
            "}\n\n"
            "//------------------------------------------------------------------------------\n\n"
        )

        sop_cpp.write(generate_footer(namespace=g_sop_class_name.lower()))


def write_attribute_test_hpp() -> None:
    """Write the AttributeTest.hpp file."""
    with open(
            os.path.join(g_test_dir, g_attribute_class_name + g_test + g_header_extension),
            mode='w',
            encoding='utf-8'
    ) as attribute_test_hpp:
        attribute_test_hpp.write(
            generate_preamble(
                includes=(
                    "#include <cppunit/extensions/HelperMacros.h>"
                ),
                header=True
            )
        )

        # Test suite / methods
        attribute_test_hpp.write(
            f"class {g_attribute_class_name}{g_test} : public CPPUNIT_NS::TestFixture\n"
            "{\n"
            f"    CPPUNIT_TEST_SUITE({g_attribute_class_name}{g_test});\n"
            f"    CPPUNIT_TEST({g_attribute_class_name.lower()}{g_test});\n"
            "    CPPUNIT_TEST_SUITE_END();\n\n"
            "public:\n"
            "    void setUp() override;\n"
            "    void tearDown() override;\n\n"
            f"    static void {g_attribute_class_name.lower()}{g_test}();\n"
            "};\n\n"
        )

        attribute_test_hpp.write(generate_footer())


def write_attribute_test_cpp() -> None:
    """Write the AttributeTest.cpp file."""
    with open(
            os.path.join(g_test_dir, g_attribute_class_name + g_test + g_source_extension),
            mode='w',
            encoding='utf-8'
    ) as attribute_test_cpp:
        attribute_test_cpp.write(
            generate_preamble(
                includes=(
                    f"#include \"{g_attribute_class_name}{g_test}{g_header_extension}\"\n\n"
                    f"#include <data/dicom/{g_attribute_class_name}{g_header_extension}>\n\n"
                    f"CPPUNIT_TEST_SUITE_REGISTRATION(sight::data::dicom::ut::{g_attribute_class_name}{g_test});"
                ),
                header=False
            )
        )

        attribute_test_cpp.write(
            f"void {g_attribute_class_name}{g_test}::setUp()\n"
            "{\n"
            "}\n\n"
            "//------------------------------------------------------------------------------\n\n"
            f"void {g_attribute_class_name}{g_test}::tearDown()\n"
            "{\n"
            "}\n\n"
            "//------------------------------------------------------------------------------\n\n"
        )

        attribute_test_cpp.write(
            f"void {g_attribute_class_name}{g_test}::{g_attribute_class_name.lower()}{g_test}()\n"
            "{\n"
        )

        for tag in sorted(dicom_parser.g_filtered_attributes):
            attribute = dicom_parser.g_filtered_attributes[tag]
            uint16_tag = tag_to_uint16(tag)

            attribute_test_cpp.write(
                f"    CPPUNIT_ASSERT_EQUAL({g_attribute_class_name.lower()}::{g_attribute_class_name}<{g_attribute_class_name.lower()}::Keyword::{attribute.keyword}>::s_keyword, {g_attribute_class_name.lower()}::get({uint16_tag.group}, {uint16_tag.element}).m_keyword);\n"
                f"    CPPUNIT_ASSERT_EQUAL({g_attribute_class_name.lower()}::{g_attribute_class_name}<{g_attribute_class_name.lower()}::Keyword::{attribute.keyword}>::s_keyword, {g_attribute_class_name.lower()}::get({g_attribute_class_name.lower()}::Keyword::{attribute.keyword}).m_keyword);\n"
            )

        attribute_test_cpp.write(
            "}\n\n"
            "//------------------------------------------------------------------------------\n\n"
        )

        attribute_test_cpp.write(generate_footer())


def write_sop_test_hpp() -> None:
    """Write the SopTest.hpp file."""
    with open(
            os.path.join(g_test_dir, g_sop_class_name + g_test + g_header_extension),
            mode='w',
            encoding='utf-8'
    ) as sop_test_hpp:
        sop_test_hpp.write(
            generate_preamble(
                includes=(
                    "#include <cppunit/extensions/HelperMacros.h>"
                ),
                header=True
            )
        )

        # Test suite / methods
        sop_test_hpp.write(
            f"class {g_sop_class_name}{g_test} : public CPPUNIT_NS::TestFixture\n"
            "{\n"
            f"    CPPUNIT_TEST_SUITE({g_sop_class_name}{g_test});\n"
            f"    CPPUNIT_TEST({g_sop_class_name.lower()}{g_test});\n"
            "    CPPUNIT_TEST_SUITE_END();\n\n"
            "public:\n"
            "    void setUp() override;\n"
            "    void tearDown() override;\n\n"
            f"    static void {g_sop_class_name.lower()}{g_test}();\n"
            "};\n\n"
        )

        sop_test_hpp.write(generate_footer())


def write_sop_test_cpp() -> None:
    """Write the AttributeTest.cpp file."""
    with open(
            os.path.join(g_test_dir, g_sop_class_name + g_test + g_source_extension),
            mode='w',
            encoding='utf-8'
    ) as sop_test_cpp:
        sop_test_cpp.write(
            generate_preamble(
                includes=(
                    f"#include \"{g_sop_class_name}{g_test}{g_header_extension}\"\n\n"
                    f"#include <data/dicom/{g_sop_class_name}{g_header_extension}>\n\n"
                    f"CPPUNIT_TEST_SUITE_REGISTRATION(sight::data::dicom::ut::{g_sop_class_name}{g_test});"
                ),
                header=False
            )
        )

        sop_test_cpp.write(
            f"void {g_sop_class_name}{g_test}::setUp()\n"
            "{\n"
            "}\n\n"
            "//------------------------------------------------------------------------------\n\n"
            f"void {g_sop_class_name}{g_test}::tearDown()\n"
            "{\n"
            "}\n\n"
            "//------------------------------------------------------------------------------\n\n"
        )

        sop_test_cpp.write(
            f"void {g_sop_class_name}{g_test}::{g_sop_class_name.lower()}{g_test}()\n"
            "{\n"
        )

        for uid in sorted(dicom_parser.g_filtered_sops):
            sop_test_cpp.write(
                f"    CPPUNIT_ASSERT_EQUAL({g_sop_class_name.lower()}::{g_sop_class_name}<{g_sop_class_name.lower()}::Keyword::{uid.keyword}>::s_keyword, {g_sop_class_name.lower()}::get({g_sop_class_name.lower()}::Keyword::{uid.keyword}).m_keyword);\n"
                f"    CPPUNIT_ASSERT_EQUAL({g_sop_class_name.lower()}::{g_sop_class_name}<{g_sop_class_name.lower()}::Keyword::{uid.keyword}>::s_keyword, {g_sop_class_name.lower()}::get(\"{uid.value}\").m_keyword);\n"
            )

        sop_test_cpp.write(
            "}\n\n"
            "//------------------------------------------------------------------------------\n\n"
        )

        sop_test_cpp.write(generate_footer())


def main():
    """Main function"""

    result = dicom_parser.main()

    # Attribute
    write_attribute_hpp()
    write_attribute_cpp()

    # Attribute tests
    write_attribute_test_hpp()
    write_attribute_test_cpp()

    # Sop
    write_sop_hpp()
    write_sop_cpp()

    # Sop tests
    write_sop_test_hpp()
    write_sop_test_cpp()

    return result


if __name__ == "__main__":
    sys.exit(main())
