/*******************************************************************************
 * Copyright 2011 See AUTHORS file.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package com.badlogic.gdx.graphics.glutils;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.nio.ByteBuffer;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.BufferUtils;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.StreamUtils;

/** Class for encoding and decoding ETC1 compressed images. Also provides methods to add a PKM header.
 * @author mzechner */
public class ETC1 {
	/** The PKM header size in bytes **/
	public static int PKM_HEADER_SIZE = 16;
	public static int ETC1_RGB8_OES = 0x00008d64;

	/** Class for storing ETC1 compressed image data.
	 * @author mzechner */
	public final static class ETC1Data implements Disposable {
		/** the width in pixels **/
		public final int width;
		/** the height in pixels **/
		public final int height;
		/** the optional PKM header and compressed image data **/
		public final ByteBuffer compressedData;
		/** the offset in bytes to the actual compressed data. Might be 16 if this contains a PKM header, 0 otherwise **/
		public final int dataOffset;

		public ETC1Data (int width, int height, ByteBuffer compressedData, int dataOffset) {
			this.width = width;
			this.height = height;
			this.compressedData = compressedData;
			this.dataOffset = dataOffset;
			checkNPOT();
		}

		public ETC1Data (FileHandle pkmFile) {
			byte[] buffer = new byte[1024 * 10];
			DataInputStream in = null;
			try {
				in = new DataInputStream(new BufferedInputStream(new GZIPInputStream(pkmFile.read())));
				int fileSize = in.readInt();
				compressedData = BufferUtils.newUnsafeByteBuffer(fileSize);
				int readBytes = 0;
				while ((readBytes = in.read(buffer)) != -1) {
					compressedData.put(buffer, 0, readBytes);
				}
				compressedData.position(0);
				compressedData.limit(compressedData.capacity());
			} catch (Exception e) {
				throw new GdxRuntimeException("Couldn't load pkm file '" + pkmFile + "'", e);
			} finally {
				StreamUtils.closeQuietly(in);
			}

			width = getWidthPKM(compressedData, 0);
			height = getHeightPKM(compressedData, 0);
			dataOffset = PKM_HEADER_SIZE;
			compressedData.position(dataOffset);
			checkNPOT();
		}
		
		private void checkNPOT() {
			if(!MathUtils.isPowerOfTwo(width) || !MathUtils.isPowerOfTwo(height)) {
				Gdx.app.debug("ETC1Data", "warning: non-power-of-two ETC1 textures may crash the driver of PowerVR GPUs");
			}
		}

		/** @return whether this ETC1Data has a PKM header */
		public boolean hasPKMHeader () {
			return dataOffset == 16;
		}

		/** Writes the ETC1Data with a PKM header to the given file.
		 * @param file the file. */
		public void write (FileHandle file) {
			DataOutputStream write = null;
			byte[] buffer = new byte[10 * 1024];
			int writtenBytes = 0;
			compressedData.position(0);
			compressedData.limit(compressedData.capacity());
			try {
				write = new DataOutputStream(new GZIPOutputStream(file.write(false)));
				write.writeInt(compressedData.capacity());
				while (writtenBytes != compressedData.capacity()) {
					int bytesToWrite = Math.min(compressedData.remaining(), buffer.length);
					compressedData.get(buffer, 0, bytesToWrite);
					write.write(buffer, 0, bytesToWrite);
					writtenBytes += bytesToWrite;
				}
			} catch (Exception e) {
				throw new GdxRuntimeException("Couldn't write PKM file to '" + file + "'", e);
			} finally {
				StreamUtils.closeQuietly(write);
			}
			compressedData.position(dataOffset);
			compressedData.limit(compressedData.capacity());
		}

		/** Releases the native resources of the ETC1Data instance. */
		public void dispose () {
			BufferUtils.disposeUnsafeByteBuffer(compressedData);
		}

		public String toString () {
			if (hasPKMHeader()) {
				return (ETC1.isValidPKM(compressedData, 0) ? "valid" : "invalid") + " pkm [" + ETC1.getWidthPKM(compressedData, 0)
					+ "x" + ETC1.getHeightPKM(compressedData, 0) + "], compressed: "
					+ (compressedData.capacity() - ETC1.PKM_HEADER_SIZE);
			} else {
				return "raw [" + width + "x" + height + "], compressed: " + (compressedData.capacity() - ETC1.PKM_HEADER_SIZE);
			}
		}
	}

	private static int getPixelSize (Format format) {
		if (format == Format.RGB565) return 2;
		if (format == Format.RGB888) return 3;
		throw new GdxRuntimeException("Can only handle RGB565 or RGB888 images");
	}

	/** Encodes the image via the ETC1 compression scheme. Only {@link Format#RGB565} and {@link Format#RGB888} are supported.
	 * @param pixmap the {@link Pixmap}
	 * @return the {@link ETC1Data} */
	public static ETC1Data encodeImage (Pixmap pixmap) {
		int pixelSize = getPixelSize(pixmap.getFormat());
		ByteBuffer compressedData = encodeImage(pixmap.getPixels(), 0, pixmap.getWidth(), pixmap.getHeight(), pixelSize);
		BufferUtils.newUnsafeByteBuffer(compressedData);
		return new ETC1Data(pixmap.getWidth(), pixmap.getHeight(), compressedData, 0);
	}

	/** Encodes the image via the ETC1 compression scheme. Only {@link Format#RGB565} and {@link Format#RGB888} are supported. Adds
	 * a PKM header in front of the compressed image data.
	 * @param pixmap the {@link Pixmap}
	 * @return the {@link ETC1Data} */
	public static ETC1Data encodeImagePKM (Pixmap pixmap) {
		int pixelSize = getPixelSize(pixmap.getFormat());
		ByteBuffer compressedData = encodeImagePKM(pixmap.getPixels(), 0, pixmap.getWidth(), pixmap.getHeight(), pixelSize);
		BufferUtils.newUnsafeByteBuffer(compressedData);
		return new ETC1Data(pixmap.getWidth(), pixmap.getHeight(), compressedData, 16);
	}

	/** Takes ETC1 compressed image data and converts it to a {@link Format#RGB565} or {@link Format#RGB888} {@link Pixmap}. Does
	 * not modify the ByteBuffer's position or limit.
	 * @param etc1Data the {@link ETC1Data} instance
	 * @param format either {@link Format#RGB565} or {@link Format#RGB888}
	 * @return the Pixmap */
	public static Pixmap decodeImage (ETC1Data etc1Data, Format format) {
		int dataOffset = 0;
		int width = 0;
		int height = 0;

		if (etc1Data.hasPKMHeader()) {
			dataOffset = 16;
			width = ETC1.getWidthPKM(etc1Data.compressedData, 0);
			height = ETC1.getHeightPKM(etc1Data.compressedData, 0);
		} else {
			dataOffset = 0;
			width = etc1Data.width;
			height = etc1Data.height;
		}

		int pixelSize = getPixelSize(format);
		Pixmap pixmap = new Pixmap(width, height, format);
		decodeImage(etc1Data.compressedData, dataOffset, pixmap.getPixels(), 0, width, height, pixelSize);
		return pixmap;
	}

	// @off
	/*JNI
	#include <etc1/etc1_utils.h>
	#include <stdlib.h>
	 */

	/** @param width the width in pixels
	 * @param height the height in pixels
	 * @return the number of bytes needed to store the compressed data */
	public static native int getCompressedDataSize (int width, int height); /*
		return etc1_get_encoded_data_size(width, height);
	*/
	

	/** Writes a PKM header to the {@link ByteBuffer}. Does not modify the position or limit of the ByteBuffer.
	 * @param header the direct native order {@link ByteBuffer}
	 * @param offset the offset to the header in bytes
	 * @param width the width in pixels
	 * @param height the height in pixels */
	public static native void formatHeader (ByteBuffer header, int offset, int width, int height); /*
		etc1_pkm_format_header((etc1_byte*)header + offset, width, height);
	*/

	/** @param header direct native order {@link ByteBuffer} holding the PKM header
	 * @param offset the offset in bytes to the PKM header from the ByteBuffer's start
	 * @return the width stored in the PKM header */
	static native int getWidthPKM (ByteBuffer header, int offset); /*
		return etc1_pkm_get_width((etc1_byte*)header + offset);
	*/

	/** @param header direct native order {@link ByteBuffer} holding the PKM header
	 * @param offset the offset in bytes to the PKM header from the ByteBuffer's start
	 * @return the height stored in the PKM header */
	static native int getHeightPKM (ByteBuffer header, int offset); /*
		return etc1_pkm_get_height((etc1_byte*)header + offset);
	*/

	/** @param header direct native order {@link ByteBuffer} holding the PKM header
	 * @param offset the offset in bytes to the PKM header from the ByteBuffer's start
	 * @return the width stored in the PKM header */
	static native boolean isValidPKM (ByteBuffer header, int offset); /*
		return etc1_pkm_is_valid((etc1_byte*)header + offset) != 0?true:false;
	*/

	/** Decodes the compressed image data to RGB565 or RGB888 pixel data. Does not modify the position or limit of the
	 * {@link ByteBuffer} instances.
	 * @param compressedData the compressed image data in a direct native order {@link ByteBuffer}
	 * @param offset the offset in bytes to the image data from the start of the buffer
	 * @param decodedData the decoded data in a direct native order ByteBuffer, must hold width * height * pixelSize bytes.
	 * @param offsetDec the offset in bytes to the decoded image data.
	 * @param width the width in pixels
	 * @param height the height in pixels
	 * @param pixelSize the pixel size, either 2 (RBG565) or 3 (RGB888) */
	private static native void decodeImage (ByteBuffer compressedData, int offset, ByteBuffer decodedData, int offsetDec,
		int width, int height, int pixelSize); /*
		etc1_decode_image((etc1_byte*)compressedData + offset, (etc1_byte*)decodedData + offsetDec, width, height, pixelSize, width * pixelSize);
	*/

	/** Encodes the image data given as RGB565 or RGB888. Does not modify the position or limit of the {@link ByteBuffer}.
	 * @param imageData the image data in a direct native order {@link ByteBuffer}
	 * @param offset the offset in bytes to the image data from the start of the buffer
	 * @param width the width in pixels
	 * @param height the height in pixels
	 * @param pixelSize the pixel size, either 2 (RGB565) or 3 (RGB888)
	 * @return a new direct native order ByteBuffer containing the compressed image data */
	private static native ByteBuffer encodeImage (ByteBuffer imageData, int offset, int width, int height, int pixelSize); /*
		int compressedSize = etc1_get_encoded_data_size(width, height);
		etc1_byte* compressedData = (etc1_byte*)malloc(compressedSize);
		etc1_encode_image((etc1_byte*)imageData + offset, width, height, pixelSize, width * pixelSize, compressedData);
		return env->NewDirectByteBuffer(compressedData, compressedSize);
	*/

	/** Encodes the image data given as RGB565 or RGB888. Does not modify the position or limit of the {@link ByteBuffer}.
	 * @param imageData the image data in a direct native order {@link ByteBuffer}
	 * @param offset the offset in bytes to the image data from the start of the buffer
	 * @param width the width in pixels
	 * @param height the height in pixels
	 * @param pixelSize the pixel size, either 2 (RGB565) or 3 (RGB888)
	 * @return a new direct native order ByteBuffer containing the compressed image data */
	private static native ByteBuffer encodeImagePKM (ByteBuffer imageData, int offset, int width, int height, int pixelSize); /*
		int compressedSize = etc1_get_encoded_data_size(width, height);
		etc1_byte* compressed = (etc1_byte*)malloc(compressedSize + ETC_PKM_HEADER_SIZE);
		etc1_pkm_format_header(compressed, width, height);
		etc1_encode_image((etc1_byte*)imageData + offset, width, height, pixelSize, width * pixelSize, compressed + ETC_PKM_HEADER_SIZE);
		return env->NewDirectByteBuffer(compressed, compressedSize + ETC_PKM_HEADER_SIZE);
	*/
}
