package com.jme3.scene.plugins.blender.modifiers;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.jme3.bounding.BoundingBox;
import com.jme3.bounding.BoundingSphere;
import com.jme3.bounding.BoundingVolume;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.BlenderContext;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedFeatureDataType;
import com.jme3.scene.plugins.blender.exceptions.BlenderFileException;
import com.jme3.scene.plugins.blender.file.DynamicArray;
import com.jme3.scene.plugins.blender.file.FileBlockHeader;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.objects.ObjectHelper;
import com.jme3.scene.shape.Curve;

/**
 * This modifier allows to array modifier to the object.
 * 
 * @author Marcin Roguski (Kaelthas)
 */
/*package*/ class ArrayModifier extends Modifier {
	private static final Logger LOGGER = Logger.getLogger(ArrayModifier.class.getName());
	
	/** Parameters of the modifier. */
	private Map<String, Object> modifierData = new HashMap<String, Object>();
	
	/**
	 * This constructor reads array data from the modifier structure. The
	 * stored data is a map of parameters for array modifier. No additional data
	 * is loaded.
	 * 
	 * @param objectStructure
	 *            the structure of the object
	 * @param modifierStructure
	 *            the structure of the modifier
	 * @param blenderContext
	 *            the blender context
	 * @throws BlenderFileException
	 *             this exception is thrown when the blender file is somehow
	 *             corrupted
	 */
	@SuppressWarnings("unchecked")
	public ArrayModifier(Structure modifierStructure, BlenderContext blenderContext) throws BlenderFileException {
		if(this.validate(modifierStructure, blenderContext)) {
	        Number fittype = (Number) modifierStructure.getFieldValue("fit_type");
	        modifierData.put("fittype", fittype);
	        switch (fittype.intValue()) {
	            case 0:// FIXED COUNT
	            	modifierData.put("count", modifierStructure.getFieldValue("count"));
	                break;
	            case 1:// FIXED LENGTH
	            	modifierData.put("length", modifierStructure.getFieldValue("length"));
	                break;
	            case 2:// FITCURVE
	                Pointer pCurveOb = (Pointer) modifierStructure.getFieldValue("curve_ob");
	                float length = 0;
	                if (pCurveOb.isNotNull()) {
	                    Structure curveStructure = pCurveOb.fetchData(blenderContext.getInputStream()).get(0);
	                    ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
	                    Node curveObject = (Node) objectHelper.toObject(curveStructure, blenderContext);
	                    Set<Number> referencesToCurveLengths = new HashSet<Number>(curveObject.getChildren().size());
	                    for (Spatial spatial : curveObject.getChildren()) {
	                        if (spatial instanceof Geometry) {
	                            Mesh mesh = ((Geometry) spatial).getMesh();
	                            if (mesh instanceof Curve) {
	                                length += ((Curve) mesh).getLength();
	                            } else {
	                                //if bevel object has several parts then each mesh will have the same reference
	                                //to length value (and we should use only one)
	                                Number curveLength = spatial.getUserData("curveLength");
	                                if (curveLength != null && !referencesToCurveLengths.contains(curveLength)) {
	                                    length += curveLength.floatValue();
	                                    referencesToCurveLengths.add(curveLength);
	                                }
	                            }
	                        }
	                    }
	                }
	                modifierData.put("length", Float.valueOf(length));
	                modifierData.put("fittype", Integer.valueOf(1));// treat it like FIXED LENGTH
	                break;
	            default:
	                assert false : "Unknown array modifier fit type: " + fittype;
	        }
	
	        // offset parameters
	        int offsettype = ((Number) modifierStructure.getFieldValue("offset_type")).intValue();
	        if ((offsettype & 0x01) != 0) {// Constant offset
	            DynamicArray<Number> offsetArray = (DynamicArray<Number>) modifierStructure.getFieldValue("offset");
	            float[] offset = new float[]{offsetArray.get(0).floatValue(), offsetArray.get(1).floatValue(), offsetArray.get(2).floatValue()};
	            modifierData.put("offset", offset);
	        }
	        if ((offsettype & 0x02) != 0) {// Relative offset
	            DynamicArray<Number> scaleArray = (DynamicArray<Number>) modifierStructure.getFieldValue("scale");
	            float[] scale = new float[]{scaleArray.get(0).floatValue(), scaleArray.get(1).floatValue(), scaleArray.get(2).floatValue()};
	            modifierData.put("scale", scale);
	        }
	        if ((offsettype & 0x04) != 0) {// Object offset
	            Pointer pOffsetObject = (Pointer) modifierStructure.getFieldValue("offset_ob");
	            if (pOffsetObject.isNotNull()) {
	            	modifierData.put("offsetob", pOffsetObject);
	            }
	        }
	
	        // start cap and end cap
	        Pointer pStartCap = (Pointer) modifierStructure.getFieldValue("start_cap");
	        if (pStartCap.isNotNull()) {
	        	modifierData.put("startcap", pStartCap);
	        }
	        Pointer pEndCap = (Pointer) modifierStructure.getFieldValue("end_cap");
	        if (pEndCap.isNotNull()) {
	        	modifierData.put("endcap", pEndCap);
	        }
		}
	}
	
	@Override
	public Node apply(Node node, BlenderContext blenderContext) {
		if(invalid) {
			LOGGER.log(Level.WARNING, "Array modifier is invalid! Cannot be applied to: {0}", node.getName());
			return node;
		}
        int fittype = ((Number) modifierData.get("fittype")).intValue();
        float[] offset = (float[]) modifierData.get("offset");
        if (offset == null) {// the node will be repeated several times in the same place
            offset = new float[]{0.0f, 0.0f, 0.0f};
        }
        float[] scale = (float[]) modifierData.get("scale");
        if (scale == null) {// the node will be repeated several times in the same place
            scale = new float[]{0.0f, 0.0f, 0.0f};
        } else {
            // getting bounding box
            node.updateModelBound();
            BoundingVolume boundingVolume = node.getWorldBound();
            if (boundingVolume instanceof BoundingBox) {
                scale[0] *= ((BoundingBox) boundingVolume).getXExtent() * 2.0f;
                scale[1] *= ((BoundingBox) boundingVolume).getYExtent() * 2.0f;
                scale[2] *= ((BoundingBox) boundingVolume).getZExtent() * 2.0f;
            } else if (boundingVolume instanceof BoundingSphere) {
                float radius = ((BoundingSphere) boundingVolume).getRadius();
                scale[0] *= radius * 2.0f;
                scale[1] *= radius * 2.0f;
                scale[2] *= radius * 2.0f;
            } else {
                throw new IllegalStateException("Unknown bounding volume type: " + boundingVolume.getClass().getName());
            }
        }

        // adding object's offset
        float[] objectOffset = new float[]{0.0f, 0.0f, 0.0f};
        Pointer pOffsetObject = (Pointer) modifierData.get("offsetob");
        if (pOffsetObject != null) {
            FileBlockHeader offsetObjectBlock = blenderContext.getFileBlock(pOffsetObject.getOldMemoryAddress());
            ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
            try {// we take the structure in case the object was not yet loaded
                Structure offsetStructure = offsetObjectBlock.getStructure(blenderContext);
                Vector3f translation = objectHelper.getTransformation(offsetStructure, blenderContext).getTranslation();
                objectOffset[0] = translation.x;
                objectOffset[1] = translation.y;
                objectOffset[2] = translation.z;
            } catch (BlenderFileException e) {
                LOGGER.log(Level.WARNING, "Problems in blender file structure! Object offset cannot be applied! The problem: {0}", e.getMessage());
            }
        }

        // getting start and end caps
        Node[] caps = new Node[]{null, null};
        Pointer[] pCaps = new Pointer[]{(Pointer) modifierData.get("startcap"), (Pointer) modifierData.get("endcap")};
        for (int i = 0; i < pCaps.length; ++i) {
            if (pCaps[i] != null) {
                caps[i] = (Node) blenderContext.getLoadedFeature(pCaps[i].getOldMemoryAddress(), LoadedFeatureDataType.LOADED_FEATURE);
                if (caps[i] != null) {
                    caps[i] = (Node) caps[i].clone();
                } else {
                    FileBlockHeader capBlock = blenderContext.getFileBlock(pOffsetObject.getOldMemoryAddress());
                    try {// we take the structure in case the object was not yet loaded
                        Structure capStructure = capBlock.getStructure(blenderContext);
                        ObjectHelper objectHelper = blenderContext.getHelper(ObjectHelper.class);
                        caps[i] = (Node) objectHelper.toObject(capStructure, blenderContext);
                        if (caps[i] == null) {
                            LOGGER.log(Level.WARNING, "Cap object ''{0}'' couldn''t be loaded!", capStructure.getName());
                        }
                    } catch (BlenderFileException e) {
                        LOGGER.log(Level.WARNING, "Problems in blender file structure! Cap object cannot be applied! The problem: {0}", e.getMessage());
                    }
                }
            }
        }

        Vector3f translationVector = new Vector3f(offset[0] + scale[0] + objectOffset[0], offset[1] + scale[1] + objectOffset[1], offset[2] + scale[2] + objectOffset[2]);

        // getting/calculating repeats amount
        int count = 0;
        if (fittype == 0) {// Fixed count
            count = ((Number) modifierData.get("count")).intValue() - 1;
        } else if (fittype == 1) {// Fixed length
            float length = ((Number) modifierData.get("length")).floatValue();
            if (translationVector.length() > 0.0f) {
                count = (int) (length / translationVector.length()) - 1;
            }
        } else if (fittype == 2) {// Fit curve
            throw new IllegalStateException("Fit curve should be transformed to Fixed Length array type!");
        } else {
            throw new IllegalStateException("Unknown fit type: " + fittype);
        }

        // adding translated nodes and caps
        if (count > 0) {
            Node[] arrayNodes = new Node[count];
            Vector3f newTranslation = new Vector3f();
            for (int i = 0; i < count; ++i) {
                newTranslation.addLocal(translationVector);
                Node nodeClone = (Node) node.clone();
                nodeClone.setLocalTranslation(newTranslation);
                arrayNodes[i] = nodeClone;
            }
            for (Node nodeClone : arrayNodes) {
                node.attachChild(nodeClone);
            }
            if (caps[0] != null) {
                caps[0].getLocalTranslation().set(node.getLocalTranslation()).subtractLocal(translationVector);
                node.attachChild(caps[0]);
            }
            if (caps[1] != null) {
                caps[1].getLocalTranslation().set(newTranslation).addLocal(translationVector);
                node.attachChild(caps[1]);
            }
        }
        return node;
	}
	
	@Override
	public String getType() {
		return ARRAY_MODIFIER_DATA;
	}
}
