examples/performance/memoization_dependencies.py
#!/usr/bin/env python3
"""
Exemple d'utilisation de la memoization avec validation basée sur dépendances.

Similaire au pattern de BuildParserLegacy où les commandes déterminent
si le cache est valide basé sur les fichiers sources.
"""

import hashlib
import time
from pathlib import Path

from catnip import Catnip


def example_file_timestamp_cache():
    """Exemple : invalidation automatique basée sur timestamp de fichiers."""
    print("⇒ Exemple 1 : Cache avec timestamp de fichiers")

    # Créer des fichiers de test
    src1 = Path('src1.txt')
    src2 = Path('src2.txt')
    src1.write_text('content 1')
    src2.write_text('content 2')

    cat = Catnip()

    # Fonction de clé qui inclut les mtimes des fichiers
    def file_timestamp_key(*files):
        """Génère une clé basée sur les timestamps des fichiers."""
        timestamps = []
        for f in files:
            p = Path(f)
            if p.exists():
                timestamps.append(f"{f}:{p.stat().st_mtime}")
            else:
                timestamps.append(f"{f}:missing")
        return '|'.join(sorted(timestamps))

    cat.context.globals['file_timestamp_key'] = file_timestamp_key

    code = """
    builds = 0

    # Build intelligent qui détecte les changements de fichiers
    smart_build = cached((files) => { builds = builds + 1; print("  → Build #" + str(builds) + " pour:", files); "output_" + str(builds) + ".js" }, "smart_build", file_timestamp_key)

    # Premier build
    print("Premier build:")
    out1 = smart_build(list("src1.txt", "src2.txt"))

    # Deuxième build (fichiers inchangés) : cache hit
    print("\\nDeuxième build (fichiers inchangés):")
    out2 = smart_build(list("src1.txt", "src2.txt"))

    list(out1, out2, builds)
    """

    cat.parse(code)
    result = cat.execute()
    print(f"Résultat: {result}\n")

    # Modifier un fichier
    print("Modification de src1.txt…")
    time.sleep(0.01)
    src1.write_text('modified content 1')

    # Nouveau build avec le même cache
    cat2 = Catnip()
    cat2.context.memoization = cat.context.memoization  # Réutiliser le même cache
    cat2.context.globals['file_timestamp_key'] = file_timestamp_key

    code2 = """
    builds = 1

    smart_build = cached(
        (files) => {
            builds = builds + 1
            print("  → Build #" + str(builds) + " pour:", files)
            "output_" + str(builds) + ".js"
        },
        "smart_build",
        file_timestamp_key
    )

    # Build après modification : cache miss car timestamp a changé
    print("\\nTroisième build (src1.txt modifié):")
    out3 = smart_build(list("src1.txt", "src2.txt"))

    list(out3, builds)
    """

    cat2.parse(code2)
    result2 = cat2.execute()
    print(f"Résultat: {result2}\n")

    # Cleanup
    src1.unlink()
    src2.unlink()


def example_content_hash_cache():
    """Exemple : invalidation basée sur le hash du contenu des fichiers."""
    print("⇒ Exemple 2 : Cache avec hash de contenu")

    # Créer des fichiers de test
    files = {
        'style.scss': b'body { color: red; }',
        'theme.scss': b'$primary: #007bff;',
    }

    for name, content in files.items():
        Path(name).write_bytes(content)

    cat = Catnip()

    # Fonction de clé basée sur le hash du contenu
    def content_hash_key(*file_paths):
        """Génère une clé basée sur le hash du contenu des fichiers."""
        hasher = hashlib.sha256()
        for fp in sorted(file_paths):
            p = Path(fp)
            if p.exists():
                hasher.update(p.read_bytes())
            else:
                hasher.update(b'missing')
        return hasher.hexdigest()

    cat.context.globals['content_hash_key'] = content_hash_key

    code = """
    compilations = 0

    # Compilation SASS avec cache basé sur contenu
    compile_sass = cached(
        (sources) => {
            compilations = compilations + 1
            print("  → Compilation #" + str(compilations))
            "output.css"
        },
        "compile_sass",
        content_hash_key
    )

    # Premier build
    print("Première compilation:")
    css1 = compile_sass(list("style.scss", "theme.scss"))

    # Deuxième build : cache hit
    print("\\nDeuxième compilation (contenu identique):")
    css2 = compile_sass(list("style.scss", "theme.scss"))

    list(css1, css2, compilations)
    """

    cat.parse(code)
    result = cat.execute()
    print(f"Résultat: {result}\n")

    # Modifier le contenu
    print("Modification du contenu de style.scss…")
    Path('style.scss').write_bytes(b'body { color: blue; }')

    # Nouveau build
    cat2 = Catnip()
    cat2.context.memoization = cat.context.memoization
    cat2.context.globals['content_hash_key'] = content_hash_key

    code2 = """
    compilations = 1

    compile_sass = cached(
        (sources) => {
            compilations = compilations + 1
            print("  → Compilation #" + str(compilations))
            "output.css"
        },
        "compile_sass",
        content_hash_key
    )

    # Build après modification : cache miss
    print("\\nTroisième compilation (contenu modifié):")
    css3 = compile_sass(list("style.scss", "theme.scss"))

    list(css3, compilations)
    """

    cat2.parse(code2)
    result2 = cat2.execute()
    print(f"Résultat: {result2}\n")

    # Cleanup
    Path('style.scss').unlink()
    Path('theme.scss').unlink()


def example_validator_with_external_state():
    """Exemple : validation basée sur état externe."""
    print("⇒ Exemple 3 : Validation avec état externe")

    # État externe simulant une configuration de build
    build_config = {'production': False, 'version': '1.0.0'}

    cat = Catnip()

    # Validator qui vérifie si la config n'a pas changé
    def validate_config(cached_result, *args, **kwargs):
        """Valide que le cache est compatible avec la config actuelle."""
        # En production, invalider tous les caches de dev
        if build_config['production']:
            return False
        return True

    cat.context.globals['validate_config'] = validate_config

    code = """
    builds = 0

    # Build avec validation selon la config
    build_app = cached(
        (entry) => {
            builds = builds + 1
            print("  → Build #" + str(builds))
            "app_" + str(builds) + ".js"
        },
        "build_app",
        None,
        validate_config
    )

    # Premier build en mode dev
    print("Build en mode développement:")
    app1 = build_app("main.js")

    # Deuxième build : cache hit
    print("\\nDeuxième build (mode dev, cache hit):")
    app2 = build_app("main.js")

    list(app1, app2, builds)
    """

    cat.parse(code)
    result = cat.execute()
    print(f"Résultat: {result}\n")

    # Passer en mode production
    print("Passage en mode production…")
    build_config['production'] = True

    # Nouveau build
    cat2 = Catnip()
    cat2.context.memoization = cat.context.memoization
    cat2.context.globals['validate_config'] = validate_config

    code2 = """
    builds = 1

    build_app = cached(
        (entry) => {
            builds = builds + 1
            print("  → Build #" + str(builds))
            "app_" + str(builds) + ".js"
        },
        "build_app",
        None,
        validate_config
    )

    # Build en production : validator invalide le cache
    print("\\nTroisième build (mode production, cache invalidé):")
    app3 = build_app("main.js")

    list(app3, builds)
    """

    cat2.parse(code2)
    result2 = cat2.execute()
    print(f"Résultat: {result2}\n")


def example_buildparser_pattern():
    """Exemple complet imitant le pattern BuildParserLegacy."""
    print("⇒ Exemple 4 : Pattern complet (comme BuildParserLegacy)")

    # Simuler des fichiers sources
    sources = {
        'index.html': b'<html>…</html>',
        'style.css': b'body { … }',
        'script.js': b'console.log("hello");',
    }

    for name, content in sources.items():
        Path(name).write_bytes(content)

    cat = Catnip()

    # Fonction pour calculer les dépendances (comme get_cache_dependencies)
    def get_dependencies(*files):
        """Retourne les dépendances pour la clé de cache."""
        deps = []
        for f in files:
            p = Path(f)
            if p.exists():
                # Inclure taille et mtime
                stat = p.stat()
                deps.append(f"{f}:{stat.st_size}:{stat.st_mtime}")
        return deps

    # Fonction de clé qui inclut les dépendances
    def build_cache_key(*files):
        """Génère une clé de cache incluant les dépendances."""
        deps = get_dependencies(*files)
        key_str = '|'.join(sorted(deps))
        return hashlib.sha256(key_str.encode()).hexdigest()[:16]

    # Validator qui vérifie que les fichiers existent toujours
    def validate_build(cached_result, *files):
        """Valide que tous les fichiers sources existent encore."""
        for f in files:
            if not Path(f).exists():
                print(f"  → Cache invalide : {f} n'existe plus")
                return False
        return True

    cat.context.globals['build_cache_key'] = build_cache_key
    cat.context.globals['validate_build'] = validate_build

    code = """
    builds = 0

    # Commande de build avec cache intelligent
    bundle = cached(
        (sources) => {
            builds = builds + 1
            print("  → Bundling sources (build #" + str(builds) + ")…")
            "dist/bundle_" + str(builds) + ".js"
        },
        "bundle",
        build_cache_key,
        validate_build
    )

    # Premier build
    print("Premier build:")
    out1 = bundle(list("index.html", "style.css", "script.js"))

    # Deuxième build : cache hit
    print("\\nDeuxième build (cache hit):")
    out2 = bundle(list("index.html", "style.css", "script.js"))

    list(out1, out2, builds)
    """

    cat.parse(code)
    result = cat.execute()
    print(f"Résultat: {result}\n")

    # Modifier un fichier
    print("Modification de script.js…")
    time.sleep(0.01)
    Path('script.js').write_bytes(b'console.log("modified");')

    # Nouveau build
    cat2 = Catnip()
    cat2.context.memoization = cat.context.memoization
    cat2.context.globals['build_cache_key'] = build_cache_key
    cat2.context.globals['validate_build'] = validate_build

    code2 = """
    builds = 1

    bundle = cached(
        (sources) => {
            builds = builds + 1
            print("  → Bundling sources (build #" + str(builds) + ")…")
            "dist/bundle_" + str(builds) + ".js"
        },
        "bundle",
        build_cache_key,
        validate_build
    )

    # Build après modification : cache miss
    print("\\nTroisième build (fichier modifié):")
    out3 = bundle(list("index.html", "style.css", "script.js"))

    list(out3, builds)
    """

    cat2.parse(code2)
    result2 = cat2.execute()
    print(f"Résultat: {result2}\n")

    # Cleanup
    Path('index.html').unlink()
    Path('style.css').unlink()
    Path('script.js').unlink()


def main():
    print("╔══════════════════════════════════════════════════════╗")
    print("║  Cache avec dépendances (pattern BuildParserLegacy) ║")
    print("╚══════════════════════════════════════════════════════╝\n")

    example_file_timestamp_cache()
    example_content_hash_cache()
    example_validator_with_external_state()
    example_buildparser_pattern()

    print("✓ Tous les exemples terminés !")
    print("\nCes patterns permettent de créer des commandes de build")
    print("intelligentes qui détectent automatiquement quand recalculer.")


if __name__ == "__main__":
    main()