package nxm.sys.intr;

import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.net.InetAddress;
import java.util.Map;
import java.util.Properties;

import nxm.sys.inc.InternalUseOnly;
import nxm.sys.inc.Keyable;
import nxm.sys.inc.ProvisionalUseOnly;
import nxm.sys.lib.Convert;
import nxm.sys.lib.FileName;
import nxm.sys.lib.Intrinsic;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Native;
import nxm.sys.lib.Privileges;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Time;
import nxm.sys.libg.Theme;
import nxm.sys.net.NetUtil;

/** Handles the manipulation and display of various environment variables.

    @author Jeff Schoen
    @author Jay Hallam
 */
public class Environment extends Intrinsic implements Keyable {

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

  @Override
  public int process () {
    String  func  = MA.getU("FUNC");
    String  key   = MA.getU("KEY").toUpperCase();
    String  label = MA.getU("VALUE");
    Object  value;
    boolean all   = MA.getState("/ALL");

    if (func.startsWith("SH")) {       // SHOW function
           if (key.length()==0)        showEnvironment(all);
      else if (key.startsWith("THR"))  listAllThreads();
      else if (key.startsWith("MEM"))  listAllMemory();
      else if (key.startsWith("PROP")) listAllProperties(label);
      else if (key.equals("HANDLERS")) Results.listMultipleResult(M,nxm.sys.lib.Registry.handlers,"*",0,true);
      else if (key.equals("KEYGETS") || key.equals("KEYSETS")) {
        M.deprecate("ENVIRONMENT: Since NeXtMidas 2.7.0 "+key+" is deprecated (always blank).");
        Results.listMultipleResult(M, new nxm.sys.lib.Table(),"*",0,true);
      }
      else {
        Results.listSingleResult(M,M.results,"ENV."+key,0,all);
      }
    }
    else if (func.equals("SET")) {
      value = MA.getO("VALUE");
      setKey(key,value);
      if (M.macro!=null && M.macro.isTxt() && key.equals("BREAK")) {
        if (M.pipeMode!=Midas.POFF) M.pipeMode=Midas.PSTOP;
      }
    }
    else if (func.equals("GET")) {
      value = getKey(key);
      if (label == null || label.length() == 0) {
        M.type(key+" = "+value);
      } else {
        MR.put(label,value);
      }
    }
    else if (func.startsWith("NSLOOKUP")) {
      try {
        InetAddress addr = InetAddress.getByName(key);
        String hostaddr = addr.getHostAddress();
        if (label == null || label.length() == 0) {
          M.info("Inet = "+hostaddr);
        } else {
          MR.put(label,hostaddr);
        }
      } catch (Exception e) {
        throw new MidasException("NSLOOKUP exception: "+e, e);
      }
    }
    else if (func.equals("GC")) {
      if (verbose) { M.print("before GC: "); listAllMemory(); } // since 3.7.1 using M.print instead of M.terminal;.write to allow logging - NXM-1813
      System.gc();
      Time.sleep(0.01); // Added in place of System.runFinalization() - since 4.1.3
      System.gc();
      if (verbose) { M.print("after  GC: "); listAllMemory(); }  // since 3.7.1 using M.print instead of M.terminal;.write to allow logging - NXM-1813
    }
    else {
      M.error("Unknown environment function: "+func);
    }
    return FINISH;
  }

  private static final String[] keysCS = {
    "CompletionAuxExclude", "DefaultTableFlags", "DefaultTableMode", "FileNameCase",
    "HostNameFQDN", "NumNonVirtScreens", "NumScreens", "Priv", "Server", "ServerFQDN",
    "UseNative"
  };

  private static final String[] keys = toUpper(keysCS);

  /** Makes a copy of a string array that is upper-case. */
  private static String[] toUpper (String[] orig) {
    String[] array = new String[orig.length];
    for (int i = 0; i < orig.length; i++) array[i] = orig[i].toUpperCase();
    return array;
  }

  @Override
  public String[] getKeys () { return keys; }

  @Override
  public Object setKey (String name, Object value) {
    if (name.equals("COMPLETIONAUXEXCLUDE")) {
      Shell.getCurrent().setCompletionExcludedAuxes(value.toString());
    }
    else if (name.equals("DEFAULTTABLEFLAGS")) {
      nxm.sys.lib.Table.setDefaultFlags(Convert.o2s(value));
    }
    else if (name.equals("DEFAULTTABLEMODE")) {
      nxm.sys.lib.Table.setDefaultMode(Convert.o2s(value));
    }
    else if (name.equals("FILENAMECASE") || name.equals("FNCASE")) {
      FileName.setDefaultFNCase(Convert.o2s(value));
    }
    else if (name.equals("PRIV")) {
      if (value.toString().equals("ALL")) Shell.setPriv(Privileges.PRIV_ALL);
    }
    else {
      if (name.equals("GRAPHICS" )) { Theme.setOptions(value.toString()); value=Theme.getOptions(); }
      if (name.equals("USENATIVE")) { Native.setUse(value.toString()); value = Native.getUse(); }
      if (name.equals("UIMANAGER")) { value = mergeUIManagerData((nxm.sys.lib.Table)value); }
      if (name.equals("IOOPTIONS")) { M.io.setOptions(value.toString()); value=M.io.getOptions(); }
      if (name.equals("PAUSE"    )) { M.pause = Convert.o2d(value); }
      if (name.equals("KEYMAP"   )) { value = Convert.o2t(value, this); } // since 3.7.0 to allow setting directly from .tbl file (BUG 2985)
      MR.put("ENV."+name,value,nxm.sys.lib.Table.FORCE);
    }
    return getKey(name);
  }

  @Override
  public Object getKey (String name) {
    if (name.equals("COMPLETIONAUXEXCLUDE"))  return Shell.getCurrent().getExcludedAuxes();
    if (name.equals("DEFAULTTABLEFLAGS"))     return nxm.sys.lib.Table.getDefaultFlags();
    if (name.equals("DEFAULTTABLEMODE"))      return nxm.sys.lib.Table.getDefaultMode();
    if (name.equals("FILENAMECASE"))          return FileName.getDefaultFNCase();
    if (name.equals("FNCASE"))                return FileName.getDefaultFNCase();
    if (name.equals("HOSTNAMEFQDN"))          return NetUtil.getFQDN(Convert.o2s(getKey("HOSTNAME")));
    if (name.startsWith("NUMNONVIRTSCR"))     return getNumScreens(false); // get # non-virtual screens
    if (name.startsWith("NUMSCR"))            return getNumScreens(true);  // get # screens (any type)
    if (name.equals("PRIV"))                  return Privileges.getPrivMaskString();
    if (name.equals("SERVER"))                return getServer();
    if (name.equals("SERVERFQDN"))            return NetUtil.getFQDN(Convert.o2s(getKey("SERVER")));
    if (name.equals("USENATIVE"))             return Native.getUse();
    return MR.getS("ENV."+name);
  }

  /** Get the server. Precedence is to look for the /REMOTE switch, then ENV.SERVER then the Midas Context.
      private since 3.5.2, was deprecated in 2.7.1
   */
  private String getServer () {
    String value = MA.getQ("/REMOTE",null);          // 1. try switch
    if (value==null) value = MR.getS("ENV.SERVER");  // 2. try environment table
    if (value==null) value = M.getServer();          // 3. try Midas Context
    return value;
  }

  /** Get the number of screens (including virtual screens) in the GraphicsEnvironment.
      Returns 0 if headless.
      @see #getNumScreens(boolean)
      @return the number of screens
   */
  public static int getNumScreens () {
    return getNumScreens(true);
  }

  /** Get the number of screens in the GraphicsEnvironment. Returns 0 if headless.
      @param virtuals true if you want to include virtual screens (e.g. those with xinerama on)
      @return the number of screens
      @since NeXtMidas 2.8.0
   */
  @InternalUseOnly
  @ProvisionalUseOnly("Will become private, please use the new nxm.sys.lib.GraphicsUtil.getNumScreens(boolean).")
  public static int getNumScreens (boolean virtuals) {
    int numScreens = 0;
    if (!Theme.isHeadless()) {
      try {
        GraphicsDevice[] screens = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices();
        if (virtuals) {
          numScreens = screens.length; // count all screens (including virtuals)
        }
        else {
          Rectangle virtualBounds;
          for (GraphicsDevice gd : screens) {
            GraphicsConfiguration[] gconfs = gd.getConfigurations();
            int maxx=0, maxy=0;
            for (GraphicsConfiguration gconf : gconfs) {
              virtualBounds = gconf.getBounds();
              if (virtualBounds.x != 0) maxx = virtualBounds.x;
              if (virtualBounds.y != 0) maxy = virtualBounds.y;
            }
            if (maxx == 0 && maxy == 0) numScreens++; // only count non-virtual screens
          }
        }
      } catch (Exception e) {
        Shell.warning("Unable to determine the number of screens"); // no stack trace required
      }
    }
    return numScreens;
  }

  private void showEnvironment (boolean all) {
    // The call to getNumScreens() was added in 3.3.2 so that any warnings that
    // come up when the graphics are initialized (if not already initialized)
    // appear before rather than in the middle of the listing (as is particularly
    // annoying for users who see the 'Xlib: extension "RANDR" missing on display
    // ":0.0".' warning under RHEL 6.
    getNumScreens();
    M.type("------- Environment Table Settings ------");
    Results.listSingleResult(M,M.results,"ENV",0,all);

    // List the "special" keys/results
    M.type("------- Other Environment Settings ------");
    for (int i = 0; i < keys.length; i++) {
      String keyCS = keysCS[i];
      String key   = keys[i];
      Object value = getKey(key);
      if (value==null) value = "";
      M.type(StringUtil.padRight(keyCS,20)+" = "+value);
    }
  }

  /** Lists all threads in the Environment */
  public void listAllThreads () {
    ThreadGroup root, parent;
    root = Thread.currentThread().getThreadGroup();
    parent = root.getParent();
    while (parent != null) {
      root = parent;
      parent = parent.getParent();
    }
    listThreadGroup (root,"");
  }

  /** Lists information about a given Thread Group
   *  @param g      Thread Group to be listed
   *  @param indent indentation for output messages
   */
  public void listThreadGroup (ThreadGroup g, String indent) {
    if (g == null) return;
    int num_threads = g.activeCount();
    int num_groups = g.activeGroupCount();
    Thread[] threads = new Thread[num_threads];
    ThreadGroup[] groups = new ThreadGroup[num_groups];
    g.enumerate(threads,false);
    g.enumerate(groups,false);
    M.println( indent + "ThreadGroup: " + g.getName() +
                    " MaxPriority: " + g.getMaxPriority());
    for (int i=0; i<num_threads; i++) {
      if (threads[i] == null) continue;
      M.println( "  " + indent + "Thread: " + threads[i].getName() +
                             " Priority: " + threads[i].getPriority() +
                         (threads[i].isDaemon()? "  Daemon": "") );
    }
    for (int i=0; i<num_groups; i++)
      listThreadGroup(groups[i],indent+"  ");
  }

  /** List all memory information in the current Environment */
  public void listAllMemory () {
    Runtime rt = Runtime.getRuntime();
    long tm = rt.totalMemory();
    long fm = rt.freeMemory();
    M.println ("Memory: Total="+tm+" Free="+fm+" bytes");
  }

  /** @deprecated since 3.5.2, this method is not meant to be public and will become private in 3.9+ */
  @Deprecated
  public void listAllProperties () {
    listAllProperties(null);
  }

  /** List Java system properties with option for a simple string filter. since 3.5.2.
      @param filter simple string (case-insensitive) to filter both key and value before output to terminal
   */
  private void listAllProperties (String filter) {
    Properties p = System.getProperties();
    if (filter != null) filter = filter.toUpperCase();
    for (Map.Entry<Object,Object> elem : p.entrySet()) {
      String keyAndValue = elem.toString();
      if (filter == null || keyAndValue.toUpperCase().contains(filter)) {
        M.println(keyAndValue);
      }
    }
  }

  /** Merges the existing UIManager data with the new input(s), so that the old
      settings are not lost. Also calls the update on the UIManager.
      @param tab The new inputs / settings to merge into the SWING UIManager
   */
  private Object mergeUIManagerData (nxm.sys.lib.Table tab) {
    nxm.sys.lib.Table uim = MR.getTable("ENV.UIMANAGER");
    if (uim==null) uim = new nxm.sys.lib.Table();

    if (tab != null) {
      tab = nxm.sys.lib.Table.merge(uim, tab, true);
      tab = Theme.updateUIManager(tab);
      return tab;  // returns so that the ENV table gets updated correctly
    } else {
      return uim;
    }
  }

}
