/*----------------------------------------------------------------*/
/*                                                                */
/*                     FILE I/O METHODS                           */
/*                                                                */
/*----------------------------------------------------------------*/

// -------- BEGIN FILE I/O FOR APPLYING COLOR --------- //

/*
 * Here is a special section of code for TutorialOne and other applications that
 * color a grayscale image with color data from a file. The color and saturation
 * come from the selected file, the brightness (gray values, more or less) come
 * from an image you supply, such as display image.
 */


/**
 * Call to initiate process of opening an image file to get its color data.
 */
public void chooseColorImage() {
  selectInput("Choose an image file to apply color: ", "colorFileSelected");
}

/**
 * callback method for chooseColorImage()
 * @param selectedFile    the File the user selected
 */
public void colorFileSelected(File selectedFile) {
  if (null != selectedFile) {
    String filePath = selectedFile.getAbsolutePath();
    String fileName = selectedFile.getName();
    String fileTag = fileName.substring(fileName.lastIndexOf('.') + 1);
    fileName = fileName.substring(0, fileName.lastIndexOf('.'));
    if (fileTag.equalsIgnoreCase("png") || fileTag.equalsIgnoreCase("jpg") || fileTag.equalsIgnoreCase("jpeg")) {
      imageFile = selectedFile;
      imageFilePath = filePath;
      imageFileName = fileName;
      imageFileTag = fileTag;
      println("--- Selected color file "+ fileName +"."+ fileTag);
      // apply the color data (hue, saturation) in the selected image to our display image, mapImage
      applyImageColor(imageFile, mapImage);
    } else {
      println("----- File is not a recognized image format ending with \"png\", \"jpg\", or \"jpeg\".");
    }
  } else {
    println("----- No file was selected.");
  }
}

/**
 * Apply the hue and saturation of a chosen image file to the brightness channel of the display image.
 * @param imgFile        selected image file, source of hue and saturation values
 * @param targetImage    target image where brightness will remain unchanged
 */
public void applyImageColor(File imgFile, PImage targetImage) {
  PImage colorImage = loadImage(imgFile.getAbsolutePath());
  int w = colorImage.width > mapImage.width ? targetImage.width : colorImage.width;
  int h = colorImage.height > targetImage.height ? targetImage.height : colorImage.height;
  float[] hsbPixel = new float[3];
  colorImage.loadPixels();
  int[] colorSource = colorImage.pixels;
  targetImage.loadPixels();
  int[] graySource = targetImage.pixels;
  for (int y = 0; y < h; y++) {
    int rowStart = y * w;
    for (int x = 0; x < w; x++) {
      int cPos = rowStart + x;
      int gPos = y * targetImage.width + x;
      graySource[gPos] = PixelAudioMapper.applyColor(colorSource[cPos], graySource[gPos], hsbPixel);
    }
  }
  targetImage.updatePixels();
}

// -------- END FILE I/O FOR APPLYING COLOR --------- //

/*
 * Here is a section of "regular" file i/o methods for audio and image files.
 */


/**
 * Wrapper method for Processing's selectInput command
 */
public void chooseFile() {
  selectInput("Choose an audio file or an image file: ", "fileSelected");
}

/**
 * callback method for chooseFile(), handles standard audio and image formats for Processing.
 * If a file has been successfully selected, continues with a call to loadAudioFile() or loadImageFile().
 *
 * @param selectedFile    the File the user selected
 */
public void fileSelected(File selectedFile) {
  if (null != selectedFile) {
    String filePath = selectedFile.getAbsolutePath();
    String fileName = selectedFile.getName();
    String fileTag = fileName.substring(fileName.lastIndexOf('.') + 1);
    fileName = fileName.substring(0, fileName.lastIndexOf('.'));
    if (fileTag.equalsIgnoreCase("mp3") || fileTag.equalsIgnoreCase("wav") || fileTag.equalsIgnoreCase("aif")
      || fileTag.equalsIgnoreCase("aiff")) {
      // we chose an audio file
      audioFile = selectedFile;
      audioFilePath = filePath;
      audioFileName = fileName;
      audioFileTag = fileTag;
      println("----- Selected file " + fileName + "." + fileTag + " at "
        + filePath.substring(0, filePath.length() - fileName.length()));
      loadAudioFile(audioFile);
    } else if (fileTag.equalsIgnoreCase("png") || fileTag.equalsIgnoreCase("jpg")
      || fileTag.equalsIgnoreCase("jpeg")) {
      // we chose an image file
      imageFile = selectedFile;
      imageFilePath = filePath;
      imageFileName = fileName;
      imageFileTag = fileTag;
      loadImageFile(imageFile);
    } else {
      println("----- File is not a recognized audio format ending with \"mp3\", \"wav\", \"aif\", or \"aiff\".");
    }
  } else {
    println("----- No audio or image file was selected.");
  }
}

/**
 * Attempts to load audio data from a selected file into playBuffer, then calls
 * writeAudioToImage() to transcode audio data and write it to mapImage.
 * Resamples files that are recorded with a different sample rate than the current audio output.
 * If you want to load the image file and audio file separately, comment out writeAudioToImage().
 *
 * @param audFile    an audio file
 */
public void loadAudioFile(File audFile) {
  MultiChannelBuffer buff = new MultiChannelBuffer(1024, 1);
  fileSampleRate =  minim.loadFileIntoBuffer(audFile.getAbsolutePath(), buff);
  if (fileSampleRate <= 0) {
    println("-- Unable to load file. File may be empty, wrong format, or damaged.");
    return;
  }
  float sig[];
  // load audio to sig, resampling it if required
  if (fileSampleRate != audioOut.sampleRate() && doResample) {
    sig = AudioUtility.resampleMonoToOutput(buff.getChannel(0), fileSampleRate, audioOut);
    bufferSampleRate = sampleRate;
  } else {
    sig = Arrays.copyOf(buff.getChannel(0), buff.getBufferSize());
    bufferSampleRate = fileSampleRate;
  }
  this.audioFileLength = sig.length;
  println("---- file sample rate = "+ this.fileSampleRate
    +", buffer sample rate = "+ bufferSampleRate
    +", audio output sample rate = "+ audioOut.sampleRate());
  // make sure the synth is ready (a precaution)
  ensureSamplerReady();
  // update the audio variables that depend on a newly loaded audio file
  updateAudioChain(sig);
  // write the signal to mapImage
  // we do it automatically here, but that will change in later examples
  writeAudioToImage(audioSignal, mapper, mapImage, chan);
  //if (applyColorMapOnLoad) applyColorMapToDisplay(true);
}


/**
 * Transcodes audio data in sig[] and writes it to color channel chan of mapImage
 * using the lookup tables in mapper to redirect indexing. Calls mapper.mapSigToImg(),
 * which will throw an IllegalArgumentException if sig.length != img.pixels.length
 * or sig.length != mapper.getSize(). We typically use PixelAudioMapper.ChannelNames.ALL
 * or PixelAudioMapper.ChannelNames.L as the chan value. Both result in gray values,
 * with PixelAudioMapper.ChannelNames.L maintaining previous hue and saturation color
 * values in the image.
 *
 * @param sig         an array of float, should be audio data in the range [-1.0, 1.0]
 * @param mapper      a PixelAudioMapper
 * @param img         a PImage
 * @param chan        a color channel
 */
public void writeAudioToImage(float[] sig, PixelAudioMapper mapper, PImage img, PixelAudioMapper.ChannelNames chan) {
  // If sig.length == mapper.getSize() == mapImage.width * mapImage.height, we can call safely mapper.mapSigToImg()
  img.loadPixels();
  mapper.mapSigToImg(sig, img.pixels, chan);
  img.updatePixels();
}

/**
 * Attempts to load image data from a selected file into mapImage, then calls writeImageToAudio()
 * to transcode HSB brightness color data from the image to audio and writes it to playBuffer and audioSignal.
 *
 * @param imgFile    an image file
 */
public void loadImageFile(File imgFile) {
  PImage img = loadImage(imgFile.getAbsolutePath());
  // stash information about the image in imgFileWidth, imageFileHeight for future use
  imageFileWidth = img.width;
  imageFileHeight = img.height;
  // calculate w and h for copying image to display (mapImage)
  int w = img.width > mapImage.width ? mapImage.width : img.width;
  int h = img.height > mapImage.height ? mapImage.height : img.height;
  if (chan == PixelAudioMapper.ChannelNames.ALL) {
    // copy the image directly using Processing copy command
    mapImage.copy(img, 0, 0, w, h, 0, 0, w, h);
  } else {
    // copy only specified channels of the new image
    PImage mixImage = createImage(w, h, RGB);
    mixImage.copy(mapImage, 0, 0, w, h, 0, 0, w, h);
    img.loadPixels();
    mixImage.loadPixels();
    mixImage.pixels = PixelAudioMapper.pushChannelToPixel(img.pixels, mixImage.pixels, chan);
    mixImage.updatePixels();
    mapImage.copy(mixImage, 0, 0, w, h, 0, 0, w, h);
  }
  // initialize a new buffer for audioSignal
  float[] sig = new float[mapper.getSize()];
  audioSignal = sig;
  // render the HSB brightness channel (ChannelNames.L) to the audio signal as transcoded samples
  renderMapImageToAudio(PixelAudioMapper.ChannelNames.L);
  // update all audio data structures that depend on audioSignal
  updateAudioChain(sig);
}

/**
 * This method writes a color channel from an image to playBuffer, fulfilling a
 * central concept of the PixelAudio library: image is sound. Calls mapper.mapImgToSig(),
 * which will throw an IllegalArgumentException if img.pixels.length != sig.length or
 * img.width * img.height != mapper.getWidth() * mapper.getHeight().
 * Sets totalShift = 0 on completion: the image and audio are now in sync. TODO
 *
 * @param img       a PImage, a source of data
 * @param mapper    a PixelAudioMapper, handles mapping between image and audio signal
 * @param sig       an target array of float in audio format
 * @param chan      a color channel
 */
public void writeImageToAudio(PImage img, PixelAudioMapper mapper, float[] sig, PixelAudioMapper.ChannelNames chan) {
  // If img is the *display* (shifted) image, commit its phase into audio:
  sig = mapper.mapImgToSig(img.pixels, sig, chan);
}

/**
 * Writes a specified channel of mapImage to audioSignal.
 *
 * @param chan    the selected color channel
 */
public void renderMapImageToAudio(PixelAudioMapper.ChannelNames chan) {
  writeImageToAudio(mapImage, mapper, audioSignal, chan);
}


public void saveToAudio() {
  // File folderToStartFrom = new File(dataPath("") + "/");
  // selectOutput("Select an audio file to write to:", "audioFileSelectedWrite", folderToStartFrom);
  selectOutput("Select an audio file to write to:", "audioFileSelectedWrite");
}

public void audioFileSelectedWrite(File selection) {
  if (selection == null) {
    println("Window was closed or the user hit cancel.");
    return;
  }
  String fileName = selection.getAbsolutePath();
  if (selection.getName().indexOf(".wav") != selection.getName().length() - 4) {
    fileName += ".wav";
  }
  try {
    saveAudioToFile(audioSignal, sampleRate, fileName);
  }
  catch (IOException e) {
    println("--->> There was an error outputting the audio file " + fileName +", "  + e.getMessage());
  }
  catch (UnsupportedAudioFileException e) {
    println("--->> The file format is unsupported " + e.getMessage());
  }
}

/**
 * Saves audio data to 16-bit integer PCM format, which Processing can also open.
 * This same method can be called as a static method in PixelAudio.
 *
 * @param samples       an array of floats in the audio range (-1.0f, 1.0f)
 * @param sampleRate    audio sample rate for the file
 * @param fileName      name of the file to save to
 * @throws IOException  an Exception you'll need to handle to call this method
 * @throws UnsupportedAudioFileException    another Exception
 */
public void saveAudioToFile(float[] samples, float sampleRate, String fileName)
  throws IOException, UnsupportedAudioFileException {
  // Convert samples from float to 16-bit PCM
  byte[] audioBytes = new byte[samples.length * 2];
  int index = 0;
  for (float sample : samples) {
    // Scale sample to 16-bit signed integer
    int intSample = (int) (sample * 32767);
    // Convert to bytes
    audioBytes[index++] = (byte) (intSample & 0xFF);
    audioBytes[index++] = (byte) ((intSample >> 8) & 0xFF);
  }
  // Create an AudioInputStream
  ByteArrayInputStream byteStream = new ByteArrayInputStream(audioBytes);
  AudioFormat format = new AudioFormat(sampleRate, 16, 1, true, false);
  AudioInputStream audioInputStream = new AudioInputStream(byteStream, format, samples.length);
  // Save the AudioInputStream to a WAV file
  File outFile = new File(fileName);
  AudioSystem.write(audioInputStream, AudioFileFormat.Type.WAVE, outFile);
}

public void saveToImage() {
  // File folderToStartFrom = new File(dataPath(""));
  selectOutput("Select an image file to write to:", "imageFileSelectedWrite");
}

public void imageFileSelectedWrite(File selection) {
  if (selection == null) {
    println("Window was closed or the user hit cancel.");
    return;
  }
  String fileName = selection.getAbsolutePath();
  if (selection.getName().indexOf(".png") != selection.getName().length() - 4) {
    fileName += ".png";
  }
  saveImageToFile(mapImage, fileName);
}

public void saveImageToFile(PImage img, String fileName) {
  img.save(fileName);
}
