/*
 * Copyright (c) 2009-2012 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.asset;

import com.jme3.asset.cache.AssetCache;
import com.jme3.asset.cache.SimpleAssetCache;
import com.jme3.audio.AudioData;
import com.jme3.audio.AudioKey;
import com.jme3.font.BitmapFont;
import com.jme3.material.Material;
import com.jme3.post.FilterPostProcessor;
import com.jme3.renderer.Caps;
import com.jme3.scene.Spatial;
import com.jme3.shader.Glsl100ShaderGenerator;
import com.jme3.shader.Glsl150ShaderGenerator;
import com.jme3.shader.Shader;
import com.jme3.shader.ShaderGenerator;
import com.jme3.shader.ShaderKey;
import com.jme3.system.JmeSystem;
import com.jme3.texture.Texture;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * <code>AssetManager</code> is the primary method for managing and loading
 * assets inside jME.
 *
 * @author Kirill Vainer
 */
public class DesktopAssetManager implements AssetManager {

    private static final Logger logger = Logger.getLogger(AssetManager.class.getName());
    private ShaderGenerator shaderGenerator;
    
    private final ImplHandler handler = new ImplHandler(this);

    private CopyOnWriteArrayList<AssetEventListener> eventListeners = 
            new CopyOnWriteArrayList<AssetEventListener>();
    
    private List<ClassLoader> classLoaders =
            Collections.synchronizedList(new ArrayList<ClassLoader>());

    public DesktopAssetManager(){
        this(null);
    }
    
    public DesktopAssetManager(boolean usePlatformConfig){
        this(usePlatformConfig ? JmeSystem.getPlatformAssetConfigURL() : null);
    }

    public DesktopAssetManager(URL configFile){
        if (configFile != null){
            loadConfigFile(configFile);
        }        
        logger.fine("DesktopAssetManager created.");
    }

    private void loadConfigFile(URL configFile) {
        try {
            AssetConfig.loadText(this, configFile);
        } catch (IOException ex) {
            logger.log(Level.SEVERE, "Failed to load asset config", ex);
        }
    }
    
    public void addClassLoader(ClassLoader loader) {
        classLoaders.add(loader);
    }
    
    public void removeClassLoader(ClassLoader loader) {
        classLoaders.remove(loader);
    }

    public List<ClassLoader> getClassLoaders(){
        return Collections.unmodifiableList(classLoaders);
    }
    
    public void addAssetEventListener(AssetEventListener listener) {
        eventListeners.add(listener);
    }

    public void removeAssetEventListener(AssetEventListener listener) {
        eventListeners.remove(listener);
    }

    public void clearAssetEventListeners() {
        eventListeners.clear();
    }
    
    public void setAssetEventListener(AssetEventListener listener){
        eventListeners.clear();
        eventListeners.add(listener);
    }

    public void registerLoader(Class<? extends AssetLoader> loader, String ... extensions){
        handler.addLoader(loader, extensions);
        if (logger.isLoggable(Level.FINER)){
            logger.log(Level.FINER, "Registered loader: {0} for extensions {1}",
              new Object[]{loader.getSimpleName(), Arrays.toString(extensions)});
        }
    }

    public void registerLoader(String clsName, String ... extensions){
        Class<? extends AssetLoader> clazz = null;
        try{
            clazz = (Class<? extends AssetLoader>) Class.forName(clsName);
        }catch (ClassNotFoundException ex){
            logger.log(Level.WARNING, "Failed to find loader: "+clsName, ex);
        }catch (NoClassDefFoundError ex){
            logger.log(Level.WARNING, "Failed to find loader: "+clsName, ex);
        }
        if (clazz != null){
            registerLoader(clazz, extensions);
        }
    }
    
    public void unregisterLoader(Class<? extends AssetLoader> loaderClass) {
        handler.removeLoader(loaderClass);
        if (logger.isLoggable(Level.FINER)){
            logger.log(Level.FINER, "Unregistered loader: {0}",
                    loaderClass.getSimpleName());
        }
    }

    public void registerLocator(String rootPath, Class<? extends AssetLocator> locatorClass){
        handler.addLocator(locatorClass, rootPath);
        if (logger.isLoggable(Level.FINER)){
            logger.log(Level.FINER, "Registered locator: {0}",
                    locatorClass.getSimpleName());
        }
    }

    public void registerLocator(String rootPath, String clsName){
        Class<? extends AssetLocator> clazz = null;
        try{
            clazz = (Class<? extends AssetLocator>) Class.forName(clsName);
        }catch (ClassNotFoundException ex){
            logger.log(Level.WARNING, "Failed to find locator: "+clsName, ex);
        }catch (NoClassDefFoundError ex){
            logger.log(Level.WARNING, "Failed to find loader: "+clsName, ex);
        }
        if (clazz != null){
            registerLocator(rootPath, clazz);
        }
    }
    
    public void unregisterLocator(String rootPath, Class<? extends AssetLocator> clazz){
        handler.removeLocator(clazz, rootPath);
        if (logger.isLoggable(Level.FINER)){
            logger.log(Level.FINER, "Unregistered locator: {0}",
                    clazz.getSimpleName());
        }
    }
    
    public AssetInfo locateAsset(AssetKey<?> key){
        AssetInfo info = handler.tryLocate(key);
        if (info == null){
            logger.log(Level.WARNING, "Cannot locate resource: {0}", key);
        }
        return info;
    }
    
    @Override
    public <T> T getFromCache(AssetKey<T> key) {
        AssetCache cache = handler.getCache(key.getCacheType());
        if (cache != null) {
            T asset = cache.getFromCache(key);
            if (asset != null) {
                // Since getFromCache fills the load stack, it has to be popped
                cache.notifyNoAssetClone();
            }
            return asset;
        } else {
            throw new IllegalArgumentException("Key " + key + " specifies no cache.");
        }
    }
    
    @Override
    public <T> void addToCache(AssetKey<T> key, T asset) {
        AssetCache cache = handler.getCache(key.getCacheType());
        if (cache != null) {
            cache.addToCache(key, asset);
            cache.notifyNoAssetClone();
        } else {
            throw new IllegalArgumentException("Key " + key + " specifies no cache.");
        }
    }
    
    @Override
    public <T> boolean deleteFromCache(AssetKey<T> key) {
        AssetCache cache = handler.getCache(key.getCacheType());
        if (cache != null) {
            return cache.deleteFromCache(key);
        } else {
            throw new IllegalArgumentException("Key " + key + " specifies no cache.");
        }
    }
    
    @Override
    public void clearCache(){
        handler.clearCache();
        if (logger.isLoggable(Level.FINER)){
            logger.log(Level.FINER, "All asset caches cleared.");
        }
    }

    /**
     * Loads an asset that has already been located.
     * @param <T> The asset type
     * @param key The asset key
     * @param info The AssetInfo from the locator
     * @param proc AssetProcessor to use, or null to disable processing
     * @param cache The cache to store the asset in, or null to disable caching
     * @return The loaded asset
     * 
     * @throws AssetLoadException If failed to load asset due to exception or
     * other error.
     */
    protected <T> T loadLocatedAsset(AssetKey<T> key, AssetInfo info, AssetProcessor proc, AssetCache cache) {
        AssetLoader loader = handler.aquireLoader(key);
        Object obj;
        try {
            handler.establishParentKey(key);
            obj = loader.load(info);
        } catch (IOException ex) {
            throw new AssetLoadException("An exception has occured while loading asset: " + key, ex);
        } finally {
            handler.releaseParentKey(key);
        }
        if (obj == null) {
            throw new AssetLoadException("Error occured while loading asset \""
                    + key + "\" using " + loader.getClass().getSimpleName());
        } else {
            if (logger.isLoggable(Level.FINER)) {
                logger.log(Level.FINER, "Loaded {0} with {1}",
                        new Object[]{key, loader.getClass().getSimpleName()});
            }

            if (proc != null) {
                // do processing on asset before caching
                obj = proc.postProcess(key, obj);
            }

            if (cache != null) {
                // At this point, obj should be of type T
                cache.addToCache(key, (T) obj);
            }

            for (AssetEventListener listener : eventListeners) {
                listener.assetLoaded(key);
            }

            return (T) obj;
        }
    }
    
    /**
     * Clones the asset using the given processor and registers the clone
     * with the cache.
     * 
     * @param <T> The asset type
     * @param key The asset key
     * @param obj The asset to clone / register, must implement 
     * {@link CloneableSmartAsset}.
     * @param proc The processor which will generate the clone, cannot be null
     * @param cache The cache to register the clone with, cannot be null.
     * @return The cloned asset, cannot be the same as the given asset since
     * it is a clone.
     * 
     * @throws IllegalStateException If asset does not implement 
     * {@link CloneableSmartAsset}, if the cache is null, or if the 
     * processor did not clone the asset.
     */
    protected <T> T registerAndCloneSmartAsset(AssetKey<T> key, T obj, AssetProcessor proc, AssetCache cache) {
        // object obj is the original asset
        // create an instance for user
        if (proc == null) {
            throw new IllegalStateException("Asset implements "
                    + "CloneableSmartAsset but doesn't "
                    + "have processor to handle cloning");
        } else {
            T clone = (T) proc.createClone(obj);
            if (cache != null && clone != obj) {
                cache.registerAssetClone(key, clone);
            } else {
                throw new IllegalStateException("Asset implements "
                        + "CloneableSmartAsset but doesn't have cache or "
                        + "was not cloned");
            }
            return clone;
        }
    }
    
    @Override
    public <T> T loadAssetFromStream(AssetKey<T> key, InputStream inputStream) {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        }
        
        for (AssetEventListener listener : eventListeners){
            listener.assetRequested(key);
        }
        
        AssetProcessor proc = handler.getProcessor(key.getProcessorType());
        StreamAssetInfo info = new StreamAssetInfo(this, key, inputStream);
        return loadLocatedAsset(key, info, proc, null);
    }
    
    @Override
    public <T> T loadAsset(AssetKey<T> key){
        if (key == null)
            throw new IllegalArgumentException("key cannot be null");
        
        for (AssetEventListener listener : eventListeners){
            listener.assetRequested(key);
        }
        
        AssetCache cache = handler.getCache(key.getCacheType());
        AssetProcessor proc = handler.getProcessor(key.getProcessorType());
        
        Object obj = cache != null ? cache.getFromCache(key) : null;
        if (obj == null){
            // Asset not in cache, load it from file system.
            AssetInfo info = handler.tryLocate(key);
            if (info == null){
                if (handler.getParentKey() != null){
                    // Inform event listener that an asset has failed to load.
                    // If the parent AssetLoader chooses not to propagate
                    // the exception, this is the only means of finding
                    // that something went wrong.
                    for (AssetEventListener listener : eventListeners){
                        listener.assetDependencyNotFound(handler.getParentKey(), key);
                    }
                }
                throw new AssetNotFoundException(key.toString());
            }
            
            obj = loadLocatedAsset(key, info, proc, cache);
        }

        T clone = (T) obj;
        
        if (obj instanceof CloneableSmartAsset) {
            clone = registerAndCloneSmartAsset(key, clone, proc, cache);
        }
        
        return clone;
    }

    public Object loadAsset(String name){
        return loadAsset(new AssetKey(name));
    }

    public Texture loadTexture(TextureKey key){                
        return (Texture) loadAsset(key);
    }

    public Material loadMaterial(String name){
        return (Material) loadAsset(new MaterialKey(name));
    }

    public Texture loadTexture(String name){
        TextureKey key = new TextureKey(name, true);
        key.setGenerateMips(true);
        return loadTexture(key);
    }

    public AudioData loadAudio(AudioKey key){
        return (AudioData) loadAsset(key);
    }

    public AudioData loadAudio(String name){
        return loadAudio(new AudioKey(name, false));
    }

    public BitmapFont loadFont(String name){
        return (BitmapFont) loadAsset(new AssetKey(name));
    }

    public Spatial loadModel(ModelKey key){
        return (Spatial) loadAsset(key);
    }

    public Spatial loadModel(String name){
        return loadModel(new ModelKey(name));
    }

    public FilterPostProcessor loadFilter(FilterKey key){
        return (FilterPostProcessor) loadAsset(key);
    }

    public FilterPostProcessor loadFilter(String name){
        return loadFilter(new FilterKey(name));
    }

    /**
     * Load a vertex/fragment shader combo.
     *
     * @param key
     * @return the loaded {@link Shader}
     */
    public Shader loadShader(ShaderKey key){
        // cache abuse in method
        // that doesn't use loaders/locators
        AssetCache cache = handler.getCache(SimpleAssetCache.class);
        Shader shader = (Shader) cache.getFromCache(key);
        if (shader == null){
            if (key.isUsesShaderNodes()) {
                if(shaderGenerator == null){
                    throw new UnsupportedOperationException("ShaderGenerator was not initialized, make sure assetManager.getGenerator(caps) has been called");
                }
                shader = shaderGenerator.generateShader();
            } else {
                shader = new Shader();
                shader.initialize();
                for (Shader.ShaderType shaderType : key.getUsedShaderPrograms()) {
                    shader.addSource(shaderType,key.getShaderProgramName(shaderType),(String) loadAsset(new AssetKey(key.getShaderProgramName(shaderType))),key.getDefines().getCompiled(),key.getShaderProgramLanguage(shaderType));
                }
            }

            cache.addToCache(key, shader);
        }
        return shader;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ShaderGenerator getShaderGenerator(EnumSet<Caps> caps) {
        if (shaderGenerator == null) {
            if(caps.contains(Caps.GLSL150)){
                shaderGenerator = new Glsl150ShaderGenerator(this);
            }else{
                shaderGenerator = new Glsl100ShaderGenerator(this);
            }
        }
        return shaderGenerator;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setShaderGenerator(ShaderGenerator shaderGenerator) {
        this.shaderGenerator = shaderGenerator;
    }

    
}
