/**
 *  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.filetransfer;

import jakarta.websocket.DecodeException;
import jakarta.websocket.EncodeException;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import net.javacomm.client.environment.Environment;
import net.javacomm.protocol.MESSAGE;
import net.javacomm.protocol.MessageDecoder;
import net.javacomm.protocol.MessageEncoder;

public class SocketClient implements Runnable {

  
  private final static Logger log = LogManager.getLogger(SocketClient.class);
  private final static int BUFFERSIZE = 8192;

  private Socket socket;
  private BufferedWriter outBuffer;
  private BufferedReader inBuffer;
  private List<TCPListener> tcpListener = Collections.synchronizedList(new LinkedList<TCPListener>());
  private boolean hangup;
  

  private int port;
  private String host;
  
  
  
  SocketClient() {
    this(false);
  }
  
  
  /**
   * 
   * 
   * 
   * @param hangup {@code true}, der Listener wird entfernt, wenn der Socket geschlossen wird
   */
  SocketClient(boolean hangup) {
    setHangUp(hangup);
  }
  
  
  /**
   * 
   * @param host Host
   * @param port Port
   */
  public SocketClient(String host, int port) {
    this();
    setHost(host);
    setPort(port);
  }
  
  
  /**
   * Eine IP-Adresse oder Domain.
   * 
   * @param value Host
   */
  public void setHost(String value) {
    host = value;
  }
  
  
  /**
   * 
   * 
   * @param value
   */
  public void setPort(int value) {
    port = value;
  }
  
  
  public int getPort() {
    return port;
  }
  
  
  public String getHost() {
    return host;
  }
  
  
  
  
  
  /**
   * Ein Verbindungsaufbau zum Server wird unternommen.
   * Diese Methode erst nach {@link #addTCPListener(TCPListener)} aufrufen.
   * 
   * 
   * @param host Name oder IPv4
   * @param port der Serverport
   * @throws UnknownHostException IP-Adresse oder Host konnte nicht gefunden werden
   * @throws IOException generischer I/O-Fehler
   */
  private void connect(String host, int port) {
    try {
      if (!Environment.PRODUCTION_MODE) {
        host = "localhost";
        this.setHost(host);
      }
      log.info("Host/Port) -----> ("+host+ "/" + port +")");
      socket = new Socket(host, port);
      socket.setReceiveBufferSize(BUFFERSIZE);
      socket.setSendBufferSize(BUFFERSIZE);
      inBuffer = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"), BUFFERSIZE); // nicht in run() TimeRacing
      outBuffer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), BUFFERSIZE); // nicht in run() TimeRacing
      TCPEvent event = new TCPEvent(SocketClient.this);
      for (TCPListener listener: tcpListener) {
        listener.onOpen(event);
      }
    }
    catch (IOException e) {
      log.error(e.getMessage(), e.getCause());
      TCPEvent event = new TCPEvent(SocketClient.this, e.getMessage());
      for (TCPListener listener: tcpListener) {
        listener.onError(event);
      }
      if (isHangUp()) tcpListener.removeAll(tcpListener);
    }
  }
  
  

  /**
   * Die Task nimmt den Verbindungsaufbau zum Server vor. Ein Verbindungsaufbau
   * kann sich hinziehen, wenn der Server nicht erreichbar ist. Aus diesem Grund
   * wird der Verbindungsaufbau in eine eigene Task ausgelagert. Bevor die Task
   * ausgeführt wird, die Methode {@link #addTCPListener(TCPListener)} aufrufen.
   * 
   * 
   * @param host
   *             der Server ist eine IP-Adresse oder ein Name
   * @param port
   *             der Port, auf dem der Server horcht.
   * 
   * @return die Task wird von einem Executor ausgeführt
   */
  Runnable task(String host, int port) {

    Runnable run = () -> {
      try {
        log.info("Host/Port) -----> (" + host + "/" + port + ")");
        socket = new Socket(host, port);
        socket.setReceiveBufferSize(BUFFERSIZE);
        socket.setSendBufferSize(BUFFERSIZE);
        inBuffer = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"), BUFFERSIZE); // nicht
                                                                                                            // in
                                                                                                            // run()
                                                                                                            // TimeRacing
        outBuffer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8"), BUFFERSIZE); // nicht
                                                                                                               // in
                                                                                                               // run()
                                                                                                               // TimeRacing
        TCPEvent event = new TCPEvent(SocketClient.this);
        for (TCPListener listener : tcpListener) {
          listener.onOpen(event);
        }
      }
      catch (IOException e) {
        log.error(e.getMessage(), e.getCause());
        TCPEvent event = new TCPEvent(SocketClient.this, e.getMessage());
        for (TCPListener listener : tcpListener) {
          listener.onError(event);
        }
        if (isHangUp()) tcpListener.removeAll(tcpListener);
      }

    };

    return run;
  }



  /**
   * Ein Verbindungsaufbau zum Server wird unternommen.
   * Diese Methode erst nach {@link #addTCPListener(TCPListener)} aufrufen.
   * 
   * 
   */
  private void connect() {
    connect(getHost(), getPort());
  }
  
  
  /**
   * Entferne alle Listener nach einem OnError oder OnClose.
   * 
   * 
   * @param value {@code true}, entferne alle Listener
   */
  public void setHangUp(boolean value) {
    hangup = value;
  }
  
  
  /**
   * Sollen alle Listener am Ende der Task automatisch geschlossen werden?
   * 
   * @return {@code true}, alle Listener werden automatisch entfernt
   */
  public boolean isHangUp() {
    return hangup;
  }
  
  
  
  /**
   * Andere Objekte können sich registrieren, um Clientereignisse abzuhören. Wenn
   * {@literal hangup} true ist, werden alle Listener nach einem onError oder
   * onClose entfernt.
   * 
   * 
   * @param listener
   *                 hört Ereignisse ab
   * @param hangUp
   *                 {@code true}, automtisch deregistrieren
   */
  public synchronized void addTCPListener(TCPListener listener) {
    tcpListener.add(listener);
  }
  

  
  /**
   * Ein Objekt möchte nicht mehr über Netwerkaktivitäten benachrichtigt werden.
   * 
   * @param listener
   *                 hörte Ereignisse ab
   */
  public synchronized void removeTCPListener(TCPListener listener) {
    tcpListener.remove(listener);
  }

  
  
  public void send(MESSAGE message) throws EncodeException, UnsupportedEncodingException {
    MessageEncoder encoder = new MessageEncoder();

    String protocol = encoder.encode(message);
    send(protocol);
  }
  
  
  private void send(String str)  {
    if (log.isDebugEnabled()) log.debug(str);
    try  {
      outBuffer.write(str);
      outBuffer.newLine();
      outBuffer.flush();
    }
    catch (IOException e)  {
      log.warn(e.getMessage());
    }
  }

  
  
  @Override
  public void run() {
    try {
      while(true) {
        String incoming = inBuffer.readLine();
        if (incoming==null) break;
        parse(incoming);
      }
    }
    catch (UnsupportedEncodingException e) {
      log.error(e.getMessage(), e.getCause());
    }
    catch (IOException e1) {
      log.warn(e1.getMessage(), e1.getCause());
    }
  }
  
  

  synchronized void parse(String incoming) {
    MessageDecoder decoder = new MessageDecoder();
    try {
      MESSAGE message = decoder.decode(incoming);
      TCPEvent event = new TCPEvent(SocketClient.this, message);
      for (TCPListener listener: tcpListener) {
        listener.onMessage(event);
      }
    }
    catch (DecodeException e) {
      log.error(e.getMessage(), e.getCause());
      TCPEvent event = new TCPEvent(SocketClient.this, e.getMessage());
      for (TCPListener listener: tcpListener) {
        listener.onError(event);
      }
    }
  }
  
  
  
  /**
   * Der Client schliesst den Socket und baut die Verbindung zum Server ab.
   * 
   */
  void disconnect() {
    try {
      if (socket==null) return;
      while(!socket.isClosed()) {
        if (log.isDebugEnabled()) log.debug("checkClosed");
        socket.close();
      }
      if (log.isDebugEnabled()) log.debug("client socket closed");
    }
    catch (IOException e) {
      log.warn(e.getMessage(), e.getCause());
    }
    finally {
      try {
        inBuffer.close();
      }
      catch (Exception e) {
        log.error(e.getMessage(), e.getCause());
      }
      try {
        outBuffer.close();
      }
      catch (Exception e) {
        log.error(e.getMessage(), e.getCause());
      }
      TCPEvent event = new TCPEvent(SocketClient.this);
      for (TCPListener listener: tcpListener) {
        log.info("benachrichtige onClose()");
        listener.onClose(event);
      }
      if (isHangUp()) tcpListener.removeAll(tcpListener);
    }
  }
  

  
  void hardDisconnect() {
    try {
      if (socket==null) return;
      while(!socket.isClosed()) {
        if (log.isDebugEnabled()) log.debug("checkClosed");
        socket.close();
      }
      if (log.isDebugEnabled()) log.debug("client socket closed");
    }
    catch (IOException e) {
      log.warn(e.getMessage(), e.getCause());
    }
    try {
      inBuffer.close();
    }
    catch (Exception e) {
      log.error(e.getMessage(), e.getCause());
    }
    try {
      outBuffer.close();
    }
    catch (Exception e) {
      log.error(e.getMessage(), e.getCause());
    }
    finally {
      if (isHangUp()) tcpListener.removeAll(tcpListener);
    }
  }

  
  
}






