package org.anddev.andengine.opengl.texture;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;

import javax.microedition.khronos.opengles.GL10;

import org.anddev.andengine.util.Debug;

/**
 * (c) 2010 Nicolas Gramlich 
 * (c) 2011 Zynga Inc.
 * 
 * @author Nicolas Gramlich
 * @since 17:48:46 - 08.03.2010
 */
public class TextureManager {
	// ===========================================================
	// Constants
	// ===========================================================

	// ===========================================================
	// Fields
	// ===========================================================

	private final HashSet<ITexture> mTexturesManaged = new HashSet<ITexture>();

	private final ArrayList<ITexture> mTexturesLoaded = new ArrayList<ITexture>();

	private final ArrayList<ITexture> mTexturesToBeLoaded = new ArrayList<ITexture>();
	private final ArrayList<ITexture> mTexturesToBeUnloaded = new ArrayList<ITexture>();

	// ===========================================================
	// Constructors
	// ===========================================================

	// ===========================================================
	// Getter & Setter
	// ===========================================================

	// ===========================================================
	// Methods for/from SuperClass/Interfaces
	// ===========================================================

	// ===========================================================
	// Methods
	// ===========================================================

	protected synchronized void clear() {
		this.mTexturesToBeLoaded.clear();
		this.mTexturesLoaded.clear();
		this.mTexturesManaged.clear();
	}

	/**
	 * @param pTexture the {@link ITexture} to be loaded before the very next frame is drawn (Or prevent it from being unloaded then).
	 * @return <code>true</code> when the {@link ITexture} was previously not managed by this {@link TextureManager}, <code>false</code> if it was already managed.
	 */
	public synchronized boolean loadTexture(final ITexture pTexture) {
		if(this.mTexturesManaged.contains(pTexture)) {
			/* Just make sure it doesn't get deleted. */
			this.mTexturesToBeUnloaded.remove(pTexture);
			return false;
		} else {
			this.mTexturesManaged.add(pTexture);
			this.mTexturesToBeLoaded.add(pTexture);
			return true;
		}
	}

	/**
	 * @param pTexture the {@link ITexture} to be unloaded before the very next frame is drawn (Or prevent it from being loaded then).
	 * @return <code>true</code> when the {@link ITexture} was already managed by this {@link TextureManager}, <code>false</code> if it was not managed.
	 */
	public synchronized boolean unloadTexture(final ITexture pTexture) {
		if(this.mTexturesManaged.contains(pTexture)) {
			/* If the Texture is loaded, unload it.
			 * If the Texture is about to be loaded, stop it from being loaded. */
			if(this.mTexturesLoaded.contains(pTexture)){
				this.mTexturesToBeUnloaded.add(pTexture);
			} else if(this.mTexturesToBeLoaded.remove(pTexture)){
				this.mTexturesManaged.remove(pTexture);
			}
			return true;
		} else {
			return false;
		}
	}

	public void loadTextures(final ITexture ... pTextures) {
		for(int i = pTextures.length - 1; i >= 0; i--) {
			this.loadTexture(pTextures[i]);
		}
	}

	public void unloadTextures(final ITexture ... pTextures) {
		for(int i = pTextures.length - 1; i >= 0; i--) {
			this.unloadTexture(pTextures[i]);
		}
	}

	public synchronized void reloadTextures() {
		final HashSet<ITexture> managedTextures = this.mTexturesManaged;
		for(final ITexture texture : managedTextures) { // TODO Can the use of the iterator be avoided somehow?
			texture.setLoadedToHardware(false);
		}

		this.mTexturesToBeLoaded.addAll(this.mTexturesLoaded); // TODO Check if addAll uses iterator internally!
		this.mTexturesLoaded.clear();

		this.mTexturesManaged.removeAll(this.mTexturesToBeUnloaded); // TODO Check if removeAll uses iterator internally!
		this.mTexturesToBeUnloaded.clear();
	}

	public synchronized void updateTextures(final GL10 pGL) {
		final HashSet<ITexture> texturesManaged = this.mTexturesManaged;
		final ArrayList<ITexture> texturesLoaded = this.mTexturesLoaded;
		final ArrayList<ITexture> texturesToBeLoaded = this.mTexturesToBeLoaded;
		final ArrayList<ITexture> texturesToBeUnloaded = this.mTexturesToBeUnloaded;

		/* First reload Textures that need to be updated. */
		final int textursLoadedCount = texturesLoaded.size();

		if(textursLoadedCount > 0){
			for(int i = textursLoadedCount - 1; i >= 0; i--){
				final ITexture textureToBeReloaded = texturesLoaded.get(i);
				if(textureToBeReloaded.isUpdateOnHardwareNeeded()){
					try {
						textureToBeReloaded.reloadToHardware(pGL);
					} catch(IOException e) {
						Debug.e(e);
					}
				}
			}
		}

		/* Then load pending Textures. */
		final int texturesToBeLoadedCount = texturesToBeLoaded.size();

		if(texturesToBeLoadedCount > 0){
			for(int i = texturesToBeLoadedCount - 1; i >= 0; i--){
				final ITexture textureToBeLoaded = texturesToBeLoaded.remove(i);
				if(!textureToBeLoaded.isLoadedToHardware()){
					try {
						textureToBeLoaded.loadToHardware(pGL);
					} catch(IOException e) {
						Debug.e(e);
					}
				}

				if (textureToBeLoaded.isLoadedToHardware())
					texturesLoaded.add(textureToBeLoaded);
				else
					texturesToBeUnloaded.add(textureToBeLoaded);
			}
		}

		/* Then unload pending Textures. */
		final int texturesToBeUnloadedCount = texturesToBeUnloaded.size();

		if(texturesToBeUnloadedCount > 0){
			for(int i = texturesToBeUnloadedCount - 1; i >= 0; i--){
				final ITexture textureToBeUnloaded = texturesToBeUnloaded.remove(i);
				if(textureToBeUnloaded.isLoadedToHardware()){
					textureToBeUnloaded.unloadFromHardware(pGL);
				}
				texturesLoaded.remove(textureToBeUnloaded);
				texturesManaged.remove(textureToBeUnloaded);
			}
		}

		/* Finally invoke the GC if anything has changed. */
		if(texturesToBeLoadedCount > 0 || texturesToBeUnloadedCount > 0){
			System.gc();
		}
	}

	// ===========================================================
	// Inner and Anonymous Classes
	// ===========================================================
}
