/**
 *  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.List;
import java.util.concurrent.ArrayBlockingQueue;
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;
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;



/**
 * Diese Klasse ist ein Producer für Mikrofonaufnahmen.
 */
public class MicProducer extends Thread implements Babelfish {

  private static final Logger log = LogManager.getLogger(MicProducer.class);

  private final AtomicBoolean terminate = new AtomicBoolean(false);
  private ArrayBlockingQueue<byte[]> queue;
  private Telefonformat telefonformat;
  private TargetDataLine targetDataline;
  private byte[] rawAudiobuffer;
  private MultilingualString mikrofonnameIstUnbekannt = new MultilingualString(
      KEY.STRING_DER_MIKROFONNAME_IST_UNBEKANNT
  );

  public MicProducer(ArrayBlockingQueue<byte[]> queue) {
    this(queue, 7680);// 3*564+576];
  }



  /**
   * Der Producer wird eingereichtet. Die Puffergröße sollte ein Vielfaches von
   * 576 sein. Ein zu großer Puffer erhöht die Latenzzeit.
   * 
   * @param queue
   *                        diese Queue muss mit einem Consumer gekoppelt werden
   * @param audioBufferSize
   *                        die Puffergröße ist anhängig vom Telefonformat
   */
  public MicProducer(ArrayBlockingQueue<byte[]> queue, int audioBufferSize) {
    super("MicProducer");
    this.queue = queue;
    rawAudiobuffer = new byte[audioBufferSize]; // 3*564+576];
  }



  private void cleanup() {
    if (targetDataline != null) {
      targetDataline.stop();
      targetDataline.close();
      log.info("MicProducer: TargetDataLine geschlossen.");
    }
  }



  /**
   * Öffnet eine Mikrofonleitung. Die Mikrofonleitung darf nicht belegt sein.
   * 
   * @param audioformat
   *                    dieses Audioformat
   * @throws MikrofonException
   *                           die Mikrofonleitung wird benutzt; schließe die
   *                           Mikrofonleitung
   */
  private 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.");
    }
  }



  @Override
  public void run() {
    log.info("MicProducer gestartet...");
    if (targetDataline == null) {
      log.warn("TargetDataLine ist null. Audioübertragungen nicht möglich.");
      return;
    }

    targetDataline.start();

    while (!terminate.get()) {
      try {
        int read = targetDataline.read(rawAudiobuffer, 0, rawAudiobuffer.length);
        if (read <= 0) {
          // keine Daten, kurz warten und erneut prüfen
          try {
            Thread.sleep(5);
          }
          catch (InterruptedException e) {
            if (terminate.get()) break;
            Thread.currentThread().interrupt();
          }
          continue; // Loop fortsetzen
        }

        byte[] waveByteArray = toWave(ByteBuffer.wrap(rawAudiobuffer, 0, read), telefonformat);
        byte[] flacArray = EncodeWavToFlac.entry(waveByteArray);

        // put() blockiert, wenn Queue voll -> stabiler
        queue.put(flacArray);

      }
      catch (AudioFormatException | IOException e) {
        log.error("Fehler bei Audiokonvertierung", e);
        terminate();
      }
      catch (InterruptedException e) {
        log.info("MicProducer: Thread unterbrochen -> Abbruch.");
        terminate();
        Thread.currentThread().interrupt();
        break;
      }
    }

    cleanup();
    log.info("MicProducer hat terminiert.");
  }



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



  /**
   * Diese Methode muss aufgerufen werden, bevor der Producer gestartet wird.
   * 
   * Für ein Mikrofon wird das {@link net.javacomm.client.phone.Telefonformat}
   * festgelegt. Der übergebene Mikrofonname muss gültig sein. Der Mikrofonname
   * konnte über
   * {@link net.javacomm.client.phone.TelefonUtil#microMixer(Telefonformat) }
   * ermittelt werden.
   * 
   * @param mikroname
   *                  dieser Mikrofonname
   * @param value
   *                  dieses Audioformat
   * 
   * @throws MikrofonException
   *                            für das ausgewählte Mikrofon ist die zugehörige
   *                            Leitung belegt
   * 
   * @throws MicronameException
   *                            dieser Mikrofonname ist nicht gültig
   */
  public void setTelefonformat(String mikroname, Telefonformat value)
      throws MikrofonException, MicronameException {
    telefonformat = value;

    List<Mixer.Info> mixer = TelefonUtil.microMixer(telefonformat);
    for (Mixer.Info tmp : mixer) {
      if (tmp.getName().equals(mikroname)) {
        try {
          targetDataline = TelefonUtil.getTargetDataLine(tmp, telefonformat);
        }
        catch (LineUnavailableException e) {
          log.error(e.getMessage(), e.getCause());
          throw new MikrofonException("Für dieses Mikrofon ist die Leitung belegt. - " + mikroname);
        }

        // Listener lokal definieren
        LineListener lineListener = new LineListener() {
          @Override
          public void update(LineEvent event) {
            if (event.getType() == LineEvent.Type.CLOSE) {
              targetDataline.removeLineListener(this);
            }
            else if (event.getType() == LineEvent.Type.START) {
              targetDataline.flush();
            }
          }
        };
        targetDataline.addLineListener(lineListener);
        open(TelefonUtil.getAudioFormat(telefonformat));
        return;
      }
    }
    throw new MicronameException("<b>'" + mikroname + "'</b> - " + mikrofonnameIstUnbekannt.toString());

  }



  public void terminate() {
    terminate.set(true);
    interrupt(); // falls im put() oder read() blockiert
  }



  /**
   * 
   * 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);
  }

}
