import base64
import io
import math
import os
import traceback as tb
import zipfile
from typing import Literal, Optional

import click
from anvil.api.actors.materials import MaterialsObject
from anvil.api.core.sounds import BlocksJSONObject, MusicDefinition, SoundDefinition, SoundEvent
from anvil.api.core.textures import FlipBookTexturesObject, ItemTexturesObject, TerrainTexturesObject
from anvil.lib.blockbench import _Blockbench
from anvil.lib.config import CONFIG, ConfigPackageTarget, _AnvilConfig
from anvil.lib.format_versions import MANIFEST_VERSION, MODULE_MINECRAFT_SERVER, MODULE_MINECRAFT_SERVER_UI
from anvil.lib.lib import (
    CreateDirectory,
    File,
    FileExists,
    RemoveDirectory,
    process_subcommand,
    validate_namespace_project_name,
    zipit,
)
from anvil.lib.reports import ReportType
from anvil.lib.schemas import AddonObject, JsonSchemes
from anvil.lib.translator import AnvilTranslator
from halo import Halo
from PIL import Image

from ...__version__ import __version__


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,
    )


def extract_world_pack(extract_world: str | None = None):
    if extract_world != None and type(extract_world) is str:
        RemoveDirectory(CONFIG._WORLD_PATH)
        with zipfile.ZipFile(os.path.join("world", f"{extract_world}.mcworld"), "r") as zip_ref:
            zip_ref.extractall(CONFIG._WORLD_PATH)


def manifests(extract_world: str | None = None):
    release_list = [int(i) for i in CONFIG._RELEASE.split(".")]
    if extract_world != None and type(extract_world) is str:
        File(
            "manifest.json",
            JsonSchemes.manifest_world(release_list),
            CONFIG._WORLD_PATH,
            "w",
        )
        File(
            "world_resource_packs.json",
            JsonSchemes.world_packs(release_list, CONFIG._RP_UUID),
            CONFIG._WORLD_PATH,
            "w",
        )
        File(
            "world_behavior_packs.json",
            JsonSchemes.world_packs(release_list, CONFIG._BP_UUID),
            CONFIG._WORLD_PATH,
            "w",
        )


def scriptapi():
    if any([CONFIG._SCRIPT_API, CONFIG._SCRIPT_UI]):
        if not FileExists("tsconfig.json"):
            File(
                "tsconfig.json",
                JsonSchemes.tsconfig(CONFIG.BP_PATH),
                "",
                "w",
                False,
            )
        if not FileExists("esbuild.js") and "esbuild" in CONFIG._SCRIPT_BUNDLE_SCRIPT:
            File(
                "esbuild.js",
                JsonSchemes.esbuild_config_js(CONFIG.BP_PATH, CONFIG._MINIFY),
                "",
                "w",
                False,
            )
        if not FileExists("package.json"):
            File(
                "package.json",
                JsonSchemes.package_json(
                    CONFIG.PROJECT_NAME,
                    CONFIG._RELEASE,
                    CONFIG.PROJECT_DESCRIPTION,
                    CONFIG.COMPANY,
                ),
                "",
                "w",
                True,
            )


def addon_object_exception(object: AddonObject, e: Exception):
    full_traceback = tb.format_exc()
    creation_info = f"<{object.__class__.__name__} created from:\n{''.join(getattr(object, "_created_from", None))}>"
    click.echo(click.style(f"\r{'='*60}", fg="red"), err=True)
    click.echo(click.style(f"\r\nCreation Context:", fg="cyan"), err=True)
    click.echo(click.style(f"\r{creation_info}", fg="white"), err=True)
    click.echo(click.style(f"\r\nFull Traceback:", fg="cyan"), err=True)
    click.echo(click.style(f"\r{full_traceback}", fg="white"), err=True)
    click.echo(click.style(f"\r{'='*60}", fg="red"), err=True)
    click.echo(
        click.style(
            f"\rERROR EXPORTING OBJECT: {getattr(object, '_name', 'Unknown')}",
            fg="red",
        ),
        err=True,
    )
    click.echo(click.style(f"\r{'='*60}", fg="red"), err=True)
    click.echo(
        click.style(
            f"\rObject Type: {object.__class__.__name__}",
            fg="yellow",
        ),
        err=True,
    )
    click.echo(
        click.style(
            f"\rObject Name: {getattr(object, '_name', 'Unknown')}",
            fg="yellow",
        ),
        err=True,
    )
    click.echo(click.style(f"\rObject Content: {object._content}", fg="yellow"), err=True)
    click.echo(click.style(f"\rError Message: {str(e)}", fg="red"), err=True)
    click.echo(click.style(f"\r{'='*60}", fg="red"), err=True)


@Halo(text="Processing Art", spinner="dots")
def process_art(
    config: _AnvilConfig,
    apply_overlay: bool = False,
    zip: bool = True,
):
    """Process marketing art for packaging."""
    pack_icon_size = (256, 256)
    store_screenshot_size = (800, 450)
    marketing_screenshot_size = (1920, 1080)

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

    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"))

            if config._TARGET == ConfigPackageTarget.WORLD:
                resize(
                    original,
                    "world_icon.jpeg",
                    config._WORLD_PATH,
                    store_screenshot_size,
                    95,
                    72,
                    overlay,
                )
            resize(
                original,
                f"{config.PROJECT_NAME}_Thumbnail_0.jpg",
                output_store,
                store_screenshot_size,
                95,
                72,
                overlay,
            )
            resize(
                original,
                f"{config.PROJECT_NAME}_MarketingKeyArt.jpg",
                output_marketing,
                marketing_screenshot_size,
                100,
                300,
                overlay,
            )

        else:
            raise FileNotFoundError("keyart.png not found in marketing directory. Please ensure the file exists.")

        if FileExists(os.path.join(source, "panorama.png")):
            original = Image.open(os.path.join(source, "panorama.png"))
            scale_factor = 450 / original.size[1]
            if (round(original.size[0] * scale_factor)) > 4000:
                raise RuntimeError(
                    f"panorama.png width exceeds 4000 pixels when resized to 450 pixels in height. Please use a smaller image. The recommended original size for this image would be {math.floor(4000 / scale_factor)}x450 pixels."
                )

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

        else:
            click.echo(
                click.style(
                    f"\r[INFO]: panorama.png does not exist. It is optional but might have been left out unintentionally.",
                    fg="yellow",
                )
            )

        for i in range(999):
            if not FileExists(os.path.join(source, f"{i}.png")):
                if i < 5:
                    raise FileNotFoundError(f"{i}.png not found in marketing directory. Please ensure the file exists.")
                break
            else:
                original = Image.open(os.path.join(source, f"{i}.png"))
                resize(
                    original,
                    f"{config.PROJECT_NAME}_screenshot_{i}.jpg",
                    output_store,
                    store_screenshot_size,
                    95,
                    72,
                )
                resize(
                    original,
                    f"{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"{config.PROJECT_NAME}_PartnerArt.jpg",
                output_marketing,
                marketing_screenshot_size,
                100,
                300,
            )

        else:
            raise FileNotFoundError("partner_art.png not found in marketing directory. Please ensure the file exists.")

        if FileExists(os.path.join(source, "pack_icon.png")):
            original = Image.open(os.path.join(source, "pack_icon.png"))
            resize(
                original,
                f"{config.PROJECT_NAME}_packicon_0.jpg",
                output_store,
                pack_icon_size,
            )
        else:
            raise FileNotFoundError("pack_icon.png not found in marketing directory. Please ensure the file exists.")


@Halo(text="Generating technical notes", spinner="dots")
def generate_technical_notes_pdf(config: _AnvilConfig):
    """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("output", "technical_notes.pdf"),
        pagesize=A4,
        leftMargin=1 * cm,
        rightMargin=1 * cm,
        topMargin=1 * cm,
        bottomMargin=1 * cm,
        title=f"{config.DISPLAY_NAME} Technical Notes",
        author=config.COMPANY,
        subject=f"{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"{config.DISPLAY_NAME}:", title_style),
        Paragraph(f"Developed by: {config.COMPANY}", body_style),
        Paragraph(
            f'Generated with <a href="https://github.com/StarkTMA/Anvil"><u><font color="blue">StarkTMA/Anvil {__version__}</font></u></a>',
            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:", config.Report.dict[ReportType.ENTITY]),
        *add_table("Attachables:", config.Report.dict[ReportType.ATTACHABLE]),
        *add_table("Items:", config.Report.dict[ReportType.ITEM]),
        *add_table("Blocks:", config.Report.dict[ReportType.BLOCK]),
        *add_table("Particles:", config.Report.dict[ReportType.PARTICLE]),
        *add_table("Sounds:", config.Report.dict[ReportType.SOUND]),
    ]

    doc.build(elements)


@Halo(text="Packaging ZIP", spinner="dots")
def package_zip_core(
    config: _AnvilConfig,
    apply_overlay: bool = False,
    generate_technical_notes: bool = False,
):
    """Core logic for packaging the project into a zip file for Marketplace."""
    process_art(config, apply_overlay)

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

    if config._TARGET == ConfigPackageTarget.ADDON:
        content_structure.update(
            {
                config.RP_PATH: os.path.join(
                    "Content",
                    "resource_packs",
                    f"RP_{config._PASCAL_PROJECT_NAME}",
                ),
                config.BP_PATH: os.path.join(
                    "Content",
                    "behavior_packs",
                    f"BP_{config._PASCAL_PROJECT_NAME}",
                ),
            }
        )

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

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

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

    if generate_technical_notes:
        generate_technical_notes_pdf(config)


@Halo(text="Packaging mcaddon", spinner="dots")
def mcaddon_core(config):
    """Core logic for packaging the project into a .mcaddon file."""
    process_art(config, False, False)

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

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


@Halo(text="Packaging mcworld", spinner="dots")
def mcworld_core(config: _AnvilConfig):
    """Core logic for packaging the project into a .mcworld file."""
    process_art(config, False, False)

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

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


@Halo(text="Compiling", spinner="dots")
def compile_objects(anvil: "_Anvil", extract_world: str = None):
    extract_world_pack(extract_world)
    manifests(extract_world)
    scriptapi()

    ItemTexturesObject().queue()
    TerrainTexturesObject().queue()
    FlipBookTexturesObject().queue()
    SoundDefinition().queue()
    SoundEvent().queue()
    MusicDefinition().queue()
    BlocksJSONObject().queue()
    MaterialsObject().queue()
    _Blockbench._export()
    anvil._queue(AnvilTranslator())
    ManifestBP().queue()
    ManifestRP().queue()

    from anvil.api.blocks.blocks import _PermutationComponents

    if _PermutationComponents._count > 10000:
        if CONFIG._TARGET == ConfigPackageTarget.ADDON:
            raise RuntimeError(
                f"\rTotal Block permutations exceeded 10000 ({_PermutationComponents._count}). Addons must not exceed this limit."
            )
        else:
            click.echo(
                click.style(
                    f"\r[INFO]: Total Block permutations exceeded 10000 ({_PermutationComponents._count}). This may cause issues when submitting to the Marketplace.",
                    fg="yellow",
                )
            )

    for object in anvil._objects_list:
        try:
            object._export()
        except Exception as e:
            addon_object_exception(object, e)
            return

    if CONFIG._TARGET == ConfigPackageTarget.ADDON:
        if len(CONFIG._RP_UUID) > 1:
            raise RuntimeError(
                "Multiple resource pack UUIDs found. Please ensure only one UUID is set for the resource pack."
            )
        if len(CONFIG._BP_UUID) > 1:
            raise RuntimeError(
                "Multiple behavior pack UUIDs found. Please ensure only one UUID is set for the behavior pack."
            )

    if CONFIG._SCRIPT_API:
        process_subcommand(
            CONFIG._SCRIPT_BUNDLE_SCRIPT + f' --outdir="{os.path.join(CONFIG.BP_PATH, "scripts")}"',
            "Building scripts error",
        )

    source = os.path.join("marketing")
    pack_icon_size = (256, 256)

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

    else:
        click.echo(
            click.style(
                f"\r[INFO]: pack_icon.png not found in marketing directory. Using placeholder pack icon.",
                fg="yellow",
            )
        )
        placeholder_base64 = " "
        placeholder_bytes = base64.b64decode(placeholder_base64)
        placeholder_image = Image.open(io.BytesIO(placeholder_bytes))

        resize(placeholder_image, "pack_icon.png", CONFIG.BP_PATH, pack_icon_size)
        resize(placeholder_image, "pack_icon.png", CONFIG.RP_PATH, pack_icon_size)

    anvil._compiled = True


@Halo(text="Translating", spinner="dots")
def translate(languages: Optional[list[str]] = None) -> None:
    """Translates the project."""
    AnvilTranslator().auto_translate_all(languages)


class ManifestRP(AddonObject):
    _instance = None
    _extension = ".json"
    _path = os.path.join(CONFIG.RP_PATH)
    _setting_ids: set[str] = set()

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(ManifestRP, cls).__new__(cls)
        return cls._instance

    def __init__(self):
        """Initializes a ManifestRP instance."""
        if not hasattr(self, "_initialized"):
            super().__init__("manifest")
            self.content(JsonSchemes.manifest_rp(version=CONFIG._RELEASE))
            self._initialized = True

    @property
    def settings(self) -> "_ManifestSettings":
        """Returns a Manifest Settings object to add settings to the manifest."""
        return _ManifestSettings(self)

    def queue(self):
        if CONFIG._PBR:
            self._content.update({"capabilities": ["pbr"]})

        if CONFIG._TARGET == "addon":
            self._content["header"]["pack_scope"] = "world"
            self._content["metadata"]["product_type"] = "addon"

        return super().queue()


class ManifestBP(AddonObject):
    _instance = None
    _extension = ".json"
    _path = os.path.join(CONFIG.BP_PATH)
    _setting_ids: set[str] = set()

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super(ManifestBP, cls).__new__(cls)
        return cls._instance

    def __init__(self):
        """Initializes a ManifestBP instance."""
        if not hasattr(self, "_initialized"):
            super().__init__("manifest")
            self.content(JsonSchemes.manifest_bp(version=CONFIG._RELEASE))
            self._initialized = True

    @property
    def settings(self) -> "_ManifestSettings":
        """Returns a Manifest Settings object to add settings to the manifest."""
        if not CONFIG._PREVIEW:
            raise RuntimeError("Behaviour Pack Settings are currently only supported in current preview builds.")
        return _ManifestSettings(self)

    def queue(self):
        if CONFIG._SCRIPT_API:
            self._content["modules"].append(
                {
                    "uuid": CONFIG._SCRIPT_MODULE_UUID,
                    "version": CONFIG._RELEASE,
                    "type": "script",
                    "language": "javascript",
                    "entry": "scripts/main.js",
                }
            )
            self._content["dependencies"].append(
                {
                    "module_name": "@minecraft/server",
                    "version": MODULE_MINECRAFT_SERVER,
                }
            )
            if CONFIG._SCRIPT_UI:
                self._content["dependencies"].append(
                    {
                        "module_name": "@minecraft/server-ui",
                        "version": MODULE_MINECRAFT_SERVER_UI,
                    }
                )

        if CONFIG._TARGET == "addon":
            self._content["header"]["pack_scope"] = "world"
            self._content["metadata"]["product_type"] = "addon"

        return super().queue()


class _ManifestSettings:
    def __init__(self, manifest: ManifestRP | ManifestBP):
        if MANIFEST_VERSION < 3:
            raise RuntimeError("Settings labels are only supported in manifest version 3 and above.")
        self.manifest = manifest

        # Initialize settings in _content if not present
        if "settings" not in self.manifest._content:
            self.manifest._content["settings"] = []

    def label(self, label: str):
        self.manifest._content["settings"].append({"type": "label", "text": label})
        return self

    def toggle(self, id: str, text: str, default: bool = True):
        if id in self.manifest._setting_ids:
            raise ValueError(f"Setting ID '{id}' is already used.")

        self.manifest._setting_ids.add(id)
        self.manifest._content["settings"].append(
            {
                "type": "toggle",
                "name": f"{CONFIG.NAMESPACE}:{id}",
                "text": text,
                "default": default,
            }
        )
        return self

    def slider(self, id: str, text: str, min: int, max: int, step: int, default: int):
        if min >= max:
            raise ValueError(f"Min value '{min}' must be less than max '{max}'.")

        if step <= 0:
            raise ValueError(f"Step value '{step}' must be greater than 0.")

        if not (min <= default <= max):
            raise ValueError(f"Default value '{default}' must be between min '{min}' and max '{max}'.")

        if id in self.manifest._setting_ids:
            raise ValueError(f"Setting ID '{id}' is already used.")

        self.manifest._setting_ids.add(id)
        self.manifest._content["settings"].append(
            {
                "type": "slider",
                "name": f"{CONFIG.NAMESPACE}:{id}",
                "text": text,
                "min": min,
                "max": max,
                "step": step,
                "default": default,
            }
        )
        return self

    def dropdown(self, id: str, text: str, options: list[dict[str, str]], default: str):
        if not isinstance(options, list) or len(options) == 0:
            raise ValueError("Options must be a non-empty list of dictionaries.")

        if default not in [key for option in options for key in option.keys()]:
            raise ValueError(f"Default value '{default}' is not in the options list.")

        if id in self.manifest._setting_ids:
            raise ValueError(f"Setting ID '{id}' is already used.")

        self.manifest._setting_ids.add(id)
        self.manifest._content["settings"].append(
            {
                "type": "dropdown",
                "name": f"{CONFIG.NAMESPACE}:{id}",
                "text": text,
                "options": [{"name": k, "text": v} for option in options for k, v in option.items()],
                "default": default,
            }
        )
        return self


class _Anvil:
    """A class representing an Anvil instance."""

    _instance = None
    _objects_list: list[AddonObject] = []

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

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

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

        self._compiled = False
        self.config = CONFIG
        validate_namespace_project_name(CONFIG.NAMESPACE, CONFIG.PROJECT_NAME)

    def translate(self, languages: Optional[list[str]] = None) -> None:
        """Translates the project."""
        translate(languages)

    def compile(self, extract_world: str = None) -> None:
        """Compiles the project."""
        compile_objects(self, extract_world)

    def package_zip(
        self,
        apply_overlay: bool = False,
        generate_technical_notes: bool = False,
    ) -> None:
        """Packages the project into a zip file for Marketplace.

        Parameters:
            apply_overlay (bool, optional): Whether to apply the overlay to the marketing art. Defaults to False.
            generate_technical_notes (bool, optional): Whether to generate technical notes PDF. Defaults to False.
        """
        if not self._compiled:
            raise RuntimeError("Project must be compiled before packaging.")

        package_zip_core(self.config, apply_overlay, generate_technical_notes)

    def mcaddon(self):
        """Packages the project into a .mcaddon file."""
        if not self._compiled:
            raise RuntimeError("Project must be compiled before packaging.")

        mcaddon_core(self.config)

    def mcworld(self):
        """Packages the project into a .mcworld file."""
        if not self._compiled:
            raise RuntimeError("Project must be compiled before packaging.")

        mcworld_core(self.config)


ANVIL = _Anvil()
