package nxm.sys.lib;

import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Predicate;

import nxm.sys.inc.InternalUseOnly;

/** A class to both hold a java.util.function.Predicate and its Type
 *  and to support creating of Maps with String Keys and PredicateType values.
 *  This is intended to create Maps that can act as filters and
 *  limit which points, etc. are displayed.
 *  Java's Type erasure would cause the original Type of the Predicate to be lost.
 *  This class saves it so that incoming test values can be converted to the appropriate
 *  Type before applying the Predicate.
 *
 *  @author Jeanette McCall
 *  @since NeXtMidas 4.1.1
 *
 */
@InternalUseOnly
public class PredicateType {
  private Type type;
  private Class<?> typeClass;
  private Predicate<?> predicate;
  private String lambdaString = "";
  private static boolean debug = false;
  private static boolean extendedDebug = debug || false;

  /** Constructor
   *
   *  @param predicate the Java Predicate that implements test()
   *  @param type      the Java Type (or Class) of the Predicate
   */
  @InternalUseOnly
  public PredicateType (Predicate<?> predicate, Type type) {
    this.predicate = predicate;
    this.type = type;
    try {
      this.typeClass = (Class<?>)type;
    }
    catch (ClassCastException cce) {};
  }

  /** Constructor with String describing the Predicate
   *  This allows a more readable toString with lambda expressions
   *
   *  @param predicate        the Java Predicate that implements test()
   *  @param type             the Java Type (or Class) of the Predicate
   *  @param lambdaExpression the String description of the Predicate
   */
  @InternalUseOnly
  public PredicateType (Predicate<?> predicate, Type type, String lambdaExpression) {
    this(predicate,type);
    this.lambdaString = lambdaExpression;
  }

  /** get the Type associated with the Predicate
   *  @return the Java Type */
  @InternalUseOnly
  public Type getType () {
    return type;
  }

  /** If the Predicate Type is a Class, get the Class
   *  @return the Class or null, if the Type is not a Class
   */
  @InternalUseOnly
  public Class<?> getTypeClass () {
    return typeClass;
  }

  /** Get the Java Predicate
   *  @return the Java Predicate
   */
  @InternalUseOnly
  public Predicate<?> getPredicate () {
    return predicate;
  }

  /** Get the String description of the Predicate if there is one.
   *  This is used by toString when available instead of the default
   *  toString, which is not very informative when the Predicate is a lambda.
   *  @return the String description of the Predicate or an empty String if
   *          this information was not supplied.
   */
  @InternalUseOnly
  public String getLambdaString () {
    return lambdaString;
  }

  /** Add a Key and Predicate with associated Type to a Filter Map
   *
   *  @param map  Map to act as filter
   *  @param key  key (ex: column namd in T5000 file)
   *  @param pred Java Predicate giving test associated with the key
   *  @param type The Type/Class of the Java Predicate
   */
  @InternalUseOnly
  public static void addKeyFunction (Map<String,PredicateType> map, String key,
                                     Predicate<?> pred, Type type) {
    PredicateType pt = new PredicateType(pred, type);
    map.put(key, pt);
  }

  /** Add a Key and Predicate with associated Type to a Filter Map
   *  along with a description of the Predicate
   *
   *  @param map  Map to act as filter
   *  @param key  key (ex: column namd in T5000 file)
   *  @param pred Java Predicate giving test associated with the key
   *  @param type The Type/Class of the Java Predicate
   *  @param description a String description of the Java Predicate
   */
  @InternalUseOnly
  public static void addKeyFunction (Map<String,PredicateType> map, String key,
                                     Predicate<?> pred, Type type, String description) {
    if (extendedDebug) System.out.println("addKeyFunction key:"+key+" type:"+type);
    PredicateType pt = new PredicateType(pred, type, description);
    map.put(key, pt);
  }

  /** Select one of the built in 'IF' style Predicates and add the key and
   *  Predicate to the Map
   *  @param map        Map to act as filter
   *  @param key        (ex: column named in T5000 file)
   *  @param predTest     String specifying built-in TEST
   *  @param predExpected String specifying expected
   */
  @InternalUseOnly
  public static void addKeyFunction (Map<String,PredicateType> map, String key, String predTest, String predExpected) {
    PredicateType pt = predicateTypeCreator(predTest,predExpected);
    if (pt != null) map.put(key,pt);
    else {
      LogUtilities.warning("NO entry was added to the filter map for key " + key+ " , as PredicateType.predicateTypeCreator did not find :"+predTest);
    }
  }

  /** Select one of the built in 'IF' style Predicates and add the key and
   *  Predicate to the Map
   *
   *  @param map        Map to act as filter
   *  @param key        (ex: column named in T5000 file)
   *  @param predString String specifying built-in TEST to be added as Predicate
   */
  @InternalUseOnly
  public static void addKeyFunction (Map<String,PredicateType> map, String key, String predString) {
    predString = predString.trim();
    int spaceIdx = predString.indexOf(" ");
    boolean hasExpected = spaceIdx > 1;
    String predTest = (hasExpected) ? predString.substring(0, spaceIdx) : predString;
    String predExpected = (hasExpected) ? predString.substring(spaceIdx+1).trim() : "";
    PredicateType pt = (hasExpected) ?
        predicateTypeCreator(predTest,predExpected) : predicateTypeCreator(predTest);
    if (pt != null) map.put(key,pt);
    else {
      LogUtilities.warning("NO entry was added to the filter map for key " + key+ " , as PredicateType.predicateTypeCreator did not find :"+predTest);
    }
  }

  /** Select one of the built in 'IF' style Predicates and add the key and
   *  Predicate to the Map
   *
   *  @param map        Map to act as filter
   *  @param key        (ex: column named in T5000 file)
   *  @param min        The min value for the filter.
   *  @param max        The max value for the filter.
   */
  @InternalUseOnly
  public static void addKeyFunction (Map<String,PredicateType> map, String key, double min, double max)
  {
    PredicateType pt = predicateTypeCreator(min, max);
    if (pt != null)
      map.put(key,pt);
    else
      LogUtilities.warning("NO entry was added to the filter map for key " + key);
  }

  /** Create a predicate using an Midas IF style expression
   *
   *  @param predTest      IF style test
   *  @param predExpected  Expected in Strimg form
   *  @return  formed Predicate or null if test not found
   */
  public static Predicate<?> predicateCreator (String predTest, String predExpected) {
    PredicateType pt = (predExpected==null || predExpected.isEmpty()) ?
        predicateTypeCreator(predTest) : predicateTypeCreator(predTest,predExpected);
    if (pt != null) return pt.getPredicate();
    else return null;
  }

  /** Create a PredicateType that does not need the extra expected value
   *
   *  @param predTest the IF style test
   *  @return PredicateType for IF style test
   */
  private static PredicateType predicateTypeCreator (String predTest) {
    switch (predTest) {
      case "ISNULL": {
        Predicate<Optional<?>> isNull = o -> !o.isPresent();
        return new PredicateType(isNull, Optional.class, predTest);
      }
      case "NISNULL": {
        Predicate<Optional<?>> nIsNull = o -> o.isPresent();
        return new PredicateType(nIsNull, Optional.class, predTest);
      }
      default: {
        LogUtilities.warning("PredicateType predicate creator without expected value did not find :"+predTest);
        return null;
      }
    }
  }

  /** Create a PredicateType that has an expected value
   *
   *  @param predTest     the IF style test
   *  @param predExpected the expected value
   *  @return PredicateType for IF style test
   */
  private static PredicateType predicateTypeCreator (String predTest, String predExpected) {
    String defaultDescription = predTest + " " + predExpected;
    switch (predTest) {
      // Normal IF
      case "EQ":
      case "==": {
        final Double expected = Double.valueOf(predExpected);
        Predicate<Double> eq = d -> d.equals(expected);
        return new PredicateType(eq, Double.TYPE, defaultDescription);
      }
      case "NE":
      case "!=": {
        final Double expected = Double.valueOf(predExpected);
        Predicate<Double> neq = d -> !d.equals(expected);
        return new PredicateType(neq, Double.TYPE, defaultDescription);
      }
      case "GT":
      case ">": {
        final Double expected = Double.valueOf(predExpected);
        Predicate<Double> gt = d -> d > expected;
        return new PredicateType(gt, Double.TYPE, defaultDescription);
      }
      case "LT":
      case "<": {
        final Double expected = Double.valueOf(predExpected);
        Predicate<Double> lt = d -> d < expected;
        return new PredicateType(lt, Double.TYPE, defaultDescription);
      }
      case "GE":
      case ">=": {
        final Double expected = Double.valueOf(predExpected);
        Predicate<Double> ge = d -> d >= expected;
        return new PredicateType(ge, Double.TYPE, defaultDescription);
      }
      case "LE":
      case "<=": {
        final Double expected = Double.valueOf(predExpected);
        Predicate<Double> le = d -> d <= expected;
        return new PredicateType(le, Double.TYPE, predTest + " " + predExpected);
      }
      case "EQ/T":
      case "EQT": {
        final Double expected = Double.valueOf(predExpected);
        Predicate<Double> eqt = d -> Math.abs(d - expected) < 1e-6;
        return new PredicateType(eqt, Double.TYPE, predTest + " " + predExpected);
      }
      case "NEQ/T":
      case "NEQT": {
        final Double expected = Double.valueOf(predExpected);
        Predicate<Double> nEqt = d -> !(Math.abs(d - expected) < 1e-6);
        return new PredicateType(nEqt, Double.TYPE, predTest + " " + predExpected);
      }
      case "EQS":
      case "SEQS": {
        final String expected = predExpected;
        Predicate<String> eqs = s -> s.equals(expected);
        return new PredicateType(eqs, String.class, defaultDescription);
      }
      case "NSEQS":
      case "NEQS": {
        final String expected = predExpected;
        Predicate<String> nEqs = s -> !s.equals(expected);
        return new PredicateType(nEqs, String.class, defaultDescription);
      }
      case "STARTS": {
        final String expected = predExpected;
        Predicate<String> starts = s -> s.startsWith(expected);
        return new PredicateType(starts, String.class, defaultDescription);
      }
      case "NSTARTS": {
        final String expected = predExpected;
        Predicate<String> nStarts = s -> !s.startsWith(expected);
        return new PredicateType(nStarts, String.class, defaultDescription);
      }
      case "ENDS": {
        final String expected = predExpected;
        Predicate<String> ends = s -> s.endsWith(expected);
        return new PredicateType(ends, String.class, defaultDescription);
      }
      case "NENDS": {
        final String expected = predExpected;
        Predicate<String> nEnds = s -> !s.endsWith(expected);
        return new PredicateType(nEnds, String.class, defaultDescription);
      }
      case "SUBS": {
        final String expected = predExpected;
        Predicate<String> subs = s -> expected.contains(s);
        return new PredicateType(subs, String.class, defaultDescription);
      }
      case "NSUBS": {
        final String expected = predExpected;
        Predicate<String> nSubs = s -> !expected.contains(s);
        return new PredicateType(nSubs, String.class, defaultDescription);
      }
      case "CONTAINS/S": {
        final String expected = predExpected;
        Predicate<String> containsS = s -> s.contains(expected);
        return new PredicateType(containsS, String.class, defaultDescription);
      }
      case "NCONTAINS/S": {
        final String expected = predExpected;
        Predicate<String> nContainsS = s -> !s.contains(expected);
        return new PredicateType(nContainsS, String.class, defaultDescription);
      }
      case "CONTAINS/K":
      case "CONTAINS": {
        final String expected = predExpected;
        Predicate<Table> contains = t -> t.containsKey(expected);
        return new PredicateType(contains, Table.class, defaultDescription);
      }
      case "NCONTAINS/K":
      case "NCONTAINS": {
        final String expected = predExpected;
        Predicate<Table> nContains = t -> !t.containsKey(expected);
        return new PredicateType(nContains, Table.class, defaultDescription);
      }
      // TIME
      case "MAXDTIME2CUR": {
        final Double expected = Double.valueOf(predExpected);
        Predicate<Double> time2Cur = d -> Time.currentTime().diff(new Time(d)) < expected;
        return new PredicateType(time2Cur, Double.TYPE, "d -> Time.currentTime().diff(new Time(d)) < expected");
      }
      case "MAXTIME2CUR": {
        final Double expected = Double.valueOf(predExpected);
        Predicate<Time> time2Cur = t -> Time.currentTime().diff(t) < expected;
        return new PredicateType(time2Cur, Time.class, "t -> Time.currentTime().diff(t) < expected");
      }
      case "MAXSTDTIME2CUR": {
        final Double expected = Double.valueOf(predExpected);
        Predicate<String> time2Cur = s -> Time.currentTime().diff(new Time(s,Time.STD)) < expected;
        return new PredicateType(time2Cur, String.class, "s -> Time.currentTime().diff(new Time(s,Time.STD)) < expected");
      }
      case "MAXISOTIME2CUR": {
        final Double expected = Double.valueOf(predExpected);
        Predicate<String> time2Cur = s -> Time.currentTime().diff(new Time(s,Time.ISO8601)) < expected;
        return new PredicateType(time2Cur, String.class, "s -> Time.currentTime().diff(new Time(s,Time.ISO8601)) < expected");
      }
      default: {
        LogUtilities.warning("PredicateType predicate creator with expected value did not find :"+predTest);
        return null;
      }
    }
  }

  private static PredicateType predicateTypeCreator(Double min, Double max)
  {
    Predicate<Double> pred = d -> ((d.doubleValue() < max) && (d.doubleValue() > min));
    return new PredicateType(pred, Double.TYPE, "d -> (d.doubleValue() < max) && (d.doubleValue() > min");
  }

  /** Check if the supplied test (actual) value passes the test associated with
   *  the key
   *
   *  @param mapPT   map containing keys with their associated test (Predicate)
   *  @param key     the key
   *  @param testVal the test (actual) value
   *  @return true if the supplied value passes the test associated with this key
   */
  @InternalUseOnly
  public static boolean passesFilter (Map<String,PredicateType> mapPT, String key, Object testVal) {
    PredicateType pt = mapPT.get(key);
    return passesFilter(pt, testVal);
  }

  /** Check if the test value passes the Predicate's test
   *
   *  @param pt      PredicateType (contains Predicate and the Predicate's Type)
   *  @param testVal value to run through the test
   *  @return true if the value passes the test
   */
  public static boolean passesFilter (PredicateType pt, Object testVal) {
    if (extendedDebug) System.out.println("passesFilter Predicate:"+pt+" testVal:"+testVal);
    Type predType = pt.getType();
    Class<?> predClass = pt.getTypeClass();
    try {
      if (!(testVal instanceof Double) && ((predType == Double.TYPE) || (predType == Double.class))) {
        Double testValDouble = null;
        try {
          testValDouble = Convert.o2d(testVal,null,Double.NaN);
        }
        catch (MidasException e){
          return false;
        }
        return passesFilter(pt, predType, testValDouble);
      }
      else if (!(testVal instanceof String) && (predType == String.class)) {
        String testValString = Convert.o2s(testVal,null,null);
        if (extendedDebug) System.out.println("converting testVal:"+testVal +" to:"+testValString);
        if (testValString == null) return false;
        return passesFilter(pt, predType, testValString);
      }
      else if (!(testVal instanceof Table) && (predType == Table.class)) {
        Table testValTable = null;
        try {
          testValTable = Convert.o2tStrict(testVal);
        }
        catch (MidasException e) {
          return false;
        }
        if (testValTable == null) return false;
        return passesFilter(pt, predType, testValTable);
      }
      else if ((testVal instanceof Number) && (predType == Time.class)) {
        Time testTime = new Time(Double.valueOf(testVal.toString()));
        return passesFilter(pt, predType, testTime);
      }
      else if (testVal == null) {
        if (predType == Optional.class) {
          Optional<?> nullOptional = Optional.empty();
          return passesFilter(pt, predType, nullOptional);
        }
        else return false;
      }
      else if ((testVal.getClass() == predClass) &&
                !((predClass!=null) && predClass.isAssignableFrom(testVal.getClass()))) {  /// look into null case
        if (debug) System.out.println("CLASS_NOT_MATCH: classes not match testVal:"+testVal+" testVal.class:"+testVal.getClass() +" predType"+predType+" predClass:"+predClass+" pred:"+pt);
        return false;
      }
      else {
        return passesFilter(pt, predType, testVal);
      }
    }
    catch (Exception e) {
      Shell.warning("PredicateType Filter Check threw exception for testVal:"+testVal+" with PredicateType:"+pt+" exception:"+e);
      return false;
    }
  }

  private static <T> boolean passesFilter (PredicateType pt, T type, T testVal) {
    Predicate<T> pred = (Predicate<T>)pt.getPredicate();
    if (debug) System.out.println("testVal class:"+((testVal!=null) ? testVal.getClass() : null)+" type:"+type+" pred:"+pred);
    return pred.test(testVal);
  }

  /** Get an empty/starter Map that will hold String keys and
   *  PredicateType values
   *
   *  @return new Map
   */
  @InternalUseOnly
  public static Map<String,PredicateType> getNewPredicateTypeMap () {
    return new HashMap<>();
  }

  public String toString () {
    if (lambdaString.isEmpty()) return "PredicateType={Type="+type+",Predicate="+predicate+"}";
    else return "PredicateType={Type="+type+", lambdaString=\""+lambdaString+"\"}";
  }

}
