/**
 *  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 static net.javacomm.protocol.HEADER.ERROR;
import static net.javacomm.protocol.HEADER.REQUEST;
import static net.javacomm.protocol.HEADER.RESPONSE;
import jakarta.websocket.EncodeException;
import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Base64;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import net.javacomm.client.environment.Environment;
import net.javacomm.client.filetransfer.ChannelEvent.Status;
import net.javacomm.protocol.Command;
import net.javacomm.protocol.FTSDOWNLOAD;
import net.javacomm.protocol.MESSAGE;
import net.javacomm.protocol.Protocol;
import net.javacomm.protocol.TRANSFER;



/**
 * Ein Channel ist ein Übertragungskanal für eine einzige Datei. Nach der
 * Übertragung kann der Channel gelöscht werden. Der Channel ist für eine
 * weitere Übertragung nicht mehr zu gebrauchen.
 * 
 * @author llange
 *
 */
public class Channel {

  /**
   * Der SocketClient benachrichtigt den Channel über Ereignisse auf dem Port.
   * 
   * @author llange
   *
   */
  class TcpAction extends TCPEventAdapter {
    @Override
    public void onClose(TCPEvent event) {
      synchronized (this) {
        semaphor = 0;
      }
      ChannelEvent channelEvent = new ChannelEvent(Channel.this, Status.CLOSED);
      for (ChannelListener listener : channelListener) {
        listener.onClosed(channelEvent);
      }
      channelListener.removeAll(channelListener);
    }



    @Override
    public void onError(TCPEvent event) {
      synchronized (this) {
        semaphor = 0;
      }
      event.getErrorMessage(); // weiterleiten an die Außenwelt
      ChannelEvent channelEvent = new ChannelEvent(Channel.this, Status.CANCELED, 0, event.getErrorMessage());
      for (ChannelListener listener : channelListener) {
        listener.onError(channelEvent);
      }
    }



    @Override
    public void onMessage(TCPEvent event) {
      synchronized (this) {
        semaphor = 0;
      }

      Runnable runnable = () -> {
        parse(event.getMessage());
      };
      try {
        executorClient.execute(runnable);
      }
      catch (RejectedExecutionException e) {
        log.warn(
            "Die Task konnte nicht ausgeführt werden, weil der Executor heruntergefahren wurde. Alles ok."
        );
      }
    }



    @Override
    public void onOpen(TCPEvent event) {
      try {
        ChannelEvent channelEvent = new ChannelEvent(this, Status.STARTED, 0);
        for (ChannelListener listener : channelListener) {
          listener.onStarted(channelEvent);
        }

        // serverPresent ist für JUnit-Tests
        Runnable serverPresent = () -> {
          while (semaphor == 0) {
            semaphor = semaphor + 1;
            try {
              Thread.sleep(10000);
            }
            catch (InterruptedException e) {
              log.error(e.getMessage(), e.getCause());
            }
          }
          softClosing();
        };
        executorClient.execute(serverPresent);

        executorClient.execute(socketClient);

        Runnable runnable = () -> {

          FTSDOWNLOAD ftsdownload = new FTSDOWNLOAD();
          ftsdownload.setCommand(Command.FTSDOWNLOAD);
          ftsdownload.setHeader(REQUEST);
          ftsdownload.setDataset(Protocol.DATASET);
          ftsdownload.setFilename(slot.getFilename());
          ftsdownload.setPath(slot.getSourcepath());
          ftsdownload.setSlot(slot.getSlotnumber());
          try {
            socketClient.send(ftsdownload);
          }
          catch (EncodeException | UnsupportedEncodingException e) {
            log.warn(e.getMessage(), e.getCause());
          }
        };
        executorClient.execute(runnable);
      }
      catch (RejectedExecutionException e) {
        log.warn(
            "Die Task konnte nicht ausgeführt werden, weil der Executor heruntergefahren wurde. Alles ok."
        );
      }
    }



    /**
     * Eine Nachricht für den File Transfer Client.
     * 
     * @param message
     *                eine Botschaft
     */
    void parse(MESSAGE message) {
      if (Command.TRANSFER.equals(message.getCommand())) {
        if (message instanceof final TRANSFER transfer) {
          if (RESPONSE == message.getHeader()) {
            if (transfer.getEndflag()) {
              ChannelEvent event = new ChannelEvent(Channel.this, Status.FINISHED, 100);
              for (ChannelListener listener : channelListener) {
                listener.onFinished(event);
              }
              return;
            }
            Path destination = Paths.get(slot.getDownloadDir(), transfer.getFilename());
            if (transfer.getBlockindex() == 0) {
              // bestehende Datei löschen
              try {
                Files.deleteIfExists(destination);
              }
              catch (IOException e) {
                log.error(e.getMessage(), e.getCause());
                return;
              }
            }

            // muss in das downloadverzeichnis kopiert werden
            // Downloaddir aus filetransferservice
            // payload muss decoded werden

            byte[] decoded = Base64.getDecoder().decode(transfer.getPayload());

            final int attempts = 3;
            int index = 0;
            boolean hasAppended = false;
            do {
              hasAppended = writeAppend(destination, decoded);
              if (hasAppended) break;
              index++;
              try {
                Thread.sleep(1000);
              }
              catch (InterruptedException e) {
                log.error(e.getMessage(), e.getCause());
              }
            }
            while (index < attempts);
            if (index >= attempts) {
//              log.error("Die Datenübertragung wurde abgebrochen.");
//              log.error(transfer.getFilename());
//              log.error("--------");
              ChannelEvent event = new ChannelEvent(
                  Channel.this, Status.CANCELED, transfer.getBlockindex() * 100 / transfer.getMaxDatablocks(),
                  "Die Übertragung wird abgebrochen von " + transfer.getFilename()
              );
              for (ChannelListener listener : channelListener) {
                listener.onFinished(event);
              }
              return;
            }
            else if (index > 0) {
              log.info("recovered, " + destination.toAbsolutePath().toString());
            }

            // nächste Anfrage bis Ende

            // log.info("maxdatablocks=" + transfer.getMaxDatablocks());
            if (transfer.getMaxDatablocks() == 0) {
              log.error("MAXDATABLOCKS is zero");
              return;
            }
            ChannelEvent event = new ChannelEvent(
                Channel.this, Status.PROGRESSED, transfer.getBlockindex() * 100 / transfer.getMaxDatablocks()
            );
            for (ChannelListener listener : channelListener) {
              listener.onProgressed(event);
            }
            TRANSFER request = new TRANSFER();
            request.setCommand(Command.TRANSFER);
            request.setHeader(REQUEST);
            request.setDataset(Protocol.DATASET);
            request.setBlockindex(transfer.getBlockindex() + 1);
            request.setFilename(transfer.getFilename());
            request.setPathfile(transfer.getPathfile());
            request.setMaxDatablocks(transfer.getMaxDatablocks());
            request.setSlot(transfer.getSlot());
            try {
              socketClient.send(request);
            }
            catch (EncodeException | UnsupportedEncodingException e) {
              log.error(e.getMessage(), e.getCause());
            }
          }
        }
      }
      else if (Command.FTSDOWNLOAD.equals(message.getCommand())) {
        if (RESPONSE == message.getHeader()) {
          FTSDOWNLOAD ftsdownload = (FTSDOWNLOAD) message;
//          log.info("Blöcke anfragen und übertragen");
//          log.info("client pathfile=" + ftsdownload.getPath());
//          log.info("client filename=" + ftsdownload.getFilename());
//          log.info("client slot=" + ftsdownload.getSlot());
//          log.info("maxdatablocks=" + ftsdownload.getMaxDatablocks());
//          
//          log.info("filename=" + ftsdownload.getFilename());

          TRANSFER transfer = new TRANSFER();
          transfer.setCommand(Command.TRANSFER);
          transfer.setHeader(REQUEST);
          transfer.setDataset(Protocol.DATASET);
          transfer.setBlockindex(0);
          transfer.setFilename(ftsdownload.getFilename());
          transfer.setPathfile(ftsdownload.getPath());
          transfer.setSlot(ftsdownload.getSlot());
          transfer.setMaxDatablocks(ftsdownload.getMaxDatablocks());
          try {
            socketClient.send(transfer);
          }
          catch (EncodeException | UnsupportedEncodingException e) {
            log.error(e.getMessage(), e.getCause());
            // socketClient.disconnect();
          }
        }
        if (ERROR == message.getHeader()) {
          FTSDOWNLOAD ftsdownload = (FTSDOWNLOAD) message;
          ChannelEvent event = new ChannelEvent(
              Channel.this, Status.CANCELED, 0, ftsdownload.getErrorMessage()
          );
          for (ChannelListener listener : channelListener) {
            listener.onFinished(event);
          }
          // socketClient.disconnect();
        }
      }
    }



    /**
     * 
     * Die empfangene Datei wird zusammengebaut. Neue Datenblöcke werden am Ende der
     * Datei angehängt.
     * 
     * @param path
     *                eingehende Datei
     * @param payload
     *                der Dateiinhalt
     * @return {@code true}, der Datenblock konnte angehängt werden
     */
    public boolean writeAppend(Path path, byte[] payload) {
      boolean done = false;
      try(
        OutputStream output = Files
            .newOutputStream(path, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
        BufferedOutputStream outbuffer = new BufferedOutputStream(output, 65535);
      ) {
        outbuffer.write(payload);
        done = true;
      }
      catch (FileNotFoundException e) {
        log.warn(e.getMessage());
      }
      catch (IOException e) {
        // TOTO NoSuchFileException wenn downloaddir beispielsweise fehlr ist ein
        // CANCELED
        log.error(e);
      }
      return done;
    }

  }
  private final Logger log = LogManager.getLogger(Channel.class);
  private Slot slot;
  private int port;
  private String host;
  private SocketClient socketClient;
  private ExecutorService executorClient;
  private List<ChannelListener> channelListener = Collections
      .synchronizedList(new LinkedList<ChannelListener>());

  private int semaphor = 0;



  public Channel() {
    /**
     * Ein Thread für eingehende Nachrichten Ein Thread für ausgehende Nachrichten.
     * Ein Thread für das Testen von ServerPresent
     * 
     */
    executorClient = Executors.newFixedThreadPool(3);
  }



  /**
   * Der Listener informiert über alle Serveraktivitäten
   * 
   * @param listener
   *                 ein Abhörer
   */
  public synchronized void addChannelListener(ChannelListener listener) {
    channelListener.add(listener);
  }



  /**
   * Verbinde dich mit dem Server. Vor dem Aufruf der Methode sollte
   * {@link #addChannelListener(ChannelListener)} aufgerufen werden, um über alle
   * Serveraktivitäten informiert zu werden.
   * 
   * @throws ChannelException
   *                          der Kanal wird schon für eine Übertragung verwendet
   */
  public void connectToServer() throws ChannelException {
    if (socketClient != null) {
      throw new ChannelException("channel is in used");
    }
    socketClient = new SocketClient(true);
    socketClient.addTCPListener(new TcpAction());
    if (!Environment.PRODUCTION_MODE) {
      setHost("localhost");
    }
    socketClient.setHost(getHost());
    socketClient.setPort(getPort());
    executorClient.execute(socketClient.task(socketClient.getHost(), socketClient.getPort()));
//    socketClient.connect();
  }



  /**
   * Wie lautet der Hostname?
   * 
   * @return ein Name oder eine IP
   */
  public String getHost() {
    return host;
  }



  /**
   * Der Serverport liegt im Bereich [1025..65535].
   * 
   * @return der Serverport
   */
  public int getPort() {
    return port;
  }



  public Slot getSlot() {
    return slot;
  }



  /**
   * Der Stecker wurde gezogen. Bei einem hardClosing wird im Gegensatz zu einem
   * softClosing der oder die Abhörer nicht benachrichtigt. Dadurch werden
   * mögliche Interruptrekursionen verhindert. Alles soll heruntergefahren werden.
   * 
   * 
   */
  public void hardClosing() {
    if (executorClient.isTerminated()) return;
    executorClient.shutdown();
    boolean done = false;
    try {
      done = executorClient.awaitTermination(1, TimeUnit.SECONDS);
    }
    catch (InterruptedException e) {
      log.info(e);
    }
    if (!done) {
      executorClient.shutdownNow();
    }
    socketClient.hardDisconnect();
  }



  public synchronized void removeChannelListener(ChannelListener listener) {
    channelListener.remove(listener);
  }



  /**
   * Der Host ist ein Domainname oder eine IP-Adresse.
   * 
   * @param host
   *             ein Name oder eine IP
   */
  public void setHost(String host) {
    this.host = host;
  }



  /**
   * Der Serverport wird festgelegt.
   * 
   * @param port
   *             ein Port im Bereich [1025..65535]
   */
  public void setPort(int port) {
    this.port = port;
  }



  public void setSlot(Slot slot) {
    this.slot = slot;
  }

  ///////////////////////////////////////////////////////////////////////////////////

  /**
   * Der Channel wird geschlossen. Anschließend kann der Channel nicht mehr für
   * die Übetragung genutzt werden.
   */
  public synchronized void softClosing() {
    if (executorClient.isTerminated()) return;
    socketClient.disconnect();
    executorClient.shutdown();
    boolean done = false;
    try {
      done = executorClient.awaitTermination(1, TimeUnit.SECONDS); // von 2 auf eine Sekunde gesenkt
    }
    catch (InterruptedException e) {
      log.info(e);
    }
    if (!done) {
      executorClient.shutdownNow();
    }
  }

}
