/**
 *  Copyright © 2020-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.websocket;

import static net.javacomm.protocol.HEADER.REQUEST;
import jakarta.websocket.ClientEndpoint;
import jakarta.websocket.CloseReason;
import jakarta.websocket.ContainerProvider;
import jakarta.websocket.DeploymentException;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnError;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.WebSocketContainer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReentrantLock;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import net.javacomm.client.websocket.WebsocketEvent.Event;
import net.javacomm.protocol.CONFERENCEAUDIO;
import net.javacomm.protocol.Command;
import net.javacomm.protocol.MESSAGE;
import net.javacomm.protocol.MessageDecoder;
import net.javacomm.protocol.MessageEncoder;
import net.javacomm.protocol.Protocol;



@ClientEndpoint(encoders = {MessageEncoder.class}, decoders = {MessageDecoder.class})
public class WebsocketClient {

  private final Logger log = LogManager.getLogger(WebsocketClient.class);
  private List<WebsocketListener> websocketListenerList = new CopyOnWriteArrayList<>();
  private Session activeSession;
  private ReentrantLock lock;
  private ByteArrayOutputStream out = new ByteArrayOutputStream();

  public WebsocketClient() {
    lock = new ReentrantLock(true);
  }



  public void addWebsocketListener(WebsocketListener listener) {
    websocketListenerList.add(listener);
  }



  @OnClose
  public void close(Session session, CloseReason reason) {
    WebsocketEvent event = new WebsocketEvent(this, Event.ON_CLOSE);
    for (WebsocketListener listener : websocketListenerList) {
      listener.onClose(event);
    }
  }



  public Session connectToServer(String url) throws DeploymentException, IOException, URISyntaxException {
    WebSocketContainer container = ContainerProvider.getWebSocketContainer();
    container.setDefaultMaxTextMessageBufferSize(32768);
    container.setDefaultMaxBinaryMessageBufferSize(32768);
    container.setAsyncSendTimeout(2000);
    Session sessionToSend = container.connectToServer(this, new URI(url));
    setSessionToSend(sessionToSend);
    return sessionToSend;
  }



  @OnError
  public void error(Session session, Throwable throwable) {
    WebsocketEvent event = new WebsocketEvent(this, Event.ON_ERROR);
    for (WebsocketListener listener : websocketListenerList) {
      listener.onError(event);
    }
  }



  @OnOpen
  public void onOpen(Session value) {
    WebsocketEvent event = new WebsocketEvent(this, Event.ON_OPEN);
    for (WebsocketListener listener : websocketListenerList) {
      listener.onOpen(event);
    }
  }



  @OnMessage
  public void receiveBinary(Session peer, byte[] data) {

    // Finde Start des FLAC-Headers
    int flacStart = -1;
    for (int i = 0; i < data.length - 3; i++) {
      if (data[i] == 0x66 && data[i + 1] == 0x4C && data[i + 2] == 0x61 && data[i + 3] == 0x43) {
        flacStart = i;
        break;
      }
    }
    if (flacStart == -1) {
      // Kein FLAC Header gefunden → Paket verwerfen
      return;
    }
    // SessionID = alles vor FLAC
    byte[] sessionIdBytes = Arrays.copyOfRange(data, 0, flacStart);
    byte[] flacData = Arrays.copyOfRange(data, flacStart, data.length);

//    ByteArrayInputStream in = new ByteArrayInputStream(data, 0, 8);
//    byte[] userid = in.readAllBytes();
//    ByteArrayInputStream flac = new ByteArrayInputStream(data, 8, data.length);
//    byte[] result = flac.readAllBytes();

    CONFERENCEAUDIO message = new CONFERENCEAUDIO();
    message.setCommand(Command.CONFERENCEAUDIO);
    message.setHeader(REQUEST);
    message.setDataset(Protocol.DATASET);
    message.setUserid(new String(sessionIdBytes));
    message.setContent(flacData);

    WebsocketEvent event = new WebsocketEvent(this, message);
    for (WebsocketListener listener : websocketListenerList) {
      listener.onMessage(event);
    }

  }



  @OnMessage
  public void receiveMessage(Session peer, MESSAGE message) {
    WebsocketEvent event = new WebsocketEvent(this, message);
    for (WebsocketListener listener : websocketListenerList) {
      listener.onMessage(event);
    }
  }



  public void removeWebsocketListener(WebsocketListener listener) {
    websocketListenerList.remove(listener);
  }



  /**
   * Sendet FLAC-Audiodaten aus einem CONFERENCEAUDIO-Objekt als Binary-Frame.
   * Stream-ID wird direkt aus CONFERENCEAUDIO.getStreamId() genommen.
   *
   * @param streamId
   *                 diese id
   * @param content
   *                 diese FLAC Audiodaten
   * 
   * @return {@code true}, Binärstrom konnte versendet werden
   */
  public boolean sendConferenceAudioBinary(String streamId, byte[] content) {
    try {
      lock.lock();
      out.reset();
      out.writeBytes(streamId.getBytes());
      out.write(content);
      activeSession.getAsyncRemote().sendBinary(ByteBuffer.wrap(out.toByteArray()));
      return true;
    }
    catch (Exception e) {
      log.error("Fehler beim Senden von Binary-ConferenceAudio", e);
      return false;
    }
    finally {
      lock.unlock();
    }
  }



  public boolean sendMessage(MESSAGE message) {
    if (log.isDebugEnabled()) log.debug("Command=" + message.getCommand() + "|" + message.getHeader());
    try {
      lock.lock();
      if (activeSession == null || !activeSession.isOpen()) {
        log.warn("WebSocket session ist bereits geschlossen – Befehl {} wird verworfen", message.getHeader());
        return false;
      }
      activeSession.getAsyncRemote().sendObject(message);
      return true;
    }
    catch (Exception e) {
      log.error("Command=" + message.getCommand() + "|" + message.getHeader());
      log.error(e.getMessage(), e.getCause());
      return false;
    }
    finally {
      lock.unlock();
    }
  }



  void setSessionToSend(Session value) {
    lock.lock();
    activeSession = value;
    lock.unlock();
  }



  public void shutdown() {
    lock.lock();
    try {
      if (activeSession == null) return;
      try {
        activeSession.close();
      }
      catch (IOException e) {
        log.error(e.getMessage(), e.getCause());
      }
    }
    finally {
      lock.unlock();
    }
  }

}
