@*
    Scan du code-barres ISBN par la caméra.

    Le décodage se fait dans wwwroot/js/scanner-camera.js, par zbar compilé en WebAssembly.
    ⚠️ Il était en C# (ZXing.Net) jusqu'au 2026-08-19 : voir CLAUDE.md, ZXing.Net ne lisait pas
    des codes-barres que zbar lit sur le même livre. Ce composant ne voit plus passer un seul
    pixel — seulement la valeur décodée.

    On ne décode pas à chaque frame de la caméra : la cadence est volontairement limitée
    (voir IntervalleMs). 10-15 images/s suffisent largement pour scanner un livre tenu à la
    main, et cela ménage la batterie du téléphone.
*@
@using System.Diagnostics
@using MaBibli.Shared.Isbn
@implements IAsyncDisposable
@inject IJSRuntime JS

<div class="scanner">
    @if (_etat == Etat.Erreur)
    {
        <p class="message-erreur" role="alert">@_erreur</p>
    }
    else
    {
        <div class="scanner-cadre">
            <video @ref="_video" class="scanner-video" muted playsinline autoplay></video>
            <div class="scanner-viseur" aria-hidden="true"></div>
        </div>

        <p class="message-discret">
            @if (_etat == Etat.Demarrage)
            {
                <text>Ouverture de la caméra…</text>
            }
            else
            {
                <text>Visez le code-barres au dos du livre, bien à plat et bien éclairé.</text>
            }
        </p>

        @if (_avertissementFormat is not null)
        {
            <p class="message-avertissement">@_avertissementFormat</p>
        }

        @if (_framesDecodees > 0)
        {
            @* Mesure sur l'appareil de l'utilisateur. ⚠️ Elle porte maintenant sur la capture,
               le décodage zbar ET l'aller-retour d'interopérabilité : elle n'est pas comparable
               aux chiffres ZXing.Net de CLAUDE.md, qui ne mesuraient que le décodage C#. *@
            <p class="message-discret scanner-mesure">
                Décodage : @_msMoyen.ToString("0.0") ms/frame en moyenne (@_framesDecodees frames)
            </p>
        }
    }

    <div class="actions-formulaire">
        <button type="button" class="bouton bouton-discret" @onclick="AnnulerAsync">Annuler</button>
    </div>
</div>

@code {
    /// <summary>Appelé avec l'ISBN-13 dès qu'un code-barres exploitable est décodé.</summary>
    [Parameter] public EventCallback<CodeScanne> OnCodeDetecte { get; set; }

    /// <summary>Appelé quand l'utilisateur renonce au scan.</summary>
    [Parameter] public EventCallback OnAnnuler { get; set; }

    private enum Etat { Demarrage, Actif, Erreur }

    // Chemin nu : il n'y a plus d'import map depuis que les empreintes WASM sont désactivées
    // (voir MaBibli.Client.csproj), donc plus de réécriture vers un nom empreinté que l'hôte
    // ne servirait pas. Le contournement « ?m=1 » qui neutralisait cette réécriture n'a plus
    // lieu d'être. Vérifié en développement et en publication.
    private const string CheminModule = "./js/scanner-camera.js";

    // ~12 images/s : bien assez pour scanner, et deux fois moins de travail que 25 fps.
    private const int IntervalleMs = 80;

    // L'image est réduite avant décodage. Elle ne traverse plus le pont JS→C# — c'est zbar,
    // côté JS, qui la lit — mais 960 px de large restent un bon compromis entre le détail
    // nécessaire à un EAN-13 dense et le temps de décodage.
    //
    // ⚠️ Plus large qu'avec ZXing.Net (640 px), et surtout l'image ENTIÈRE est analysée : le
    // recadrage sur la bande centrale n'existait que pour alléger le transfert de pixels.
    private const int LargeurMax = 960;

    private ElementReference _video;
    private IJSObjectReference? _module;
    private CancellationTokenSource? _boucle;
    private Etat _etat = Etat.Demarrage;
    private string? _erreur;
    private string? _avertissementFormat;
    private long _framesDecodees;
    private double _totalMs;
    private double _msMoyen => _framesDecodees == 0 ? 0 : _totalMs / _framesDecodees;

    protected override async Task OnAfterRenderAsync(bool premierRendu)
    {
        if (!premierRendu)
        {
            return;
        }

        try
        {
            _module = await JS.InvokeAsync<IJSObjectReference>("import", CheminModule);

            var diagnostic = await _module.InvokeAsync<string>("diagnostic");
            if (diagnostic != "ok")
            {
                Echouer(diagnostic switch
                {
                    // Piège garanti en développement : depuis un téléphone, pointer l'IP locale
                    // du PC (http://192.168.x.x) n'est PAS un contexte sécurisé pour le navigateur.
                    "contexte-non-securise" =>
                        "La caméra n'est accessible qu'en HTTPS (ou sur localhost). "
                        + "En développement, ouvrir l'application par l'adresse IP du PC ne suffit pas. "
                        + "Saisissez l'ISBN à la main.",
                    _ => "Ce navigateur ne donne pas accès à la caméra. Saisissez l'ISBN à la main.",
                });
                return;
            }

            // On ne laisse jamais un écran noir sans explication : chaque code dit précisément
            // ce qui manque à l'utilisateur pour que le scan fonctionne.
            var statut = await _module.InvokeAsync<string>("demarrer", _video);
            if (statut != "ok")
            {
                Echouer(statut switch
                {
                    "permission-refusee" =>
                        "L'accès à la caméra a été refusé. Autorisez-le dans les réglages du navigateur, "
                        + "ou saisissez l'ISBN à la main.",
                    "aucune-camera" =>
                        "Aucune caméra utilisable n'a été trouvée sur cet appareil. Saisissez l'ISBN à la main.",
                    "camera-occupee" =>
                        "La caméra est déjà utilisée par une autre application. Fermez-la, ou saisissez l'ISBN à la main.",
                    _ => $"La caméra n'a pas pu démarrer ({statut[(statut.IndexOf(':') + 1)..].Trim()}). "
                        + "Saisissez l'ISBN à la main.",
                });
                return;
            }

            _etat = Etat.Actif;
            StateHasChanged();

            _boucle = new CancellationTokenSource();
            _ = BoucleAsync(_boucle.Token);
        }
        catch (Exception ex)
        {
            Echouer($"La caméra n'a pas pu démarrer ({ex.Message}). Saisissez l'ISBN à la main.");
        }
    }

    private async Task BoucleAsync(CancellationToken jeton)
    {
        try
        {
            while (!jeton.IsCancellationRequested)
            {
                await Task.Delay(IntervalleMs, jeton);

                if (_module is null || jeton.IsCancellationRequested)
                {
                    return;
                }

                var depart = Stopwatch.GetTimestamp();
                var lecture = await _module.InvokeAsync<LectureCodeBarres?>("decoder", LargeurMax);
                var code = lecture?.Code;
                _totalMs += Stopwatch.GetElapsedTime(depart).TotalMilliseconds;
                _framesDecodees++;

                if (code is null)
                {
                    // Une frame sur dix suffit à rafraîchir la mesure sans repeindre en boucle.
                    if (_framesDecodees % 10 == 0)
                    {
                        StateHasChanged();
                    }
                    continue;
                }

                var isbn = IsbnUtils.Normaliser(code);
                if (isbn is null || !IsbnUtils.EstIsbn13Valide(isbn))
                {
                    // Un EAN-8, ou l'EAN d'un produit non-livre : on le dit et on continue.
                    _avertissementFormat = $"Code lu (« {code} »), mais ce n'est pas un ISBN. Continuez à viser le code-barres du livre.";
                    StateHasChanged();
                    continue;
                }

                await ArreterCameraAsync();
                await OnCodeDetecte.InvokeAsync(new CodeScanne(isbn, lecture?.Parution));
                return;
            }
        }
        catch (OperationCanceledException)
        {
            // Sortie normale : le composant a été quitté.
        }
        catch (JSDisconnectedException)
        {
            // La page se ferme, plus rien à libérer côté JS.
        }
    }

    private async Task AnnulerAsync()
    {
        await ArreterCameraAsync();
        await OnAnnuler.InvokeAsync();
    }

    private void Echouer(string message)
    {
        _erreur = message;
        _etat = Etat.Erreur;
        StateHasChanged();
    }

    private async Task ArreterCameraAsync()
    {
        _boucle?.Cancel();
        _boucle = null;

        if (_module is null)
        {
            return;
        }

        try
        {
            await _module.InvokeVoidAsync("arreter");
        }
        catch (JSDisconnectedException)
        {
        }
    }

    // Quitter l'écran doit libérer la caméra, pas seulement arrêter la boucle.
    public async ValueTask DisposeAsync()
    {
        await ArreterCameraAsync();

        if (_module is not null)
        {
            try
            {
                await _module.DisposeAsync();
            }
            catch (JSDisconnectedException)
            {
            }

            _module = null;
        }
    }
}
