/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.geode.cache.internal.execute;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import org.apache.logging.log4j.Logger;

import org.apache.geode.cache.CacheClosedException;
import org.apache.geode.cache.CacheFactory;
import org.apache.geode.cache.Declarable;
import org.apache.geode.cache.execute.Function;
import org.apache.geode.cache.execute.FunctionService;
import org.apache.geode.internal.cache.InternalCache;
import org.apache.geode.internal.classloader.ClassPathLoader;
import org.apache.geode.logging.internal.log4j.api.LogService;
import org.apache.geode.management.internal.deployment.FunctionScanner;
import org.apache.geode.management.internal.utils.JarFileUtils;

/**
 * This class is used to register {@link Function}s from deployed jar files. It provides convenience
 * over working with the {@link FunctionService} directly.
 */
public class FunctionToFileTracker {

  private static final Logger logger = LogService.getLogger();

  private final Map<String, List<Function<?>>> deploymentToFunctionsMap = new ConcurrentHashMap<>();

  /**
   * Scan the JAR file and attempt to register any function classes found.
   *
   * @param jarFile The {@link File} that contains {@link Function} that are to be registered
   */
  public synchronized void registerFunctionsFromFile(File jarFile)
      throws ClassNotFoundException, IOException {
    logger.debug("Registering functions for: {}", jarFile.getName());

    List<Function<?>> registeredFunctions = new LinkedList<>();
    try {
      Collection<String> functionClasses = findFunctionsInThisJar(jarFile);
      String filePath = jarFile.getAbsolutePath();
      Collection<Function<?>> functions = new LinkedList<>();
      for (String functionClass : functionClasses) {
        logger.debug("Attempting to load class: {}, from JAR file: {}", functionClass,
            filePath);
        try {
          functions.addAll(loadFunctionFromClassName(functionClass));
        } catch (ClassNotFoundException | NoClassDefFoundError cnfex) {
          logger.error("Unable to load all classes from JAR file: {}",
              filePath, cnfex);
          throw cnfex;
        }
      }
      registeredFunctions.addAll(registerFunction(filePath, functions));
    } catch (IOException ioex) {
      logger.error("Exception when trying to find function classes from Jar", ioex);
      throw ioex;
    }
    String artifactId = JarFileUtils.toArtifactId(jarFile.getName());
    List<Function<?>> previouslyRegisteredFunctions =
        deploymentToFunctionsMap.remove(artifactId);
    if (!registeredFunctions.isEmpty()) {
      deploymentToFunctionsMap.put(artifactId, registeredFunctions);
    }
    unregisterUndeployedFunctions(previouslyRegisteredFunctions, registeredFunctions);
  }

  private void unregisterUndeployedFunctions(List<Function<?>> previouslyRegisteredFunctions,
      List<Function<?>> registeredFunctions) {

    if (previouslyRegisteredFunctions == null) {
      return;
    }

    List<String> currentlyRegisteredFunctionIDs =
        registeredFunctions.stream().map(Function::getId).collect(Collectors.toList());
    previouslyRegisteredFunctions.stream().map(Function::getId)
        .filter(functionId -> !currentlyRegisteredFunctionIDs.contains(functionId))
        .forEach(FunctionService::unregisterFunction);
  }

  private Collection<Function<?>> loadFunctionFromClassName(String className)
      throws ClassNotFoundException {
    Class<?> clazz = ClassPathLoader.getLatest().forName(className);
    return getRegisterableFunctionsFromClass(clazz);
  }

  private List<Function<?>> registerFunction(String jarFilePath,
      Collection<Function<?>> functions) {
    List<Function<?>> registeredFunctions = new ArrayList<>();
    for (Function<?> function : functions) {
      FunctionService.registerFunction(function);
      logger.debug("Registering function class: {}, from JAR file: {}",
          function.getClass().getName(), jarFilePath);
      registeredFunctions.add(function);
    }
    return registeredFunctions;
  }

  /**
   * Unregisters functions from a previously deployed jar that are not present in its replacement.
   * If newJar is null, all functions registered from oldJar will be removed.
   *
   * @param jarFileName - The name of the deployment that linked to the functions that are to be
   *        unregistered.
   */
  public void unregisterFunctionsForDeployment(String jarFileName) {
    List<Function<?>> functions =
        deploymentToFunctionsMap.remove(JarFileUtils.getArtifactId(jarFileName));
    if (functions != null) {
      functions.stream().map(Function::getId).forEach(FunctionService::unregisterFunction);
    }
  }

  private static Collection<String> findFunctionsInThisJar(File jarFile) throws IOException {
    return new FunctionScanner().findFunctionsInJar(jarFile);
  }

  /**
   * Check to see if the class implements the Function interface. If so, it will be registered with
   * FunctionService. Also, if the functions's class was originally declared in a cache.xml file
   * then any properties specified at that time will be reused when re-registering the function.
   *
   * @param clazz Class to check for implementation of the Function class
   * @return A collection of Objects that implement the Function interface.
   */
  @SuppressWarnings({"deprecation", "unchecked"})
  private Collection<Function<?>> getRegisterableFunctionsFromClass(Class<?> clazz) {
    final List<Function<?>> registerableFunctions = new ArrayList<>();

    try {
      if (Function.class.isAssignableFrom(clazz) && !Modifier.isAbstract(clazz.getModifiers())) {
        boolean registerUninitializedFunction = true;
        if (Declarable.class.isAssignableFrom(clazz)) {
          try {
            InternalCache cache = (InternalCache) CacheFactory.getAnyInstance();
            final List<Properties> propertiesList = cache.getDeclarableProperties(clazz.getName());

            if (!propertiesList.isEmpty()) {
              registerUninitializedFunction = false;
              // It's possible that the same function was declared multiple times in cache.xml
              // with different properties. So, register the function using each set of
              // properties.
              for (Properties properties : propertiesList) {
                Function<?> function = newFunction((Class<Function<?>>) clazz, true);
                if (function != null) {
                  ((Declarable) function).initialize(cache, properties);
                  ((Declarable) function).init(properties); // for backwards compatibility
                  if (function.getId() != null) {
                    registerableFunctions.add(function);
                  }
                }
              }
            }
          } catch (CacheClosedException ignored) {
            // That's okay, it just means there were no properties to init the function with
          }
        }

        if (registerUninitializedFunction) {
          @SuppressWarnings("unchecked")
          Function<?> function = newFunction((Class<Function<?>>) clazz, false);
          if (function != null && function.getId() != null) {
            registerableFunctions.add(function);
          }
        }
      }
    } catch (Exception ex) {
      logger.error("Attempting to register function from class: {}", clazz, ex);
    }

    return registerableFunctions;
  }

  private Function<?> newFunction(final Class<Function<?>> clazz,
      final boolean errorOnNoSuchMethod) {
    try {
      final Constructor<Function<?>> constructor = clazz.getConstructor();
      return constructor.newInstance();
    } catch (NoSuchMethodException nsmex) {
      if (errorOnNoSuchMethod) {
        logger.error("Zero-arg constructor is required, but not found for class: {}",
            clazz.getName(), nsmex);
      } else {
        logger.debug(
            "Not registering function because it doesn't have a zero-arg constructor: {}",
            clazz.getName());
      }
    } catch (Exception ex) {
      logger.error("Error when attempting constructor for function for class: {}", clazz.getName(),
          ex);
    }

    return null;
  }
}
