/**
 *  Copyright © 2025, Luis Andrés Lange <https://javacomm.net>
 *
 *  This Source Code Form is subject to the terms of the Mozilla Public
 *  License, v. 2.0. If a copy of the MPL was not distributed with this
 *  file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 *  ----------------------------------------------------------------------------
 *
 *  Exhibit B - "Incompatible With Secondary Licenses" Notice
 *
 *  This Source Code Form is "Incompatible With Secondary Licenses",
 *  as defined by the Mozilla Public License, v. 2.0.
 *
 *  In short:
 *  - This file may be used, modified, and distributed under MPL 2.0 only.
 *  - It may NOT be relicensed under GPL, LGPL, AGPL, or any other Secondary License.
 *
 *  Rationale:
 *  - Ensures that the code remains MPL-2.0.
 *  - Avoids legal conflicts with GPL-licensed libraries (e.g., VideoLAN).
 *  - Maximizes usability for commercial and security-critical applications.
 *
 */
package net.javacomm.client.screencaster;

import static net.javacomm.share.Constants.DOMAIN;
import static net.javacomm.share.Constants.HTTPS;
import jakarta.ws.rs.ProcessingException;
import jakarta.ws.rs.client.Client;
import jakarta.ws.rs.client.ClientBuilder;
import jakarta.ws.rs.client.Entity;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.concurrent.atomic.AtomicLong;
import javax.swing.SwingUtilities;
import javax.swing.event.EventListenerList;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.glassfish.jersey.client.ClientProperties;
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import net.javacomm.client.screencaster.FrameBuffer.FrameData;
import net.jpountz.xxhash.XXHash64;
import net.jpountz.xxhash.XXHashFactory;



/**
 * Consumer: sendet das erhaltene Frame per HTTP (Jersey) an den Server. Loggt
 * geglättete FPS (EMA).
 */
public class SendPicture implements Runnable {

  private static final XXHashFactory FACTORY = XXHashFactory.fastestJavaInstance();
  private static final XXHash64 XX64 = FACTORY.hash64();
  private static final int SEED = 0; // deterministisch für Bildvergleich
  private final static Logger log = LogManager.getLogger(SendPicture.class);
  private final FrameBuffer buffer;
  private final AtomicLong lastTakenProductionTimestamp;
  private Client client;
  private final String pseudoid;
  private volatile String conferencekey;
  private long lastHash = -1; // bisher kein Screenshot gesendet

  // FPS-Berechnung (EMA)
  private long lastSendNanos = -1L;
  private double emaFps = -1.0;
//  Nur 18 % des neuen Wertes fließen sofort in den Durchschnitt ein.
//  82 % stammen vom alten Wert.
//  Ergebnis: sehr ruhige, aber träge Anzeige (dauert länger, bis sich Sprünge bei FPS bemerkbar machen).
  private final double emaAlpha = 0.18; // Glättungsfaktor (0..1), kleiner = stärker geglättet
  private final DecimalFormat df = new DecimalFormat("#0.00");
  private EventListenerList listenerList = new EventListenerList();

  /**
   * 
   * @param buffer
   * @param timestamp
   * @param pseudoid
   *                    dieser Anwender hat das Bild abgeschickt
   * @param receiverIds
   *                    an diese Empfängersessions wird gesendet
   */
  SendPicture(FrameBuffer buffer, AtomicLong timestamp, String pseudoid, String conferencekey) {
    this.buffer = buffer;
    this.conferencekey = conferencekey;
    lastTakenProductionTimestamp = timestamp;
    this.pseudoid = pseudoid;
  }



  /**
   * Fügt einen FpsListener hinzu
   */
  public void addFpsListener(FpsListener listener) {
    listenerList.add(FpsListener.class, listener);
  }



  /**
   * Der AWT-Thread feuert dieses Ereignis
   * 
   * @param fps
   *            Bilder pro Sekunde
   */
  private void fireFpsUpdated(double fps) {
    FpsEvent event = new FpsEvent(this, fps); // this = SendPicture als Quelle
    for (FpsListener listener : listenerList.getListeners(FpsListener.class)) {
      SwingUtilities.invokeLater(() -> listener.fpsUpdated(event));
    }
  }



  /**
   * Entfernt einen FpsListener
   */
  public void removeFpsListener(FpsListener listener) {
    listenerList.remove(FpsListener.class, listener);
  }



  @Override
  public void run() {
    client = ClientBuilder.newClient().property(ClientProperties.READ_TIMEOUT, 2000)
        .property(ClientProperties.CONNECT_TIMEOUT, 3000);
    client.register(MultiPartFeature.class);

    try {
      // Thread.currentThread().isInterrupted() funktioniert auch bei Virtual Threads
      // perfekt
      while (!Thread.currentThread().isInterrupted()) {
        FrameData data = buffer.takeFrame(); // Blockiert billig im RAM
        if (data == null || data.bytes == null) {
          log.warn("Consumer: kein Frame erhalten (null)");
          continue;
        }

        // Markiere, dass Consumer dieses Produktions-Timestamp jetzt verarbeitet hat.
        lastTakenProductionTimestamp.set(data.timestampNanos);

        // xxHash berechnen
        long hash = XX64.hash(data.bytes, 0, data.bytes.length, SEED);
        if (hash == lastHash) {
          log.info("Screenshot identisch – nicht gesendet");
          continue;
        }
        lastHash = hash;

        // Senden (kann blockieren / Netzwerkkosten verursachen)
        long t0 = System.nanoTime();
        try {
          sendToTomcat(data.bytes); // Blockierender Netzwerk-Call fängt Carrier-Thread ab
          // connected = true
        }
        catch (ProcessingException e) {
//          if (!connected) {
//            // Erstmaliger Verbindungsaufbau gescheitert → Anwender informieren
//            log.error(
//                "Verbindungsaufbau fehlgeschlagen: {}", cause != null ? cause.getMessage() : e.getMessage()
//            );
//            fireConnectionFailed(cause); // ← Event an die UI
//            return; // Thread beenden – kein Retry sinnvoll
//          }
        }
        long t1 = System.nanoTime();
        double sendMillis = (t1 - t0) / 1_000_000.0;

        // FPS-Berechnung anhand tatsächlicher Sendezeitpunkte (System.nanoTime)
        long now = System.nanoTime();
        if (lastSendNanos > 0) {
          double instantFps = 1_000_000_000.0 / (now - lastSendNanos);
          if (emaFps < 0) {
            emaFps = instantFps;
          }
          else {
            emaFps = emaAlpha * instantFps + (1.0 - emaAlpha) * emaFps;
          }
          fireFpsUpdated(emaFps);

          if (log.isDebugEnabled()) {
            log.debug(
                "Consumer: Frame {} versendet | {} FPS | HTTP-Senden: {} ms", data.number, df.format(emaFps),
                String.format("%.2f", sendMillis)
            );
          }
        }
        else {
          if (log.isDebugEnabled()) log.debug("Consumer: Frame {} versendet | (Initial)", data.number);
        }
        lastSendNanos = now;
      }
    }
    catch (Exception e) {
      log.info("Consumer sauber beendet");
    }
    finally {
      client.close();
    }
  }



  /**
   * Sende den Screenshot an den JavacommServer.
   */
  private void sendToTomcat(byte[] picture) {
    if (picture == null) {
      log.warn("Kein Frame vorhanden – nichts gesendet");
      return;
    }

    StringBuilder url = new StringBuilder(HTTPS).append(DOMAIN).append("/javacommserver/screenshot/save");
    try(FormDataMultiPart multipart = new FormDataMultiPart()) {
      multipart.field("pseudoid", pseudoid);
      multipart.field("image", picture, MediaType.APPLICATION_OCTET_STREAM_TYPE);
      multipart.field("conferencekey", conferencekey);
      try(
        Response response = client.target(url.toString()).request(MediaType.TEXT_PLAIN)
            .post(Entity.entity(multipart, multipart.getMediaType()))
      ) {}
    }
    catch (IOException e) {
      log.error(e.getMessage(), e);
    }
  }

}