import os
from datetime import datetime

import commentjson
from halo import Halo
from PIL import Image

from anvil.api.blockbench import _Blockbench
from anvil.api.types import Identifier
from anvil.lib.config import _AnvilConfig
from anvil.lib.lib import (CopyFiles, CreateDirectory, File, FileExists,
                           RemoveDirectory, process_subcommand,
                           validate_namespace_project_name, zipit)
from anvil.api.materials import MaterialsObject
from anvil.lib.reports import ReportType
from anvil.lib.schemas import AddonObject, JsonSchemes
from anvil.lib.sounds import (EntitySoundEvent, MusicCategory, MusicDefinition,
                              SoundCategory, SoundDefinition, SoundEvent)
from anvil.lib.textures import (BlocksJSONObject, ItemTexturesObject,
                                TerrainTexturesObject)

from ..__version__ import __version__


class _AnvilDefinitions:
    _materials_object: MaterialsObject = None
    _sound_definition_object: SoundDefinition = None
    _music_definition_object: MusicDefinition = None
    _sound_event_object: SoundEvent = None
    _item_textures_object: ItemTexturesObject = None
    _terrain_texture_object: TerrainTexturesObject = None

    _scores: dict[str, int] = {}
    _language: dict[str, str] = {}
    _tags: set[str] = ()
    _raw_text: int = 0

    _blocks_object: BlocksJSONObject = None

    def __init__(self, config: _AnvilConfig):
        self.config = config

    def add_material(self, material_name: str, base_material: str = None):
        """Adds a material to the materials.json file.

        Args:
            material_name (str): The name of the material.
            base_material (str, optional): The name of the base material. Defaults to None.

        """
        if self._materials_object is None:
            self._materials_object = MaterialsObject()

        return self._materials_object.add_material(material_name, base_material)

    def register_sound_definition(
        self,
        sound_reference: str,
        category: SoundCategory,
        use_legacy_max_distance: bool = False,
        max_distance: int = 0,
        min_distance: int = 9999,
    ):
        """Adds a sound to the sound definition.

        Args:
            sound_reference (str): The name of the sound definition.
            category (SoundCategory): The category of the sound.
            use_legacy_max_distance (bool, optional): Whether to use legacy max distance. Defaults to False.
            max_distance (int, optional): The max distance of the sound. Defaults to 0.
            min_distance (int, optional): The min distance of the sound. Defaults to 9999.
        """
        if self._sound_definition_object is None:
            self._sound_definition_object = SoundDefinition()
        return self._sound_definition_object.sound_reference(
            sound_reference,
            category,
            use_legacy_max_distance,
            max_distance,
            min_distance,
        )

    def register_entity_sound_event(
        self,
        entity_identifier: Identifier,
        sound_identifier,
        sound_event: EntitySoundEvent,
        volume: float = 1.0,
        pitch: tuple[float, float] = (0.8, 1.2),
    ):
        if self._sound_event_object is None:
            self._sound_event_object = SoundEvent()
        return self._sound_event_object.add_entity_event(entity_identifier, sound_identifier, sound_event, volume, pitch)

    def register_block_sound_event(self):
        """# TO IMPLEMENT
        Registers a block sound event."""
        pass

    def register_item_textures(self, item_name: str, directory: str, *item_sprites: str):
        if self._item_textures_object is None:
            self._item_textures_object = ItemTexturesObject()
        return self._item_textures_object.add_item(item_name, directory, *item_sprites)

    def register_music(self, music_category: MusicCategory, min_delay: int = 60, max_delay: int = 180):
        """Adds a music to the music definition.

        Args:
            music_category (MusicCategory): The category of the music.
            min_delay (int, optional): The min delay of the music. Defaults to 60.
            max_delay (int, optional): The max delay of the music. Defaults to 180.

        """
        if self._music_definition_object is None:
            self._music_definition_object = MusicDefinition()
        if self._sound_definition_object is None:
            self._sound_definition_object = SoundDefinition()
        self._music_definition_object.music_definition(music_category, min_delay, max_delay)
        return self._sound_definition_object.sound_reference(f"music.{music_category}", SoundCategory.Music)

    def register_scores(self, **score_id_value: dict[str, int]):
        """
        Adds the provided scores to the setup functions as well as setting the global score values.
        Score objective must be 16 characters or less.

        Parameters:
        ---------
        `score_id_value` : `kwargs`
            The score and it's initial value.

        Examples:
        ---------
        >>> ANVIL.score(player_id=0,test=4)
        >>> ANVIL.score(**{'level':5,'type':1})
        """
        for score_id, score_value in score_id_value.items():
            if len(score_id) > 16:
                self.config.Logger.score_error(score_id)

            start = f"{self.config.NAMESPACE}."
            if not score_id.startswith(start) and self.config._TARGET == "addon":
                self.config.Logger.invalid_score_format(start, score_id)

            if not score_id in self._scores.keys():
                self._scores[score_id] = score_value

    def register_lang(self, key: str, value: str):
        """Adds a localized string to en_US. Translatable."""
        if key not in self._language:
            self._language.update({key: value})

    def register_tag(self, *tags: str):
        """Registers tags.

        Args:
            tags (str): The tags to register.
        """
        for tag in tags:

            start = f"{self.config.NAMESPACE}."
            if not tag.startswith(start) and self.config._TARGET == "addon":
                self.config.Logger.invalid_tag_format(start, tag)

            if not tag in self._tags:
                self._tags.add(tags)

    def register_block(self, block_identifier: Identifier, block_data: dict):
        """Registers blocks.

        Args:
            blocks (dict): The blocks to register.
        """
        if self._blocks_object is None:
            self._blocks_object = BlocksJSONObject()

        self._blocks_object.add_block(block_identifier, block_data)

    def register_terrain_texture(self, texture_name: str, texture_path: str, *block_textures):
        """Registers a terrain texture.

        Args:
            texture_name (str): The name of the texture.
            texture_path (str): The path to the texture.
        """
        if self._terrain_texture_object is None:
            self._terrain_texture_object = TerrainTexturesObject()
        self._terrain_texture_object.add_block(texture_name, texture_path, *block_textures)

    @property
    def get_new_score(self):
        id = f"{self.config.NAMESPACE}.{len(self._scores)}"
        self.register_scores(**{id: 0})
        return id

    @property
    def get_new_lang(self):
        id = f"raw_text_{self._raw_text}"
        self._raw_text += 1
        return id

    def _export_manifest(self):
        release_list = [int(i) for i in self.config._RELEASE.split(".")]

        File("manifest.json", JsonSchemes.manifest_rp(version=release_list), self.config.RP_PATH, "w")
        File("manifest.json", JsonSchemes.manifest_bp(version=release_list), self.config.BP_PATH, "w")
        File("manifest.json", JsonSchemes.manifest_world(version=release_list), "", "w")

        File("world_resource_packs.json", JsonSchemes.world_packs(release_list, self.config._RP_UUID), "", "w")
        File("world_behavior_packs.json", JsonSchemes.world_packs(release_list, self.config._BP_UUID), "", "w")

    def _export_language(self):
        default_langs = JsonSchemes.pack_name_lang(self.config.DISPLAY_NAME, self.config.RESOURCE_DESCRIPTION)
        langs = default_langs.copy()
        langs.extend([f"{k}={v}" for k, v in self._language.items()])
        langs.sort(reverse=True)

        File("languages.json", ["en_US"], os.path.join(self.config.BP_PATH, "texts"), "w")
        File("languages.json", ["en_US"], os.path.join(self.config.RP_PATH, "texts"), "w")


        File("en_US.lang", "\n".join(langs), os.path.join(self.config.RP_PATH, "texts"), "w")
        File(
            "en_US.lang",
            "\n".join(JsonSchemes.pack_name_lang(self.config.DISPLAY_NAME, self.config.BEHAVIOR_DESCRIPTION)),
            os.path.join(self.config.BP_PATH, "texts"),
            "w",
        )

        if self.config._TARGET == "world":
            File("languages.json", ["en_US"], "texts", "w")
            File(
                "en_US.lang",
                "\n".join(JsonSchemes.pack_name_lang(self.config.DISPLAY_NAME, self.config.PROJECT_DESCRIPTION)),
                "texts",
                "w",
            )

    def _export_scripts(self):
        File(
            "package.json",
            JsonSchemes.packagejson(
                self.config.PROJECT_NAME, self.config._RELEASE, self.config.PROJECT_DESCRIPTION, self.config.COMPANY
            ),
            "",
            "w",
            True,
        )

    def _export_helper_functions(self):
        from anvil.api.commands import (Scoreboard, ScriptEvent, Tag, Target,
                                        Tellraw)
        from anvil.api.features import Function, _Tick

        self._setup_function = Function("setup")
        for f in Function._setup:
            self._setup_function.add(f.execute)
        self._setup_function.queue()

        _Tick().add_function(*Function._ticking).queue

        self._setup_scores = Function("setup_scores")
        self._remove_scores = Function("remove_scores")
        self._remove_tags = Function("remove_tags")

        Function("version").add(
            Tellraw(Target.A).text.text("[Anvil Debug Message]"),
            Tellraw(Target.A).text.text("This message contains information about the creating of this pack."),
            Tellraw(Target.A).text.text(f"Last compiled on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"),
            Tellraw(Target.A).text.text(f"Minecraft Version: {self.config._VANILLA_VERSION}"),
            Tellraw(Target.A).text.text(f"Pack Version: {self.config._RELEASE}"),
        ).queue()

        for score, value in self._scores.items():
            self._setup_scores.add(
                Scoreboard().objective.add(score, score.title()),
                Scoreboard().players.set(self.config.PROJECT_NAME, score, value),
            )
            self._remove_scores.add(Scoreboard().objective.remove(score))

        for tag in self._tags:
            self._remove_tags.add(Tag(Target.E).remove(tag))

        self._setup_scores.queue()
        self._remove_scores.queue()
        self._remove_tags.queue()

        self._setup_function.add(
            self._remove_tags.execute,
            self._remove_scores.execute,
            self._setup_scores.execute,
            ScriptEvent(f"{self.config.NAMESPACE}:setup"),
        )
        self._setup_function.queue()

    @property
    def queue(self):
        if not self._materials_object == None:
            self._materials_object.queue
        if not self._sound_definition_object == None:
            self._sound_definition_object.queue
        if not self._music_definition_object == None:
            self._music_definition_object.queue
        if not self._sound_event_object == None:
            self._sound_event_object.queue
        if not self._item_textures_object == None:
            self._item_textures_object.queue
        if not self._terrain_texture_object == None:
            self._terrain_texture_object.queue
        if not self._blocks_object == None:
            self._blocks_object.queue

        self._export_manifest()
        self._export_language()
        self._export_helper_functions()

        if any([self.config._SCRIPT_API, self.config._SCRIPT_UI]):
            self._export_scripts()


class _AnvilCore:
    _instance = None
    _objects_list: list[AddonObject] = []
    _definitions: _AnvilDefinitions
    _config: _AnvilConfig

    def __init__(self, config: _AnvilConfig):
        """Initializes an Anvil instance.

        Args:
            config (_Config): The config of the Anvil instance.
            logger (Logger): The logger of the Anvil instance.
        """
        if _Anvil._instance is None:
            _Anvil._instance = self
        else:
            raise Exception("Anvil instance already exists.")

        super().__init__()
        self._compiled = False
        self._config = config
        validate_namespace_project_name(config.NAMESPACE, config.PROJECT_NAME)
        self._definitions = _AnvilDefinitions(config)

    @property
    def definitions(self):
        return self._definitions

    @property
    def config(self):
        return self._config

    def require_config(self, *options):
        """Checks if the config has the required options.

        Args:
            options (str): The options to check.
        """
        if not self.config.Config.has_section(self.config.PROJECT_NAME):
            self.config.Logger.project_missing_config()
            self.config.Logger.config_option_changeable(*options)
            self.config.Config.add_section(self.config.PROJECT_NAME)

        for option in options:
            if not self.config.Config.has_option(self.config.PROJECT_NAME, option):
                self.config.Config.add_option(self.config.PROJECT_NAME, str(option), input(f"Enter {option}: "))
                self.config.Logger.config_added_option(self.config.PROJECT_NAME, option)

    @Halo(text="Translating", spinner="dots")
    def translate(self) -> None:
        """
        Translates en_US to all supported Minecraft languages.
        This is a time consuming function, it will be executed with anvil.package(), so it's better to avoid it unless you really want to use it.
        """

        from deep_translator import GoogleTranslator

        def _to_lang(translator: GoogleTranslator, langs: dict):
            lang = JsonSchemes.pack_name_lang(self.config.DISPLAY_NAME, self.config.PROJECT_DESCRIPTION)
            translated = translator.translate_batch(langs.values())
            for k, v in zip(langs.keys(), translated):
                lang.append(f"{k}={v}")
            return lang

        File("languages.json", JsonSchemes.languages(), os.path.join(self.config.BP_PATH, "texts"), "w")
        File("languages.json", JsonSchemes.languages(), os.path.join(self.config.RP_PATH, "texts"), "w")

        if self.config._TARGET == "world":
            File("languages.json", JsonSchemes.languages(), "texts", "w")

        for language in JsonSchemes.languages():
            destination_language = (
                language.replace("zh_CN", "zh-CN").replace("zh_TW", "zh-TW").replace("nb_NO", "no").split("_")[0]
            )
            if not FileExists(
                os.path.join(
                    self.config.RP_PATH,
                    "texts",
                    f"{language}.lang",
                )
            ):
                self._langs = dict(sorted(self.definitions._language.items()))
                Translator = GoogleTranslator(target=destination_language)
                File(
                    f"{language}.lang",
                    "\n".join(_to_lang(Translator, self._langs)),
                    os.path.join(self.config.RP_PATH, "texts"),
                    "w",
                )
                File(
                    f"{language}.lang",
                    "\n".join(JsonSchemes.pack_name_lang(self.config.DISPLAY_NAME, self.config.PROJECT_DESCRIPTION)),
                    os.path.join(self.config.BP_PATH, "texts"),
                    "w",
                )
                if self.config._TARGET == "world":
                    File(
                        f"{language}.lang",
                        "\n".join(JsonSchemes.pack_name_lang(self.config.DISPLAY_NAME, self.config.PROJECT_DESCRIPTION)),
                        os.path.join("texts"),
                        "w",
                    )

    @Halo(text="Compiling", spinner="dots")
    def compile(self) -> None:
        """Compiles the project."""

        self._definitions.queue
        _Blockbench._export()

        for object in self._objects_list:
            try:
                object._export()
            except Exception as e:
                self.config.Logger.object_export_error(object._name, e)

        from anvil.api.blocks import _PermutationComponents

        if _PermutationComponents._count > 10000:
            if self.config._TARGET == "addon":
                self.config.Logger.too_many_permutations(_PermutationComponents._count)
            else:
                self.config.Logger.too_many_permutations_warn(_PermutationComponents._count)

        if self.config._SCRIPT_API:
            File(
                "anvilConstants.ts",
                JsonSchemes.tsconstants(self.config.NAMESPACE, self.config.PROJECT_NAME),
                os.path.join("assets", "javascript"),
                "w",
                False,
            )

            source = os.path.join("assets", "javascript")
            target = os.path.join(self.config.BP_PATH, "scripts")

            do_ts = False
            for subdir, dirname, files in os.walk(source):
                for file in files:
                    input_file_path = os.path.join(subdir, file)
                    relative_subdir = os.path.relpath(subdir, source)
                    new_output_dir = os.path.join(target, relative_subdir)
                    if file.endswith(".js"):
                        CopyFiles(subdir, new_output_dir, file)
                    elif file.endswith(".ts"):
                        do_ts = True

            if do_ts:
                tsconfig = {}
                if FileExists("tsconfig.json"):
                    with open("tsconfig.json", "r") as file:
                        tsconfig = commentjson.load(file)
                        tsconfig["compilerOptions"]["outDir"] = target
                        tsconfig["include"] = ["assets/javascript/**/*"]
                else:
                    tsconfig = JsonSchemes.tsconfig(self.config._PASCAL_PROJECT_NAME)

                File("tsconfig.json", tsconfig, "", "w", False)
                process_subcommand("tsc", "Typescript compilation error")

        self._compiled = True

    def _queue(self, object: AddonObject):
        """Queues an object to be compiled."""
        self._objects_list.append(object)


class _Anvil(_AnvilCore):
    """A class representing an Anvil instance."""
    def __init__(self, config: _AnvilConfig):
        super().__init__(config)

    def _process_art(self, apply_overlay: bool = False, zip: bool = True):
        def resize(image: Image.Image, name: str, output: str, size, quality=95, dpi=72, overlay: Image.Image = None):
            resized = image.resize(size)

            if overlay:
                overlay = overlay.resize(size)
                resized.paste(overlay, mask=overlay.split()[3])

            resized.convert("RGB").save(
                os.path.join(output, name),
                dpi=(dpi, dpi),
                quality=quality,
            )

        pack_icon_size = (256, 256)
        store_screenshot_size = (800, 450)
        marketing_screenshot_size = (1920, 1080)

        source = os.path.join("assets", "marketing")
        output_store = os.path.join("assets", "output", "Store Art")
        output_marketing = os.path.join("assets", "output", "Marketing Art")

        if FileExists(os.path.join(source, "pack_icon.png")):
            original = Image.open(os.path.join(source, "pack_icon.png"))
            resize(original, "pack_icon.png", self.config.BP_PATH, pack_icon_size)
            resize(original, "pack_icon.png", self.config.RP_PATH, pack_icon_size)

        else:
            self.config.Logger.file_exist_warning("pack_icon.png")

        if zip:
            CreateDirectory(output_store)
            CreateDirectory(output_marketing)

            if FileExists(os.path.join(source, "keyart.png")):
                original = Image.open(os.path.join(source, "keyart.png")).convert("RGB")
                overlay = None

                if apply_overlay:
                    overlay = Image.open(os.path.join(source, "keyart_overlay.png"))

                resize(original, "world_icon.jpeg", "", store_screenshot_size, 95, 72, overlay)
                resize(
                    original, f"{self.config.PROJECT_NAME}_Thumbnail_0.jpg", output_store, store_screenshot_size, 95, 72, overlay
                )
                resize(
                    original,
                    f"{self.config.PROJECT_NAME}_MarketingKeyArt.jpg",
                    output_marketing,
                    marketing_screenshot_size,
                    100,
                    300,
                    overlay,
                )

            else:
                self.config.Logger.file_exist_warning("keyart.png")

            if FileExists(os.path.join(source, "panorama.png")):
                original = Image.open(os.path.join(source, "panorama.png"))
                scale_factor = 450 / original.size[1]

                resize(
                    original,
                    f"{self.config.PROJECT_NAME}_panorama_0.jpg",
                    output_store,
                    (round(original.size[0] * scale_factor), 450),
                    95,
                    72,
                )

            else:
                self.config.Logger.file_exist_info("panorama.png")

            for i in range(999):
                if not FileExists(os.path.join(source, f"{i}.png")):
                    if i < 5:
                        self.config.Logger.file_exist_warning(f"{i}.png")
                    break
                else:
                    original = Image.open(os.path.join(source, f"{i}.png"))
                    resize(
                        original,
                        f"{self.config.PROJECT_NAME}_screenshot_{i}.jpg",
                        output_store,
                        store_screenshot_size,
                        95,
                        72,
                    )
                    resize(
                        original,
                        f"{self.config.PROJECT_NAME}_MarketingScreenshot_{i}.jpg",
                        output_marketing,
                        marketing_screenshot_size,
                        100,
                        300,
                    )

            if FileExists(os.path.join(source, "partner_art.png")):
                original = Image.open(os.path.join(source, "partner_art.png"))

                resize(
                    original,
                    f"{self.config.PROJECT_NAME}_PartnerArt.jpg",
                    output_marketing,
                    marketing_screenshot_size,
                    100,
                    300,
                )

            else:
                self.config.Logger.file_exist_warning("partner_art.png")

            if FileExists(os.path.join(source, "pack_icon.png")):
                original = Image.open(os.path.join(source, "pack_icon.png"))
                resize(original, f"{self.config.PROJECT_NAME}_packicon_0.jpg", output_store, pack_icon_size)
            else:
                self.config.Logger.file_exist_warning("pack_icon.png")

    def package_zip(
        self,
        skip_translation: bool = False,
        apply_overlay: bool = False,
    ) -> None:
        """Packages the project into a zip file for Marketplace."""
        if not self._compiled:
            self.config.Logger.not_compiled()
        self.config.Logger.packaging_zip()

        if not skip_translation:
            self.translate()

        self._process_art(apply_overlay)

        content_structure = {
            os.path.join("assets", "output", "Store Art"): os.path.join("Store Art"),
            os.path.join("assets", "output", "Marketing Art"): os.path.join("Marketing Art"),
        }

        if self.config._TARGET == "addon":
            if len(self.config._RP_UUID) > 1:
                self.config.Logger.multiple_rp_uuids()
            if len(self.config._BP_UUID) > 1:
                self.config.Logger.multiple_bp_uuids()

            content_structure.update(
                {
                    self.config.RP_PATH: os.path.join("Content", "resource_packs", f"RP_{self.config._PASCAL_PROJECT_NAME}"),
                    self.config.BP_PATH: os.path.join("Content", "behavior_packs", f"BP_{self.config._PASCAL_PROJECT_NAME}"),
                }
            )

        else:
            content_structure.update(
                {
                    self.config.RP_PATH: os.path.join(
                        "Content", "world_template", "resource_packs", f"RP_{self.config._PASCAL_PROJECT_NAME}"
                    ),
                    self.config.BP_PATH: os.path.join(
                        "Content", "world_template", "behavior_packs", f"BP_{self.config._PASCAL_PROJECT_NAME}"
                    ),
                    "texts": os.path.join("Content", "world_template", "texts"),
                    "level.dat": os.path.join("Content", "world_template"),
                    "levelname.txt": os.path.join("Content", "world_template"),
                    "manifest.json": os.path.join("Content", "world_template"),
                    "world_icon.jpeg": os.path.join("Content", "world_template"),
                    "world_behavior_packs.json": os.path.join("Content", "world_template"),
                    "world_resource_packs.json": os.path.join("Content", "world_template"),
                }
            )
            if not self.config._RANDOM_SEED:
                content_structure.update(
                    {
                        "db": os.path.join("Content", "world_template", "db"),
                    }
                )

        zipit(
            os.path.join("assets", "output", f"{self.config.PROJECT_NAME}.zip"),
            content_structure,
        )

        RemoveDirectory(os.path.join("assets", "output", "Store Art"))
        RemoveDirectory(os.path.join("assets", "output", "Marketing Art"))

    def mcaddon(self):
        """Packages the project into a .mcaddon file."""
        if not self._compiled:
            self.config.Logger.not_compiled()
        self.config.Logger.packaging_mcaddon()

        self._process_art(False, False)

        content_structure = {
            self.config.RP_PATH: f"RP_{self.config.PROJECT_NAME}",
            self.config.BP_PATH: f"BP_{self.config.PROJECT_NAME}",
        }

        zipit(os.path.join("assets", "output", f"{self.config.PROJECT_NAME}.mcaddon"), content_structure)

    def mcworld(self):
        """Packages the project into a .mcworld file."""
        if not self._compiled:
            self.config.Logger.not_compiled()
        self.config.Logger.packaging_mcworld()

        self._process_art(False, False)

        content_structure = {
            self.config.RP_PATH: os.path.join("resource_packs", f"RP_{self.config.PROJECT_NAME}"),
            self.config.BP_PATH: os.path.join("behavior_packs", f"BP_{self.config.PROJECT_NAME}"),
            "texts": "texts",
            "level.dat": "",
            "levelname.txt": "",
            "manifest.json": "",
            "world_icon.jpeg": "",
            "world_behavior_packs.json": "",
            "world_resource_packs.json": "",
        }

        zipit(
            os.path.join("assets", "output", f"{self.config.PROJECT_NAME}.mcworld"),
            content_structure,
        )

    def generate_technical_notes(self):
        """Generates a technical notes PDF that contains information about included entities, blocks, items, sounds and more."""
        from reportlab.lib import colors
        from reportlab.lib.pagesizes import A4
        from reportlab.lib.styles import getSampleStyleSheet
        from reportlab.lib.units import cm
        from reportlab.platypus import (Paragraph, SimpleDocTemplate, Spacer,
                                        Table, TableStyle)

        def add_table(section_name: str, data: dict[bool, set[str]]):
            title_style.spaceBefore = 0
            title_style.fontSize = 14
            title_style.textColor = colors.royalblue
            title = Paragraph(section_name, title_style)

            converted_data = []
            vanilla_true_rows = []

            for idx, (row, columns) in enumerate(data.items()):
                vals = []
                for col_name, col_values in columns.items():
                    value_string = "<br/>".join(col_values)
                    if col_name != "vanilla":
                        vals.append(Paragraph(value_string, styles["Normal"]))
                    elif value_string == "True":
                        vanilla_true_rows.append(idx)
                converted_data.append([Paragraph(row, styles["Normal"]), *vals])

            table = Table(converted_data, hAlign="LEFT", colWidths=doc.width / len(converted_data[0]))

            style_commands = [
                ("BACKGROUND", (0, 0), (-1, 0), colors.lightblue),
                ("TEXTCOLOR", (0, 0), (-1, 0), colors.black),
                ("ALIGN", (0, 0), (-1, -1), "LEFT"),
                ("VALIGN", (0, 0), (-1, -1), "TOP"),
                ("FONTSIZE", (0, 0), (-1, 0), 12),
                ("FONTNAME", (0, 0), (-1, 0), "Helvetica"),
                ("BOTTOMPADDING", (0, 0), (-1, 0), 12),
                ("BACKGROUND", (0, 1), (-1, -1), colors.white),
                ("GRID", (0, 0), (-1, -1), 1, colors.black),
            ]
            for row in vanilla_true_rows:
                style_commands.append(("BACKGROUND", (0, row), (-1, row), colors.lightgreen))
            table.setStyle(TableStyle(style_commands))

            return title, Spacer(1, 0.3 * cm), table, Spacer(1, 1 * cm)

        doc = SimpleDocTemplate(
            os.path.join("assets", "output", "technical_notes.pdf"),
            pagesize=A4,
            leftMargin=1 * cm,
            rightMargin=1 * cm,
            topMargin=1 * cm,
            bottomMargin=1 * cm,
            title=f"{self.config.DISPLAY_NAME} Technical Notes",
            author=self.config.COMPANY,
            subject=f"{self.config.DISPLAY_NAME} Technical Notes",
            creator=f"Anvil@stark_lg {__version__}",
        )
        styles = getSampleStyleSheet()
        title_style = styles["Heading1"]
        body_style = styles["BodyText"]
        bullet_style = styles["Bullet"]

        elements = [
            Paragraph(f"{self.config.DISPLAY_NAME}:", title_style),
            Paragraph(f"Developed by: {self.config.COMPANY}", body_style),
            Paragraph(f"Generated with StarkTMA/Anvil {__version__}", body_style),
            Spacer(1, 1 * cm),
            Paragraph("General information:", title_style),
            Paragraph("The following technical notes have been entirely generated from source code using Anvil.", body_style),
            Paragraph("Features overwriting vanilla defaults will be highlighted in green.", bullet_style, "*"),
            Spacer(1, 1 * cm),
            *add_table("Entities:", self.config.Report.dict[ReportType.ENTITY]),
            *add_table("Attachables:", self.config.Report.dict[ReportType.ATTACHABLE]),
            *add_table("Items:", self.config.Report.dict[ReportType.ITEM]),
            *add_table("Blocks:", self.config.Report.dict[ReportType.BLOCK]),
            *add_table("Particles:", self.config.Report.dict[ReportType.PARTICLE]),
            *add_table("Sounds:", self.config.Report.dict[ReportType.SOUND]),
        ]

        doc.build(elements)
