/**
 * BobEngine - 2D game engine for Android
 *
 * Copyright (C) 2014, 2015, 2016 Benjamin Blaszczak
 *
 * BobEngine is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser Public License
 * version 2.1 as published by the free software foundation.
 *
 * BobEngine is provided without warranty; without even the implied
 * warranty of merchantability or fitness for a particular
 * purpose. See the GNU Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General
 * Public License along with BobEngine; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth
 * Floor, Boston, MA 02110-1301 USA
 *
 */
package com.bobbyloujo.bobengine.graphics;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.PortUnreachableException;

import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;
import android.util.Log;

/**
 * This class helps load graphics. Every BobView has it's own GraphicsHelper.
 * Use getGraphicsHelper() in a BobView to get it's GraphicsHelper.
 * 
 * @author Ben
 * 
 */
public class GraphicsHelper {

	// Constants
	public final static int MIN_SMOOTH = GL11.GL_LINEAR;
	public final static int MIN_SMOOTH_MIPMAP = GL11.GL_LINEAR_MIPMAP_LINEAR;
	public final static int MAG_SMOOTH = GL11.GL_LINEAR;
	public final static int MIN_PIXEL = GL11.GL_NEAREST;
	public final static int MIN_PIXEL_MIPMAP = GL11.GL_NEAREST_MIPMAP_NEAREST;
	public final static int MAG_PIXEL = GL11.GL_NEAREST;

	private final static int START_NUM_TEX = 50;     // Starting maximum number of textures (graphics)
	public final static int DEF_CLEANUPS = 2;        // Default number of cleanups until a graphic is removed.

	// Variables
	private int numGFX;                              // Number of added graphics
	private Graphic[] graphics;                      // Textures as drawables
	private boolean useMipMaps;                      // Flag indicates if added graphics should be mip mapped
	private boolean repeating;                       // Flag indicates if added graphics should repeat instead of clamping to the edge.
	private int magFilter;                           // Upscale filter to use
	private int minFilter;                           // Downscale filter to use
	private int	cleanupsTilRemoval;                  // Number of cleanups until a graphic is removed.

	// Object
	private Context context;
	private Graphic defGraphic;

	public GraphicsHelper(Context context) {
		this.context = context;
		defGraphic = new Graphic();

		numGFX = 0;
		graphics = new Graphic[START_NUM_TEX];

		useMipMaps = true;
		repeating = true;
		magFilter = GL11.GL_LINEAR;
		minFilter = GL11.GL_LINEAR_MIPMAP_LINEAR;
		cleanupsTilRemoval = DEF_CLEANUPS;
	}

	/**
	 * Specify the texture parameters for all graphics loaded after this method
	 * is called. <br/>
	 * <br/>
	 * 
	 * For 'retro' pixelated graphics, use (false, GL11.GL_NEAREST,
	 * GL11.GL_NEAREST)
	 * 
	 * @param useMipMaps
	 *            - true = better performance. Is already true by default.
	 * @param minFilter
	 *            - Filter to use for downscaling. Must be one of the OpenGL
	 *            filters (eg. GL11.GL_LINEAR_MIPMAP_LINEAR (default))
	 * @param magFilter
	 *            - Filter for upscaling. Must be an OpenGL filter (eg.
	 *            GL11.GL_LINEAR (default))
	 */
	public void setParameters(boolean useMipMaps, int minFilter, int magFilter, boolean repeating) {
		this.useMipMaps = useMipMaps;
		this.minFilter = minFilter;
		this.magFilter = magFilter;
		this.repeating = repeating;
	}

	/**
	 * Returns a default graphic.
	 * @return
	 */
	public Graphic getDefaultGraphic() {
		return defGraphic;
	}

	/**
	 * Create a usable graphic from a drawable image.
	 *
	 * @param drawable
	 *            - Drawable resource in R.drawable.&#42;
	 * @param shouldLoad
	 *            - Can be set to false if you don't want the graphic to be loaded right away.
	 * @return A Graphic object containing information about the newly created
	 *         graphic. Store this somewhere where it can be accessed by
	 *         GameObjects (Like as a static property in a BobView).
	 */
	public Graphic getGraphic(int drawable, boolean shouldLoad) {
		// Data
		int graphic = 1;

		Graphic alreadyAdded = findGraphic(drawable, useMipMaps, minFilter, magFilter, repeating);
		if (alreadyAdded != null) {
			return alreadyAdded;
		}

		numGFX++;

		if (numGFX >= graphics.length) {                           // Hit max graphics
			cleanUp();                                             // Try to get rid of some that haven't been used recently

			if (numGFX >= graphics.length) {                       // Still too many, increase size of graphics
				Graphic[] temp = graphics;
				graphics = new Graphic[graphics.length + START_NUM_TEX];

				for (int i = 0; i < temp.length; i++) {
					graphics[i] = temp[i];
				}
			}
		}

		for (int i = 1; i < graphics.length; i++) {
			if (graphics[i] == null) {
				graphic = i;
				break;
			}
		}

		try {
			// Load the bitmap just to get the height and width
			Bitmap bmp = null;
			InputStream is = context.getResources().openRawResource(drawable);

			try {
				bmp = BitmapFactory.decodeStream(is);
			} catch (Exception e) {
				Log.e("BobEngine", "Failed to load graphic.");
				e.printStackTrace();
			} finally {
				try {
					is.close();
				} catch (IOException e) {
					e.printStackTrace();
				}
			}

			if (bmp != null){
				graphics[graphic] = new Graphic(drawable, bmp.getHeight(), bmp.getWidth(), minFilter, magFilter, useMipMaps, repeating);
				bmp.recycle();
			}
		} catch (OutOfMemoryError e) {
			graphics[graphic] = new Graphic(drawable, 100, 100, minFilter, magFilter, useMipMaps, repeating);
			Log.e("BobEngine", "Image too large. Unable to get height and width.");
		}

		if (shouldLoad) graphics[graphic].load();

		return graphics[graphic];
	}

	/**
	 * Create a usable graphic from a drawable image. This graphic will be loaded when the view is created.
	 *
	 * @param drawable
	 *            - Drawable resource in R.drawable.&#42;
	 * @return A Graphic object containing information about the newly created
	 *         graphic. Store this somewhere where it can be accessed by
	 *         GameObjects (Like as a static property in a BobView).
	 */
	public Graphic getGraphic(int drawable) {
		return getGraphic(drawable, true);
	}

	/**
	 * Create a usable graphic from a drawable image.
	 * 
	 * @param drawable
	 *            - Drawable resource in R.drawable.&#42;
     * @param shouldLoad
     *            - Can be set to false if you don't want the graphic to be loaded right away.
	 * @return A Graphic object containing information about the newly created
	 *         graphic. Store this somewhere where it can be accessed by
	 *         GameObjects (Like as a static property in a BobView).
	 */
	@Deprecated
	public Graphic addGraphic(int drawable, boolean shouldLoad) {
		return getGraphic(drawable, shouldLoad);
	}

    /**
     * Create a usable graphic from a drawable image. This graphic will be loaded when the view is created.
     *
     * @param drawable
     *            - Drawable resource in R.drawable.&#42;
     * @return A Graphic object containing information about the newly created
     *         graphic. Store this somewhere where it can be accessed by
     *         GameObjects (Like as a static property in a BobView).
     */
	@Deprecated
    public Graphic addGraphic(int drawable) {
        return getGraphic(drawable, true);
    }

	/**
	 * Add a graphic object. graphic may be assigned a new ID.
	 * @param graphic
	 */
	public void addGraphic(Graphic graphic) {
		// Data
		int g = 1;

		Graphic alreadyAdded = findGraphic(graphic.drawable, graphic.useMipMaps, graphic.minFilter, graphic.magFilter, graphic.repeating);
		if (alreadyAdded != null) {
			graphic.id = alreadyAdded.id;
			graphic.indicateUsed(cleanupsTilRemoval);
			return;
		}

		numGFX++;

		if (numGFX >= graphics.length) {                           // Hit max graphics
			cleanUp();                                             // Try to get rid of some that haven't been used recently

			if (numGFX >= graphics.length) {                       // Still too many, increase size of graphics
				Graphic[] temp = graphics;
				graphics = new Graphic[graphics.length + START_NUM_TEX];

				for (int i = 0; i < temp.length; i++) {
					graphics[i] = temp[i];
				}
			}
		}

		for (int i = 1; i < graphics.length; i++) {
			if (graphics[i] == null) {
				g = i;
				break;
			}
		}

		graphic.id = g;
		graphic.indicateUsed(cleanupsTilRemoval);
		graphics[g] = graphic;
	}

	/**
	 * Signify that a graphic should be removed from the list.
	 * @param graphic
	 */
	public void removeGraphic(Graphic graphic) {
		if (graphics[graphic.id] == graphic) graphics[graphic.id].remove();
	}

	/**
	 * Get the graphic created from a drawable that has already been added.
	 *
	 * @param drawable The drawable to find
	 * @return A graphic object created from the drawable or null if the drawable has not been added.
	 */
	public Graphic findGraphic(int drawable) {
		for (int i = 0; i < graphics.length; i++) {
			if (graphics[i] != null && graphics[i].drawable == drawable) return graphics[i];
		}

		return null;
	}

	/**
	 * Get the graphic created from a drawable and with the same parameters that has already been added.
	 *
	 * @param drawable The drawable to find
	 * @return A graphic object created from the drawable or null if the drawable has not been added.
	 */
	public Graphic findGraphic(int drawable, boolean useMipMaps, int minFilter, int magFilter, boolean repeating) {
		for (int i = 0; i < graphics.length; i++) {
			if (graphics[i] != null
					&& graphics[i].drawable == drawable
					&& graphics[i].useMipMaps == useMipMaps
					&& graphics[i].minFilter == minFilter
					&& graphics[i].magFilter == magFilter
					&& graphics[i].repeating == repeating) {
				return graphics[i];
			}
		}

		return null;
	}

	/**
	 * Set the number of cleanups since a graphic has last been used needed
	 * to remove the graphic from the GraphicHelper.
	 * @param cleanups
	 */
	public void setCleanupsTilRemoval(int cleanups) {
		cleanupsTilRemoval = cleanups;
	}

	/**
	 * Get the number of cleanups since a graphic has last been used needed
	 * to remove the graphic from the GraphicHelper.
	 * @return
	 */
	public int getCleanupsTilRemoval() {
		return cleanupsTilRemoval;
	}

	/**
	 * Will find all graphics that have not been recently used and mark them for removal.
	 */
	public void cleanUp() {
		for (int i = 1; i < graphics.length; i++) {
			if (graphics[i] != null) {
				graphics[i].cleanup();
			}
		}
	}

	/**
	 * Perform outstanding graphic commands (load, unload, remove).
	 * 
	 * @param gl
	 */
	public void handleGraphics(GL11 gl) {
		int sampleSize = 1;
		boolean success = false;
		boolean changed = false;

		do {
			try {
				for (int g = 0; g < graphics.length; g++) {
					if (graphics[g] != null) {
						if (graphics[g].shouldLoad()) {                             // Should we load it?
							loadGraphic(gl, g, sampleSize);
							changed = true;
						} else if (graphics[g].shouldUnload()) {                    // Should we unload it?
							unloadGraphic(gl, g);
							changed = true;
						} else if (graphics[g].shouldRemove()) {
							unloadGraphic(gl, g);
							graphics[g].removed();
							graphics[g] = null;
							numGFX--;
							changed = true;
						}
					}
				}
				
				success = true;
			} catch (OutOfMemoryError e) {  // Not enough memory to load all the graphics. BobEngine will try down sampling them.
				sampleSize++;
				Log.e("BobEngine", "Not enough memory. Retrying in sample size " + Integer.toString(sampleSize));
				success = false;
			}
		} while (!success);  // Try again

		if (changed) gl.glFinish();
	}

	public void loadAllGraphics(GL10 gl) {
		int sampleSize = 1;
		boolean success = false;
		do {
			try {
				for (int g = 0; g < graphics.length; g++) {
					if (graphics[g] != null) {
						loadGraphic((GL11) gl, g, sampleSize);
					}
				}

				success = true;
			} catch (OutOfMemoryError e) {  // Not enough memory to load all the graphics. BobEngine will try down sampling them.
				sampleSize++;
				Log.e("BobEngine", "Not enough memory. Retrying in sample size " + Integer.toString(sampleSize));
				success = false;
			}
		} while (!success);  // Try again

		gl.glFinish();
	}

	/**
	 * Load a particular graphic.
	 * 
	 * @param gl The OpenGL object to handle gl functions
	 * @param g The index of the graphic in graphics[] to load
	 * @param sampleSize The sample size to load the graphic.
	 */
	private void loadGraphic(GL11 gl, int g, int sampleSize) {
		Bitmap bmp;
		BitmapFactory.Options op = new BitmapFactory.Options();
		op.inSampleSize = sampleSize;

		InputStream is = context.getResources().openRawResource(graphics[g].drawable);

		try {
			bmp = BitmapFactory.decodeStream(is, null, op);
		} finally {
			try {
				is.close();
			} catch (IOException e) {
				Log.e("BobEngine", "Failed to load graphic.");
				e.printStackTrace();
			}
		}

		// Generate an ID for the graphic
		final int[] texID = new int[1];
		gl.glGenTextures(1, texID, 0);
		graphics[g].id = texID[0];

		// Tell openGL which texture we are working with
		gl.glBindTexture(GL11.GL_TEXTURE_2D, graphics[g].id);

		// Create mipmaps and set texture parameters.
		gl.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, graphics[g].minFilter);                 // Filtering for downscaling
		gl.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, graphics[g].magFilter);                 // Upscale filtering
		if (graphics[g].useMipMaps) gl.glTexParameterx(GL11.GL_TEXTURE_2D, GL11.GL_GENERATE_MIPMAP, GL11.GL_TRUE); // Use mipmapping

		// Texture wrapping
		if (graphics[g].repeating) {
			gl.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);
			gl.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);
		} else {
			gl.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_CLAMP_TO_EDGE);
			gl.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_CLAMP_TO_EDGE);
		}

		// This assigns bmp to the texture we are working with (g)
		GLUtils.texImage2D(GL11.GL_TEXTURE_2D, 0, bmp, 0);

		// Set the face rotation
		gl.glFrontFace(GL11.GL_CCW);

		bmp.recycle();

		graphics[g].loaded();

		gl.glFinish();
	}

	/**
	 * Unload a particular graphic.
	 * @param gl OpenGL object for unloading
	 * @param g The index of the graphic to delete.
	 */
	private void unloadGraphic(GL11 gl, int g) {
		int[] tex = { graphics[g].id };
		gl.glDeleteTextures(1, tex, 0);
		graphics[g].deleted();
	}

	/**
	 * Returns the number of added graphics. Included graphics that aren't loaded.
	 */
	public int getNumGraphics() {
		return numGFX;
	}
}