import java.io.*;
import java.lang.*;
import java.util.*;
import java.awt.*;
import java.awt.image.*;

public class RenderWave
{

  // file layout variables

  private int chunkSize;
  private short formatTag;
  private short channels;
  private int samplesPerSecond;
  private short sampleFrameSize;
  private int averageBytesPerSecond;
  private short bitsPerSample;

  // image variables
 
  private int width, height;
  private int[] pixels;

  Label percentCompleteLabel;

  private double minFreq;
  private double maxFreq;

  private int power = 6;

  // waveform buffer variables

  public int outputWaveFormSize;

  final static int SAMPLES_PER_GRAIN = 1000; 

  private double[] leftWaveForm;
  private double[] rightWaveForm;

  public byte[] binary;
  private int binaryIdx = 0;

  private double expTerm;

  private double maxSample = 0.0;

  public RenderWave(
    Image image, int width, int height, int outputWaveFormSize,
    short channels, double minFreq, double maxFreq, int power,
    Label percentCompleteLabel) {
    this.width = width;
    this.height = height;
    this.outputWaveFormSize = outputWaveFormSize;
    this.channels = channels;
    this.minFreq = minFreq;
    this.maxFreq = maxFreq;
    this.power = power;
    this.percentCompleteLabel = percentCompleteLabel;
    ImageToPixels imageToPixels = new ImageToPixels(image, width, height);
    pixels = imageToPixels.grabPixels();

    leftWaveForm = new double[outputWaveFormSize];
    rightWaveForm = new double[outputWaveFormSize];
    initStructValues();
    binary = new byte[44 + outputWaveFormSize * sampleFrameSize];
    buildWave();
    writeWave(channels);
  }

  private void initStructValues() {
    chunkSize = 16;
    formatTag = 1; // no compression
    // channels set by constructor
    samplesPerSecond = 44100;
    if (channels == 1) // mono
      sampleFrameSize = 2;
    else // stereo
      sampleFrameSize = 4;
    averageBytesPerSecond = samplesPerSecond * sampleFrameSize;
    bitsPerSample = 16;
  }

  private int percent(int numerator, int denominator) {
    return (int)(((double)numerator / (double)denominator) * 100.0);
  }

  private void buildWave() {
    expTerm = 2.5 / Math.sqrt(2.0 * Math.PI);
    double pan = .5; // set to the middle
    double shadeFraction = 0.0;
    int shade;
    Random r = new Random();
    int i, j;
    for (j = 0; j < height; j++) {
      percentCompleteLabel.setText(percent(j, height) + "%");
      for (i = 0; i < width; i++) {
        shade = pixels[j * width + i] & 0xff;
        if (shade != 255) { // skip white
          shadeFraction = 1.0 - (double)shade / 256.0;
          shadeFraction = Math.pow(shadeFraction, (double)power);
          makeGrain(
            ((double)i / (double)width) *
              (double)outputWaveFormSize +
              (double)(r.nextInt() % 500), // position
            shadeFraction, 
            (1.0 - ((double)j / (double)height)) *
              (maxFreq - minFreq) + minFreq, // pitch
            ((double)(r.nextInt() % 100) / 100.0) * 2.0 * Math.PI, // phase
            pan,
            2); // method - smooth curve envelope
        }
      }
    }
  }

  private double linearInterpolate(double fx1, double fx2, double eta) {
    return fx1 + (fx2 - fx1) * eta;
  }

  private int roundInteger(double floatNumber) {
    int integer;

    integer = (int)floatNumber;
    if (floatNumber - (double)integer >= .5)
      integer++;
    return integer;
  }

  private double getGrainEnvelop(int method, double sampleIdx) {
    if (method == 1)
      return expTerm * Math.exp(-((sampleIdx * sampleIdx) / 30000.0));
    else { // method = 2
      sampleIdx = sampleIdx + 500.0;
      if (sampleIdx < 300)
        return sampleIdx / 300.0;
      else
        if (sampleIdx < 700.0)
          return(1.0);
        else
          return linearInterpolate(1.0, 0.0, (sampleIdx - 700.0) / 300.0);
    }
  }

  private void makeGrain(
    double position, double amplitude, double pitch,
    double phase, double pan, int method) {
    int integerSampleIdx;
    double sampleIdx;
    double shiftSampleIdx;
    double lastSampleIdx;
    double envelop;
    double sampleValue;

    sampleIdx = position;
    lastSampleIdx = position + SAMPLES_PER_GRAIN;
    while (sampleIdx < lastSampleIdx) {
      shiftSampleIdx = (sampleIdx - 500.0) - position;
      integerSampleIdx = roundInteger(sampleIdx);
      if (integerSampleIdx < outputWaveFormSize &&
          integerSampleIdx >= 0) {
        envelop = getGrainEnvelop(method, shiftSampleIdx);
        sampleValue = amplitude *
          envelop *
          Math.cos((pitch/22050.0) * Math.PI * shiftSampleIdx + phase);
        leftWaveForm[integerSampleIdx] += pan * sampleValue;
        rightWaveForm[integerSampleIdx] += (1.0 - pan) * sampleValue;
      }
      sampleIdx = sampleIdx + 1.0;
    }
  }

  private void saveByte(byte value) {
    binary[binaryIdx] = value;
    binaryIdx++;
  }

  private void saveShort(short value) {
    saveByte((byte)(value & 0xff));
    saveByte((byte)((value >> 8) & 0xff));
  }

  private void saveShortFromInt(int value) {
    saveByte((byte)(value & 0xff));
    saveByte((byte)((value >> 8) & 0xff));
  }

  private void saveInteger(int value) {
    saveByte((byte)(value & 0xff));
    saveByte((byte)((value >> 8) & 0xff));
    saveByte((byte)((value >> 16) & 0xff));
    saveByte((byte)((value >> 24) & 0xff));
  }

  private void writeWave(short channels) {
    int sampleIdx;

    double normalize = normalizeWaveForm();

    saveByte((byte)'R');
    saveByte((byte)'I');
    saveByte((byte)'F');
    saveByte((byte)'F');
    saveByte((byte)'r');
    saveByte((byte)0);
    saveByte((byte)0);
    saveByte((byte)0);
    saveByte((byte)'W');
    saveByte((byte)'A');
    saveByte((byte)'V');
    saveByte((byte)'E');
    saveByte((byte)'f');
    saveByte((byte)'m');
    saveByte((byte)'t');
    saveByte((byte)' ');
    saveInteger(chunkSize);
    saveShort(formatTag);
    saveShort(channels);
    saveInteger(samplesPerSecond);
    saveInteger(averageBytesPerSecond);
    saveShort(sampleFrameSize);
    saveShort(bitsPerSample);
  
    saveByte((byte)'d');
    saveByte((byte)'a');
    saveByte((byte)'t');
    saveByte((byte)'a');
    saveInteger(outputWaveFormSize * sampleFrameSize);

    for (sampleIdx = 0; sampleIdx < outputWaveFormSize; sampleIdx++) {
      saveShortFromInt((int)(leftWaveForm[sampleIdx] * normalize));
      if (channels == 2) // stereo
        saveShortFromInt((int)(rightWaveForm[sampleIdx] * normalize));
    }
  }

  private double floatAbs(double x) {
    if (x < 0.0)
      x = -x;
    return x;
  }

  private double normalizeWaveForm() {
    int sampleIdx;
    double absValue;

    maxSample = 0.0;
    for (sampleIdx = 0; sampleIdx < outputWaveFormSize; sampleIdx++) {
      absValue = floatAbs(leftWaveForm[sampleIdx]);
      if (absValue > maxSample)
        maxSample = absValue;
      absValue = floatAbs(rightWaveForm[sampleIdx]);
      if (absValue > maxSample)
        maxSample = absValue;
    }
    if (maxSample == 0.0)
      return 1.0;
    return 32767.0 / maxSample;
  }

  public byte[] getBinary() {
    return binary;
  }
}
