/*
 * Copyright (c) 2009-2010 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.texture.plugins;

import com.jme3.asset.*;
import com.jme3.util.*;
import com.jme3.math.FastMath;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.logging.Level;
import java.util.logging.Logger;

public class HDRLoader implements AssetLoader {

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

    private boolean writeRGBE = false;
    private ByteBuffer rleTempBuffer;
    private ByteBuffer dataStore;
    private final float[] tempF = new float[3];

    public HDRLoader(boolean writeRGBE){
        this.writeRGBE = writeRGBE;
    }

    public HDRLoader(){
    }
    
    public static void convertFloatToRGBE(byte[] rgbe, float red, float green, float blue){
        double max = red;
        if (green > max) max = green;
        if (blue > max) max = blue;
        if (max < 1.0e-32){
            rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0;
        }else{
            double exp = Math.ceil( Math.log10(max) / Math.log10(2) );
            double divider = Math.pow(2.0, exp);
            rgbe[0] = (byte) ((red   / divider) * 255.0);
            rgbe[1] = (byte) ((green / divider) * 255.0);
            rgbe[2] = (byte) ((blue  / divider) * 255.0);
            rgbe[3] = (byte) (exp + 128.0);
      }
    }

    public static void convertRGBEtoFloat(byte[] rgbe, float[] rgbf){
        int R = rgbe[0] & 0xFF, 
            G = rgbe[1] & 0xFF,
            B = rgbe[2] & 0xFF, 
            E = rgbe[3] & 0xFF;
        
        float e = (float) Math.pow(2f, E - (128 + 8) );
        rgbf[0] = R * e;
        rgbf[1] = G * e;
        rgbf[2] = B * e;
    }

    public static void convertRGBEtoFloat2(byte[] rgbe, float[] rgbf){
        int R = rgbe[0] & 0xFF,
            G = rgbe[1] & 0xFF,
            B = rgbe[2] & 0xFF,
            E = rgbe[3] & 0xFF;

        float e = (float) Math.pow(2f, E - 128);
        rgbf[0] = (R / 256.0f) * e;
        rgbf[1] = (G / 256.0f) * e;
        rgbf[2] = (B / 256.0f) * e;
    }

    public static void convertRGBEtoFloat3(byte[] rgbe, float[] rgbf){
        int R = rgbe[0] & 0xFF,
            G = rgbe[1] & 0xFF,
            B = rgbe[2] & 0xFF,
            E = rgbe[3] & 0xFF;

        float e = (float) Math.pow(2f, E - (128 + 8) );
        rgbf[0] = R * e;
        rgbf[1] = G * e;
        rgbf[2] = B * e;
    }

    private short flip(int in){
        return (short) ((in << 8 & 0xFF00) | (in >> 8));
    }
    
    private void writeRGBE(byte[] rgbe){
        if (writeRGBE){
            dataStore.put(rgbe);
        }else{
            convertRGBEtoFloat(rgbe, tempF);
            dataStore.putShort(FastMath.convertFloatToHalf(tempF[0]))
                     .putShort(FastMath.convertFloatToHalf(tempF[1])).
                      putShort(FastMath.convertFloatToHalf(tempF[2]));
        }
    }
    
    private String readString(InputStream is) throws IOException{
        StringBuilder sb = new StringBuilder();
        while (true){
            int i = is.read();
            if (i == 0x0a || i == -1) // new line or EOF
                return sb.toString();
            
            sb.append((char)i);
        }
    }
    
    private boolean decodeScanlineRLE(InputStream in, int width) throws IOException{
        // must deocde RLE data into temp buffer before converting to float
        if (rleTempBuffer == null){
            rleTempBuffer = BufferUtils.createByteBuffer(width * 4);
        }else{
            rleTempBuffer.clear();
            if (rleTempBuffer.remaining() < width * 4)
                rleTempBuffer = BufferUtils.createByteBuffer(width * 4);
        }
        
	// read each component seperately
        for (int i = 0; i < 4; i++) {
            // read WIDTH bytes for the channel
            for (int j = 0; j < width;) {
                int code = in.read();
                if (code > 128) { // run
                    code -= 128;
                    int val = in.read();
                    while ((code--) != 0) {
                        rleTempBuffer.put( (j++) * 4 + i , (byte)val);
                        //scanline[j++][i] = val;
                    }
                } else {	// non-run
                    while ((code--) != 0) {
                        int val = in.read();
                        rleTempBuffer.put( (j++) * 4 + i, (byte)val);
                        //scanline[j++][i] = in.read();
                    }
                }
            }
        }
        
        rleTempBuffer.rewind();
        byte[] rgbe = new byte[4];
//        float[] temp = new float[3];
            
        // decode temp buffer into float data
        for (int i = 0; i < width; i++){
            rleTempBuffer.get(rgbe);
            writeRGBE(rgbe);
        }
        
        return true;
    }
    
    private boolean decodeScanlineUncompressed(InputStream in, int width) throws IOException{
        byte[] rgbe = new byte[4];
        
        for (int i = 0; i < width; i+=3){
            if (in.read(rgbe) < 1)
                return false;

            writeRGBE(rgbe);
        }
        return true;
    }
    
    private void decodeScanline(InputStream in, int width) throws IOException{
        if (width < 8 || width > 0x7fff){
            // too short/long for RLE compression
            decodeScanlineUncompressed(in, width);
        }
        
        // check format
        byte[] data = new byte[4];
        in.read(data);
        if (data[0] != 0x02 || data[1] != 0x02 || (data[2] & 0x80) != 0){
            // not RLE data
            decodeScanlineUncompressed(in, width-1);
        }else{
            // check scanline width
            int readWidth = (data[2] & 0xFF) << 8 | (data[3] & 0xFF);
            if (readWidth != width)
                throw new IOException("Illegal scanline width in HDR file: "+width+" != "+readWidth);
            
            // RLE data
            decodeScanlineRLE(in, width);
        }
    }

    public Image load(InputStream in, boolean flipY) throws IOException{
        float gamma = -1f;
        float exposure = -1f;
        float[] colorcorr = new float[]{ -1f, -1f, -1f };

        int width = -1, height = -1;
        boolean verifiedFormat = false;

        while (true){
            String ln = readString(in);
            ln = ln.trim();
            if (ln.startsWith("#") || ln.equals("")){
                if (ln.equals("#?RADIANCE") || ln.equals("#?RGBE"))
                    verifiedFormat = true;

                continue; // comment or empty statement
            } else if (ln.startsWith("+") || ln.startsWith("-")){
                // + or - mark image resolution and start of data
                String[] resData = ln.split(" ");
                if (resData.length != 4){
                    throw new IOException("Invalid resolution string in HDR file");
                }

                if (!resData[0].equals("-Y") || !resData[2].equals("+X")){
                    logger.warning("Flipping/Rotating attributes ignored!");
                }

                //if (resData[0].endsWith("X")){
                    // first width then height
                //    width = Integer.parseInt(resData[1]);
                //    height = Integer.parseInt(resData[3]);
                //}else{
                    width = Integer.parseInt(resData[3]);
                    height = Integer.parseInt(resData[1]);
                //}

                break;
            } else {
                // regular command
                int index = ln.indexOf("=");
                if (index < 1){
                    logger.log(Level.FINE, "Ignored string: {0}", ln);
                    continue;
                }

                String var = ln.substring(0, index).trim().toLowerCase();
                String value = ln.substring(index+1).trim().toLowerCase();
                if (var.equals("format")){
                    if (!value.equals("32-bit_rle_rgbe") && !value.equals("32-bit_rle_xyze")){
                        throw new IOException("Unsupported format in HDR picture");
                    }
                }else if (var.equals("exposure")){
                    exposure = Float.parseFloat(value);
                }else if (var.equals("gamma")){
                    gamma = Float.parseFloat(value);
                }else{
                    logger.log(Level.WARNING, "HDR Command ignored: {0}", ln);
                }
            }
        }

        assert width != -1 && height != -1;

        if (!verifiedFormat)
            logger.warning("Unsure if specified image is Radiance HDR");

        // some HDR images can get pretty big
        System.gc();

        // each pixel times size of component times # of components
        Format pixelFormat;
        if (writeRGBE){
            pixelFormat = Format.RGBA8;
        }else{
            pixelFormat = Format.RGB16F;
        }

        dataStore = BufferUtils.createByteBuffer(width * height * pixelFormat.getBitsPerPixel());

        int bytesPerPixel = pixelFormat.getBitsPerPixel() / 8;
        int scanLineBytes = bytesPerPixel * width;
        for (int y = height - 1; y >= 0; y--) {
            if (flipY)
                dataStore.position(scanLineBytes * y);

            decodeScanline(in, width);
        }
        in.close();

        dataStore.rewind();
        return new Image(pixelFormat, width, height, dataStore);
    }

    public Object load(AssetInfo info) throws IOException {
        if (!(info.getKey() instanceof TextureKey))
            throw new IllegalArgumentException("Texture assets must be loaded using a TextureKey");

        boolean flip = ((TextureKey) info.getKey()).isFlipY();
        InputStream in = null;
        try {
            in = info.openStream();
            Image img = load(in, flip);
            return img;
        } finally {
            if (in != null){
                in.close();
            }
        }
    }

}
