/*
 * 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.scene.plugins.blender;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.jme3.animation.Animation;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetLoader;
import com.jme3.asset.AssetLocator;
import com.jme3.asset.AssetManager;
import com.jme3.asset.BlenderKey;
import com.jme3.asset.ModelKey;
import com.jme3.asset.StreamAssetInfo;
import com.jme3.light.Light;
import com.jme3.math.ColorRGBA;
import com.jme3.post.Filter;
import com.jme3.renderer.Camera;
import com.jme3.scene.CameraNode;
import com.jme3.scene.LightNode;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.animations.AnimationHelper;
import com.jme3.scene.plugins.blender.cameras.CameraHelper;
import com.jme3.scene.plugins.blender.constraints.ConstraintHelper;
import com.jme3.scene.plugins.blender.curves.CurvesHelper;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.BlenderInputStream;
import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.FileBlockHeader.BlockCode;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.landscape.LandscapeHelper;
import com.jme3.scene.plugins.blender.lights.LightHelper;
import com.jme3.scene.plugins.blender.materials.MaterialContext;
import com.jme3.scene.plugins.blender.materials.MaterialHelper;
import com.jme3.scene.plugins.blender.meshes.MeshHelper;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
import com.jme3.scene.plugins.blender.modifiers.ModifierHelper;
import com.jme3.scene.plugins.blender.objects.ObjectHelper;
import com.jme3.scene.plugins.blender.particles.ParticlesHelper;
import com.jme3.scene.plugins.blender.textures.TextureHelper;
import com.jme3.texture.Texture;

/**
 * This is the main loading class. Have in notice that asset manager needs to have loaders for resources like textures.
 * @author Marcin Roguski (Kaelthas)
 */
public class BlenderLoader implements AssetLoader {
    private static final Logger     LOGGER = Logger.getLogger(BlenderLoader.class.getName());

    @Override
    public Spatial load(AssetInfo assetInfo) throws IOException {
        try {
            BlenderContext blenderContext = this.setup(assetInfo);

            AnimationHelper animationHelper = blenderContext.getHelper(AnimationHelper.class);
            animationHelper.loadAnimations();

            BlenderKey blenderKey = blenderContext.getBlenderKey();
            LoadedFeatures loadedFeatures = new LoadedFeatures();
            for (FileBlockHeader block : blenderContext.getBlocks()) {
                switch (block.getCode()) {
                    case BLOCK_OB00:
                        ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
                        Node object = (Node) objectHelper.toObject(block.getStructure(blenderContext), blenderContext);
                        if (LOGGER.isLoggable(Level.FINE)) {
                            LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { object.getName(), object.getLocalTranslation().toString(), object.getParent() == null ? "null" : object.getParent().getName() });
                        }
                        if (object.getParent() == null) {
                            loadedFeatures.objects.add(object);
                        }
                        if (object instanceof LightNode && ((LightNode) object).getLight() != null) {
                            loadedFeatures.lights.add(((LightNode) object).getLight());
                        } else if (object instanceof CameraNode && ((CameraNode) object).getCamera() != null) {
                            loadedFeatures.cameras.add(((CameraNode) object).getCamera());
                        }
                        break;
                    case BLOCK_SC00:// Scene
                        loadedFeatures.sceneBlocks.add(block);
                        break;
                    case BLOCK_MA00:// Material
                        MaterialHelper materialHelper = blenderContext.getHelper(MaterialHelper.class);
                        MaterialContext materialContext = materialHelper.toMaterialContext(block.getStructure(blenderContext), blenderContext);
                        loadedFeatures.materials.add(materialContext);
                        break;
                    case BLOCK_ME00:// Mesh
                        MeshHelper meshHelper = blenderContext.getHelper(MeshHelper.class);
                        TemporalMesh temporalMesh = meshHelper.toTemporalMesh(block.getStructure(blenderContext), blenderContext);
                        loadedFeatures.meshes.add(temporalMesh);
                        break;
                    case BLOCK_IM00:// Image
                        TextureHelper textureHelper = blenderContext.getHelper(TextureHelper.class);
                        Texture image = textureHelper.loadImageAsTexture(block.getStructure(blenderContext), 0, blenderContext);
                        if (image != null && image.getImage() != null) {// render results are stored as images but are not being loaded
                            loadedFeatures.images.add(image);
                        }
                        break;
                    case BLOCK_TE00:
                        Structure textureStructure = block.getStructure(blenderContext);
                        int type = ((Number) textureStructure.getFieldValue("type")).intValue();
                        if (type == TextureHelper.TEX_IMAGE) {
                            TextureHelper texHelper = blenderContext.getHelper(TextureHelper.class);
                            Texture texture = texHelper.getTexture(textureStructure, null, blenderContext);
                            if (texture != null) {// null is returned when texture has no image
                                loadedFeatures.textures.add(texture);
                            }
                        } else {
                            LOGGER.fine("Only image textures can be loaded as unlinked assets. Generated textures will be applied to an existing object.");
                        }
                        break;
                    case BLOCK_WO00:// World
                        LandscapeHelper landscapeHelper = blenderContext.getHelper(LandscapeHelper.class);
                        Structure worldStructure = block.getStructure(blenderContext);

                        String worldName = worldStructure.getName();
                        if (blenderKey.getUsedWorld() == null || blenderKey.getUsedWorld().equals(worldName)) {

                            Light ambientLight = landscapeHelper.toAmbientLight(worldStructure);
                            if (ambientLight != null) {
                                loadedFeatures.objects.add(new LightNode(null, ambientLight));
                                loadedFeatures.lights.add(ambientLight);
                            }
                            loadedFeatures.sky = landscapeHelper.toSky(worldStructure);
                            loadedFeatures.backgroundColor = landscapeHelper.toBackgroundColor(worldStructure);

                            Filter fogFilter = landscapeHelper.toFog(worldStructure);
                            if (fogFilter != null) {
                                loadedFeatures.filters.add(landscapeHelper.toFog(worldStructure));
                            }
                        }
                        break;
                    case BLOCK_AC00:
                        LOGGER.fine("Loading unlinked animations is not yet supported!");
                        break;
                    default:
                        LOGGER.log(Level.FINEST, "Ommiting the block: {0}.", block.getCode());
                }
            }

            LOGGER.fine("Baking constraints after every feature is loaded.");
            ConstraintHelper constraintHelper = blenderContext.getHelper(ConstraintHelper.class);
            constraintHelper.bakeConstraints(blenderContext);

            LOGGER.fine("Loading scenes and attaching them to the root object.");
            for (FileBlockHeader sceneBlock : loadedFeatures.sceneBlocks) {
                loadedFeatures.scenes.add(this.toScene(sceneBlock.getStructure(blenderContext), blenderContext));
            }

            LOGGER.fine("Creating the root node of the model and applying loaded nodes of the scene and loaded features to it.");
            Node modelRoot = new Node(blenderKey.getName());
            for (Node scene : loadedFeatures.scenes) {
                modelRoot.attachChild(scene);
            }

            if (blenderKey.isLoadUnlinkedAssets()) {
                LOGGER.fine("Setting loaded content as user data in resulting sptaial.");
                Map<String, Map<String, Object>> linkedData = new HashMap<String, Map<String, Object>>();

                Map<String, Object> thisFileData = new HashMap<String, Object>();
                thisFileData.put("scenes", loadedFeatures.scenes == null ? new ArrayList<Object>() : loadedFeatures.scenes);
                thisFileData.put("objects", loadedFeatures.objects == null ? new ArrayList<Object>() : loadedFeatures.objects);
                thisFileData.put("meshes", loadedFeatures.meshes == null ? new ArrayList<Object>() : loadedFeatures.meshes);
                thisFileData.put("materials", loadedFeatures.materials == null ? new ArrayList<Object>() : loadedFeatures.materials);
                thisFileData.put("textures", loadedFeatures.textures == null ? new ArrayList<Object>() : loadedFeatures.textures);
                thisFileData.put("images", loadedFeatures.images == null ? new ArrayList<Object>() : loadedFeatures.images);
                thisFileData.put("animations", loadedFeatures.animations == null ? new ArrayList<Object>() : loadedFeatures.animations);
                thisFileData.put("cameras", loadedFeatures.cameras == null ? new ArrayList<Object>() : loadedFeatures.cameras);
                thisFileData.put("lights", loadedFeatures.lights == null ? new ArrayList<Object>() : loadedFeatures.lights);
                thisFileData.put("filters", loadedFeatures.filters == null ? new ArrayList<Object>() : loadedFeatures.filters);
                thisFileData.put("backgroundColor", loadedFeatures.backgroundColor);
                thisFileData.put("sky", loadedFeatures.sky);

                linkedData.put("this", thisFileData);
                linkedData.putAll(blenderContext.getLinkedFeatures());

                modelRoot.setUserData("linkedData", linkedData);
            }

            return modelRoot;
        } catch (BlenderFileException e) {
            throw new IOException(e.getLocalizedMessage(), e);
        } catch (Exception e) {
            throw new IOException("Unexpected importer exception occured: " + e.getLocalizedMessage(), e);
        } finally {
            this.clear(assetInfo);
        }
    }

    /**
     * This method converts the given structure to a scene node.
     * @param structure
     *            structure of a scene
     *            @param blenderContext the blender context
     * @return scene's node
     * @throws BlenderFileException
     *             an exception throw when problems with blender file occur
     */
    private Node toScene(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
        ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
        Node result = new Node(structure.getName());
        List<Structure> base = ((Structure) structure.getFieldValue("base")).evaluateListBase();
        for (Structure b : base) {
            Pointer pObject = (Pointer) b.getFieldValue("object");
            if (pObject.isNotNull()) {
                Structure objectStructure = pObject.fetchData().get(0);

                Object object = objectHelper.toObject(objectStructure, blenderContext);
                if (object instanceof LightNode) {
                    result.addLight(((LightNode) object).getLight());// FIXME: check if this is needed !!!
                    result.attachChild((LightNode) object);
                } else if (object instanceof Node) {
                    if (LOGGER.isLoggable(Level.FINE)) {
                        LOGGER.log(Level.FINE, "{0}: {1}--> {2}", new Object[] { ((Node) object).getName(), ((Node) object).getLocalTranslation().toString(), ((Node) object).getParent() == null ? "null" : ((Node) object).getParent().getName() });
                    }
                    if (((Node) object).getParent() == null) {
                        result.attachChild((Spatial) object);
                    }
                }
            }
        }
        return result;
    }

    /**
     * This method sets up the loader.
     * @param assetInfo
     *            the asset info
     * @throws BlenderFileException
     *             an exception is throw when something wrong happens with blender file
     */
    protected BlenderContext setup(AssetInfo assetInfo) throws BlenderFileException {
        // registering loaders
        ModelKey modelKey = (ModelKey) assetInfo.getKey();
        BlenderKey blenderKey;
        if (modelKey instanceof BlenderKey) {
            blenderKey = (BlenderKey) modelKey;
        } else {
            blenderKey = new BlenderKey(modelKey.getName());
        }

        // opening stream
        BlenderInputStream inputStream = new BlenderInputStream(assetInfo.openStream());

        // reading blocks
        List<FileBlockHeader> blocks = new ArrayList<FileBlockHeader>();
        FileBlockHeader fileBlock;
        BlenderContext blenderContext = new BlenderContext();
        blenderContext.setBlenderVersion(inputStream.getVersionNumber());
        blenderContext.setAssetManager(assetInfo.getManager());
        blenderContext.setInputStream(inputStream);
        blenderContext.setBlenderKey(blenderKey);

        // creating helpers
        blenderContext.putHelper(AnimationHelper.class, new AnimationHelper(inputStream.getVersionNumber(), blenderContext));
        blenderContext.putHelper(TextureHelper.class, new TextureHelper(inputStream.getVersionNumber(), blenderContext));
        blenderContext.putHelper(MeshHelper.class, new MeshHelper(inputStream.getVersionNumber(), blenderContext));
        blenderContext.putHelper(ObjectHelper.class, new ObjectHelper(inputStream.getVersionNumber(), blenderContext));
        blenderContext.putHelper(CurvesHelper.class, new CurvesHelper(inputStream.getVersionNumber(), blenderContext));
        blenderContext.putHelper(LightHelper.class, new LightHelper(inputStream.getVersionNumber(), blenderContext));
        blenderContext.putHelper(CameraHelper.class, new CameraHelper(inputStream.getVersionNumber(), blenderContext));
        blenderContext.putHelper(ModifierHelper.class, new ModifierHelper(inputStream.getVersionNumber(), blenderContext));
        blenderContext.putHelper(MaterialHelper.class, new MaterialHelper(inputStream.getVersionNumber(), blenderContext));
        blenderContext.putHelper(ConstraintHelper.class, new ConstraintHelper(inputStream.getVersionNumber(), blenderContext));
        blenderContext.putHelper(ParticlesHelper.class, new ParticlesHelper(inputStream.getVersionNumber(), blenderContext));
        blenderContext.putHelper(LandscapeHelper.class, new LandscapeHelper(inputStream.getVersionNumber(), blenderContext));

        // reading the blocks (dna block is automatically saved in the blender context when found)
        FileBlockHeader sceneFileBlock = null;
        do {
            fileBlock = new FileBlockHeader(inputStream, blenderContext);
            if (!fileBlock.isDnaBlock()) {
                blocks.add(fileBlock);
                // save the scene's file block
                if (fileBlock.getCode() == BlockCode.BLOCK_SC00) {
                    sceneFileBlock = fileBlock;
                }
            }
        } while (!fileBlock.isLastBlock());
        if (sceneFileBlock != null) {
            blenderContext.setSceneStructure(sceneFileBlock.getStructure(blenderContext));
        }
        
        // adding locator for linked content
        assetInfo.getManager().registerLocator(assetInfo.getKey().getName(), LinkedContentLocator.class);
        
        return blenderContext;
    }

    /**
     * The internal data is only needed during loading so make it unreachable so that the GC can release
     * that memory (which can be quite large amount).
     */
    protected void clear(AssetInfo assetInfo) {
        assetInfo.getManager().unregisterLocator(assetInfo.getKey().getName(), LinkedContentLocator.class);
    }

    /**
     * This class holds the loading results according to the given loading flag.
     * @author Marcin Roguski (Kaelthas)
     */
    private static class LoadedFeatures {
        private List<FileBlockHeader> sceneBlocks     = new ArrayList<FileBlockHeader>();
        /** The scenes from the file. */
        private List<Node>            scenes          = new ArrayList<Node>();
        /** Objects from all scenes. */
        private List<Node>            objects         = new ArrayList<Node>();
        /** All meshes. */
        private List<TemporalMesh>    meshes          = new ArrayList<TemporalMesh>();
        /** Materials from all objects. */
        private List<MaterialContext> materials       = new ArrayList<MaterialContext>();
        /** Textures from all objects. */
        private List<Texture>         textures        = new ArrayList<Texture>();
        /** The images stored in the blender file. */
        private List<Texture>         images          = new ArrayList<Texture>();
        /** Animations of all objects. */
        private List<Animation>       animations      = new ArrayList<Animation>();
        /** All cameras from the file. */
        private List<Camera>          cameras         = new ArrayList<Camera>();
        /** All lights from the file. */
        private List<Light>           lights          = new ArrayList<Light>();
        /** Loaded sky. */
        private Spatial               sky;
        /** Scene filters (ie. FOG). */
        private List<Filter>          filters         = new ArrayList<Filter>();
        /**
         * The background color of the render loaded from the horizon color of the world. If no world is used than the gray color
         * is set to default (as in blender editor.
         */
        private ColorRGBA             backgroundColor = ColorRGBA.Gray;
    }
    
    public static class LinkedContentLocator implements AssetLocator {
        private File rootFolder;
        
        @Override
        public void setRootPath(String rootPath) {
            rootFolder = new File(rootPath);
            if(rootFolder.isFile()) {
                rootFolder = rootFolder.getParentFile();
            }
        }

        @SuppressWarnings("rawtypes")
        @Override
        public AssetInfo locate(AssetManager manager, AssetKey key) {
            if(key instanceof BlenderKey) {
                File linkedAbsoluteFile = new File(key.getName());
                if(linkedAbsoluteFile.exists() && linkedAbsoluteFile.isFile()) {
                    try {
                        return new StreamAssetInfo(manager, key, new FileInputStream(linkedAbsoluteFile));
                    } catch (FileNotFoundException e) {
                        return null;
                    }
                }
                
                File linkedFileInCurrentAssetFolder = new File(rootFolder, linkedAbsoluteFile.getName());
                if(linkedFileInCurrentAssetFolder.exists() && linkedFileInCurrentAssetFolder.isFile()) {
                    try {
                        return new StreamAssetInfo(manager, key, new FileInputStream(linkedFileInCurrentAssetFolder));
                    } catch (FileNotFoundException e) {
                        return null;
                    }
                }
                
                File linkedFileInCurrentFolder = new File(".", linkedAbsoluteFile.getName());
                if(linkedFileInCurrentFolder.exists() && linkedFileInCurrentFolder.isFile()) {
                    try {
                        return new StreamAssetInfo(manager, key, new FileInputStream(linkedFileInCurrentFolder));
                    } catch (FileNotFoundException e) {
                        return null;
                    }
                }
            }
            return null;
        }
    }
}
