/**
 *  Copyright © 2021-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.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.sound.sampled.AudioFormat;



/**
 * Mit dieser Klasse kann eine PCM-Audiodatei in eine WAVE-Datei konvertiert
 * werden.
 * 
 * @author llange
 *
 */
public class Raw2Wave {

  private final static Charset CHARACTER_SET = Charset.forName("ISO-8859-1");
  private Path rawFile;
  private short channels;
  private int sampleRate;
  private int byteRate;
  private short frameSize;
  private short sampleSizeInBits;



  /**
   * Der Enum-Typ Telefonformat enthält gültige Audioformate für Telefonaufnahmen.
   * Ein Audioformat muss immer die Attribute PCM_SIGNED und LITTLE_ENDIAN
   * enthalten.
   * 
   * @param value
   *              ein Telefonformat
   * @throws AudioFormatException
   *                              in dieses Audioformat wird nicht konvertiert
   */
  public Raw2Wave(Telefonformat value) throws AudioFormatException {
    this(TelefonUtil.getAudioFormat(value));
  }



  /**
   * Das Audioformat muss immer die Attribute PCM_SIGNED und LITTLE_ENDIAN
   * enthalten.
   * 
   * 
   * @param audioformat
   *                    das Audioformat
   * @throws AudioFormatException
   *                              in dieses Audioformat wird nicht konvertiert
   */
  public Raw2Wave(AudioFormat audioformat) throws AudioFormatException {
    if (audioformat.isBigEndian()) throw new AudioFormatException("BIG_ENDIAN ist nicht erlaubt");
    if (audioformat.getEncoding() != AudioFormat.Encoding.PCM_SIGNED)
      throw new AudioFormatException(audioformat.getEncoding() + " ist nicht erlaubt");
    channels = (short) audioformat.getChannels();
    sampleRate = (int) audioformat.getSampleRate();
    sampleSizeInBits = (short) audioformat.getSampleSizeInBits();
    frameSize = (short) (channels * sampleSizeInBits / 8);
    byteRate = sampleRate * frameSize;
  }




  /**
   * Wandle eine Raw-Datei in eine WAVE-Datei um.
   * 
   * 
   * 
   * @param inputfile
   *                   (*.raw)
   * @param outputfile
   *                   (*.wav)
   * @throws IOException
   *                            encodieren ist nicht möglich
   * 
   * @throws CheckReadException
   *                            die Raw-Datei kann nicht gelesen werden
   * 
   */
  public void encode(Path inputfile, Path outputfile) throws IOException {
    rawFile = inputfile;
    if (
      !Files.isRegularFile(inputfile)
    ) throw new CheckReadException(
        "Die RAW-Datei ist keine reguläre Datei im Sinne von ist lesbar - "
            + inputfile.toAbsolutePath().toString()
    );
    try(BufferedInputStream inbuffer = new BufferedInputStream(Files.newInputStream(inputfile));
        BufferedOutputStream outbuffer = new BufferedOutputStream(Files.newOutputStream(outputfile))) {

      outbuffer.write(this.toByteArray());
      byte[] puffer = new byte[4096];
      int len = 0;
      while ((len = inbuffer.read(puffer)) != -1) {
        outbuffer.write(puffer, 0, len);
      }
    }
  }




  /**
   * Wandle einen Fluss von Rohbytes in eine WAVE-Datei um. Die Rohbytes könnten
   * aus einem Mikrofon stammen.
   * 
   * 
   * @param rawData
   *                ein Bytefluss von Rohdaten
   * 
   * @return die Rohbytes wurden in das WAV-Format konvertiert
   * 
   * @throws IOException
   *                     encodieren ist nicht möglich
   */
  public byte[] encode(ByteBuffer rawData) throws IOException {

    ByteArrayOutputStream in = new ByteArrayOutputStream();
    in.writeBytes(getRiff());
    in.writeBytes(
        ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(rawData.capacity() + 44 - 8).array()
    );
    in.writeBytes(getWave());
    in.writeBytes(getFmt());
    in.writeBytes(getLenFmtHeader());
    in.writeBytes(getPcm());
    in.writeBytes(getChannels());
    in.writeBytes(getSampleRate());
    in.writeBytes(getByteRate());
    in.writeBytes(getFrameSize());
    in.writeBytes(getSampleSizeInBits());
    in.writeBytes(getData());
    in.writeBytes(ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(rawData.capacity()).array());

    ByteArrayOutputStream outbuffer = new ByteArrayOutputStream();
    outbuffer.write(in.toByteArray()); // Wave Header
    outbuffer.write(rawData.array());
    return outbuffer.toByteArray();
  }



  /**
   * Ein Headerfeld im WAVE-Format.
   * 
   * 
   * @return "data"
   */
  public byte[] getData() {
    return "data".getBytes(CHARACTER_SET);
  }



  /**
   * Eine WAVE-Datei beginnt mit der Kennung RIFF.
   * 
   * @return RIFF
   */
  public byte[] getRiff() {
    return "RIFF".getBytes(CHARACTER_SET);
  }



  /**
   * WAVE ist ein Teil aus dem RIFF-Header.
   * 
   * @return WAVE
   */
  public byte[] getWave() {
    return "WAVE".getBytes(CHARACTER_SET);
  }



  /**
   * {@code fmt} leitet die Headersignatur ein.
   * 
   * @return fmt mit einem abschließenden SPACE
   */
  public byte[] getFmt() {
    return "fmt ".getBytes(CHARACTER_SET);
  }



  /**
   * PCM hat den Identifier 1.
   * 
   * @return 1
   */
  public byte[] getPcm() {
    return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort((short) 1).array();
  }



  /**
   * Die RiffLength ist die physische Dateigröße - 8. Die Methode sollte erst nach
   * {@code encode(...)} aufgerufen werden.
   * 
   * @return Dateigröße -8
   */
  protected byte[] getRiffLength() {
    int dateigroesse = 0;
    try {
      dateigroesse = (int) Files.size(rawFile);
    }
    catch (Exception e) {}
    return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(dateigroesse + 44 - 8).array();
  }



  /**
   * Die physische Dateigröße - 44. 44 ist die Headerlänge. Die Methode sollte
   * erst nach {@code encode(...)} aufgerufen werden.
   * 
   * @return Dateigröße - 44
   */
  protected byte[] getDataLength() {
    int dateigroesse = 0;
    try {
      dateigroesse = (int) Files.size(rawFile);
    }
    catch (Exception e) {}
    return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(dateigroesse).array();
  }



  /**
   * Gib die restliche Länge vom FMT-Header zurück.
   * 
   * @return die Restlänge vom FMT-Header
   */
  public byte[] getLenFmtHeader() {
    return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(16).array();
  }



  /**
   * Die Kanäle(1=Mono/2=Stereo) sind im Telefonformat enthalten.
   * 
   * @return die Anzahl der Kanäle
   * 
   * 
   */
  public byte[] getChannels() {
    return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(channels).array();
  }



  /**
   * Die Frequenz oder die Samples pro Sekunde.
   * 
   * 
   * @return Samples per Second
   * 
   */
  public byte[] getSampleRate() {
    return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(sampleRate).array();
  }



  public byte[] getByteRate() {
    return ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt(byteRate).array();
  }



  /**
   * Die Framesize berechnet sich aus den Channels(1=Mono,2=Stereo)
   * SampleSizeInBits(16,8) / 8.
   * 
   * @return die FrameSize
   */
  public byte[] getFrameSize() {
    return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(frameSize).array();
  }



  public byte[] getSampleSizeInBits() {
    return ByteBuffer.allocate(2).order(ByteOrder.LITTLE_ENDIAN).putShort(sampleSizeInBits).array();
  }

  
  
  /**
   * Der komplette Header für eine WAVE-Datei. Solange {@code encode(...)} nicht
   * aufgerufen wurde, ist der Header vorläufig, weil die genaue
   * {@code RiffLength} und {@code DataLength} unbekannt sind.
   * 
   * 
   * @return der Header f�r eine WAVE-Datei
   * 
   * @see #getRiffLength()
   * @see #getDataLength()
   * 
   */
  protected byte[] toByteArray() {
    ByteArrayOutputStream in = new ByteArrayOutputStream();
    in.writeBytes(getRiff());
    in.writeBytes(getRiffLength());
    in.writeBytes(getWave());
    in.writeBytes(getFmt());
    in.writeBytes(getLenFmtHeader());
    in.writeBytes(getPcm());
    in.writeBytes(getChannels());
    in.writeBytes(getSampleRate());
    in.writeBytes(getByteRate());
    in.writeBytes(getFrameSize());
    in.writeBytes(getSampleSizeInBits());
    in.writeBytes(getData());
    in.writeBytes(getDataLength());
    return in.toByteArray();
  }

}
