package nxm.sys.libg;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.function.Function;
import java.util.logging.Level;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.PlotFile;
import nxm.sys.lib.CsvFile;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.FileUtil.PlotFileWrapper;
import nxm.sys.lib.GeoJSONFile;
import nxm.sys.lib.ImageFile;
import nxm.sys.lib.KmlFile;
import nxm.sys.lib.LogUtilities;
import nxm.sys.lib.Midas;
import nxm.sys.lib.OptionTree;
import nxm.sys.lib.PropsUtilities;
import nxm.sys.lib.ShapeFile;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Table;

/** This is a Layer Utility to provide getLayer methods that were initially located in the individual PlotFile classes.
 *  @author Kevin Esterheld
 *  @since NeXtMidas 3.9.2
 */
public class LayerUtil {

  /** Holder of the getLayer Functions */
  private static Map<Class<?>, Function<PlotFile, Layer>> getLayerRegistry;

  /** Map that holds the list of option trees that have been loaded to prevent duplication. */
  private static Map<String,Boolean> optionTrees;

  static {
    getLayerRegistry = new HashMap<>();
    optionTrees = new HashMap<>();
    // Load the getlayer functions that are found in the sys option tree. once they are loaded, there is no way to change these values
    addFunction(CsvFile.class,         (pf) -> LayerMulti.getLayer());
    addFunction(ImageFile.class,       (pf) -> LayerImage.getLayer());
    addFunction(ShapeFile.class,       (pf) -> LayerShape.getLayer());
    addFunction(KmlFile.class,         (pf) -> LayerKML.getLayer());
    addFunction(GeoJSONFile.class,     (pf) -> LayerGeoJSON.getLayer());
    addFunction(DataFile.class,        (pf) -> dataFileGetLayer(pf));
    addFunction(PlotFileWrapper.class, (pf) -> plotFileWrapperGetLayer(pf));
    optionTrees.put("SYS", true);
    loadFunctionsFromPropsFile();
  }

  /** This constructor was automatically provided by Java,
   *  but it is unneeded as all the methods are static methods.
   *  @deprecated now that Javadocs are required, it is temporarily
   *              being explicitely provided but deprecated
   */
  @Deprecated
  public LayerUtil () {}

  /** This will execute the instance specific getLayer Function
   *  @param pf The plot file
   *  @return The Layer that executes the instance specific getLayer() method for layer creation.
   */
  public static Layer plotFileWrapperGetLayer (PlotFile pf) {
    return getFunction(pf.getClass()).apply(pf);
  }

  /** get DataFile getLayer
   *
   *  @param pf     PlotFile
   *  @return Layer the Layer that should be used for the given PlotFile
   */
  public static Layer dataFileGetLayer (PlotFile pf) { return getDataFileLayer((DataFile)pf,0); }

  /** Special version of PlotFile.getLayer() that is used internal to {@link Layer}.
      This is necessary since {@link DataFile} supports so many types and needs to
      know some of the PLOT options in order to determine which layer type to use.
      This version is for internal use only and may change without notice.
      @param df DataFile type
      @param plotType plot type
      @return layer the Layer that should be used for the given PlotFile
   */
  @InternalUseOnly
  public static Layer getDataFileLayer (DataFile df, int plotType) {
    switch (df.getType()) {
      case 1998: return new LayerPoly(MColor.MAPLINE);
      case 1999: return new LayerPoly(MColor.MAPLINE);
    }

    boolean oneFrame   = (df.getSize() == 1) && !df.isStream();
    boolean isLinePlot = (plotType==PlotFile.T_LINE) || (plotType==PlotFile.T_POINT) || (plotType==PlotFile.T_DOT);

    switch (df.getTypeCodeClass()) {
      case 1:  return new Layer1D();
      case 2:  return (oneFrame||isLinePlot)? new Layer1D() : new Layer2D();
      case 3:  return ((df.findRec("ABSC")>=0||df.findRec("TIME")>=0))? new LayerXY() : new LayerMulti();
      case 6:  return ((df.findRec("ABSC")>=0||df.findRec("TIME")>=0))? new LayerXY() : new LayerMulti();
      case 5:  return new LayerT5();
      default: return null;
    }
  }

  /** Add a Function for getLayer methods
   *  @param clazz The class to add the method to
   *  @param method The lambda of the method
   *  @return true if it was added successfully
   */
  public static boolean addFunction (Class<?> clazz, Function<PlotFile, Layer> method) {
    // This if will protect any sys functions from being overwritten
    if(getLayerRegistry.containsKey(clazz) && clazz.getPackage().getName().equals("nxm.sys.lib")) {
      System.out.println("Hello");
      Midas.log("Overwriting of "+clazz+" not allowed. Not adding to the registry", Midas.WARNING);
      return false;
    }
    getLayerRegistry.put(clazz, method);
    return true;
  }

  /** Get a function for the provided class.
   *  @param clazz The class that the method is assigned to.
   *  @return The function method.
   */
  public static Function<PlotFile, Layer> getFunction (Class<?> clazz) {
    return getLayerRegistry.get(clazz);
  }

  /** This will check to see if all the option trees on the path are currently loaded into the registry. If an option
   *  tree has not been loaded, it will be loaded. It is only possible to add an option tree once using this strategy. To
   *  add an option tree out of this method (i.e. need to reload it due to addition of getLayer method) please use
   *  {@link #loadFunctionFromPropsFile(String)}
   */
  public static void refreshRegistry () {
    String[] opts = Shell.getSharedMidasContext().getCurrentPathArray();
    for (String opt : opts) {
      if (!optionTrees.containsKey(opt)) {
        loadFunctionFromPropsFile(opt);
      }
    }
  }

  /** This is a wrapper method to load the getLayer methods of an options tree.
   *  This is used when an option tree is added and needs to have it's methods added.
   *  @param opt option tree
   */
  private static final void loadFunctionFromPropsFile(String opt) {
    Table optionTrees = OptionTree.getAllOptions(Shell.getMidasContext());
    loadOptionTreeFunction(optionTrees, opt);
  }

  /** Utility method that will load all the getLayer methods that are defined in
   *  the PlotFileLayer.props located in each option tree.
   */
  private static final void loadFunctionsFromPropsFile () {
    Table optionTrees = OptionTree.getAllOptions(Shell.getMidasContext());
    for(String opt : optionTrees.getKeys()) {
      loadOptionTreeFunction(optionTrees, opt);
    }
  }

  /** This method will load the option tree form the give table of option trees.
   *  @param optionTrees Table holding the option trees
   *  @param opt The option tree to be loaded
   */
  private static final void loadOptionTreeFunction (Table optionTrees, String opt) {
    LayerUtil.optionTrees.put(opt.toUpperCase(), true); // We have loaded the option tree - if there is an error loading it is probably because there is nothing to load so we don't want to try every time we call refresh. Do this first!
    Table optionTree = (Table) optionTrees.get(opt); // get the option tree as a table
    String propsFile = optionTree.get("CFGROOT").toString()+"cfg"+File.separator+"PlotFileLayer.props"; // get the file location of the PlotFileLayer.props
    if (propsFile.startsWith("classpath:")) {// Need to remove classpath: if we are using a jar
      propsFile = propsFile.replace("classpath:", File.separator);
    }

    Properties props = new Properties();
    try {
      props.load(LayerUtil.class.getResourceAsStream(propsFile)); // Load from java classpath if it is in a jar
    } catch (IOException|NullPointerException e) {
      if (!(new File(propsFile).exists())) {
        return; // Need to see if it exists if not in a jar, if not simply return.
      }
      props = PropsUtilities.loadProps(propsFile); // load from the props file
    }
    if (props == null) {
      return; // no props, lets get out of here
    }
    for(String classNameFileType : props.stringPropertyNames()) { // iterate through the properties
      Class<?> clazz = null; // the class object that will server as a key in the registry
      String methodName = null; // the method to save in the registry
      final boolean needPlotFile; // Does the method need the PlotFile parameter
      String classNameGetLayer = null; // The class that holds the method.
      try {
       clazz = Class.forName(classNameFileType); // Define the class object that will serve as the key
       if(!PlotFile.class.isAssignableFrom(clazz)) {
         LogUtilities.log(Level.WARNING, "The file type provided in PlotFileLayer.props is not a PlotFile type. PlotFile="+classNameFileType);
         continue;
       }
       if(getLayerRegistry.containsKey(clazz)) {
         // No need to go any further, we do not override items. Once the item is loaded, it can be considered final.
         LogUtilities.log(Level.FINE, "Class:"+classNameFileType+" already loaded into the registry."); // I think that fine is a good level, otherwise we blow up the console every time an opt is added to the path
         continue;
       }
       String value = props.getProperty(classNameFileType).trim(); // Get the value out of the props file and remove any whitespace.
       int indexOfDot = value.lastIndexOf("."); // find the last index of "." - that will separate class from method.
       classNameGetLayer = value.substring(0, indexOfDot); // get the class out of the string
       methodName = value.substring(indexOfDot+1); // get the method out of the string
       if (classNameGetLayer.startsWith("p->") || classNameGetLayer.startsWith("pf->")) { // if it has either p or pf, it needs to have a paramter for plotfile
         needPlotFile=true; // needplotFile is now true
         classNameGetLayer= classNameGetLayer.substring(classNameGetLayer.indexOf(">")+1).trim(); // remove the p-> or pf->

       }
       else needPlotFile = false; // don;t need a parameter
        try {
          // get the method based on parameter - does it need PlotFile paramter
          final Method m = needPlotFile ? Class.forName(classNameGetLayer).getMethod(methodName, PlotFile.class)
              : Class.forName(classNameGetLayer).getMethod(methodName);
          // Add the function based on parameter
          LayerUtil.addFunction(clazz, pf -> {
            try {
              return needPlotFile ? (Layer) m.invoke(null, pf) : (Layer) m.invoke(null);
            } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
              LogUtilities.log(Level.WARNING, "Exception Loading getLayer Functions for "+value, e);
              return null;
            }
          });
        } catch (NoSuchMethodException | SecurityException e) {
          LogUtilities.log(Level.WARNING, "Exception Loading getLayer Functions for "+value, e);
        }
      } catch (ClassNotFoundException e) {
        LogUtilities.log(Level.WARNING, "Exception Loading getLayer Functions for class "+classNameFileType, e);
      }
    }
  }
}
