/*
 * Copyright (c) 2009-2012 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.audio.plugins;

import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetLoader;
import com.jme3.audio.AudioBuffer;
import com.jme3.audio.AudioData;
import com.jme3.audio.AudioKey;
import com.jme3.audio.AudioStream;
import com.jme3.audio.SeekableStream;
import com.jme3.util.BufferUtils;
import com.jme3.util.LittleEndien;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.logging.Level;
import java.util.logging.Logger;

public class WAVLoader implements AssetLoader {

    private static final Logger logger = Logger.getLogger(WAVLoader.class.getName());

    // all these are in big endian
    private static final int i_RIFF = 0x46464952;
    private static final int i_WAVE = 0x45564157;
    private static final int i_fmt  = 0x20746D66;
    private static final int i_data = 0x61746164;

    private boolean readStream = false;

    private AudioBuffer audioBuffer;
    private AudioStream audioStream;
    private AudioData audioData;
    private int bytesPerSec;
    private float duration;

    private ResettableInputStream in;
    private int inOffset = 0;
    
    private static class ResettableInputStream extends LittleEndien implements SeekableStream {
        
        private AssetInfo info;
        private int resetOffset = 0;
        
        public ResettableInputStream(AssetInfo info, InputStream in) {
            super(in);
            this.info = info;
        }
        
        public void setResetOffset(int resetOffset) {
            this.resetOffset = resetOffset;
        }

        public void setTime(float time) {
            if (time != 0f) {
                throw new UnsupportedOperationException("Seeking WAV files not supported");
            }
            InputStream newStream = info.openStream();
            try {
                newStream.skip(resetOffset);
                this.in = new BufferedInputStream(newStream);
            } catch (IOException ex) {
                // Resource could have gotten lost, etc.
                try {
                    newStream.close();
                } catch (IOException ex2) {
                }
                throw new RuntimeException(ex);
            }
        }
    }

    private void readFormatChunk(int size) throws IOException{
        // if other compressions are supported, size doesn't have to be 16
//        if (size != 16)
//            logger.warning("Expected size of format chunk to be 16");

        int compression = in.readShort();
        if (compression != 1){
            throw new IOException("WAV Loader only supports PCM wave files");
        }

        int channels = in.readShort();
        int sampleRate = in.readInt();

        bytesPerSec = in.readInt(); // used to calculate duration

        int bytesPerSample = in.readShort();
        int bitsPerSample = in.readShort();

        int expectedBytesPerSec = (bitsPerSample * channels * sampleRate) / 8;
        if (expectedBytesPerSec != bytesPerSec){
            logger.log(Level.WARNING, "Expected {0} bytes per second, got {1}",
                    new Object[]{expectedBytesPerSec, bytesPerSec});
        }
        
        if (bitsPerSample != 8 && bitsPerSample != 16)
            throw new IOException("Only 8 and 16 bits per sample are supported!");

        if ( (bitsPerSample / 8) * channels != bytesPerSample)
            throw new IOException("Invalid bytes per sample value");

        if (bytesPerSample * sampleRate != bytesPerSec)
            throw new IOException("Invalid bytes per second value");

        audioData.setupFormat(channels, bitsPerSample, sampleRate);

        int remaining = size - 16;
        if (remaining > 0){
            in.skipBytes(remaining);
        }
    }

    private void readDataChunkForBuffer(int len) throws IOException {
        ByteBuffer data = BufferUtils.createByteBuffer(len);
        byte[] buf = new byte[512];
        int read = 0;
        while ( (read = in.read(buf)) > 0){
            data.put(buf, 0, Math.min(read, data.remaining()) );
        }
        data.flip();
        audioBuffer.updateData(data);
        in.close();
    }

    private void readDataChunkForStream(int offset, int len) throws IOException {
        in.setResetOffset(offset);
        audioStream.updateData(in, duration);
    }

    private AudioData load(AssetInfo info, InputStream inputStream, boolean stream) throws IOException{
        this.in = new ResettableInputStream(info, inputStream);
        inOffset = 0;
        
        int sig = in.readInt();
        if (sig != i_RIFF)
            throw new IOException("File is not a WAVE file");
        
        // skip size
        in.readInt();
        if (in.readInt() != i_WAVE)
            throw new IOException("WAVE File does not contain audio");

        inOffset += 4 * 3;
        
        readStream = stream;
        if (readStream){
            audioStream = new AudioStream();
            audioData = audioStream;
        }else{
            audioBuffer = new AudioBuffer();
            audioData = audioBuffer;
        }

        while (true) {
            int type = in.readInt();
            int len = in.readInt();
            
            inOffset += 4 * 2;

            switch (type) {
                case i_fmt:
                    readFormatChunk(len);
                    inOffset += len;
                    break;
                case i_data:
                    // Compute duration based on data chunk size
                    duration = (float)(len / bytesPerSec);

                    if (readStream) {
                        readDataChunkForStream(inOffset, len);
                    } else {
                        readDataChunkForBuffer(len);
                    }
                    return audioData;
                default:
                    int skipped = in.skipBytes(len);
                    if (skipped <= 0) {
                        return null;
                    }
                    inOffset += skipped;
                    break;
            }
        }
    }
    
    public Object load(AssetInfo info) throws IOException {
        AudioData data;
        InputStream inputStream = null;
        try {
            inputStream = info.openStream();
            data = load(info, inputStream, ((AudioKey)info.getKey()).isStream());
            if (data instanceof AudioStream){
                inputStream = null;
            }
            return data;
        } finally {
            if (inputStream != null){
                inputStream.close();
            }
        }
    }
}
