/*
 * 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.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;

import com.jme3.asset.AssetNotFoundException;
import com.jme3.asset.BlenderKey;
import com.jme3.export.Savable;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.scene.Spatial;
import com.jme3.scene.plugins.blender.BlenderContext.LoadedDataType;
import com.jme3.scene.plugins.blender.file.BlenderFileException;
import com.jme3.scene.plugins.blender.file.Pointer;
import com.jme3.scene.plugins.blender.file.Structure;
import com.jme3.scene.plugins.blender.objects.Properties;

/**
 * A purpose of the helper class is to split calculation code into several classes. Each helper after use should be cleared because it can
 * hold the state of the calculations.
 * @author Marcin Roguski
 */
public abstract class AbstractBlenderHelper {
    private static final Logger LOGGER = Logger.getLogger(AbstractBlenderHelper.class.getName());

    /** The blender context. */
    protected BlenderContext    blenderContext;
    /** The version of the blend file. */
    protected final int         blenderVersion;
    /** This variable indicates if the Y asxis is the UP axis or not. */
    protected boolean           fixUpAxis;
    /** Quaternion used to rotate data when Y is up axis. */
    protected Quaternion        upAxisRotationQuaternion;

    /**
     * This constructor parses the given blender version and stores the result. Some functionalities may differ in different blender
     * versions.
     * @param blenderVersion
     *            the version read from the blend file
     * @param blenderContext
     *            the blender context
     */
    public AbstractBlenderHelper(String blenderVersion, BlenderContext blenderContext) {
        this.blenderVersion = Integer.parseInt(blenderVersion);
        this.blenderContext = blenderContext;
        fixUpAxis = blenderContext.getBlenderKey().isFixUpAxis();
        if (fixUpAxis) {
            upAxisRotationQuaternion = new Quaternion().fromAngles(-FastMath.HALF_PI, 0, 0);
        }
    }

    /**
     * This method loads the properties if they are available and defined for the structure.
     * @param structure
     *            the structure we read the properties from
     * @param blenderContext
     *            the blender context
     * @return loaded properties or null if they are not available
     * @throws BlenderFileException
     *             an exception is thrown when the blend file is somehow corrupted
     */
    protected Properties loadProperties(Structure structure, BlenderContext blenderContext) throws BlenderFileException {
        Properties properties = null;
        Structure id = (Structure) structure.getFieldValue("ID");
        if (id != null) {
            Pointer pProperties = (Pointer) id.getFieldValue("properties");
            if (pProperties.isNotNull()) {
                Structure propertiesStructure = pProperties.fetchData().get(0);
                properties = new Properties();
                properties.load(propertiesStructure, blenderContext);
            }
        }
        return properties;
    }

    /**
     * The method applies properties to the given spatial. The Properties
     * instance cannot be directly applied because the end-user might not have
     * the blender plugin jar file and thus receive ClassNotFoundException. The
     * values are set by name instead.
     * 
     * @param spatial
     *            the spatial that is to have properties applied
     * @param properties
     *            the properties to be applied
     */
    public void applyProperties(Spatial spatial, Properties properties) {
        List<String> propertyNames = properties.getSubPropertiesNames();
        if (propertyNames != null && propertyNames.size() > 0) {
            for (String propertyName : propertyNames) {
                Object value = properties.findValue(propertyName);
                if (value instanceof Savable || value instanceof Boolean || value instanceof String || value instanceof Float || value instanceof Integer || value instanceof Long) {
                    spatial.setUserData(propertyName, value);
                } else if (value instanceof Double) {
                    spatial.setUserData(propertyName, ((Double) value).floatValue());
                } else if (value instanceof int[]) {
                    spatial.setUserData(propertyName, Arrays.toString((int[]) value));
                } else if (value instanceof float[]) {
                    spatial.setUserData(propertyName, Arrays.toString((float[]) value));
                } else if (value instanceof double[]) {
                    spatial.setUserData(propertyName, Arrays.toString((double[]) value));
                }
            }
        }
    }

    /**
     * The method loads library of a given ID from linked blender file.
     * @param id
     *            the ID of the linked feature (it contains its name and blender path)
     * @return loaded feature or null if none was found
     * @throws BlenderFileException
     *             and exception is throw when problems with reading a blend file occur
     */
    protected Object loadLibrary(Structure id) throws BlenderFileException {
        Pointer pLib = (Pointer) id.getFieldValue("lib");
        if (pLib.isNotNull()) {
            String fullName = id.getFieldValue("name").toString();// we need full name with the prefix
            String nameOfFeatureToLoad = id.getName();
            Structure library = pLib.fetchData().get(0);
            String path = library.getFieldValue("filepath").toString();

            if (!blenderContext.getLinkedFeatures().keySet().contains(path)) {
                Spatial loadedAsset = null;
                BlenderKey blenderKey = new BlenderKey(path);
                blenderKey.setLoadUnlinkedAssets(true);
                try {
                    loadedAsset = blenderContext.getAssetManager().loadAsset(blenderKey);
                } catch (AssetNotFoundException e) {
                    LOGGER.log(Level.FINEST, "Cannot locate linked resource at path: {0}.", path);
                }
                
                if (loadedAsset != null) {
                    Map<String, Map<String, Object>> linkedData = loadedAsset.getUserData("linkedData");
                    
                    for (Entry<String, Map<String, Object>> entry : linkedData.entrySet()) {
                        String linkedDataFilePath = "this".equals(entry.getKey()) ? path : entry.getKey();
                        blenderContext.getLinkedFeatures().put(linkedDataFilePath, entry.getValue());
                    }
                } else {
                    LOGGER.log(Level.WARNING, "No features loaded from path: {0}.", path);
                }
            }

            Object result = blenderContext.getLinkedFeature(path, fullName);
            if (result == null) {
                LOGGER.log(Level.WARNING, "Could NOT find asset named {0} in the library of path: {1}.", new Object[] { nameOfFeatureToLoad, path });
            } else {
                blenderContext.addLoadedFeatures(id.getOldMemoryAddress(), LoadedDataType.STRUCTURE, id);
                blenderContext.addLoadedFeatures(id.getOldMemoryAddress(), LoadedDataType.FEATURE, result);
            }
            return result;
        } else {
            LOGGER.warning("Library link points to nothing!");
        }
        return null;
    }
}
