package nxm.sys.inc;

import static nxm.sys.inc.MapKBT.newMap;

import java.io.Serializable;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import java.util.WeakHashMap;

import nxm.sys.lib.Table;

/** Utilities to support {@link KeyValuePair}.
 *  @author  J. Czechowski
 *  @since   NeXtMidas 3.9.0
 */
@InternalUseOnly("This should only be used by KeyValuePair.")
final class KeyValuePairSupport implements KeyValuePairSwapBCEL {

  // Apache commons BCEL Classes and methods needed to find lambda argument names
  private static final boolean FALLBACK = false; // Fallback when BCEL fails?

  public static void main (String... argv) {
    System.out.println(">>> "+toMapEntry(ABC->123));
  }

  // Implementation Note:
  //   Since toMap(..) and toTable(..) represent the 90% use-case it may be tempting to
  //   bypass the creation of the Map.Entry used in the cache. This is intentionally
  //   avoided since the cached values hold the results of our *first* (and *our* only)
  //   call to the KeyValuePair.value(..) method thereby ensuring consistent results
  //   even if the user has their KeyValuePair point to a non-constant value (despite
  //   us imploring them not to).
  //
  //   For example, with the current implementation:
  //     KeyValuePair p = KEY->random();
  //
  //     System.out.println(toMap(p));  // prints {KEY=7}
  //     System.out.println(toMap(p));  // prints {KEY=7}
  //     System.out.println(toMap(p));  // prints {KEY=7}
  //
  //   If we didn't cache things, we would get:
  //     KeyValuePair p = KEY->random();
  //
  //     System.out.println(toMap(p));  // prints {KEY=7}
  //     System.out.println(toMap(p));  // prints {KEY=2}
  //     System.out.println(toMap(p));  // prints {KEY=4}

  private KeyValuePairSupport () { } // prevent instantiation

  private static final WeakHashMap<KeyValuePair<?>,Entry<String,?>> cache = new WeakHashMap<>(128);

  /** Creates an {@link Entry} for a {@link KeyValuePair}.
   *  @param <V> The value type.
   *  @param p   The {@link KeyValuePair} (not null).
   *  @return The corresponding {@link Entry}.
   */
  @SuppressWarnings("unchecked")
  static <V> Entry<String,V> toMapEntry (KeyValuePair<V> p) {
    Entry<String,?> e = cache.get(p);
    if (e != null) return (Entry<String,V>)e;

    String[] params = getLambdaParameterNames(p);
    String   key    = params[params.length-1];

    synchronized (cache) {
      e = cache.get(p);
      if (e != null) return (Entry<String,V>)e;
      V val = p.value(null);
      e = new SimpleImmutableEntry<>(key, val);
      cache.put(p, e);
      return (Entry<String,V>)e;
    }
  }

  /** Builds a map out of an array of {@link KeyValuePair}.
   *  @param kvPairs The array of {@link KeyValuePair}.
   *  @return The map.
   */
  @SuppressWarnings("unchecked")
  static MapKBT toMap (KeyValuePair<?>... kvPairs) {
    // The array and Iterable versions of this method should be identical below this point...
    MapKBT map = newMap();

    for (KeyValuePair<?> p : kvPairs) {
      Entry<String,?> e = p.toMapEntry();
      map.put(e.getKey(), e.getValue());
    }
    return map;
  }

  /** Builds a map out of an collection of {@link KeyValuePair}.
   *  @param kvPairs The collection of {@link KeyValuePair}.
   *  @return The map.
   */
  @SuppressWarnings("unchecked")
  static MapKBT toMap (Iterable<KeyValuePair<?>> kvPairs) {
    // The array and Iterable versions of this method should be identical below this point...
    MapKBT map = newMap();

    for (KeyValuePair<?> p : kvPairs) {
      Entry<String,?> e = p.toMapEntry();
      map.put(e.getKey(), e.getValue());
    }
    return map;
  }

  /** Builds a table out of an array of {@link KeyValuePair}. Unlike {@link #toMap}
   *  this will handle table-specific features such as converting the keys to
   *  upper-case.
   *  @param kvPairs The array of {@link KeyValuePair}.
   *  @return The table.
   */
  @SuppressWarnings("unchecked")
  static Table toTable (KeyValuePair<?>... kvPairs) {
    // The array and Iterable versions of this method should be identical below this point...
    Table tbl = new Table();

    for (KeyValuePair<?> p : kvPairs) {
      Entry<String,?> e = p.toMapEntry();
      tbl.put(e.getKey().toUpperCase(), e.getValue());
    }
    return tbl;
  }

  /** Builds a table out of an collection of {@link KeyValuePair}. Unlike {@link #toMap}
   *  this will handle table-specific features such as converting the keys to
   *  upper-case.
   *  @param kvPairs The collection of {@link KeyValuePair}.
   *  @return The table.
   */
  @SuppressWarnings("unchecked")
  static Table toTable (Iterable<KeyValuePair<?>> kvPairs) {
    // The array and Iterable versions of this method should be identical below this point...
    Table tbl = new Table();

    for (KeyValuePair<?> p : kvPairs) {
      Entry<String,?> e = p.toMapEntry();
      tbl.put(e.getKey().toUpperCase(), e.getValue());
    }
    return tbl;
  }

  private static String[] findLambdaParams (Method m, byte[] classAsBytes) {
    String methodName = m.getName();
    byte[] methodNameBytes;
    try {
      methodNameBytes = methodName.getBytes("UTF8");
    }
    catch (Exception e) {
      System.out.println("Exception ......:"+e);
      return null;
    }
    int argsLoc = findLocation(classAsBytes, methodNameBytes);
    if (argsLoc==-1) return null;
    int charToPrint = argsLoc;
    do {
      byte thisByte = classAsBytes[charToPrint++];
      System.out.println("arg byte:"+thisByte);
      if (thisByte > 32 && thisByte < 126) System.out.println("arg char:"+((char)thisByte));
    } while (charToPrint < classAsBytes.length && charToPrint < (argsLoc +80) );
    int stackTableLoc = findLocation(classAsBytes, "StackMapTable".getBytes());
    if (stackTableLoc==-1) return null;
    charToPrint = stackTableLoc;
    do {
      byte thisByte = classAsBytes[charToPrint++];
      if (thisByte > 32 && thisByte < 126) System.out.println("arg char:"+((char)thisByte));
    } while (charToPrint < classAsBytes.length && charToPrint < (stackTableLoc +80) );
    return null;
  }

  private static int findLocation (byte[] in, byte[] find) {
    boolean done = false;
    for (int i=0; i < in.length; i++) {
      if (in[i] == find[0]) {
        for (int ii = 1; ii < find.length; ii++) {
          if (in[i+ii] != find[ii]) break;
          else if (ii==find.length-1) done = true;
        }
        if (done) return i-1;
      }
    }
    return -1;
  }

  /** Gets the parameters to the lambda expression. Note that the interface implemented
   *  by the lambda expression must be serializable.
   *  @param obj The object instance created by the lambda expression.
   *  @return The parameters or null if not able to be find..
   */
  private static String[] getLambdaParameterNames (KeyValuePair<?> obj) {
    // This is partially based on the example provided by:
    //   http://benjiweber.co.uk/blog/2015/08/17/lambda-parameter-names-with-reflection/

    if (!(obj instanceof Serializable)) {
      // This is an AssertionError rather than RuntimeException since it means that
      // someone took the Serialized off of KeyValuePair.
      throw new AssertionError("Object not serializable: "+obj);
    }
    try {
      Method wr = obj.getClass().getDeclaredMethod("writeReplace");
      wr.setAccessible(true);
      SerializedLambda sl = (SerializedLambda)wr.invoke(obj);

      String         sig   = sl.getImplMethodSignature();
      int            end   = sig.lastIndexOf(')');
      List<Class<?>> types = new ArrayList<>(8);
      int            i     = 1;
      int            semi;
      String         clazz;

      while (i < end) {
        char t = sig.charAt(i++);

        switch (t) {
          case 'B': types.add(Byte.TYPE     ); break;
          case 'S': types.add(Short.TYPE    ); break;
          case 'I': types.add(Integer.TYPE  ); break;
          case 'J': types.add(Long.TYPE     ); break;
          case 'F': types.add(Float.TYPE    ); break;
          case 'D': types.add(Double.TYPE   ); break;
          case 'C': types.add(Character.TYPE); break;
          case 'Z': types.add(Boolean.TYPE  ); break;
          case 'L':
            semi  = sig.indexOf(';',i);
            clazz = sig.substring(i,semi).replace('/', '.');
            types.add(Class.forName(clazz));
            i = semi+1;
            break;
          case '[':
            clazz = "[";
            t     = sig.charAt(i++);

            while (t == '[') { // multi-dimension array
              t = sig.charAt(i++);
              clazz += "[";
            }

            if (t == 'L') {
              semi = sig.indexOf(';',i);
              clazz += sig.substring(i,semi);
              types.add(Class.forName(clazz));
              i = semi+1;
            }
            else {
              clazz += t;
              types.add(Class.forName(clazz));
            }
            break;
          default:
            // This violates the Java VM spec
            throw new AssertionError("Invald type: "+t+" in "+sig);
        }
      }
      String      fullPathClassFile   = sl.getImplClass() + ".class";
      String      className = sl.getImplClass().replace('/','.');
      Class<?>[]  typeArray = types.toArray(new Class<?>[types.size()]);
      Class<?>    cls       = Class.forName(className);
      Method      m         = cls.getDeclaredMethod(sl.getImplMethodName(), typeArray);

      return KeyValuePairSwapBCEL.getParamNames(cls, m, className+".class", fullPathClassFile);
    }
    catch (ReflectiveOperationException e) {
      // This is an AssertionError rather than RuntimeException since it means that:
      //   1. Someone created a non-conforming implementation of KeyValuePair,
      //   2. Something has changed in the Java VM that impairs our access, or
      //   3. The class was not compiled with the proper flags.
      throw new AssertionError("Object not serializable: "+obj, e);
    }
  }

  /** Gets the parameter names using the basic reflection methods.
   *  @param c  The class.
   *  @param m  The method.
   *  @throws ReflectiveOperationException On error.
   *  @since NextMidas 3.9.3 for NXM-3560 JDK breakage
   */
  static String[] getParamNamesBasic (Class<?> c, Method m) throws ReflectiveOperationException {
    Parameter[] params = m.getParameters();
    String[]    names  = new String[params.length];
    for (int i = 0; i < params.length; i++) {
      names[i] = params[i].getName();
    }
    return names;
  }

}