/**
 *  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.HTTP;
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 java.io.IOException;
import java.text.DecimalFormat;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
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.media.multipart.FormDataMultiPart;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import net.javacomm.client.screencaster.FrameBuffer.FrameData;
import net.javacomm.restserver.Wrapper;
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 extends Thread {

  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 userid;
  private volatile CopyOnWriteArrayList<String> receiverlist;
  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 userid
   *                    dieser Anwender hat das Bild abgeschickt
   * @param receiverIds
   *                    an diese Empfängersessions wird gesendet
   */
  SendPicture(FrameBuffer buffer, AtomicLong timestamp, String userid, List<String> receiverIds) {
    this.buffer = buffer;
    lastTakenProductionTimestamp = timestamp;
    this.userid = userid;
    this.receiverlist = new CopyOnWriteArrayList<>(receiverIds);
  }



  /**
   * 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();
    client.register(MultiPartFeature.class);

    try {
      while (!Thread.currentThread().isInterrupted()) {
        FrameData data = buffer.takeFrame();
        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();
        sendToTomcat(data.bytes);
        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 (InterruptedException 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(HTTP).append(DOMAIN).append("/javacommserver/screenshot/save");
    try(FormDataMultiPart multipart = new FormDataMultiPart()) {
      multipart.field("userid", userid);
      multipart.field("receivers", Wrapper.toString(receiverlist));
      multipart.field("image", picture, MediaType.APPLICATION_OCTET_STREAM_TYPE);
      client.target(url.toString()).request(MediaType.TEXT_PLAIN)
          .post(Entity.entity(multipart, multipart.getMediaType()), Boolean.class);
    }
    catch (IOException e) {
      log.error(e.getMessage(), e);
    }
  }



  /**
   * Eine neue Empfängerliste wird übergeben.
   * 
   * @param newReceiverlist
   *                        alles Empfängersessions
   */
  public void setReceiverlist(List<String> newReceiverlist) {
    receiverlist = new CopyOnWriteArrayList<>(newReceiverlist);

  }
}