/*
 * 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.util.ArrayList;
import java.util.EmptyStackException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Stack;

import com.jme3.animation.Animation;
import com.jme3.animation.Bone;
import com.jme3.animation.Skeleton;
import com.jme3.asset.AssetManager;
import com.jme3.asset.BlenderKey;
import com.jme3.light.Light;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.post.Filter;
import com.jme3.renderer.Camera;
import com.jme3.scene.Node;
import com.jme3.scene.plugins.blender.animations.BlenderAction;
import com.jme3.scene.plugins.blender.animations.BoneContext;
import com.jme3.scene.plugins.blender.constraints.Constraint;
import com.jme3.scene.plugins.blender.file.BlenderInputStream;
import com.jme3.scene.plugins.blender.file.DnaBlockData;
import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.FileBlockHeader.BlockCode;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.materials.MaterialContext;
import com.jme3.scene.plugins.blender.meshes.TemporalMesh;
import com.jme3.texture.Texture;

/**
 * The class that stores temporary data and manages it during loading the belnd
 * file. This class is intended to be used in a single loading thread. It holds
 * the state of loading operations.
 * 
 * @author Marcin Roguski (Kaelthas)
 */
public class BlenderContext {
    /** The blender file version. */
    private int                                    blenderVersion;
    /** The blender key. */
    private BlenderKey                             blenderKey;
    /** The header of the file block. */
    private DnaBlockData                           dnaBlockData;
    /** The scene structure. */
    private Structure                              sceneStructure;
    /** The input stream of the blend file. */
    private BlenderInputStream                     inputStream;
    /** The asset manager. */
    private AssetManager                           assetManager;
    /** The blocks read from the file. */
    protected List<FileBlockHeader>                blocks                 = new ArrayList<FileBlockHeader>();
    /**
     * A map containing the file block headers. The key is the old memory address.
     */
    private Map<Long, FileBlockHeader>             fileBlockHeadersByOma  = new HashMap<Long, FileBlockHeader>();
    /** A map containing the file block headers. The key is the block code. */
    private Map<BlockCode, List<FileBlockHeader>>  fileBlockHeadersByCode = new HashMap<BlockCode, List<FileBlockHeader>>();
    /**
     * This map stores the loaded features by their old memory address. The
     * first object in the value table is the loaded structure and the second -
     * the structure already converted into proper data.
     */
    private Map<Long, Map<LoadedDataType, Object>> loadedFeatures         = new HashMap<Long, Map<LoadedDataType, Object>>();
    /** Features loaded from external blender files. The key is the file path and the value is a map between feature name and loaded feature. */
    private Map<String, Map<String, Object>>       linkedFeatures         = new HashMap<String, Map<String, Object>>();
    /** A stack that hold the parent structure of currently loaded feature. */
    private Stack<Structure>                       parentStack            = new Stack<Structure>();
    /** A list of constraints for the specified object. */
    protected Map<Long, List<Constraint>>          constraints            = new HashMap<Long, List<Constraint>>();
    /** Animations loaded for features. */
    private Map<Long, List<Animation>>             animations             = new HashMap<Long, List<Animation>>();
    /** Loaded skeletons. */
    private Map<Long, Skeleton>                    skeletons              = new HashMap<Long, Skeleton>();
    /** A map between skeleton and node it modifies. */
    private Map<Skeleton, Node>                    nodesWithSkeletons     = new HashMap<Skeleton, Node>();
    /** A map of bone contexts. */
    protected Map<Long, BoneContext>               boneContexts           = new HashMap<Long, BoneContext>();
    /** A map og helpers that perform loading. */
    private Map<String, AbstractBlenderHelper>     helpers                = new HashMap<String, AbstractBlenderHelper>();
    /** Markers used by loading classes to store some custom data. This is made to avoid putting this data into user properties. */
    private Map<String, Map<Object, Object>>       markers                = new HashMap<String, Map<Object, Object>>();
    /** A map of blender actions. The key is the action name and the value is the action itself. */
    private Map<String, BlenderAction>             actions                = new HashMap<String, BlenderAction>();

    /**
     * This method sets the blender file version.
     * 
     * @param blenderVersion
     *            the blender file version
     */
    public void setBlenderVersion(String blenderVersion) {
        this.blenderVersion = Integer.parseInt(blenderVersion);
    }

    /**
     * @return the blender file version
     */
    public int getBlenderVersion() {
        return blenderVersion;
    }

    /**
     * This method sets the blender key.
     * 
     * @param blenderKey
     *            the blender key
     */
    public void setBlenderKey(BlenderKey blenderKey) {
        this.blenderKey = blenderKey;
    }

    /**
     * This method returns the blender key.
     * 
     * @return the blender key
     */
    public BlenderKey getBlenderKey() {
        return blenderKey;
    }

    /**
     * This method sets the dna block data.
     * 
     * @param dnaBlockData
     *            the dna block data
     */
    public void setBlockData(DnaBlockData dnaBlockData) {
        this.dnaBlockData = dnaBlockData;
    }

    /**
     * This method returns the dna block data.
     * 
     * @return the dna block data
     */
    public DnaBlockData getDnaBlockData() {
        return dnaBlockData;
    }

    /**
     * This method sets the scene structure data.
     * 
     * @param sceneStructure
     *            the scene structure data
     */
    public void setSceneStructure(Structure sceneStructure) {
        this.sceneStructure = sceneStructure;
    }

    /**
     * This method returns the scene structure data.
     * 
     * @return the scene structure data
     */
    public Structure getSceneStructure() {
        return sceneStructure;
    }

    /**
     * This method returns the asset manager.
     * 
     * @return the asset manager
     */
    public AssetManager getAssetManager() {
        return assetManager;
    }

    /**
     * This method sets the asset manager.
     * 
     * @param assetManager
     *            the asset manager
     */
    public void setAssetManager(AssetManager assetManager) {
        this.assetManager = assetManager;
    }

    /**
     * This method returns the input stream of the blend file.
     * 
     * @return the input stream of the blend file
     */
    public BlenderInputStream getInputStream() {
        return inputStream;
    }

    /**
     * This method sets the input stream of the blend file.
     * 
     * @param inputStream
     *            the input stream of the blend file
     */
    public void setInputStream(BlenderInputStream inputStream) {
        this.inputStream = inputStream;
    }

    /**
     * This method adds a file block header to the map. Its old memory address
     * is the key.
     * 
     * @param oldMemoryAddress
     *            the address of the block header
     * @param fileBlockHeader
     *            the block header to store
     */
    public void addFileBlockHeader(Long oldMemoryAddress, FileBlockHeader fileBlockHeader) {
        blocks.add(fileBlockHeader);
        fileBlockHeadersByOma.put(oldMemoryAddress, fileBlockHeader);
        List<FileBlockHeader> headers = fileBlockHeadersByCode.get(fileBlockHeader.getCode());
        if (headers == null) {
            headers = new ArrayList<FileBlockHeader>();
            fileBlockHeadersByCode.put(fileBlockHeader.getCode(), headers);
        }
        headers.add(fileBlockHeader);
    }

    /**
     * @return the block headers
     */
    public List<FileBlockHeader> getBlocks() {
        return blocks;
    }

    /**
     * This method returns the block header of a given memory address. If the
     * header is not present then null is returned.
     * 
     * @param oldMemoryAddress
     *            the address of the block header
     * @return loaded header or null if it was not yet loaded
     */
    public FileBlockHeader getFileBlock(Long oldMemoryAddress) {
        return fileBlockHeadersByOma.get(oldMemoryAddress);
    }

    /**
     * This method returns a list of file blocks' headers of a specified code.
     * 
     * @param code
     *            the code of file blocks
     * @return a list of file blocks' headers of a specified code
     */
    public List<FileBlockHeader> getFileBlocks(BlockCode code) {
        return fileBlockHeadersByCode.get(code);
    }

    /**
     * This method adds a helper instance to the helpers' map.
     * 
     * @param <T>
     *            the type of the helper
     * @param clazz
     *            helper's class definition
     * @param helper
     *            the helper instance
     */
    public <T> void putHelper(Class<T> clazz, AbstractBlenderHelper helper) {
        helpers.put(clazz.getSimpleName(), helper);
    }

    @SuppressWarnings("unchecked")
    public <T> T getHelper(Class<?> clazz) {
        return (T) helpers.get(clazz.getSimpleName());
    }

    /**
     * This method adds a loaded feature to the map. The key is its unique old
     * memory address.
     * 
     * @param oldMemoryAddress
     *            the address of the feature
     * @param featureName
     *            the name of the feature
     * @param structure
     *            the filled structure of the feature
     * @param feature
     *            the feature we want to store
     */
    public void addLoadedFeatures(Long oldMemoryAddress, LoadedDataType featureDataType, Object feature) {
        if (oldMemoryAddress == null || featureDataType == null || feature == null) {
            throw new IllegalArgumentException("One of the given arguments is null!");
        }
        Map<LoadedDataType, Object> map = loadedFeatures.get(oldMemoryAddress);
        if (map == null) {
            map = new HashMap<BlenderContext.LoadedDataType, Object>();
            loadedFeatures.put(oldMemoryAddress, map);
        }
        map.put(featureDataType, feature);
    }

    /**
     * This method returns the feature of a given memory address. If the feature
     * is not yet loaded then null is returned.
     * 
     * @param oldMemoryAddress
     *            the address of the feature
     * @param loadedFeatureDataType
     *            the type of data we want to retreive it can be either filled
     *            structure or already converted feature
     * @return loaded feature or null if it was not yet loaded
     */
    public Object getLoadedFeature(Long oldMemoryAddress, LoadedDataType loadedFeatureDataType) {
        Map<LoadedDataType, Object> result = loadedFeatures.get(oldMemoryAddress);
        if (result != null) {
            return result.get(loadedFeatureDataType);
        }
        return null;
    }

    /**
     * The method adds linked content to the blender context.
     * @param blenderFilePath
     *            the path of linked blender file
     * @param featureGroup
     *            the linked feature group (ie. scenes, materials, meshes, etc.)
     * @param feature
     *            the linked feature
     */
    @Deprecated
    public void addLinkedFeature(String blenderFilePath, String featureGroup, Object feature) {
        // the method is deprecated and empty at the moment
    }

    /**
     * The method returns linked feature of a given name from the specified blender path.
     * @param blenderFilePath
     *            the blender file path
     * @param featureName
     *            the feature name we want to get
     * @return linked feature or null if none was found
     */
    @SuppressWarnings("unchecked")
    public Object getLinkedFeature(String blenderFilePath, String featureName) {
        Map<String, Object> linkedFeatures = this.linkedFeatures.get(blenderFilePath);
        if(linkedFeatures != null) {
            String namePrefix = (featureName.charAt(0) + "" + featureName.charAt(1)).toUpperCase();
            featureName = featureName.substring(2);
            
            if("SC".equals(namePrefix)) {
                List<Node> scenes = (List<Node>) linkedFeatures.get("scenes");
                if(scenes != null) {
                    for(Node scene : scenes) {
                        if(featureName.equals(scene.getName())) {
                            return scene;
                        }
                    }
                }
            } else if("OB".equals(namePrefix)) {
                List<Node> features = (List<Node>) linkedFeatures.get("objects");
                if(features != null) {
                    for(Node feature : features) {
                        if(featureName.equals(feature.getName())) {
                            return feature;
                        }
                    }
                }
            } else if("ME".equals(namePrefix)) {
                List<TemporalMesh> temporalMeshes = (List<TemporalMesh>) linkedFeatures.get("meshes");
                if(temporalMeshes != null) {
                    for(TemporalMesh temporalMesh : temporalMeshes) {
                        if(featureName.equals(temporalMesh.getName())) {
                            return temporalMesh;
                        }
                    }
                }
            } else if("MA".equals(namePrefix)) {
                List<MaterialContext> features = (List<MaterialContext>) linkedFeatures.get("materials");
                if(features != null) {
                    for(MaterialContext feature : features) {
                        if(featureName.equals(feature.getName())) {
                            return feature;
                        }
                    }
                }
            } else if("TX".equals(namePrefix)) {
                List<Texture> features = (List<Texture>) linkedFeatures.get("textures");
                if(features != null) {
                    for(Texture feature : features) {
                        if(featureName.equals(feature.getName())) {
                            return feature;
                        }
                    }
                }
            } else if("IM".equals(namePrefix)) {
                List<Texture> features = (List<Texture>) linkedFeatures.get("images");
                if(features != null) {
                    for(Texture feature : features) {
                        if(featureName.equals(feature.getName())) {
                            return feature;
                        }
                    }
                }
            } else if("AC".equals(namePrefix)) {
                List<Animation> features = (List<Animation>) linkedFeatures.get("animations");
                if(features != null) {
                    for(Animation feature : features) {
                        if(featureName.equals(feature.getName())) {
                            return feature;
                        }
                    }
                }
            } else if("CA".equals(namePrefix)) {
                List<Camera> features = (List<Camera>) linkedFeatures.get("cameras");
                if(features != null) {
                    for(Camera feature : features) {
                        if(featureName.equals(feature.getName())) {
                            return feature;
                        }
                    }
                }
            } else if("LA".equals(namePrefix)) {
                List<Light> features = (List<Light>) linkedFeatures.get("lights");
                if(features != null) {
                    for(Light feature : features) {
                        if(featureName.equals(feature.getName())) {
                            return feature;
                        }
                    }
                }
            } else if("FI".equals(featureName)) {
                List<Filter> features = (List<Filter>) linkedFeatures.get("lights");
                if(features != null) {
                    for(Filter feature : features) {
                        if(featureName.equals(feature.getName())) {
                            return feature;
                        }
                    }
                }
            }
        }
        return null;
    }

    /**
     * @return all linked features for the current blend file
     */
    public Map<String, Map<String, Object>> getLinkedFeatures() {
        return linkedFeatures;
    }

    /**
     * This method adds the structure to the parent stack.
     * 
     * @param parent
     *            the structure to be added to the stack
     */
    public void pushParent(Structure parent) {
        parentStack.push(parent);
    }

    /**
     * This method removes the structure from the top of the parent's stack.
     * 
     * @return the structure that was removed from the stack
     */
    public Structure popParent() {
        try {
            return parentStack.pop();
        } catch (EmptyStackException e) {
            return null;
        }
    }

    /**
     * This method retreives the structure at the top of the parent's stack but
     * does not remove it.
     * 
     * @return the structure from the top of the stack
     */
    public Structure peekParent() {
        try {
            return parentStack.peek();
        } catch (EmptyStackException e) {
            return null;
        }
    }

    /**
     * This method adds a new modifier to the list.
     * 
     * @param ownerOMA
     *            the owner's old memory address
     * @param constraints
     *            the object's constraints
     */
    public void addConstraints(Long ownerOMA, List<Constraint> constraints) {
        List<Constraint> objectConstraints = this.constraints.get(ownerOMA);
        if (objectConstraints == null) {
            objectConstraints = new ArrayList<Constraint>();
            this.constraints.put(ownerOMA, objectConstraints);
        }
        objectConstraints.addAll(constraints);
    }

    /**
     * Returns constraints applied to the feature of the given OMA.
     * @param ownerOMA
     *            the constraints' owner OMA
     * @return a list of constraints or <b>null</b> if no constraints are applied to the feature
     */
    public List<Constraint> getConstraints(Long ownerOMA) {
        return constraints.get(ownerOMA);
    }

    /**
     * @return all available constraints
     */
    public List<Constraint> getAllConstraints() {
        List<Constraint> result = new ArrayList<Constraint>();
        for (Entry<Long, List<Constraint>> entry : constraints.entrySet()) {
            result.addAll(entry.getValue());
        }
        return result;
    }

    /**
     * This method adds the animation for the specified OMA of its owner.
     * 
     * @param ownerOMA
     *            the owner's old memory address
     * @param animation
     *            the animation for the feature specified by ownerOMA
     */
    public void addAnimation(Long ownerOMA, Animation animation) {
        List<Animation> animList = animations.get(ownerOMA);
        if (animList == null) {
            animList = new ArrayList<Animation>();
            animations.put(ownerOMA, animList);
        }
        animList.add(animation);
    }

    /**
     * This method returns the animation data for the specified owner.
     * 
     * @param ownerOMA
     *            the old memory address of the animation data owner
     * @return the animation or null if none exists
     */
    public List<Animation> getAnimations(Long ownerOMA) {
        return animations.get(ownerOMA);
    }

    /**
     * This method sets the skeleton for the specified OMA of its owner.
     * 
     * @param skeletonOMA
     *            the skeleton's old memory address
     * @param skeleton
     *            the skeleton specified by the given OMA
     */
    public void setSkeleton(Long skeletonOMA, Skeleton skeleton) {
        skeletons.put(skeletonOMA, skeleton);
    }

    /**
     * The method stores a binding between the skeleton and the proper armature
     * node.
     * 
     * @param skeleton
     *            the skeleton
     * @param node
     *            the armature node
     */
    public void setNodeForSkeleton(Skeleton skeleton, Node node) {
        nodesWithSkeletons.put(skeleton, node);
    }

    /**
     * This method returns the armature node that is defined for the skeleton.
     * 
     * @param skeleton
     *            the skeleton
     * @return the armature node that defines the skeleton in blender
     */
    public Node getControlledNode(Skeleton skeleton) {
        return nodesWithSkeletons.get(skeleton);
    }

    /**
     * This method returns the skeleton for the specified OMA of its owner.
     * 
     * @param skeletonOMA
     *            the skeleton's old memory address
     * @return the skeleton specified by the given OMA
     */
    public Skeleton getSkeleton(Long skeletonOMA) {
        return skeletons.get(skeletonOMA);
    }

    /**
     * This method sets the bone context for the given bone old memory address.
     * If the context is already set it will be replaced.
     * 
     * @param boneOMA
     *            the bone's old memory address
     * @param boneContext
     *            the bones's context
     */
    public void setBoneContext(Long boneOMA, BoneContext boneContext) {
        boneContexts.put(boneOMA, boneContext);
    }

    /**
     * This method returns the bone context for the given bone old memory
     * address. If no context exists then <b>null</b> is returned.
     * 
     * @param boneOMA
     *            the bone's old memory address
     * @return bone's context
     */
    public BoneContext getBoneContext(Long boneOMA) {
        return boneContexts.get(boneOMA);
    }

    /**
     * Returns bone by given name.
     * 
     * @param skeletonOMA
     *            the OMA of the skeleton where the bone will be searched
     * @param name
     *            the name of the bone
     * @return found bone or null if none bone of a given name exists
     */
    public BoneContext getBoneByName(Long skeletonOMA, String name) {
        for (Entry<Long, BoneContext> entry : boneContexts.entrySet()) {
            if (entry.getValue().getArmatureObjectOMA().equals(skeletonOMA)) {
                Bone bone = entry.getValue().getBone();
                if (bone != null && name.equals(bone.getName())) {
                    return entry.getValue();
                }
            }
        }
        return null;
    }

    /**
     * Returns bone context for the given bone.
     * 
     * @param bone
     *            the bone
     * @return the bone's bone context
     */
    public BoneContext getBoneContext(Bone bone) {
        for (Entry<Long, BoneContext> entry : boneContexts.entrySet()) {
            if (entry.getValue().getBone().getName().equals(bone.getName())) {
                return entry.getValue();
            }
        }
        throw new IllegalStateException("Cannot find context for bone: " + bone);
    }

    /**
     * This metod returns the default material.
     * 
     * @return the default material
     */
    public synchronized Material getDefaultMaterial() {
        if (blenderKey.getDefaultMaterial() == null) {
            Material defaultMaterial = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
            defaultMaterial.setColor("Color", ColorRGBA.DarkGray);
            blenderKey.setDefaultMaterial(defaultMaterial);
        }
        return blenderKey.getDefaultMaterial();
    }

    /**
     * Adds a custom marker for scene's feature.
     * 
     * @param marker
     *            the marker name
     * @param feature
     *            te scene's feature (can be node, material or texture or
     *            anything else)
     * @param markerValue
     *            the marker value
     */
    public void addMarker(String marker, Object feature, Object markerValue) {
        if (markerValue == null) {
            throw new IllegalArgumentException("The marker's value cannot be null.");
        }
        Map<Object, Object> markersMap = markers.get(marker);
        if (markersMap == null) {
            markersMap = new HashMap<Object, Object>();
            markers.put(marker, markersMap);
        }
        markersMap.put(feature, markerValue);
    }

    /**
     * Returns the marker value. The returned value is null if no marker was
     * defined for the given feature.
     * 
     * @param marker
     *            the marker name
     * @param feature
     *            the scene's feature
     * @return marker value or null if it was not defined
     */
    public Object getMarkerValue(String marker, Object feature) {
        Map<Object, Object> markersMap = markers.get(marker);
        return markersMap == null ? null : markersMap.get(feature);
    }

    /**
     * Adds blender action to the context.
     * @param action
     *            the action loaded from the blend file
     */
    public void addAction(BlenderAction action) {
        actions.put(action.getName(), action);
    }

    /**
     * @return a map of blender actions; the key is the action name and the value is action itself
     */
    public Map<String, BlenderAction> getActions() {
        return actions;
    }

    /**
     * This enum defines what loaded data type user wants to retreive. It can be
     * either filled structure or already converted data.
     * 
     * @author Marcin Roguski (Kaelthas)
     */
    public static enum LoadedDataType {
        STRUCTURE, FEATURE, TEMPORAL_MESH;
    }
    
    @Override
    public String toString() {
        return blenderKey == null ? "BlenderContext [key = null]" : "BlenderContext [ key = " + blenderKey.toString() + " ]";
    }
}
