/*----------------------------------------------------------------*/
/*                                                                */
/*                   BEGIN 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() {
    oldIsAnimating = isAnimating;
    isAnimating = false;
    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 or image format ending with "
                    + "\"mp3\", \"wav\", \"aif\", \"aiff\", \"png\", \"jpg\" or \"jpeg\" .");
        }
    } 
    else {
        println("----- No audio or image file was selected.");
    }
    isAnimating = oldIsAnimating;
}

/**
 * Attempts to load audio data from a selected file into playBuffer, then calls
 * writeAudioToImage() to transcode audio data and write it to mapImage.
 * 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) {
    println("-- local loadAudioFile ");
    boolean wasAnimating = isAnimating;
    boolean wasWindowing = isWindowing;
    boolean wasBlending = isBlending;
    isAnimating = false;
    isWindowing = false;
    isBlending = false;
    // hard stop old sampler/granular activity before replacing buffers
    suspendWindowAudio();
    // now the standard calls for loading audio from a file
    MultiChannelBuffer buff = new MultiChannelBuffer(1024, 1);
    fileSampleRate =  minim.loadFileIntoBuffer(audFile.getAbsolutePath(), buff);
    // load the audio file and resample it if necessary
    if (fileSampleRate > 0) {
        if (fileSampleRate != audioOut.sampleRate()) {
            float[] resampled = AudioUtility.resampleMonoToOutput(buff.getChannel(0), fileSampleRate, audioOut);
            buff.setBufferSize(resampled.length);
            buff.setChannel(0, resampled);
            bufferSampleRate = audioOut.sampleRate();
        }
        else {
            bufferSampleRate = fileSampleRate;
        }
        this.audioFileLength = buff.getBufferSize();
        println("---- file sample rate = "+ this.fileSampleRate 
                +", buffer sample rate = "+ bufferSampleRate
                +", audio output sample rate = "+ audioOut.sampleRate());
    }
    else {
        println("-- Unable to load file. File may be empty, wrong format, or damaged.");
        return;
    }
    // everything looks good so far, proceed (but blending is turned off) 
    // TODO omit blending with window buffer
    if (isBlending) {
        blendInto(playBuffer, buff, 0.5f, false, -12.0f);
        updateAudioChain(playBuffer.getChannel(0), bufferSampleRate);
    }
    else {
        updateAudioChain(buff.getChannel(0), bufferSampleRate);
    }
    // update dependent audio sources
    if (isLoadToBoth) {
        println("-- loading transcoded audio to display image\n");
        renderAudioToMapImage(chan, 0);   // or writeAudioToImage(audioSignal, mapper, mapImage, chan)
        commitMapImageToBaseImage();
    }
    // restore state
    isBlending = wasBlending;
    isAnimating = wasAnimating;
    isWindowing = wasWindowing;    
}

/**
 * Attempts to load image data from a selected file into mapImage, then calls writeImageToAudio() 
 * to transcode HSB brightness channel 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) {
        if (isBlending) {
            PImage dest = mapImage;
            PImage src = img;
            src.loadPixels();
            for (int i = 0; i < src.pixels.length; i++) {
                int pixel = src.pixels[i];
                src.pixels[i] = setAlphaWithBlack(pixel, 96);
            }
            src.updatePixels();
            dest.blend(src, 0, 0, src.width, src.height, 0, 0, dest.width, dest.height, BLEND);
        }
        else {
            // 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);
    }
    if (isLoadToBoth) {
        // create a new array for the audio signal
        float[] sig = new float[mapper.getSize()];
        audioSignal = sig;
        // write transcoded pixel data from HSB Brightness channel to audioSignal
        renderMapImageToAudio(PixelAudioMapper.ChannelNames.L);
        // update all dependent audio data
        updateAudioChain(sig);
        // update baseImage from mapImage
    }
    commitMapImageToBaseImage();
}

/**
 * Blends audio data from buffer "src" into buffer "dest" in place.
 *
 * The formula per sample is:
 *    dest[i] = weight * src[i] + (1 - weight) * dest[i]
 *
 * @param dest   Destination buffer (will be modified)
 * @param src    Source buffer to blend into dest
 * @param weight Blend ratio (0.0 = keep dest, 1.0 = replace with src)
 */
public static void blendInto(MultiChannelBuffer dest, MultiChannelBuffer src, float weight, boolean normalize, float targetDB) {
    // Clamp blend ratio to [0, 1]
    weight = Math.max(0f, Math.min(1f, weight));
    float invWeight = 1f - weight;
    // Match dimensions safely
    int channels = Math.min(dest.getChannelCount(), src.getChannelCount());
    int frames = Math.min(dest.getBufferSize(), src.getBufferSize());
    // Perform blending directly on dest channels
    for (int c = 0; c < channels; c++) {
        float[] d = dest.getChannel(c);
        float[] s = src.getChannel(c);
        for (int i = 0; i < frames; i++) {
            d[i] = weight * s[i] + invWeight * d[i];
        }
    }
    if (normalize) {
        for (int c = 0; c < channels; c++) {
            float[] d = dest.getChannel(c);
            normalize(d, targetDB);
        }

    }
}
    
/**
 * Normalizes a single-channel signal array to a target RMS level in dBFS (decibels relative to full scale).
 * 0 is the maximum digital amplitude. -6.0 dB is 50% of the maximum level. 
 *  
 * @param signal
 * @param targetPeakDB
 */
public static void normalize(float[] signal, float targetPeakDB) {
    AudioUtility.normalizeRmsWithCeiling(signal, targetPeakDB, -3.0f);
}
    
/**
 * Transcodes audio data in audioSignal and writes it to color channel chan of mapImage.
 * 
 * @param chan     A color channel
 * @param shift    number of index positions to shift the audio signal
 */
public void renderAudioToMapImage(PixelAudioMapper.ChannelNames chan, int shift) {
    // Render current audioSignal into mapImage using current mapper & current totalShift
    writeAudioToImage(audioSignal, mapper, mapImage, chan, shift);
}

/**
 * Transcodes audio data in sig[] and writes it to color channel chan of img 
 * using the lookup tables in mapper to redirect indexing. Calls mapper.mapSigToImgShifted(), 
 * which will throw an IllegalArgumentException if sig.length != img.pixels.length
 * or sig.length != mapper.getSize(). 
 * 
 * @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
 * @param shift       the number of indices to shift when writing audio
 */
public void writeAudioToImage(float[] sig, PixelAudioMapper mapper, PImage img, PixelAudioMapper.ChannelNames chan, int shift) {
    img.loadPixels();
    mapper.mapSigToImgShifted(sig, img.pixels, chan, shift); // commit current phase
    img.updatePixels();
}

/**
 * Sets the alpha channel of an RGBA color, conditionally setting alpha = 0 if all other channels = 0.
 * 
 * @param argb     an RGBA color value
 * @param alpha    the desired alpha value to apply to argb
 * @return         the argb color with changed alpha channel value
 */
public int setAlphaWithBlack(int argb, int alpha) {
    int[] c = PixelAudioMapper.rgbaComponents(argb);
    if (c[0] == c[1] && c[1] == c[2] && c[2] == 0) {
        alpha = 0;
    }
    return alpha << 24 | c[0] << 16 | c[1] << 8 | c[2];
}

/**
 * Sets the alpha channel of an RGBA color.
 * 
 * @param argb     an RGBA color value
 * @param alpha    the desired alpha value to apply to argb
 * @return         the argb color with changed alpha channel value
 */
public static int setAlpha(int argb, int alpha) {
        return (argb & 0x00FFFFFF) | (alpha << 24);
}

/**
 * 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. 
 * 
 * @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
 * @param shift     number of indices to shift 
 */
public void writeImageToAudio(PImage img, PixelAudioMapper mapper, float[] sig, PixelAudioMapper.ChannelNames chan, int shift) {
    // If img is the *display* (shifted) image, commit its phase into audio:
    sig = mapper.mapImgToSigShifted(img.pixels, sig, chan, shift);
}

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

/**
 * Writes the mapImage, which may change with animation, to the baseImage, a reference image
 * that usually only changes when a new file is loaded.
 */
public void commitMapImageToBaseImage() {
    baseImage = mapImage.copy();
    totalShift = 0;
}

/**
 * Copies the supplied PImage to mapImage and baseImage, sets totalShift to 0 (the images are identical).
 * @param img
 */
public void commitNewBaseImage(PImage img) {
    baseImage = img.copy();
    mapImage = img.copy();
    totalShift = 0;
}

/**
 * Writes baseImage to mapImage with an index position offset of totalShift.
 */
public void refreshMapImageFromBase() {
    mapImage.loadPixels();
    mapper.copyPixelsAlongPathShifted(baseImage.pixels, mapImage.pixels, totalShift);
    mapImage.updatePixels();
}

/**
 * Calls Processing's selectOutput method to start the process of saving 
 * the current audio signal to a .wav file. 
 */
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");
}

/**
 * @param selection    a File to write as audio
 */
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 AudioUtility.
 * 
 * @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);
}

/**
 * Calls Processing's selectOutput method to start the process of saving 
 * the mapImage (the offscreen copy of the display image) to a .png file. 
 */
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);
}
