// Dstl (c) Crown Copyright 2017
// Modified by NCA (c) Crown Copyright 2017
package uk.gov.dstl.baleen.core.web.servlets;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.uima.fit.descriptor.ConfigurationParameter;
import org.apache.uima.fit.descriptor.ExternalResource;
import org.apache.uima.fit.factory.ExternalResourceFactory;
import org.slf4j.LoggerFactory;

import com.google.common.net.MediaType;

import uk.gov.dstl.baleen.core.utils.ReflectionUtils;
import uk.gov.dstl.baleen.exceptions.InvalidParameterException;

/**
 * Abstract class for listing components based on their super type (for example, BaleenAnnotator).
 * Returns a YAML formatted list of components, which could in theory be copied into a pipeline
 * configuration. In practice though, the order may not be appropriate and some components will
 * require configuration.
 */
public class AbstractComponentApiServlet extends AbstractApiServlet {
  private static final long serialVersionUID = 1L;

  private final Class<?> clazz;

  private final String componentClass;

  private final String componentPackage;

  private final transient List<String> excludePackage;

  private final transient List<String> excludeClass;

  private final transient Optional<String> components;

  /**
   * Constructor
   *
   * @param componentClass The name of the supertype for which we are listing subclasses
   * @param componentPackage The default package name, which will be stripped from class names
   * @param excludeClass A list of classes of which subclasses will be excluded from the listing
   * @param excludePackage A list of regular expressions against which packages will be tested and,
   *     if matched, excluded from the listing
   * @param clazz The subclass of AbstractComponentApiServlet, for creating a logger
   */
  public AbstractComponentApiServlet(
      String componentClass,
      String componentPackage,
      List<String> excludeClass,
      List<String> excludePackage,
      Class<?> clazz) {
    super(LoggerFactory.getLogger(clazz), clazz);
    this.componentClass = componentClass;
    this.componentPackage = componentPackage;
    this.excludeClass = excludeClass;
    this.excludePackage = excludePackage;

    this.clazz = clazz;
    components = calculateComponents();
  }

  private Optional<String> calculateComponents() {
    Class<?> componentClazz = null;
    try {
      componentClazz = Class.forName(componentClass);
    } catch (ClassNotFoundException e) {
      LoggerFactory.getLogger(clazz)
          .warn("Unable to find component class - annotator listing will not be available", e);
    }

    List<Class<?>> excludeClazz = new ArrayList<>();
    for (String c : excludeClass) {
      try {
        Class<?> cl = Class.forName(c);
        excludeClazz.add(cl);
      } catch (ClassNotFoundException e) {
        LoggerFactory.getLogger(clazz)
            .warn("Unable to find component class - component will not be excluded", e);
      }
    }

    if (componentClazz == null) {
      return Optional.empty();
    } else {
      List<String> componentsList =
          classesToFilteredList(
              ReflectionUtils.getSubTypes(componentClazz),
              componentPackage,
              excludeClazz,
              excludePackage);

      StringBuilder componentBuilder = new StringBuilder();
      for (String s : componentsList) {
        componentBuilder.append("- " + s + "\n");
      }

      return Optional.of(componentBuilder.toString());
    }
  }

  private List<String> classesToFilteredList(
      Set<?> components,
      String componentPackage,
      List<Class<?>> excludeClass,
      List<String> excludePackage) {
    List<String> ret = new ArrayList<>();
    for (Object o : components) {

      try {
        Class<?> c = (Class<?>) o;

        String s = c.getName();
        String p = c.getPackage().getName();

        if (excludeByPackage(p, excludePackage)
            || excludeByClass(c, excludeClass)
            || isAbstract(c)) {
          continue;
        }

        if (s.startsWith(componentPackage)) {
          s = s.substring(componentPackage.length());
          s = StringUtils.strip(s, ".");
        }

        ret.add(s);
      } catch (ClassCastException cce) {
        LoggerFactory.getLogger(clazz).warn("Unable to cast to class", cce);
      }
    }

    Collections.sort(ret);

    return ret;
  }

  private static boolean isAbstract(Class<?> clazz) {
    return Modifier.isAbstract(clazz.getModifiers());
  }

  /**
   * Compare a package with a list of packages to determine whether the package should be excluded
   * or not.
   *
   * @param pkg The package to test
   * @param excludePkg A list of RegEx patterns that describe the set of packages to test against
   * @return True if the package should be excluded, false otherwise
   */
  public static boolean excludeByPackage(String pkg, List<String> excludePkg) {
    for (String ep : excludePkg) {
      if (pkg.matches(ep)) {
        return true;
      }
    }

    return false;
  }

  /**
   * Compare a package with a list of classes to determine whether the class should be excluded or
   * not. Classes in the list, or that inherit from a class in the list, will be excluded.
   *
   * @param clazz The class to test
   * @param excludeClazz A list of classes to test against
   * @return True if the class should be excluded, false otherwise
   */
  public static boolean excludeByClass(Class<?> clazz, List<Class<?>> excludeClazz) {
    for (Class<?> ec : excludeClazz) {
      if (ec.isAssignableFrom(clazz)) {
        return true;
      }
    }

    return false;
  }

  @Override
  protected void get(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    String path = req.getPathInfo();
    if (path == null) {
      path = "";
    } else if (path.startsWith("/")) {
      path = path.substring(1);
    }

    if (path.isEmpty()) {
      if (!getComponents().isPresent()) {
        respondWithError(resp, 503, "Unable to load annotator class");
        return;
      }
      respond(resp, MediaType.create("text", "x-yaml"), getComponents().orElse(""));
    } else {
      try {
        Class<?> component = getClassFromString(path, componentPackage);
        List<Map<String, Object>> parameters = getParameters(component);

        respondWithJson(resp, parameters);
      } catch (InvalidParameterException ipe) {
        LoggerFactory.getLogger(clazz).warn("Could not find requested resource", ipe);
        respondWithNotFound(resp);
      }
    }
  }

  /**
   * Takes a string of the class name and return a Class. First tries looking in the default
   * packages, and then if not found it will assume the class is fully qualified and try to use the
   * name as it is provided
   *
   * @param className The name of the class
   * @param defaultPackage The package to look in if the className isn't a fully qualified name
   * @return The class specified
   */
  @SuppressWarnings("unchecked")
  protected <S extends T, T> Class<S> getClassFromString(String className, String... defaultPackage)
      throws InvalidParameterException {
    for (String pkg : defaultPackage) {
      try {
        return (Class<S>) Class.forName(pkg + "." + className);
      } catch (Exception e) {
        LoggerFactory.getLogger(clazz)
            .debug("Couldn't find class {} in package {}", className, pkg, e);
      }
    }

    try {
      return (Class<S>) Class.forName(className);
    } catch (Exception e) {
      throw new InvalidParameterException(
          "Could not find or instantiate analysis engine " + className, e);
    }
  }

  /**
   * Get all the parameters (fields annotated with @ConfigurationParameter) and resources (fields
   * annotated with @ExternalResource) for the given class
   *
   * @param clazz
   * @return A list containing maps which contains information about each parameter and resource
   */
  protected static List<Map<String, Object>> getParameters(Class<?> clazz) {
    List<Map<String, Object>> parametersOutput = new ArrayList<>();

    Field[] fields = clazz.getDeclaredFields();
    if (fields != null) {
      for (Field field : fields) {
        parametersOutput.addAll(processParameters(field));
        parametersOutput.addAll(processResources(field));
      }
    }

    if (clazz.getSuperclass() != null) {
      parametersOutput.addAll(getParameters(clazz.getSuperclass()));
    }

    return parametersOutput;
  }

  private static List<Map<String, Object>> processParameters(Field field) {
    List<Map<String, Object>> parametersOutput = new ArrayList<>();
    ConfigurationParameter[] parameters = field.getAnnotationsByType(ConfigurationParameter.class);

    for (ConfigurationParameter param : parameters) {
      if (ExternalResourceFactory.PARAM_RESOURCE_NAME.equals(param.name())) {
        continue;
      }

      Map<String, Object> parameterOutput = new HashMap<>();
      parameterOutput.put("name", param.name());
      parameterOutput.put("defaultValue", stringArrayToString(param.defaultValue()));
      parameterOutput.put("type", "parameter");

      parametersOutput.add(parameterOutput);
    }

    return parametersOutput;
  }

  private static List<Map<String, Object>> processResources(Field field) {
    List<Map<String, Object>> resourcesOutput = new ArrayList<>();
    ExternalResource[] resources = field.getAnnotationsByType(ExternalResource.class);

    for (ExternalResource resource : resources) {
      Map<String, Object> resourceOutput = new HashMap<>();
      resourceOutput.put("key", resource.key());
      resourceOutput.put("class", field.getType().getName());
      resourceOutput.put("type", "resource");

      resourceOutput.put("parameters", getParameters(field.getType()));

      resourcesOutput.add(resourceOutput);
    }

    return resourcesOutput;
  }

  protected static Object stringArrayToString(String[] arr) {
    if (arr.length == 1) {
      return arr[0];
    } else {
      return arr;
    }
  }

  protected Optional<String> getComponents() {
    return components;
  }
}
