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

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.Mixer.Info;
import javax.sound.sampled.TargetDataLine;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import net.javacomm.multilingual.Babelfish;
import net.javacomm.multilingual.MultilingualString;
import net.javacomm.multilingual.schema.ISO639;
import net.javacomm.multilingual.schema.KEY;
import io.nayuki.flac.app.EncodeWavToFlac;



/**
 * Mikrofonaufnahmen werden entgegengenommen. Das Objekt kann nur einmal
 * verwendet werden. Wird die Aufzeichnung gestoppt und soll später fortgestzt
 * werden, dann muss ein neues Objekt erzeugt werden.
 * 
 * @author llange
 *
 */
public class RecordMic implements Babelfish {

  private final static Logger log = LogManager.getLogger(RecordMic.class);
  final static int UDPPUFFER = 2304; // 3*564+576
  private Telefonformat telefonformat;
  private List<RecordMicListener> recMicListenerList = Collections
      .synchronizedList(new LinkedList<RecordMicListener>());
  private volatile TargetDataLine targetDataline;
  private static ExecutorService executorMic;
  private CompletableFuture<Void> future;
  private MultilingualString mikrofonnameIstFalsch = new MultilingualString(
      KEY.STRING_DER_MIKROFONNAME_IST_UNBEKANNT
  );
  private AtomicBoolean stopped = new AtomicBoolean(false);
  private AtomicBoolean isMuted = new AtomicBoolean(false);
//  private AtomicBoolean hasPermission = new AtomicBoolean(true);
  private Semaphore semaphore = new Semaphore(1);
  private LineListener lineListener = new LineListener() {

    @Override
    public void update(LineEvent event) {
      // wird vom JavaSoundEventDispatcher Thread ausgeführt
      if (event.getType().equals(LineEvent.Type.OPEN)) {}
      else if (event.getType().equals(LineEvent.Type.START)) {
        targetDataline.flush();
      }
      else if (event.getType().equals(LineEvent.Type.STOP)) {}
      else if (event.getType().equals(LineEvent.Type.CLOSE)) {
        removeAllRecordMicListener();
        targetDataline.removeLineListener(lineListener);
      }
    }
  };

  static {
    executorMic = Executors.newSingleThreadExecutor((runnable) -> {
      return new Thread(Thread.currentThread().getThreadGroup(), runnable, "Microphone");
    });
  }

  /**
   * Eine Mikrofonverbindung wird hergestellt.
   * 
   * @param mikroname
   *                  der Mirofonname
   * @param value
   *                  das verwendete Telefonformat
   * 
   * @throws MicronameException
   *                            das Mikrofon ist unter diesem Namen nicht bekannt
   * @throws MikrofonException
   *                            die Mikrofonleitung wird von einem anderen
   *                            Mikrofon blockiert
   */
  public RecordMic(String mikroname, Telefonformat value) throws MicronameException, MikrofonException {
    telefonformat = value;
    List<Info> mixer = TelefonUtil.microMixer(telefonformat);
    for (Info tmp : mixer) {
      if (tmp.getName().equals(mikroname)) {
        if (log.isDebugEnabled()) log.debug("Mikro=" + mikroname);
        try {
          targetDataline = TelefonUtil.getTargetDataLine(tmp, telefonformat);
        }
        catch (LineUnavailableException e) {
          log.error(e.getMessage(), e.getCause());
          throw new MikrofonException("die Mikrofonleitung wird von einer anderen Anwendung benutzt");
        }
        targetDataline.addLineListener(lineListener);
        return;
      }
    }
    throw new MicronameException("<b>'" + mikroname + "'</b> - " + mikrofonnameIstFalsch.toString());
  }



  TargetDataLine getTargetDataLine() {
    return targetDataline;
  }



  /**
   * Öffnet eine Mikrofonleitung. Die Mikrofonleitung darf nicht belegt sein.
   * 
   * @throws MikrofonException
   *                           die Mikrofonleitung wird benutzt; schließe die
   *                           Mikrofonleitung
   */
  void open() throws MikrofonException {
    open(TelefonUtil.getAudioFormat(telefonformat));
  }



  void open(AudioFormat audioformat) throws MikrofonException {
    try {
      if (targetDataline == null) return;
      targetDataline.open(audioformat);
    }
    catch (LineUnavailableException e) {
      throw new MikrofonException("Diese Mikrofonleitung wird bereits benutzt.");
    }
  }



  /**
   * Die Mikrofonleitung wird geschlossen.
   * 
   */
  void close() {
    if (targetDataline == null) return;
    targetDataline.close();
  }



  /**
   * Der Listener empfängt die Mikrofoneingaben. Den Listener vor
   * {@link #startRecord()} registrieren.
   * 
   * @param listener
   */
  public synchronized void addRecordMicListener(RecordMicListener listener) {
    recMicListenerList.add(listener);
  }



  public synchronized void removeRecordMicListener(RecordMicListener listener) {
    recMicListenerList.remove(listener);
  }



  /**
   * Alle Listener werden implizit vor der Beendigung von {@link #startRecord()}
   * aufgerufen.
   * 
   */
  public synchronized void removeAllRecordMicListener() {
    recMicListenerList.clear();
  }



  /**
   * Die Mikrofonaufzeichnungen werden im Mikrofon-Thread gestartet.
   * 
   */
  public void startRecord() {
    if (targetDataline == null) return;

    if (future != null && !future.isDone()) return;
    future = CompletableFuture.runAsync(() -> {
      try {
        open();
        targetDataline.start();
        byte[] rawAudiobuffer = new byte[UDPPUFFER];
        int read = 0;
        while (!stopped.get()) {
          if (isMuted.get()) {
            try {
              Thread.sleep(500);
            }
            catch (InterruptedException e) {
              log.info(e.getMessage(), e);
            }
            continue;
          }
          try {
            // TCP
            semaphore.acquire();
            read = targetDataline.read(rawAudiobuffer, 0, rawAudiobuffer.length);
            byte[] waveByteArray = toWave(ByteBuffer.wrap(rawAudiobuffer, 0, read), telefonformat);
            byte[] flacArray = EncodeWavToFlac.entry(waveByteArray);
            RecordMicEvent contentEvent = new RecordMicEvent(this, flacArray, flacArray.length);
            for (RecordMicListener tmp : recMicListenerList) {
              tmp.onRecord(contentEvent);
            }
          }
          catch (Exception e1) {
            log.error(e1.getMessage(), e1);
            break;
          }
        }
        targetDataline.close();
      }
      catch (MikrofonException e) {
        throw new CompletionException(e);
      }
    }, executorMic).exceptionally(ex -> {
      log.error(ex);
      return null;
    });
  }



  /**
   * 
   * Die gerade empfangenen Audiodaten werden übergeben. Die Audiodaten stammen
   * von einem Mikrofon.
   * 
   * 
   * @param micro
   *               aufgezeichnete Mikrofonbytes
   * @param format
   *               das Telefonformat der Mikrofonbytes
   * 
   * @return WAVE-Audiodaten als ByteArray
   * 
   * @throws AudioFormatException
   *                              das Audioformat wird nicht unterstützt
   * @throws IOException
   *                              encodieren ist fehlgeschlagen
   */
  protected byte[] toWave(ByteBuffer micro, Telefonformat format) throws AudioFormatException, IOException {
    Raw2Wave raw2wave = new Raw2Wave(format);
    return raw2wave.encode(micro);
  }



  void stop() {
    stopped.set(true);
    if (targetDataline != null) targetDataline.stop();
  }



  /**
   * Die Mikrofonaufzeichnungen werden beendet. Sie können mit diesem Objekt nicht
   * fortgesetzt werden.
   * 
   */
  public void stopRecord() {
    stop();
  }



  /**
   * Der Mikrofon-Thread wird terminiert. Mikrofonaufzeichnungen sind nicht mehr
   * möglich.
   * 
   */
  public static void destroy() {
    executorMic.shutdown();
    boolean done = false;
    try {
      done = executorMic.awaitTermination(1, TimeUnit.SECONDS);
    }
    catch (InterruptedException e) {
      log.error(e.getMessage(), e.getCause());
    }
    finally {
      if (!done) executorMic.shutdownNow();
    }

  }



  /**
   * Die Mifrofonaufnahme übertragen oder nicht übertragen.
   * 
   * @param value
   *              {@code true}, die Aufnahme wird nicht übertragen
   */
  public void setMuted(boolean value) {
    isMuted.set(value);
  }



  /**
   * Mikrofonaufzeichnung darf an den Server übertragen werden.
   */
  public void enableHasPermission() {
    semaphore.release();
  }



  /**
   * Mikrofonaufzeichnung darf nicht an den Server übertragen werden.
   */
//  public void disableHasPermission() {
//    hasPermission.set(false);
//  }

  @Override
  public void setLanguage(ISO639 code) {
    mikrofonnameIstFalsch.setLanguage(code);
  }

}
