
package edu.stanford.nlp.util.logging;

import java.util.Collections;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import edu.stanford.nlp.util.logging.Redwood.RedwoodChannels;
import edu.stanford.nlp.util.Generics;

/**
 * Primarily for debugging, PrettyLogger helps you dump various collection
 * objects in a reasonably structured way via Redwood logging. It has support
 * for many built in collection types (Mapping, Iterable, arrays, Properties) as
 * well as anything that implements PrettyLoggable.
 *
 * @see PrettyLoggable
 * @author David McClosky
 * @author Gabor Angeli (+ primitive arrays; dictionaries)
 */

// TODO Should have an optional maximum depth, perhaps

public class PrettyLogger {

  private static final RedwoodChannels DEFAULT_CHANNELS = new RedwoodChannels(Redwood.FORCE);

  /**
   * Static class.
   */
  private PrettyLogger() {}

  /*
   * Main entry methods and utilities
   */

  /**
   * Pretty log an object. It will be logged to the default channel. Its class
   * name will be used as a description.
   *
   * @param obj
   *          the object to be pretty logged
   */
  public static void log(Object obj) {
    log(obj.getClass().getSimpleName(), obj);
  }

  /**
   * Pretty log an object along with its description. It will be logged to the
   * default channel.
   *
   * @param description
   *          denote the object in the logs (via a track name, etc.).
   * @param obj
   *          the object to be pretty logged
   */
  public static void log(String description, Object obj) {
    log(DEFAULT_CHANNELS, description, obj);
  }

  /**
   * Pretty log an object. Its class name will be used as a description.
   *
   * @param channels
   *          the channels to pretty log to
   * @param obj
   *          the object to be pretty logged
   */
  public static void log(RedwoodChannels channels, Object obj) {
    log(channels, obj.getClass().getSimpleName(), obj);
  }

  /**
   * Pretty log an object.
   *
   * @param channels
   *          the channels to pretty log to
   * @param description
   *          denote the object in the logs (via a track name, etc.).
   * @param obj
   *          the object to be pretty logged
   */
  // TODO perhaps some reflection magic can simplify this process?
  @SuppressWarnings("unchecked")
  public static <T> void log(RedwoodChannels channels, String description, Object obj) {
    if (obj instanceof Map) {
      log(channels, description, (Map)obj);
    } else if (obj instanceof PrettyLoggable) {
      ((PrettyLoggable) obj).prettyLog(channels, description);
    } else if (obj instanceof Dictionary) {
      log(channels, description, (Dictionary)obj);
    } else if (obj instanceof Iterable) {
      log(channels, description, (Iterable)obj);
    } else if (obj.getClass().isArray()) {
      Object[] arrayCopy; // the array to log
      if(obj.getClass().getComponentType().isPrimitive()){
        //(case: a primitive array)
        Class componentClass = obj.getClass().getComponentType();
        if(boolean.class.isAssignableFrom(componentClass)){
          arrayCopy = new Object[((boolean[]) obj).length];
          for(int i=0; i<arrayCopy.length; i++){ arrayCopy[i] = ((boolean[]) obj)[i]; }
        } else if(byte.class.isAssignableFrom(componentClass)){
          arrayCopy = new Object[((byte[]) obj).length];
          for(int i=0; i<arrayCopy.length; i++){ arrayCopy[i] = ((byte[]) obj)[i]; }
        } else if(char.class.isAssignableFrom(componentClass)){
          arrayCopy = new Object[((char[]) obj).length];
          for(int i=0; i<arrayCopy.length; i++){ arrayCopy[i] = ((char[]) obj)[i]; }
        } else if(short.class.isAssignableFrom(componentClass)){
          arrayCopy = new Object[((short[]) obj).length];
          for(int i=0; i<arrayCopy.length; i++){ arrayCopy[i] = ((short[]) obj)[i]; }
        } else if(int.class.isAssignableFrom(componentClass)){
          arrayCopy = new Object[((int[]) obj).length];
          for(int i=0; i<arrayCopy.length; i++){ arrayCopy[i] = ((int[]) obj)[i]; }
        } else if(long.class.isAssignableFrom(componentClass)){
          arrayCopy = new Object[((long[]) obj).length];
          for(int i=0; i<arrayCopy.length; i++){ arrayCopy[i] = ((long[]) obj)[i]; }
        } else if(float.class.isAssignableFrom(componentClass)){
          arrayCopy = new Object[((float[]) obj).length];
          for(int i=0; i<arrayCopy.length; i++){ arrayCopy[i] = ((float[]) obj)[i]; }
        } else if(double.class.isAssignableFrom(componentClass)){
          arrayCopy = new Object[((double[]) obj).length];
          for(int i=0; i<arrayCopy.length; i++){ arrayCopy[i] = ((double[]) obj)[i]; }
        } else {
          throw new IllegalStateException("I forgot about the primitive class: " + componentClass);
        }
      } else {
        //(case: a regular array)
        arrayCopy = (T[]) obj;
      }
      log(channels, description, arrayCopy);
    } else {
      if (!description.equals("")) {
        description += ": ";
      }
      channels.log(description + obj);
    }
  }

  /**
   * Returns true if an object has special logic for pretty logging (e.g.
   * implements PrettyLoggable). If so, we ask it to pretty log itself. If not,
   * we can safely use its toString() in logs.
   * @param obj The object to test
   * @return true if the object is dispatchable
   */
  @SuppressWarnings("unchecked")
  public static boolean dispatchable(Object obj) {
    if (obj == null) {
      return false;
    }
    return obj instanceof PrettyLoggable ||
      obj instanceof Map ||
      obj instanceof Dictionary ||
      obj instanceof Iterable ||
      obj.getClass().isArray();
  }

  /*
   * Mappings
   */

  private static <K, V> void log(RedwoodChannels channels, String description, Map<K, V> mapping) {
    Redwood.startTrack(description);
    if (mapping == null) {
      channels.log("(mapping is null)");
    } else if (mapping.isEmpty()) {
      channels.log("(empty)");
    } else {
      // convert keys to sorted list, if possible
      List<K> keys = new LinkedList<>();
      for (K key : mapping.keySet()) {
        keys.add(key);
      }
      Collections.sort(keys, (a, b) -> {
        if (a != null && Comparable.class.isAssignableFrom(a.getClass())) {
          return ((Comparable) a).compareTo(b);
        } else {
          return 0;
        }
      });
      // log key/value pairs
      int entryCounter = 0;
      for (K key : keys) {
        V value = mapping.get(key);
        if (!dispatchable(key) && dispatchable(value)) {
          log(channels, key.toString(), value);
        } else if (dispatchable(key) || dispatchable(value)) {
          Redwood.startTrack("Entry " + entryCounter);
          if (dispatchable(key)) {
            log(channels, "Key", key);
          } else {
            channels.logf("Key %s", key);
          }

          if (dispatchable(value)) {
            log(channels, "Value", value);
          } else {
            channels.logf("Value %s", value);
          }
          Redwood.endTrack("Entry " + entryCounter);
        } else {
          channels.logf("%s = %s", key, value);
        }
        entryCounter++;
      }
    }
    Redwood.endTrack(description);
  }

  /*
   * Dictionaries (notably, Properties) -- convert them to Maps and dispatch
   */

  private static <K,V> void log(RedwoodChannels channels, String description, Dictionary<K,V> dict) {
    //(a real data structure)
    Map<K, V> map = Generics.newHashMap();
    //(copy to map)
    Enumeration<K> keys = dict.keys();
    while(keys.hasMoreElements()){
      K key = keys.nextElement();
      V value = dict.get(key);
      map.put(key,value);
    }
    //(log like normal)
    log(channels, description, map);
  }

  /*
   * Iterables (includes Collection, List, Set, etc.)
   */

  private static <T> void log(RedwoodChannels channels, String description, Iterable<T> iterable) {
    Redwood.startTrack(description);
    if (iterable == null) {
      channels.log("(iterable is null)");
    } else {
      int index = 0;
      for (T item : iterable) {
        if (dispatchable(item) && item != iterable) {
          log(channels, "Index " + index, item);
        } else {
          channels.logf("Index %d: %s", index, item == iterable ? "...<infinite loop>" : item);
        }
        index++;
      }

      if (index == 0) {
        channels.log("(empty)");
      }
    }
    Redwood.endTrack(description);
  }

  /*
   * Arrays
   */

  private static <T> void log(RedwoodChannels channels, String description, T[] array) {
    Redwood.startTrack(description);
    if (array == null) {
      channels.log("(array is null)");
    } else if (array.length == 0) {
      channels.log("(empty)");
    } else {
      int index = 0;
      for (T item : array) {
        if (dispatchable(item)) {
          log(channels, "Index " + index, item);
        } else {
          channels.logf("Index %d: %s", index, item);
        }
        index++;
      }
    }
    Redwood.endTrack(description);
  }

}
