/**
   * The method applies animations to the given node. The names of the animations should be the same
   * as actions names in the blender file.
   *
   * @param node the node to whom the animations will be applied
   * @param animationMatchMethod the way animation should be matched with node
   */
  public void applyAnimations(Node node, AnimationMatchMethod animationMatchMethod) {
    List<BlenderAction> actions = this.getActions(node, animationMatchMethod);
    if (actions.size() > 0) {
      List<Animation> animations = new ArrayList<Animation>();
      for (BlenderAction action : actions) {
        SpatialTrack[] tracks = action.toTracks(node);
        if (tracks != null && tracks.length > 0) {
          Animation spatialAnimation = new Animation(action.getName(), action.getAnimationTime());
          spatialAnimation.setTracks(tracks);
          animations.add(spatialAnimation);
          blenderContext.addAnimation(
              (Long) node.getUserData(ObjectHelper.OMA_MARKER), spatialAnimation);
        }
      }

      if (animations.size() > 0) {
        AnimControl control = new AnimControl();
        HashMap<String, Animation> anims = new HashMap<String, Animation>(animations.size());
        for (int i = 0; i < animations.size(); ++i) {
          Animation animation = animations.get(i);
          anims.put(animation.getName(), animation);
        }
        control.setAnimations(anims);
        node.addControl(control);
      }
    }
  }
  @Override
  @SuppressWarnings("unchecked")
  public Node apply(Node node, BlenderContext blenderContext) {
    if (invalid) {
      LOGGER.log(
          Level.WARNING, "Armature modifier is invalid! Cannot be applied to: {0}", node.getName());
    } // if invalid, animData will be null
    if (animData == null) {
      return node;
    }

    // setting weights for bones
    List<Geometry> geomList =
        (List<Geometry>)
            blenderContext.getLoadedFeature(this.meshOMA, LoadedFeatureDataType.LOADED_FEATURE);
    for (Geometry geom : geomList) {
      Mesh mesh = geom.getMesh();
      if (this.verticesWeights != null) {
        mesh.setMaxNumWeights(this.boneGroups);
        mesh.setBuffer(this.verticesWeights);
        mesh.setBuffer(this.verticesWeightsIndices);
      }
    }

    ArrayList<Animation> animList = animData.anims;
    if (animList != null && animList.size() > 0) {
      List<Constraint> constraints = blenderContext.getConstraints(this.armatureObjectOMA);
      HashMap<String, Animation> anims = new HashMap<String, Animation>();
      for (int i = 0; i < animList.size(); ++i) {
        Animation animation = (Animation) animList.get(i).clone();

        // baking constraints into animations
        if (constraints != null && constraints.size() > 0) {
          for (Constraint constraint : constraints) {
            Long boneOMA = constraint.getBoneOMA();
            Bone bone =
                (Bone)
                    blenderContext.getLoadedFeature(boneOMA, LoadedFeatureDataType.LOADED_FEATURE);
            int targetIndex =
                bone == null
                    ? 0
                    : animData.skeleton.getBoneIndex(
                        bone); // bone==null may mean the object animation
            constraint.affectAnimation(animation, targetIndex);
          }
        }

        anims.put(animation.getName(), animation);
      }

      // applying the control to the node
      SkeletonControl skeletonControl = new SkeletonControl(animData.skeleton);
      AnimControl control = new AnimControl(animData.skeleton);

      control.setAnimations(anims);
      node.addControl(control);
      node.addControl(skeletonControl);
    }
    return node;
  }
  /**
   * The method applies skeleton animations to the given node.
   *
   * @param node the node where the animations will be applied
   * @param skeleton the skeleton of the node
   * @param animationMatchMethod the way animation should be matched with skeleton
   */
  public void applyAnimations(
      Node node, Skeleton skeleton, AnimationMatchMethod animationMatchMethod) {
    node.addControl(new SkeletonControl(skeleton));
    blenderContext.setNodeForSkeleton(skeleton, node);
    List<BlenderAction> actions = this.getActions(skeleton, animationMatchMethod);

    if (actions.size() > 0) {
      List<Animation> animations = new ArrayList<Animation>();
      for (BlenderAction action : actions) {
        BoneTrack[] tracks = action.toTracks(skeleton);
        if (tracks != null && tracks.length > 0) {
          Animation boneAnimation = new Animation(action.getName(), action.getAnimationTime());
          boneAnimation.setTracks(tracks);
          animations.add(boneAnimation);
          Long animatedNodeOMA =
              ((Number) blenderContext.getMarkerValue(ObjectHelper.OMA_MARKER, node)).longValue();
          blenderContext.addAnimation(animatedNodeOMA, boneAnimation);
        }
      }
      if (animations.size() > 0) {
        AnimControl control = new AnimControl(skeleton);
        HashMap<String, Animation> anims = new HashMap<String, Animation>(animations.size());
        for (int i = 0; i < animations.size(); ++i) {
          Animation animation = animations.get(i);
          anims.put(animation.getName(), animation);
        }
        control.setAnimations(anims);
        node.addControl(control);

        // make sure that SkeletonControl is added AFTER the AnimControl
        SkeletonControl skeletonControl = node.getControl(SkeletonControl.class);
        if (skeletonControl != null) {
          node.removeControl(SkeletonControl.class);
          node.addControl(skeletonControl);
        }
      }
    }
  }
  /**
   * 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
   */
  @SuppressWarnings("unchecked")
  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)) {
        File file = new File(path);
        List<String> pathsToCheck = new ArrayList<String>();
        String currentPath = file.getName();
        do {
          pathsToCheck.add(currentPath);
          file = file.getParentFile();
          if (file != null) {
            currentPath = file.getName() + '/' + currentPath;
          }
        } while (file != null);

        Spatial loadedAsset = null;
        BlenderKey blenderKey = null;
        for (String p : pathsToCheck) {
          blenderKey = new BlenderKey(p);
          blenderKey.setLoadUnlinkedAssets(true);
          try {
            loadedAsset = blenderContext.getAssetManager().loadAsset(blenderKey);
            break; // break if no exception was thrown
          } catch (AssetNotFoundException e) {
            LOGGER.log(Level.FINEST, "Cannot locate linked resource at path: {0}.", p);
          }
        }

        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();

            List<Node> scenes = (List<Node>) entry.getValue().get("scenes");
            for (Node scene : scenes) {
              blenderContext.addLinkedFeature(linkedDataFilePath, "SC" + scene.getName(), scene);
            }
            List<Node> objects = (List<Node>) entry.getValue().get("objects");
            for (Node object : objects) {
              blenderContext.addLinkedFeature(linkedDataFilePath, "OB" + object.getName(), object);
            }
            List<TemporalMesh> meshes = (List<TemporalMesh>) entry.getValue().get("meshes");
            for (TemporalMesh mesh : meshes) {
              blenderContext.addLinkedFeature(linkedDataFilePath, "ME" + mesh.getName(), mesh);
            }
            List<MaterialContext> materials =
                (List<MaterialContext>) entry.getValue().get("materials");
            for (MaterialContext materialContext : materials) {
              blenderContext.addLinkedFeature(
                  linkedDataFilePath, "MA" + materialContext.getName(), materialContext);
            }
            List<Texture> textures = (List<Texture>) entry.getValue().get("textures");
            for (Texture texture : textures) {
              blenderContext.addLinkedFeature(
                  linkedDataFilePath, "TE" + texture.getName(), texture);
            }
            List<Texture> images = (List<Texture>) entry.getValue().get("images");
            for (Texture image : images) {
              blenderContext.addLinkedFeature(linkedDataFilePath, "IM" + image.getName(), image);
            }
            List<Animation> animations = (List<Animation>) entry.getValue().get("animations");
            for (Animation animation : animations) {
              blenderContext.addLinkedFeature(
                  linkedDataFilePath, "AC" + animation.getName(), animation);
            }
            List<Camera> cameras = (List<Camera>) entry.getValue().get("cameras");
            for (Camera camera : cameras) {
              blenderContext.addLinkedFeature(linkedDataFilePath, "CA" + camera.getName(), camera);
            }
            List<Light> lights = (List<Light>) entry.getValue().get("lights");
            for (Light light : lights) {
              blenderContext.addLinkedFeature(linkedDataFilePath, "LA" + light.getName(), light);
            }
            Spatial sky = (Spatial) entry.getValue().get("sky");
            if (sky != null) {
              blenderContext.addLinkedFeature(linkedDataFilePath, sky.getName(), sky);
            }
            List<Filter> filters = (List<Filter>) entry.getValue().get("filters");
            for (Filter filter : filters) {
              blenderContext.addLinkedFeature(linkedDataFilePath, filter.getName(), filter);
            }
          }
        } 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;
  }