package nxm.sys.prim;

import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.lang.management.MemoryUsage;
import java.lang.management.ThreadMXBean;

import nxm.sys.inc.MessageHandler;
import nxm.sys.inc.Units;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.IOResource;
import nxm.sys.lib.Midas;
import nxm.sys.lib.OS;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Primitive;
import nxm.sys.lib.Table;
import nxm.sys.lib.Time;

/** Reports various system statistics.

    @author Jeff Schoen
 */
public class system extends Primitive  {

  private DataFile df;
  private Data dfb;
  private int stats;
  private String func,label;
  private Table tab;
  private double pause,timeLast;
  private Parser auxes;
  private boolean list=true, all, top, monThreads, monMemory;

  /** list of all possible system stats */
  public static String statsList = "Load,Mem,Swap,Page,Cpu0,Cpu1,Cpu2,Cpu3,Disk";

  /** mask for system stat LOAD */ public static int LOAD =  0x1;
  /** mask for system stat MEM  */ public static int MEM  =  0x2;
  /** mask for system stat SWAP */ public static int SWAP =  0x4;
  /** mask for system stat PAGE */ public static int PAGE =  0x8;
  /** mask for system stat CPU0 */ public static int CPU0 = 0x10;
  /** mask for system stat CPU1 */ public static int CPU1 = 0x20;
  /** mask for system stat CPU2 */ public static int CPU2 = 0x40;
  /** mask for system stat CPU3 */ public static int CPU3 = 0x80;
  /** mask for system stat DISK */ public static int DISK = 0x100;

  private MemoryMXBean memoryMXBean; // The current MemoryMXBean
  private ThreadMXBean threadMXBean; // The current ThreadMXBean

  /** default constructor */
  public system () {}

  public int open () {

    // get the mask of stats
    func = MA.getU("FUNC");

    // get the loop delay
    pause = MA.getD("/PAUSE",1.0);

    if (func.equals("THREADS")) {
      showThreads(); // Added NeXtMidas 2.1.0
      return FINISH;
    }

    if (func.startsWith("MONITOR/")) {
           if (func.equals("MONITOR/MEMORY")) monMemory=true;
      else if (func.equals("MONITOR/THREADS")) monThreads=true;
      else M.error("Illegal MONITOR function: "+func);

      df = MA.getDataFile("FILE","3000","NH",0);
      df.setSize(1000);
      df.setFormat("SX");
      df.setXUnits(Units.TIME_S);
      df.setXDelta(pause);
      if (monMemory) {
        df.setYUnits(Units.UNKNOWN);
        df.addSubRec("USED","SX");
        df.addSubRec("COMM","SX");
        df.addSubRec("HEAP","SX");
        df.addSubRec("NONH","SX");
      } else {
        df.setYUnits(Units.COUNTS);
        df.addSubRec("CURR","SX");
        df.addSubRec("PEAK","SX");
        df.addSubRec("TOTL","SX");
      }
      df.open(BaseFile.OUTPUT);
      if (df.isOpen) dfb = df.getDataBuffer(1);

      if (monMemory)       memoryMXBean = ManagementFactory.getMemoryMXBean();
      else if (monThreads) threadMXBean = ManagementFactory.getThreadMXBean();
    }
    else if (func.equals("TOP")) {
      top = true;
    }
    else {
      stats = MA.getOptionMask("FUNC",statsList,0);

      // get the Output Table
      label = MA.getU("TABLE");
      tab = new Table();

      // get Output File
      df = MA.getDataFile("FILE","3000","NH",0);

      // get aux list
      if ((stats&DISK)!=0) {
        Table taux = M.results.getTable("AUX");
        String raux = taux.getString("READ");
        raux = MA.getS("/AUXLIST",raux);
        auxes = new Parser(raux,'|');
      }

      // set up fields
      for (int i=0; i<9; i++) {
        int mask = (0x1<<i);
        if ((mask&stats)==0) continue;
        if (mask==DISK) {
          for (auxes.reset(); auxes.more(); ) {
            String name = auxes.next();
            df.addSubRec(name,"SL");
          }
        }
        else {
          String name = Parser.get(statsList,i+1).toUpperCase();
          df.addSubRec(name,"SL");
        }
      }
      df.open(BaseFile.OUTPUT|BaseFile.OPTIONAL);
      if (df.isOpen) dfb = df.getDataBuffer(1);

      // list to screen
      list = (label.length()==0 && !df.isOpen) && (getMessageHandler() == null);
      all = MA.getState("/ALL",true);

      timeLast = 0;
      setPollTime(0.1);
    }

    return NORMAL;
  }

  public int process () {

    double time = Time.current();
    if (time-timeLast<pause) return NOOP;
    timeLast = time;

    if (monMemory) {
      writeMemoryUsageInfo();
    }
    else if (monThreads) {
      writeThreadInfo();
    }
    else if (top) {
      byte[] buf = new byte[8192];
      int bytes = OS.getStats(0,buf);
      if (bytes>0) M.type( new String(buf,0,bytes) );
      return FINISH;
    }
    else {
      int i=0;
      if ((stats&DISK)!=0) {
        for (auxes.reset(); auxes.more(); i++) {
          String aux = auxes.next();
          String path = M.io.getAuxPath(aux);
          Table stats = null;
          try {
            IOResource res = IOResource.getInstance(M,0,path,0,null);
            stats = res.getStatFS();
          } catch (Exception ex) {
            M.warning("Unable to get disk statistics for AUX="+aux+" at path="+path+ " cause="+ex.getMessage());
          }
          if (df.isOpen && stats!=null) dfb.packL(i*4,stats.getL("USED"));
          tab.put("DISK_"+aux,stats);
        }
      }
      if (df.isOpen) df.write(dfb);
      if (label.length()>0) MR.put(label,tab);
      if (list && all) nxm.sys.intr.Results.listMultiple (M,tab,"*",0,true);
      if (list && !all) {
        for (Table.Iterator ti=tab.iterator(); ti.getNext(); ) {
          M.println("Key="+ti.key+" Value="+ti.value); // since 3.7.1 use M.println instead of terminal.writeln to allow logging of interactive commands NXM-2813
        }
      }
      MessageHandler msgid = getMessageHandler();
      if (msgid!=null) MQ.put ("SYSTEM",0,tab,msgid,this);
    }

    return (M.pipeMode==Midas.POFF)? FINISH : NORMAL;
  }

  public int close () {
    if (df!=null) {
      df.close();
      df = null;
    }
    return NORMAL;
  }

  /** Get disk stats
   *  TODO: just returns whole output table ??
   *  @return output table
   */
  public Table getDiskStats () {
    return tab;
  }

  /** Get CPU stats
   *  TODO: just returns whole output table ??
   *  @return output table
   */
  public Table getCPUStats () {
    return tab;
  }

  /** Get Memory stats
   *  TODO: just returns null ??
   *  @return null
   */
  public Table getMemStats () {
    return null;
  }

  /** Get Top stats
   *  TODO: just returns null ??
   *  @return null
   */
  public String getTop () {
    return null;
  }

  /** Recursively displays all threads starting at the top thread group.
      @since NeXtMidas 2.1.0
   */
  private void showThreads () {
    ThreadGroup group = Thread.currentThread().getThreadGroup();

    while(group.getParent() != null) {
      group = group.getParent();
    }

    M.type("ALL THREADS");
    showThreads(group, 0);
  }

  /** Recursively displays all threads.

      @param group The current thread group.
      @param depth The recursive depth.
      @since NeXtMidas 2.1.0
   */
  private void showThreads (ThreadGroup group, int depth) {
    final int    EXTRA  = 16; // Extra threads/groups to get due to synchronization issues.
    final String TAB    = "    ";

    ThreadGroup[] groups  = new ThreadGroup[group.activeGroupCount() + EXTRA];
    Thread[]      threads = new Thread[group.activeCount() + EXTRA];

    int numGroups  = group.enumerate(groups,  false);
    int numThreads = group.enumerate(threads, false);

    for (int i = 0; i < numGroups; i++) {
      StringBuilder str = new StringBuilder();

      for (int j = 0; j < depth; j++) {
        str.append(TAB);
        str.append('|');
      }
      str.append(TAB);
      str.append("+-- (Group: ");
      str.append(groups[i].getName());
      str.append(')');

      M.type(str);
      showThreads(groups[i], depth + 1);
    }

    for (int i = 0; i < numThreads; i++) {
      StringBuilder str = new StringBuilder();

      for (int j = 0; j < depth; j++) {
        str.append(TAB);
        str.append('|');
      }
      str.append(TAB);
      str.append("+-- ");
      str.append(threads[i].getName());

      M.type(str);
    }
  }

  private static enum MemoryType {HEAP_MEMORY_USAGE, NON_HEAP_MEMORY_USAGE};

  /** Writes a memory usage element out to the file.
      @param type The type of info to print out HEAP_MEMORY_USAGE or NON_HEAP_MEMORY_USAGE.
   */
  private long[] getMemoryUsageValues (MemoryType type) {
    long[] usage = new long[4];

    MemoryUsage memoryUsage;
    switch (type) {
      case NON_HEAP_MEMORY_USAGE:
        memoryUsage = memoryMXBean.getNonHeapMemoryUsage(); break;
      case HEAP_MEMORY_USAGE: default:
        memoryUsage = memoryMXBean.getHeapMemoryUsage(); break;
    }

    usage[0] = memoryUsage.getInit();
    usage[1] = memoryUsage.getUsed();
    usage[2] = memoryUsage.getCommitted();
    usage[3] = memoryUsage.getMax();
    return usage;
  }

  /** Writes a memory usage element out to the file. */
  private void writeMemoryUsageInfo () {
    long[] heap    = getMemoryUsageValues(MemoryType.HEAP_MEMORY_USAGE);
    long[] nonHeap = getMemoryUsageValues(MemoryType.NON_HEAP_MEMORY_USAGE);

    dfb.packX( 0, heap[1]+nonHeap[1]);
    dfb.packX( 8, heap[2]+nonHeap[2]);
    dfb.packX(16, heap[1]);
    dfb.packX(24, nonHeap[2]);

    df.write(dfb);
  }

  /** Writes a memory usage element out to the file. */
  private void writeThreadInfo () {
    dfb.packX( 0, threadMXBean.getThreadCount());
    dfb.packX( 8, threadMXBean.getPeakThreadCount());
    dfb.packX(16, threadMXBean.getTotalStartedThreadCount());

    df.write(dfb);
  }
}
