package nxm.audio.lib;

import nxm.sys.inc.DataTypes;
import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.lib.Data;

/** Library class for working with linear audio files
 *
 *  @author jczecho
 *  @since NeXtMidas 3.9.2
 */
@ProvisionalUseOnly("User contributed and not fully tested")
public final class MuLaw {

  private MuLaw () {} // Prevent instantiation

  private static final byte[]  MULAW_ENCODE_BYTE  = new byte[10001];
  private static final byte[]  MULAW_DECODE_BYTE  = new byte[256];
  private static final short[] MULAW_DECODE_SHORT = new short[256];
  static {
    // ==== CREATE ENCODING TABLE ==================================================================
    float factor1 = (float) (127.0 / Math.log(256.0));
    float factor2 = (float) (255.0 / 5000.0);

    // creating mulaw table from -127 <= 0 >= 127 (-5000 <= 0 >= 5000 buffer size)
    for (int i=0; i<=5000; i++) {
      MULAW_ENCODE_BYTE[i+5000] = (byte) (Math.round(factor1 * Math.log(1.0 + factor2 * i)));
      MULAW_ENCODE_BYTE[i     ] = (byte) -MULAW_ENCODE_BYTE[i+5000];
    }

    for (int i=10000; i>=0; i--) {
      if (MULAW_ENCODE_BYTE[i] < 0) {
        MULAW_ENCODE_BYTE[i] = (byte)(MULAW_ENCODE_BYTE[i] - 1         ); // subtract one
        MULAW_ENCODE_BYTE[i] = (byte)(MULAW_ENCODE_BYTE[i] ^ (byte)0x80); // negate sign bit
      }
      else {
        MULAW_ENCODE_BYTE[i] = (byte)(MULAW_ENCODE_BYTE[i] ^ (byte)0xFF); // negate all bits
      }
    }

    // ==== CREATE DECODING TABLES =================================================================
    for (int i=-128; i<=126; i++) {
      byte btemp = (byte)i;

      // check for the sign of the byte.
      // positive or negative.
      if (btemp >= 0) {
        btemp = (byte)((btemp ^ (byte)0x80) + 1); // negate sign bit and add one
      }
      else {
        btemp = (byte)(btemp ^ (byte)0XFF); // negate all bits
      }

      // compute the decode mulaw value
      MULAW_DECODE_SHORT[i+128] = (short)Math.round((Math.pow(256.0,Math.abs(btemp/127.0)) - 1.0) * 32767.0 / 255.0);
      MULAW_DECODE_BYTE[i+128]  = (byte )Math.round((Math.pow(256.0,Math.abs(btemp/127.0)) - 1.0) *   127.0 / 255.0);

      if (btemp < 0) {
        MULAW_DECODE_SHORT[i+128] = (short)-MULAW_DECODE_SHORT[i+128];
        MULAW_DECODE_BYTE[i+128]  = (byte )-MULAW_DECODE_BYTE[i+128];
      }
    }
  }

  /** Decode a buffer.
   *  @param inData  (IN)  The input to decode. Must be of type 'I'/'B'.
   *  @param outData (OUT) The decoded output.  Must be of type 'I'.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void decode (Data inData, Data outData, int numElem) {
    if (outData.getFormatTypeChar() != 'I') {
      throw new IllegalArgumentException("Unsupported output type: "+outData.getFormatTypeChar());
    }

    switch (inData.getFormatTypeChar()) {
      case DataTypes.INT: {
        short[] inBuf  = inData.castI(true);
        short[] outBuf = outData.castI(false);
        decode(inBuf, outBuf, numElem);
        inData.uncast(inBuf, false);
        outData.uncast(outBuf, true);
        break;
      }
      case DataTypes.BYTE: {
        byte[]  inBuf  = inData.castB(true);
        short[] outBuf = outData.castI(false);
        decode(inBuf, outBuf, numElem);
        inData.uncast(inBuf, false);
        outData.uncast(outBuf, true);
        break;
      }
      default: {
        throw new IllegalArgumentException("Unsupported input type: "+inData.getFormatTypeChar());
      }
    }
  }

  /** Encode a buffer.
   *  @param inData  (IN)  The input to encode. Must be of type 'D'/'F'/'X'/'L'/'I'/'B'.
   *  @param outData (OUT) The encoded output.  Must be of type 'B'.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void encode (Data inData, Data outData, int numElem) {
    if (outData.getFormatTypeChar() != 'B') {
      throw new IllegalArgumentException("Unsupported output type: "+outData.getFormatTypeChar());
    }

    switch (inData.getFormatTypeChar()) {
      case DataTypes.DOUBLE: {
        double[] inBuf  = inData.castD(true);
        byte[]   outBuf = outData.castB(false);
        encode(inBuf, outBuf, numElem);
        inData.uncast(inBuf, false);
        outData.uncast(outBuf, true);
        break;
      }
      case DataTypes.FLOAT: {
        float[] inBuf  = inData.castF(true);
        byte[]  outBuf = outData.castB(false);
        encode(inBuf, outBuf, numElem);
        inData.uncast(inBuf, false);
        outData.uncast(outBuf, true);
        break;
      }
      case DataTypes.XLONG: {
        long[] inBuf  = inData.castX(true);
        byte[] outBuf = outData.castB(false);
        encode(inBuf, outBuf, numElem);
        inData.uncast(inBuf, false);
        outData.uncast(outBuf, true);
        break;
      }
      case DataTypes.LONG: {
        int[]  inBuf  = inData.castL(true);
        byte[] outBuf = outData.castB(false);
        encode(inBuf, outBuf, numElem);
        inData.uncast(inBuf, false);
        outData.uncast(outBuf, true);
        break;
      }
      case DataTypes.INT: {
        short[] inBuf  = inData.castI(true);
        byte[]  outBuf = outData.castB(false);
        encode(inBuf, outBuf, numElem);
        inData.uncast(inBuf, false);
        outData.uncast(outBuf, true);
        break;
      }
      case DataTypes.BYTE: {
        byte[] inBuf  = inData.castB(true);
        byte[] outBuf = outData.castB(false);
        encode(inBuf, outBuf, numElem);
        inData.uncast(inBuf, false);
        outData.uncast(outBuf, true);
        break;
      }
      default: {
        throw new IllegalArgumentException("Unsupported input type: "+inData.getFormatTypeChar());
      }
    }
  }

  /** Decode a buffer.
   *  @param inBuf   (IN)  The input to encode.
   *  @param outBuf  (OUT) The encoded output.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void decode (short[] inBuf, short[] outBuf, int numElem) {
    for (int i = 0; i < numElem; i++) {
      outBuf[i] = MULAW_DECODE_SHORT[inBuf[i]+128];
    }
  }

  /** Decode a buffer.
   *  @param inBuf   (IN)  The input to decode.
   *  @param outBuf  (OUT) The decoded output.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void decode (byte[] inBuf, short[] outBuf, int numElem) {
    for (int i = 0; i < numElem; i++) {
      outBuf[i] = MULAW_DECODE_BYTE[inBuf[i]+128];
    }
  }

  /** Decode a buffer.
   *  @param inBuf   (IN)  The input to decode.
   *  @param outBuf  (OUT) The decoded output.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void decode (byte[] inBuf, byte[] outBuf, int numElem) {
   for (int i = 0; i < numElem; i++) {
      outBuf[i] = MULAW_DECODE_BYTE[inBuf[i]+128];
    }
  }

  /** Encode a buffer.
   *  @param inBuf   (IN)  The input to encode.
   *  @param outBuf  (OUT) The encoded output.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void encode (double[] inBuf, double[] outBuf, int numElem) {
    for (int i = 0; i < numElem; i++) {
      outBuf[i] = encode(inBuf[i]);
    }
  }

  /** Encode a buffer.
   *  @param inBuf   (IN)  The input to encode.
   *  @param outBuf  (OUT) The encoded output.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void encode (double[] inBuf, byte[] outBuf, int numElem) {
    for (int i = 0; i < numElem; i++) {
      outBuf[i] = encode(inBuf[i]);
    }
  }

  /** Encode a buffer.
   *  @param inBuf   (IN)  The input to encode.
   *  @param outBuf  (OUT) The encoded output.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void encode (float[] inBuf, float[] outBuf, int numElem) {
    for (int i = 0; i < numElem; i++) {
      outBuf[i] = encode(inBuf[i]);
    }
  }

  /** Encode a buffer.
   *  @param inBuf   (IN)  The input to encode.
   *  @param outBuf  (OUT) The encoded output.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void encode (float[] inBuf, byte[] outBuf, int numElem) {
    for (int i = 0; i < numElem; i++) {
      outBuf[i] = encode(inBuf[i]);
    }
  }

  /** Encode a buffer.
   *  @param inBuf   (IN)  The input to encode.
   *  @param outBuf  (OUT) The encoded output.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void encode (long[] inBuf, long[] outBuf, int numElem) {
    for (int i = 0; i < numElem; i++) {
      outBuf[i] = encode(inBuf[i]);
    }
  }

  /** Encode a buffer.
   *  @param inBuf   (IN)  The input to encode.
   *  @param outBuf  (OUT) The encoded output.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void encode (long[] inBuf, byte[] outBuf, int numElem) {
    for (int i = 0; i < numElem; i++) {
      outBuf[i] = encode(inBuf[i]);
    }
  }

  /** Encode a buffer.
   *  @param inBuf   (IN)  The input to encode.
   *  @param outBuf  (OUT) The encoded output.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void encode (int[] inBuf, int[] outBuf, int numElem) {
    for (int i = 0; i < numElem; i++) {
      outBuf[i] = encode(inBuf[i]);
    }
  }

  /** Encode a buffer.
   *  @param inBuf   (IN)  The input to encode.
   *  @param outBuf  (OUT) The encoded output.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void encode (int[] inBuf, byte[] outBuf, int numElem) {
    for (int i = 0; i < numElem; i++) {
      outBuf[i] = encode(inBuf[i]);
    }
  }

  /** Encode a buffer.
   *  @param inBuf   (IN)  The input to encode.
   *  @param outBuf  (OUT) The encoded output.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void encode (short[] inBuf, short[] outBuf, int numElem) {
    for (int i = 0; i < numElem; i++) {
      outBuf[i] = encode(inBuf[i]);
    }
  }

  /** Encode a buffer.
   *  @param inBuf   (IN)  The input to encode.
   *  @param outBuf  (OUT) The encoded output.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void encode (short[] inBuf, byte[] outBuf, int numElem) {
    for (int i = 0; i < numElem; i++) {
      outBuf[i] = encode(inBuf[i]);
    }
  }

  /** Encode a buffer.
   *  @param inBuf   (IN)  The input to encode.
   *  @param outBuf  (OUT) The encoded output.
   *  @param numElem (IN)  The number of values to process.
   */
  public static void encode (byte[] inBuf, byte[] outBuf, int numElem) {
    for (int i = 0; i < numElem; i++) {
      outBuf[i] = encode(inBuf[i]);
    }
  }

  /** Encode a single value.
   *  @param value The unencoded value.
   *  @return The encoded value.
   */
  private static byte encode (double value) {
    int val = Math.min(10000, (int)(Math.round(0.25 * value)));
    if (val < 0) {
      val = -val;
    }
    else if (val > 0) {
      val = 5000 + val;
    }
    return MULAW_ENCODE_BYTE[Math.max(0, val)];
  }
}