/*
 * Copyright (c) 2009-2015 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.renderer.android;

import android.graphics.Bitmap;
import android.opengl.ETC1;
import android.opengl.ETC1Util.ETC1Texture;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import com.jme3.asset.AndroidImageInfo;
import com.jme3.renderer.RendererException;
import com.jme3.texture.Image;
import com.jme3.texture.Image.Format;
import com.jme3.util.BufferUtils;
import java.nio.ByteBuffer;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * @deprecated Should not be used anymore. Use {@link GLRenderer} instead.
 */
@Deprecated
public class TextureUtil {

    private static final Logger logger = Logger.getLogger(TextureUtil.class.getName());
    //TODO Make this configurable through appSettings
    public static boolean ENABLE_COMPRESSION = true;
    private static boolean NPOT = false;
    private static boolean ETC1support = false;
    private static boolean DXT1 = false;
    private static boolean DEPTH24_STENCIL8 = false;
    private static boolean DEPTH_TEXTURE = false;
    private static boolean RGBA8 = false;
    
    // Same constant used by both GL_ARM_rgba8 and GL_OES_rgb8_rgba8.
    private static final int GL_RGBA8 = 0x8058;
    
    private static final int GL_DXT1 = 0x83F0;
    private static final int GL_DXT1A = 0x83F1;
    
    private static final int GL_DEPTH_STENCIL_OES = 0x84F9;
    private static final int GL_UNSIGNED_INT_24_8_OES = 0x84FA;
    private static final int GL_DEPTH24_STENCIL8_OES = 0x88F0;

    public static void loadTextureFeatures(String extensionString) {
        ETC1support = extensionString.contains("GL_OES_compressed_ETC1_RGB8_texture");
        DEPTH24_STENCIL8 = extensionString.contains("GL_OES_packed_depth_stencil");
        NPOT = extensionString.contains("GL_IMG_texture_npot") 
                || extensionString.contains("GL_OES_texture_npot") 
                || extensionString.contains("GL_NV_texture_npot_2D_mipmap");
        
        DXT1 = extensionString.contains("GL_EXT_texture_compression_dxt1");
        DEPTH_TEXTURE = extensionString.contains("GL_OES_depth_texture");
        
        RGBA8 = extensionString.contains("GL_ARM_rgba8") ||
                extensionString.contains("GL_OES_rgb8_rgba8");
        
        logger.log(Level.FINE, "Supports ETC1? {0}", ETC1support);
        logger.log(Level.FINE, "Supports DEPTH24_STENCIL8? {0}", DEPTH24_STENCIL8);
        logger.log(Level.FINE, "Supports NPOT? {0}", NPOT);
        logger.log(Level.FINE, "Supports DXT1? {0}", DXT1);
        logger.log(Level.FINE, "Supports DEPTH_TEXTURE? {0}", DEPTH_TEXTURE);
        logger.log(Level.FINE, "Supports RGBA8? {0}", RGBA8);
    }

    private static void buildMipmap(Bitmap bitmap, boolean compress) {
        int level = 0;
        int height = bitmap.getHeight();
        int width = bitmap.getWidth();

        logger.log(Level.FINEST, " - Generating mipmaps for bitmap using SOFTWARE");

        GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);

        while (height >= 1 || width >= 1) {
            //First of all, generate the texture from our bitmap and set it to the according level
            if (compress) {
                logger.log(Level.FINEST, " - Uploading LOD level {0} ({1}x{2}) with compression.", new Object[]{level, width, height});
                uploadBitmapAsCompressed(GLES20.GL_TEXTURE_2D, level, bitmap, false, 0, 0);
            } else {
                logger.log(Level.FINEST, " - Uploading LOD level {0} ({1}x{2}) directly.", new Object[]{level, width, height});
                GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, level, bitmap, 0);
            }

            if (height == 1 || width == 1) {
                break;
            }

            //Increase the mipmap level
            height /= 2;
            width /= 2;
            Bitmap bitmap2 = Bitmap.createScaledBitmap(bitmap, width, height, true);

            // Recycle any bitmaps created as a result of scaling the bitmap.
            // Do not recycle the original image (mipmap level 0)
            if (level != 0) {
                bitmap.recycle();
            }

            bitmap = bitmap2;

            level++;
        }
    }

    private static void uploadBitmapAsCompressed(int target, int level, Bitmap bitmap, boolean subTexture, int x, int y) {
        if (bitmap.hasAlpha()) {
            logger.log(Level.FINEST, " - Uploading bitmap directly. Cannot compress as alpha present.");
            if (subTexture) {
                GLUtils.texSubImage2D(target, level, x, y, bitmap);
                RendererUtil.checkGLError();
            } else {
                GLUtils.texImage2D(target, level, bitmap, 0);
                RendererUtil.checkGLError();
            }
        } else {
            // Convert to RGB565
            int bytesPerPixel = 2;
            Bitmap rgb565 = bitmap.copy(Bitmap.Config.RGB_565, true);

            // Put texture data into ByteBuffer
            ByteBuffer inputImage = BufferUtils.createByteBuffer(bitmap.getRowBytes() * bitmap.getHeight());
            rgb565.copyPixelsToBuffer(inputImage);
            inputImage.position(0);

            // Delete the copied RGB565 image
            rgb565.recycle();

            // Encode the image into the output bytebuffer
            int encodedImageSize = ETC1.getEncodedDataSize(bitmap.getWidth(), bitmap.getHeight());
            ByteBuffer compressedImage = BufferUtils.createByteBuffer(encodedImageSize);
            ETC1.encodeImage(inputImage, bitmap.getWidth(),
                    bitmap.getHeight(),
                    bytesPerPixel,
                    bytesPerPixel * bitmap.getWidth(),
                    compressedImage);

            // Delete the input image buffer
            BufferUtils.destroyDirectBuffer(inputImage);

            // Create an ETC1Texture from the compressed image data
            ETC1Texture etc1tex = new ETC1Texture(bitmap.getWidth(), bitmap.getHeight(), compressedImage);

            // Upload the ETC1Texture
            if (bytesPerPixel == 2) {
                int oldSize = (bitmap.getRowBytes() * bitmap.getHeight());
                int newSize = compressedImage.capacity();
                logger.log(Level.FINEST, " - Uploading compressed image to GL, oldSize = {0}, newSize = {1}, ratio = {2}", new Object[]{oldSize, newSize, (float) oldSize / newSize});
                if (subTexture) {
                    GLES20.glCompressedTexSubImage2D(target,
                            level,
                            x, y,
                            bitmap.getWidth(),
                            bitmap.getHeight(),
                            ETC1.ETC1_RGB8_OES,
                            etc1tex.getData().capacity(),
                            etc1tex.getData());
                    
                    RendererUtil.checkGLError();
                } else {
                    GLES20.glCompressedTexImage2D(target,
                            level,
                            ETC1.ETC1_RGB8_OES,
                            bitmap.getWidth(),
                            bitmap.getHeight(),
                            0,
                            etc1tex.getData().capacity(),
                            etc1tex.getData());
                    
                    RendererUtil.checkGLError();
                }

//                ETC1Util.loadTexture(target, level, 0, GLES20.GL_RGB,
//                        GLES20.GL_UNSIGNED_SHORT_5_6_5, etc1Texture);
//            } else if (bytesPerPixel == 3) {
//                ETC1Util.loadTexture(target, level, 0, GLES20.GL_RGB,
//                        GLES20.GL_UNSIGNED_BYTE, etc1Texture);
            }

            BufferUtils.destroyDirectBuffer(compressedImage);
        }
    }

    /**
     * <code>uploadTextureBitmap</code> uploads a native android bitmap
     */
    public static void uploadTextureBitmap(final int target, Bitmap bitmap, boolean needMips) {
        uploadTextureBitmap(target, bitmap, needMips, false, 0, 0);
    }

    /**
     * <code>uploadTextureBitmap</code> uploads a native android bitmap
     */
    public static void uploadTextureBitmap(final int target, Bitmap bitmap, boolean needMips, boolean subTexture, int x, int y) {
        boolean recycleBitmap = false;
        //TODO, maybe this should raise an exception when NPOT is not supported

        boolean willCompress = ENABLE_COMPRESSION && ETC1support && !bitmap.hasAlpha();
        if (needMips && willCompress) {
            // Image is compressed and mipmaps are desired, generate them
            // using software.
            buildMipmap(bitmap, willCompress);
        } else {
            if (willCompress) {
                // Image is compressed but mipmaps are not desired, upload directly.
                logger.log(Level.FINEST, " - Uploading compressed bitmap. Mipmaps are not generated.");
                uploadBitmapAsCompressed(target, 0, bitmap, subTexture, x, y);

            } else {
                // Image is not compressed, mipmaps may or may not be desired.
                logger.log(Level.FINEST, " - Uploading bitmap directly.{0}",
                        (needMips
                        ? " Mipmaps will be generated in HARDWARE"
                        : " Mipmaps are not generated."));
                if (subTexture) {
                    System.err.println("x : " + x + " y :" + y + " , " + bitmap.getWidth() + "/" + bitmap.getHeight());
                    GLUtils.texSubImage2D(target, 0, x, y, bitmap);
                    RendererUtil.checkGLError();
                } else {
                    GLUtils.texImage2D(target, 0, bitmap, 0);
                    RendererUtil.checkGLError();
                }

                if (needMips) {
                    // No pregenerated mips available,
                    // generate from base level if required
                    GLES20.glGenerateMipmap(target);
                    RendererUtil.checkGLError();
                }
            }
        }

        if (recycleBitmap) {
            bitmap.recycle();
        }
    }

    public static void uploadTextureAny(Image img, int target, int index, boolean needMips) {
        if (img.getEfficentData() instanceof AndroidImageInfo) {
            logger.log(Level.FINEST, " === Uploading image {0}. Using BITMAP PATH === ", img);
            // If image was loaded from asset manager, use fast path
            AndroidImageInfo imageInfo = (AndroidImageInfo) img.getEfficentData();
            uploadTextureBitmap(target, imageInfo.getBitmap(), needMips);
        } else {
            logger.log(Level.FINEST, " === Uploading image {0}. Using BUFFER PATH === ", img);
            boolean wantGeneratedMips = needMips && !img.hasMipmaps();
            if (wantGeneratedMips && img.getFormat().isCompressed()) {
                logger.log(Level.WARNING, "Generating mipmaps is only"
                        + " supported for Bitmap based or non-compressed images!");
            }

            // Upload using slower path
            logger.log(Level.FINEST, " - Uploading bitmap directly.{0}",
                    (wantGeneratedMips
                    ? " Mipmaps will be generated in HARDWARE"
                    : " Mipmaps are not generated."));
            
            uploadTexture(img, target, index);

            // Image was uploaded using slower path, since it is not compressed,
            // then compress it
            if (wantGeneratedMips) {
                // No pregenerated mips available,
                // generate from base level if required
                GLES20.glGenerateMipmap(target);
            }
        }
    }

    private static void unsupportedFormat(Format fmt) {
        throw new UnsupportedOperationException("The image format '" + fmt + "' is unsupported by the video hardware.");
    }

    public static AndroidGLImageFormat getImageFormat(Format fmt, boolean forRenderBuffer)
            throws UnsupportedOperationException {
        AndroidGLImageFormat imageFormat = new AndroidGLImageFormat();
        switch (fmt) {
            case Depth32:
            case Depth32F:
                throw new UnsupportedOperationException("The image format '"
                        + fmt + "' is not supported by OpenGL ES 2.0 specification.");
            case Alpha8:
                imageFormat.format = GLES20.GL_ALPHA;
                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
                if (RGBA8) {
                    imageFormat.renderBufferStorageFormat = GL_RGBA8;
                } else {
                    // Highest precision alpha supported by vanilla OGLES2
                    imageFormat.renderBufferStorageFormat = GLES20.GL_RGBA4;
                }
                break;
            case Luminance8:
                imageFormat.format = GLES20.GL_LUMINANCE;
                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
                if (RGBA8) {
                    imageFormat.renderBufferStorageFormat = GL_RGBA8;
                } else {
                    // Highest precision luminance supported by vanilla OGLES2
                    imageFormat.renderBufferStorageFormat = GLES20.GL_RGB565;
                }
                break;
            case Luminance8Alpha8:
                imageFormat.format = GLES20.GL_LUMINANCE_ALPHA;
                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
                if (RGBA8) {
                    imageFormat.renderBufferStorageFormat = GL_RGBA8;
                } else {
                    imageFormat.renderBufferStorageFormat = GLES20.GL_RGBA4;
                }
                break;
            case RGB565:
                imageFormat.format = GLES20.GL_RGB;
                imageFormat.dataType = GLES20.GL_UNSIGNED_SHORT_5_6_5;
                imageFormat.renderBufferStorageFormat = GLES20.GL_RGB565;
                break;
            case RGB5A1:
                imageFormat.format = GLES20.GL_RGBA;
                imageFormat.dataType = GLES20.GL_UNSIGNED_SHORT_5_5_5_1;
                imageFormat.renderBufferStorageFormat = GLES20.GL_RGB5_A1;
                break;
            case RGB8:
                imageFormat.format = GLES20.GL_RGB;
                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
                if (RGBA8) {
                    imageFormat.renderBufferStorageFormat = GL_RGBA8;
                } else {
                    // Fallback: Use RGB565 if RGBA8 is not available.
                    imageFormat.renderBufferStorageFormat = GLES20.GL_RGB565;
                }
                break;
            case BGR8:
                imageFormat.format = GLES20.GL_RGB;
                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
                if (RGBA8) {
                    imageFormat.renderBufferStorageFormat = GL_RGBA8;
                } else {
                    imageFormat.renderBufferStorageFormat = GLES20.GL_RGB565;
                }
                break;
            case RGBA8:
                imageFormat.format = GLES20.GL_RGBA;
                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
                if (RGBA8) {
                    imageFormat.renderBufferStorageFormat = GL_RGBA8;
                } else {
                    imageFormat.renderBufferStorageFormat = GLES20.GL_RGBA4;
                }
                break;
            case Depth:
            case Depth16:
                if (!DEPTH_TEXTURE && !forRenderBuffer) {
                    unsupportedFormat(fmt);
                }
                imageFormat.format = GLES20.GL_DEPTH_COMPONENT;
                imageFormat.dataType = GLES20.GL_UNSIGNED_SHORT;
                imageFormat.renderBufferStorageFormat = GLES20.GL_DEPTH_COMPONENT16;
                break;
            case Depth24:
            case Depth24Stencil8:
                if (!DEPTH_TEXTURE) {
                    unsupportedFormat(fmt);
                }
                if (DEPTH24_STENCIL8) {
                    // NEW: True Depth24 + Stencil8 format.
                    imageFormat.format = GL_DEPTH_STENCIL_OES;
                    imageFormat.dataType = GL_UNSIGNED_INT_24_8_OES;
                    imageFormat.renderBufferStorageFormat = GL_DEPTH24_STENCIL8_OES;
                } else {
                    // Vanilla OGLES2, only Depth16 available.
                    imageFormat.format = GLES20.GL_DEPTH_COMPONENT;
                    imageFormat.dataType = GLES20.GL_UNSIGNED_SHORT;
                    imageFormat.renderBufferStorageFormat = GLES20.GL_DEPTH_COMPONENT16;
                }
                break;
            case DXT1:
                if (!DXT1) {
                    unsupportedFormat(fmt);
                }
                imageFormat.format = GL_DXT1;
                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
                imageFormat.compress = true;
                break;
            case DXT1A:
                if (!DXT1) {
                    unsupportedFormat(fmt);
                }
                imageFormat.format = GL_DXT1A;
                imageFormat.dataType = GLES20.GL_UNSIGNED_BYTE;
                imageFormat.compress = true;
                break;
            default:
                throw new UnsupportedOperationException("Unrecognized format: " + fmt);
        }
        return imageFormat;
    }

    public static class AndroidGLImageFormat {

        boolean compress = false;
        int format = -1;
        int renderBufferStorageFormat = -1;
        int dataType = -1;
    }

    private static void uploadTexture(Image img,
            int target,
            int index) {

        if (img.getEfficentData() instanceof AndroidImageInfo) {
            throw new RendererException("This image uses efficient data. "
                    + "Use uploadTextureBitmap instead.");
        }

        // Otherwise upload image directly.
        // Prefer to only use power of 2 textures here to avoid errors.
        Image.Format fmt = img.getFormat();
        ByteBuffer data;
        if (index >= 0 || img.getData() != null && img.getData().size() > 0) {
            data = img.getData(index);
        } else {
            data = null;
        }

        int width = img.getWidth();
        int height = img.getHeight();

        if (!NPOT && img.isNPOT()) {
            // Check if texture is POT
            throw new RendererException("Non-power-of-2 textures "
                    + "are not supported by the video hardware "
                    + "and no scaling path available for image: " + img);
        }
        AndroidGLImageFormat imageFormat = getImageFormat(fmt, false);

        if (data != null) {
            GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
        }

        int[] mipSizes = img.getMipMapSizes();
        int pos = 0;
        if (mipSizes == null) {
            if (data != null) {
                mipSizes = new int[]{data.capacity()};
            } else {
                mipSizes = new int[]{width * height * fmt.getBitsPerPixel() / 8};
            }
        }

        for (int i = 0; i < mipSizes.length; i++) {
            int mipWidth = Math.max(1, width >> i);
            int mipHeight = Math.max(1, height >> i);

            if (data != null) {
                data.position(pos);
                data.limit(pos + mipSizes[i]);
            }

            if (imageFormat.compress && data != null) {
                GLES20.glCompressedTexImage2D(target,
                        i,
                        imageFormat.format,
                        mipWidth,
                        mipHeight,
                        0,
                        data.remaining(),
                        data);
            } else {
                GLES20.glTexImage2D(target,
                        i,
                        imageFormat.format,
                        mipWidth,
                        mipHeight,
                        0,
                        imageFormat.format,
                        imageFormat.dataType,
                        data);
            }

            pos += mipSizes[i];
        }
    }

    /**
     * Update the texture currently bound to target at with data from the given
     * Image at position x and y. The parameter index is used as the zoffset in
     * case a 3d texture or texture 2d array is being updated.
     *
     * @param image Image with the source data (this data will be put into the
     * texture)
     * @param target the target texture
     * @param index the mipmap level to update
     * @param x the x position where to put the image in the texture
     * @param y the y position where to put the image in the texture
     */
    public static void uploadSubTexture(
            Image img,
            int target,
            int index,
            int x,
            int y) {
        if (img.getEfficentData() instanceof AndroidImageInfo) {
            AndroidImageInfo imageInfo = (AndroidImageInfo) img.getEfficentData();
            uploadTextureBitmap(target, imageInfo.getBitmap(), true, true, x, y);
            return;
        }

        // Otherwise upload image directly.
        // Prefer to only use power of 2 textures here to avoid errors.
        Image.Format fmt = img.getFormat();
        ByteBuffer data;
        if (index >= 0 || img.getData() != null && img.getData().size() > 0) {
            data = img.getData(index);
        } else {
            data = null;
        }

        int width = img.getWidth();
        int height = img.getHeight();

        if (!NPOT && img.isNPOT()) {
            // Check if texture is POT
            throw new RendererException("Non-power-of-2 textures "
                    + "are not supported by the video hardware "
                    + "and no scaling path available for image: " + img);
        }
        AndroidGLImageFormat imageFormat = getImageFormat(fmt, false);

        if (data != null) {
            GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
        }

        int[] mipSizes = img.getMipMapSizes();
        int pos = 0;
        if (mipSizes == null) {
            if (data != null) {
                mipSizes = new int[]{data.capacity()};
            } else {
                mipSizes = new int[]{width * height * fmt.getBitsPerPixel() / 8};
            }
        }

        for (int i = 0; i < mipSizes.length; i++) {
            int mipWidth = Math.max(1, width >> i);
            int mipHeight = Math.max(1, height >> i);

            if (data != null) {
                data.position(pos);
                data.limit(pos + mipSizes[i]);
            }

            if (imageFormat.compress && data != null) {
                GLES20.glCompressedTexSubImage2D(target, i, x, y, mipWidth, mipHeight, imageFormat.format, data.remaining(), data);
                RendererUtil.checkGLError();
            } else {
                GLES20.glTexSubImage2D(target, i, x, y, mipWidth, mipHeight, imageFormat.format, imageFormat.dataType, data);
                RendererUtil.checkGLError();
            }

            pos += mipSizes[i];
        }
    }
}
