package basics.profiler;

import java.awt.Toolkit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;
import javax.swing.table.DefaultTableModel;
import basics.unexpected.Failure;
import basics.utl.Empty;
import basics.utl.FormatterUtl;
import basics.utl.NumUtl;
import basics.utl.SUtl;
import basics.utl.SysUtl;

/**
 * @author ks
 */
public class Profiler
{
public static final String []          COLDEF                      = new String [] { "id", "estT",
   "toT", "O", "OT", "dif", "Xmb"                                 };
private static final Profiler          singleton                   = new Profiler();
private Vector<InspectAlertListener>   alertListeners              = new Vector<InspectAlertListener>();
private Vector<InspectReportListener>  stateListeners              = new Vector<InspectReportListener>();
private long                           initialtime                 = 0;
static Thread                          watchdog                    = null;
static volatile boolean                on                          = true;
static final long                      REPORTEVERY                 = 2;
static final long                      INITIALSLEEPTIME            = 1500;
static long                            sleeptime                   = INITIALSLEEPTIME;
public static long                     RUNTIME_REPORTING_THRESHOLD = 10;
static long                            counter                     = 0;
private Map<String, Profiler.TimeInfo> map                         = null;

private Profiler()
{
   initialtime = System.currentTimeMillis();
   map = Collections.synchronizedMap(new HashMap<String, Profiler.TimeInfo>());
   start();
}

public static void shutdown()
{
// SysUtl.trace("Shutdown % (runinng? %)", watchdog, on);
   on = false;
   try{
      watchdog.stop();
   }
   catch(Exception e){}
}

public static void addAlertListener(InspectAlertListener k)
{
   Profiler.singleton.alertListeners.add(k);
}

public static void addStateListener(InspectReportListener k)
{
   Profiler.singleton.stateListeners.add(k);
}

public static String uptime()
{
   long time = System.currentTimeMillis() - Profiler.singleton.initialtime;
   if(time < 1000) return time + "ms";
   time /= 1000;
   if(time < 60) return time + "s";
   double d = time / 60;
   if(d < 60) return NumUtl.round(d, 2) + "m";
   d /= 60;
   return NumUtl.round(d, 2) + "h";
}

public void start()
{
   Runnable m = new Runnable()
   {
      public void run()
      {
         while(on)
         {
            Set<String> keys = map.keySet();
            //            for (Iterator<String> I = map.keySet().iterator(); I.hasNext();)
            for(String id : keys)
            {
               //               String id = I.next();
               Profiler.TimeInfo ti = map.get(id);
               if(ti.running)
               {
                  long ub = MemoryInfo.getUsedByte();
                  if(ti.maxmem < ub) ti.maxmem = ub;
                  if(ti.runningOverLimitAndNotAlerted())
                  {
                     alert(id, ti.limit);
                     ti.alerted = true;
                  }
               }
            }
            if(counter % REPORTEVERY == 0)
            {
               if(stateListeners.size() == 0)
               {
                  String r = Profiler.report();
                  if(!Empty.is(SUtl.xtrim(r)))
                  {
                     SysUtl.println("------------------------------------");
                     SysUtl.println(r);
                     SysUtl.println("------------------------------------");
                  }
               }
               else stateMessage();
               if(!on) return;
            }
            counter++;
            try
            {
               Thread.sleep(sleeptime);
            }
            catch(InterruptedException e)
            {
            }
         }
      }
   };
   watchdog = new Thread(m);
   watchdog.setDaemon(true);
   watchdog.start();
}

private void alert(String id, double limit)
{
   final String msg = "PROFILER: MORE THAN " + limit + " MB USED: " + id;
   if(alertListeners.size() == 0)
   {
      SysUtl.println(msg);
      Toolkit.getDefaultToolkit().beep();
   }
   else for(Iterator<InspectAlertListener> iter = alertListeners.iterator(); iter.hasNext();)
      iter.next().memoryAlert(msg);
}

private synchronized void stateMessage()
{
   DefaultTableModel t = reportAsTable(RUNTIME_REPORTING_THRESHOLD);
   for(Iterator<InspectReportListener> iter = stateListeners.iterator(); iter.hasNext();)
      iter.next().profilerStateEvent(t);
}

public static void start(String id, long limit)
{
   if(NoWatch.contains(id)) throw new Failure(id + " is in nowatch list.");
   Profiler.TimeInfo ti = new Profiler.TimeInfo(id);
   ti.limit = limit;
   ti.startTimeZone();
   singleton.map.put(id, ti);
}

public static void start(String id)
{
   if(NoWatch.contains(id)) return;
   Profiler.TimeInfo ti = new Profiler.TimeInfo(id);
   ti.startTimeZone();
   ti.counter = 0;
   singleton.map.put(id, ti);
}

public static void resume(String id)
{
   if(NoWatch.contains(id)) return;
   if(singleton.map.containsKey(id))
   {
      if(((TimeInfo)singleton.map.get(id)).running) return;
      ((TimeInfo)singleton.map.get(id)).startTimeZone();
   }
   else
   {
      start(id);
   }
}

public static void resume(String id, long limit)
{
   if(NoWatch.contains(id)) return;
   if(singleton.map.containsKey(id))
   {
      if(((TimeInfo)singleton.map.get(id)).running) return;
      ((TimeInfo)singleton.map.get(id)).limit = limit;
      ((TimeInfo)singleton.map.get(id)).startTimeZone();
   }
   else
   {
      Profiler.TimeInfo ti = new Profiler.TimeInfo(id);
      ti.limit = limit;
      ti.startTimeZone();
      singleton.map.put(id, ti);
   }
}

public static void stopAll()
{
   for(String id : singleton.map.keySet())
   {
      stop(id);
   }
}

public static void reset()
{
   singleton.map.clear();
}

public static void reset(String id)
{
   singleton.map.remove(id);
}

public static void resumeAll()
{
   for(String id : singleton.map.keySet())
      resume(id);
}

public static long stopAndSetCounter(String id, long c)
{
   if(singleton.map.containsKey(id))
   {
      Profiler.TimeInfo ti = ((TimeInfo)singleton.map.get(id));
      ti.stopTimeZone();
      ti.counter = c;
      singleton.map.put(id, ti);
      return ti.runtime;
   }
   else return 0;
}

public static long setCounter(String id, long c)
{
   if(singleton.map.containsKey(id))
   {
      Profiler.TimeInfo ti = ((TimeInfo)singleton.map.get(id));
      ti.counter = c;
      singleton.map.put(id, ti);
      return ti.runtime;
   }
   else return 0;
}

public static synchronized void increment(String id, long c)
{
   if(NoWatch.contains(id)) return;
   if(!singleton.map.containsKey(id))
   {
      Profiler.TimeInfo ti = new Profiler.TimeInfo(id);
      ti.startTimeZone();
      ti.stopTimeZone();
      ti.counter = c;
      singleton.map.put(id, ti);
   }
   else
   {
      Profiler.TimeInfo ti = singleton.map.get(id);
      ti.counter += c;
      singleton.map.put(id, ti);
   }
}

public static long targetsize(String id)
{
   if(!singleton.map.containsKey(id)) return 0;
   Profiler.TimeInfo ti = singleton.map.get(id);
   return ti.targetsize;
}

public static void targetsize(String id, long c)
{
   if(NoWatch.contains(id)) return;
   if(!singleton.map.containsKey(id))
   {
      Profiler.TimeInfo ti = new Profiler.TimeInfo(id);
      ti.startTimeZone();
      ti.stopTimeZone();
      ti.targetsize = c;
      singleton.map.put(id, ti);
   }
   else
   {
      Profiler.TimeInfo ti = singleton.map.get(id);
      ti.targetsize = c;
      singleton.map.put(id, ti);
   }
}

public static void increment(String id)
{
   increment(id, 1L);
}

public static boolean counterModuloIsNullFor(String id, long m)
{
   if(NoWatch.contains(id)) return false;
   Profiler.TimeInfo ti = singleton.map.containsKey(id) ? singleton.map.get(id)
      : new Profiler.TimeInfo(id);
   return ti.counter % m == 0;
}

public static long counter(String id)
{
   if(!NoWatch.contains(id)) return -1;
   Profiler.TimeInfo ti = singleton.map.get(id);
   return ti.counter;
}

public static long stop(String id)
{
   if(NoWatch.contains(id)) return 0;
   if(singleton.map.containsKey(id))
   {
      Profiler.TimeInfo ti = ((TimeInfo)singleton.map.get(id));
      ti.stopTimeZone();
      if(targetsize(id) < 1) targetsize(id, counter(id));
      singleton.map.put(id, ti);
      return ti.runtime;
   }
   else return 0;
}

public static long currentMillis(String id)
{
   if(!singleton.map.containsKey(id)) start(id);
   return ((TimeInfo)singleton.map.get(id)).currenttime();
}

public static String report()
{
   return singleton.toString();
}

public static String report(String id)
{
   TimeInfo ti = (TimeInfo)singleton.map.get(id);
   if(ti == null)
   {
      if(NoWatch.contains(id)) return id + ": in nowatch list.";
      else return id + ": no profiling info";
   }
   boolean restart = ti.running;
   if(restart) stop(id);
   ti.stopTimeZone();
   if(restart) resume(id);
   StringBuilder s = new StringBuilder();
   s.append(id);
   s.append(": ");
   s.append(ti.toString());
   s.append(" n: ");
   s.append(ti.counter);
   s.append("/");
   s.append(ti.targetsize);
   s.append(" up: ");
   s.append(uptime());
   return s.toString();
}

public static String reportEstimatedTime(String id)
{
   if(NoWatch.contains(id)) return " -nowatch-";
   double e = estimateTime(id);
   if(e == 0) return " done!";
   if(e == -1) return " (counter?)";
   if(e == -2) return " (size?)";
   if(e < 10000) return NumUtl.round(e, 0) + "ms";
   if(e < 600000) return NumUtl.round(e / 1000, 2) + "s";
   return NumUtl.round(e / 60000, 2) + "m";
}

public static double estimateTime(String id)
{
   TimeInfo ti = (TimeInfo)singleton.map.get(id);
   return ti.estimatedTime();
}

public static String reportOT(String id)
{
   if(NoWatch.contains(id)) return "";
   TimeInfo ti = (TimeInfo)singleton.map.get(id);
   boolean restart = ti.running;
   if(restart) stop(id);
   ti.stopTimeZone();
   if(restart) resume(id);
   if(ti != null) return ti.operationTime();
   else return id + " [OT: no info]";
}

public static void remove(String id)
{
   if(singleton.map.containsKey(id)) singleton.map.remove(id);
}

@SuppressWarnings("unchecked")
public String toString()
{
   if(map == null || map.size() == 0) return "";
   StringBuilder sb = new StringBuilder();
   DefaultTableModel tm = reportAsTable(RUNTIME_REPORTING_THRESHOLD);
   int rows = tm.getRowCount();
   for(int i = 0; i < rows; i++)
   {
      for(int c = 0; c < COLDEF.length; c++)
      {
         Object v = null;
         try
         {
            v = tm.getValueAt(i, c);
         }
         catch(Exception e)
         {
            SysUtl.trace(i + "/" + rows + ", " + c + "/" + COLDEF.length);
            e.printStackTrace();
         }
         if(c == 0) sb.append(FormatterUtl.padr(SUtl.prune(v.toString(), 30), " ", 30));
         else sb.append(FormatterUtl.padl(v.toString(), " ", 12));
         if(c < COLDEF.length - 1) sb.append("  ");
      }
      if(i < rows - 1) sb.append("\n");
   }
   return sb.toString();
}

@SuppressWarnings("unchecked")
public DefaultTableModel reportAsTable(long runtimeThreshold)
{
   DefaultTableModel tableModel = new DefaultTableModel();
   tableModel.setColumnIdentifiers(COLDEF);
   List<TimeInfo> tm = new ArrayList<TimeInfo>();
   synchronized(map)
   {
      for(String id : map.keySet())
      {
         tm.add((TimeInfo)map.get(id));
      }
      Collections.sort(tm, new Comparator()
      {
         public int compare(Object o1, Object o2)
         {
            TimeInfo r1 = (TimeInfo)o1;
            TimeInfo r2 = (TimeInfo)o2;
            Long d1 = r1.runtime;
            Long d2 = r2.runtime;
            return d2.compareTo(d1);
         }
      });
   }
   if(tm.size() == 0)
   {
      Object [] obj = new Object [COLDEF.length];
      for(int i = 0; i < obj.length; i++)
         obj[i] = "-";
      tableModel.addRow(obj);
   }
   else
   {
      for(int i = 0; i < tm.size(); i++)
      {
         TimeInfo ti = (TimeInfo)tm.get(i);
         boolean restart = ti.running;
         boolean C1 = ti.counter < 2;
         if(restart) ti.stopTimeZone();
         else if(ti.runtime < runtimeThreshold) continue;
         Object [] obj = new Object [] {
            ti.id,
            verboseET(ti.estimatedTime()),
            FormatterUtl.formatMillisPadLeft(ti.runtime, 6),
            FormatterUtl.shortenNumber(ti.counter),
            C1 ? "" : ti.OT() < 1000 ? ti.OT() + "ms" : FormatterUtl.formatMillisPadLeft((long)ti
               .OT(), 6), C1 ? "" : NumUtl.round(ti.increase, 2),
            FormatterUtl.formatBytesPadLeft(ti.maxmem, 6) };
         tableModel.addRow(obj);
         if(restart) ti.startTimeZone();
      }
   }
   return tableModel;
}

private static String verboseET(double t)
{
   if(t > 0) return FormatterUtl.formatMillisPadLeft((long)t, 0);
   else
   {
      if(t == -1) return "...";
      if(t == -2) return ""; //"size?";
      return "";
   }
}

public static class TimeInfo
{
String  id;
boolean running       = false;
double  increase      = 0;
double  lastperiod    = 0;
long    runtime       = 0;
long    targetsize    = 1;
long    lastmilestone = 0;
long    counter       = 0;
long    limit         = 0;
long    maxmem        = 0;
boolean alerted       = false;

public TimeInfo(String id)
{
   this.id = id;
   lastperiod = 0;
   runtime = 0;
}

public String getId()
{
   return id;
}

public void startTimeZone()
{
   lastmilestone = System.currentTimeMillis();
   running = true;
}

public boolean runningOverLimitAndNotAlerted()
{
   if(alerted || limit <= 0) return false;
   return(currenttime() > limit);
}

public long stopTimeZone()
{
   if(!running) return runtime;
   running = false;
   long period = System.currentTimeMillis() - lastmilestone;
   runtime += period;
   lastperiod = lastperiod == 0 ? period : lastperiod;
   if(period == lastperiod) increase = 0;
   else
   {
      increase = (double)period - lastperiod;
      lastperiod = (double)period;
   }
   return runtime;
}

public long currenttime()
{
   if(!running) return runtime;
   else return runtime + (System.currentTimeMillis() - lastmilestone);
}

public long maxMem()
{
   return maxmem;
}

public static String report(String id)
{
   TimeInfo ti = (TimeInfo)singleton.map.get(id);
   if(ti != null) return id + ": " + ti.toString();
   else return id + ": no info";
}

public String toString()
{
   return toString(false);
}

public String toString(boolean runnning)
{
   StringBuilder sb = new StringBuilder();
   sb.append(FormatterUtl.formatMillisPadLeft(runtime, 0));
   if(counter != 0)
   {
      double d = runtime / ((double)counter);
      sb.append(" O:");
      sb.append(counter);
      sb.append(" OT:");
      sb.append(FormatterUtl.formatMillisPadLeft((long)d, 0));
      double relIncrease = 1;
      try
      {
         relIncrease = increase / counter;
         if(relIncrease != 0)
         {
            sb.append(" (");
            if(relIncrease > 0) sb.append("++");
            else sb.append("-");
            sb.append(FormatterUtl.format(relIncrease, 1, 2));
            sb.append(")");
         }
      }
      catch(Exception e)
      {
      }
      sb.append(" ");
      sb.append(FormatterUtl.formatBytesPadLeft(maxMem(), 0));
   }
   return sb.toString();
}

public double quotaDone()
{
   if(counter == 0 || counter > targetsize) return 0;
   return (double)counter / (double)targetsize;
}

public double estimatedTime()
{
   if(counter == 0) return -1;
   if(counter > targetsize)
   {
      return -2;
   }
   double x = (double)targetsize / (double)counter;
   double ct = currenttime();
   double r = (x * ct) - ct;
   return r;
}

public String operationTime()
{
   StringBuilder sb = new StringBuilder();
   sb.append("[");
   if(counter == 0)
   {
      double d = runtime;
      sb.append(NumUtl.round(d, 3));
      sb.append("ms / 0");
      if(limit > 0 && runtime > limit)
      {
         sb.append(" OVER ");
         sb.append(limit);
      }
      sb.append("]");
   }
   else
   {
      double d = runtime / ((double)counter);
      sb.append(counter);
      sb.append(" x ");
      sb.append(NumUtl.round(d, 3));
      sb.append(" ms ");
      if(limit > 0 && runtime > limit)
      {
         sb.append(" OVER ");
         sb.append(limit);
      }
      sb.append("]");
   }
   return sb.toString();
}

public double OT()
{
   if(counter == 0) return 0;
   double d = runtime / ((double)counter);
   return NumUtl.round(d, 4);
}
}

/**
 */
@SuppressWarnings("unused")
public static void main(String [] args)
{
   for(int i = 0; i < 5; i++)
   {
      Profiler.resume("eins");
      long j = 0;
      while(j++ < 1000000)
         Math.sqrt(16);
      Profiler.stop("eins");
      Profiler.increment("eins", 1);
      Profiler.resume("zwei");
      j = 0;
      while(j++ < 1000000)
         Math.sqrt(32);
      long x = Profiler.currentMillis("eins");
      Profiler.stopAndSetCounter("zwei", (j - 1));
      System.out.println(Profiler.report("eins"));
   }
   System.out.println(Profiler.report("eins"));
   System.out.println(Profiler.report("zwei"));
}
}
