#!/usr/bin/env python3
"""
Grouping symbols in binary into coherent categories.
"""

from __future__ import annotations

import sys
from pathlib import Path
from typing import Callable

from binsize import BinarySize, DataRow, StatisticsPlugin, set_root_dir

HERE = Path(__file__).resolve().parent
CORE_DIR = HERE.parent.parent

if len(sys.argv) > 1:
    BIN_TO_ANALYZE = sys.argv[1]
else:
    BIN_TO_ANALYZE = CORE_DIR / "build/firmware/firmware.elf"
FILE_TO_SAVE = HERE / "size_binary_firmware_elf_results.txt"


def _categories_func(row: DataRow) -> str | None:
    # Defined inside the function so it can be seen in the function definition
    # (which is optionally printed)
    CATEGORIES: dict[str, Callable[[DataRow], bool]] = {
        "UI": lambda row: (
            row.source_definition.startswith(
                ("src/trezor/ui/", "embed/upymod/modtrezorui/")
            )
        ),
        "Crypto": lambda row: (
            row.source_definition.startswith(
                (
                    "vendor/trezor-crypto/",
                    "src/trezor/crypto/",
                    "embed/upymod/modtrezorcrypto/",
                )
            )
        ),
        "Secp256": lambda row: (
            row.source_definition.startswith("vendor/secp256k1-zkp/")
        ),
        "Storage": lambda row: (
            row.source_definition.startswith(("src/storage/", "vendor/trezor-storage/"))
        ),
        "Micropython": lambda row: row.source_definition.startswith(
            "vendor/micropython/"
        ),
        "Bitcoin app": lambda row: row.source_definition.startswith(
            "src/apps/bitcoin/"
        ),
        "Ethereum app": lambda row: row.source_definition.startswith(
            "src/apps/ethereum/"
        ),
        "Monero app": lambda row: row.source_definition.startswith("src/apps/monero/"),
        "Cardano app": lambda row: row.source_definition.startswith(
            "src/apps/cardano/"
        ),
        "Management app": lambda row: row.source_definition.startswith(
            "src/apps/management/"
        ),
        "Common apps": lambda row: row.source_definition.startswith("src/apps/common/"),
        "Webauthn app": lambda row: row.source_definition.startswith(
            "src/apps/webauthn/"
        ),
        "Altcoin apps": lambda row: (
            row.source_definition.startswith(
                (
                    "src/apps/nem/",
                    "src/apps/stellar/",
                    "src/apps/eos/",
                    "src/apps/tezos/",
                    "src/apps/tron/",
                    "src/apps/ripple/",
                    "src/apps/zcash/",
                )
            )
        ),
        "Other apps": lambda row: row.source_definition.startswith("src/apps/"),
        "Rest of src/": lambda row: row.source_definition.startswith("src/"),
        "Embed firmware": lambda row: row.source_definition.startswith(
            "embed/projects/firmware/"
        ),
        "Trezorhal": lambda row: row.source_definition.startswith(
            ("embed/io/", "embed/sys/", "embed/sec/", "embed/util/")
        ),
        "Trezorio": lambda row: row.source_definition.startswith(
            "embed/upymod/modtrezorio/"
        ),
        "Trezorconfig": lambda row: row.source_definition.startswith(
            "embed/upymod/modtrezorconfig/"
        ),
        "Trezorutils": lambda row: row.source_definition.startswith(
            "embed/upymod/modtrezorutils/"
        ),
        "Embed upymod": lambda row: row.source_definition.startswith("embed/upymod/"),
        "Embed lib": lambda row: row.source_definition.startswith("embed/rtl/"),
        "Rust": lambda row: (
            row.language == "Rust"
            or row.source_definition.startswith(("embed/rust/", "/cargo/registry"))
            or row.symbol_name.startswith(("trezor_tjpgdec", "qrcodegen"))
            or ".cargo/registry" in row.symbol_name
        ),
        "Frozen modules": lambda row: row.symbol_name.startswith("frozen_module"),
        ".bootloader": lambda row: row.symbol_name == ".bootloader",
        ".rodata + qstr + misc": lambda row: (
            row.symbol_name.startswith(
                (".rodata", "mp_qstr", "str1", "*fill*", ".text", "OUTLINED_FUNCTION")
            )
            or _has_32_hex(row.symbol_name)
        ),
    }

    for category, func in CATEGORIES.items():
        if func(row):
            return category
    return None


def _has_32_hex(text: str) -> bool:
    if "." in text:
        text = text.split(".")[0]
    return len(text) == 32 and all(c in "0123456789abcdef" for c in text)


def show_categories_statistics(
    STATS: StatisticsPlugin, include_categories_func: bool = False
) -> None:
    STATS.show(include_none=True, include_categories_func=include_categories_func)


def show_data_with_categories(
    STATS: StatisticsPlugin, file_to_save: str | Path | None = None
) -> None:
    STATS.show_data_with_categories(file_to_save, include_none=True)


def show_only_one_category(
    BS: BinarySize, category: str | None, file_to_save: str | Path | None = None
) -> None:
    BS.filter(lambda row: _categories_func(row) == category).show(
        file_to_save, debug=True
    )


def show_raw_bloaty_data() -> None:
    BinarySize().load_file(BIN_TO_ANALYZE, sections=(".flash", ".flash2")).show(
        HERE / "size_binary_firmware_elf_results_no_aggregation.txt"
    )


if __name__ == "__main__":
    set_root_dir(str(CORE_DIR))

    BS = (
        BinarySize()
        .load_file(BIN_TO_ANALYZE, sections=(".flash", ".flash2"))
        .use_map_file(
            CORE_DIR / "build/firmware/firmware.map", sections=(".flash", ".flash2")
        )
        .add_basic_info()
        .aggregate()
        .sort()
        .add_definitions()
    )
    STATS = StatisticsPlugin(BS, _categories_func)

    show_categories_statistics(STATS, include_categories_func=True)
    show_data_with_categories(STATS, FILE_TO_SAVE)
    show_only_one_category(BS, None, HERE / "size_binary_firmware_elf_results_None.txt")
