import java.util.*;
import java.util.zip.*;
import java.util.List;
import java.util.regex.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.concurrent.locks.*;
import java.util.function.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.table.*;
import java.io.*;
import java.net.*;
import java.lang.reflect.*;
import java.lang.ref.*;
import java.lang.management.*;
import java.security.*;
import java.security.spec.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.awt.geom.*;
import javax.imageio.*;
import java.math.*;
import javax.imageio.metadata.*;
import javax.imageio.stream.*;
import javax.swing.undo.UndoManager;
import java.awt.geom.*;
import java.text.SimpleDateFormat;
import java.nio.charset.Charset;
import static x30_pkg.x30_util.DynamicObject;
import javax.swing.Icon;
import org.fife.ui.autocomplete.*;
import org.fife.rsta.ui.CollapsibleSectionPanel;
import org.fife.rsta.ui.GoToDialog;
import org.fife.rsta.ui.SizeGripIcon;
import org.fife.rsta.ui.search.FindDialog;
import org.fife.rsta.ui.search.ReplaceDialog;
import org.fife.rsta.ui.search.ReplaceToolBar;
import org.fife.rsta.ui.search.SearchEvent;
import org.fife.rsta.ui.search.SearchListener;
import org.fife.rsta.ui.search.FindToolBar;
import org.fife.ui.rsyntaxtextarea.ErrorStrip;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.fife.ui.rtextarea.RTextScrollPane;
import org.fife.ui.rtextarea.SearchContext;
import org.fife.ui.rtextarea.SearchEngine;
import org.fife.ui.rtextarea.SearchResult;
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
import org.fife.ui.rsyntaxtextarea.SyntaxScheme;
import java.awt.datatransfer.StringSelection;
import javax.swing.border.*;
import java.util.jar.*;
import java.awt.datatransfer.*;
import java.awt.dnd.*;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.DataFlavor;
import java.text.*;
import org.fife.ui.rsyntaxtextarea.*;
import java.nio.file.Path;
import javax.swing.event.AncestorListener;
import javax.swing.event.AncestorEvent;
import javax.swing.Timer;
import java.text.NumberFormat;
import java.util.TimeZone;
import java.awt.datatransfer.UnsupportedFlavorException;

class main {

static class G22RunSingleAnalyzerPanel implements Swingable , IFieldsToList{
  G22Utils g22utils;
  BufferedImage image;
  G22Analyzer analyzer;
  G22RunSingleAnalyzerPanel() {}
  G22RunSingleAnalyzerPanel(G22Utils g22utils, BufferedImage image, G22Analyzer analyzer) {
  this.analyzer = analyzer;
  this.image = image;
  this.g22utils = g22utils;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + g22utils + ", " + image + ", " + analyzer + ")"; }public Object[] _fieldsToList() { return new Object[] {g22utils, image, analyzer}; }

  transient ReliableSingleThread rst = new ReliableSingleThread(new Runnable() {  public void run() { try {  _run(); 
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "_run();"; }});
  transient SingleComponentPanel scp = singleComponentPanel();
  transient String pleaseSelectMsg = ""; //"Please select an analyzer";
  
  transient Set<IVF2<G22Analyzer.CompiledAnalyzer, Object>> onAnalyzerResult;
public G22RunSingleAnalyzerPanel onAnalyzerResult(IVF2<G22Analyzer.CompiledAnalyzer, Object> f) { onAnalyzerResult = createOrAddToSyncLinkedHashSet(onAnalyzerResult, f); return this; }
public G22RunSingleAnalyzerPanel removeAnalyzerResultListener(IVF2<G22Analyzer.CompiledAnalyzer, Object> f) { main.remove(onAnalyzerResult, f); return this; }
public void analyzerResult(G22Analyzer.CompiledAnalyzer analyzer, Object result) {  pcallFAll(onAnalyzerResult, analyzer, result); }
  transient Set<IVF2<G22Analyzer.CompiledAnalyzer, Throwable>> onAnalyzerError;
public G22RunSingleAnalyzerPanel onAnalyzerError(IVF2<G22Analyzer.CompiledAnalyzer, Throwable> f) { onAnalyzerError = createOrAddToSyncLinkedHashSet(onAnalyzerError, f); return this; }
public G22RunSingleAnalyzerPanel removeAnalyzerErrorListener(IVF2<G22Analyzer.CompiledAnalyzer, Throwable> f) { main.remove(onAnalyzerError, f); return this; }
public void analyzerError(G22Analyzer.CompiledAnalyzer analyzer, Throwable error) {  pcallFAll(onAnalyzerError, analyzer, error); }
  
  public JComponent visualize() {
    reRun();
    return scp;
  }
  
  void reRun() {
    rst.trigger();
  }
  
  void setImage(BufferedImage image) {
    this.image = image;
    reRun();
  }
  
  void _run() {
    if (analyzer == null) { scp.set(jcenteredlabel(pleaseSelectMsg)); return; }
    if (image == null) { scp.set(jcenteredlabel("No image")); return; }
  
    G22Analyzer.CompiledAnalyzer compiled = analyzer.compileForAutoRun();
    if (compiled == null)
      { scp.set(jcenteredlabel(analyzer + " is not cleared for auto-run")); return; }
      
    if (compiled.compileError != null)
      { scp.set(jErrorView(compiled.compileError)); return; }
    
    if (scp.isEmpty())  
      scp.set(jcenteredlabel("Running analyzer..."));
      
    try {
      long time = nanoTime();
      Object result = compiled.get(image);
      time = nanoTime()-time;
      analyzerResult(compiled, result);
      var visualizer = new G22JavaObjectVisualizer(g22utils, result);
      visualizer.nanos(time);
      scp.set(visualizer);
    } catch (Throwable e) {
      scp.set(jErrorView(e));
      analyzerError(compiled, e);
    }
  }
}
static String shortClassName_dropNumberPrefix(Object o) {
  return dropNumberPrefix(shortClassName(o));
}


static Class _run(String progID, String... args) {
  Class main = hotwire(progID);
  callMain(main, args);
  return main;
}


static RuntimeException rethrow(Throwable t) {
  
  if (t instanceof Error)
    _handleError((Error) t);
  
  throw t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t);
}

static RuntimeException rethrow(String msg, Throwable t) {
  throw new RuntimeException(msg, t);
}


static SingleComponentPanel singleComponentPanel() {
  return singleComponentPanel(null);
}

static SingleComponentPanel singleComponentPanel(final Component c) {
  return swing(new F0<SingleComponentPanel>() { public SingleComponentPanel get() { try { 
    return new SingleComponentPanel(c);
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return new SingleComponentPanel(c);"; }});
}


static <A> Set<A> createOrAddToSyncLinkedHashSet(Set<A> set, A a) {
  if (set == null) set = syncLinkedHashSet();
  set.add(a);
  return set;
}


static <A> void remove(List<A> l, int i) {
  if (l != null && i >= 0 && i < l(l))
    l.remove(i);
}

static <A> void remove(Collection<A> l, A a) {
  if (l != null) l.remove(a);
}

static <A, B> B remove(Map<A, B> map, Object a) {
  return map == null ? null : map.remove(a);
}

static void remove(BitSet bs, int i) {
  bs.clear(i);
}


static void pcallFAll(Collection l, Object... args) {
  if (l != null) for (Object f : cloneList(l)) pcallF(f, args);
}

static void pcallFAll(Iterator it, Object... args) {
  while (it.hasNext()) pcallF(it.next(), args);
}


static JLabel jcenteredlabel(String text) {
  return jcenteredLabel(text);
}

static JLabel jcenteredlabel() {
  return jcenteredLabel();
}


static JComponent jErrorView(Throwable e) {
  //ret jscroll(jMultiLineLabel(renderStackTrace(e)));
  return jSmallErrorView(e);
}


static long nanoTime() {
  return System.nanoTime();
}




static String dropNumberPrefix(String s) {
  return dropFirst(s, indexOfNonDigit(s));
}


static String shortClassName(Object o) {
  if (o == null) return null;
  Class c = o instanceof Class ? (Class) o : o.getClass();
  String name = c.getName();
  return shortenClassName(name);
}


// custom mainClass only works with hotwire_here
static Class<?> hotwire(String src) { return hotwire(src, __1 -> mainClassNameForClassLoader(__1)); }
static Class<?> hotwire(String src, IF1<ClassLoader, String> calculateMainClass) {
  assertFalse(_inCore());
  Class j = getJavaX();
  if (isAndroid()) {
    synchronized(j) { // hopefully this goes well...
      List<File> libraries = new ArrayList<File>();
      File srcDir = (File) call(j, "transpileMain", src, libraries);
      if (srcDir == null)
        throw fail("transpileMain returned null (src=" + quote(src) + ")");
    
      Object androidContext = get(j, "androidContext");
      return (Class) call(j, "loadx2android", srcDir, src);
    }
  } else {
    
    
    Class c =  (Class) (call(j, "hotwire", src));
    hotwire_copyOver(c);
    return c;
    
  }
}


static <A> A callMain(A c, String... args) {
  callOpt(c, "main", new Object[] {args});
  return c;
}

static void callMain() {
  callMain(mc());
}


static void _handleError(Error e) {
  call(javax(), "_handleError", e);
}


static Object swing(Object f) {
  return swingAndWait(f);
}

static void swing(Runnable f) {
  swingAndWait(f);
}

static <A> A swing(F0<A> f) {
  return (A) swingAndWait(f);
}

static <A> A swing(IF0<A> f) {
  return (A) swingAndWait(f);
}


// get purpose 1: access a list/array/map (safer version of x.get(y))

static <A> A get(List<A> l, int idx) {
  return l != null && idx >= 0 && idx < l(l) ? l.get(idx) : null;
}

// seems to conflict with other signatures
/*static <A, B> B get(Map<A, B> map, A key) {
  ret map != null ? map.get(key) : null;
}*/

static <A> A get(A[] l, int idx) {
  return idx >= 0 && idx < l(l) ? l[idx] : null;
}

// default to false
static boolean get(boolean[] l, int idx) {
  return idx >= 0 && idx < l(l) ? l[idx] : false;
}

// get purpose 2: access a field by reflection or a map

static Object get(Object o, String field) {
  try {
    if (o == null) return null;
    if (o instanceof Class) return get((Class) o, field);
    
    if (o instanceof Map)
      return ((Map) o).get(field);
      
    Field f = getOpt_findField(o.getClass(), field);
    if (f != null) {
      makeAccessible(f);
      return f.get(o);
    }
      
    
      if (o instanceof DynamicObject)
        return getOptDynOnly(((DynamicObject) o), field);
    
  } catch (Exception e) {
    throw asRuntimeException(e);
  }
  throw new RuntimeException("Field '" + field + "' not found in " + o.getClass().getName());
}

static Object get_raw(String field, Object o) {
  return get_raw(o, field);
}

static Object get_raw(Object o, String field) { try {
  if (o == null) return null;
  Field f = get_findField(o.getClass(), field);
  makeAccessible(f);
  return f.get(o);
} catch (Exception __e) { throw rethrow(__e); } }

static Object get(Class c, String field) {
  try {
    Field f = get_findStaticField(c, field);
    makeAccessible(f);
    return f.get(null);
  } catch (Exception e) {
    throw new RuntimeException(e);
  }
}

static Field get_findStaticField(Class<?> c, String field) {
  Class _c = c;
  do {
    for (Field f : _c.getDeclaredFields())
      if (f.getName().equals(field) && (f.getModifiers() & java.lang.reflect.Modifier.STATIC) != 0)
        return f;
    _c = _c.getSuperclass();
  } while (_c != null);
  throw new RuntimeException("Static field '" + field + "' not found in " + c.getName());
}

static Field get_findField(Class<?> c, String field) {
  Class _c = c;
  do {
    for (Field f : _c.getDeclaredFields())
      if (f.getName().equals(field))
        return f;
    _c = _c.getSuperclass();
  } while (_c != null);
  throw new RuntimeException("Field '" + field + "' not found in " + c.getName());
}

static Object get(String field, Object o) {
  return get(o, field);
}

static boolean get(BitSet bs, int idx) {
  return bs != null && bs.get(idx);
}


static <A> Set<A> syncLinkedHashSet() {
  return synchroLinkedHashSet();
}


static int l(Object[] a) { return a == null ? 0 : a.length; }
static int l(boolean[] a) { return a == null ? 0 : a.length; }
static int l(byte[] a) { return a == null ? 0 : a.length; }
static int l(short[] a) { return a == null ? 0 : a.length; }
static int l(long[] a) { return a == null ? 0 : a.length; }
static int l(int[] a) { return a == null ? 0 : a.length; }
static int l(float[] a) { return a == null ? 0 : a.length; }
static int l(double[] a) { return a == null ? 0 : a.length; }
static int l(char[] a) { return a == null ? 0 : a.length; }
static int l(Collection c) { return c == null ? 0 : c.size(); }

static int l(Iterator i) { return iteratorCount_int_close(i); } // consumes the iterator && closes it if possible

static int l(Map m) { return m == null ? 0 : m.size(); }
static int l(CharSequence s) { return s == null ? 0 : s.length(); }
static long l(File f) { return f == null ? 0 : f.length(); }



static int l(Object o) {
  return o == null ? 0
    : o instanceof String ? l((String) o)
    : o instanceof Map ? l((Map) o)
    : o instanceof Collection ? l((Collection) o)
    : o instanceof Object[] ? l((Object[]) o)
    : o instanceof boolean[] ? l((boolean[]) o)
    : o instanceof byte[] ? l((byte[]) o)
    : o instanceof char[] ? l((char[]) o)
    : o instanceof short[] ? l((short[]) o)
    : o instanceof int[] ? l((int[]) o)
    : o instanceof float[] ? l((float[]) o)
    : o instanceof double[] ? l((double[]) o)
    : o instanceof long[] ? l((long[]) o)
    : (Integer) call(o, "size");
}



  static int l(MultiSet ms) { return ms == null ? 0 : ms.size(); }





  static int l(IntRange r) { return r == null ? 0 : r.length(); }











  static int l(AppendableChain a) { return a == null ? 0 : a.size; }






static <A> ArrayList<A> cloneList(Iterable<A> l) {
  return l instanceof Collection ? cloneList((Collection) l) : asList(l);
}

static <A> ArrayList<A> cloneList(Collection<A> l) {
  if (l == null) return new ArrayList();
  synchronized(collectionMutex(l)) {
    return new ArrayList<A>(l);
  }
}


static Object pcallF(Object f, Object... args) {
  return pcallFunction(f, args);
}


static <A> A pcallF(F0<A> f) {
  try { return f == null ? null : f.get(); } catch (Throwable __e) { printStackTrace(__e); } return null;
}



static <A, B> B pcallF(F1<A, B> f, A a) {
  try { return f == null ? null : f.get(a); } catch (Throwable __e) { printStackTrace(__e); } return null;
}



static <A> void pcallF(VF1<A> f, A a) {
  try {
    { if (f != null) f.get(a); }
  } catch (Throwable __e) { printStackTrace(__e); }
}


static Object pcallF(Runnable r) {
  try { { if (r != null) r.run(); } } catch (Throwable __e) { printStackTrace(__e); } return null;
}

static <A> A pcallF(IF0<A> f) {
  try { return f == null ? null : f.get(); } catch (Throwable __e) { printStackTrace(__e); } return null;
}

static <A, B> B pcallF(IF1<A, B> f, A a) {
  try { return f == null ? null : f.get(a); } catch (Throwable __e) { printStackTrace(__e); } return null;
}



static JLabel jcenteredLabel(String text) {
  return setHorizontalAlignment(JLabel.CENTER, jLabel(text));
}

static JLabel jcenteredLabel() {
  return jcenteredLabel(" ");
}



static JComponent jSmallErrorView(Throwable e) {
  var btn = jimageButtonScaledToWidth(32, errorIconID(), "Show error details",
    () -> showErrorFrame(e));
  var label = jlabel_noAutoToolTip(exceptionType(e));
  return jline(btn, label);
}




static String[] dropFirst(int n, String[] a) {
  return drop(n, a);
}

static String[] dropFirst(String[] a) {
  return drop(1, a);
}

static Object[] dropFirst(Object[] a) {
  return drop(1, a);
}

static <A> List<A> dropFirst(List<A> l) {
  return dropFirst(1, l);
}

static <A> List<A> dropFirst(int n, Iterable<A> i) { return dropFirst(n, toList(i)); }
static <A> List<A> dropFirst(Iterable<A> i) { return dropFirst(toList(i)); }

static <A> List<A> dropFirst(int n, List<A> l) {
  return n <= 0 ? l : new ArrayList(l.subList(Math.min(n, l.size()), l.size()));
}

static <A> List<A> dropFirst(List<A> l, int n) {
  return dropFirst(n, l);
}

static String dropFirst(int n, String s) { return substring(s, n); }
static String dropFirst(String s, int n) { return substring(s, n); }
static String dropFirst(String s) { return substring(s, 1); }




static int indexOfNonDigit(String s) {
  int n = l(s);
  for (int i = 0; i < n; i++)
    if (!isDigit(s.charAt(i)))
      return i;
  return -1;
}


static String shortenClassName(String name) {
  if (name == null) return null;
  int i = lastIndexOf(name, "$");
  if (i < 0) i = lastIndexOf(name, ".");
  return i < 0 ? name : substring(name, i+1);
}


static String mainClassNameForClassLoader(ClassLoader cl) {
  return or((String) callOpt(cl, "mainClassName"), "main");
}


static void assertFalse(Object o) {
  if (!(eq(o, false) /*|| isFalse(pcallF(o))*/))
    throw fail(str(o));
}
  
static boolean assertFalse(boolean b) {
  if (b) throw fail("oops");
  return b;
}

static boolean assertFalse(String msg, boolean b) {
  if (b) throw fail(msg);
  return b;
}



static boolean _inCore() {
  return false;
}


static Class __javax;

static Class getJavaX() { try {
  
  return __javax;
} catch (Exception __e) { throw rethrow(__e); } }

static void __setJavaX(Class j) {
  __javax = j;
  _onJavaXSet();
}


static int isAndroid_flag;

static boolean isAndroid() {
  if (isAndroid_flag == 0)
    isAndroid_flag = System.getProperty("java.vendor").toLowerCase().indexOf("android") >= 0 ? 1 : -1;
  return isAndroid_flag > 0;
}



static Object call(Object o) {
  return callF(o);
}

// varargs assignment fixer for a single string array argument
static Object call(Object o, String method, String[] arg) {
  return call(o, method, new Object[] {arg});
}

static Object call(Object o, String method, Object... args) {
  //ret call_cached(o, method, args);
  return call_withVarargs(o, method, args);
}


static RuntimeException fail() { throw new RuntimeException("fail"); }
static RuntimeException fail(Throwable e) { throw asRuntimeException(e); }
static RuntimeException fail(Object msg) { throw new RuntimeException(String.valueOf(msg)); }


static RuntimeException fail(Object... objects) { throw new Fail(objects); }


static RuntimeException fail(String msg) { throw new RuntimeException(msg == null ? "" : msg); }
static RuntimeException fail(String msg, Throwable innerException) { throw new RuntimeException(msg, innerException); }



static String quote(Object o) {
  if (o == null) return "null";
  return quote(str(o));
}

static String quote(String s) {
  if (s == null) return "null";
  StringBuilder out = new StringBuilder((int) (l(s)*1.5+2));
  quote_impl(s, out);
  return out.toString();
}
  
static void quote_impl(String s, StringBuilder out) {
  out.append('"');
  int l = s.length();
  for (int i = 0; i < l; i++) {
    char c = s.charAt(i);
    if (c == '\\' || c == '"')
      out.append('\\').append(c);
    else if (c == '\r')
      out.append("\\r");
    else if (c == '\n')
      out.append("\\n");
    else if (c == '\t')
      out.append("\\t");
    else if (c == '\0')
      out.append("\\0");
    else
      out.append(c);
  }
  out.append('"');
}


static List hotwire_copyOver_after = synchroList();

static void hotwire_copyOver(Class c) {
  // TODO: make a mechanism for making such "inheritable" fields
  for (String field : ll("print_log", "print_silent", "androidContext", "_userHome"))
    setOptIfNotNull(c, field, getOpt(mc(), field));
    
  
  
  setOptIfNotNull(c, "mainBot" , getMainBot());
  setOpt(c, "creator_class" , new WeakReference(mc()));
  pcallFAll(hotwire_copyOver_after, c);
}


static Object callOpt(Object o) {
  return callF(o);
}

static Object callOpt(Object o, String method, Object... args) {
  return callOpt_withVarargs(o, method, args);
}


static Class mc() {
  return main.class;
}


static Class javax() {
  return getJavaX();
}


static void swingAndWait(Runnable r) { try {
  if (isAWTThread())
    r.run();
  else
    EventQueue.invokeAndWait(addThreadInfoToRunnable(r));
} catch (Exception __e) { throw rethrow(__e); } }

static Object swingAndWait(final Object f) {
  if (isAWTThread())
    return callF(f);
  else {
    final Var result = new Var();
    swingAndWait(new Runnable() {  public void run() { try { 
      result.set(callF(f));
    
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "result.set(callF(f));"; }});
    return result.get();
  }
}


static Field getOpt_findField(Class<?> c, String field) {
  Class _c = c;
  do {
    for (Field f : _c.getDeclaredFields())
      if (f.getName().equals(field))
        return f;
    _c = _c.getSuperclass();
  } while (_c != null);
  return null;
}


static Field makeAccessible(Field f) {
  try {
    f.setAccessible(true);
  } catch (Throwable e) {
    // Note: The error reporting only works with Java VM option --illegal-access=deny
    
    vmBus_send("makeAccessible_error", e, f);
    
  }
  return f;
}

static Method makeAccessible(Method m) {
  try {
    m.setAccessible(true);
  } catch (Throwable e) {
    
    vmBus_send("makeAccessible_error", e, m);
    
  }
  return m;
}

static Constructor makeAccessible(Constructor c) {
  try {
    c.setAccessible(true);
  } catch (Throwable e) {
    
    vmBus_send("makeAccessible_error", e, c);
    
  }
  return c;
}


static Object getOptDynOnly(DynamicObject o, String field) {
  if (o == null || o.fieldValues == null) return null;
  return o.fieldValues.get(field);
}


static RuntimeException asRuntimeException(Throwable t) {
  
  if (t instanceof Error)
    _handleError((Error) t);
  
  return t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t);
}


// BREAKING CHANGE!
// Also NOTE: Iterators of these sync-wrapped collections
// after generally NOT thread-safe!
// TODO: change that?
static <A> Set<A> synchroLinkedHashSet() {
  
  
  return Collections.synchronizedSet(new CompactLinkedHashSet());
  
}


static <A> int iteratorCount_int_close(Iterator<A> i) { try {
  int n = 0;
  if (i != null) while (i.hasNext()) { i.next(); ++n; }
  if (i instanceof AutoCloseable) ((AutoCloseable) i).close();
  return n;
} catch (Exception __e) { throw rethrow(__e); } }


// unclear semantics as to whether return null on null

static <A> ArrayList<A> asList(A[] a) {
  return a == null ? new ArrayList<A>() : new ArrayList<A>(Arrays.asList(a));
}

static ArrayList<Integer> asList(int[] a) {
  if (a == null) return null;
  ArrayList<Integer> l = emptyList(a.length);
  for (int i : a) l.add(i);
  return l;
}

static ArrayList<Long> asList(long[] a) {
  if (a == null) return null;
  ArrayList<Long> l = emptyList(a.length);
  for (long i : a) l.add(i);
  return l;
}

static ArrayList<Float> asList(float[] a) {
  if (a == null) return null;
  ArrayList<Float> l = emptyList(a.length);
  for (float i : a) l.add(i);
  return l;
}

static ArrayList<Double> asList(double[] a) {
  if (a == null) return null;
  ArrayList<Double> l = emptyList(a.length);
  for (double i : a) l.add(i);
  return l;
}

static ArrayList<Short> asList(short[] a) {
  if (a == null) return null;
  ArrayList<Short> l = emptyList(a.length);
  for (short i : a) l.add(i);
  return l;
}

static <A> ArrayList<A> asList(Iterator<A> it) {
  ArrayList l = new ArrayList();
  if (it != null)
    while (it.hasNext())
      l.add(it.next());
  return l;  
}

// disambiguation
static <A> ArrayList<A> asList(IterableIterator<A> s) {
  return asList((Iterator) s);
}

static <A> ArrayList<A> asList(Iterable<A> s) {
  if (s instanceof ArrayList) return (ArrayList) s;
  ArrayList l = new ArrayList();
  if (s != null)
    for (A a : s)
      l.add(a);
  return l;
}


static <A> ArrayList<A> asList(Producer<A> p) {
  ArrayList l = new ArrayList();
  A a;
  if (p != null) while ((a = p.next()) != null)
    l.add(a);
  return l;
}


static <A> ArrayList<A> asList(Enumeration<A> e) {
  ArrayList l = new ArrayList();
  if (e != null)
    while (e.hasMoreElements())
      l.add(e.nextElement());
  return l;
}




static <A> List<A> asList(Pair<A, A> p) {
  return p == null ? null : ll(p.a, p.b);
}



// TODO: JDK 17!! ?? No! Yes? Yes!!

static Object collectionMutex(List l) {
  return l;
}

static Object collectionMutex(Object o) {
  

  if (o instanceof List) return o;
  
  // TODO: actually use our own maps so we can get the mutex properly
  
  String c = className(o);
  
    
  
    
  
  return o;
}


static Object pcallFunction(Object f, Object... args) {
  try { return callFunction(f, args); } catch (Throwable __e) { printStackTrace(__e); }
  return null;
}


static <A extends Throwable> A printStackTrace(A e) {
  // we go to system.out now - system.err is nonsense
  if (e != null) print(getStackTrace(e));
  return e;
}

static void printStackTrace() {
  printStackTrace(new Throwable());
}

static void printStackTrace(String msg) {
  printStackTrace(new Throwable(msg));
}

static void printStackTrace(String msg, Throwable e) {
  printStackTrace(new Throwable(msg, e));
}


static <A extends JLabel> A setHorizontalAlignment(final int pos, final A a) {
  swingCall(a, "setHorizontalAlignment", pos);
  return a;
}

static <A extends AbstractButton> A setHorizontalAlignment(final int pos, final A a) {
  swingCall(a, "setHorizontalAlignment", pos);
  return a;
}

static <A extends JTextField> A setHorizontalAlignment(final int pos, final A a) {
  swingCall(a, "setHorizontalAlignment", pos);
  return a;
}


static JLabel jLabel(String text) {
  return jlabel(text);
}

static JLabel jLabel() {
  return jlabel();
}


static JButton jimageButtonScaledToWidth(int w, String imageID, Runnable action) { return jimageButtonScaledToWidth(w, imageID, "", action); }
static JButton jimageButtonScaledToWidth(int w, String imageID, String toolTip, Runnable action) {
  return jimageButton(scaleImageToWidth(w, imageID), toolTip, action);
}


static String errorIconID() {
  return "#1101390";
}


static JComponent showErrorFrame(Throwable e) {
  return showText("Error", renderStackTrace(/*getInnerException*/(e)));
}


static JLabel jlabel_noAutoToolTip() { return jlabel_noAutoToolTip(" "); }
static JLabel jlabel_noAutoToolTip(String text) {
  var lbl = swingConstruct(BetterLabel.class, text);
  lbl.autoToolTip = false;
  return lbl;
}


static String exceptionType(Throwable e) {
  e = getInnerException(e);
  return shortClassName(e);
}


static JPanel jline(final Component... components) {
  return swing(new F0<JPanel>() { public JPanel get() { try {  return new LeftAlignedLine(components);  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return new LeftAlignedLine(components);"; }});
}

static <A extends Component> JPanel jline(List<A> components) {
  return jline(asArray(Component.class, components));
}




static volatile StringBuffer local_log = new StringBuffer(); // not redirected




static boolean printAlsoToSystemOut = true;

static volatile Appendable print_log = local_log; // might be redirected, e.g. to main bot

// in bytes - will cut to half that
static volatile int print_log_max = 1024*1024;
static volatile int local_log_max = 100*1024;

static boolean print_silent = false; // total mute if set

static Object print_byThread_lock = new Object();
static volatile ThreadLocal<Object> print_byThread; // special handling by thread - prefers F1<S, Bool>
static volatile Object print_allThreads;
static volatile Object print_preprocess;

static void print() {
  print("");
}

static <A> A print(String s, A o) {
  print(combinePrintParameters(s, o));
  return o;
}

// slightly overblown signature to return original object...
static <A> A print(A o) {
  ping_okInCleanUp();
  if (print_silent) return o;
  String s = o + "\n";
  print_noNewLine(s);
  return o;
}

static void print_noNewLine(String s) {
  
  try {
    Object f = getThreadLocal(print_byThread_dontCreate());
    if (f == null) f = print_allThreads;
      if (f != null)
        // We do need the general callF machinery here as print_byThread is sometimes shared between modules
        if (isFalse(
          
            f instanceof F1 ? ((F1) f).get(s) :
          
          callF(f, s))) return;
  } catch (Throwable e) {
    System.out.println(getStackTrace(e));
  }
  

  print_raw(s);
}

static void print_raw(String s) {
  
  if (print_preprocess != null) s = (String) callF(print_preprocess, s);
  s = fixNewLines(s);
  
  Appendable loc = local_log;
  Appendable buf = print_log;
  int loc_max = print_log_max;
  if (buf != loc && buf != null) {
    print_append(buf, s, print_log_max);
    loc_max = local_log_max;
  }
  if (loc != null) 
    print_append(loc, s, loc_max);
  
  
  if (printAlsoToSystemOut)
    System.out.print(s);
  
  vmBus_send("printed", mc(), s);
}

static void print_autoRotate() {
  
}


static String[] drop(int n, String[] a) {
  n = Math.min(n, a.length);
  String[] b = new String[a.length-n];
  System.arraycopy(a, n, b, 0, b.length);
  return b;
}

static Object[] drop(int n, Object[] a) {
  n = Math.min(n, a.length);
  Object[] b = new Object[a.length-n];
  System.arraycopy(a, n, b, 0, b.length);
  return b;
}


static <A> ArrayList<A> toList(A[] a) { return asList(a); }
static ArrayList<Integer> toList(int[] a) { return asList(a); }
static <A> ArrayList<A> toList(Set<A> s) { return asList(s); }
static <A> ArrayList<A> toList(Iterable<A> s) { return asList(s); }


static String substring(String s, int x) {
  return substring(s, x, strL(s));
}

static String substring(String s, int x, int y) {
  if (s == null) return null;
  if (x < 0) x = 0;
  int n = s.length();
  if (y < x) y = x;
  if (y > n) y = n;
  if (x >= y) return "";
  return s.substring(x, y);
}


static String substring(String s, IntRange r) {
  return r == null ? null : substring(s, r.start, r.end);
}


// convenience method for quickly dropping a prefix
static String substring(String s, CharSequence l) {
  return substring(s, lCharSequence(l));
}


static boolean isDigit(char c) {
  return Character.isDigit(c);
}


static int lastIndexOf(String a, String b) {
  return a == null || b == null ? -1 : a.lastIndexOf(b);
}

static int lastIndexOf(String a, char b) {
  return a == null ? -1 : a.lastIndexOf(b);
}

// starts searching from i-1
static <A> int lastIndexOf(List<A> l, int i, A a) {
  if (l == null) return -1;
  for (i = min(l(l), i)-1; i >= 0; i--)
    if (eq(l.get(i), a))
      return i;
  return -1;
}

static <A> int lastIndexOf(List<A> l, A a) {
  if (l == null) return -1;
  for (int i = l(l)-1; i >= 0; i--)
    if (eq(l.get(i), a))
      return i;
  return -1;
}


static <A> A or(A a, A b) {
  return a != null ? a : b;
}


static boolean eq(Object a, Object b) {
  return a == b || a != null && b != null && a.equals(b);
}


// a little kludge for stuff like eq(symbol, "$X")
static boolean eq(Symbol a, String b) {
  return eq(str(a), b);
}



static String str(Object o) {
  return o == null ? "null" : o.toString();
}

static String str(char[] c) {
  return new String(c);
}

static String str(char[] c, int offset, int count) {
  return new String(c, offset, count);
}


static void _onJavaXSet() {}


static Map<Class, ArrayList<Method>> callF_cache = newDangerousWeakHashMap();


  static <A> A callF(F0<A> f) {
    return f == null ? null : f.get();
  }



  static <A, B> B callF(F1<A, B> f, A a) {
    return f == null ? null : f.get(a);
  }



  static <A> A callF(IF0<A> f) {
    return f == null ? null : f.get();
  }



  static <A, B> B callF(IF1<A, B> f, A a) {
    return f == null ? null : f.get(a);
  }


static <A, B> B callF(A a, IF1<A, B> f) {
  return f == null ? null : f.get(a);
}




  static <A, B, C> C callF(IF2<A, B, C> f, A a, B b) {
    return f == null ? null : f.get(a, b);
  }



  static <A> void callF(VF1<A> f, A a) {
    if (f != null) f.get(a);
  }


static <A> void callF(A a, IVF1<A> f) {
  if (f != null) f.get(a);
}

static <A> void callF(IVF1<A> f, A a) {
  if (f != null) f.get(a);
}


static Object callF(Runnable r) { { if (r != null) r.run(); } return null; }

static Object callF(Object f, Object... args) {
  
    
  return safeCallF(f, args);
}

static Object safeCallF(Object f, Object... args) {
  if (f instanceof Runnable) {
    ((Runnable) f).run();
    return null;
  }
  if (f == null) return null;
  
  Class c = f.getClass();
  ArrayList<Method> methods;
  synchronized(callF_cache) {
    methods = callF_cache.get(c);
    if (methods == null)
      methods = callF_makeCache(c);
  }
  
  int n = l(methods);
  if (n == 0) {
    
    
    if (f instanceof String)
      throw fail("Legacy call: " + f);
    
    throw fail("No get method in " + getClassName(c));
  }
  if (n == 1) return invokeMethod(methods.get(0), f, args);
  for (int i = 0; i < n; i++) {
    Method m = methods.get(i);
    if (call_checkArgs(m, args, false))
      return invokeMethod(m, f, args);
  }
  throw fail("No matching get method in " + getClassName(c));
}

// used internally
static ArrayList<Method> callF_makeCache(Class c) {
  ArrayList<Method> l = new ArrayList();
  Class _c = c;
  do {
    for (Method m : _c.getDeclaredMethods())
      if (m.getName().equals("get")) {
        makeAccessible(m);
        l.add(m);
      }
    if (!l.isEmpty()) break;
    _c = _c.getSuperclass();
  } while (_c != null);
  callF_cache.put(c, l);
  return l;
}


static Object call_withVarargs(Object o, String method, Object... args) { try {
  if (o == null) return null;
  
  if (o instanceof Class) {
    Class c = (Class) o;
    _MethodCache cache = callOpt_getCache(c);
    
    Method me = cache.findStaticMethod(method, args);
    if (me != null)
      return invokeMethod(me, null, args);
      
    // try varargs
    List<Method> methods = cache.cache.get(method);
    if (methods != null) methodSearch: for (Method m : methods) {
      { if (!(m.isVarArgs())) continue; }
      { if (!(isStaticMethod(m))) continue; }
      Object[] newArgs = massageArgsForVarArgsCall(m, args);
      if (newArgs != null)
        return invokeMethod(m, null, newArgs);
    }
    
    throw fail("Method " + c.getName() + "." + method + "(" + joinWithComma(classNames(args)) + ") not found");
  } else {
    Class c = o.getClass();
    _MethodCache cache = callOpt_getCache(c);

    Method me = cache.findMethod(method, args);
    if (me != null)
      return invokeMethod(me, o, args);
      
    // try varargs
    List<Method> methods = cache.cache.get(method);
    if (methods != null) methodSearch: for (Method m : methods) {
      { if (!(m.isVarArgs())) continue; }
      Object[] newArgs = massageArgsForVarArgsCall(m, args);
      if (newArgs != null)
        return invokeMethod(m, o, newArgs);
    }
    
    throw fail("Method " + c.getName() + "." + method + "(" + joinWithComma(classNames(args)) + ") not found");
  }
} catch (Exception __e) { throw rethrow(__e); } }


static <A> List<A> synchroList() {
  return synchroList(new ArrayList<A>());
}

static <A> List<A> synchroList(List<A> l) {
  
  
    return Collections.synchronizedList(l);
  
}



static <A> List<A> ll(A... a) {
  ArrayList l = new ArrayList(a.length);
  if (a != null) for (A x : a) l.add(x);
  return l;
}


static void setOptIfNotNull(Object o, String field, Object value) {
  if (value != null) setOpt(o, field, value);
}


static Object getOpt(Object o, String field) {
  return getOpt_cached(o, field);
}

static Object getOpt(String field, Object o) {
  return getOpt_cached(o, field);
}

static Object getOpt_raw(Object o, String field) { try {
  Field f = getOpt_findField(o.getClass(), field);
  if (f == null) return null;
  makeAccessible(f);
  return f.get(o);
} catch (Exception __e) { throw rethrow(__e); } }

// access of static fields is not yet optimized
static Object getOpt(Class c, String field) { try {
  if (c == null) return null;
  Field f = getOpt_findStaticField(c, field);
  if (f == null) return null;
  makeAccessible(f);
  return f.get(null);
} catch (Exception __e) { throw rethrow(__e); } }

static Field getOpt_findStaticField(Class<?> c, String field) {
  Class _c = c;
  do {
    for (Field f : _c.getDeclaredFields())
      if (f.getName().equals(field) && (f.getModifiers() & java.lang.reflect.Modifier.STATIC) != 0)
        return f;
    _c = _c.getSuperclass();
  } while (_c != null);
  return null;
}



static Object mainBot;

static Object getMainBot() {
  return mainBot;
}


static Field setOpt_findField(Class c, String field) {
  HashMap<String, Field> map;
  synchronized(getOpt_cache) {
    map = getOpt_cache.get(c);
    if (map == null)
      map = getOpt_makeCache(c);
  }
  return map.get(field);
}

static void setOpt(Object o, String field, Object value) { try {
  if (o == null) return;
  
  
  
  Class c = o.getClass();
  HashMap<String, Field> map;
  
  if (getOpt_cache == null)
    map = getOpt_makeCache(c); // in class init
  else synchronized(getOpt_cache) {
    map = getOpt_cache.get(c);
    if (map == null)
      map = getOpt_makeCache(c);
  }
  
  if (map == getOpt_special) {
    if (o instanceof Class) {
      setOpt((Class) o, field, value);
      return;
    }
    
    // It's probably a subclass of Map. Use raw method. TODO: huh?
    setOpt_raw(o, field, value);
    return;
  }
  
  Field f = map.get(field);
  
  if (f != null)
    { smartSet(f, o, value); return; } // possible improvement: skip setAccessible
  
    if (o instanceof DynamicObject)
      { setDyn(((DynamicObject) o), field, value); return; }
  
  if (o instanceof IMeta)
    setDyn(((IMeta) o), field, value);
} catch (Exception __e) { throw rethrow(__e); } }

static void setOpt(Class c, String field, Object value) {
  if (c == null) return;
  try {
    Field f = setOpt_findStaticField(c, field); // TODO: optimize
    if (f != null)
      smartSet(f, null, value);
  } catch (Exception e) {
    throw new RuntimeException(e);
  }
}
  
static Field setOpt_findStaticField(Class<?> c, String field) {
  Class _c = c;
  do {
    for (Field f : _c.getDeclaredFields())
      if (f.getName().equals(field) && (f.getModifiers() & java.lang.reflect.Modifier.STATIC) != 0) {
        makeAccessible(f);
        return f;
      }
    _c = _c.getSuperclass();
  } while (_c != null);
  return null;
}


static Object callOpt_withVarargs(Object o, String method, Object... args) { try {
  if (o == null) return null;
  
  if (o instanceof Class) {
    Class c = (Class) o;
    _MethodCache cache = callOpt_getCache(c);
    
    Method me = cache.findMethod(method, args);
    if (me == null) {
      // TODO: varargs
      return null;
    }
    if ((me.getModifiers() & Modifier.STATIC) == 0)
      return null;
    return invokeMethod(me, null, args);
  } else {
    Class c = o.getClass();
    _MethodCache cache = callOpt_getCache(c);

    Method me = cache.findMethod(method, args);
    if (me != null)
      return invokeMethod(me, o, args);
      
    // try varargs
    List<Method> methods = cache.cache.get(method);
    if (methods != null) methodSearch: for (Method m : methods) {
      { if (!(m.isVarArgs())) continue; }
      Object[] newArgs = massageArgsForVarArgsCall(m, args);
      if (newArgs != null)
        return invokeMethod(m, o, newArgs);
    }
    
    return null;
  }
} catch (Exception __e) { throw rethrow(__e); } }


// TODO: test if android complains about this
static boolean isAWTThread() {
  if (isAndroid()) return false;
  if (isHeadless()) return false;
  return isAWTThread_awt();
}

static boolean isAWTThread_awt() {
  return SwingUtilities.isEventDispatchThread();
}


static Runnable addThreadInfoToRunnable(final Object r) {
  final Object info = _threadInfo();
  return info == null ? asRunnable(r) : new Runnable() {  public void run() { try {  _inheritThreadInfo(info); callF(r); 
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "_inheritThreadInfo(info); callF(r);"; }};
}


static void vmBus_send(String msg, Object... args) {
  Object arg = vmBus_wrapArgs(args);
  pcallFAll_minimalExceptionHandling(vm_busListeners_live(), msg, arg);
  pcallFAll_minimalExceptionHandling(vm_busListenersByMessage_live().get(msg), msg, arg);
}

static void vmBus_send(String msg) {
  vmBus_send(msg, (Object) null);
}


static ArrayList emptyList() {
  return new ArrayList();
  //ret Collections.emptyList();
}

static ArrayList emptyList(int capacity) {
  return new ArrayList(max(0, capacity));
}

// Try to match capacity
static ArrayList emptyList(Iterable l) {
  return l instanceof Collection ? emptyList(((Collection) l).size()) : emptyList();
}

static ArrayList emptyList(Object[] l) {
  return emptyList(l(l));
}

// get correct type at once
static <A> ArrayList<A> emptyList(Class<A> c) {
  return new ArrayList();
}


static String className(Object o) {
  return getClassName(o);
}


static Object callFunction(Object f, Object... args) {
  return callF(f, args);
}


static String getStackTrace(Throwable throwable) {
  lastException(throwable);
  return getStackTrace_noRecord(throwable);
}

static String getStackTrace_noRecord(Throwable throwable) {
  StringWriter writer = new StringWriter();
  throwable.printStackTrace(new PrintWriter(writer));
  return hideCredentials(writer.toString());
}

static String getStackTrace() {
  return getStackTrace_noRecord(new Throwable());
}

static String getStackTrace(String msg) {
  return getStackTrace_noRecord(new Throwable(msg));
}


static Object swingCall(final Object o, final String method, final Object... args) {
  return swing(new F0<Object>() { public Object get() { try {  return call(o, method, args);  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return call(o, method, args);"; }});
}


static JLabel jlabel(final String text) {
  return swingConstruct(BetterLabel.class, text);
}

static JLabel jlabel() {
  return jlabel(" ");
}


static JButton jimageButton(String imageID, Object action) {
  JButton btn = jbutton("", action);
  btn.setIcon(imageIcon(imageID));
  return btn;
}

// button without action
static JButton jimageButton(String imageID) {
  return jimageButton(imageID, null);
}

static JButton jimageButton(Image img) {
  return jimageButton(img, null, null);
}

static JButton jimageButton(String imageID, String toolTip, Runnable action) {
  return jimageButton(imageIcon(imageID), toolTip, action);
}

static JButton jimageButton(Image img, String toolTip, Runnable action) {
  var btn = jbutton("", action);
  setButtonImage(btn, img);
  return setToolTip(toolTip, btn);
}

static JButton jimageButton(ImageIcon img, String toolTip, Runnable action) {
  var btn = jbutton("", action);
  setButtonImage(btn, img);
  return setToolTip(toolTip, btn);
}


static BufferedImage scaleImageToWidth(BufferedImage img, int newW) {
  return resizeImage(img, newW);
}

static BufferedImage scaleImageToWidth(int newW, BufferedImage img) {
  return scaleImageToWidth(img, newW);
}

static BufferedImage scaleImageToWidth(int newW, String imageID) {
  return scaleImageToWidth(newW, loadImage2(imageID));
}


// Note: very SLOW with big text (JTextArea)
// Use showText_fast_noWrap instead

static JTextArea showText(final String title, Object text) {
  return showText(null, title, text);
}

static JTextArea showText(JTextArea ta, final String title, Object text) {
  final String _text = str(text);
  if (ta != null)
    return activateFrameAndReturnComponent(setFrameTitle(title, setText(ta, _text)));
    
  return swing(new F0<JTextArea>() { public JTextArea get() { try { 
    JTextArea textArea = newTypeWriterTextArea(_text);
    makeFrame(title, new JScrollPane(textArea));
    return textArea;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "JTextArea textArea = newTypeWriterTextArea(_text);\r\n    makeFrame(title, new ..."; }});
}

static JTextArea showText(Object text) {
  return showText(str(text));
}

static JTextArea showText(String text) {
  return showText(autoFrameTitle(), text);
}

static JTextArea showText() {
  return showText("");
}


static String renderStackTrace(StackTraceElement[] st) {
  return stackTraceToString(st);
}

static String renderStackTrace(Throwable e) {
  return stackTraceToString(e);
}

static String renderStackTrace(String msg) {
  return renderStackTrace(new Throwable(msg));
}


static <A> A swingConstruct(final Class<A> c, final Object... args) {
  return swing(new F0<A>() { public A get() { try {  return nuObject(c, args);  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return nuObject(c, args);"; }});
}


static Throwable getInnerException(Throwable e) {
  if (e == null) return null;
  while (e.getCause() != null)
    e = e.getCause();
  return e;
}

static Throwable getInnerException(Runnable r) {
  return getInnerException(getException(r));
}


static Object[] asArray(List l) {
  return toObjectArray(l);
}

static <A> A[] asArray(Class<A> type, List l) {
  return (A[]) l.toArray((Object[]) Array.newInstance(type, l.size()));
}




static AutoCloseable tempInterceptPrintIfNotIntercepted(F1<String, Boolean> f) {
  return print_byThread().get() == null ? tempInterceptPrint(f) : null;
}


//static final Map<Class, HashMap<S, Field>> getOpt_cache = newDangerousWeakHashMap(f getOpt_special_init);

static class getOpt_Map extends WeakHashMap {
  getOpt_Map() {
    if (getOpt_special == null) getOpt_special = new HashMap();
    clear();
  }
  
  public void clear() {
    super.clear();
    //print("getOpt clear");
    put(Class.class, getOpt_special);
    put(String.class, getOpt_special);
  }
}

static final Map<Class, HashMap<String, Field>> getOpt_cache = _registerDangerousWeakMap(synchroMap(new getOpt_Map()));
//static final Map<Class, HashMap<S, Field>> getOpt_cache = _registerWeakMap(synchroMap(new getOpt_Map));
static HashMap getOpt_special; // just a marker

/*static void getOpt_special_init(Map map) {
  map.put(Class.class, getOpt_special);
  map.put(S.class, getOpt_special);
}*/

static Map<String, Field> getOpt_getFieldMap(Object o) {
  Class c = _getClass(o);
  HashMap<String, Field> map = getOpt_cache.get(c);
  if (map == null)
    map = getOpt_makeCache(c);
  return map;
}

static Object getOpt_cached(Object o, String field) { try {
  if (o == null) return null;

  Map<String, Field> map = getOpt_getFieldMap(o);

  if (map == getOpt_special) {
    if (o instanceof Class)
      return getOpt((Class) o, field);
    /*if (o instanceof S)
      ret getOpt(getBot((S) o), field);*/
    if (o instanceof Map)
      return ((Map) o).get(field);
  }
    
  Field f = map.get(field);
  if (f != null) return f.get(o);
  
    if (o instanceof DynamicObject)
      return syncMapGet2(((DynamicObject) o).fieldValues, field);
  
  return null;
} catch (Exception __e) { throw rethrow(__e); } }

// used internally - we are in synchronized block
static HashMap<String, Field> getOpt_makeCache(Class c) {
  HashMap<String, Field> map;
  if (isSubtypeOf(c, Map.class))
    map = getOpt_special;
  else {
    map = new HashMap();
    if (!reflection_classesNotToScan().contains(c.getName())) {
      Class _c = c;
      do {
        for (Field f : _c.getDeclaredFields()) {
          makeAccessible(f);
          String name = f.getName();
          if (!map.containsKey(name))
            map.put(name, f);
        }
        _c = _c.getSuperclass();
      } while (_c != null);
    }
  }
  if (getOpt_cache != null) getOpt_cache.put(c, map);
  return map;
}


static String combinePrintParameters(String s, Object o) {
  return (endsWithLetterOrDigit(s) ? s + ": " : s) + o;
}


static void ping_okInCleanUp() {


  if (ping_pauseAll || ping_anyActions)
    ping_impl(true);


}


// this syntax should be removed...
static Object getThreadLocal(Object o, String name) {
  ThreadLocal t =  (ThreadLocal) (getOpt(o, name));
  return t != null ? t.get() : null;
}

static <A> A getThreadLocal(ThreadLocal<A> tl) {
  return tl == null ? null : tl.get();
}

static <A> A getThreadLocal(ThreadLocal<A> tl, A defaultValue) {
  return or(getThreadLocal(tl), defaultValue);
}


static ThreadLocal<Object> print_byThread_dontCreate() {
  return print_byThread;
}


static boolean isFalse(Object o) {
  return eq(false, o);
}


static String fixNewLines(String s) {
  int i = indexOf(s, '\r');
  if (i < 0) return s;
  int l = s.length();
  StringBuilder out = new StringBuilder(l);
  out.append(s, 0, i);
  for (; i < l; i++) {
    char c = s.charAt(i);
    if (c != '\r')
      out.append(c);
    else {
      out.append('\n');
      if (i+1 < l && s.charAt(i+1) == '\n') ++i;
    }
  }
  return out.toString();
}


static void print_append(Appendable buf, String s, int max) { try {
  synchronized(buf) {
    buf.append(s);
    if (buf instanceof StringBuffer)
      rotateStringBuffer(((StringBuffer) buf), max);
    else if (buf instanceof StringBuilder)
      rotateStringBuilder(((StringBuilder) buf), max);
  }
} catch (Exception __e) { throw rethrow(__e); } }


static int strL(String s) {
  return s == null ? 0 : s.length();
}


static int lCharSequence(CharSequence s) {
  return s == null ? 0 : s.length();
}


static int min(int a, int b) {
  return Math.min(a, b);
}

static long min(long a, long b) {
  return Math.min(a, b);
}

static float min(float a, float b) { return Math.min(a, b); }
static float min(float a, float b, float c) { return min(min(a, b), c); }

static double min(double a, double b) {
  return Math.min(a, b);
}

static double min(double[] c) {
  double x = Double.MAX_VALUE;
  for (double d : c) x = Math.min(x, d);
  return x;
}

static float min(float[] c) {
  float x = Float.MAX_VALUE;
  for (float d : c) x = Math.min(x, d);
  return x;
}

static byte min(byte[] c) {
  byte x = 127;
  for (byte d : c) if (d < x) x = d;
  return x;
}

static short min(short[] c) {
  short x = 0x7FFF;
  for (short d : c) if (d < x) x = d;
  return x;
}

static int min(int[] c) {
  int x = Integer.MAX_VALUE;
  for (int d : c) if (d < x) x = d;
  return x;
}


static <A, B> Map<A, B> newDangerousWeakHashMap() {
  return _registerDangerousWeakMap(synchroMap(new WeakHashMap()));
}

// initFunction: voidfunc(Map) - is called initially, and after clearing the map
static <A, B> Map<A, B> newDangerousWeakHashMap(Object initFunction) {
  return _registerDangerousWeakMap(synchroMap(new WeakHashMap()), initFunction);
}


static String getClassName(Object o) {
  return o == null ? "null" : o instanceof Class ? ((Class) o).getName() : o.getClass().getName();
}


static Object invokeMethod(Method m, Object o, Object... args) { try {
  try {
    return m.invoke(o, args);
  } catch (InvocationTargetException e) {
    throw rethrow(getExceptionCause(e));
  } catch (IllegalArgumentException e) {
    throw new IllegalArgumentException(e.getMessage() + " - was calling: " + m + ", args: " + joinWithSpace(classNames(args)));
  }
} catch (Exception __e) { throw rethrow(__e); } }


static boolean call_checkArgs(Method m, Object[] args, boolean debug) {
  Class<?>[] types = m.getParameterTypes();
  if (types.length != args.length) {
    if (debug)
      print("Bad parameter length: " + args.length + " vs " + types.length);
    return false;
  }
  for (int i = 0; i < types.length; i++) {
    Object arg = args[i];
    if (!(arg == null ? !types[i].isPrimitive()
      : isInstanceX(types[i], arg))) {
      if (debug)
        print("Bad parameter " + i + ": " + arg + " vs " + types[i]);
      return false;
    }
  }
  return true;
}


static final Map<Class, _MethodCache> callOpt_cache = newDangerousWeakHashMap();

static Object callOpt_cached(Object o, String methodName, Object... args) { try {
  if (o == null) return null;
  
  if (o instanceof Class) {
    Class c = (Class) o;
    _MethodCache cache = callOpt_getCache(c);
    
    // TODO: (super-rare) case where method exists static and non-static
    // with different args
    
    Method me = cache.findMethod(methodName, args);
    if (me == null || (me.getModifiers() & Modifier.STATIC) == 0) return null;
    return invokeMethod(me, null, args);
  } else {
    Class c = o.getClass();
    _MethodCache cache = callOpt_getCache(c);

    Method me = cache.findMethod(methodName, args);
    if (me == null) return null;
    return invokeMethod(me, o, args);
  }
} catch (Exception __e) { throw rethrow(__e); } }

// no longer synchronizes! (see #1102990)
static _MethodCache callOpt_getCache(Class c) {
  _MethodCache cache = callOpt_cache.get(c);
  if (cache == null)
    callOpt_cache.put(c, cache = new _MethodCache(c));
  return cache;
}


static boolean isStaticMethod(Method m) {
  return methodIsStatic(m);
}


static Object[] massageArgsForVarArgsCall(Method m, Object[] args) {
  Class<?>[] types = m.getParameterTypes();
  int n = types.length-1, nArgs = args.length;
  if (nArgs < n) return null;
  for (int i = 0; i < n; i++)
    if (!argumentCompatibleWithType(args[i], types[i]))
      return null;
  Class varArgType = types[n].getComponentType();
  for (int i = n; i < nArgs; i++)
    if (!argumentCompatibleWithType(args[i], varArgType))
      return null;
  Object[] newArgs = new Object[n+1];
  arraycopy(args, 0, newArgs, 0, n);
  Object[] varArgs = arrayOfType(varArgType, nArgs-n);
  arraycopy(args, n, varArgs, 0, nArgs-n);
  newArgs[n] = varArgs;
  return newArgs;
}


static <A> String joinWithComma(Collection<A> c) {
  return join(", ", c);
}

static String joinWithComma(Object... c) {
  return join(", ", c);
}

static String joinWithComma(String... c) {
  return join(", ", c);
}


static String joinWithComma(Pair p) {
  return p == null ? "" : joinWithComma(str(p.a), str(p.b));
}



static List<String> classNames(Collection l) {
  return getClassNames(l);
}

static List<String> classNames(Object[] l) {
  return getClassNames(Arrays.asList(l));
}


// TODO: optimize (use getOpt_cache)
static void setOpt_raw(Object o, String field, Object value) { try {
  if (o == null) return;
  if (o instanceof Class) setOpt_raw((Class) o, field, value);
  else {
    Field f = setOpt_raw_findField(o.getClass(), field);
    if (f != null) {
      makeAccessible(f);
      smartSet(f, o, value);
    }
  }
} catch (Exception __e) { throw rethrow(__e); } }

static void setOpt_raw(Class c, String field, Object value) { try {
  if (c == null) return;
  Field f = setOpt_raw_findStaticField(c, field);
  if (f != null) {
    makeAccessible(f);
    smartSet(f, null, value);
  }
} catch (Exception __e) { throw rethrow(__e); } }
  
static Field setOpt_raw_findStaticField(Class<?> c, String field) {
  Class _c = c;
  do {
    for (Field f : _c.getDeclaredFields())
      if (f.getName().equals(field) && (f.getModifiers() & java.lang.reflect.Modifier.STATIC) != 0)
        return f;
    _c = _c.getSuperclass();
  } while (_c != null);
  return null;
}

static Field setOpt_raw_findField(Class<?> c, String field) {
  Class _c = c;
  do {
    for (Field f : _c.getDeclaredFields())
      if (f.getName().equals(field))
        return f;
    _c = _c.getSuperclass();
  } while (_c != null);
  return null;
}


static void smartSet(Field f, Object o, Object value) throws Exception {
  try {
    f.set(o, value);
  } catch (Exception e) {
    Class type = f.getType();
    
    // take care of common case (long to int)
    if (type == int.class && value instanceof Long)
      { f.set(o, ((Long) value).intValue()); return; }
      
    if (type == boolean.class && value instanceof String)
      { f.set(o, isTrueOrYes(((String) value))); return; }
    
    if (type == LinkedHashMap.class && value instanceof Map)
      { f.set(o, asLinkedHashMap((Map) value)); return; }
    
    
      try {
        if (f.getType() == Concept.Ref.class)
          { f.set(o, ((Concept) o).new Ref((Concept) value)); return; }
        if (o instanceof Concept.Ref)
          { f.set(o, ((Concept.Ref) o).get()); return; }
      } catch (Throwable _e) {}
    
    throw e;
  }
}


static <A extends DynamicObject> A setDyn(A o, String key, Object value) {
  setDynObjectValue(o, key, value);
  return o;
}

static void setDyn(IMeta o, String key, Object value) {
  metaMapPut(o, key, value);
}


static Boolean isHeadless_cache;

static boolean isHeadless() {
  if (isHeadless_cache != null) return isHeadless_cache;
  if (isAndroid()) return isHeadless_cache = true;
  if (GraphicsEnvironment.isHeadless()) return isHeadless_cache = true;
  
  // Also check if AWT actually works.
  // If DISPLAY variable is set but no X server up, this will notice.
  
  try {
    SwingUtilities.isEventDispatchThread();
    return isHeadless_cache = false;
  } catch (Throwable e) { return isHeadless_cache = true; }
}


static List<VF1<Map>> _threadInfo_makers = synchroList();

static Object _threadInfo() {
  if (empty(_threadInfo_makers)) return null;
  HashMap map = new HashMap();
  pcallFAll(_threadInfo_makers, map);
  return map;
}


static Runnable asRunnable(Object o) {
  return toRunnable(o);
}




static void _inheritThreadInfo(Object info) {
  _threadInheritInfo(info);
}


static Object vmBus_wrapArgs(Object... args) {
  return empty(args) ? null
    : l(args) == 1 ? args[0]
    : args;
}


static void pcallFAll_minimalExceptionHandling(Collection l, Object... args) {
  if (l != null) for  (Object f : cloneList(l)) { ping(); pcallF_minimalExceptionHandling(f, args); }
}

static void pcallFAll_minimalExceptionHandling(Iterator it, Object... args) {
  while  (it.hasNext()) { ping(); pcallF_minimalExceptionHandling(it.next(), args); }
}


static Set vm_busListeners_live_cache;
static Set vm_busListeners_live() { if (vm_busListeners_live_cache == null) vm_busListeners_live_cache = vm_busListeners_live_load(); return vm_busListeners_live_cache;}

static Set vm_busListeners_live_load() {
  return vm_generalIdentityHashSet("busListeners");
}


static Map<String, Set> vm_busListenersByMessage_live_cache;
static Map<String, Set> vm_busListenersByMessage_live() { if (vm_busListenersByMessage_live_cache == null) vm_busListenersByMessage_live_cache = vm_busListenersByMessage_live_load(); return vm_busListenersByMessage_live_cache;}

static Map<String, Set> vm_busListenersByMessage_live_load() {
  return vm_generalHashMap("busListenersByMessage");
}


static int max(int a, int b) { return Math.max(a, b); }
static int max(int a, int b, int c) { return max(max(a, b), c); }
static long max(int a, long b) { return Math.max((long) a, b); }
static long max(long a, long b) { return Math.max(a, b); }
static double max(int a, double b) { return Math.max((double) a, b); }
static float max(float a, float b) { return Math.max(a, b); }
static double max(double a, double b) { return Math.max(a, b); }

static int max(Collection<Integer> c) {
  int x = Integer.MIN_VALUE;
  for (int i : c) x = max(x, i);
  return x;
}

static double max(double[] c) {
  if (c.length == 0) return Double.MIN_VALUE;
  double x = c[0];
  for (int i = 1; i < c.length; i++) x = Math.max(x, c[i]);
  return x;
}

static float max(float[] c) {
  if (c.length == 0) return Float.MAX_VALUE;
  float x = c[0];
  for (int i = 1; i < c.length; i++) x = Math.max(x, c[i]);
  return x;
}

static byte max(byte[] c) {
  byte x = -128;
  for (byte d : c) if (d > x) x = d;
  return x;
}

static short max(short[] c) {
  short x = -0x8000;
  for (short d : c) if (d > x) x = d;
  return x;
}

static int max(int[] c) {
  int x = Integer.MIN_VALUE;
  for (int d : c) if (d > x) x = d;
  return x;
}

static <A extends Comparable<A>> A max(A a, A b) {
  return cmp(a, b) >= 0 ? a : b;
}


// PersistableThrowable doesn't hold GC-disturbing class references in backtrace
static volatile PersistableThrowable lastException_lastException;

static PersistableThrowable lastException() {
  return lastException_lastException;
}

static void lastException(Throwable e) {
  lastException_lastException = persistableThrowable(e);
}


static String hideCredentials(URL url) { return url == null ? null : hideCredentials(str(url)); }

static String hideCredentials(String url) {
  try {
    if (startsWithOneOf(url, "http://", "https://") && isAGIBlueDomain(hostNameFromURL(url))) return url;
  } catch (Throwable e) {
    print("HideCredentials", e);
  }
  return url.replaceAll("([&?])(_pass|key|cookie)=[^&\\s\"]*", "$1$2=<hidden>");
}

static String hideCredentials(Object o) {
  return hideCredentials(str(o));
}


static JButton jbutton(String text, Runnable action) {
  return newButton(text, action);
}

static JButton jbutton(String text, Object action) {
  return newButton(text, action);
}

// button without action
static JButton jbutton(String text) {
  return newButton(text, null);
}

/*static JButton jbutton(BufferedImage img, O action) {
  ret setButtonImage(img, jbutton("", action));
}*/

static JButton jbutton(Action action) {
  return swingNu(JButton.class, action);
}


static int imageIcon_cacheSize = 10;
static boolean imageIcon_verbose = false;
static Map<String, ImageIcon> imageIcon_cache;
static Lock imageIcon_lock = lock();
static ThreadLocal<Boolean> imageIcon_fixGIF = new ThreadLocal();

// not going through BufferedImage preserves animations
static ImageIcon imageIcon(String imageID) { try {
  if (imageID == null) return null;
  Lock __0 = imageIcon_lock; lock(__0); try {
  if (imageIcon_cache == null)
    imageIcon_cache = new MRUCache(imageIcon_cacheSize);
  imageID = fsI(imageID);
  ImageIcon ii = imageIcon_cache.get(imageID);
  if (ii == null) {
    if (imageIcon_verbose) print("Loading image icon: " + imageID);
    File f = loadBinarySnippet(imageID);
    
      Boolean b = imageIcon_fixGIF.get();
      if (!isFalse(b))
        ii = new ImageIcon(loadBufferedImageFixingGIFs(f));
      else
    
    ii = new ImageIcon(f.toURI().toURL());
  } else
    imageIcon_cache.remove(imageID); // move to front of cache on access
  imageIcon_cache.put(imageID, ii);
  return ii;
} finally { unlock(__0); } } catch (Exception __e) { throw rethrow(__e); } }

// doesn't fix GIFs
static ImageIcon imageIcon(File f) { try {
  return new ImageIcon(f.toURI().toURL());
} catch (Exception __e) { throw rethrow(__e); } }

static ImageIcon imageIcon(Image img) {
  return img == null ? null : new ImageIcon(img);
}


  static ImageIcon imageIcon(RGBImage img) {
    return imageIcon(img.getBufferedImage());
  }



static JButton setButtonImage(Icon img, JButton btn) {
  btn.setIcon(img);
  return btn;
}

static JButton setButtonImage(Image img, JButton btn) {
  btn.setIcon(imageIcon(img));
  return btn;
}

static <A extends AbstractButton> A setButtonImage(Image img, A btn) {
  btn.setIcon(imageIcon(img));
  return btn;
}

static <A extends AbstractButton> A setButtonImage(A btn, Image img) {
  return setButtonImage(img, btn);
}

static <A extends AbstractButton> A setButtonImage(A btn, String imageID) {
  btn.setIcon(imageIcon(imageID));
  return btn;
}

static JButton setButtonImage(JButton btn, Image img) {
  return setButtonImage(img, btn);
}

static JButton setButtonImage(JButton btn, Icon img) {
  return setButtonImage(img, btn);
}


static <A extends JComponent> A setToolTip(A c, Object toolTip) {
  return setToolTipText(c, toolTip);
}

static <A extends JComponent> A setToolTip(Object toolTip, A c) {
  return setToolTipText(c, toolTip);
}

static void setToolTip(TrayIcon trayIcon, String toolTip) {
  setTrayIconToolTip(trayIcon, toolTip);
}


static BufferedImage resizeImage(BufferedImage img, int newW, int newH) {
  return resizeImage(img, newW, newH, Image.SCALE_SMOOTH);
}

static BufferedImage resizeImage(BufferedImage img, int newW, int newH, int scaleType) {
  if (newW == img.getWidth() && newH == img.getHeight()) return img;
  Image tmp = img.getScaledInstance(newW, newH, scaleType);
  BufferedImage dimg = new BufferedImage(newW, newH, BufferedImage.TYPE_INT_ARGB);
  Graphics2D g2d = dimg.createGraphics();
  g2d.drawImage(tmp, 0, 0, null);
  g2d.dispose();
  return dimg;
}

static BufferedImage resizeImage(BufferedImage img, int newW) {
  int newH = iround(img.getHeight()*(double) newW/img.getWidth());
  return resizeImage(img, newW, newH);
}

static BufferedImage resizeImage(int newW, BufferedImage img) {
  return resizeImage(img, newW);
}


static BufferedImage loadImage2(String snippetIDOrURL) {
  return loadBufferedImage(snippetIDOrURL);
}

static BufferedImage loadImage2(File file) {
  return loadBufferedImage(file);
}


static <A extends Component> A activateFrameAndReturnComponent(A c) {
  activateFrame(c);
  return c;
}


static <A> A setFrameTitle(A c, final String title) {
  final Frame f = getAWTFrame(c);
  if (f != null) { swing(() -> {  f.setTitle(title); }); }
  return c;
}

static <A extends Component> A setFrameTitle(String title, A c) {
  return setFrameTitle(c, title);
}

// magically find a field called "frame" in main class :-)
static JFrame setFrameTitle(String title) {
  Object f = getOpt(mc(), "frame");
  if (f instanceof JFrame)
    return setFrameTitle((JFrame) f, title);
  return null;
}




static boolean setText_opt = true; // optimize by calling getText first

static <A extends JTextComponent> A setText(A c, Object text) {
  setText((JComponent) c, text);
  return c;
}

static <A extends JComboBox> A setText(final A c, Object text) {
  // only for editable combo boxes at this point
  final String s = strUnnull(text);
  { swing(() -> { 
    c.getEditor().setItem(s);
  }); }
  return c;
}

static void setText(JLabel c, Object text) {
  setText((JComponent) c, text);
}

static JButton setText(JButton c, Object text) {
  setText((JComponent) c, jlabel_textAsHTML_center_ifNeeded(strUnnull(text)));
  return c;
}

static <A extends JComponent> A setText(final A c, Object text) {
  if (c == null) return null;
  final String s = strUnnull(text);
  { swing(() -> { 
    if (!setText_opt || neq(callOpt(c, "getText"), s))
      call(c, "setText", s);
  }); }
  return c;
}






static JTextArea newTypeWriterTextArea() {
  return newTypeWriterTextArea("");
}

static JTextArea newTypeWriterTextArea(String text) {
  return withTypeWriterFont(jTextArea(text));
}


static String makeFrame_defaultIcon;
static boolean makeFrame_hideConsole = false;
static ThreadLocal<VF1<JFrame>> makeFrame_post = new ThreadLocal();

static JFrame makeFrame() {
  return makeFrame((Component) null);
}

static JFrame makeFrame(Object content) {
  return makeFrame(programTitle(), content);
}

static JFrame makeFrame(String title) {
  return makeFrame(title, null);
}

static JFrame makeFrame(String title, Object content) {
  return makeFrame(title, content, true);
}

static JFrame makeFrame(final String title, final Object content, final boolean showIt) {
  final VF1<JFrame> post = optParam(makeFrame_post);
  return swing(new F0<JFrame>() { public JFrame get() { try { 
    if (getFrame(content) != null)
      return getFrame(setFrameTitle((Component) content, title));
    final JFrame frame = new JFrame(title);
    if (makeFrame_defaultIcon != null)
      setFrameIconLater(frame, makeFrame_defaultIcon);
    _initFrame(frame);
    Component wrapped = wrap(content);
    if (wrapped != null)
      frame.getContentPane().add(wrapped);
    frame.setBounds(defaultNewFrameBounds());
    
    callF(post, frame);
    
    if (showIt)
      frame.setVisible(true);
    //callOpt(content, "requestFocus");
    //exitOnFrameClose(frame);
    
    if (showIt && makeFrame_hideConsole) {
      hideConsole();
      makeFrame_hideConsole = false;
    }
    
    return frame;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "if (getFrame(content) != null)\r\n      ret getFrame(setFrameTitle((Component) ..."; }});
}


static String autoFrameTitle_value;

static String autoFrameTitle() {
  return autoFrameTitle_value != null ? autoFrameTitle_value : getProgramTitle();
}

static void autoFrameTitle(Component c) {
  setFrameTitle(getFrame(c), autoFrameTitle());
}


static String stackTraceToString(StackTraceElement[] st) {
  return lines(st);
}

static String stackTraceToString(Throwable e) {
  return getStackTrace_noRecord(e);
}


static Object nuObject(String className, Object... args) { try {
  return nuObject(classForName(className), args);
} catch (Exception __e) { throw rethrow(__e); } }

// too ambiguous - maybe need to fix some callers
/*static O nuObject(O realm, S className, O... args) {
  ret nuObject(_getClass(realm, className), args);
}*/

static <A> A nuObject(Class<A> c, Object... args) { try {
  if (args.length == 0) return nuObjectWithoutArguments(c); // cached!
  
  Constructor m = nuObject_findConstructor(c, args);
  makeAccessible(m);
  return (A) m.newInstance(args);
} catch (Exception __e) { throw rethrow(__e); } }

static Constructor nuObject_findConstructor(Class c, Object... args) {
  for (Constructor m : c.getDeclaredConstructors()) {
    if (!nuObject_checkArgs(m.getParameterTypes(), args, false))
      continue;
    return m;
  }
  throw fail("Constructor " + c.getName() + getClasses(args) + " not found"
    + (args.length == 0 && (c.getModifiers() & java.lang.reflect.Modifier.STATIC) == 0 ? " - hint: it's a non-static class!" : ""));
}

 static boolean nuObject_checkArgs(Class[] types, Object[] args, boolean debug) {
    if (types.length != args.length) {
      if (debug)
        System.out.println("Bad parameter length: " + args.length + " vs " + types.length);
      return false;
    }
    for (int i = 0; i < types.length; i++)
      if (!(args[i] == null || isInstanceX(types[i], args[i]))) {
        if (debug)
          System.out.println("Bad parameter " + i + ": " + args[i] + " vs " + types[i]);
        return false;
      }
    return true;
  }


static Throwable getException(Runnable r) {
  try {
    callF(r);
    return null;
  } catch (Throwable e) {
    return e;
  }
}


// binary legacy signature
static Object[] toObjectArray(Collection c) {
  return toObjectArray((Iterable) c);
}

static Object[] toObjectArray(Iterable c) {
  List l = asList(c);
  return l.toArray(new Object[l.size()]);
}





static ThreadLocal<Object> print_byThread() {
  synchronized(print_byThread_lock) {
    if (print_byThread == null)
      print_byThread = new ThreadLocal();
  }
  return print_byThread;
}


// f can return false to suppress regular printing
// call print_raw within f to actually print something
static AutoCloseable tempInterceptPrint(F1<String, Boolean> f) {
  return tempSetThreadLocal(print_byThread(), f);
}


static void clear(Collection c) {
  if (c != null) c.clear();
}

static void clear(Map map) {
  if (map != null) map.clear();
}


static <A, B> void put(Map<A, B> map, A a, B b) {
  if (map != null) map.put(a, b);
}

static <A> void put(List<A> l, int i, A a) {
  if (l != null && i >= 0 && i < l(l)) l.set(i, a);
}


static List<Pair> _registerDangerousWeakMap_preList;

static <A> A _registerDangerousWeakMap(A map) {
  return _registerDangerousWeakMap(map, null);
}

static <A> A _registerDangerousWeakMap(A map, Object init) {
  
  callF(init, map);
  
  if (init instanceof String) {
    final String f =  (String) init;
    init = new VF1<Map>() { public void get(Map map) { try {  callMC(f, map) ; } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "callMC(f, map)"; }};
  }
    
  if (javax() == null) {
    // We're in class init
    if (_registerDangerousWeakMap_preList == null) _registerDangerousWeakMap_preList = synchroList();
    _registerDangerousWeakMap_preList.add(pair(map, init));
    return map;
  }
  
  call(javax(), "_registerDangerousWeakMap", map, init);
  
  return map;
}

static void _onLoad_registerDangerousWeakMap() {
  
  assertNotNull(javax());
  if (_registerDangerousWeakMap_preList == null) return;
  for (Pair p : _registerDangerousWeakMap_preList)
    _registerDangerousWeakMap(p.a, p.b);
  _registerDangerousWeakMap_preList = null;
  
}


static Map synchroMap() {
  return synchroHashMap();
}

static <A, B> Map<A, B> synchroMap(Map<A, B> map) {
  
  
    return Collections.synchronizedMap(map);
  
}


static Class<?> _getClass(String name) {
  try {
    return Class.forName(name);
  } catch (ClassNotFoundException e) {
    return null; // could optimize this
  }
}

static Class _getClass(Object o) {
  return o == null ? null
    : o instanceof Class ? (Class) o : o.getClass();
}

static Class _getClass(Object realm, String name) {
  try {
    return classLoaderForObject(realm).loadClass(classNameToVM(name));
  } catch (ClassNotFoundException e) {
    return null; // could optimize this
  }
}


static <A, B> B syncMapGet2(Map<A, B> map, A a) {
  if (map == null) return null;
  synchronized(collectionMutex(map)) {
    return map.get(a);
  }
}

static <A, B> B syncMapGet2(A a, Map<A, B> map) {
  return syncMapGet2(map, a);
}


static boolean isSubtypeOf(Class a, Class b) {
  return a != null && b != null && b.isAssignableFrom(a); // << always hated that method, let's replace it!
}


static Set<String> reflection_classesNotToScan_value = litset(
  "jdk.internal.loader.URLClassPath"
);

static Set<String> reflection_classesNotToScan() {
  return reflection_classesNotToScan_value;
}


static boolean endsWithLetterOrDigit(String s) {
  return s != null && s.length() > 0 && Character.isLetterOrDigit(s.charAt(s.length()-1));
}








//sbool ping_actions_shareable = true;
static volatile boolean ping_pauseAll = false;
static int ping_sleep = 100; // poll pauseAll flag every 100
static volatile boolean ping_anyActions = false;
static Map<Thread, Object> ping_actions = newWeakHashMap();
static ThreadLocal<Boolean> ping_isCleanUpThread = new ThreadLocal();

// always returns true
static boolean ping() {
  //ifdef useNewPing
  newPing();
  //endifdef
  if (ping_pauseAll || ping_anyActions) ping_impl(true /* XXX */);
  //ifndef LeanMode ping_impl(); endifndef
  return true;
}

// returns true when it slept
static boolean ping_impl(boolean okInCleanUp) { try {
  if (ping_pauseAll && !isAWTThread()) {
    do
      Thread.sleep(ping_sleep);
    while (ping_pauseAll);
    return true;
  }
  
  if (ping_anyActions) { // don't allow sharing ping_actions
    if (!okInCleanUp && !isTrue(ping_isCleanUpThread.get()))
      failIfUnlicensed();
    Object action = null;
    synchronized(ping_actions) {
      if (!ping_actions.isEmpty()) {
        action = ping_actions.get(currentThread());
        if (action instanceof Runnable)
          ping_actions.remove(currentThread());
        if (ping_actions.isEmpty()) ping_anyActions = false;
      }
    }
    
    if (action instanceof Runnable)
      ((Runnable) action).run();
    else if (eq(action, "cancelled"))
      throw fail("Thread cancelled.");
  }

  return false;
} catch (Exception __e) { throw rethrow(__e); } }




static <A> int indexOf(List<A> l, A a, int startIndex) {
  if (l == null) return -1;
  int n = l(l);
  for (int i = startIndex; i < n; i++)
    if (eq(l.get(i), a))
      return i;
  return -1;
}

static <A> int indexOf(List<A> l, int startIndex, A a) {
  return indexOf(l, a, startIndex);
}

static <A> int indexOf(List<A> l, A a) {
  if (l == null) return -1;
  return l.indexOf(a);
}

static int indexOf(String a, String b) {
  return a == null || b == null ? -1 : a.indexOf(b);
}

static int indexOf(String a, String b, int i) {
  return a == null || b == null ? -1 : a.indexOf(b, i);
}

static int indexOf(String a, char b) {
  return a == null ? -1 : a.indexOf(b);
}

static int indexOf(String a, int i, char b) {
  return indexOf(a, b, i);
}

static int indexOf(String a, char b, int i) {
  return a == null ? -1 : a.indexOf(b, i);
}

static int indexOf(String a, int i, String b) {
  return a == null || b == null ? -1 : a.indexOf(b, i);
}

static <A> int indexOf(A[] x, A a) {
  int n = l(x);
  for (int i = 0; i < n; i++)
    if (eq(x[i], a))
      return i;
  return -1;
}

static <A> int indexOf(Iterable<A> l, A a) {
  if (l == null) return -1;
  int i = 0;
  for (A x : l) {
    if (eq(x, a))
      return i;
    i++;
  }
  return -1;
}



static void rotateStringBuffer(StringBuffer buf, int max) { try {
  if (buf == null) return;
  synchronized(buf) {
    if (buf.length() <= max) return;
    
    try {
      int newLength = max/2;
      int ofs = buf.length()-newLength;
      String newString = buf.substring(ofs);
      buf.setLength(0);
      buf.append("[...] ").append(newString);
    } catch (Exception e) {
      buf.setLength(0);
    }
    buf.trimToSize();
  }
} catch (Exception __e) { throw rethrow(__e); } }


static void rotateStringBuilder(StringBuilder buf, int max) { try {
  if (buf == null) return;
  synchronized(buf) {
    if (buf.length() <= max) return;
    
    try {
      int newLength = max/2;
      int ofs = buf.length()-newLength;
      String newString = buf.substring(ofs);
      buf.setLength(0);
      buf.append("[...] ").append(newString);
    } catch (Exception e) {
      buf.setLength(0);
    }
    buf.trimToSize();
  }
} catch (Exception __e) { throw rethrow(__e); } }


static Throwable getExceptionCause(Throwable e) {
  Throwable c = e.getCause();
  return c != null ? c : e;
}


static String joinWithSpace(Iterable c) {
  return join(" ", c);
}

static String joinWithSpace(String... c) {
  return join(" ", c);
}



static boolean isInstanceX(Class type, Object arg) {
  if (type == boolean.class) return arg instanceof Boolean;
  if (type == int.class) return arg instanceof Integer;
  if (type == long.class) return arg instanceof Long;
  if (type == float.class) return arg instanceof Float;
  if (type == short.class) return arg instanceof Short;
  if (type == char.class) return arg instanceof Character;
  if (type == byte.class) return arg instanceof Byte;
  if (type == double.class) return arg instanceof Double;
  return type.isInstance(arg);
}


static boolean methodIsStatic(Method m) {
  return (m.getModifiers() & Modifier.STATIC) != 0;
}


static boolean argumentCompatibleWithType(Object arg, Class type) {
  return arg == null ? !type.isPrimitive() : isInstanceX(type, arg);
}


static void arraycopy(Object[] a, Object[] b) {
  if (a != null && b != null)
    arraycopy(a, 0, b, 0, Math.min(a.length, b.length));
}

static void arraycopy(Object src, int srcPos, int destPos, int n) { arraycopy(src, srcPos, src, destPos, n); }
static void arraycopy(Object src, int srcPos, Object dest, int destPos, int n) {
  if (n != 0)
    System.arraycopy(src, srcPos, dest, destPos, n);
}


static <A> A[] arrayOfType(Class<A> type, int n) {
  return makeArray(type, n);
}

static <A> A[] arrayOfType(int n, Class<A> type) {
  return arrayOfType(type, n);
}


public static <A> String join(String glue, Iterable<A> strings) {
  if (strings == null) return "";
  if (strings instanceof Collection) {
    if (((Collection) strings).size() == 1) return str(first((Collection) strings));
  }
  StringBuilder buf = new StringBuilder();
  Iterator<A> i = strings.iterator();
  if (i.hasNext()) {
    buf.append(i.next());
    while (i.hasNext())
      buf.append(glue).append(i.next());
  }
  return buf.toString();
}

public static String join(String glue, String... strings) {
  return join(glue, Arrays.asList(strings));
}

public static String join(String glue, Object... strings) {
  return join(glue, Arrays.asList(strings));
}

static <A> String join(Iterable<A> strings) {
  return join("", strings);
}

static <A> String join(Iterable<A> strings, String glue) {
  return join(glue, strings);
}

public static String join(String[] strings) {
  return join("", strings);
}


static String join(String glue, Pair p) {
  return p == null ? "" : str(p.a) + glue + str(p.b);
}



static List<String> getClassNames(Collection l) {
  List<String> out = new ArrayList();
  if (l != null) for (Object o : l)
    out.add(o == null ? null : getClassName(o));
  return out;
}


static boolean isTrueOrYes(Object o) {
  return isTrueOpt(o) || o instanceof String && (eqicOneOf(((String) o), "1", "t", "true") || isYes(((String) o)));
}


static <A, B> LinkedHashMap<A, B> asLinkedHashMap(Map<A, B> map) {
  if (map instanceof LinkedHashMap) return (LinkedHashMap) map;
  LinkedHashMap<A, B> m = new LinkedHashMap();
  if (map != null) synchronized(collectionMutex(map)) {
    m.putAll(map);
  }
  return m;
}


static void setDynObjectValue(DynamicObject o, String field, Object value) {
  dynamicObject_setRawFieldValue(o, field, value);
}


static void metaMapPut(IMeta o, Object key, Object value) {
  { if (o != null) o.metaPut(key, value); }
}

static void metaMapPut(Object o, Object key, Object value) {
  var meta = initIMeta(o);
  { if (meta != null) meta.metaPut(key, value); }
}


static boolean empty(Collection c) { return c == null || c.isEmpty(); }
static boolean empty(Iterable c) { return c == null || !c.iterator().hasNext(); }
static boolean empty(CharSequence s) { return s == null || s.length() == 0; }
static boolean empty(Map map) { return map == null || map.isEmpty(); }
static boolean empty(Object[] o) { return o == null || o.length == 0; }
static boolean empty(BitSet bs) { return bs == null || bs.isEmpty(); }


static boolean empty(Object o) {
  if (o instanceof Collection) return empty((Collection) o);
  if (o instanceof String) return empty((String) o);
  if (o instanceof Map) return empty((Map) o);
  if (o instanceof Object[]) return empty((Object[]) o);
  if (o instanceof byte[]) return empty((byte[]) o);
  if (o == null) return true;
  throw fail("unknown type for 'empty': " + getType(o));
}


static boolean empty(Iterator i) { return i == null || !i.hasNext(); }

static boolean empty(double[] a) { return a == null || a.length == 0; }
static boolean empty(float[] a) { return a == null || a.length == 0; }
static boolean empty(int[] a) { return a == null || a.length == 0; }
static boolean empty(long[] a) { return a == null || a.length == 0; }
static boolean empty(byte[] a) { return a == null || a.length == 0; }
static boolean empty(short[] a) { return a == null || a.length == 0; }


static boolean empty(MultiSet ms) { return ms == null || ms.isEmpty(); }



static boolean empty(MultiMap mm) { return mm == null || mm.isEmpty(); }


static boolean empty(File f) { return getFileSize(f) == 0; }


static boolean empty(IntRange r) { return r == null || r.empty(); }









static boolean empty(Rect r) { return !(r != null && r.w != 0 && r.h != 0); }





static boolean empty(AppendableChain c) { return c == null; }



static Runnable toRunnable(final Object o) {
  if (o instanceof Runnable) return (Runnable) o;
  
  if (o instanceof String) throw fail("callF_legacy");
  
  return new Runnable() {  public void run() { try {  callF(o) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "callF(o)"; }};
}


static List<VF1<Map>> _threadInheritInfo_retrievers = synchroList();

static void _threadInheritInfo(Object info) {
  if (info == null) return;
  pcallFAll(_threadInheritInfo_retrievers, (Map) info);
}


static Object pcallF_minimalExceptionHandling(Object f, Object... args) {
  try {
    return callFunction(f, args);
  } catch (Throwable e) {
    System.out.println(getStackTrace(e));
    _storeException(e);
  }
  return null;
}


static Set vm_generalIdentityHashSet(Object name) {
  synchronized(vm_generalMap()) {
    Set set =  (Set) (vm_generalMap_get(name));
    if (set == null)
      vm_generalMap_put(name, set = syncIdentityHashSet());
    return set;
  }
}



static Map vm_generalHashMap(Object name) {
  synchronized(vm_generalMap()) {
    Map m =  (Map) (vm_generalMap_get(name));
    if (m == null)
      vm_generalMap_put(name, m = syncHashMap());
    return m;
  }
}



static int cmp(Number a, Number b) {
  return a == null ? b == null ? 0 : -1 : cmp(a.doubleValue(), b.doubleValue());
}

static int cmp(double a, double b) {
  return a < b ? -1 : a == b ? 0 : 1;
}

static int cmp(int a, int b) {
  return a < b ? -1 : a == b ? 0 : 1;
}

static int cmp(long a, long b) {
  return a < b ? -1 : a == b ? 0 : 1;
}

static int cmp(Object a, Object b) {
  if (a == null) return b == null ? 0 : -1;
  if (b == null) return 1;
  return ((Comparable) a).compareTo(b);
}


static PersistableThrowable persistableThrowable(Throwable e) {
  return e == null ? null : new PersistableThrowable(e);
}


static boolean startsWithOneOf(String s, String... l) {
  for (String x : l) if (startsWith(s, x)) return true; return false;
}

static boolean startsWithOneOf(String s, Matches m, String... l) {
  for (String x : l) if (startsWith(s, x, m)) return true; return false;
}


static boolean isAGIBlueDomain(String domain) {
  return domainIsUnder(domain, theAGIBlueDomain());
}


static String hostNameFromURL(String url) { try {
  return empty(url) ? null : new URL(url).getHost();
} catch (Exception __e) { throw rethrow(__e); } }


static boolean newButton_autoToolTip = true;

// action can be Runnable or a function name
static JButton newButton(final String text, final Object action) {
  return swing(new F0<JButton>() { public JButton get() { try { 
    String text2 = dropPrefix("[disabled] ", text);
    JButton btn = basicJButton(text2);
    if (l(text2) < l(text)) btn.setEnabled(false);
    if (newButton_autoToolTip) {
      btn.setToolTipText(btn.getText());
      //onChangeAndNow(btn, r { btn.setToolTipText(btn.getText()) });
    }
    // submitButtonOnEnter(btn); // test this first
    if (action != null)
      btn.addActionListener(actionListener(action, btn));
    return btn;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "S text2 = dropPrefix(\"[disabled] \", text);\r\n    JButton btn = basicJButton(te..."; }});
}


static <A> A swingNu(final Class<A> c, final Object... args) {
  return swingConstruct(c, args);
}


static void lock(Lock lock) { try {
  ping();
  if (lock == null) return;
  try {
    vmBus_send("locking", lock, "thread" , currentThread());
    lock.lockInterruptibly();
    vmBus_send("locked", lock, "thread" , currentThread());
  } catch (InterruptedException e) {
    Object reason = vm_threadInterruptionReasonsMap().get(currentThread());
    print("Locking interrupted! Reason: " + strOr(reason, "Unknown"));
    printStackTrace(e);
    rethrow(e);
  }
  // NO call to ping here! Make sure lock is always released.
} catch (Exception __e) { throw rethrow(__e); } }

static void lock(Lock lock, String msg) {
  print("Locking: " + msg);
  lock(lock);
}

static void lock(Lock lock, String msg, long timeout) {
  print("Locking: " + msg);
  lockOrFail(lock, timeout);
}

static ReentrantLock lock() {
  return fairLock();
}


static String fsI(String id) {
  return formatSnippetID(id);
}

static String fsI(long id) {
  return formatSnippetID(id);
}


static File loadBinarySnippet(String snippetID) {
  
  IResourceLoader rl = vm_getResourceLoader();
  if (rl != null)
    return rl.loadLibrary(snippetID);
  
  
  return loadBinarySnippet_noResourceLoader(snippetID);
}
  
static File loadBinarySnippet_noResourceLoader(String snippetID) { try {
  long id = parseSnippetID(snippetID);
  if (isImageServerSnippet(id)) return loadImageAsFile(snippetID);
  File f = DiskSnippetCache_getLibrary(id);
  if (fileSize(f) == 0)
    f = loadDataSnippetToFile_noResourceLoader(snippetID);
  return f;
} catch (Exception __e) { throw rethrow(__e); } }







static boolean loadBufferedImageFixingGIFs_debug = false;
static ThreadLocal<Var<byte[]>> loadBufferedImageFixingGIFs_output = new ThreadLocal();

static Image loadBufferedImageFixingGIFs(File file) { try {
  if (!file.exists()) return null;

  // Load anything but GIF the normal way
  if (!isGIF(file))
    return ImageIO.read(file);
    
  if (loadBufferedImageFixingGIFs_debug) print("loadBufferedImageFixingGIFs" + ": checking gif");

  // Get GIF reader
  ImageReader reader = ImageIO.getImageReadersByFormatName("gif").next();
  // Give it the stream to decode from
  reader.setInput(ImageIO.createImageInputStream(file));

  int numImages = reader.getNumImages(true);

  // Get 'metaFormatName'. Need first frame for that.
  IIOMetadata imageMetaData = reader.getImageMetadata(0);
  String metaFormatName = imageMetaData.getNativeMetadataFormatName();

  // Find out if GIF is bugged
  boolean foundBug = false;
  for (int i = 0; i < numImages && !foundBug; i++) {
      // Get metadata
      IIOMetadataNode root = (IIOMetadataNode)reader.getImageMetadata(i).getAsTree(metaFormatName);

      // Find GraphicControlExtension node
      int nNodes = root.getLength();
      for (int j = 0; j < nNodes; j++) {
          org.w3c.dom.Node node = root.item(j);
          if (node.getNodeName().equalsIgnoreCase("GraphicControlExtension")) {
              // Get delay value
              String delay = ((IIOMetadataNode)node).getAttribute("delayTime");

              // Check if delay is bugged
              if (Integer.parseInt(delay) == 0) {
                  foundBug = true;
              }

              break;
          }
      }
  }

  if (loadBufferedImageFixingGIFs_debug) print("loadBufferedImageFixingGIFs" + ": " + f2s(file) + " foundBug=" + foundBug);
  
  // Load non-bugged GIF the normal way
  Image image;
  if (!foundBug) {
    image = Toolkit.getDefaultToolkit().createImage(f2s(file));
  } else {
    // Prepare streams for image encoding
    ByteArrayOutputStream baoStream = new ByteArrayOutputStream();
    {
       ImageOutputStream ios = ImageIO.createImageOutputStream(baoStream); try {
      // Get GIF writer that's compatible with reader
      ImageWriter writer = ImageIO.getImageWriter(reader);
      // Give it the stream to encode to
      writer.setOutput(ios);

      writer.prepareWriteSequence(null);

      for (int i = 0; i < numImages; i++) {
          // Get input image
          BufferedImage frameIn = reader.read(i);

          // Get input metadata
          IIOMetadataNode root = (IIOMetadataNode)reader.getImageMetadata(i).getAsTree(metaFormatName);

          // Find GraphicControlExtension node
          int nNodes = root.getLength();
          for (int j = 0; j < nNodes; j++) {
              org.w3c.dom.Node node = root.item(j);
              if (node.getNodeName().equalsIgnoreCase("GraphicControlExtension")) {
                  // Get delay value
                  String delay = ((IIOMetadataNode)node).getAttribute("delayTime");

                  // Check if delay is bugged
                  if (Integer.parseInt(delay) == 0) {
                      // Overwrite with a valid delay value
                      ((IIOMetadataNode)node).setAttribute("delayTime", "10");
                  }

                  break;
              }
          }

          // Create output metadata
          IIOMetadata metadata = writer.getDefaultImageMetadata(new ImageTypeSpecifier(frameIn), null);
          // Copy metadata to output metadata
          metadata.setFromTree(metadata.getNativeMetadataFormatName(), root);

          // Create output image
          IIOImage frameOut = new IIOImage(frameIn, null, metadata);

          // Encode output image
          writer.writeToSequence(frameOut, writer.getDefaultWriteParam());
      }

      writer.endWriteSequence();
    } finally { _close(ios); }}

    // Create image using encoded data
    byte[] data = baoStream.toByteArray();
    setVar(loadBufferedImageFixingGIFs_output.get(), data);
    if (loadBufferedImageFixingGIFs_debug) print("Data size: " + l(data));
    image = Toolkit.getDefaultToolkit().createImage(data);
  }

  return image;
} catch (Exception __e) { throw rethrow(__e); } }




static void unlock(Lock lock, String msg) {
  if (lock == null) return;
  lock.unlock();
  vmBus_send("unlocked", lock, "thread" , currentThread());
  print("Unlocked: " + msg); // print afterwards to make sure the lock is always unlocked
}

static void unlock(Lock lock) {
  if (lock == null) return;
  lock.unlock();
  vmBus_send("unlocked", lock, "thread" , currentThread());
}


static <A extends JComponent> A setToolTipText(final A c, final Object toolTip) {
  if (c == null) return null;
  { swing(() -> { 
    String s = nullIfEmpty(str(toolTip));
    if (neq(s, c.getToolTipText()))
      c.setToolTipText(s);
  }); }
  return c;
}

static <A extends JComponent> A setToolTipText(Object toolTip, A c) {
  return setToolTipText(c, toolTip);
}


static void setTrayIconToolTip(TrayIcon trayIcon, String toolTip) {
  if (trayIcon != null) trayIcon.setToolTip(toolTip);
}


static int iround(double d) {
  return (int) Math.round(d);
}


static int iround(Number n) {
  return iround(toDouble(n));
}



static boolean loadBufferedImage_useImageCache = true;

static BufferedImage loadBufferedImage(String snippetIDOrURLOrFile) { try {
  ping();
  if (snippetIDOrURLOrFile == null) return null;
  if (isURL(snippetIDOrURLOrFile))
    return imageIO_readURL(snippetIDOrURLOrFile);

  if (isSnippetID(snippetIDOrURLOrFile)) {
    String snippetID = "" + parseSnippetID(snippetIDOrURLOrFile);
    
    
    IResourceLoader rl = vm_getResourceLoader();
    if (rl != null)
      return loadBufferedImage(rl.loadLibrary(snippetID));
    
    
    File dir = imageSnippetsCacheDir();
    if (loadBufferedImage_useImageCache) {
      dir.mkdirs();
      File file = new File(dir, snippetID + ".png");
      if (file.exists() && file.length() != 0)
        try {
          return ImageIO.read(file);
        } catch (Throwable e) {
          e.printStackTrace();
          // fall back to loading from sourceforge
        }
    }
  
    String imageURL = snippetImageURL_http(snippetID);
    print("Loading image: " + imageURL);
    BufferedImage image = imageIO_readURL(imageURL);
  
    if (loadBufferedImage_useImageCache) {
      File tempFile = new File(dir, snippetID + ".tmp." + System.currentTimeMillis());
      ImageIO.write(image, "png", tempFile);
      tempFile.renameTo(new File(dir, snippetID + ".png"));
      //Log.info("Cached image.");
    }
  
    //Log.info("Loaded image.");
    return image;
  } else
    return loadBufferedImage(new File(snippetIDOrURLOrFile));
} catch (Exception __e) { throw rethrow(__e); } }

static BufferedImage loadBufferedImage(File file) {
  return loadBufferedImageFile(file);
}


static boolean activateFrame(final Component c, Object... __) {
  return swing(new F0<Boolean>() { public Boolean get() { try { 
    Frame f = getAWTFrame(c);
    if (f == null) return false;
    if (!f.isVisible()) f.setVisible(true);
    boolean windowsHack = optPar("windowsHack", __, true);
    boolean iconified = f.getState() == Frame.ICONIFIED;
    boolean maximize = boolPar("maximize", __);
    if (iconified)
      f.setState(maximize ? Frame.MAXIMIZED_BOTH : Frame.NORMAL);
      
    // My glorious Windows hack
    // See: https://stackoverflow.com/questions/309023/how-to-bring-a-window-to-the-front
    if (windowsHack && !iconified && isWindows()) {
      boolean fullscreen = f.getExtendedState() == Frame.MAXIMIZED_BOTH;
      f.setExtendedState(JFrame.ICONIFIED);
      f.setExtendedState(fullscreen ? JFrame.MAXIMIZED_BOTH : JFrame.NORMAL);
    }
    
    f.toFront();
    return true;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "Frame f = getAWTFrame(c);\r\n    if (f == null) false;\r\n    if (!f.isVisible())..."; }});
}


static Frame getAWTFrame(final Object _o) {
  return swing(new F0<Frame>() { public Frame get() { try { 
    Object o = _o;
    /*
    ifdef HaveProcessing
      if (o instanceof PApplet) o = ((PApplet) o).getSurface();
    endifdef
    */
    if (o instanceof ButtonGroup) o = first(buttonsInGroup((ButtonGroup) o));
    if (!(o instanceof Component)) return null;
    Component c = (Component) o;
    while (c != null) {
      if (c instanceof Frame) return (Frame) c;
      c = c.getParent();
    }
    return null;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "O o = _o;\r\n    /*\r\n    ifdef HaveProcessing\r\n      if (o instanceof PApplet) ..."; }});
}


static String strUnnull(Object o) {
  return o == null ? "" : str(o);
}


static String jlabel_textAsHTML_center_ifNeeded(String text) {
  if (swic(text, "<html>") && ewic(text, "</html>")) return text;
  if (!containsNewLines(text)) return text;
  return jlabel_textAsHTML_center(text);
}


static boolean neq(Object a, Object b) {
  return !eq(a, b);
}


static <A extends JComponent> A withTypeWriterFont(A c) {
  return setFont(c, typeWriterFont());
}


static JTextArea jTextArea() {
  return jTextArea("");
}

static JTextArea jTextArea(final String text) {
  return jTextAreaWithUndo(text);
}


static String programTitle() {
  return getProgramName();
}


static <A> A optParam(ThreadLocal<A> tl, A defaultValue) {
  return optPar(tl, defaultValue);
}

static <A> A optParam(ThreadLocal<A> tl) {
  return optPar(tl);
}

static Object optParam(String name, Map params) {
  return mapGet(params, name);
}

// now also takes a map as single array entry
static <A> A optParam(Object[] opt, String name, A defaultValue) {
  int n = l(opt);
  if (n == 1 && opt[0] instanceof Map) {
    Map map =  (Map) (opt[0]);
    return map.containsKey(name) ? (A) map.get(name) : defaultValue;
  }
  if (!even(l(opt))) throw fail("Odd parameter length");
  for (int i = 0; i < l(opt); i += 2)
    if (eq(opt[i], name))
      return (A) opt[i+1];
  return defaultValue;
}

static Object optParam(Object[] opt, String name) {
  return optParam(opt, name, null);
}

static Object optParam(String name, Object[] params) {
  return optParam(params, name);
}


static JFrame getFrame(final Object _o) {
  return swing(new F0<JFrame>() { public JFrame get() { try { 
    Object o = _o;
    if (o instanceof ButtonGroup) o = first(buttonsInGroup((ButtonGroup) o));
    if (!(o instanceof Component)) return null;
    Component c = (Component) o;
    while (c != null) {
      if (c instanceof JFrame) return (JFrame) c;
      c = c.getParent();
    }
    return null;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "O o = _o;\r\n    if (o instanceof ButtonGroup) o = first(buttonsInGroup((Button..."; }});
}


static JFrame setFrameIconLater(Component c, final String imageID) {
  final JFrame frame = getFrame(c);
  if (frame != null)
    startThread("Loading Icon", new Runnable() {  public void run() { try {    
      final Image i = imageIcon(or2(imageID, "#1005557")).getImage();
      swingLater(new Runnable() {  public void run() { try { 
        frame.setIconImage(i);
      
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "frame.setIconImage(i);"; }});
    
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "final Image i = imageIcon(or2(imageID, \"#1005557\")).getImage();\r\n      swingL..."; }});
  return frame;
}


static void _initFrame(JFrame f) {
  myFrames_list.put(f, Boolean.TRUE);
  standardTitlePopupMenu(f);
}


// c = Component or something implementing swing()
static JComponent wrap(Object swingable) {
  return _recordNewSwingComponent(wrap_2(swingable));
}

static JComponent wrap_2(Object swingable) {
  if (swingable == null) return null;
  JComponent c;
  if (swingable instanceof Component) c = componentToJComponent((Component) swingable);
  
    else if (swingable instanceof Swingable) c = componentToJComponent(((Swingable) swingable).visualize());
  
  else c = componentToJComponent((Component) callOpt(swingable, "swing"));
  if (c instanceof JTable || c instanceof JList
    || c instanceof JTextArea || c instanceof JEditorPane
    || c instanceof JTextPane || c instanceof JTree)
    return jscroll(c);
  return c == null ? jlabel(str(swingable)) : c;
}


static Rectangle defaultNewFrameBounds_r = new Rectangle(300, 100, 500, 400);

static Rectangle defaultNewFrameBounds() {
  return swing(new F0<Rectangle>() { public Rectangle get() { try { 
    defaultNewFrameBounds_r.translate(60, 20);
    var bounds = preferredScreenBounds();
    if (!bounds.contains(defaultNewFrameBounds_r))
      //defaultNewFrameBounds_r.setLocation(bounds.x+30+random(30), bounds.y+20+random(20));
      defaultNewFrameBounds_r.setLocation(centerX(bounds)+random_incl(-30, 30), centerY(bounds)+random_incl(-20, 20));
    return new Rectangle(defaultNewFrameBounds_r);
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "defaultNewFrameBounds_r.translate(60, 20);\r\n    var bounds = preferredScreenB..."; }});
}


static void hideConsole() {
  final JFrame frame = consoleFrame();
  if (frame != null) {
    autoVMExit();
    swingLater(new Runnable() {  public void run() { try { 
      frame.setVisible(false);
    
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "frame.setVisible(false);"; }});
  }
}


static String getProgramTitle() {
  return getProgramName();
}


static String lines(Iterable lines) { return fromLines(lines); }
static String lines(Object[] lines) { return fromLines(asList(lines)); }
static List<String> lines(String s) { return toLines(s); }

// convenience map call
static <A> String lines(Iterable<A> l, IF1<A, String> f) {
  return mapToLines(l, f);
}


static Map<String, Class> classForName_cache = synchroHashMap();

static Class classForName(String name) { return classForName(name, null); }
static Class classForName(String name, Object classFinder) {
  // first clause is when we're in class init
  if (classForName_cache == null || classFinder != null)
    return classForName_uncached(name, classFinder);
  Class c = classForName_cache.get(name);
  if (c == null)
    classForName_cache.put(name, c = classForName_uncached(name, null));
  return c;
}

static Class classForName_uncached(String name, Object classFinder) { try {
  if (classFinder != null) return (Class) callF(classFinder, name);
  return Class.forName(name);
} catch (Exception __e) { throw rethrow(__e); } }


static Map<Class, Constructor> nuObjectWithoutArguments_cache = newDangerousWeakHashMap();

static Object nuObjectWithoutArguments(String className) { try {
  return nuObjectWithoutArguments(classForName(className));
} catch (Exception __e) { throw rethrow(__e); } }

static <A> A nuObjectWithoutArguments(Class<A> c) { try {
  if (nuObjectWithoutArguments_cache == null)
    // in class init
    return (A) nuObjectWithoutArguments_findConstructor(c).newInstance();
    
  Constructor m = nuObjectWithoutArguments_cache.get(c);
  if (m == null)
    nuObjectWithoutArguments_cache.put(c, m = nuObjectWithoutArguments_findConstructor(c));
  return (A) m.newInstance();
} catch (Exception __e) { throw rethrow(__e); } }

static Constructor nuObjectWithoutArguments_findConstructor(Class c) {
  for (Constructor m : c.getDeclaredConstructors())
    if (empty(m.getParameterTypes())) {
      makeAccessible(m);
      return m;
    }
  throw fail("No default constructor found in " + c.getName());
}



static List<Class> getClasses(Object[] array) {
  List<Class> l = emptyList(l(array));
  for (Object o : array) l.add(_getClass(o));
  return l;
}




static Map<JFrame, Boolean> myFrames_list = weakHashMap();

static List<JFrame> myFrames() {
  return swing(new F0<List<JFrame>>() { public List<JFrame> get() { try {  return keysList(myFrames_list);  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return keysList(myFrames_list);"; }});
}



static <A> AutoCloseable tempSetThreadLocal(final ThreadLocal<A> tl, A a) {
  if (tl == null) return null;
  final A prev = setThreadLocal(tl, a);
  return new AutoCloseable() { public String toString() { return "tl.set(prev);"; } public void close() throws Exception { tl.set(prev); }};
}


static HashMap<String, List<Method>> callMC_cache = new HashMap();
static String callMC_key;
static Method callMC_value;

// varargs assignment fixer for a single string array argument
static Object callMC(String method, String[] arg) {
  return callMC(method, new Object[] {arg});
}

static Object callMC(String method, Object... args) { try {
  Method me;
  if (callMC_cache == null) callMC_cache = new HashMap(); // initializer time workaround
  synchronized(callMC_cache) {
    me = method == callMC_key ? callMC_value : null;
  }
  if (me != null) try {
    return invokeMethod(me, null, args);
  } catch (IllegalArgumentException e) {
    throw new RuntimeException("Can't call " + me + " with arguments " + classNames(args), e);
  }

  List<Method> m;
  synchronized(callMC_cache) {
    m = callMC_cache.get(method);
  }
  if (m == null) {
    if (callMC_cache.isEmpty()) {
      callMC_makeCache();
      m = callMC_cache.get(method);
    }
    if (m == null) throw fail("Method named " + method + " not found in main");
  }
  int n = m.size();
  if (n == 1) {
    me = m.get(0);
    synchronized(callMC_cache) {
      callMC_key = method;
      callMC_value = me;
    }
    try {
      return invokeMethod(me, null, args);
    } catch (IllegalArgumentException e) {
      throw new RuntimeException("Can't call " + me + " with arguments " + classNames(args), e);
    }
  }
  for (int i = 0; i < n; i++) {
    me = m.get(i);
    if (call_checkArgs(me, args, false))
      return invokeMethod(me, null, args);
  }
  throw fail("No method called " + method + " with arguments (" + joinWithComma(getClasses(args)) + ") found in main");
} catch (Exception __e) { throw rethrow(__e); } }

static void callMC_makeCache() {
  synchronized(callMC_cache) {
    callMC_cache.clear();
    Class _c = (Class) mc(), c = _c;
    while (c != null) {
      for (Method m : c.getDeclaredMethods())
        if ((m.getModifiers() & java.lang.reflect.Modifier.STATIC) != 0) {
          makeAccessible(m);
          multiMapPut(callMC_cache, m.getName(), m);
        }
      c = c.getSuperclass();
    }
  }
}


static <A, B> Pair<A, B> pair(A a, B b) {
  return new Pair(a, b);
}

static <A> Pair<A, A> pair(A a) {
  return new Pair(a, a);
}


static <A> A assertNotNull(A a) {
  assertTrue(a != null);
  return a;
}

static <A> A assertNotNull(String msg, A a) {
  assertTrue(msg, a != null);
  return a;
}




static Map synchroHashMap() {
  return synchronizedMap(new HashMap());
}



static ClassLoader classLoaderForObject(Object o) {
  if (o instanceof ClassLoader) return ((ClassLoader) o);
  if (o == null) return null;
  return _getClass(o).getClassLoader();
}


// Note: This is actually broken. Inner classes must stay with a $ separator
static String classNameToVM(String name) {
  return name.replace(".", "$");
}


static <A> HashSet<A> litset(A... items) {
  return lithashset(items);
}


static <A, B> Map<A, B> newWeakHashMap() {
  return _registerWeakMap(synchroMap(new WeakHashMap()));
}


static void newPing() {
  var tl = newPing_actionTL();
  Runnable action = tl == null ? null : tl.get();
  { if (action != null) action.run(); }
}


static boolean isTrue(Object o) {
  if (o instanceof Boolean)
    return ((Boolean) o).booleanValue();
  if (o == null) return false;
  if (o instanceof ThreadLocal) // TODO: remove this
    return isTrue(((ThreadLocal) o).get());
  throw fail(getClassName(o));
}

static boolean isTrue(Boolean b) {
  return b != null && b.booleanValue();
}


static void failIfUnlicensed() {
  assertTrue("license off", licensed());
}


static Thread currentThread() {
  return Thread.currentThread();
}


static <A> A[] makeArray(Class<A> type, int n) {
  return (A[]) Array.newInstance(type, n);
}



static Object first(Object list) {
  return first((Iterable) list);
}


static <A> A first(List<A> list) {
  return empty(list) ? null : list.get(0);
}

static <A> A first(A[] bla) {
  return bla == null || bla.length == 0 ? null : bla[0];
}

static <A, B> Pair<A, B> first(Map<A, B> map) {
  return mapEntryToPair(first(entrySet(map)));
}

static <A, B> Pair<A, B> first(MultiMap<A, B> mm) {
  if (mm == null) return null;
  var e = first(mm.data.entrySet());
  if (e == null) return null;
  return pair(e.getKey(), first(e.getValue()));
}


static <A> A first(IterableIterator<A> i) {
  return first((Iterator<A>) i);
}


static <A> A first(Iterator<A> i) {
  return i == null || !i.hasNext() ? null : i.next();
}

static <A> A first(Iterable<A> i) {
  if (i == null) return null;
  Iterator<A> it = i.iterator();
  return it.hasNext() ? it.next() : null;
}

static Character first(String s) { return empty(s) ? null : s.charAt(0); }
static Character first(CharSequence s) { return empty(s) ? null : s.charAt(0); }


static <A, B> A first(Pair<A, B> p) {
  return p == null ? null : p.a;
}



static <A, B, C> A first(T3<A, B, C> t) {
  return t == null ? null : t.a;
}


static Byte first(byte[] l) { 
  return empty(l) ? null : l[0];
}





static <A> A first(A[] l, IF1<A, Boolean> pred) {
  return firstThat(l, pred);
}

static <A> A first(Iterable<A> l, IF1<A, Boolean> pred) {
  return firstThat(l, pred);
}

static <A> A first(IF1<A, Boolean> pred, Iterable<A> l) {
  return firstThat(pred, l);
}


static <A> A first(AppendableChain<A> a) {
  return a == null ? null : a.element;
}




static boolean isTrueOpt(Object o) {
  if (o instanceof Boolean)
    return ((Boolean) o).booleanValue();
  return false;
}

static boolean isTrueOpt(String field, Object o) {
  return isTrueOpt(getOpt(field, o));
}


static boolean eqicOneOf(String s, String... l) {
  for (String x : l) if (eqic(s, x)) return true; return false;
}


static List<String> isYes_yesses = litlist("y", "yes", "yeah", "y", "yup", "yo", "corect", "sure", "ok", "afirmative"); // << collapsed words, so "corect" means "correct"

static boolean isYes(String s) {
  return isYes_yesses.contains(collapseWord(toLowerCase(firstWord2(s))));
}


static void dynamicObject_setRawFieldValue(DynamicObject o, Object key, Object value) {
  if (o == null) return;
  
  // double sync, but should be OK here because of locking order o > o.fieldValues
  synchronized(o) {
    o.fieldValues = syncMapPut2_createLinkedHashMap((LinkedHashMap) o.fieldValues, key, value);
  }
}


static IMeta initIMeta(Object o) {
  if (o == null) return null;
  if (o instanceof IMeta) return ((IMeta) o);
  if (o instanceof JComponent) return initMetaOfJComponent((JComponent) o);
  if (o instanceof BufferedImage) return optCast(IMeta.class, ((BufferedImage) o).getProperty("meta"));
  return null;
}


static String getType(Object o) {
  return getClassName(o);
}


static long getFileSize(String path) {
  return path == null ? 0 : new File(path).length();
}

static long getFileSize(File f) {
  return f == null ? 0 : f.length();
}


static Throwable _storeException_value;

static void _storeException(Throwable e) {
  _storeException_value = e;
}


static Map vm_generalMap_map;

static Map vm_generalMap() {
  if (vm_generalMap_map == null)
    
    
    
    vm_generalMap_map = (Map) get(javax(), "generalMap");
    
    
  return vm_generalMap_map;
}


static Object vm_generalMap_get(Object key) {
  return vm_generalMap().get(key);
}


static Object vm_generalMap_put(Object key, Object value) {
  return mapPutOrRemove(vm_generalMap(), key, value);
}


static <A> Set<A> syncIdentityHashSet() {
  return (Set) synchronizedSet(identityHashSet());
}


static Map syncHashMap() {
  return synchroHashMap();
}


static boolean startsWith(String a, String b) {
  return a != null && a.startsWith(unnull(b));
}

static boolean startsWith(String a, char c) {
  return nemptyString(a) && a.charAt(0) == c;
}


  static boolean startsWith(String a, String b, Matches m) {
    if (!startsWith(a, b)) return false;
    if (m != null) m.m = new String[] {substring(a, strL(b))};
    return true;
  }


static boolean startsWith(List a, List b) {
  if (a == null || listL(b) > listL(a)) return false;
  for (int i = 0; i < listL(b); i++)
    if (neq(a.get(i), b.get(i)))
      return false;
  return true;
}




static boolean domainIsUnder(String domain, String mainDomain) {
  return eqic(domain, mainDomain) || ewic(domain, "." + mainDomain);
}


static String theAGIBlueDomain() {
  return "agi.blue";
}


static String dropPrefix(String prefix, String s) {
  return s == null ? null : s.startsWith(prefix) ? s.substring(l(prefix)) : s;
}


static JButton basicJButton(String text) {
  return swing(() -> new JButton(text));
}


static ActionListener actionListener(final Object runnable) {
  return actionListener(runnable, null);
}

static ActionListener actionListener(final Object runnable, final Object instanceToHold) {
  if (runnable instanceof ActionListener) return (ActionListener) runnable;
  final Object info = _threadInfo();
  return new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent _evt) { try {
    _threadInheritInfo(info);
     AutoCloseable __1 = holdInstance(instanceToHold); try {
    pcallF(runnable);
  } finally { _close(__1); }} catch (Throwable __e) { messageBox(__e); }}};
}


static Map<Thread, Object> vm_threadInterruptionReasonsMap() {
  return vm_generalWeakSubMap("Thread interruption reasons");
}


static String strOr(Object o, String ifNull) {
  return o == null ? ifNull : str(o);
}


static void lockOrFail(Lock lock, long timeout) { try {
  ping();
  vmBus_send("locking", lock, "thread" , currentThread());
  if (!lock.tryLock(timeout, TimeUnit.MILLISECONDS)) {
    String s = "Couldn't acquire lock after " + timeout + " ms.";
    if (lock instanceof ReentrantLock) {
      ReentrantLock l =  (ReentrantLock) lock;
      s += " Hold count: " + l.getHoldCount() + ", owner: " + call(l, "getOwner");
    }
    throw fail(s);
  }
  vmBus_send("locked", lock, "thread" , currentThread());
  ping();
} catch (Exception __e) { throw rethrow(__e); } }


static ReentrantLock fairLock() {
  return new ReentrantLock(true);
}


static String formatSnippetID(String id) {
  return "#" + parseSnippetID(id);
}

static String formatSnippetID(long id) {
  return "#" + id;
}


static IResourceLoader vm_getResourceLoader() {
  return proxy(IResourceLoader.class, vm_generalMap_get("_officialResourceLoader"));
}


public static long parseSnippetID(String snippetID) {
  long id = Long.parseLong(shortenSnippetID(snippetID));
  if (id == 0) throw fail("0 is not a snippet ID");
  return id;
}


static boolean isImageServerSnippet(long id) {
  return id >= 1100000 && id < 1200000;
}



static File loadImageAsFile(String snippetIDOrURL) { try {
  if (isURL(snippetIDOrURL))
    throw fail("not implemented");

  if (!isSnippetID(snippetIDOrURL)) throw fail("Not a URL or snippet ID: " + snippetIDOrURL);
  String snippetID = "" + parseSnippetID(snippetIDOrURL);
  
  File file = imageSnippetCacheFile(snippetID);
  if (fileSize(file) > 0) return file;

  String imageURL = snippetImageURL_noHttps(snippetID);
  System.err.println("Loading image: " + imageURL);
  byte[] data = loadBinaryPage(imageURL);

  saveBinaryFile(file, data);
  return file;
} catch (Exception __e) { throw rethrow(__e); } }



// If you change this, also change DiskSnippetCache_fileToLibID
static File DiskSnippetCache_file(long snippetID) {
  return new File(getGlobalCache(), "data_" + snippetID + ".jar");
}
  
// Data files are immutable, use centralized cache
public static File DiskSnippetCache_getLibrary(long snippetID) throws IOException {
  File file = DiskSnippetCache_file(snippetID);
  return file.exists() ? file : null;
}

public static File DiskSnippetCache_getLibrary(String snippetID) { try {
  return DiskSnippetCache_getLibrary(psI(snippetID));
} catch (Exception __e) { throw rethrow(__e); } }

public static void DiskSnippetCache_putLibrary(long snippetID, byte[] data) throws IOException {
  saveBinaryFile(DiskSnippetCache_file(snippetID), data);
}

static byte[] loadDataSnippetImpl(String snippetID) throws IOException {
  byte[] data;
  try {
    URL url = new URL(dataSnippetLink(snippetID));
    print("Loading library: " + hideCredentials(url));
    try {
      data = loadBinaryPage(url.openConnection());
    } catch (RuntimeException e) {
      data = null;
    }
    
    if (data == null || data.length == 0) {
      url = new URL(tb_mainServer() + "/blobs/" + parseSnippetID(snippetID));
      print("Loading library: " + hideCredentials(url));
      data = loadBinaryPage(url.openConnection());
    }
    print("Bytes loaded: " + data.length);
  } catch (FileNotFoundException e) {
    throw new IOException("Binary snippet #" + snippetID + " not found or not public");
  }
  return data;
}


static long fileSize(String path) { return getFileSize(path); }
static long fileSize(File f) { return getFileSize(f); }



static File loadDataSnippetToFile(String snippetID) { try {
  
  IResourceLoader rl = vm_getResourceLoader();
  if (rl != null)
    return rl.loadLibrary(snippetID);
  
  
  return loadDataSnippetToFile_noResourceLoader(snippetID);
} catch (Exception __e) { throw rethrow(__e); } }
  
static File loadDataSnippetToFile_noResourceLoader(String snippetID) { try {
  snippetID = fsI(snippetID);
  
  File f = DiskSnippetCache_file(parseSnippetID(snippetID));
  List<URL> urlsTried = new ArrayList();
  List<Throwable> errors = new ArrayList();
  try {
    URL url = addAndReturn(urlsTried, new URL(dataSnippetLink(snippetID)));
    print("Loading library: " + hideCredentials(url));
    try {
      loadBinaryPageToFile(openConnection(url), f);
      if (fileSize(f) == 0) throw fail();
    } catch (Throwable e) {
      errors.add(e);
      url = addAndReturn(urlsTried, new URL(tb_mainServer() + "/blobs/" + psI(snippetID)));
      print(e);
      print("Trying other server: " + hideCredentials(url));
      loadBinaryPageToFile(openConnection(url), f);
      print("Got bytes: " + fileSize(f));
    }
    // TODO: check if we hit the "LOADING" message
    if (fileSize(f) == 0) throw fail();
    System.err.println("Bytes loaded: " + fileSize(f));
  } catch (Throwable e) {
    //printStackTrace(e);
    errors.add(e);
    throw fail("Binary snippet " + snippetID + " not found or not public. URLs tried: " + allToString(urlsTried) + ", errors: " + allToString(errors));
  }
  return f;
} catch (Exception __e) { throw rethrow(__e); } }


static byte[] isGIF_magic = bytesFromHex("47494638"); // Actual signature is longer, but we're lazy

static boolean isGIF(byte[] data) {
  return byteArrayStartsWith(data, isGIF_magic);
}

static boolean isGIF(File f) {
  return isGIF(loadBeginningOfBinaryFile(f, l(isGIF_magic)));
}


static String f2s(File f) {
  return f == null ? null : f.getAbsolutePath();
}

static String f2s(String s) { return f2s(newFile(s)); }


 static String f2s(java.nio.file.Path p) {
  return p == null ? null : f2s(p.toFile());
}



static void _close(AutoCloseable c) {
  if (c != null) try {
    c.close();
  } catch (Throwable e) {
    // Some classes stupidly throw an exception on double-closing
    if (c instanceof javax.imageio.stream.ImageOutputStream)
      return;
    else throw rethrow(e);
  }
}


static <A> void setVar(IVar<A> v, A value) {
  if (v != null) v.set(value);
}

static <A> IVF1<A> setVar(IVar<A> v) {
  return a -> { if (v != null) v.set(a); };
}


static String nullIfEmpty(String s) {
  return isEmpty(s) ? null : s;
}

static <A, B> Map<A, B> nullIfEmpty(Map<A, B> map) {
  return isEmpty(map) ? null : map;
}

static <A> List<A> nullIfEmpty(List<A> l) {
  return isEmpty(l) ? null : l;
}


static double toDouble(Object o) {
  if (o instanceof Number)
    return ((Number) o).doubleValue();
  if (o instanceof BigInteger)
    return ((BigInteger) o).doubleValue();
  if (o instanceof String)
    return parseDouble((String) o);
  if (o == null) return 0.0;
  throw fail(o);
}


static boolean isURL(String s) {
  return startsWithOneOf(s, "http://", "https://", "file:");
}


static BufferedImage imageIO_readURL(String url) { try {
  return ImageIO.read(new URL(url));
} catch (Exception __e) { throw rethrow(__e); } }



  public static boolean isSnippetID(String s) {
    try {
      parseSnippetID(s);
      return true;
    } catch (RuntimeException e) {
      return false;
    }
  }


static File imageSnippetsCacheDir() {
  return javaxCachesDir("Image-Snippets");
}


static String snippetImageURL_http(String snippetID) {
  return snippetImageURL_http(snippetID, "png");
}

static String snippetImageURL_http(String snippetID, String contentType) {
  return replacePrefix("https://", "http://", snippetImageURL(snippetID, contentType)).replace(":8443", ":8080");
}


static BufferedImage loadBufferedImageFile(File file) { try {
  return isFile(file) ? ImageIO.read(file) : null;
} catch (Exception __e) { throw rethrow(__e); } }


static <A> A optPar(ThreadLocal<A> tl, A defaultValue) {
  A a = tl.get();
  if (a != null) {
    tl.set(null);
    return a;
  }
  return defaultValue;
}

static <A> A optPar(ThreadLocal<A> tl) {
  return optPar(tl, null);
}

static Object optPar(Object[] params, String name) {
  return optParam(params, name);
}

static Object optPar(String name, Object[] params) {
  return optParam(params, name);
}

static Object optPar(String name, Map params) {
  return optParam(name, params);
}

static <A> A optPar(Object[] params, String name, A defaultValue) {
  return optParam(params, name, defaultValue);
}

static <A> A optPar(String name, Object[] params, A defaultValue) {
  return optParam(params, name, defaultValue);
}


static boolean boolPar(ThreadLocal<Boolean> tl) {
  return boolOptParam(tl);
}



// defaults to false
static boolean boolPar(Object[] __, String name) {
  return boolOptParam(__, name);
}

static boolean boolPar(String name, Object[] __) {
  return boolOptParam(__, name);
}

static boolean boolPar(String name, Map __) {
  return boolOptParam(name, __);
}

static boolean boolPar(String name, Object[] params, boolean defaultValue) {
  return optParam(params, name, defaultValue);
}



public static boolean isWindows() {
  return System.getProperty("os.name").contains("Windows");
}


static List<AbstractButton> buttonsInGroup(ButtonGroup g) {
  if (g == null) return ll();
  return asList(g.getElements());
}


static boolean swic(String a, String b) {
  return startsWithIgnoreCase(a, b);
}


  static boolean swic(String a, String b, Matches m) {
    if (!swic(a, b)) return false;
    m.m = new String[] {substring(a, l(b))};
    return true;
  }



static boolean ewic(String a, String b) {
  return endsWithIgnoreCase(a, b);
}


static boolean ewic(String a, String b, Matches m) {
  return endsWithIgnoreCase(a, b, m);
}



static boolean containsNewLines(String s) {
  return containsNewLine(s);
}


static String jlabel_textAsHTML_center(String text) {
  return "<html><div style=\"text-align: center;\">"
    + replace(htmlencode2(text), "\n", "<br>")
    + "</div></html>";
}


static <A extends JComponent> A setFont(final Font font, final A a) {
  if (a != null) { swing(() -> {  a.setFont(font); }); }
  return a;
}

static <A extends JComponent> A setFont(A a, Font font) {
  return setFont(font, a);
}

static <A extends JComponent> A setFont(final String fontID, float fontSize, final A a) {
  return setFont(loadFont_cached(fontID, fontSize), a);
}


static Font typeWriterFont() {
  return typeWriterFont(iround(14*getSwingFontScale()));
}
  
static Font typeWriterFont(int size) {
  return new Font("Courier", Font.PLAIN, size);
}


static JTextArea jTextAreaWithUndo() {
  return jTextAreaWithUndo("");
}

static JTextArea jTextAreaWithUndo(final String text) {
  return jenableUndoRedo(swingNu(JTextArea.class, text));
}


static String getProgramName_cache;

static String getProgramName() {
  Lock __0 = downloadLock(); lock(__0); try {
  if (getProgramName_cache == null)
    getProgramName_cache = getSnippetTitleOpt(programID());
  return getProgramName_cache;
} finally { unlock(__0); } }

static void _onLoad_getProgramName() {
  { startThread(new Runnable() {  public void run() { try {  getProgramName(); 
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "getProgramName();"; }}); }
}


static <A, B> B mapGet(Map<A, B> map, A a) {
  return map == null || a == null ? null : map.get(a);
}

static <A, B> B mapGet(A a, Map<A, B> map) {
  return map == null || a == null ? null : map.get(a);
}


static boolean even(int i) {
  return (i & 1) == 0;
}

static boolean even(long i) {
  return (i & 1) == 0;
}

static boolean even(BigInteger n) {
  return even(n.intValue());
}


static Thread startThread(Object runnable) {
  return startThread(defaultThreadName(), runnable);
}

static Thread startThread(String name, Runnable runnable) {
  runnable = wrapAsActivity(runnable);
  return startThread(newThread(runnable, name));
}

static Thread startThread(String name, Object runnable) {
  runnable = wrapAsActivity(runnable);
  return startThread(newThread(toRunnable(runnable), name));
}

static Thread startThread(Thread t) {
  
  _registerThread(t);
  
  t.start();
  return t;
}


static String or2(String a, String b) {
  return nempty(a) ? a : b;
}

static String or2(String a, String b, String c) {
  return or2(or2(a, b), c);
}


static void swingLater(long delay, final Object r) {
  javax.swing.Timer timer = new javax.swing.Timer(toInt(delay), actionListener(wrapAsActivity(r)));
  timer.setRepeats(false);
  timer.start();
}

static void swingLater(Object r) {
  SwingUtilities.invokeLater(toRunnable(r));
}



static void standardTitlePopupMenu(final JFrame frame) {
  // standard right-click behavior on titles
  if (!isSubstanceLAF()) return;
  titlePopupMenu(frame, new VF1<JPopupMenu>() { public void get(JPopupMenu menu) { try { 
    boolean alwaysOnTop = frame.isAlwaysOnTop();
    
      menu.add(jmenuItem("Restart Program", new Runnable() {  public void run() { try {  restart(); 
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "restart();"; }}));
      menu.add(jmenuItem("Duplicate Program", new Runnable() {  public void run() { try {  duplicateThisProgram(); 
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "duplicateThisProgram();"; }}));
    
    menu.add(jmenuItem("Show Console", new Runnable() {  public void run() { try {  showConsole(); 
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "showConsole();"; }}));
    menu.add(jCheckBoxMenuItem("Always On Top", alwaysOnTop, new Runnable() {  public void run() { try {  toggleAlwaysOnTop(frame) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "toggleAlwaysOnTop(frame)"; }}));
    /*ifndef standardTitlePopupMenu_noShootWindow
      { menu.add(jMenuItem("Shoot Window", r { shootWindowGUI_external(frame, 500) })); }
    endifndef*/
    //addMenuItem(menu, "Bigger fonts", r swingBiggerFonts);
    //addMenuItem(menu, "Smaller fonts", r swingSmallerFonts);
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "bool alwaysOnTop = frame.isAlwaysOnTop();\r\n    ifndef standardTitlePopupMenu_..."; }});
}


static <A extends Component> A _recordNewSwingComponent(A c) {
  if (c != null)
    callF((Object) vm_generalMap_get("newSwingComponentRegistry"), (Object) c);
  return c;
}


static JComponent componentToJComponent(Component c) {
  if (c instanceof JComponent) return (JComponent) c;
  if (c instanceof JFrame) return ((JFrame) c).getRootPane();
  if (c == null) return null;
  throw fail("boohoo " + getClassName(c));
}



static JScrollPane jscroll(final Component c) {
  return swing(new F0<JScrollPane>() { public JScrollPane get() { try {  return new JScrollPane(c);  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return new JScrollPane(c);"; }});
}


static Rect preferredScreenBounds() {
  return screenBounds_safe(preferredScreen());
}


static Integer centerX(Rect r) {
  return rectCenterX(r);
}


static int random_incl(int min, int max) { return random_incl(min, max, defaultRandomizer()); }
static int random_incl(int min, int max, Random random) {
  return random(min, max+1, random);
}

static int random_incl(int max) {
  return random(0, max+1);
}


static Integer centerY(Rect r) {
  return rectCenterY(r);
}


static JFrame consoleFrame() {
  return (JFrame) getOpt(get(getJavaX(), "console"), "frame");
}


static void autoVMExit() {
  call(getJavaX(), "autoVMExit");
}


// usually L<S>
static String fromLines(Iterable lines) {
  StringBuilder buf = new StringBuilder();
  if (lines != null)
    for (Object line : lines)
      buf.append(str(line)).append('\n');
  return buf.toString();
}

static String fromLines(String... lines) {
  return fromLines(asList(lines));
}


static IterableIterator<String> toLines(File f) {
  return linesFromFile(f);
}

static List<String> toLines(String s) {
  List<String> lines = new ArrayList<String>();
  if (s == null) return lines;
  int start = 0;
  while (true) {
    int i = toLines_nextLineBreak(s, start);
    if (i < 0) {
      if (s.length() > start) lines.add(s.substring(start));
      break;
    }

    lines.add(s.substring(start, i));
    if (s.charAt(i) == '\r' && i+1 < s.length() && s.charAt(i+1) == '\n')
      i += 2;
    else
      ++i;

    start = i;
  }
  return lines;
}

static int toLines_nextLineBreak(String s, int start) {
  int n = s.length();
  for (int i = start; i < n; i++) {
    char c = s.charAt(i);
    if (c == '\r' || c == '\n')
      return i;
  }
  return -1;
}


static List<String> mapToLines(Map map) {
  List<String> l = new ArrayList();
  for (Object key : keys(map))
    l.add(str(key) + " = " + str(map.get(key)));
  return l;
}

static String mapToLines(Map map, Object f) {
  return lines(map(map, f));
}

static String mapToLines(Object f, Map map) {
  return lines(map(map, f));
}

static String mapToLines(Object f, Iterable l) {
  return lines(map(f, l));
}

static <A> String mapToLines(Iterable<A> l, IF1<A, String> f) {
  return mapToLines((Object) f, l);
}

static <A> String mapToLines(IF1<A, String> f, Iterable<A> l) {
  return mapToLines((Object) f, l);
}

static <A, B> String mapToLines(Map<A, B> map, IF2<A, B, String> f) {
  return lines(map(map, f));
}

static <A> String mapToLines(IF1<A, String> f, A data1, A... moreData) {
  return lines(map(f, data1, moreData));
}




static <A, B> Map<A, B> weakHashMap() {
  return newWeakHashMap();
}


static <A, B> List<A> keysList(Map<A, B> map) {
  return cloneListSynchronizingOn(keys(map), map);
}


  static <A> List<A> keysList(MultiSet<A> ms) {
    return ms == null ? null : keysList(ms.map);
  }



static <A> A setThreadLocal(ThreadLocal<A> tl, A value) {
  if (tl == null) return null;
  A old = tl.get();
  tl.set(value);
  return old;
}


static <A, B> void multiMapPut(Map<A, List<B>> map, A a, B b) {
  List<B> l = map.get(a);
  if (l == null)
    map.put(a, l = new ArrayList());
  l.add(b);
}


static <A, B> void multiMapPut(MultiMap<A, B> mm, A key, B value) {
  if (mm != null && key != null && value != null) mm.put(key, value);
}



static void assertTrue(Object o) {
  if (!(eq(o, true) /*|| isTrue(pcallF(o))*/))
    throw fail(str(o));
}
  
static boolean assertTrue(String msg, boolean b) {
  if (!b)
    throw fail(msg);
  return b;
}

static boolean assertTrue(boolean b) {
  if (!b)
    throw fail("oops");
  return b;
}


static Map synchronizedMap() {
  return synchroMap();
}

static <A, B> Map<A, B> synchronizedMap(Map<A, B> map) {
  return synchroMap(map);
}


static <A> HashSet<A> lithashset(A... items) {
  HashSet<A> set = new HashSet();
  for (A a : items) set.add(a);
  return set;
}


static List _registerWeakMap_preList;

static <A> A _registerWeakMap(A map) {
  if (javax() == null) {
    // We're in class init
    if (_registerWeakMap_preList == null) _registerWeakMap_preList = synchroList();
    _registerWeakMap_preList.add(map);
    return map;
  }
  
  try {
    call(javax(), "_registerWeakMap", map);
  } catch (Throwable e) {
    printException(e);
    print("Upgrade JavaX!!");
  }
  return map;
}

static void _onLoad_registerWeakMap() {
  assertNotNull(javax());
  if (_registerWeakMap_preList == null) return;
  for (Object o : _registerWeakMap_preList)
    _registerWeakMap(o);
  _registerWeakMap_preList = null;
}


static x30_pkg.x30_util.BetterThreadLocal<Runnable> newPing_actionTL;

static x30_pkg.x30_util.BetterThreadLocal<Runnable> newPing_actionTL() {
  if (newPing_actionTL == null)
    newPing_actionTL = vm_generalMap_getOrCreate("newPing_actionTL",
      () -> {
        Runnable value =  (Runnable) (callF_gen(vm_generalMap_get("newPing_valueForNewThread")));
        var tl = new x30_pkg.x30_util.BetterThreadLocal<Runnable>();
        tl.set(value);
        return tl;
      });
  return newPing_actionTL;
}



static volatile boolean licensed_yes = true;

static boolean licensed() {
  if (!licensed_yes) return false;
  ping_okInCleanUp();
  return true;
}

static void licensed_off() {
  licensed_yes = false;
}


static <A, B> Pair<A, B> mapEntryToPair(Map.Entry<A, B> e) {
  return e == null ? null : pair(e.getKey(), e.getValue());
}


static <A, B> Set<Map.Entry<A,B>> entrySet(Map<A, B> map) {
  return _entrySet(map);
}


static <A> A firstThat(Iterable<A> l, IF1<A, Boolean> pred) {
  for (A a : unnullForIteration(l))
    if (pred.get(a))
      return a;
  return null;
}

static <A> A firstThat(A[] l, IF1<A, Boolean> pred) {
  for (A a : unnullForIteration(l))
    if (pred.get(a))
      return a;
  return null;
}

static <A> A firstThat(IF1<A, Boolean> pred, Iterable<A> l) {
  return firstThat(l, pred);
}

static <A> A firstThat(IF1<A, Boolean> pred, A[] l) {
  return firstThat(l, pred);
}



static boolean eqic(String a, String b) {
  
  
    if ((a == null) != (b == null)) return false;
    if (a == null) return true;
    return a.equalsIgnoreCase(b);
  
}


static boolean eqic(Symbol a, Symbol b) {
  return eq(a, b);
}

static boolean eqic(Symbol a, String b) {
  return eqic(asString(a), b);
}


static boolean eqic(char a, char b) {
  if (a == b) return true;
  
    char u1 = Character.toUpperCase(a);
    char u2 = Character.toUpperCase(b);
    if (u1 == u2) return true;
  
  return Character.toLowerCase(u1) == Character.toLowerCase(u2);
}


static <A> ArrayList<A> litlist(A... a) {
  ArrayList l = new ArrayList(a.length);
  for (A x : a) l.add(x);
  return l;
}


static String collapseWord(String s) {
  if (s == null) return "";
  StringBuilder buf = new StringBuilder();
  for (int i = 0; i < l(s); i++)
    if (i == 0 || !charactersEqualIC(s.charAt(i), s.charAt(i-1)))
      buf.append(s.charAt(i));
  return buf.toString();
}


static List<String> toLowerCase(List<String> strings) {
  List<String> x = new ArrayList();
  for (String s : strings)
    x.add(s.toLowerCase());
  return x;
}

static String[] toLowerCase(String[] strings) {
  String[] x = new String[l(strings)];
  for (int i = 0; i < l(strings); i++)
    x[i] = strings[i].toLowerCase();
  return x;
}

static String toLowerCase(String s) {
  return s == null ? "" : s.toLowerCase();
}


static String firstWord2(String s) {
  s = xltrim(s);
  if (empty(s)) return "";
  if (isLetterOrDigit(first(s)))
    return takeCharsWhile(__29 -> isLetterOrDigit(__29), s);
  else return "" + first(s);
}


static <A, B> LinkedHashMap<A, B> syncMapPut2_createLinkedHashMap(LinkedHashMap<A, B> map, A key, B value) {
  if (key != null)
    if (value != null) {
      if (map == null) map = new LinkedHashMap();
      synchronized(collectionMutex(map)) { map.put(key, value); }
    } else if (map != null) synchronized(collectionMutex(map)) { map.remove(key); }
  return map;
}


static IMeta initMetaOfJComponent(JComponent c) {
  if (c == null) return null;
  IMeta meta =  (IMeta) (c.getClientProperty(IMeta.class));
  if (meta == null)
    c.putClientProperty(IMeta.class, meta = new Meta());
  
  return meta;
}


static <A> A optCast(Class<A> c, Object o) {
  return isInstance(c, o) ? (A) o : null;
}


static <A, B> B mapPutOrRemove(Map<A, B> map, A key, B value) {
  if (map != null && key != null)
    if (value != null) return map.put(key, value);
    else return map.remove(key);
  return null;
}


static <A> Set<A> synchronizedSet() {
  return synchroHashSet();
}

static <A> Set<A> synchronizedSet(Set<A> set) {
  
  
    return Collections.synchronizedSet(set);
  
}


static <A> Set<A> identityHashSet() {
  return Collections.newSetFromMap(new IdentityHashMap());
}


static String unnull(String s) {
  return s == null ? "" : s;
}

static <A> Collection<A> unnull(Collection<A> l) {
  return l == null ? emptyList() : l;
}

static <A> List<A> unnull(List<A> l) { return l == null ? emptyList() : l; }
static int[] unnull(int[] l) { return l == null ? emptyIntArray() : l; }
static char[] unnull(char[] l) { return l == null ? emptyCharArray() : l; }
static double[] unnull(double[] l) { return l == null ? emptyDoubleArray() : l; }

static <A, B> Map<A, B> unnull(Map<A, B> l) {
  return l == null ? emptyMap() : l;
}

static <A> Iterable<A> unnull(Iterable<A> i) {
  return i == null ? emptyList() : i;
}

static <A> A[] unnull(A[] a) {
  return a == null ? (A[]) emptyObjectArray() : a;
}

static BitSet unnull(BitSet b) {
  return b == null ? new BitSet() : b;
}


static Pt unnull(Pt p) {
  return p == null ? new Pt() : p;
}


//ifclass Symbol

static Symbol unnull(Symbol s) {
  return s == null ? emptySymbol() : s;
}
//endif



static <A, B> Pair<A, B> unnull(Pair<A, B> p) {
  return p != null ? p : new Pair(null, null);
}


static int unnull(Integer i) { return i == null ? 0 : i; }
static long unnull(Long l) { return l == null ? 0L : l; }
static double unnull(Double l) { return l == null ? 0.0 : l; }


static boolean nemptyString(String s) {
  return s != null && s.length() > 0;
}


static int listL(Collection l) {
  return l == null ? 0 : l.size();
}


static ThreadLocal<List<Object>> holdInstance_l = new ThreadLocal();

static AutoCloseable holdInstance(Object o) {
  if (o == null) return null;
  listThreadLocalAdd(holdInstance_l, o);
  return new AutoCloseable() {
    public void close() {
      listThreadLocalPopLast(holdInstance_l);
    }
  };
}


static void messageBox(final String msg) {
  if (headless()) print(msg);
  else { swing(() -> { 
    JOptionPane.showMessageDialog(null, msg, "JavaX", JOptionPane.INFORMATION_MESSAGE);
  }); }
}

static void messageBox(Throwable e) {
  //showConsole();
  printStackTrace(e);
  messageBox(hideCredentials(innerException2(e)));
}


static Map vm_generalWeakSubMap(Object name) {
  synchronized(vm_generalMap()) {
    Map map =  (Map) (vm_generalMap_get(name));
    if (map == null)
      vm_generalMap_put(name, map = newWeakMap());
    return map;
  }
}



static <A> A proxy(Class<A> intrface, final Object target) {
  if (target == null) return null;
  if (isInstance(intrface, target)) return (A) target;
  return (A) java.lang.reflect.Proxy.newProxyInstance(intrface.getClassLoader(),
    new Class[] { intrface },
    new proxy_InvocationHandler(target));
}

static <A> A proxy(Object target, Class<A> intrface) {
  return proxy(intrface, target);
}


static String shortenSnippetID(String snippetID) {
  if (snippetID.startsWith("#"))
    snippetID = snippetID.substring(1);
  String httpBlaBla = "http://tinybrain.de/";
  if (snippetID.startsWith(httpBlaBla))
    snippetID = snippetID.substring(httpBlaBla.length());
  return "" + parseLong(snippetID);
}


static File imageSnippetCacheFile(String snippetID) {
  File dir = imageSnippetsCacheDir();
  
  if (!loadBufferedImage_useImageCache) return null;
  
  return new File(dir, parseSnippetID(snippetID) + ".png");
}


static String snippetImageURL_noHttps(String snippetID) {
  return snippetImageURL_noHttps(snippetID, "png");
}

static String snippetImageURL_noHttps(String snippetID, String contentType) {
  return snippetImageURL(snippetID, contentType)
    .replace("https://www.botcompany.de:8443/", "http://www.botcompany.de:8080/")
    .replace("https://botcompany.de/", "http://botcompany.de/");
}


static ThreadLocal<Map<String, List<String>>> loadBinaryPage_responseHeaders = new ThreadLocal();
static ThreadLocal<Map<String, String>> loadBinaryPage_extraHeaders = new ThreadLocal();

static byte[] loadBinaryPage(String url) { try {
  print("Loading " + url);
  return loadBinaryPage(loadPage_openConnection(new URL(url)));
} catch (Exception __e) { throw rethrow(__e); } }

static byte[] loadBinaryPage(URLConnection con) { try {
  Map<String, String> extraHeaders = getAndClearThreadLocal(loadBinaryPage_extraHeaders);
  setHeaders(con);
  for (String key : keys(extraHeaders))
    con.setRequestProperty(key, extraHeaders.get(key));
  return loadBinaryPage_noHeaders(con);
} catch (Exception __e) { throw rethrow(__e); } }

static byte[] loadBinaryPage_noHeaders(URLConnection con) { try {
  ByteArrayOutputStream buf = new ByteArrayOutputStream();
  InputStream inputStream = con.getInputStream();
  loadBinaryPage_responseHeaders.set(con.getHeaderFields());
  long len = 0;
  try { len = con.getContentLength/*Long*/(); } catch (Throwable e) { printStackTrace(e); }
int n = 0;
  while (true) {
    int ch = inputStream.read();
    if (ch < 0)
      break;
    buf.write(ch);
    if (++n % 100000 == 0)
      println("  " + n + (len != 0 ? "/" + len : "") + " bytes loaded.");
  }
  inputStream.close();
  return buf.toByteArray();
} catch (Exception __e) { throw rethrow(__e); } }



/** writes safely (to temp file, then rename) */
public static byte[] saveBinaryFile(String fileName, byte[] contents) { try {
  File file = new File(fileName);
  File parentFile = file.getParentFile();
  if (parentFile != null)
    parentFile.mkdirs();
  String tempFileName = fileName + "_temp";
  FileOutputStream fileOutputStream = newFileOutputStream(tempFileName);
  fileOutputStream.write(contents);
  fileOutputStream.close();
  if (file.exists() && !file.delete())
    throw new IOException("Can't delete " + fileName);

  if (!new File(tempFileName).renameTo(file))
    throw new IOException("Can't rename " + tempFileName + " to " + fileName);
    
  
  vmBus_send("wroteFile", file);
  
  return contents;
} catch (Exception __e) { throw rethrow(__e); } }

static byte[] saveBinaryFile(File fileName, byte[] contents) {
  return saveBinaryFile(fileName.getPath(), contents);
}


static File getGlobalCache() {
  File file = new File(javaxCachesDir(), "Binary Snippets");
  file.mkdirs();
  return file;
}



static long psI(String snippetID) {
  return parseSnippetID(snippetID);
}


static String dataSnippetLink(String snippetID) {
  long id = parseSnippetID(snippetID);
  if (id >= 1100000 && id < 1200000)
    return imageServerURL() + id;
  if (id >= 1200000 && id < 1300000) { // Woody files, actually
    String pw = muricaPassword();
    if (empty(pw)) throw fail("Please set 'murica password by running #1008829");
    return "https://botcompany.de/files/" + id + "?_pass=" + pw; // XXX, although it typically gets hidden when printing
  }
  return fileServerURL() + "/" + id /*+ "?_pass=" + muricaPassword()*/;
}


static String tb_mainServer_default = "https://code.botcompany.de:9898";
static Object tb_mainServer_override; // func -> S

static String tb_mainServer() {
  if (tb_mainServer_override != null) return (String) callF(tb_mainServer_override);
  return trim(loadTextFile(tb_mainServer_file(),
    tb_mainServer_default));
}

static File tb_mainServer_file() {
  return getProgramFile("#1001638", "mainserver.txt");
}

static boolean tb_mainServer_isDefault() {
  return eq(tb_mainServer(), tb_mainServer_default);
}


static <B, A extends B> A addAndReturn(Collection<B> c, A a) {
  if (c != null) c.add(a);
  return a;
}


static void loadBinaryPageToFile(String url, File file) { try {
  print("Loading " + url);
  loadBinaryPageToFile(openConnection(new URL(url)), file);
} catch (Exception __e) { throw rethrow(__e); } }

static void loadBinaryPageToFile(URLConnection con, File file) { try {
  setHeaders(con);
  loadBinaryPageToFile_noHeaders(con, file);
} catch (Exception __e) { throw rethrow(__e); } }

static void loadBinaryPageToFile_noHeaders(URLConnection con, File file) { try {
  File ftemp = new File(f2s(file) + "_temp");
  FileOutputStream buf = newFileOutputStream(mkdirsFor(ftemp));
  try {
    InputStream inputStream = con.getInputStream();
    long len = 0;
    try { len = con.getContentLength/*Long*/(); } catch (Throwable e) { printStackTrace(e); }
    String pat = "  {*}" + (len != 0 ? "/" + len : "") + " bytes loaded.";
    copyStreamWithPrints(inputStream, buf, pat);
    inputStream.close();
    buf.close();
    file.delete();
    renameFile_assertTrue(ftemp, file);
  } finally {
    if (buf != null) buf.close();
  }
} catch (Exception __e) { throw rethrow(__e); } }



static URLConnection openConnection(String url) { try {
  return openConnection(new URL(url));
} catch (Exception __e) { throw rethrow(__e); } }

static URLConnection openConnection(URL url) { try {
  ping();
  
  callOpt(javax(), "recordOpenURLConnection", str(url));
  
  return url.openConnection();
} catch (Exception __e) { throw rethrow(__e); } }


static List<String> allToString(Iterable c) {
  List<String> l = new ArrayList();
  for (Object o : unnull(c)) l.add(str(o));
  return l;
}

static List<String> allToString(Object[] c) {
  List<String> l = new ArrayList();
  for (Object o : unnull(c)) l.add(str(o));
  return l;
}


static byte[] bytesFromHex(String s) {
  return hexToBytes(s);
}


static boolean byteArrayStartsWith(byte[] a, byte[] b) {
  if (a == null || b == null) return false;
  if (a.length < b.length) return false;
  for (int i = 0; i < b.length; i++)
    if (a[i] != b[i])
      return false;
  return true;
}


static byte[] loadBeginningOfBinaryFile(File file, int maxBytes) {
  return loadBinaryFilePart(file, 0, maxBytes);
}


static File newFile(File base, String... names) {
  for (String name : names) base = new File(base, name);
  return base;
}

static File newFile(String name) {
  return name == null ? null : new File(name);
}

static File newFile(String base, String... names) {
  return newFile(newFile(base), names);
}


static boolean isEmpty(Collection c) {
  return c == null || c.isEmpty();
}

static boolean isEmpty(CharSequence s) {
  return s == null || s.length() == 0;
}

static boolean isEmpty(Object[] a) { return a == null || a.length == 0; }
static boolean isEmpty(byte[] a) { return a == null || a.length == 0; }

static boolean isEmpty(Map map) {
  return map == null || map.isEmpty();
}




static boolean isEmpty(AppendableChain c) { return c == null; }



static double parseDouble(String s) {
  return empty(s) ? 0.0 : Double.parseDouble(s);
}


static File javaxCachesDir_dir; // can be set to work on different base dir

static File javaxCachesDir() {
  return javaxCachesDir_dir != null ? javaxCachesDir_dir : new File(userHome(), "JavaX-Caches");
}

static File javaxCachesDir(String sub) {
  return newFile(javaxCachesDir(), sub);
}


static String replacePrefix(String prefix, String replacement, String s) {
  if (!startsWith(s, prefix)) return s;
  return replacement + substring(s, l(prefix));
}


static String snippetImageURL(long snippetID) {
  return snippetImageURL(fsI(snippetID));
}

static String snippetImageURL(String snippetID) {
  return snippetImageURL(snippetID, "png");
}

static String snippetImageURL(String snippetID, String contentType) {
  if (snippetID == null || isURL(snippetID)) return snippetID;
  long id = parseSnippetID(snippetID);
  String url;
  if (isImageServerSnippet(id))
    url = imageServerLink(id);
  else
    //url = "http://eyeocr.sourceforge.net/filestore/filestore.php?cmd=serve&file=blob_" + id + "&contentType=image/" + contentType;
    url = "https://botcompany.de/img/" + id;
  return url;
}


static boolean isFile(File f) {
  return f != null && f.isFile();
}

static boolean isFile(String path) {
  return isFile(newFile(path));
}


static boolean boolOptParam(ThreadLocal<Boolean> tl) {
  return isTrue(optPar(tl));
}

// defaults to false
static boolean boolOptParam(Object[] __, String name) {
  return isTrue(optParam(__, name));
}

static boolean boolOptParam(String name, Object[] __) {
  return boolOptParam(__, name);
}

static boolean boolOptParam(String name, Map __) {
  return isTrue(optPar(name, __));
}


static boolean startsWithIgnoreCase(String a, String b) {
  return regionMatchesIC(a, 0, b, 0, b.length());
}


static boolean endsWithIgnoreCase(String a, String b) {
  int la = l(a), lb = l(b);
  return la >= lb && regionMatchesIC(a, la-lb, b, 0, lb);
}


static boolean endsWithIgnoreCase(String a, String b, Matches m) {
  if (!endsWithIgnoreCase(a, b)) return false;
  if (m != null)
    m.m = new String[] { substring(a, 0, l(a)-l(b)) };
  return true;
}



static boolean containsNewLine(String s) {
  return contains(s, '\n'); // screw \r, nobody needs it
}


static <A> List<A> replace(List<A> l, A a, A b) {
  for (int i = 0; i < l(l); i++)
    if (eq(l.get(i), a))
      l.set(i, b);
  return l;
}

static <A> List<A> replace(A a, A b, List<A> l) {
  return replace(l, a, b);
}

// replace all occurrences of a in s with b
static String replace(String s, String a, String b) {
  return s == null ? null : a == null || b == null ? s : s.replace(a, b);
}

static String replace(String s, char a, char b) {
  return s == null ? null : s.replace(a, b);
}


static String htmlencode2(String s) {
  return htmlencode_noQuotes(s);
}


static Map<String, Font> loadFont_cached_cache = new HashMap();

static synchronized Font loadFont_cached(String snippetID) { try {
  snippetID = formatSnippetID(snippetID);
  Font f = loadFont_cached_cache.get(snippetID);
  if (f == null)
    loadFont_cached_cache.put(snippetID, f = loadFont(snippetID, 12f));
  return f;
} catch (Exception __e) { throw rethrow(__e); } }

static synchronized Font loadFont_cached(String snippetID, float size) { try {
  return loadFont_cached(snippetID).deriveFont(size);
} catch (Exception __e) { throw rethrow(__e); } }


static float getSwingFontScale() {
  return or((Float) vm_generalMap_get("swingFontScale_value"), 1f);
}




static <A extends JTextComponent> A jenableUndoRedo(final A textcomp) {
  { swing(() -> { 
    final UndoManager undo = new UndoManager();
    vm_generalWeakSet("Undo Managers").add(undo);
    textcomp.getDocument().addUndoableEditListener(new UndoableEditListener() {
      public void undoableEditHappened(UndoableEditEvent evt) {
        undo.addEdit(evt.getEdit());
      }
    });
    
    textcomp.getActionMap().put("Undo", abstractAction("Undo", new Runnable() {  public void run() { try { 
      if (undo.canUndo()) undo.undo()
    ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "if (undo.canUndo()) undo.undo()"; }}));
    textcomp.getActionMap().put("Redo", abstractAction("Redo", new Runnable() {  public void run() { try { 
      if (undo.canRedo()) undo.redo()
    ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "if (undo.canRedo()) undo.redo()"; }}));
    textcomp.getInputMap().put(KeyStroke.getKeyStroke("control Z"), "Undo");
    textcomp.getInputMap().put(KeyStroke.getKeyStroke("control Y"), "Redo");
  }); }
  return textcomp;
}


static Lock downloadLock_lock = fairLock();

static Lock downloadLock() {
  return downloadLock_lock;
}


static String getSnippetTitleOpt(String s) {
  try {
    return isSnippetID(s) ? getSnippetTitle(s) : s;
  } catch (Throwable __e) { printStackTrace(__e); }
  return s;
}


static String programID() {
  return getProgramID();
}

static String programID(Object o) {
  return getProgramID(o);
}


static String defaultThreadName_name;

static String defaultThreadName() {
  if (defaultThreadName_name == null)
    defaultThreadName_name = "A thread by " + programID();
  return defaultThreadName_name;
}


static Runnable wrapAsActivity(Object r) {
  if (r == null) return null;
  Runnable r2 = toRunnable(r);


  Object mod = dm_current_generic();
  if (mod == null) return r2;
  return new Runnable() {  public void run() { try { 
    AutoCloseable c =  (AutoCloseable) (rcall("enter", mod));
     AutoCloseable __1 = c; try {
    r2.run();
  
} finally { _close(__1); }} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "AutoCloseable c =  (AutoCloseable) (rcall enter(mod));\r\n    temp c;\r\n    r2.r..."; }};

}


// runnable = Runnable or String (method name)
static Thread newThread(Object runnable) {
  return new BetterThread(_topLevelErrorHandling(toRunnable(runnable)));
}

static Thread newThread(Object runnable, String name) {
  if (name == null) name = defaultThreadName();
  return new BetterThread(_topLevelErrorHandling(toRunnable(runnable)), name);
}

static Thread newThread(String name, Object runnable) {
  return newThread(runnable, name);
}


static Map<Thread, Boolean> _registerThread_threads;
static Object _onRegisterThread; // voidfunc(Thread)

static Thread _registerThread(Thread t) {
  if (_registerThread_threads == null)
    _registerThread_threads = newWeakHashMap();
  _registerThread_threads.put(t, true);
  vm_generalWeakSubMap("thread2mc").put(t, weakRef(mc()));
  callF(_onRegisterThread, t);
  return t;
}

static void _registerThread() {
  _registerThread(Thread.currentThread());
}


static boolean nempty(Collection c) {
  return !empty(c);
}

static boolean nempty(CharSequence s) {
  return !empty(s);
}

static boolean nempty(Object[] o) { return !empty(o); }
static boolean nempty(byte[] o) { return !empty(o); }
static boolean nempty(int[] o) { return !empty(o); }

static boolean nempty(BitSet bs) { return !empty(bs); }

static boolean nempty(Map m) {
  return !empty(m);
}

static boolean nempty(Iterator i) {
  return i != null && i.hasNext();
}


static boolean nempty(MultiMap mm) { return mm != null && !mm.isEmpty(); }



static boolean nempty(Object o) { return !empty(o); }



static boolean nempty(IntRange r) { return !empty(r); }







static boolean nempty(Rect r) { return r != null && r.w != 0 && r.h != 0; }





static boolean nempty(MultiSet ms) { return ms != null && !ms.isEmpty(); }



static int toInt(Object o) {
  if (o == null) return 0;
  if (o instanceof Number)
    return ((Number) o).intValue();
  if (o instanceof String)
    return parseInt((String) o);
  if (o instanceof Boolean)
    return boolToInt((Boolean) o);
  throw fail("woot not int: " + getClassName(o));
}

static int toInt(long l) {
  if (l != (int) l) throw fail("Too large for int: " + l);
  return (int) l;
}


static boolean isSubstanceLAF() {
  return substanceLookAndFeelEnabled();
}


// menuMaker = voidfunc(JPopupMenu)
// return true if menu could be added
static boolean titlePopupMenu(final Component c, final Object menuMaker) {
  JComponent titleBar = getTitlePaneComponent(getPossiblyInternalFrame(c));
  if (titleBar == null)
    { print("Can't add title right click!"); return false; }
  else
    { componentPopupMenu(titleBar, menuMaker); return true; }
}


static boolean jmenuItem_newThreads = false;

static JMenuItem jmenuItem(final String text) {
  return jMenuItem(text, null);
}

static JMenuItem jmenuItem(final String text, final Object r) {
  return swing(new F0<JMenuItem>() { public JMenuItem get() { try { 
    Pair<String, Integer> p = jmenu_autoMnemonic(dropPrefix("[disabled] ", text));
    JMenuItem mi = new JMenuItem(p.a);
    if (startsWith(text, "[disabled] ")) disableMenuItem(mi);
    if (p.b != 0) mi.setMnemonic(p.b);
    mi.addActionListener(jmenuItem_newThreads
      ? actionListenerInNewThread(r)
      : actionListener(r));
    return mi;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "Pair<S, Int> p = jmenu_autoMnemonic(dropPrefix(\"[disabled] \", text));\r\n    JM..."; }});
}


static void restart() {
  Object j = getJavaX();
  call(j, "cleanRestart", get(j, "fullArgs"));
}


static void duplicateThisProgram() {
  nohupJavax(trim(programID() + " " + smartJoin((String[]) get(getJavaX(), "fullArgs"))));
}


static void showConsole() {
  callOpt(get(javax(), "console"), "showConsole");
}


// r : runnable or voidfunc(bool)
static JCheckBoxMenuItem jCheckBoxMenuItem(String text, boolean checked, final Object r) {
  final JCheckBoxMenuItem mi = swing(() -> new JCheckBoxMenuItem(text, checked));
  addActionListener(mi, new Runnable() {  public void run() { try {  callF(r, isChecked(mi)) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "callF(r, isChecked(mi))"; }});
  return mi;
}

static JCheckBoxMenuItem jCheckBoxMenuItem(String text, boolean checked, IVF1<Boolean> r) {
  return jCheckBoxMenuItem(text, checked, (Object) r);
}



static void toggleAlwaysOnTop(Window frame) {
  if (frame == null) return;
  { swing(() -> { 
    frame.setAlwaysOnTop(!frame.isAlwaysOnTop());
  }); }
}


static Rect screenBounds_safe(int iScreen) {
  return screenBounds(min(iScreen, screenCount()-1));
}


static  IF0<Integer> preferredScreen;
static int preferredScreen() { return preferredScreen != null ? preferredScreen.get() : preferredScreen_base(); }
final static int preferredScreen_fallback(IF0<Integer> _f) { return _f != null ? _f.get() : preferredScreen_base(); }
static int preferredScreen_base() {
  return 0;
}


static Integer rectCenterX(Rect r) {
  return r == null ? null : r.x+r.w/2;
}


static Random defaultRandomizer() {
  return defaultRandomGenerator();
}


static int random(int n) { return random(n, defaultRandomGenerator()); }
static int random(int n, Random r) {
  return random(r, n);
}

static int random(Random r, int n) {
  return n <= 0 ? 0 : getRandomizer(r).nextInt(n);
}

static double random(double max) {
  return random()*max;
}

static double random() {
  return defaultRandomGenerator().nextInt(100001)/100000.0;
}

static double random(double min, double max) {
  return min+random()*(max-min);
}

// min <= value < max
static int random(int min, int max) {
  return min+random(max-min);
}

static int random(int min, int max, Random r) {
  return random(r, min, max);
}

static int random(Random r, int min, int max) {
  return min+random(r, max-min);
}

static <A> A random(List<A> l) {
  return oneOf(l);
}

static <A> A random(Collection<A> c) {
  if (c instanceof List) return random((List<A>) c);
  int i = random(l(c));
  return collectionGet(c, i);
}


static int random(IntRange r) {
  return random(r.start, r.end);
}


static <A, B> Pair<A, B> random(Map<A, B> map) {
  return entryToPair(random(entries(map)));
}


static Integer rectCenterY(Rect r) {
  return r == null ? null : r.y+r.h/2;
}


static CloseableIterableIterator<String> linesFromFile(File f) { return linesFromFile(f, null); }
static CloseableIterableIterator<String> linesFromFile(File f, IResourceHolder resourceHolder) { try {
  if (!f.exists()) return emptyCloseableIterableIterator();
  
  if (ewic(f.getName(), ".gz"))
    return linesFromReader(utf8bufferedReader(newGZIPInputStream(f)), resourceHolder);
  
  return linesFromReader(utf8bufferedReader(f), resourceHolder);
} catch (Exception __e) { throw rethrow(__e); } }

static CloseableIterableIterator<String> linesFromFile(String path) { return linesFromFile(path, null); }
static CloseableIterableIterator<String> linesFromFile(String path, IResourceHolder resourceHolder) {
  return linesFromFile(newFile(path), resourceHolder);
}


static <A, B> Set<A> keys(Map<A, B> map) {
  return map == null ? new HashSet() : map.keySet();
}

// convenience shortcut for keys_gen
static Set keys(Object map) {
  return keys((Map) map);
}


  static <A> Set<A> keys(MultiSet<A> ms) {
    return ms.keySet();
  }



  static <A, B> Set<A> keys(MultiMap<A, B> mm) {
    return mm.keySet();
  }







static List map(Iterable l, Object f) { return map(f, l); }

static List map(Object f, Iterable l) {
  List x = emptyList(l);
  if (l != null) for  (Object o : l)
    { ping(); x.add(callF(f, o)); }
  return x;
}


  static <A, B> List<B> map(Iterable<A> l, F1<A, B> f) { return map(f, l); }

  static <A, B> List<B> map(F1<A, B> f, Iterable<A> l) {
    List x = emptyList(l);
    if (l != null) for  (A o : l)
      { ping(); x.add(callF(f, o)); }
    return x;
  }


static <A, B> List<B> map(IF1<A, B> f, Iterable<A> l) { return map(l, f); }
static <A, B> List<B> map(Iterable<A> l, IF1<A, B> f) {
  List x = emptyList(l);
  if (l != null) for  (A o : l)
    { ping(); x.add(f.get(o)); }
  return x;
}
  
static <A, B> List<B> map(IF1<A, B> f, A[] l) { return map(l, f); }
static <A, B> List<B> map(A[] l, IF1<A, B> f) {
  List x = emptyList(l);
  if (l != null) for  (A o : l)
    { ping(); x.add(f.get(o)); }
  return x;
}
  
static List map(Object f, Object[] l) { return map(f, asList(l)); }
static List map(Object[] l, Object f) { return map(f, l); }

static List map(Object f, Map map) {
  return map(map, f);
}

// map: func(key, value) -> list element
static List map(Map map, Object f) {
  List x = new ArrayList();
  if (map != null) for  (Object _e : map.entrySet()) { ping(); 
    Map.Entry e = (Map.Entry) _e;
    x.add(callF(f, e.getKey(), e.getValue()));
  }
  return x;
}

static <A, B, C> List<C> map(Map<A, B> map, IF2<A, B, C> f) {
  return map(map, (Object) f);
}

// new magic alias for mapLL - does it conflict?

static <A, B> List<A> map(IF1<A, B> f, A data1, A... moreData) {
  List x = emptyList(l(moreData)+1);
  x.add(f.get(data1));
  if (moreData != null) for  (A o : moreData)
    { ping(); x.add(f.get(o)); }
  return x;
}




static <A> ArrayList<A> cloneListSynchronizingOn(Collection<A> l, Object mutex) {
  if (l == null) return new ArrayList();
  synchronized(mutex) {
    return new ArrayList<A>(l);
  }
}


static <A extends Throwable> A printException(A e) {
  printStackTrace(e);
  return e;
}


static <A> A vm_generalMap_getOrCreate(Object key, F0<A> create) {
  return vm_generalMap_getOrCreate(key, f0ToIF0(create));
}

static <A> A vm_generalMap_getOrCreate(Object key, IF0<A> create) {
  Map generalMap = vm_generalMap();
  if (generalMap == null) return null; // must be x30 init
  
  synchronized(generalMap) { // should switch to locks here
    A a =  (A) (vm_generalMap_get(key));
    if (a == null)
      vm_generalMap_put(key, a = create == null ? null : create.get());
    return a;
  }
}




  static <A> A callF_gen(F0<A> f) {
    return f == null ? null : f.get();
  }



  static <A, B> B callF_gen(F1<A, B> f, A a) {
    return f == null ? null : f.get(a);
  }



  static <A> A callF_gen(IF0<A> f) {
    return f == null ? null : f.get();
  }



  static <A, B> B callF_gen(IF1<A, B> f, A a) {
    return f == null ? null : f.get(a);
  }


static <A, B> B callF_gen(A a, IF1<A, B> f) {
  return f == null ? null : f.get(a);
}




  static <A, B, C> C callF_gen(IF2<A, B, C> f, A a, B b) {
    return f == null ? null : f.get(a, b);
  }



  static <A> void callF_gen(VF1<A> f, A a) {
    { if (f != null) f.get(a); }
  }


static <A> void callF_gen(A a, IVF1<A> f) {
  { if (f != null) f.get(a); }
}

static <A> void callF_gen(IVF1<A> f, A a) {
  { if (f != null) f.get(a); }
}

static Object callF_gen(Runnable r) { { if (r != null) r.run(); } return null; }

static Object callF_gen(Object f, Object... args) {
  return callF(f, args);
}


static <A, B> Set<Map.Entry<A,B>> _entrySet(Map<A, B> map) {
  return map == null ? Collections.EMPTY_SET : map.entrySet();
}


static String unnullForIteration(String s) {
  return s == null ? "" : s;
}

static <A> Collection<A> unnullForIteration(Collection<A> l) {
  return l == null ? immutableEmptyList() : l;
}

static <A> List<A> unnullForIteration(List<A> l) { return l == null ? immutableEmptyList() : l; }
static int[] unnullForIteration(int[] l) { return l == null ? emptyIntArray() : l; }
static char[] unnullForIteration(char[] l) { return l == null ? emptyCharArray() : l; }
static double[] unnullForIteration(double[] l) { return l == null ? emptyDoubleArray() : l; }
static short[] unnullForIteration(short[] l) { return l == null ? emptyShortArray() : l; }

static <A, B> Map<A, B> unnullForIteration(Map<A, B> l) {
  return l == null ? immutableEmptyMap() : l;
}

static <A> Iterable<A> unnullForIteration(Iterable<A> i) {
  return i == null ? immutableEmptyList() : i;
}

static <A> A[] unnullForIteration(A[] a) {
  return a == null ? (A[]) emptyObjectArray() : a;
}

static BitSet unnullForIteration(BitSet b) {
  return b == null ? new BitSet() : b;
}


static Pt unnullForIteration(Pt p) {
  return p == null ? new Pt() : p;
}


//ifclass Symbol

static Symbol unnullForIteration(Symbol s) {
  return s == null ? emptySymbol() : s;
}
//endif



static <A, B> Pair<A, B> unnullForIteration(Pair<A, B> p) {
  return p != null ? p : new Pair(null, null);
}


static long unnullForIteration(Long l) { return l == null ? 0L : l; }


static String asString(Object o) {
  return o == null ? null : o.toString();
}


static boolean charactersEqualIC(char c1, char c2) {
  if (c1 == c2) return true;
  char u1 = Character.toUpperCase(c1);
  char u2 = Character.toUpperCase(c2);
  if (u1 == u2) return true;
  return Character.toLowerCase(u1) == Character.toLowerCase(u2);
}



static String xltrim(String s) {
  int i = 0, n = l(s);
  while (i < n && contains(" \t\r\n", s.charAt(i)))
    ++i;
  return substr(s, i);
}


static boolean isLetterOrDigit(char c) {
  return Character.isLetterOrDigit(c);
}


// pred: char -> bool
static String takeCharsWhile(String s, Object pred) {
  int i = 0;
  while (i < l(s) && isTrue(callF(pred, s.charAt(i)))) ++i;
  return substring(s, 0, i);
}

static String takeCharsWhile(IF1<Character, Boolean> f, String s) {
  return takeCharsWhile(s, f);
}


static boolean isInstance(Class type, Object arg) {
  return type.isInstance(arg);
}


static <A> Set<A> synchroHashSet() {
  return synchronizedSet(new HashSet<A>());
}



static int[] emptyIntArray_a = new int[0];
static int[] emptyIntArray() { return emptyIntArray_a; }


static char[] emptyCharArray = new char[0];
static char[] emptyCharArray() { return emptyCharArray; }


static double[] emptyDoubleArray = new double[0];
static double[] emptyDoubleArray() { return emptyDoubleArray; }


static Map emptyMap() {
  return new HashMap();
}


static Object[] emptyObjectArray_a = new Object[0];
static Object[] emptyObjectArray() { return emptyObjectArray_a; }


static Symbol emptySymbol_value;

static Symbol emptySymbol() {
  if (emptySymbol_value == null) emptySymbol_value = symbol("");
  return emptySymbol_value;
}


static <A> void listThreadLocalAdd(ThreadLocal<List<A>> tl, A a) {
  List<A> l = tl.get();
  if (l == null) tl.set(l = new ArrayList());
  l.add(a);
}


static <A> A listThreadLocalPopLast(ThreadLocal<List<A>> tl) {
  List<A> l = tl.get();
  if (l == null) return null;
  A a = popLast(l);
  if (empty(l)) tl.set(null);
  return a;
}


static boolean headless() {
  return isHeadless();
}


static Throwable innerException2(Throwable e) {
  if (e == null) return null;
  while (empty(e.getMessage()) && e.getCause() != null)
    e = e.getCause();
  return e;
}


static <A, B> Map<A, B> newWeakMap() {
  return newWeakHashMap();
}


static long parseLong(String s) {
  if (empty(s)) return 0;
  return Long.parseLong(dropSuffix("L", s));
}

static long parseLong(Object s) {
  return Long.parseLong((String) s);
}


static int loadPage_defaultTimeout = 60000;
static ThreadLocal<String> loadPage_charset = new ThreadLocal();
static boolean loadPage_allowGzip = true, loadPage_debug;
static boolean loadPage_anonymous = false; // don't send computer ID
static int loadPage_verboseness = 100000;
static int loadPage_retries = 1; //60; // seconds
static ThreadLocal<Boolean> loadPage_silent = new ThreadLocal();
static volatile int loadPage_forcedTimeout; // ms
static ThreadLocal<Integer> loadPage_forcedTimeout_byThread = new ThreadLocal(); // ms
static ThreadLocal<Map<String, List<String>>> loadPage_responseHeaders = new ThreadLocal();
static ThreadLocal<Map<String, String>> loadPage_extraHeaders = new ThreadLocal();
static ThreadLocal<Long> loadPage_sizeLimit = new ThreadLocal();

public static String loadPageSilently(String url) { try {
  return loadPageSilently(new URL(loadPage_preprocess(url)));
} catch (Exception __e) { throw rethrow(__e); } }

public static String loadPageSilently(URL url) { try {
  if (!networkAllowanceTest(str(url))) throw fail("Not allowed: " + url);
    
  IOException e = null;
  for (int tries = 0; tries < loadPage_retries; tries++)
    try {
      URLConnection con = loadPage_openConnection(url);
      return loadPage(con, url);
    } catch (IOException _e) {
      e = _e;
      if (loadPage_debug)
        print(exceptionToStringShort(e));
      if (tries < loadPage_retries-1) sleepSeconds(1);
    }
  throw e;
} catch (Exception __e) { throw rethrow(__e); } }

static String loadPage_preprocess(String url) {  
  if (url.startsWith("tb/")) // don't think we use this anymore
    url = tb_mainServer() + "/" + url;
  if (url.indexOf("://") < 0)
    url = "http://" + url;
  return url;
}

static String loadPage(String url) { try {
  url = loadPage_preprocess(url);
  if (!isTrue(loadPage_silent.get()))
    printWithTime("Loading: " + hideCredentials(url));
  return loadPageSilently(new URL(url));
} catch (Exception __e) { throw rethrow(__e); } }

static String loadPage(URL url) {
  return loadPage(url.toExternalForm());
}

static String loadPage(URLConnection con, URL url) throws IOException {
  return loadPage(con, url, true);
}

static String loadPage(URLConnection con, URL url, boolean addHeaders) throws IOException {
  Map<String, String> extraHeaders = getAndClearThreadLocal(loadPage_extraHeaders);
  Long limit = optPar(loadPage_sizeLimit);
  if (addHeaders) try {
    if (!loadPage_anonymous)
      setHeaders(con);
    if (loadPage_allowGzip)
      con.setRequestProperty("Accept-Encoding", "gzip");
    con.setRequestProperty("X-No-Cookies", "1");
    for (String key : keys(extraHeaders))
      con.setRequestProperty(key, extraHeaders.get(key));
  } catch (Throwable e) {} // fails if within doPost
  
  
  vm_generalSubMap("URLConnection per thread").put(currentThread(), con);
  
  loadPage_responseHeaders.set(con.getHeaderFields());
  InputStream in = null;
  try {
    in = urlConnection_getInputStream(con);
  //vm_generalSubMap("InputStream per thread").put(currentThread(), in);
  if (loadPage_debug)
    print("Put stream in map: " + currentThread());
    String contentType = con.getContentType();
    if (contentType == null) {
      //printStruct("Headers: ", con.getHeaderFields());
      throw new IOException("Page could not be read: " + hideCredentials(url));
    }
    //print("Content-Type: " + contentType);
    String charset = loadPage_charset == null ? null : loadPage_charset.get();
    if (charset == null) charset = loadPage_guessCharset(contentType);
    
    if ("gzip".equals(con.getContentEncoding())) {
      if (loadPage_debug)
        print("loadPage: Using gzip.");
      in = newGZIPInputStream(in);
    }
    Reader r;
    try {
      r = new InputStreamReader(in, unquote(charset));
    } catch (UnsupportedEncodingException e) {
      print(toHex(utf8(charset)));
      throw e;
    }
    
    boolean silent = isTrue(loadPage_silent.get());
    StringBuilder buf = new StringBuilder();
    int n = 0;
    while (limit == null || n < limit) {
      ping();
      int ch = r.read();
      if (ch < 0)
        break;
      buf.append((char) ch);
      ++n;
      if (!silent && (n % loadPage_verboseness) == 0)
        print("  " + n + " chars read");
    }
    return buf.toString();
  } finally {
    if (loadPage_debug)
      print("loadPage done");
    //vm_generalSubMap("InputStream per thread").remove(currentThread());
    
    vm_generalSubMap("URLConnection per thread").remove(currentThread());
    
    if (in != null) in.close();
  }
}

static String loadPage_guessCharset(String contentType) {
  Matcher m = regexpMatcher("text/[a-z]+;\\s*charset=([^\\s]+)\\s*", contentType);
  String match = m.matches() ? m.group(1) : null;
  if (loadPage_debug)
    print("loadPage: contentType=" + contentType + ", match: " + match);
  /* If Content-Type doesn't match this pre-conception, choose default and hope for the best. */
  //return or(match, "ISO-8859-1");
  return or(match, "UTF-8");
}

static URLConnection loadPage_openConnection(URL url) {
  URLConnection con = openConnection(url);
  int timeout = toInt(loadPage_forcedTimeout_byThread.get());
  if (timeout == 0) timeout = loadPage_forcedTimeout;
  if (timeout != 0)
    setURLConnectionTimeouts(con, loadPage_forcedTimeout);
  else
    setURLConnectionDefaultTimeouts(con, loadPage_defaultTimeout);
  return con;
}


static <A> A getAndClearThreadLocal(ThreadLocal<A> tl) {
  A a = tl.get();
  tl.set(null);
  return a;
}


static void setHeaders(URLConnection con) throws IOException {
  
  String computerID = getComputerID_quick();
  if (computerID != null) try {
    con.setRequestProperty("X-ComputerID", computerID);
    con.setRequestProperty("X-OS", System.getProperty("os.name") + " " + System.getProperty("os.version"));
  } catch (Throwable e) {
    //printShortException(e);
  }
  
}


static <A> A println(A a) {
  return print(a);
}


static FileOutputStream newFileOutputStream(File path) throws IOException {
  return newFileOutputStream(path.getPath());
}

static FileOutputStream newFileOutputStream(String path) throws IOException {
  return newFileOutputStream(path, false);
}

static FileOutputStream newFileOutputStream(File path, boolean append) throws IOException {
  return newFileOutputStream(path.getPath(), append);
}

static FileOutputStream newFileOutputStream(String path, boolean append) throws IOException {
  mkdirsForFile(path);
  FileOutputStream f = new FileOutputStream(path, append);
  
  _registerIO(f, path, true);
  
  return f;
}


static String imageServerURL() {
  return or2(trim(loadTextFile(javaxDataDir("image-server-url.txt"))), "http://botcompany.de/images/raw/");
}


static volatile boolean muricaPassword_pretendNotAuthed = false;

static String muricaPassword() {
  if (muricaPassword_pretendNotAuthed) return null;
  return trim(loadTextFile(muricaPasswordFile()));
}


static String fileServerURL() {
  return "https://botcompany.de/files";
}


static String trim(String s) { return s == null ? null : s.trim(); }
static String trim(StringBuilder buf) { return buf.toString().trim(); }
static String trim(StringBuffer buf) { return buf.toString().trim(); }


static String loadTextFile(String fileName) {
  return loadTextFile(fileName, null);
}

static String loadTextFile(File f, String defaultContents) { return loadTextFile(f, defaultContents, "UTF-8"); }
static String loadTextFile(File f, String defaultContents, String encoding) { try {
  
  checkFileNotTooBigToRead(f);
  
  if (f == null || !f.exists()) return defaultContents;

  FileInputStream fileInputStream = new FileInputStream(f);
  InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, encoding);
  return loadTextFile(inputStreamReader);
} catch (Exception __e) { throw rethrow(__e); } }

public static String loadTextFile(File fileName) {
  return loadTextFile(fileName, null);
}

static String loadTextFile(String fileName, String defaultContents) {
  return fileName == null ? defaultContents : loadTextFile(newFile(fileName), defaultContents);
}

static String loadTextFile(Reader reader) throws IOException {
  StringBuilder builder = new StringBuilder();
  try {
    char[] buffer = new char[1024];
    int n;
    while (-1 != (n = reader.read(buffer)))
      builder.append(buffer, 0, n);
  } finally {
    reader.close();
  }
  return str(builder);
}


static File getProgramFile(String progID, String fileName) {
  if (new File(fileName).isAbsolute())
    return new File(fileName);
  return new File(getProgramDir(progID), fileName);
}

static File getProgramFile(String fileName) {
  return getProgramFile(getProgramID(), fileName);
}



public static File mkdirsFor(File file) {
  return mkdirsForFile(file);
}



static void copyStreamWithPrints(InputStream in, OutputStream out, String pat) { try {
  byte[] buf = new byte[65536];
  int total = 0;
  while (true) {
    int n = in.read(buf);
    if (n <= 0) return;
    out.write(buf, 0, n);
    if ((total+n)/100000 > total/100000)
      print(pat.replace("{*}", str(roundDownTo(100000, total))));
    total += n;
  }
} catch (Exception __e) { throw rethrow(__e); } }


static File renameFile_assertTrue(File a, File b) { try {
  if (a.equals(b)) return b; // no rename necessary
  if (!a.exists()) throw fail("Source file not found: " + f2s(a));
  if (b.exists()) throw fail("Target file exists: " + f2s(b));
  mkdirsForFile(b);
  
  
  if (!a.renameTo(b))
    throw fail("Can't rename " + f2s(a) + " to " + f2s(b));
  
  return b;
} catch (Exception __e) { throw rethrow(__e); } }


static byte[] hexToBytes(String s) {
  if (odd(l(s))) throw fail("Hex string has odd length: " + quote(shorten(10, s)));
  int n = l(s) / 2;
  byte[] bytes = new byte[n];
  for (int i = 0; i < n; i++) {
    int a = parseHexChar(s.charAt(i*2));
    int b = parseHexChar(s.charAt(i*2+1));
    if (a < 0 || b < 0)
      throw fail("Bad hex byte: " + quote(substring(s, i*2, i*2+2)) + " at " + i*2 + "/" + l(s));
    bytes[i] = (byte) ((a << 4) | b);
  }
  return bytes;
}


static byte[] loadBinaryFilePart(File file, long start, long end) { try {
  RandomAccessFile raf = new RandomAccessFile(file, "r");
  int n = toInt(min(raf.length(), end-start));
  byte[] buffer = new byte[n];
  try {
    raf.seek(start);
    raf.readFully(buffer, 0, n);
    return buffer;
  } finally {
    raf.close();
  }
} catch (Exception __e) { throw rethrow(__e); } }


static String _userHome;
static String userHome() {
  if (_userHome == null)
    return actualUserHome();
  return _userHome;
}

static File userHome(String path) {
  return new File(userDir(), path);
}


static String imageServerLink(String md5OrID) {
  if (possibleMD5(md5OrID))
    return "https://botcompany.de/images/md5/" + md5OrID;
  return imageServerLink(parseSnippetID(md5OrID));
}

static String imageServerLink(long id) {
  return "https://botcompany.de/images/" + id;
}


static boolean regionMatchesIC(String a, int offsetA, String b, int offsetB, int len) {
  
  
    return a != null && a.regionMatches(true, offsetA, b, offsetB, len);
  
}


static boolean contains(Collection c, Object o) {
  return c != null && c.contains(o);
}

static boolean contains(Iterable it, Object a) {
  if (it != null)
    for (Object o : it)
      if (eq(a, o))
        return true;
  return false;
}

static boolean contains(Object[] x, Object o) {
  if (x != null)
    for (Object a : x)
      if (eq(a, o))
        return true;
  return false;
}

static boolean contains(String s, char c) {
  return s != null && s.indexOf(c) >= 0;
}

static boolean contains(String s, String b) {
  return s != null && s.indexOf(b) >= 0;
}

static boolean contains(BitSet bs, int i) {
  return bs != null && bs.get(i);
}


static <A> boolean contains(Producer<A> p, A a) {
  if (p != null && a != null) while (true) {
    A x = p.next();
    if (x == null) break;
    if (eq(x, a)) return true;
  }
  return false;
}



static boolean contains(Rect r, Pt p) { return rectContains(r, p); }



static String htmlencode_noQuotes(String s) {
  if (s == null) return "";
  int n = s.length();
  StringBuilder out = null;
  
  
  
  for (int i = 0; i < n; i++) {
    char c = s.charAt(i);
    if (c == '<') {
      
    if (out == null) out = new StringBuilder(Math.max(16, n)).append(takeFirst(i, s));
    out
  .append("&lt;");
    }
    else if (c == '>') {
      
    if (out == null) out = new StringBuilder(Math.max(16, n)).append(takeFirst(i, s));
    out
  .append("&gt;");
    }
    else if (c > 127 || c == '&') {
      int cp = s.codePointAt(i);
      
    if (out == null) out = new StringBuilder(Math.max(16, n)).append(takeFirst(i, s));
    out
  .append("&#x");
      out.append(intToHex_flexLength(cp));
      out.append(';');
      i += Character.charCount(cp)-1;
    } else
      { if (out != null) out.append(c); }
  }
  return out == null ? s : out.toString();
}


static Font loadFont(String snippetID) { try {
  return loadFont(snippetID, 12f);
} catch (Exception __e) { throw rethrow(__e); } }

static Font loadFont(InputStream in) { try {
  return Font.createFont(Font.TRUETYPE_FONT, in);
} catch (Exception __e) { throw rethrow(__e); } }

static Font loadFont(String snippetID, float fontSize) {
  return loadFont(loadLibrary(snippetID), fontSize);
}

static Font loadFont(File f, float fontSize) { try {
  return Font.createFont(Font.TRUETYPE_FONT, f).deriveFont(fontSize);
} catch (Exception __e) { throw rethrow(__e); } }

static Font loadFont(InputStream in, float fontSize) { try {
  return Font.createFont(Font.TRUETYPE_FONT, in).deriveFont(fontSize);
} catch (Exception __e) { throw rethrow(__e); } }


static Set vm_generalWeakSet(Object name) {
  synchronized(vm_generalMap()) {
    Set set =  (Set) (vm_generalMap_get(name));
    if (set == null)
      vm_generalMap_put(name, set = newWeakHashSet());
    return set;
  }
}



static AbstractAction abstractAction(String name, final Object runnable) {
  return new AbstractAction(name) {
    public void actionPerformed(ActionEvent evt) {
      pcallF(runnable);
    }
  };
}


static String getSnippetTitle(String id) {
  if (id == null) return null;
  if (!isSnippetID(id)) return "?";
  
  
  IResourceLoader rl = vm_getResourceLoader();
  if (rl != null)
    return rl.getSnippetTitle(id);
  
  
  return getSnippetTitle_noResourceLoader(id);
}
  
static String getSnippetTitle_noResourceLoader(String id) { try {
  if (isLocalSnippetID(id)) return localSnippetTitle(id);
  long parsedID = parseSnippetID(id);
  String url;
  if (isImageServerSnippet(parsedID))
    url = imageServerURL() + "title/" + parsedID + muricaCredentialsQuery();
  else if (isGeneralFileServerSnippet(parsedID))
    url = "http://butter.botcompany.de:8080/files/name/" + parsedID;
  else
    url = tb_mainServer() + "/tb-int/getfield.php?id=" + parsedID + "&field=title" + standardCredentials_noCookies();
  String title = trim(loadPageSilently(url));
  if (title != null)
    try { saveTextFileIfChanged(snippetTitle_cacheFile(id), title); } catch (Throwable __e) { print(exceptionToStringShort(__e)); }
  return or(title, "?");
} catch (Exception __e) { throw rethrow(__e); } }

static String getSnippetTitle(long id) {
  return getSnippetTitle(fsI(id));
}




static String programID;
static String getProgramID() {
  return nempty(programID) ? formatSnippetIDOpt(programID) : "?";
}


// TODO: ask JavaX instead
static String getProgramID(Class c) {
  String id = (String) getOpt(c, "programID");
  if (nempty(id))
    return formatSnippetID(id);
  return "?";
}


static String getProgramID(Object o) {
  return getProgramID(getMainClass(o));
}


static Object dm_current_generic() {
  return getWeakRef(dm_current_generic_tl().get());
}


static Object rcall(String method, Object o, Object... args) {
  return call_withVarargs(o, method, args);
}


static Runnable _topLevelErrorHandling(Runnable r) {


  if (r == null) return null;
  
  // maybe we don't want this anymore. just dm_current_generic()
  Object info = _threadInfo();
  Object mod = dm_current_generic();
  if (info == null && mod == null) return r;
  
  return new Runnable() {  public void run() { try { 
     AutoCloseable __1 =  (AutoCloseable) (rcall("enter", mod)); try {
    _threadInheritInfo(info);
    r.run();
  
} finally { _close(__1); }} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "temp (AutoCloseable) rcall enter(mod);\r\n    _threadInheritInfo(info);\r\n    r...."; }};

}


static <A> WeakReference<A> weakRef(A a) {
  return newWeakReference(a);
}


static int parseInt(String s) {
  return emptyString(s) ? 0 : Integer.parseInt(s);
}

static int parseInt(char c) {
  return Integer.parseInt(str(c));
}


static int boolToInt(boolean b) {
  return b ? 1 : 0;
}


static boolean substanceLookAndFeelEnabled() {
  return startsWith(getLookAndFeel(), "org.pushingpixels.");
}


static JComponent getTitlePaneComponent(RootPaneContainer window) {
  if (window instanceof JInternalFrame)
	  return getInternalFrameTitlePaneComponent((JInternalFrame) window);
	  
  if (!substanceLookAndFeelEnabled() || window == null) return null;
	JRootPane rootPane = window.getRootPane();
	if (rootPane != null) {
		Object /*SubstanceRootPaneUI*/ ui = rootPane.getUI();
		return (JComponent) call(ui, "getTitlePane");
	}
	return null;
}


static RootPaneContainer getPossiblyInternalFrame(Component c) {
  JInternalFrame f = getInternalFrame(c);
  if (f != null) return f;
  return optCast(RootPaneContainer.class, getWindow(c));
}




static ThreadLocal<MouseEvent> componentPopupMenu_mouseEvent;

static void componentPopupMenu_init() {
  { swing(() -> { 
    if (componentPopupMenu_mouseEvent == null)
      componentPopupMenu_mouseEvent = (ThreadLocal<MouseEvent>) vm_generalMap_get("mouseEvent");
    if (componentPopupMenu_mouseEvent == null)
      vm_generalMap_put("componentPopupMenu_mouseEvent" , componentPopupMenu_mouseEvent = new ThreadLocal());
  }); }
}

// menuMaker = voidfunc(JPopupMenu)
static void componentPopupMenu(final JComponent component, final Object menuMaker) {
  if (component == null || menuMaker == null) return;
  { swing(() -> { 
    Object adapter = componentPopupMenu_initForComponent(component);
    ((List) _get(adapter, "maker")).add(menuMaker);
  }); }
}

static Object componentPopupMenu_initForComponent(final JComponent component) {
  return component == null ? null : swing(new F0<Object>() { public Object get() { try { 
    componentPopupMenu_init();
    Object adapter = findComponentPopupMenuListener_gen(component);
    if (adapter == null) {
      componentPopupMenu_Adapter a = new componentPopupMenu_Adapter();
      //addMouseListener_inFront(component, a);
      component.addMouseListener(a);
      adapter = a;
    }
    return adapter;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "componentPopupMenu_init();\r\n    O adapter = findComponentPopupMenuListener_ge..."; }});
}

static class componentPopupMenu_Adapter extends MouseAdapter {
  List maker = new ArrayList();
  boolean internalFrameLeftButtonMagic = false;
  boolean allowScrolling = true;
  
  Point pressedAt;

  public void mousePressed(MouseEvent e) {
    displayMenu(e);
    pressedAt = internalFrameLeftButtonMagic && e.getClickCount() == 1 && internalFrameActive(e.getComponent()) ? e.getLocationOnScreen() : null;
  }
  
  public void mouseReleased(MouseEvent e) {
    // TODO: show a little less often on left mouse click
    if (internalFrameLeftButtonMagic && eq(pressedAt, e.getLocationOnScreen()))
      displayMenu2(e);
    else
      displayMenu(e);
  }

  void displayMenu(MouseEvent e) {
    if (e.getSource() instanceof JInternalFrame) return;
    if (e.isPopupTrigger()) displayMenu2(e);
  }

  void populate(JPopupMenu menu, MouseEvent e) {  
     AutoCloseable __1 = tempSetTL(componentPopupMenu_mouseEvent, e); try {
    for (Object menuMaker : maker)
      pcallF(menuMaker, menu);
    vmBus_send("showingPopupMenu", e.getComponent(), menu);
  } finally { _close(__1); }}
    
  void displayMenu2(MouseEvent e) {
    new PopupMenuMaker(e, menu -> populate(menu, e))
      .allowScrolling(allowScrolling).run();
  }
}




static JMenuItem jMenuItem(final String text) {
  return jmenuItem(text);
}

static JMenuItem jMenuItem(String text, Object r) {
  return jmenuItem(text, r);
}


static Pair<String, Integer> jmenu_autoMnemonic(String s) {
  int i = indexOf(s, '&');
  if (i >= 0 && i < l(s) && isLetterOrDigit(s.charAt(i+1)))
    return pair(substring(s, 0, i) + substring(s, i+1), (int) s.charAt(i+1));
  return pair(s, 0);
}


static JMenuItem disableMenuItem(final JMenuItem mi) {
  if (mi != null) { swing(() -> {  mi.setEnabled(false); }); }
  return mi;
}


static ActionListener actionListenerInNewThread(final Object runnable) {
  return actionListenerInNewThread(runnable, null);
}

static ActionListener actionListenerInNewThread(final Object runnable, final Object instanceToHold) {
  if (runnable instanceof ActionListener) return (ActionListener) runnable;
  return new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent _evt) { try {
    startThread("Action Listener", new Runnable() {  public void run() { try {    
       AutoCloseable __1 = holdInstance(instanceToHold); try {
      callF(runnable);
    } finally { _close(__1); }
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "AutoCloseable __1 = holdInstance(instanceToHold); try {\r\n      callF(runnable..."; }});
  } catch (Throwable __e) { messageBox(__e); }}};
}


static void nohupJavax(final String javaxargs) {
  { startThread(new Runnable() {  public void run() { try {  call(hotwireOnce("#1008562"), "nohupJavax", javaxargs); 
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "call(hotwireOnce(\"#1008562\"), \"nohupJavax\", javaxargs);"; }}); }
}

static void nohupJavax(final String javaxargs, final String vmArgs) {
  { startThread(new Runnable() {  public void run() { try {  call(hotwireOnce("#1008562"), "nohupJavax", javaxargs, vmArgs); 
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "call(hotwireOnce(\"#1008562\"), \"nohupJavax\", javaxargs, vmArgs);"; }}); }
}


// Try to get the quoting right...

static String smartJoin(String[] args) {
  if (empty(args)) return "";
  if (args.length == 1) return args[0];
  
  String[] a = new String[args.length];
  for (int i = 0; i < a.length; i++)
    a[i] = !isJavaIdentifier(args[i]) && !isQuoted(args[i]) ? quote(args[i]) : args[i];
  return join(" ", a);
}

static String smartJoin(List<String> args) {
  return smartJoin(toStringArray(args));
}


static void addActionListener(JTextField tf, final Runnable action) {
  onEnter(tf, action);
}

static void addActionListener(final JComboBox cb, final Runnable action) {
  if (cb != null) { swing(() -> { 
    cb.addActionListener(actionListener(action));
  }); }
}

static void addActionListener(final AbstractButton b, final Runnable action) {
  if (b != null) { swing(() -> { 
    b.addActionListener(actionListener(action));
  }); }
}


static boolean isChecked(JCheckBox checkBox) {
  return checkBox != null && (boolean) swing(new F0<Boolean>() { public Boolean get() { try {  return checkBox.isSelected();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return checkBox.isSelected();"; }});
}

static boolean isChecked(JCheckBoxMenuItem mi) {
  return mi != null && (boolean) swing(new F0<Boolean>() { public Boolean get() { try {  return mi.isSelected();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return mi.isSelected();"; }});
}

static boolean isChecked(JRadioButton rb) {
  return rb != null && (boolean) swing(() -> rb.isSelected());
}


static Rect screenBounds(GraphicsDevice screen) {
  return screen == null ? null : toRect(screen.getDefaultConfiguration().getBounds());
}

static Rect screenBounds(int iScreen) {
  return screenBounds(get(screenDevices(), iScreen));
}


static int screenCount() {
  return l(GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices());
}


static Random defaultRandomGenerator() {
  { Random r = customRandomizerForThisThread(); if (r != null) return r; }
  return ThreadLocalRandom.current();
}


static Random getRandomizer(Random r) {
  return r != null ? r : defaultRandomGenerator();
}


static <A> A oneOf(List<A> l) {
  if (empty(l)) return null;
  int n = l.size();
  return n == 1 ? first(l) : l.get(defaultRandomizer().nextInt(n));
}

static char oneOf(String s) {
  return empty(s) ? '?' : s.charAt(random(l(s)));
}

static String oneOf(String... l) {
  return oneOf(asList(l));
}


static <A> A collectionGet(Collection<A> c, int idx) {
  if (c == null || idx < 0 || idx >= l(c)) return null;
  if (c instanceof List) return listGet((List<A>) c, idx);
  Iterator<A> it = c.iterator();
  for (int i = 0; i < idx; i++) if (it.hasNext()) it.next(); else return null;
  return it.hasNext() ? it.next() : null;
}


static <A, B> Pair<A, B> entryToPair(Map.Entry<A, B> e) {
  return mapEntryToPair(e);
}


static <A, B> Set<Map.Entry<A,B>> entries(Map<A, B> map) {
  return _entrySet(map);
}


static CloseableIterableIterator emptyCloseableIterableIterator_instance = new CloseableIterableIterator() {
  public Object next() { throw fail(); }
  public boolean hasNext() { return false; }
};

static <A> CloseableIterableIterator<A> emptyCloseableIterableIterator() {
  return emptyCloseableIterableIterator_instance; 
}


static CloseableIterableIterator<String> linesFromReader(Reader r) { return linesFromReader(r, null); }
static CloseableIterableIterator<String> linesFromReader(Reader r, IResourceHolder resourceHolder) {
  final BufferedReader br = bufferedReader(r);
  return holdResource(resourceHolder, iteratorFromFunction_f0_autoCloseable(new F0<String>() { public String get() { try {  return readLineFromReaderWithClose(br);  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return readLineFromReaderWithClose(br);"; }}, _wrapIOCloseable(r)));
}


static BufferedReader utf8bufferedReader(InputStream in) { try {
  return in == null ? null : bufferedReader(_registerIOWrap(new InputStreamReader(in, "UTF-8"), in));
} catch (Exception __e) { throw rethrow(__e); } }

static BufferedReader utf8bufferedReader(File f) { try {
  return utf8bufferedReader(newFileInputStream(f));
} catch (Exception __e) { throw rethrow(__e); } }


static GZIPInputStream newGZIPInputStream(File f) {
  return gzInputStream(f);
}

static GZIPInputStream newGZIPInputStream(InputStream in) {
  return gzInputStream(in);
}




static <A> IF0<A> f0ToIF0(F0<A> f) {
  return f == null ? null : () -> f.get();
}


static <A> List<A> immutableEmptyList() {
  return Collections.emptyList();
}


static short[] emptyShortArray = new short[0];
static short[] emptyShortArray() { return emptyShortArray; }


static <A, B> Map<A, B> immutableEmptyMap() {
  return Collections.emptyMap();
}


static String substr(String s, int x) {
  return substring(s, x);
}

static String substr(String s, int x, int y) {
  return substring(s, x, y);
}




static WeakHasherMap<Symbol, Boolean> symbol_map = new WeakHasherMap(new Hasher<Symbol>() {
  public int hashCode(Symbol symbol) { return symbol.text.hashCode(); }
  public boolean equals(Symbol a, Symbol b) {
    if (a == null) return b == null;
    return b != null && eq(a.text, b.text);
  }
});



static Symbol symbol(String s) {
  
  
  if (s == null) return null;
  synchronized(symbol_map) {
    // TODO: avoid object creation by passing the string to findKey
    Symbol symbol = new Symbol(s, true);
    Symbol existingSymbol = symbol_map.findKey(symbol);
    if (existingSymbol == null)
      symbol_map.put(existingSymbol = symbol, true);
    
      
    return existingSymbol;
  }
  
}

static Symbol symbol(CharSequence s) {
  if (s == null) return null;
  
  
  if (s instanceof Symbol) return (Symbol) s;
  if (s instanceof String) return symbol((String) s);
  return symbol(str(s));
  
}

static Symbol symbol(Object o) {
  return symbol((CharSequence) o);
}


static <A> A popLast(List<A> l) {
  return liftLast(l);
}

static <A> List<A> popLast(int n, List<A> l) {
  return liftLast(n, l);
}




static String dropSuffix(String suffix, String s) {
  return nempty(suffix) && endsWith(s, suffix) ? s.substring(0, l(s)-l(suffix)) : s;
}


static boolean networkAllowanceTest(String url) {
  
  
  return isAllowed("networkAllowanceTest", url);
  
}


static String exceptionToStringShort(Throwable e) {
  lastException(e);
  e = getInnerException(e);
  String msg = hideCredentials(unnull(e.getMessage()));
  if (msg.indexOf("Error") < 0 && msg.indexOf("Exception") < 0)
    return baseClassName(e) + prependIfNempty(": ", msg);
  else
    return msg;
}


static void sleepSeconds(double s) {
  if (s > 0) sleep(round(s*1000));
}


static <A> A printWithTime(A a) {
  return printWithTime("", a);
}

static <A> A printWithTime(String s, A a) {
  print(hmsWithColons() + ": " + s, a);
  return a;
}


static Map vm_generalSubMap(Object name) {
  synchronized(vm_generalMap()) {
    Map map =  (Map) (vm_generalMap_get(name));
    if (map == null)
      vm_generalMap_put(name, map = synchroMap());
    return map;
  }
}



static InputStream urlConnection_getInputStream(URLConnection con) throws IOException {
  return con.getInputStream();
}


static String unquote(String s) {
  if (s == null) return null;
  if (startsWith(s, '[')) {
    int i = 1;
    while (i < s.length() && s.charAt(i) == '=') ++i;
    if (i < s.length() && s.charAt(i) == '[') {
      String m = s.substring(1, i);
      if (s.endsWith("]" + m + "]"))
        return s.substring(i+1, s.length()-i-1);
    }
  }
  
  if (s.length() > 1) {
    char c = s.charAt(0);
    if (c == '\"' || c == '\'') {
      int l = endsWith(s, c) ? s.length()-1 : s.length();
      StringBuilder sb = new StringBuilder(l-1);
  
      for (int i = 1; i < l; i++) {
        char ch = s.charAt(i);
        if (ch == '\\') {
          char nextChar = (i == l - 1) ? '\\' : s.charAt(i + 1);
          // Octal escape?
          if (nextChar >= '0' && nextChar <= '7') {
              String code = "" + nextChar;
              i++;
              if ((i < l - 1) && s.charAt(i + 1) >= '0'
                      && s.charAt(i + 1) <= '7') {
                  code += s.charAt(i + 1);
                  i++;
                  if ((i < l - 1) && s.charAt(i + 1) >= '0'
                          && s.charAt(i + 1) <= '7') {
                      code += s.charAt(i + 1);
                      i++;
                  }
              }
              sb.append((char) Integer.parseInt(code, 8));
              continue;
          }
          switch (nextChar) {
          case '\"': ch = '\"'; break;
          case '\\': ch = '\\'; break;
          case 'b': ch = '\b'; break;
          case 'f': ch = '\f'; break;
          case 'n': ch = '\n'; break;
          case 'r': ch = '\r'; break;
          case 't': ch = '\t'; break;
          case '\'': ch = '\''; break;
          // Hex Unicode: u????
          case 'u':
              if (i >= l - 5) {
                  ch = 'u';
                  break;
              }
              int code = Integer.parseInt(
                      "" + s.charAt(i + 2) + s.charAt(i + 3)
                         + s.charAt(i + 4) + s.charAt(i + 5), 16);
              sb.append(Character.toChars(code));
              i += 5;
              continue;
          default:
            ch = nextChar; // added by Stefan
          }
          i++;
        }
        sb.append(ch);
      }
      return sb.toString();
    }
  }
    
  return s; // not quoted - return original
}


static String toHex(byte[] bytes) {
  return bytesToHex(bytes);
}

static String toHex(byte[] bytes, int ofs, int len) {
  return bytesToHex(bytes, ofs, len);
}



static byte[] utf8(String s) {
  return toUtf8(s);
}


static Matcher regexpMatcher(String pat, String s) {
  return compileRegexp(pat).matcher(unnull(s));
}

static Matcher regexpMatcher(java.util.regex.Pattern pat, String s) {
  return pat.matcher(unnull(s));
}


static URLConnection setURLConnectionTimeouts(URLConnection con, long timeout) {
  con.setConnectTimeout(toInt(timeout));
  con.setReadTimeout(toInt(timeout));
  if (con.getConnectTimeout() != timeout || con.getReadTimeout() != timeout)
    print("Warning: Timeouts not set by JDK.");
  return con;
}


static URLConnection setURLConnectionDefaultTimeouts(URLConnection con, long timeout) {
  if (con.getConnectTimeout() == 0) {
    con.setConnectTimeout(toInt(timeout));
    if (con.getConnectTimeout() != timeout)
      print("Warning: URL connect timeout not set by JDK.");
  }
  if (con.getReadTimeout() == 0) {
    con.setReadTimeout(toInt(timeout));
    if (con.getReadTimeout() != timeout)
      print("Warning: URL read timeout not set by JDK.");
  }
  return con;
}


static String getComputerID_quick() {
  return computerID();
}


public static File mkdirsForFile(File file) {
  File dir = file.getParentFile();
  if (dir != null) { // is null if file is in current dir
    dir.mkdirs();
    if (!dir.isDirectory())
      if (dir.isFile()) throw fail("Please delete the file " + f2s(dir) + " - it is supposed to be a directory!");
      else throw fail("Unknown IO exception during mkdirs of " + f2s(file));
  }
  return file;
}

public static String mkdirsForFile(String path) {
  mkdirsForFile(new File(path));
  return path;
}


static void _registerIO(Object object, String path, boolean opened) {
}


static File javaxDataDir_dir; // can be set to work on different base dir

static File javaxDataDir() {
  return javaxDataDir_dir != null ? javaxDataDir_dir : new File(userHome(), "JavaX-Data");
}

static File javaxDataDir(String... subs) {
  return newFile(javaxDataDir(), subs);
}


static File muricaPasswordFile() {
  return new File(javaxSecretDir(), "murica/muricaPasswordFile");
}


static ThreadLocal<VF1<File>> checkFileNotTooBigToRead_tl = new ThreadLocal();

static void checkFileNotTooBigToRead(File f) {
  callF(checkFileNotTooBigToRead_tl.get(), f);
}


static File getProgramDir() {
  return programDir();
}

static File getProgramDir(String snippetID) {
  return programDir(snippetID);
}


// TODO: optimize to x-(x%n) in case that's the same thing
// (or x-mod(x,n)?)
static int roundDownTo(int n, int x) {
  return x/n*n;
}

static long roundDownTo(long n, long x) {
  return x/n*n;
}


static boolean odd(int i) {
  return (i & 1) != 0;
}

static boolean odd(long i) {
  return (i & 1) != 0;
}

static boolean odd(BigInteger i) { return odd(toInt(i)); }


static int shorten_default = 100;

static String shorten(CharSequence s) { return shorten(s, shorten_default); }

static String shorten(CharSequence s, int max) {
  return shorten(s, max, "...");
}

static String shorten(CharSequence s, int max, String shortener) {
  if (s == null) return "";
  if (max < 0) return str(s);
  return s.length() <= max ? str(s) : subCharSequence(s, 0, min(s.length(), max-l(shortener))) + shortener;
}

static String shorten(int max, CharSequence s) { return shorten(s, max); }


static int parseHexChar(char c) {
  if (c >= '0' && c <= '9') return charDiff(c, '0');
  if (c >= 'a' && c <= 'f') return charDiff(c, 'a')+10;
  if (c >= 'A' && c <= 'F') return charDiff(c, 'A')+10;
  return -1;
}


static String actualUserHome_value;
static String actualUserHome() {
  if (actualUserHome_value == null) {
    if (isAndroid())
      actualUserHome_value = "/storage/emulated/0/";
    else
      actualUserHome_value = System.getProperty("user.home");
  }
  return actualUserHome_value;
}

static File actualUserHome(String sub) {
  return newFile(new File(actualUserHome()), sub);
}


static File userDir() {
  return new File(userHome());
}

static File userDir(String path) {
  return new File(userHome(), path);
}


static boolean possibleMD5(String s) { return isMD5(s); }


static boolean rectContains(int x1, int y1, int w, int h, Pt p) {
  return p.x >= x1 && p.y >= y1 && p.x < x1+w && p.y < y1+h;
}

static boolean rectContains(Rect a, Rect b) {
  return b.x >= a.x && b.y >= a.y && b.x2() <= a.x2() && b.y2() <= a.y2();
}

static boolean rectContains(Rect a, Rectangle b) {
  return rectContains(a, toRect(b));
}

static boolean rectContains(Rect a, int x, int y) {
  return a != null && a.contains(x, y);
}

static boolean rectContains(Rect a, Pt p) {
  return a != null && p != null && a.contains(p);
}


static <A> List<A> takeFirst(List<A> l, int n) {
  return l(l) <= n ? l : newSubListOrSame(l, 0, n);
}

static <A> List<A> takeFirst(int n, List<A> l) {
  return takeFirst(l, n);
}

static String takeFirst(int n, String s) { return substring(s, 0, n); }
static String takeFirst(String s, int n) { return substring(s, 0, n); }

static CharSequence takeFirst(int n, CharSequence s) { return subCharSequence(s, 0, n); }

static <A> List<A> takeFirst(int n, Iterator<A> it) {
  if (it == null) return null;
  List l = new ArrayList();
  for (int _repeat_0 = 0; _repeat_0 < n; _repeat_0++)  { if (it.hasNext()) l.add(it.next()); else break; }
  return l;
}

static <A> List<A> takeFirst(int n, Iterable<A> i) {
  if (i == null) return null;
  return i == null ? null : takeFirst(n, i.iterator());
}

static <A> List<A> takeFirst(int n, IterableIterator<A> i) {
  return takeFirst(n, (Iterator<A>) i);
}

static int[] takeFirst(int n, int[] a) { return takeFirstOfIntArray(n, a); }

static short[] takeFirst(int n, short[] a) { return takeFirstOfShortArray(n, a); }

static byte[] takeFirst(int n, byte[] a) { return takeFirstOfByteArray(n, a); }
static byte[] takeFirst(byte[] a, int n) { return takeFirstOfByteArray(n, a); }

static double[] takeFirst(int n, double[] a) { return takeFirstOfDoubleArray(n, a); }
static double[] takeFirst(double[] a, int n) { return takeFirstOfDoubleArray(n, a); }


static String intToHex_flexLength(int i) {
  return Integer.toHexString(i);
}


static File loadLibrary(String snippetID) {
  return loadBinarySnippet(snippetID);
}


static <A> Set<A> newWeakHashSet() {
  return synchroWeakHashSet();
}


static boolean isLocalSnippetID(String snippetID) {
  return isSnippetID(snippetID) && isLocalSnippetID(psI(snippetID));
}

static boolean isLocalSnippetID(long snippetID) {
  return snippetID >= 1000 && snippetID <= 9999;
}


static String localSnippetTitle(String snippetID) {
  if (!isLocalSnippetID(snippetID)) return null;
  File f = localSnippetFile(snippetID);
  if (!f.exists()) return null;
  return or2(getFileInfoField(dropExtension(f), "Title"), "Unnamed");
}


static String muricaCredentialsQuery() {
  return htmlQuery(muricaCredentials());
}


static boolean isGeneralFileServerSnippet(long id) {
  return id >= 1400000 && id < 1500000;
}


static String standardCredentials_noCookies() {
  return standardCredentials() + "&noCookies=1";
}


static boolean saveTextFileIfChanged(File f, String contents) {
  return saveTextFileIfDifferent(f, contents);
}


static File snippetTitle_cacheFile(String snippetID) {
  return javaxCachesDir("Snippet Titles/" + psI(snippetID));
}


static String formatSnippetIDOpt(String s) {
  return isSnippetID(s) ? formatSnippetID(s) : s;
}


static Class getMainClass() {
  return mc();
}

static Class getMainClass(Object o) { try {
  if (o == null) return null;
  if (o instanceof Class && eq(((Class) o).getName(), "x30")) return (Class) o;
  ClassLoader cl = (o instanceof Class ? (Class) o : o.getClass()).getClassLoader();
  if (cl == null) return null;
  String name = mainClassNameForClassLoader(cl);
  return loadClassFromClassLoader_orNull(cl, name);
} catch (Exception __e) { throw rethrow(__e); } }


static <A> A getWeakRef(Reference<A> ref) {
  return ref == null ? null : ref.get();
}


static x30_pkg.x30_util.BetterThreadLocal<WeakReference> dm_current_generic_tl;

static x30_pkg.x30_util.BetterThreadLocal<WeakReference> dm_current_generic_tl() {
  if (dm_current_generic_tl == null)
    dm_current_generic_tl = vm_generalMap_getOrCreate("currentModule", () -> new x30_pkg.x30_util.BetterThreadLocal());
  return dm_current_generic_tl;
}


static <A> WeakReference<A> newWeakReference(A a) {
  return a == null ? null : new WeakReference(a);
}


static boolean emptyString(String s) {
  return s == null || s.length() == 0;
}


static String getLookAndFeel() {
  return getClassName(UIManager.getLookAndFeel());
}


static JComponent getInternalFrameTitlePaneComponent(JInternalFrame f) {
  return (JComponent) childWithClassNameEndingWith(f, "InternalFrameTitlePane");
}



static JInternalFrame getInternalFrame(final Object _o) {
  return _o == null ? null : swing(new F0<JInternalFrame>() { public JInternalFrame get() { try { 
    Object o = _o;
    if (o instanceof ButtonGroup) o = first(buttonsInGroup((ButtonGroup) o));
    if (!(o instanceof Component)) return null;
    Component c = (Component) o;
    while (c != null) {
      if (c instanceof JInternalFrame) return (JInternalFrame) c;
      c = c.getParent();
    }
    return null;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "O o = _o;\r\n    if (o instanceof ButtonGroup) o = first(buttonsInGroup((Button..."; }});
}


static Window getWindow(Object o) {
  if (!(o instanceof Component)) return null;
  return swing(() -> {
    Component c =  (Component) o;
    while (c != null) {
      if (c instanceof Window) return ((Window) c);
      c = c.getParent();
    }
    return null;
  });
}


static <A> A _get(List<A> l, int idx) {
  return l != null && idx >= 0 && idx < l(l) ? l.get(idx) : null;
}

static Object _get(Object o, String field) {
  return get(o, field);
}

static Object _get(String field, Object o) {
  return get(o, field);
}
static <A> A _get(A[] l, int idx) {
  return idx >= 0 && idx < l(l) ? l[idx] : null;
}


static MouseListener findComponentPopupMenuListener_gen(final JComponent c) {
  return c == null ? null : swing(() ->
    firstWithClassShortNamed("componentPopupMenu_Adapter", c.getMouseListeners())
  );
}


static boolean internalFrameActive(Component c) {
  final JInternalFrame f = getInternalFrame(c);
  return f != null && swing(new F0<Boolean>() { public Boolean get() { try {  return f.isSelected();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return f.isSelected();"; }});
}


static <A> AutoCloseable tempSetTL(ThreadLocal<A> tl, A a) {
  return tempSetThreadLocal(tl, a);
}




static Class hotwireOnce(String programID) {
  return hotwireCached(programID, false);
}


static boolean isJavaIdentifier(String s) {
  if (empty(s) || !Character.isJavaIdentifierStart(s.charAt(0)))
    return false;
  for (int i = 1; i < s.length(); i++)
    if (!Character.isJavaIdentifierPart(s.charAt(i)))
      return false;
  return true;
}


// supports the usual quotings (", variable length double brackets) except ' quoting
static boolean isQuoted(String s) {
  
  
  if (isNormalQuoted(s)) return true; // use the exact version
  
  return isMultilineQuoted(s);
}


static String[] toStringArray(Collection<String> c) {
  String[] a = new String[l(c)];
  Iterator<String> it = c.iterator();
  for (int i = 0; i < l(a); i++)
    a[i] = it.next();
  return a;
}

static String[] toStringArray(Object o) {
  if (o instanceof String[])
    return (String[]) o;
  else if (o instanceof Collection)
    return toStringArray((Collection<String>) o);
  else
    throw fail("Not a collection or array: " + getClassName(o));
}



static JTextField onEnter(JTextField tf, JButton btn) {
  if (btn != null)
    onEnter(tf, new Runnable() {  public void run() { try {  clickButton(btn) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "clickButton(btn)"; }});
  return tf;
}

static JTextField onEnter(JTextField tf, Object action) {
  if (action == null || tf == null) return tf;
  tf.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent _evt) { try {
    tf.selectAll();
    callF(action);
  } catch (Throwable __e) { messageBox(__e); }}});
  return tf;
}

static JButton onEnter(JButton btn, final Object action) {
  if (action == null || btn == null) return btn;
  btn.addActionListener(actionListener(action));
  return btn;
}

static JList onEnter(JList list, Object action) {
  list.addKeyListener(enterKeyListener(rCallOnSelectedListItem(list, action)));
  return list;
}

static JComboBox onEnter(final JComboBox cb, Runnable action) {
  { swing(() -> { 
    if (cb.isEditable()) {
      JTextField text = (JTextField) cb.getEditor().getEditorComponent();
      onEnter(text, action);
    } else {
      cb.getInputMap().put(KeyStroke.getKeyStroke("ENTER"), "enter");
      cb.getActionMap().put("enter", abstractAction("", new Runnable() {  public void run() { try {  cb.hidePopup(); callF(action); 
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "cb.hidePopup(); callF(action);"; }}));
    }
  }); }
  return cb;
}

static JTable onEnter(final JTable table, final Object action) {  
  table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT)
    .put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "Enter");
    
  table.getActionMap().put("Enter", new AbstractAction() {
    public void actionPerformed(ActionEvent e) {
      callF(action, table.getSelectedRow());
    }
  });
  return table;
}

/*static JTextArea onEnter(final JTextArea ta, fO action) {
  addKeyListener(ta, enterKeyListener(action));
  ret ta;
}*/

static JTextField onEnter(Runnable action, JTextField tf) {
  return onEnter(tf, action);
}




static Rect toRect(Rectangle r) {
  return r == null ? null : new Rect(r);
}

static Rect toRect(RectangularShape r) {
  return r == null ? null : toRect(r.getBounds());
}


static Rect toRect(DoubleRect r) {
  if (r == null) return null;
  int x = iround(r.x), y = iround(r.y);
  return new Rect(x, y, iround(r.x2())-x, iround(r.y2())-y);
}


static Rect toRect(Rect r) { return r; }


static List<GraphicsDevice> screenDevices() {
  return asList(GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices());
}


static Random customRandomizerForThisThread() {
  return customRandomizerForThisThread_tl().get();
}


static <A> A listGet(List<A> l, int idx) {
  return l != null && idx >= 0 && idx < l(l) ? l.get(idx) : null;
}


static BufferedReader bufferedReader(Reader r) { return bufferedReader(r, 8192); }
static BufferedReader bufferedReader(Reader r, int bufSize) {
  if (r == null) return null;
  return r instanceof BufferedReader ? (BufferedReader) r : _registerIOWrap(new BufferedReader(r, bufSize), r);
}


static <A extends AutoCloseable> A holdResource(IResourceHolder holder, A a) {
  { if (holder != null) holder.add(a); }
  return a;
}


static <A> CloseableIterableIterator<A> iteratorFromFunction_f0_autoCloseable(final F0<A> f, final AutoCloseable closeable) {
  class IFF2 extends CloseableIterableIterator<A> {
    A a;
    boolean done = false;
    
    public boolean hasNext() {
      getNext();
      return !done;
    }
    
    public A next() {
      getNext();
      if (done) throw fail();
      A _a = a;
      a = null;
      return _a;
    }
    
    void getNext() {
      if (done || a != null) return;
      a = f.get();
      done = a == null;
    }
    
    public void close() throws Exception {
      if (closeable != null) closeable.close();
    }
  };
  return new IFF2();
}


static String readLineFromReaderWithClose(BufferedReader r) { try {
  String s = r.readLine();
  if (s == null) r.close();
  return s;
} catch (Exception __e) { throw rethrow(__e); } }


static AutoCloseable _wrapIOCloseable(final AutoCloseable c) {
  return c == null ? null : new AutoCloseable() { public String toString() { return "c.close();\r\n    _registerIO(c, null, false);"; } public void close() throws Exception { c.close();
    _registerIO(c, null, false);
  }};
}



static <A> A _registerIOWrap(A wrapper, Object wrapped) {
  return wrapper;
}


static FileInputStream newFileInputStream(File path) throws IOException {
  return newFileInputStream(path.getPath());
}

static FileInputStream newFileInputStream(String path) throws IOException {
  FileInputStream f = new FileInputStream(path);
  _registerIO(f, path, true);
  return f;
}


static int gzInputStream_defaultBufferSize = 65536;

static GZIPInputStream gzInputStream(File f) { try {
  return gzInputStream(new FileInputStream(f));
} catch (Exception __e) { throw rethrow(__e); } }

static GZIPInputStream gzInputStream(File f, int bufferSize) { try {
  return gzInputStream(new FileInputStream(f), bufferSize);
} catch (Exception __e) { throw rethrow(__e); } }

static GZIPInputStream gzInputStream(InputStream in) {
  return gzInputStream(in, gzInputStream_defaultBufferSize);
}

static GZIPInputStream gzInputStream(InputStream in, int bufferSize) { try {
  return _registerIOWrap(new GZIPInputStream(in, gzInputStream_defaultBufferSize), in);
} catch (Exception __e) { throw rethrow(__e); } }




static <A> A liftLast(List<A> l) {
  if (empty(l)) return null;
  int i = l(l)-1;
  A a = l.get(i);
  l.remove(i);
  return a;
}

static <A> List<A> liftLast(int n, List<A> l) {
  int i = l(l)-n;
  List<A> part = cloneSubList(l, i);
  removeSubList(l, i);
  return part;
}


static boolean endsWith(String a, String b) {
  return a != null && a.endsWith(b);
}

static boolean endsWith(String a, char c) {
  return nempty(a) && lastChar(a) == c;
}


  static boolean endsWith(String a, String b, Matches m) {
    if (!endsWith(a, b)) return false;
    m.m = new String[] {dropLast(l(b), a)};
    return true;
  }




static volatile Object isAllowed_function; // func(S, O[]) -> bool
static volatile boolean isAllowed_all = true;

static boolean isAllowed(String askingMethod, Object... args) {
  // check on VM level
  Object f = vm_generalMap_get("isAllowed_function");
  if (f != null && !isTrue(callF(f, askingMethod, args))) return false;
  
  // check locally
  return isAllowed_all || isTrue(callF(isAllowed_function, askingMethod, args));
}


static String baseClassName(String className) {
  return substring(className, className.lastIndexOf('.')+1);
}

static String baseClassName(Object o) {
  return baseClassName(getClassName(o));
}


static String prependIfNempty(String prefix, String s) {
  return empty(s) ? unnull(s) : prefix + s;
}


static volatile boolean sleep_noSleep = false;

static void sleep(long ms) {
  ping();
  if (ms < 0) return;
  // allow spin locks
  if (isAWTThread() && ms > 100) throw fail("Should not sleep on AWT thread");
  try {
    Thread.sleep(ms);
  } catch (Exception e) { throw new RuntimeException(e); }
}

static void sleep() { try {
  if (sleep_noSleep) throw fail("nosleep");
  print("Sleeping.");
  sleepQuietly();
} catch (Exception __e) { throw rethrow(__e); } }


static long round(double d) {
  return Math.round(d);
}

static String round(String s) {
  return roundBracket(s);
}


static Complex round(Complex c) {
  return new Complex(round(c.re), round(c.im));
}





static String hmsWithColons() {
  return hmsWithColons(now());
}

static String hmsWithColons(long time) {
  return new SimpleDateFormat("HH:mm:ss").format(time);
}



  public static String bytesToHex(byte[] bytes) {
    return bytesToHex(bytes, 0, bytes.length);
  }

  public static String bytesToHex(byte[] bytes, int ofs, int len) {
    StringBuilder stringBuilder = new StringBuilder(len*2);
    for (int i = 0; i < len; i++) {
      String s = "0" + Integer.toHexString(bytes[ofs+i]);
      stringBuilder.append(s.substring(s.length()-2, s.length()));
    }
    return stringBuilder.toString();
  }



static byte[] toUtf8(String s) { try {
  return s.getBytes(utf8charset());
} catch (Exception __e) { throw rethrow(__e); } }


static Map<String, java.util.regex.Pattern> compileRegexp_cache = syncMRUCache(10);

static java.util.regex.Pattern compileRegexp(String pat) {
  java.util.regex.Pattern p = compileRegexp_cache.get(pat);
  if (p == null) {
    
    compileRegexp_cache.put(pat, p = java.util.regex.Pattern.compile(pat));
  }
  return p;
}


static String _computerID;
static Lock computerID_lock = lock();

public static String computerID() {
  if (_computerID == null) {
    Lock __0 = computerID_lock; lock(__0); try {
    if (_computerID != null) return _computerID;
    File file = computerIDFile();
    _computerID = loadTextFile(file.getPath());
    if (_computerID == null) {
      // legacy load
      _computerID = loadTextFile(userDir(".tinybrain/computer-id"));
      if (_computerID == null)
        _computerID = makeRandomID(12, new SecureRandom());
      saveTextFile(file, _computerID);
    }
  } finally { unlock(__0); } }
  return _computerID;
}


static File javaxSecretDir_dir; // can be set to work on different base dir

static File javaxSecretDir() {
  return javaxSecretDir_dir != null ? javaxSecretDir_dir : new File(userHome(), "JavaX-Secret");
}

static File javaxSecretDir(String sub) {
  return newFile(javaxSecretDir(), sub);
}


static File programDir_mine; // set this to relocate program's data

static File programDir() {
  return programDir(getProgramID());
}

static File programDir(String snippetID) {
  boolean me = sameSnippetID(snippetID, programID());
  if (programDir_mine != null && me)
    return programDir_mine;
  File dir = new File(javaxDataDir(), formatSnippetIDOpt(snippetID));
  if (me) {
    String c = caseID();
    if (nempty(c)) dir = newFile(dir, c);
  }
  return dir;
}

static File programDir(String snippetID, String subPath) {
  return new File(programDir(snippetID), subPath);
}


static CharSequence subCharSequence(CharSequence s, int x) {
  return subCharSequence(s, x, s == null ? 0 : s.length());
}

static CharSequence subCharSequence(CharSequence s, int x, int y) {
  if (s == null) return null;
  if (x < 0) x = 0;
  if (x >= s.length()) return "";
  if (y < x) y = x;
  if (y > s.length()) y = s.length();
  return s.subSequence(x, y);
}


static int charDiff(char a, char b) {
  return (int) a-(int) b;
}

static int charDiff(String a, char b) {
  return charDiff(stringToChar(a), b);
}


static boolean isMD5(String s) {
  return l(s) == 32 && isLowerHexString(s);
}


static <A> List<A> newSubListOrSame(List<A> l, int startIndex) {
  return newSubListOrSame(l, startIndex, l(l));
}

static <A> List<A> newSubListOrSame(List<A> l, int startIndex, int endIndex) {
  if (l == null) return null;
  int n = l(l);
  startIndex = max(0, startIndex);
  endIndex = min(n, endIndex);
  if (startIndex >= endIndex) return ll();
  if (startIndex == 0 && endIndex == n) return l;
  return cloneList(l.subList(startIndex, endIndex));
}


static <A> List<A> newSubListOrSame(List<A> l, IntRange r) {
  return newSubListOrSame(l, r.start, r.end);
}



static int[] takeFirstOfIntArray(int[] b, int n) {
  return subIntArray(b, 0, n);
}

static int[] takeFirstOfIntArray(int n, int[] b) {
  return takeFirstOfIntArray(b, n);
}


static short[] takeFirstOfShortArray(short[] b, int n) {
  return subShortArray(b, 0, n);
}

static short[] takeFirstOfShortArray(int n, short[] b) {
  return takeFirstOfShortArray(b, n);
}


static byte[] takeFirstOfByteArray(byte[] b, int n) {
  return subByteArray(b, 0, n);
}

static byte[] takeFirstOfByteArray(int n, byte[] b) {
  return takeFirstOfByteArray(b, n);
}


static double[] takeFirstOfDoubleArray(double[] b, int n) {
  return subDoubleArray(b, 0, n);
}

static double[] takeFirstOfDoubleArray(int n, double[] b) {
  return takeFirstOfDoubleArray(b, n);
}


static <A> Set<A> synchroWeakHashSet() {
  return Collections.newSetFromMap((Map) newWeakHashMap());
}


static File localSnippetFile(long snippetID) {
  return localSnippetsDir(snippetID + ".text");
}

static File localSnippetFile(String snippetID) {
  return localSnippetFile(parseSnippetID(snippetID));
}


static String getFileInfoField(File f, String field) {
  return getOneLineFileInfoField(f, field);
}


static File dropExtension(File f) {
  return f == null ? null : fileInSameDir(f, dropExtension(f.getName()));
}

static String dropExtension(String s) {
  return takeFirst(s, smartLastIndexOf(s, '.'));
}


static String htmlQuery(Map params) {
  return empty(params) ? "" : "?" + makePostData(params);
}

static String htmlQuery(Object... data) {
  return empty(data) ? "" : "?" + makePostData(data);
}


static Object[] muricaCredentials() {
  String pass = muricaPassword();
  return nempty(pass) ? new Object[] {"_pass", pass } : new Object[0];
}


static String standardCredentials() {
  String user = standardCredentialsUser();
  String pass = standardCredentialsPass();
  if (nempty(user) && nempty(pass))
    return "&_user=" + urlencode(user) + "&_pass=" + urlencode(pass);
  return "";
}


static boolean saveTextFileIfDifferent(File f, String contents) {
  if (eq(loadTextFile(f), contents)) return false; // TODO: optimize
  { saveTextFile(f, contents); return true; }
}


static Class loadClassFromClassLoader_orNull(ClassLoader cl, String name) {
  try {
    return cl == null ? null : cl.loadClass(name);
  } catch (ClassNotFoundException e) {
    return null;
  }
}


static Component childWithClassNameEndingWith(Component c, String suffix) {
  if (endsWith(className(c), suffix)) return c;
  Component x;
  for (Component comp : getComponents(c))
    if ((x = childWithClassNameEndingWith(comp, suffix)) != null) return x;
  return null;
}


static <A> A firstWithClassShortNamed(String shortName, Iterable<A> l) {
  if (l != null) for (A o : l)
    if (eq(shortClassName(o), shortName))
      return o;
  return null;
}

static <A> A firstWithClassShortNamed(String shortName, A[] l) {
  if (l != null) for (A o : l)
    if (eq(shortClassName(o), shortName))
      return o;
  return null;
}


static <A> AutoCloseable tempSetThreadLocalIfNecessary(ThreadLocal<A> tl, A a) {
  if (tl == null) return null;
  A prev = tl.get();
  if (eq(prev, a)) return null;
  tl.set(a);
  return new AutoCloseable() { public String toString() { return "tl.set(prev);"; } public void close() throws Exception { tl.set(prev); }};
}




static TreeMap<String, Class> hotwireCached_cache = new TreeMap();
static Lock hotwireCached_lock = lock();

static Class hotwireCached(String programID) {
  return hotwireCached(programID, true);
}

static Class hotwireCached(String programID, boolean runMain) {
  return hotwireCached(programID, runMain, false);
}

static Class hotwireCached(String programID, boolean runMain, boolean dependent) {
  Lock __0 = hotwireCached_lock; lock(__0); try {
  
  programID = formatSnippetID(programID);
  Class c = hotwireCached_cache.get(programID);
  if (c == null) {
    c = hotwire(programID);
    if (dependent)
      makeDependent(c);
    if (runMain)
      callMain(c);
    hotwireCached_cache.put(programID, c);
  }
  return c;
} finally { unlock(__0); } }


static boolean isNormalQuoted(String s) {
  int l = l(s);
  if (!(l >= 2 && s.charAt(0) == '"' && lastChar(s) == '"')) return false;
  int j = 1;
  while (j < l)
    if (s.charAt(j) == '"')
      return j == l-1;
    else if (s.charAt(j) == '\\' && j+1 < l)
      j += 2;
    else
      ++j;
  return false;
}


static boolean isMultilineQuoted(String s) {
  if (!startsWith(s, "[")) return false;
  int i = 1;
  while (i < s.length() && s.charAt(i) == '=') ++i;
  return i < s.length() && s.charAt(i) == '[';
}


static void clickButton(final JButton b) {
  if (b != null) { swing(() -> { 
    if (b.isEnabled())
      b.doClick();
  }); }
}


static KeyListener enterKeyListener(final Object action) {
  return new KeyAdapter() {
    public void keyPressed(KeyEvent ke) {
      if (ke.getKeyCode() == KeyEvent.VK_ENTER)
        pcallF(action);
    }
  };
}


static Runnable rCallOnSelectedListItem(final JList list, final Object action) {
  return new Runnable() {  public void run() { try {  pcallF(action, getSelectedItem(list)) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "pcallF(action, getSelectedItem(list))"; }};
}


static ThreadLocal<Random> customRandomizerForThisThread_tl = new ThreadLocal();

static ThreadLocal<Random> customRandomizerForThisThread_tl() {
  return customRandomizerForThisThread_tl;
}




static <A> List<A> cloneSubList(List<A> l, int startIndex, int endIndex) {
  return newSubList(l, startIndex, endIndex);
}

static <A> List<A> cloneSubList(List<A> l, int startIndex) {
  return newSubList(l, startIndex);
}


static void removeSubList(List l, int from, int to) {
  if (l != null) subList(l, from, to).clear();
}

static void removeSubList(List l, int from) {
  if (l != null) subList(l, from).clear();
}


static char lastChar(String s) {
  return empty(s) ? '\0' : s.charAt(l(s)-1);
}


static <A> A[] dropLast(A[] a) { return dropLast(a, 1); }
static <A> A[] dropLast(A[] a, int n) {
  if (a == null) return null;
  n = Math.min(n, a.length);
  A[] b = arrayOfSameType(a, a.length-n);
  System.arraycopy(a, 0, b, 0, b.length);
  return b;
}

static <A> List<A> dropLast(List<A> l) {
  return subList(l, 0, l(l)-1);
}

static <A> List<A> dropLast(int n, List<A> l) {
  return subList(l, 0, l(l)-n);
}

static <A> List<A> dropLast(Iterable<A> l) {
  return dropLast(asList(l));
}

static String dropLast(String s) {
  return substring(s, 0, l(s)-1);
}

static String dropLast(String s, int n) {
  return substring(s, 0, l(s)-n);
}

static String dropLast(int n, String s) {
  return dropLast(s, n);
}



static Object sleepQuietly_monitor = new Object();

static void sleepQuietly() { try {
  assertFalse(isAWTThread());
  synchronized(sleepQuietly_monitor) { sleepQuietly_monitor.wait(); }
} catch (Exception __e) { throw rethrow(__e); } }


static String roundBracket(String s) {
  return "(" + s + ")";
}

static String roundBracket(Object s) {
  return roundBracket(str(s));
}


static long now_virtualTime;
static long now() {
  return now_virtualTime != 0 ? now_virtualTime : System.currentTimeMillis();
}





static Charset utf8charset_cache;
static Charset utf8charset() { if (utf8charset_cache == null) utf8charset_cache = utf8charset_load(); return utf8charset_cache;}

static Charset utf8charset_load() {
  return Charset.forName("UTF-8");
}


static <A, B> Map<A, B> syncMRUCache(int size) {
  return synchroMap(new MRUCache(size));
}


static File computerIDFile() {
  return javaxDataDir("Basic Info/computer-id.txt");
}


static String makeRandomID(int length) {
  return makeRandomID(length, defaultRandomGenerator());
}

static String makeRandomID(int length, Random random) {
  char[] id = new char[length];
  for (int i = 0; i < id.length; i++)
    id[i] = (char) ((int) 'a' + random.nextInt(26));
  return new String(id);
}

static String makeRandomID(Random r, int length) {
  return makeRandomID(length, r);
}


/** writes safely (to temp file, then rename) */
static File saveTextFile(String fileName, String contents) throws IOException {
  /*ifdef CriticalActions
  temp beginCriticalAction("Saving file " + fileName + " (" + l(contents) + " chars)");
  endifdef*/
  
  File file = new File(fileName);
  mkdirsForFile(file);
  String tempFileName = fileName + "_temp";
  File tempFile = new File(tempFileName);
  if (contents != null) {
    if (tempFile.exists()) try {
      String saveName = tempFileName + ".saved." + now();
      copyFile(tempFile, new File(saveName));
    } catch (Throwable e) { printStackTrace(e); }
    FileOutputStream fileOutputStream = newFileOutputStream(tempFile.getPath());
    OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream, "UTF-8");
    PrintWriter printWriter = new PrintWriter(outputStreamWriter);
    printWriter.print(contents);
    printWriter.close();
  }
  
  if (file.exists() && !file.delete())
    throw new IOException("Can't delete " + fileName);

  if (contents != null)
    if (!tempFile.renameTo(file))
      throw new IOException("Can't rename " + tempFile + " to " + file);
      
  
  vmBus_send("wroteFile", file);
  
  return file;
}

static File saveTextFile(File fileName, String contents) { try {
  saveTextFile(fileName.getPath(), contents);
  return fileName;
} catch (Exception __e) { throw rethrow(__e); } }


static boolean sameSnippetID(String a, String b) {
  if (!isSnippetID(a) || !isSnippetID(b)) return false;
  return parseSnippetID(a) == parseSnippetID(b);
}


static volatile String caseID_caseID;

static String caseID() { return caseID_caseID; }

static void caseID(String id) {
  caseID_caseID = id;
}


static char stringToChar(String s) {
  if (l(s) != 1) throw fail("bad stringToChar: " + s);
  return firstChar(s);
}


static boolean isLowerHexString(String s) {
  for (int i = 0; i < l(s); i++) {
    char c = s.charAt(i);
    if (c >= '0' && c <= '9' || c >= 'a' && c <= 'f') {
      // ok
    } else
      return false;
  }
  return true;
}


static int[] subIntArray(int[] b, int start) {
  return subIntArray(b, start, l(b));
}
  
static int[] subIntArray(int[] b, int start, int end) {
  start = max(start, 0); end = min(end, l(b));
  if (start == 0 && end == l(b)) return b;
  if (start >= end) return new int[0];
  int[] x = new int[end-start];
  System.arraycopy(b, start, x, 0, end-start);
  return x;
}


static int[] subIntArray(int[] a, IntRange r) {
  return r == null ? null : subIntArray(a, r.start, r.end);
}



static short[] subShortArray(short[] b, int start, int end) {
  start = max(start, 0); end = min(end, l(b));
  if (start == 0 && end == l(b)) return b;
  if (start >= end) return new short[0];
  short[] x = new short[end-start];
  System.arraycopy(b, start, x, 0, end-start);
  return x;
}


static byte[] subByteArray(byte[] b, int start) {
  return subByteArray(b, start, l(b));
}
  
static byte[] subByteArray(byte[] b, int start, int end) {
  start = max(start, 0); end = min(end, l(b));
  if (start == 0 && end == l(b)) return b;
  if (start >= end) return new byte[0];
  byte[] x = new byte[end-start];
  System.arraycopy(b, start, x, 0, end-start);
  return x;
}


static byte[] subByteArray(byte[] b, IntRange r) {
  return r == null ? null : subByteArray(b, r.start, r.end);
}



static double[] subDoubleArray(double[] b, int start) { return subDoubleArray(b, start, l(b)); }
static double[] subDoubleArray(double[] b, int start, int end) {
  start = max(start, 0); end = min(end, l(b));
  if (start == 0 && end == l(b)) return b;
  if (start >= end) return new double[0];
  double[] x = new double[end-start];
  System.arraycopy(b, start, x, 0, end-start);
  return x;
}


static File localSnippetsDir() {
  return javaxDataDir("Personal Programs");
}

static File localSnippetsDir(String sub) {
  return newFile(localSnippetsDir(), sub);
}


static String getOneLineFileInfoField(File f, String field) {
  File infoFile = associatedInfosFile(f);
  List<String> lines = lines(loadTextFile(infoFile));
  return firstStartingWithIC_drop(lines, field + ": ");
}


static File fileInSameDir(File f, String newName) {
  return newFile(parentFile(f), newName);
}


static int smartLastIndexOf(String s, char c) {
  if (s == null) return 0;
  int i = s.lastIndexOf(c);
  return i >= 0 ? i : l(s);
}

static <A> int smartLastIndexOf(List<A> l, A sub) {
  int i = lastIndexOf(l, sub);
  return i < 0 ? l(l) : i;
}


static String makePostData(Map map) {
  StringBuilder buf = new StringBuilder();
  for (Map.Entry<Object, Object> e : castMapToMapO(map).entrySet()) {
    String key =  (String) (e.getKey());
    Object val = e.getValue();
    if (val != null) {
      String value = str(val);
      if (nempty(buf)) buf.append("&");
      buf.append(urlencode(key)).append("=").append(urlencode(/*escapeMultichars*/(value)));
    }
  }
  return str(buf);
}

static String makePostData(Object... params) {
  StringBuilder buf = new StringBuilder();
  int n = l(params);
  for (int i = 0; i+1 < n; i += 2) {
    String key =  (String) (params[i]);
    Object val = params[i+1];
    if (val != null) {
      String value = str(val);
      if (nempty(buf)) buf.append("&");
      buf.append(urlencode(key)).append("=").append(urlencode(/*escapeMultichars*/(value)));
    }
  }
  return str(buf);

}



static String standardCredentialsUser() {
  return trim(loadTextFile(
    oneOfTheFiles(
      javaxSecretDir("tinybrain-username"),
      userDir(".tinybrain/username"))));
}


static String standardCredentialsPass() {
  return trim(loadTextFile(
    oneOfTheFiles(
      javaxSecretDir("tinybrain-userpass"),
      userDir(".tinybrain/userpass"))));
}


static String urlencode(String x) {
  try {
    return URLEncoder.encode(unnull(x), "UTF-8");
  } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); }
}


static List<Component> getComponents(final Component c) {
  return !(c instanceof Container) ? emptyList() : asList(swing(new F0<Component[]>() { public Component[] get() { try {  return ((Container) c).getComponents();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return ((Container) c).getComponents();"; }}));
}


static Object makeDependent_postProcess;

static void makeDependent(Object c) {
  if (c == null) return;
  assertTrue("Not a class", c instanceof Class);
  dependentClasses(); // cleans up the list
  hotwire_classes.add(new WeakReference(c));
  
  Object local_log = getOpt(mc(), "local_log");
  if (local_log != null)
    setOpt(c, "local_log", local_log);
    
  /*if (isTrue(getOpt(c, 'ping_actions_shareable)))
    setOpt(c, +ping_actions);*/
    
  Object print_byThread = getOpt(mc(), "print_byThread");
  if (print_byThread != null)
    setOpt(c, "print_byThread", print_byThread);
    
  callF(makeDependent_postProcess, c);
}



static String getSelectedItem(JList l) {
  return (String) l.getSelectedValue();
}

static String getSelectedItem(JComboBox cb) {
  return strOrNull(cb.getSelectedItem());
}




static <A> List<A> newSubList(List<A> l, int startIndex, int endIndex) {
  return cloneList(subList(l, startIndex, endIndex));
}

static <A> List<A> newSubList(List<A> l, int startIndex) {
  return cloneList(subList(l, startIndex));
}


static <A> List<A> subList(List<A> l, int startIndex) {
  return subList(l, startIndex, l(l));
}

static <A> List<A> subList(int startIndex, List<A> l) {
  return subList(l, startIndex);
}

static <A> List<A> subList(int startIndex, int endIndex, List<A> l) {
  return subList(l, startIndex, endIndex);
}

static <A> List<A> subList(List<A> l, int startIndex, int endIndex) {
  if (l == null) return null;
  int n = l(l);
  startIndex = Math.max(0, startIndex);
  endIndex = Math.min(n, endIndex);
  if (startIndex > endIndex) return ll();
  if (startIndex == 0 && endIndex == n) return l;
  
  
    return l.subList(startIndex, endIndex);
  
}


static <A> List<A> subList(List<A> l, IntRange r) {
  return subList(l, r.start, r.end);
}



static <A> A[] arrayOfSameType(A[] a, int n) {
  return newObjectArrayOfSameType(a, n);
}


static File copyFile(File src, File dest) { try {
  FileInputStream inputStream = new FileInputStream(src.getPath());
  FileOutputStream outputStream = newFileOutputStream(dest.getPath());
  try {
    copyStream(inputStream, outputStream);
    inputStream.close();
  } finally {
    outputStream.close();
  }
  return dest;
} catch (Exception __e) { throw rethrow(__e); } }


static char firstChar(String s) {
  return s.charAt(0);
}


static File associatedInfosFile(File f) {
  return replaceExtension(f, ".infos");
}


static String firstStartingWithIC_drop(Collection<String> l, final String prefix) {
  for (String s : unnull(l))
    if (swic(s, prefix))
      return substring(s, l(prefix));
  return null;
}

static String firstStartingWithIC_drop(String prefix, Collection<String> l) {
  return firstStartingWithIC_drop(l, prefix);
}


static File parentFile(File f) {
  return dirOfFile(f);
}


static Map<Object, Object> castMapToMapO(Map map) {
  return map;
}


static File oneOfTheFiles(String... paths) {
  if (paths != null) for (String path : paths)
    if (fileExists(path))
      return newFile(path);
  return null;
}

static File oneOfTheFiles(File... files) {
  return oneOfTheFiles(asList(files));
}

static File oneOfTheFiles(Iterable<File> files) {
  if (files != null) for (File f : files)
    if (fileExists(f))
      return f;
  return null;
}


static List<Class> dependentClasses() {
  return cleanUpAndGetWeakReferencesList(hotwire_classes);
}


static String strOrNull(Object o) {
  return o == null ? null : str(o);
}




static List<WeakReference<Class>> hotwire_classes = synchroList();

static Class<?> hotwireDependent(String src) {
  Class c = hotwire(src);
  makeDependent(c);
  return c;
}



static <A> A[] newObjectArrayOfSameType(A[] a) { return newObjectArrayOfSameType(a, a.length); }
static <A> A[] newObjectArrayOfSameType(A[] a, int n) {
  return (A[]) Array.newInstance(a.getClass().getComponentType(), n);
}


static void copyStream(InputStream in, OutputStream out) { try {
  byte[] buf = new byte[65536];
  while (true) {
    int n = in.read(buf);
    if (n <= 0) return;
    out.write(buf, 0, n);
  }
} catch (Exception __e) { throw rethrow(__e); } }


static File replaceExtension(File f, String extOld, String extNew) {
  return newFile(replaceExtension(f2s(f), extOld, extNew));
}

static File replaceExtension(File f, String extNew) {
  return replaceExtension(f, fileExtension(f), extNew);
}

static String replaceExtension(String s, String extOld, String extNew) {
  s = dropSuffixIC(addPrefixOptIfNempty(".", extOld), s);
  return s + addPrefixOptIfNempty(".", extNew);
}

static String replaceExtension(String name, String extNew) {
  return replaceExtension(name, fileExtension(name), extNew);
}


static File dirOfFile(File f) {
  return f == null ? null : f.getParentFile();
}


static boolean fileExists(String path) {
  return path != null && new File(path).exists();
}

static boolean fileExists(File f) {
  return f != null && f.exists();
}


static <A> List<A> cleanUpAndGetWeakReferencesList(List<WeakReference<A>> l) {
  if (l == null) return null;
  synchronized(l) {
    List<A> out = new ArrayList();
    for (int i = 0; i < l(l); i++) {
      A a = l.get(i).get();
      if (a == null)
        l.remove(i--);
      else
        out.add(a);
    }
    return out;
  }
}




static String fileExtension(File f) {
  if (f == null) return null;
  return fileExtension(f.getName());
}

static String fileExtension(String s) {
  return substring(s, smartLastIndexOf(s, '.'));
}


static String dropSuffixIC(String suffix, String s) {
  return s == null ? null : ewic(s, suffix) ? s.substring(0, l(s)-l(suffix)) : s;
}


static String addPrefixOptIfNempty(String prefix, String s) {
  return addPrefixIfNotEmpty2(prefix, s);
}




static String addPrefixIfNotEmpty2(String prefix, String s) {
  return empty(s) ? "" : addPrefix(prefix, s);
}




static String addPrefix(String prefix, String s) {
  return s.startsWith(prefix) ? s : prefix + s;
}




// immutable, has strong refs
// Do not run in a synchronized block - it goes wrong in the presence
// of elaborate classloaders (like in Gazelle BEA)
// see #1102990 and #1102991

final static class _MethodCache {
  final Class c;
  final HashMap<String, List<Method>> cache = new HashMap();
  
  _MethodCache(Class c) {
  this.c = c; _init(); }
  
  void _init() {
    Class _c = c;
    java.lang.Module myModule = getClass().getModule();
    boolean anyHiddenClasses = false;
    
    while (_c != null) {
      boolean exported = classIsExportedTo(_c, myModule);
       
      
      if (!exported)
        anyHiddenClasses = true;
      else
        for (Method m : _c.getDeclaredMethods())
          if ((anyHiddenClasses || !isAbstract(m))
            && !reflection_isForbiddenMethod(m))
            multiMapPut(cache, m.getName(), makeAccessible(m));

      _c = _c.getSuperclass();
    }
    
    // add default methods - this might lead to a duplication
    // because the overridden method is also added, but it's not
    // a problem except for minimal performance loss.
    // If any classes in the hierarchy were inaccessible, we add
    // all interface methods (see test_callForbiddenMethodByReflection for a test)
    
    for (Class intf : allInterfacesImplementedBy(c))
      for (Method m : intf.getDeclaredMethods())
        if ((anyHiddenClasses || m.isDefault()) && !reflection_isForbiddenMethod(m))
          multiMapPut(cache, m.getName(), makeAccessible(m));

     
  }
  
  // Returns only matching methods
  Method findMethod(String method, Object[] args) { try {
    List<Method> m = cache.get(method);
     
    if (m == null) return null;
    int n = m.size();
    for (int i = 0; i < n; i++) {
      Method me = m.get(i);
      if (call_checkArgs(me, args, false))
        return me;
    }
    return null;
  } catch (Exception __e) { throw rethrow(__e); } }
  
  Method findStaticMethod(String method, Object[] args) { try {
    List<Method> m = cache.get(method);
    if (m == null) return null;
    int n = m.size();
    for (int i = 0; i < n; i++) {
      Method me = m.get(i);
      if (isStaticMethod(me) && call_checkArgs(me, args, false))
        return me;
    }
    return null;
  } catch (Exception __e) { throw rethrow(__e); } }
}
static abstract class VF1<A> implements IVF1<A> {
  public abstract void get(A a);
}
// Meta - a "minimal" approach to adding meta-level to Java objects

static class Meta implements IMeta {
  
// Meta - a "minimal" approach to adding meta-level to Java objects
// (implementing the interface IMeta)

// We allocate one extra field for each Java object to make it
// reasoning-compatible (reasoning-compatible = extensible with
// fields of any name at runtime).
//
// We couldn't go for 0 extra fields (meta values must be linked
// directly from the object) and there are no half fields in
// Java... so there you go.
//
// Also, if you don't use any meta data, you are probably not
// reasoning about anything. The point of reasoning in JavaX is
// to attach information to objects directly used in the program.

// Possible information contained in the meta field:
//   Origin, destination, security level, sender, cost center,
//   purpose, list of reifications, ...

// So here it is. THE FIELD YOU HAVE BEEN WAITING FOR!

// [We also have IMeta to retrofit foreign classes (rare but
// probably useful).]

//////////////////////
// The "meta" field //
//////////////////////

// Generic meta value of any kind, but the typical case is it's a
// Map with extra field values for the object etc.
// "meta" is volatile to avoid synchronization; but you can also synchronize on
// _tempMetaMutex() which is usually the object itself. Collections
// and maps are exempt from using the collections's monitor as the meta
// mutex because their monitor tends to be held for long operations
// (e.g. cloneList). For those we use a substantially more complex
// algorithm using a weakMap. Probably overkill. I may reconsider.

volatile Object meta;

// The meta field is not transient, thus by default it will be
// persisted like anything else unless you customize your object
// to suppress or modulate this.

// ...and the interface methods

public void _setMeta(Object meta) { this.meta = meta; }
public Object _getMeta() { return meta; }

// MOST functions are implemented in IMeta (default implementations)

// Scaffolding convenience functions

final boolean scaffolding(){ return scaffoldingEnabled(); }
boolean scaffoldingEnabled() { return main.scaffoldingEnabled(this); }
boolean scaffoldingEnabled(Object o) { return main.scaffoldingEnabled(o); }
}
static class Matches {
  String[] m;
  
  Matches() {}
  Matches(String... m) {
  this.m = m;}
  
  String get(int i) { return i < m.length ? m[i] : null; }
  String unq(int i) { return unquote(get(i)); }
  
  String tlc(int i) { return unq(i).toLowerCase(); }
  boolean bool(int i) { return "true".equals(unq(i)); }
  String rest() { return m[m.length-1]; } // for matchStart
  int psi(int i) { return Integer.parseInt(unq(i)); }
  
  public String toString() { return "Matches(" + joinWithComma(quoteAll(asList(m))) + ")"; }
  
  public int hashCode() { return _hashCode(toList(m)); }
  public boolean equals(Object o) { return o instanceof Matches && arraysEqual(m, ((Matches) o).m); }
}

// for the version with MasterSymbol (used WAY back in "Smart Bot"!) see #1010608

static class Symbol implements CharSequence {
  String text;
  
  Symbol() {}
  Symbol(String text, boolean dummy) {
  this.text = text;} // weird signature to prevent accidental calling
  
  public int hashCode() { return _hashCode(text); }
  public String toString() { return text; }
  public boolean equals(Object o) {
    return this == o;
  }

  // implementation of CharSequence methods
  
  public int length() { return text.length(); }
  public char charAt(int index) { return text.charAt(index); }
  public CharSequence subSequence(int start, int end) {
    return text.substring(start, end);
  }
}
static class Var<A> implements IVar<A>, ISetter<A> {
  Var() {}
  Var(A v) {
  this.v = v;}

  
  
  A v; // you can access this directly if you use one thread
  
  public synchronized void set(A a) {
    if (v != a) {
      v = a;
      notifyAll();
    }
  }
  
  public synchronized A get() { return v; }
  public synchronized boolean has() { return v != null; }
  public void clear() { set(null); }

public String toString() { return str(this.get()); }
}
static interface IMeta {
  // see class "Meta" for the bla bla
  
  public void _setMeta(Object meta);
  public Object _getMeta();
  default public IAutoCloseableF0 _tempMetaMutex() {
    return new IAutoCloseableF0() {
      public Object get() { return IMeta.this; }
      public void close() {}
    };
  }
  
  // actually query another object
  default public Object getMeta(Object obj, Object key){ return metaGet(obj, key); }
default public Object metaGet(Object obj, Object key) {
    // call global function
    return metaMapGet(obj, key);
  }
  
  default public Object metaGet(String key, Object obj) {
    // call global function
    return metaMapGet(obj, key);
  }
  
  default public Object getMeta(Object key){ return metaGet(key); }
default public Object metaGet(Object key) {
    if (key == null) return null;
    Object meta = _getMeta();
    if (meta instanceof Map) return ((Map) meta).get(key);
    return null;
  }
  
  default public void metaSet(IMeta obj, Object key, Object value){ metaPut(obj, key, value); }
default public void metaPut(IMeta obj, Object key, Object value) {
    // call global function
    metaMapPut(obj, key, value);
  }
  
  default public void metaSet(Object key, Object value){ metaPut(key, value); }
default public void metaPut(Object key, Object value) {
    if (key == null) return;
    Map map = convertObjectMetaToMap(this);
    syncMapPutOrRemove(map, key, value);
  }
}
// a variant of thread where you can get the Runnable target later.
// Also notes its existence on the VM bus.
// We should use this exclusively instead of Thread.

static class BetterThread extends Thread {
  Runnable target;
  
  BetterThread(Runnable target) {
  this.target = target; _created(); }
  BetterThread(Runnable target, String name) { super(name);
  this.target = target; _created(); }
  
  void _created() { vmBus_send("threadCreated", this); }
  
  public void run() { try {
    try {
      vmBus_send("threadStarted", this);
      if (target != null) target.run();
    } finally {
      vmBus_send("threadEnded", this); 
    }
  } catch (Exception __e) { throw rethrow(__e); } }
  
  Runnable getTarget() { return target; }
}
// Some of the functions are dependent on the concepts field,
// others are global.

static class G22Utils implements AutoCloseable {
   final public G22Utils setBackgroundProcessesUI(BackgroundProcessesUI backgroundProcessesUI){ return backgroundProcessesUI(backgroundProcessesUI); }
public G22Utils backgroundProcessesUI(BackgroundProcessesUI backgroundProcessesUI) { this.backgroundProcessesUI = backgroundProcessesUI; return this; }  final public BackgroundProcessesUI getBackgroundProcessesUI(){ return backgroundProcessesUI(); }
public BackgroundProcessesUI backgroundProcessesUI() { return backgroundProcessesUI; }
 BackgroundProcessesUI backgroundProcessesUI;
   final public G22Utils setModule(Enterable module){ return module(module); }
public G22Utils module(Enterable module) { this.module = module; return this; }  final public Enterable getModule(){ return module(); }
public Enterable module() { return module; }
 Enterable module;
   final public G22Utils setMasterStuff(G22MasterStuff masterStuff){ return masterStuff(masterStuff); }
public G22Utils masterStuff(G22MasterStuff masterStuff) { this.masterStuff = masterStuff; return this; }  final public G22MasterStuff getMasterStuff(){ return masterStuff(); }
public G22MasterStuff masterStuff() { return masterStuff; }
 G22MasterStuff masterStuff;
   final public G22Utils setConcepts(Concepts concepts){ return concepts(concepts); }
public G22Utils concepts(Concepts concepts) { this.concepts = concepts; return this; }  final public Concepts getConcepts(){ return concepts(); }
public Concepts concepts() { return concepts; }
 Concepts concepts;
   final public G22Utils setProjectActions(G22ProjectActions projectActions){ return projectActions(projectActions); }
public G22Utils projectActions(G22ProjectActions projectActions) { this.projectActions = projectActions; return this; }  final public G22ProjectActions getProjectActions(){ return projectActions(); }
public G22ProjectActions projectActions() { return projectActions; }
 G22ProjectActions projectActions;
   final public G22AutoStarter getAutoStarter(){ return autoStarter(); }
public G22AutoStarter autoStarter() { return autoStarter; }
 G22AutoStarter autoStarter = new G22AutoStarter(this);
  
   final public CombinedStringifier getStringifier(){ return stringifier(); }
public CombinedStringifier stringifier() { return stringifier; }
 CombinedStringifier stringifier = new CombinedStringifier(
    o -> o instanceof BufferedImage ? "Image (" + ((BufferedImage) o).getWidth() + "*" + ((BufferedImage) o).getHeight() + " px)" : null
  );
  
  ImageSurface stdImageSurface() {
    var is = pixelatedImageSurface().setAutoZoomToDisplay(true).repaintInThread(false);
    is.specialPurposed = true;
    new ImageSurface_PositionToolTip(is);
    return is;
  }

  ImageSurface stdImageSurface(MakesBufferedImage img) { return stdImageSurface(toBufferedImage(img)); }

ImageSurface stdImageSurface(BufferedImage img) {
    var is = stdImageSurface();
    is.setImage(img);
    return is;
  }
  
  String stringify(Object o) { return stringifier.toString(o); }
  
  transient Set<IVF1<GazelleV_LeftArrowScriptParser>> onSettingUpParser;
public G22Utils onSettingUpParser(IVF1<GazelleV_LeftArrowScriptParser> f) { onSettingUpParser = createOrAddToSyncLinkedHashSet(onSettingUpParser, f); return this; }
public G22Utils removeSettingUpParserListener(IVF1<GazelleV_LeftArrowScriptParser> f) { main.remove(onSettingUpParser, f); return this; }
public void settingUpParser(GazelleV_LeftArrowScriptParser parser) {  pcallFAll(onSettingUpParser, parser); }

  GazelleV_LeftArrowScriptParser leftArrowParser() {
    GazelleV_LeftArrowScriptParser parser = new GazelleV_LeftArrowScriptParser();
    parser.g22utils(this);
    settingUpParser(parser);
    return parser;
  }
  
  void basicParserTest() {
    var parser = leftArrowParser();
    print("classContainerPrefixes" , parser.classContainerPrefixes());
    assertEquals(pair(1, 2), parser.parse("new Pair 1 2").get());
  }

  
  
  File byteCodePath() {
    return assertNotNull(getBytecodePathForClass(this));
  }

   ClassNameResolver classNameResolver_cache;
 ClassNameResolver classNameResolver() { if (classNameResolver_cache == null) classNameResolver_cache = classNameResolver_load(); return classNameResolver_cache;}

 ClassNameResolver classNameResolver_load() {
    return new ClassNameResolver().byteCodePath(byteCodePath()).init();
  }
  
  File databasesMotherDir() {
    return javaxDataDir("Gazelle-22");
  }
  
  AutoCloseable enter() { return module == null ? null : module.enter(); }
  
  String defaultDBName() { return "Default"; }
  
  File lastOpenedDBsFile() {
    return newFile(databasesMotherDir(), "Last Opened");
  }
  
  File autoUpdateFile() {
    return newFile(databasesMotherDir(), "Auto-Update");
  }
  
  boolean autoUpdateEnabled() {
    return fileExists(autoUpdateFile());
  }
  
  void setAutoUpdate(boolean b) {
    createOrRemoveFile(autoUpdateFile(), b);
  }
  
  List<String> dbsToOpen() {
    List<String> dbNames = new ArrayList();
    for (String name : tlft(loadTextFile(lastOpenedDBsFile())))
      if (fileExists(newFile(databasesMotherDir(), name)))
        dbNames.add(name);
    if (empty(dbNames)) dbNames.add(defaultDBName());
    return dbNames;
  }
  
  void setOpenDBs(Collection<File> dbDirs) {
    List<String> dbNames = new ArrayList();
    for (File dbDir : dbDirs)
      if (sameFile(databasesMotherDir(), dirOfFile(dbDir)))
        dbNames.add(fileName(dbDir));
      
    saveTextFile(lastOpenedDBsFile(), lines(dbNames));
  }
  
  

  Map<String, Object> scriptToMap(G22LeftArrowScript c) { return scriptToMap(c, false); }
Map<String, Object> scriptToMap(G22LeftArrowScript c, boolean allowRunOnProjectOpen) {  
    return litorderedmap(
      "Description" , str(c),
      "Status" , renderScriptStatus(c),
      "LoC" , renderScriptLoC(c),
      "Run on project open" , allowRunOnProjectOpen && c.runOnProjectOpen ? "Yes" : null);
  }
  
  String renderScriptStatus(G22LeftArrowScript c) {
    return or2_rev("Empty", joinNemptiesWithSpacedPlus(
      c.isClearForAutoRun() ? "Clear for auto-run" : null,
      c.isSavedDistinctFromAutoRunVersion() ? "Saved (not cleared)" : null,
      c.isEditing() ? "Editing" : null
    ));
  }
  
  String renderScriptLoC(G22LeftArrowScript c) {
    return n2(intMax(mapLL(__59 -> linesOfCode_javaTok(__59), 
      c.editingText,
      c.text,
      c.codeForAutoRun())));
  }
  
  // e.g. for an image file
  List<G22Label> labelsForFile(File file) {
    if (file == null) return null;
    File labelsFile = appendToFileName(file, ".labels");
    List<String> labels = tlft(loadTextFile(labelsFile));
    return map(__30 -> getLabel(__30), labels);
  }

  File labelsFile(File file) {  
    if (file == null) return null;
    return appendToFileName(file, ".labels");
  }
  
  void setLabelsForFile(File file, List<G22Label> labels) {
    List<String> list = map(labels, label -> label.name);
    File f = labelsFile(file);
    saveTextFile(f, lines(list));
    print("Saved " + nLabels(list) + " (" + joinWithComma(list) + ") to " + f);
  }
  
  G22Label getLabel(String name) {
    if (empty(name)) return null;
    if (containsNewLine(name)) throw fail("No newlines in label names allowed: " + name);
    return uniqCI(concepts, G22Label.class, "name", name);
  }
  
  File dbDir() { return conceptsDir(concepts); }
  
  File fileInDbDir(String name) { return newFile(dbDir(), name); }
  
  class GazelleDB implements IFieldsToList{
  String name;
  File dir;
  GazelleDB() {}
  GazelleDB(String name, File dir) {
  this.dir = dir;
  this.name = name;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + name + ", " + dir + ")"; }

public boolean equals(Object o) {
if (!(o instanceof GazelleDB)) return false;
    GazelleDB __0 =  (GazelleDB) o;
    return eq(name, __0.name) && eq(dir, __0.dir);
}

  public int hashCode() {
    int h = 1669530526;
    h = boostHashCombine(h, _hashCode(name));
    h = boostHashCombine(h, _hashCode(dir));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {name, dir}; }

     Boolean loaded_cache;
 boolean loaded() { if (loaded_cache == null) loaded_cache = loaded_load(); return loaded_cache;}

 Boolean loaded_load() {
      return containsFile(masterStuff.openConceptDirs(), dir);
    }
    
    File conceptsFile() { return conceptsFileIn(dir); }
  }
  
  List<GazelleDB> gazelleDBs() {
    List<GazelleDB> dbs = new ArrayList();
    for (File dir : listDirsContainingFileNamed(databasesMotherDir(),
      "concepts.structure.gz"))
      dbs.add(new GazelleDB(fileName(dir), dir));
    return dbs;
  }
  
  RSyntaxTextAreaWithSearch newSyntaxTextArea() { return newSyntaxTextArea(null); }
RSyntaxTextAreaWithSearch newSyntaxTextArea(IF1<JComponent, JComponent> wrapStatusLabel) {
    RSyntaxTextAreaWithSearch ta = new RSyntaxTextAreaWithSearch(wrapStatusLabel);
    ta.textArea().setHighlightCurrentLine(false);
    ta.menuLessOperation();
    return ta;
  }
  
  File projectStoryTextFile() { return newFile(dbDir(), "story.txt"); }
  
  String projectName() { return fileName(dbDir()); }
  
  public void close() { try { autoStarter.close(); } catch (Exception __e) { throw rethrow(__e); } }
}
static class PopupMenuMaker {
   final public PopupMenuMaker setAllowScrolling(boolean allowScrolling){ return allowScrolling(allowScrolling); }
public PopupMenuMaker allowScrolling(boolean allowScrolling) { this.allowScrolling = allowScrolling; return this; }  final public boolean getAllowScrolling(){ return allowScrolling(); }
public boolean allowScrolling() { return allowScrolling; }
 boolean allowScrolling = true; // ignored when using existingMenu
   final public PopupMenuMaker setEvent(MouseEvent event){ return event(event); }
public PopupMenuMaker event(MouseEvent event) { this.event = event; return this; }  final public MouseEvent getEvent(){ return event(); }
public MouseEvent event() { return event; }
 MouseEvent event;
   final public PopupMenuMaker setPtInComponent(PtInComponent ptInComponent){ return ptInComponent(ptInComponent); }
public PopupMenuMaker ptInComponent(PtInComponent ptInComponent) { this.ptInComponent = ptInComponent; return this; }  final public PtInComponent getPtInComponent(){ return ptInComponent(); }
public PtInComponent ptInComponent() { return ptInComponent; }
 PtInComponent ptInComponent; // if different from event
   final public PopupMenuMaker setFillMenu(IVF1<JPopupMenu> fillMenu){ return fillMenu(fillMenu); }
public PopupMenuMaker fillMenu(IVF1<JPopupMenu> fillMenu) { this.fillMenu = fillMenu; return this; }  final public IVF1<JPopupMenu> getFillMenu(){ return fillMenu(); }
public IVF1<JPopupMenu> fillMenu() { return fillMenu; }
 IVF1<JPopupMenu> fillMenu;
   final public PopupMenuMaker setExistingMenu(JPopupMenu existingMenu){ return existingMenu(existingMenu); }
public PopupMenuMaker existingMenu(JPopupMenu existingMenu) { this.existingMenu = existingMenu; return this; }  final public JPopupMenu getExistingMenu(){ return existingMenu(); }
public JPopupMenu existingMenu() { return existingMenu; }
 JPopupMenu existingMenu;
   final public PopupMenuMaker setAddSeparator(boolean addSeparator){ return addSeparator(addSeparator); }
public PopupMenuMaker addSeparator(boolean addSeparator) { this.addSeparator = addSeparator; return this; }  final public boolean getAddSeparator(){ return addSeparator(); }
public boolean addSeparator() { return addSeparator; }
 boolean addSeparator = true; // add separator if existing meun

  JPopupMenu menu;
  
  PopupMenuMaker() {}
  PopupMenuMaker(MouseEvent event, IVF1<JPopupMenu> fillMenu) {
  this.fillMenu = fillMenu;
  this.event = event;}

  public void run() { swing(() -> { 
    if (existingMenu != null) {
      var menu = existingMenu;
      int emptyCount = menu.getComponentCount();
      if (addSeparator)
        menu.addSeparator();
      int emptyCount2 = menu.getComponentCount();
      { if (fillMenu != null) fillMenu.get(menu); }
      if (menu.getComponentCount() == emptyCount2)
        truncateContainer(menu, emptyCount);
      //printVars("Extended popup menu", +emptyCount, +emptyCount2, n := menu.getComponentCount());
      packWindow(menu);
      //revalidate(menu);
    } else {
      JPopupMenu menu = new JPopupMenu();
      
      int emptyCount = menu.getComponentCount();
      { if (fillMenu != null) fillMenu.get(menu); }
      if (menu.getComponentCount() == emptyCount)
        return;
    
      if (allowScrolling) {
        menu = new JPopupMenu();
        JMenuScroller scroller = JMenuScroller.setScrollerFor(menu);
        scroller.fillMenu = toVF1(fillMenu);
      }
      
      if (ptInComponent == null) ptInComponent = ptInComponentFromEvent(event);
    
      if (hasParentOfType(JPopupMenu.class, ptInComponent.component)) {
        // component is in a menu itself. TODO: better positioning
        // Also this doesn't actually seem to work...
        menu.setInvoker(ptInComponent.component);
        menu.setVisible(true);
      } else
        menu.show(ptInComponent.component, ptInComponent.p.x, ptInComponent.p.y);
    }
  }); }
}
static class MultiMap<A,B> {
  Map<A, List<B>> data = new HashMap<A, List<B>>();
  int fullSize;
  
  MultiMap() {}
  MultiMap(boolean useTreeMap) { if (useTreeMap) data = new TreeMap(); }
  MultiMap(MultiMap<A, B> map) { putAll(map); }
  MultiMap(Map<A, List<B>> data) {
  this.data = data;}

  void put(A key, B value) { synchronized(data) {
    List<B> list = data.get(key);
    if (list == null)
      data.put(key, list = _makeEmptyList());
    list.add(value);
    ++fullSize;
  }}

  void add(A key, B value) { put(key, value); }

  void addAll(A key, Collection<B> values) { putAll(key, values); }
  
  void addAllIfNotThere(A key, Collection<B> values) { synchronized(data) {
    for (B value : values)
      setPut(key, value);
  }}
  
  void setPut(A key, B value) { synchronized(data) {
    if (!containsPair(key, value))
      put(key, value);
  }}
  
  boolean containsPair(A key, B value) { synchronized(data) {
    return get(key).contains(value);
  }}
  
  void putAll(Collection<A> keys, B value) { synchronized(data) {
    for (A key : unnullForIteration(keys))
      put(key, value);
  }}

  void putAll(A key, Collection<B> values) { synchronized(data) {
    if (nempty(values)) getActual(key).addAll(values);
  }}

  void putAll(Iterable<Pair<A, B>> pairs) { synchronized(data) {
    for (Pair<A, B> p : unnullForIteration(pairs))
      put(p.a, p.b);
  }}
  
  void removeAll(A key, Collection<B> values) { synchronized(data) {
    for (B value : values)
      remove(key, value);
  }}
  
  List<B> get(A key) { synchronized(data) {
    List<B> list = data.get(key);
    return list == null ? Collections.<B> emptyList() : list;
  }}
  
  List<B> getOpt(A key) { synchronized(data) {
    return data.get(key);
  }}

  List<B> getAndClear(A key) { synchronized(data) {
    List<B> l = cloneList(data.get(key));
    remove(key);
    return l;
  }}
  
  // returns actual mutable live list
  // creates the list if not there
  List<B> getActual(A key) { synchronized(data) {
    List<B> list = data.get(key);
    if (list == null)
      data.put(key, list = _makeEmptyList());
    return list;
  }}
 
  void clean(A key) { synchronized(data) {
    List<B> list = data.get(key);
    if (list != null && list.isEmpty()) {
      fullSize -= l(list);
      data.remove(key);
    }
  }}

  Set<A> keySet() { synchronized(data) {
    return data.keySet();
  }}

  Set<A> keys() { synchronized(data) {
    return data.keySet();
  }}

  void remove(A key) { synchronized(data) {
    fullSize -= l(this.getOpt(key));
    data.remove(key);
  }}
  
  final void remove(Pair<A, B> p){ removePair(p); }
void removePair(Pair<A, B> p) {
    if (p != null) remove(p.a, p.b);
  }

  void remove(A key, B value) { synchronized(data) {
    List<B> list = data.get(key);
    if (list != null) {
      if (list.remove(value))
        fullSize--;
      if (list.isEmpty())
        data.remove(key);
    }
  }}

  void clear() { synchronized(data) {
    data.clear();
  }}

  boolean containsKey(A key) { synchronized(data) {
    return data.containsKey(key);
  }}

  B getFirst(A key) { synchronized(data) {
    List<B> list = get(key);
    return list.isEmpty() ? null : list.get(0);
  }}
  
  void addAll(MultiMap<A, B> map) { putAll(map); }
  
  void putAll(MultiMap<A, B> map) { synchronized(data) {
    for (A key : map.keySet())
      putAll(key, map.get(key));
  }}
  
  void putAll(Map<A, B> map) { synchronized(data) {
    if (map != null) for (Map.Entry<A, B> e : map.entrySet())
      put(e.getKey(), e.getValue());
  }}
  
  final int keyCount(){ return keysSize(); }
int keysSize() { synchronized(data) { return l(data); }}
  
  // full size - note: expensive operation
  final int fullSize(){ return size(); }
int size() { synchronized(data) {
    return fullSize;
  }}
  
  // expensive operation
  List<A> reverseGet(B b) { synchronized(data) {
    List<A> l = new ArrayList();
    for (A key : data.keySet())
      if (data.get(key).contains(b))
        l.add(key);
    return l;
  }}
  
  Map<A, List<B>> asMap() { synchronized(data) {
    return cloneMap(data);
  }}
  
  boolean isEmpty() { synchronized(data) { return data.isEmpty(); }}
  
  // override in subclasses
  List<B> _makeEmptyList() {
    return new ArrayList();
  }
  
  // returns live lists
  Collection<List<B>> allLists() {
    synchronized(data) {
      return new ArrayList(data.values());
    }
  }
  Collection<List<B>> values() { return allLists(); }
  
  List<B> allValues() {
    return concatLists(data.values());
  }
  
  Object mutex() { return data; }
  
  public String toString() { return "mm" + str(data); }
}
static interface IResourceLoader {
  String loadSnippet(String snippetID);
  String getTranspiled(String snippetID); // with libs
  int getSnippetType(String snippetID);
  String getSnippetTitle(String snippetID);
  File loadLibrary(String snippetID);
  
  //ifndef NoJavaXJar
  default File pathToJavaXJar() { return pathToJavaxJar_noResourceLoader(); }
  //endifndef

  // may return null, then caller compiles themselves
  default File getSnippetJar(String snippetID, String transpiledSrc) { return null; }
}
/*
 * @(#)WeakHashMap.java 1.5 98/09/30
 *
 * Copyright 1998 by Sun Microsystems, Inc.,
 * 901 San Antonio Road, Palo Alto, California, 94303, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information
 * of Sun Microsystems, Inc. ("Confidential Information").  You
 * shall not disclose such Confidential Information and shall use
 * it only in accordance with the terms of the license agreement
 * you entered into with Sun.
 */
 
// From https://github.com/mernst/plume-lib/blob/df0bfafc3c16848d88f4ea0ef3c8bf3367ae085e/java/src/plume/WeakHasherMap.java

static final class WeakHasherMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {

    private Hasher hasher = null;
    /*@Pure*/
    private boolean keyEquals(Object k1, Object k2) {
  return (hasher==null ? k1.equals(k2)
           : hasher.equals(k1, k2));
    }
    /*@Pure*/
    private int keyHashCode(Object k1) {
  return (hasher==null ? k1.hashCode()
           : hasher.hashCode(k1));
    }

    // The WeakKey class can't be static because it depends on the hasher.
    // That in turn means that its methods can't be static.
    // However, I need to be able to call the methods such as create() that
    // were static in the original version of this code.
    // This finesses that.

    private /*@Nullable*/ WeakKey WeakKeyCreate(K k) {
  if (k == null) return null;
  else return new WeakKey(k);
    }
    private /*@Nullable*/ WeakKey WeakKeyCreate(K k, ReferenceQueue<? super K> q) {
  if (k == null) return null;
  else return new WeakKey(k, q);
    }

    // Cannot be a static class: uses keyHashCode() and keyEquals()
    private final class WeakKey extends WeakReference<K> {
  private int hash; /* Hashcode of key, stored here since the key
           may be tossed by the GC */

  private WeakKey(K k) {
      super(k);
      hash = keyHashCode(k);
  }

  private /*@Nullable*/ WeakKey create(K k) {
      if (k == null) return null;
      else return new WeakKey(k);
  }

  private WeakKey(K k, ReferenceQueue<? super K> q) {
      super(k, q);
      hash = keyHashCode(k);
  }

  private /*@Nullable*/ WeakKey create(K k, ReferenceQueue<? super K> q) {
      if (k == null) return null;
      else return new WeakKey(k, q);
  }

        /* A WeakKey is equal to another WeakKey iff they both refer to objects
     that are, in turn, equal according to their own equals methods */
  /*@Pure*/
  @Override
  public boolean equals(/*@Nullable*/ Object o) {
            if (o == null) return false; // never happens
      if (this == o) return true;
            // This test is illegal because WeakKey is a generic type,
            // so use the getClass hack below instead.
      // if (!(o instanceof WeakKey)) return false;
            if (!(o.getClass().equals(WeakKey.class))) return false;
      Object t = this.get();
            @SuppressWarnings("unchecked")
      Object u = ((WeakKey)o).get();
      if ((t == null) || (u == null)) return false;
      if (t == u) return true;
      return keyEquals(t, u);
  }

  /*@Pure*/
  @Override
  public int hashCode() {
      return hash;
  }

    }


    /* Hash table mapping WeakKeys to values */
    private HashMap<WeakKey,V> hash;

    /* Reference queue for cleared WeakKeys */
    private ReferenceQueue<? super K> queue = new ReferenceQueue<K>();


    /* Remove all invalidated entries from the map, that is, remove all entries
       whose keys have been discarded.  This method should be invoked once by
       each public mutator in this class.  We don't invoke this method in
       public accessors because that can lead to surprising
       ConcurrentModificationExceptions. */
    @SuppressWarnings("unchecked")
    private void processQueue() {
  WeakKey wk;
  while ((wk = (WeakKey)queue.poll()) != null) { // unchecked cast
      hash.remove(wk);
  }
    }


    /* -- Constructors -- */

    /**
     * Constructs a new, empty <code>WeakHashMap</code> with the given
     * initial capacity and the given load factor.
     *
     * @param  initialCapacity  the initial capacity of the
     *                          <code>WeakHashMap</code>
     *
     * @param  loadFactor       the load factor of the <code>WeakHashMap</code>
     *
     * @throws IllegalArgumentException  If the initial capacity is less than
     *                                   zero, or if the load factor is
     *                                   nonpositive
     */
    public WeakHasherMap(int initialCapacity, float loadFactor) {
  hash = new HashMap<WeakKey,V>(initialCapacity, loadFactor);
    }

    /**
     * Constructs a new, empty <code>WeakHashMap</code> with the given
     * initial capacity and the default load factor, which is
     * <code>0.75</code>.
     *
     * @param  initialCapacity  the initial capacity of the
     *                          <code>WeakHashMap</code>
     *
     * @throws IllegalArgumentException  If the initial capacity is less than
     *                                   zero
     */
    public WeakHasherMap(int initialCapacity) {
  hash = new HashMap<WeakKey,V>(initialCapacity);
    }

    /**
     * Constructs a new, empty <code>WeakHashMap</code> with the default
     * capacity and the default load factor, which is <code>0.75</code>.
     */
    public WeakHasherMap() {
  hash = new HashMap<WeakKey,V>();
    }

    /**
     * Constructs a new, empty <code>WeakHashMap</code> with the default
     * capacity and the default load factor, which is <code>0.75</code>.
     * The <code>WeakHashMap</code> uses the specified hasher for hashing
     * keys and comparing them for equality.
     * @param h the Hasher to use when hashing values for this map
     */
    public WeakHasherMap(Hasher h) {
  hash = new HashMap<WeakKey,V>();
  hasher = h;
    }


    /* -- Simple queries -- */

    /**
     * Returns the number of key-value mappings in this map.
     * <strong>Note:</strong> <em>In contrast to most implementations of the
     * <code>Map</code> interface, the time required by this operation is
     * linear in the size of the map.</em>
     */
    /*@Pure*/
    @Override
    public int size() {
  return entrySet().size();
    }

    /**
     * Returns <code>true</code> if this map contains no key-value mappings.
     */
    /*@Pure*/
    @Override
    public boolean isEmpty() {
  return entrySet().isEmpty();
    }

    /**
     * Returns <code>true</code> if this map contains a mapping for the
     * specified key.
     *
     * @param   key   the key whose presence in this map is to be tested
     */
    /*@Pure*/
    @Override
    public boolean containsKey(Object key) {
        @SuppressWarnings("unchecked")
        K kkey = (K) key;
  return hash.containsKey(WeakKeyCreate(kkey));
    }


    /* -- Lookup and modification operations -- */

    /**
     * Returns the value to which this map maps the specified <code>key</code>.
     * If this map does not contain a value for this key, then return
     * <code>null</code>.
     *
     * @param  key  the key whose associated value, if any, is to be returned
     */
    /*@Pure*/
    @Override
    public /*@Nullable*/ V get(Object key) {  // type of argument is Object, not K
        @SuppressWarnings("unchecked")
        K kkey = (K) key;
  return hash.get(WeakKeyCreate(kkey));
    }

    /**
     * Updates this map so that the given <code>key</code> maps to the given
     * <code>value</code>.  If the map previously contained a mapping for
     * <code>key</code> then that mapping is replaced and the previous value is
     * returned.
     *
     * @param  key    the key that is to be mapped to the given
     *                <code>value</code>
     * @param  value  the value to which the given <code>key</code> is to be
     *                mapped
     *
     * @return  the previous value to which this key was mapped, or
     *          <code>null</code> if if there was no mapping for the key
     */
    @Override
    public V put(K key, V value) {
  processQueue();
  return hash.put(WeakKeyCreate(key, queue), value);
    }

    /**
     * Removes the mapping for the given <code>key</code> from this map, if
     * present.
     *
     * @param  key  the key whose mapping is to be removed
     *
     * @return  the value to which this key was mapped, or <code>null</code> if
     *          there was no mapping for the key
     */
    @Override
    public V remove(Object key) { // type of argument is Object, not K
  processQueue();
        @SuppressWarnings("unchecked")
        K kkey = (K) key;
  return hash.remove(WeakKeyCreate(kkey));
    }

    /**
     * Removes all mappings from this map.
     */
    @Override
    public void clear() {
  processQueue();
  hash.clear();
    }


    /* -- Views -- */


    /* Internal class for entries */
    // This can't be static, again because of dependence on hasher.
    @SuppressWarnings("TypeParameterShadowing")
    private final class Entry<K,V> implements Map.Entry<K,V> {
  private Map.Entry<WeakKey,V> ent;
  private K key;  /* Strong reference to key, so that the GC
           will leave it alone as long as this Entry
           exists */

  Entry(Map.Entry<WeakKey,V> ent, K key) {
      this.ent = ent;
      this.key = key;
  }

  /*@Pure*/
  @Override
  public K getKey() {
      return key;
  }

  /*@Pure*/
  @Override
  public V getValue() {
      return ent.getValue();
  }

  @Override
  public V setValue(V value) {
      return ent.setValue(value);
  }

        /*@Pure*/
        private boolean keyvalEquals(K o1, K o2) {
      return (o1 == null) ? (o2 == null) : keyEquals(o1, o2);
  }

        /*@Pure*/
        private boolean valEquals(V o1, V o2) {
      return (o1 == null) ? (o2 == null) : o1.equals(o2);
  }

        /*@Pure*/
        @SuppressWarnings("NonOverridingEquals")
        public boolean equals(Map.Entry<K,V> e /* Object o*/) {
            // if (! (o instanceof Map.Entry)) return false;
            // Map.Entry<K,V> e = (Map.Entry<K,V>)o;
      return (keyvalEquals(key, e.getKey())
        && valEquals(getValue(), e.getValue()));
  }

  /*@Pure*/
  @Override
  public int hashCode() {
      V v;
      return (((key == null) ? 0 : keyHashCode(key))
        ^ (((v = getValue()) == null) ? 0 : v.hashCode()));
  }

    }


    /* Internal class for entry sets */
    private final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
  Set<Map.Entry<WeakKey,V>> hashEntrySet = hash.entrySet();

  @Override
  public Iterator<Map.Entry<K,V>> iterator() {

      return new Iterator<Map.Entry<K,V>>() {
    Iterator<Map.Entry<WeakKey,V>> hashIterator = hashEntrySet.iterator();
    Map.Entry<K,V> next = null;

    @Override
    public boolean hasNext() {
        while (hashIterator.hasNext()) {
      Map.Entry<WeakKey,V> ent = hashIterator.next();
      WeakKey wk = ent.getKey();
      K k = null;
      if ((wk != null) && ((k = wk.get()) == null)) {
          /* Weak key has been cleared by GC */
          continue;
      }
      next = new Entry<K,V>(ent, k);
      return true;
        }
        return false;
    }

    @Override
    public Map.Entry<K,V> next() {
        if ((next == null) && !hasNext())
      throw new NoSuchElementException();
        Map.Entry<K,V> e = next;
        next = null;
        return e;
    }

    @Override
    public void remove() {
        hashIterator.remove();
    }

      };
  }

  /*@Pure*/
  @Override
  public boolean isEmpty() {
      return !(iterator().hasNext());
  }

  /*@Pure*/
  @Override
  public int size() {
      int j = 0;
      for (Iterator<Map.Entry<K,V>> i = iterator(); i.hasNext(); i.next()) j++;
      return j;
  }

  @Override
  public boolean remove(Object o) {
      processQueue();
      if (!(o instanceof Map.Entry<?,?>)) return false;
            @SuppressWarnings("unchecked")
      Map.Entry<K,V> e = (Map.Entry<K,V>)o; // unchecked cast
      Object ev = e.getValue();
      WeakKey wk = WeakKeyCreate(e.getKey());
      Object hv = hash.get(wk);
      if ((hv == null)
    ? ((ev == null) && hash.containsKey(wk)) : hv.equals(ev)) {
    hash.remove(wk);
    return true;
      }
      return false;
  }

  /*@Pure*/
  @Override
  public int hashCode() {
      int h = 0;
      for (Iterator<Map.Entry<WeakKey,V>> i = hashEntrySet.iterator(); i.hasNext(); ) {
    Map.Entry<WeakKey,V> ent = i.next();
    WeakKey wk = ent.getKey();
    Object v;
    if (wk == null) continue;
    h += (wk.hashCode()
          ^ (((v = ent.getValue()) == null) ? 0 : v.hashCode()));
      }
      return h;
  }

    }


    private /*@Nullable*/ Set<Map.Entry<K,V>> entrySet = null;

    /**
     * Returns a <code>Set</code> view of the mappings in this map.
     */
    /*@SideEffectFree*/
    @Override
    public Set<Map.Entry<K,V>> entrySet() {
  if (entrySet == null) entrySet = new EntrySet();
  return entrySet;
    }

    // find matching key
    K findKey(Object key) {
      processQueue();
      K kkey = (K) key;
      // TODO: use replacement for HashMap to avoid reflection
      WeakKey wkey = WeakKeyCreate(kkey);
      WeakKey found = hashMap_findKey(hash, wkey);
      return found == null ? null : found.get();
    }
}
static class ReliableSingleThread implements Runnable {
boolean _isTransient() { return true; }
  Object runnable; // usually a Runnable. is allowed to call trigger() itself
  String name = "Single Thread";
  boolean cancelBeforeTrigger = false; // always cancel running thread and wait for it to end before starting new operation
  boolean waitBetweenCancelAndTrigger = false; // make sure the old thread is actually ended
  F0<AutoCloseable> enter; // optional ownership marker, e.g. for DynModules
  int cancelTimeOut = 10000;
  
  boolean trigger = false;
  Thread thread;
  WeakReference<Thread> threadBeingCancelled;
  List<Runnable> inserts = syncL();
  
  ReliableSingleThread(Object runnable) {
  this.runnable = runnable;}
  
  void trigger() { go(); }
  synchronized void go() {
    if (cancelBeforeTrigger) cancelAndPossiblyWait();
    trigger = true;
    if (!running()) {
       AutoCloseable __1 = callF(enter); try {
      thread = startThread(name, new Runnable() {  public void run() { try { 
         AutoCloseable __2 = callF(enter); try {
        _run();
      
} finally { _close(__2); }} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "temp callF(enter);\r\n        _run();"; }});
    } finally { _close(__1); }}
  }
  public void run() { go(); }
  void get() { go(); } // so you can use the ! syntax
  
  synchronized boolean running() { return thread != null; }
  
  // use only if this is the last time you trigger this
  void triggerAndWait() {
    trigger();
    waitUntilDone();
  }
  
  void waitUntilDone() {
    while (running()) sleep(1);
  }
  
  void _run() { try {
    while (licensed()) {
      Thread oldThread;
      synchronized(this) {
        var currentInserts = syncGetAndClear(inserts);
        pcallFAll(currentInserts);
        
        if (!trigger) {
          thread = null;
          break;
        }
        
        oldThread = getWeakRef(threadBeingCancelled);
        trigger = false;
      }
      
      if (oldThread != null && oldThread != currentThread())
        oldThread.join(cancelTimeOut);
        
      pcallF(runnable);
    }
  } catch (Exception __e) { throw rethrow(__e); } }
  
  synchronized void cancel() {
    if (thread == null) return;
    threadBeingCancelled = new WeakReference(thread);
    cancelAndInterruptThread(thread);
    thread = null;
  }
  
  void cancelAndWait() {
    Thread _thread;
    
    synchronized(this) {
      if (thread == null) return;
      _thread = thread;
      threadBeingCancelled = new WeakReference(thread);
      thread = null;
    }
    
    cancelAndInterruptThread(_thread);
  }
  
  void cancelAndTrigger() {
    cancelAndPossiblyWait();
    trigger();
  }
  
  synchronized boolean triggered() { return trigger; }
  
  void cleanMeUp() { cancel(); }
  
  ReliableSingleThread cancelBeforeTrigger() {
    cancelBeforeTrigger = true;
    return this;
  }
  
  void cancelAndPossiblyWait() {
    if (waitBetweenCancelAndTrigger)
    cancel();
      
  }
  
  // TODO: trigger technically not necessary, just notify (I guess)
  void insert(Runnable r) { inserts.add(r); trigger(); }
  
  synchronized boolean hasThread() { return thread != null; }
}
// -has fast nextElement() and prevElement()
// -design allows for more functions like reordering the list
// -Saves up to 34% in space over LinkedHashSet
//    (e.g. 22% for a set of 1,000 Ints)
static class CompactLinkedHashSet<A> extends AbstractSet<A> {
  UnsynchronizedCompactHashSet<Entry<A>> entries = new UnsynchronizedCompactHashSet();
  Entry<A> head, tail;
  
  static class Entry<A> {
    A value;
    Entry<A> prev, next;
    
    public int hashCode() {
      return _hashCode(value);
    }
    
    // "magic" equals function for CompactHashSet lookup without temp object
    public boolean equals(Object o) {
      return o == this || eq(value, o);
    }
  }
  
  public boolean add(A a) {
    if (entries.contains(a)) return false;
    Entry<A> n = new Entry();
    n.value = a;
    n.prev = tail;
    if (tail != null) tail.next = n;
    tail = n;
    if (head == null) head = n;
    entries.add(n);
    return true;
  }
  
  public boolean remove(Object a) {
    return remove(entries.find(a));
  }
  
  public boolean remove(Entry<A> node) {
    if (node == null) return false;
    if (node.next != null) node.next.prev = node.prev; else tail = node.prev;
    if (node.prev != null) node.prev.next = node.next; else head = node.next;
    entries.remove(node);
    return true;
  }
  
  public int size() { return entries.size(); }
  
  public IterableIterator<A> iterator() {
    return new IterableIterator<A>() {
      Entry<A> entry = head, prev = null;
      public boolean hasNext() { return entry != null; }
      public A next() {
        A a = entry.value;
        prev = entry;
        entry = entry.next;
        return a;
      }
      
      // untested
      public void remove() {
        if (prev == null) throw new IllegalStateException();
        CompactLinkedHashSet.this.remove(prev);
        prev = null;
      }
    };
  }
  
  public void clear() {
    entries.clear();
    head = tail = null;
  }
  
  public boolean contains(Object a) {
    return entries.contains(a);
  }
  
  public A find(Object o) {
    Entry<A> e = entries.find(o);
    return e == null ? null : e.value;
  }
  
  public A prevElement(A a) {
    Entry<A> e = entries.find(a);
    if (e == null || e.prev == null) return null;
    return e.prev.value;
  }
  
  public A nextElement(A a) {
    Entry<A> e = entries.find(a);
    if (e == null || e.next == null) return null;
    return e.next.value;
  }
  
  public A first() { return head == null ? null : head.value; }
  public A last() { return tail == null ? null : tail.value; }
  
  boolean removeIfSame(Object o) {
    A value = find(o);
    if (value == o) {
      remove(value);
      return true;
    }
    return false;
  }
}
static class proxy_InvocationHandler implements InvocationHandler {
  Object target;
  
  proxy_InvocationHandler() {}
  proxy_InvocationHandler(Object target) {
  this.target = target;}
  
  public Object invoke(Object proxy, Method method, Object[] args) {
    return call(target, method.getName(), unnull(args));
  }
}
static class SingleComponentPanel extends JPanel {
  SingleComponentPanel() { super(new BorderLayout()); }

  SingleComponentPanel(Component component) {
    this();
    if (component != null) setComponent(component);
  }
  
  void set(Swingable component) {
    set(wrap(component));
  }

  final void set(Component component){ setComponent(component); }
void setComponent(Component component) {
    { swing(() -> { 
      if (getComponent() == component) return;
      
      removeAll();
      if (component != null)
        add(BorderLayout.CENTER, wrap(component));
      _revalidate(SingleComponentPanel.this);
    }); }
  }
  
  // Sometimes we need this?
  void setComponentAndRevalidateParent(Component component) {
    setComponent(component);
    _revalidate(_getParent(this));
  }
  
  final void clear(){ noComponent(); }
void noComponent() { setComponent(null); }
  
  Component getComponent() {
    return swing( () -> getComponentCount() == 0 ? null : getComponent(0));
  }
  
  boolean isEmpty() { return getComponent() == null; }
}
static class Fail extends RuntimeException implements IFieldsToList{
  Object[] objects;
  Fail() {}
  Fail(Object... objects) {
  this.objects = objects;}public Object[] _fieldsToList() { return new Object[] {objects}; }

  Fail(Throwable cause, Object... objects) {
    super(cause);
  this.objects = objects;
  }
  
  public String toString() { return joinNemptiesWithColon("Fail", commaCombine(getCause(), objects)); }
}
static class Pair<A, B> implements Comparable<Pair<A, B>> {
  A a;
  B b;

  Pair() {}
  Pair(A a, B b) {
  this.b = b;
  this.a = a;}
  
  public int hashCode() {
    return hashCodeFor(a) + 2*hashCodeFor(b);
  }
  
  public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof Pair)) return false;
    Pair t = (Pair) o;
    return eq(a, t.a) && eq(b, t.b);
  }
  
  public String toString() {
    return "<" + a + ", " + b + ">";
  }
  
  public int compareTo(Pair<A, B> p) {
    if (p == null) return 1;
    int i = ((Comparable<A>) a).compareTo(p.a);
    if (i != 0) return i;
    return ((Comparable<B>) b).compareTo(p.b);
  }
}
static interface IResourceHolder {
  <A extends AutoCloseable> A add(A a);
  Collection<AutoCloseable> takeAll();
}
static interface Swingable {
  JComponent visualize();
}
final static class Rect implements IFieldsToList{
  static final String _fieldOrder = "x y w h";
  int x;
  int y;
  int w;
  int h;
  Rect() {}
  Rect(int x, int y, int w, int h) {
  this.h = h;
  this.w = w;
  this.y = y;
  this.x = x;}

public boolean equals(Object o) {
if (!(o instanceof Rect)) return false;
    Rect __1 =  (Rect) o;
    return x == __1.x && y == __1.y && w == __1.w && h == __1.h;
}

  public int hashCode() {
    int h = 2543108;
    h = boostHashCombine(h, _hashCode(x));
    h = boostHashCombine(h, _hashCode(y));
    h = boostHashCombine(h, _hashCode(w));
    h = boostHashCombine(h, _hashCode(h));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {x, y, w, h}; }

  Rect(Rectangle r) {
    x = r.x;
    y = r.y;
    w = r.width;
    h = r.height;
  }
  
  Rect(Pt p, int w, int h) {
  this.h = h;
  this.w = w; x = p.x; y = p.y; }
  Rect(Rect r) { x = r.x; y = r.y; w = r.w; h = r.h; }
  
  Rectangle getRectangle() {
    return new Rectangle(x, y, w, h);
  }
  
  public String toString() {
    return x + "," + y + " / " + w + "," + h;
  }
  
  int x1() { return x; }
  int y1() { return y; }
  int x2() { return x + w; }
  int y2() { return y + h; }
  
  boolean contains(Pt p) {
    return contains(p.x, p.y);
  }
  
  boolean contains(int _x, int _y) {
    return _x >= x && _y >= y && _x < x+w && _y < y+h;
  }
  
  boolean contains(Rectangle r) {
    return rectContains(this, r);
  }
  
  boolean empty() { return w <= 0 || h <= 0; }
  
  int getWidth() { return w; }
  int getHeight() { return h; }
}
static class Pt implements Comparable<Pt>, IDoublePt {
  int x, y;
  
  Pt() {}
  Pt(Point p) {
    x = p.x;
    y = p.y;
  }
  Pt(int x, int y) {
  this.y = y;
  this.x = x;}
  
  Point getPoint() {
    return new Point(x, y);
  }
  
  public boolean equals(Object o) {
    return o instanceof Pt && x == ((Pt) o).x && y == ((Pt) o).y;
  }
  
  public int hashCode() {
    return boostHashCombine(x, y);
  }
  
  // compare in scan order
  public int compareTo(Pt p) {
    if (y != p.y) return cmp(y, p.y);
    return cmp(x, p.x);
  }
  
  public String toString() {
    return x + ", " + y;
  }
  
  double length() { return sqrt(x*x+y*y); }
  
  Pt minus(Pt p) { return ptMinus(this, p); }
  
  public double x_double() { return x; }
  public double y_double() { return y; }
}
static abstract class F0<A> {
  abstract A get();
}
static class LeftAlignedLine extends JPanel {
  LeftAlignedLine(Component... components) {
    setLayout(LetterLayout.leftAlignedRow());
    for (Component component : components)
      if (component != null)
        add(component);
  }

  public void add(String text) {
    add(new JLabel(text));
  }
}
static abstract class F1<A, B> {
  abstract B get(A a);
}
// you still need to implement hasNext() and next()
static abstract class IterableIterator<A> implements Iterator<A>, Iterable<A> {
  public Iterator<A> iterator() {
    return this;
  }
  
  public void remove() {
    unsupportedOperation();
  }
}

static class G22JavaObjectVisualizer implements Swingable , IFieldsToList{
  G22Utils g22utils;
  G22JavaObjectVisualizer() {}
  G22JavaObjectVisualizer(G22Utils g22utils) {
  this.g22utils = g22utils;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + g22utils + ")"; }public Object[] _fieldsToList() { return new Object[] {g22utils}; }

   final public G22JavaObjectVisualizer setWithTypeAndTime(boolean withTypeAndTime){ return withTypeAndTime(withTypeAndTime); }
public G22JavaObjectVisualizer withTypeAndTime(boolean withTypeAndTime) { this.withTypeAndTime = withTypeAndTime; return this; }  final public boolean getWithTypeAndTime(){ return withTypeAndTime(); }
public boolean withTypeAndTime() { return withTypeAndTime; }
 boolean withTypeAndTime = true;
   final public G22JavaObjectVisualizer setSingleLineLayout(boolean singleLineLayout){ return singleLineLayout(singleLineLayout); }
public G22JavaObjectVisualizer singleLineLayout(boolean singleLineLayout) { this.singleLineLayout = singleLineLayout; return this; }  final public boolean getSingleLineLayout(){ return singleLineLayout(); }
public boolean singleLineLayout() { return singleLineLayout; }
 boolean singleLineLayout = false; // TODO
  
   final public G22JavaObjectVisualizer setObject(Object object){ return object(object); }
public G22JavaObjectVisualizer object(Object object) { this.object = object; return this; }  final public Object getObject(){ return object(); }
public Object object() { return object; }
transient Object object;
   final public G22JavaObjectVisualizer setNanos(long nanos){ return nanos(nanos); }
public G22JavaObjectVisualizer nanos(long nanos) { this.nanos = nanos; return this; }  final public long getNanos(){ return nanos(); }
public long nanos() { return nanos; }
transient long nanos;
  
  G22JavaObjectVisualizer(G22Utils g22utils, Object object) {
  this.object = object;
  this.g22utils = g22utils;}
  
  public transient JComponent visualize_cache;
public JComponent visualize() { if (visualize_cache == null) visualize_cache = visualize_load(); return visualize_cache;}

public JComponent visualize_load() { return markVisualizer(this, visualize_impl()); }
JComponent visualize_impl() {
    JComponent c;
    String type = null;
    
    try {
      c = visualize2(object);
      if (withTypeAndTime)
        type = shortClassName(object);
    } catch (Throwable e) {
      c = jErrorView(e);
      type = "Error";
    }
    
    if (withTypeAndTime) {
      String timeDesc = formatElapsedTimeWithAppropriateUnit(nanosToSeconds(nanos));
      c = northAndCenterWithMargins(
        westAndEastWithMargin(
          withLabel("Result type:", jlabel(type)),
          withLabel("Execution time:", jlabel(timeDesc))),
        c);
    }
    
    return c;
  }
  
  JComponent visualize2(Object object) {
    if (object instanceof Throwable)
      return jErrorView((Throwable) object);
    if (object == null || eq(object, ""))
      return jpanel();
    if (object instanceof Integer) object = toLong((Integer) object);
    if (object instanceof Long)
      return fontSizePlus(10, jcenteredLabel(n2((Long) object)));
    if (object instanceof Number)
      return fontSizePlus(10, jcenteredLabel(str((Number) object)));
    if (object instanceof MakesBufferedImage)      
      object = toBufferedImage((MakesBufferedImage) object);
    if (object instanceof BufferedImage)
      return jscroll_centered_borderless(g22utils.stdImageSurface((BufferedImage) object));
    return jcenteredlabel(shorten(str(object)));
  }
}
static class BetterLabel extends JLabel {
  boolean autoToolTip = true;
  
  BetterLabel() {
    // Listeners given out to componentPopupMenu must not directly
    // reference the outer object (-> weak map problem).
    final WeakReference < BetterLabel > me = new WeakReference<>(this);
    componentPopupMenu(this, BetterLabel_menuItems(me));
  }
  
  BetterLabel(String text) {
    this();
    this.setText(text);
  }
  
  public void setText(String text) {
    super.setText(text);
    if (autoToolTip)
      if (!swic(text, "<html>")) // HTML labels make super-huge, confusing tool tips
        setToolTipText(nullIfEmpty(text));
  }
}

// moved outside of class for GC reasons (see above)
static VF1<JPopupMenu> BetterLabel_menuItems(final WeakReference<BetterLabel> me) {
  return new VF1<JPopupMenu>() { public void get(JPopupMenu menu) { try { 
    addMenuItem(menu, "Copy text to clipboard", new Runnable() {  public void run() { try { 
      copyTextToClipboard(me.get().getText());
    
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "copyTextToClipboard(me.get().getText());"; }});
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "addMenuItem(menu, \"Copy text to clipboard\", r {\r\n      copyTextToClipboard(me..."; }};
}
// elements are put to front when added (not when accessed)
static class MRUCache<A, B> extends LinkedHashMap<A, B> {
  int maxSize = 10;

  MRUCache() {}
  MRUCache(int maxSize) {
  this.maxSize = maxSize;}
  
  protected boolean removeEldestEntry(Map.Entry eldest) {
    return size() > maxSize;
  }
  
  Object _serialize() {
    return ll(maxSize, cloneLinkedHashMap(this));
  }
  
  static MRUCache _deserialize(List l) {
    MRUCache m = new MRUCache();
    m.maxSize = (int) first(l);
    m.putAll((LinkedHashMap) second(l));
    return m;
  }
}
public static interface IF0<A> {
  A get();
}
static interface Hasher<A> {
  int hashCode(A a);
  boolean equals(A a, A b);
}
static abstract class CloseableIterableIterator<A> extends IterableIterator<A> implements AutoCloseable {
  public void close() throws Exception {}
}
static interface IFieldsToList {
  Object[] _fieldsToList();
}
static interface IF2<A, B, C> {
  C get(A a, B b);
}

static interface IF1<A, B> {
  B get(A a);
}
static class PersistableThrowable extends DynamicObject {
  String className;
  String msg;
  String stacktrace;
  
  PersistableThrowable() {}
  PersistableThrowable(Throwable e) {
    if (e == null)
      className = "Crazy Null Error";
    else {
      className = getClassName(e).replace('/', '.');
      msg = e.getMessage();
      stacktrace = getStackTrace_noRecord(e);
    }
  }
  
  public String toString() {
    return nempty(msg) ? className + ": " + msg : className;
  }
  
  RuntimeException asRuntimeException() {
    return new Fail(this);
  }
}
static interface IVF1<A> {
  void get(A a);
}
static interface IVF2<A, B> {
  void get(A a, B b);
}
static interface IVar<A> extends IF0<A> {
  void set(A a);
  A get();
  
  // reified type of value (if available)
  default Class<A> getType() { return null; }
  
  
  default boolean has() { return get() != null; }
  default void clear() { set(null); }
  
}


interface Enterable {
  AutoCloseable enter();
}
static class BackgroundProcessesUI {
  Set<Entry> processes = setWithNotify(syncLinkedHashSet(), () -> updateCount());
  SimpleLiveValue<Integer> lvCount = new SimpleLiveValue<Integer>(Integer.class, 0);
  
  class Entry implements AutoCloseable , IFieldsToList{
  String name;
  Entry() {}
  Entry(String name) {
  this.name = name;}

public boolean equals(Object o) {
if (!(o instanceof Entry)) return false;
    Entry __0 =  (Entry) o;
    return eq(name, __0.name);
}

  public int hashCode() {
    int h = 67115090;
    h = boostHashCombine(h, _hashCode(name));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {name}; }

     final public Entry setMenuItem(JMenuItem menuItem){ return menuItem(menuItem); }
public Entry menuItem(JMenuItem menuItem) { this.menuItem = menuItem; return this; }  final public JMenuItem getMenuItem(){ return menuItem(); }
public JMenuItem menuItem() { return menuItem; }
 JMenuItem menuItem;
    
    public String toString() { return name; }
    
    public void close() { try { processes.remove(this); } catch (Exception __e) { throw rethrow(__e); } }
  }
  
  JLabel shortLabel() {
    var lbl = bindToolTipToTransformedLiveValue(
      n -> makeToolTip(), lvCount,
        simpleTransformedLiveValueLabel(n -> n2(n), lvCount));
    
    onMouseDown_anyButton(lbl, e -> {
      var l = cloneList(processes);
      
      // make sure title is displayed fully
      JPopupMenu menu = new JPopupMenu();/* {
        public Dimension getMinimumSize() {
          TitledBorder border = optCast TitledBorder(getBorder());
          _print(+border);
          ret maxDimension(super.getMinimumSize(), border?.getMinimumSize(this));
        }
      };*/
      
      int n = componentCount(menu);
      for (Entry a : l)
        addMenuItem(menu, processToMenuItem(a));
      if (componentCount(menu) != n) {
        var border = jRaisedSectionBorder("Background Processes");
        setBorder(menu, border);
        showPopupMenu(menu, e);
        var size = menu.getSize();
        var borderSize = dimensionPlus(10 /* hack */, 0, border.getMinimumSize(menu));
        printVars("size", size, "borderSize", borderSize);
        menu.setPopupSize(maxDimension(size, borderSize));
      }
    });
    
    return lbl;
  }
  
  void add(Entry process) {
    processes.add(process);
  }
  
  void remove(Entry process) {
    processes.remove(process);
  }
  
  void addOrRemove(boolean add, Entry process) {
    if (add) add(process); else remove(process);
  }
  
  Entry tempAdd(String name) {
    return tempAdd(new Entry(or2(name, "Unnamed process")));
  }
  
  Entry tempAdd(Entry process) {
    if (process == null) return null;
    processes.add(process);
    return process;
  }
  
  void updateCount() {
    lvCount.set(l(processes));
  }
  
  String makeToolTip() {
    var l = cloneList(processes);
    return empty(l) ? "No background processes"
      : n2(l, "background process", "background processes")
      + ": " + joinWithComma(processes);
  }
  
  transient  IF1<Entry, JMenuItem> processToMenuItem;
JMenuItem processToMenuItem(Entry process) { return processToMenuItem != null ? processToMenuItem.get(process) : processToMenuItem_base(process); }
final JMenuItem processToMenuItem_fallback(IF1<Entry, JMenuItem> _f, Entry process) { return _f != null ? _f.get(process) : processToMenuItem_base(process); }
JMenuItem processToMenuItem_base(Entry process) {
    return process.getMenuItem();
  }
}
static class PtInComponent<A extends Component> implements IFieldsToList{
  A component;
  Pt p;
  PtInComponent() {}
  PtInComponent(A component, Pt p) {
  this.p = p;
  this.component = component;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + component + ", " + p + ")"; }

public boolean equals(Object o) {
if (!(o instanceof PtInComponent)) return false;
    PtInComponent __1 =  (PtInComponent) o;
    return eq(component, __1.component) && eq(p, __1.p);
}

  public int hashCode() {
    int h = -1774729068;
    h = boostHashCombine(h, _hashCode(component));
    h = boostHashCombine(h, _hashCode(p));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {component, p}; }

  PtInComponent(Pt p, A component) {
  this.component = component;
  this.p = p;}
}
/**
 * A class that provides scrolling capabilities to a long menu dropdown or
 * popup menu.  A number of items can optionally be frozen at the top and/or
 * bottom of the menu.
 * <P>
 * <B>Implementation note:</B>  The default number of items to display
 * at a time is 15, and the default scrolling interval is 125 milliseconds.
 * <P>
 *
 * @version 1.5.0 04/05/12
 * @author Darryl
 * https://tips4java.wordpress.com/2009/02/01/menu-scroller/
 */
 


static class JMenuScroller {

  VF1<JPopupMenu> fillMenu;
  private JPopupMenu menu;
  private Component[] menuItems;
  private MenuScrollItem upItem;
  private MenuScrollItem downItem;
  private final MenuScrollListener menuListener = new MenuScrollListener();
  private int scrollCount;
  private int interval;
  private int topFixedCount;
  private int bottomFixedCount;
  private int firstIndex = 0;
  private int keepVisibleIndex = -1;

  /**
   * Registers a menu to be scrolled with the default number of items to
   * display at a time and the default scrolling interval.
   * 
   * @param menu the menu
   * @return the JMenuScroller
   */
  public static JMenuScroller setScrollerFor(JMenu menu) {
    return new JMenuScroller(menu);
  }

  /**
   * Registers a popup menu to be scrolled with the default number of items to
   * display at a time and the default scrolling interval.
   * 
   * @param menu the popup menu
   * @return the JMenuScroller
   */
  public static JMenuScroller setScrollerFor(JPopupMenu menu) {
    return new JMenuScroller(menu);
  }

  /**
   * Registers a menu to be scrolled with the default number of items to
   * display at a time and the specified scrolling interval.
   * 
   * @param menu the menu
   * @param scrollCount the number of items to display at a time
   * @return the JMenuScroller
   * @throws IllegalArgumentException if scrollCount is 0 or negative
   */
  public static JMenuScroller setScrollerFor(JMenu menu, int scrollCount) {
    return new JMenuScroller(menu, scrollCount);
  }

  /**
   * Registers a popup menu to be scrolled with the default number of items to
   * display at a time and the specified scrolling interval.
   * 
   * @param menu the popup menu
   * @param scrollCount the number of items to display at a time
   * @return the JMenuScroller
   * @throws IllegalArgumentException if scrollCount is 0 or negative
   */
  public static JMenuScroller setScrollerFor(JPopupMenu menu, int scrollCount) {
    return new JMenuScroller(menu, scrollCount);
  }

  /**
   * Registers a menu to be scrolled, with the specified number of items to
   * display at a time and the specified scrolling interval.
   * 
   * @param menu the menu
   * @param scrollCount the number of items to be displayed at a time
   * @param interval the scroll interval, in milliseconds
   * @return the JMenuScroller
   * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
   */
  public static JMenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval) {
    return new JMenuScroller(menu, scrollCount, interval);
  }

  /**
   * Registers a popup menu to be scrolled, with the specified number of items to
   * display at a time and the specified scrolling interval.
   * 
   * @param menu the popup menu
   * @param scrollCount the number of items to be displayed at a time
   * @param interval the scroll interval, in milliseconds
   * @return the JMenuScroller
   * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
   */
  public static JMenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval) {
    return new JMenuScroller(menu, scrollCount, interval);
  }

  /**
   * Registers a menu to be scrolled, with the specified number of items
   * to display in the scrolling region, the specified scrolling interval,
   * and the specified numbers of items fixed at the top and bottom of the
   * menu.
   * 
   * @param menu the menu
   * @param scrollCount the number of items to display in the scrolling portion
   * @param interval the scroll interval, in milliseconds
   * @param topFixedCount the number of items to fix at the top.  May be 0.
   * @param bottomFixedCount the number of items to fix at the bottom. May be 0
   * @throws IllegalArgumentException if scrollCount or interval is 0 or
   * negative or if topFixedCount or bottomFixedCount is negative
   * @return the JMenuScroller
   */
  public static JMenuScroller setScrollerFor(JMenu menu, int scrollCount, int interval,
          int topFixedCount, int bottomFixedCount) {
    return new JMenuScroller(menu, scrollCount, interval,
            topFixedCount, bottomFixedCount);
  }

  /**
   * Registers a popup menu to be scrolled, with the specified number of items
   * to display in the scrolling region, the specified scrolling interval,
   * and the specified numbers of items fixed at the top and bottom of the
   * popup menu.
   * 
   * @param menu the popup menu
   * @param scrollCount the number of items to display in the scrolling portion
   * @param interval the scroll interval, in milliseconds
   * @param topFixedCount the number of items to fix at the top.  May be 0
   * @param bottomFixedCount the number of items to fix at the bottom.  May be 0
   * @throws IllegalArgumentException if scrollCount or interval is 0 or
   * negative or if topFixedCount or bottomFixedCount is negative
   * @return the JMenuScroller
   */
  public static JMenuScroller setScrollerFor(JPopupMenu menu, int scrollCount, int interval,
          int topFixedCount, int bottomFixedCount) {
    return new JMenuScroller(menu, scrollCount, interval,
            topFixedCount, bottomFixedCount);
  }

  /**
   * Constructs a <code>JMenuScroller</code> that scrolls a menu with the
   * default number of items to display at a time, and default scrolling
   * interval.
   * 
   * @param menu the menu
   */
  public JMenuScroller(JMenu menu) {
    this(menu, 15);
  }

  /**
   * Constructs a <code>JMenuScroller</code> that scrolls a popup menu with the
   * default number of items to display at a time, and default scrolling
   * interval.
   * 
   * @param menu the popup menu
   */
  public JMenuScroller(JPopupMenu menu) {
    this(menu, 15);
  }

  /**
   * Constructs a <code>JMenuScroller</code> that scrolls a menu with the
   * specified number of items to display at a time, and default scrolling
   * interval.
   * 
   * @param menu the menu
   * @param scrollCount the number of items to display at a time
   * @throws IllegalArgumentException if scrollCount is 0 or negative
   */
  public JMenuScroller(JMenu menu, int scrollCount) {
    this(menu, scrollCount, 150);
  }

  /**
   * Constructs a <code>JMenuScroller</code> that scrolls a popup menu with the
   * specified number of items to display at a time, and default scrolling
   * interval.
   * 
   * @param menu the popup menu
   * @param scrollCount the number of items to display at a time
   * @throws IllegalArgumentException if scrollCount is 0 or negative
   */
  public JMenuScroller(JPopupMenu menu, int scrollCount) {
    this(menu, scrollCount, 150);
  }

  /**
   * Constructs a <code>JMenuScroller</code> that scrolls a menu with the
   * specified number of items to display at a time, and specified scrolling
   * interval.
   * 
   * @param menu the menu
   * @param scrollCount the number of items to display at a time
   * @param interval the scroll interval, in milliseconds
   * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
   */
  public JMenuScroller(JMenu menu, int scrollCount, int interval) {
    this(menu, scrollCount, interval, 0, 0);
  }

  /**
   * Constructs a <code>JMenuScroller</code> that scrolls a popup menu with the
   * specified number of items to display at a time, and specified scrolling
   * interval.
   * 
   * @param menu the popup menu
   * @param scrollCount the number of items to display at a time
   * @param interval the scroll interval, in milliseconds
   * @throws IllegalArgumentException if scrollCount or interval is 0 or negative
   */
  public JMenuScroller(JPopupMenu menu, int scrollCount, int interval) {
    this(menu, scrollCount, interval, 0, 0);
  }

  /**
   * Constructs a <code>JMenuScroller</code> that scrolls a menu with the
   * specified number of items to display in the scrolling region, the
   * specified scrolling interval, and the specified numbers of items fixed at
   * the top and bottom of the menu.
   * 
   * @param menu the menu
   * @param scrollCount the number of items to display in the scrolling portion
   * @param interval the scroll interval, in milliseconds
   * @param topFixedCount the number of items to fix at the top.  May be 0
   * @param bottomFixedCount the number of items to fix at the bottom.  May be 0
   * @throws IllegalArgumentException if scrollCount or interval is 0 or
   * negative or if topFixedCount or bottomFixedCount is negative
   */
  public JMenuScroller(JMenu menu, int scrollCount, int interval,
          int topFixedCount, int bottomFixedCount) {
    this(menu.getPopupMenu(), scrollCount, interval, topFixedCount, bottomFixedCount);
  }

  /**
   * Constructs a <code>JMenuScroller</code> that scrolls a popup menu with the
   * specified number of items to display in the scrolling region, the
   * specified scrolling interval, and the specified numbers of items fixed at
   * the top and bottom of the popup menu.
   * 
   * @param menu the popup menu
   * @param scrollCount the number of items to display in the scrolling portion
   * @param interval the scroll interval, in milliseconds
   * @param topFixedCount the number of items to fix at the top.  May be 0
   * @param bottomFixedCount the number of items to fix at the bottom.  May be 0
   * @throws IllegalArgumentException if scrollCount or interval is 0 or
   * negative or if topFixedCount or bottomFixedCount is negative
   */
  public JMenuScroller(JPopupMenu menu, int scrollCount, int interval,
          int topFixedCount, int bottomFixedCount) {
    if (scrollCount <= 0 || interval <= 0) {
      throw new IllegalArgumentException("scrollCount and interval must be greater than 0");
    }
    if (topFixedCount < 0 || bottomFixedCount < 0) {
      throw new IllegalArgumentException("topFixedCount and bottomFixedCount cannot be negative");
    }

    upItem = new MenuScrollItem(UP, -1);
    downItem = new MenuScrollItem(DOWN, +1);
    setScrollCount(scrollCount);
    setInterval(interval);
    setTopFixedCount(topFixedCount);
    setBottomFixedCount(bottomFixedCount);

    this.menu = menu;
    menu.addPopupMenuListener(menuListener);
  }

  /**
   * Returns the scroll interval in milliseconds
   * 
   * @return the scroll interval in milliseconds
   */
  public int getInterval() {
    return interval;
  }

  /**
   * Sets the scroll interval in milliseconds
   * 
   * @param interval the scroll interval in milliseconds
   * @throws IllegalArgumentException if interval is 0 or negative
   */
  public void setInterval(int interval) {
    if (interval <= 0) {
      throw new IllegalArgumentException("interval must be greater than 0");
    }
    upItem.setInterval(interval);
    downItem.setInterval(interval);
    this.interval = interval;
  }

  /**
   * Returns the number of items in the scrolling portion of the menu.
   *
   * @return the number of items to display at a time
   */
  public int getscrollCount() {
    return scrollCount;
  }

  /**
   * Sets the number of items in the scrolling portion of the menu.
   * 
   * @param scrollCount the number of items to display at a time
   * @throws IllegalArgumentException if scrollCount is 0 or negative
   */
  public void setScrollCount(int scrollCount) {
    if (scrollCount <= 0) {
      throw new IllegalArgumentException("scrollCount must be greater than 0");
    }
    this.scrollCount = scrollCount;
    // XXX the following line closes all menus then this menu is made.
    // That doesn't seem right.
    // MenuSelectionManager.defaultManager().clearSelectedPath();
  }

  /**
   * Returns the number of items fixed at the top of the menu or popup menu.
   * 
   * @return the number of items
   */
  public int getTopFixedCount() {
    return topFixedCount;
  }

  /**
   * Sets the number of items to fix at the top of the menu or popup menu.
   * 
   * @param topFixedCount the number of items
   */
  public void setTopFixedCount(int topFixedCount) {
    if (firstIndex <= topFixedCount) {
      firstIndex = topFixedCount;
    } else {
      firstIndex += (topFixedCount - this.topFixedCount);
    }
    this.topFixedCount = topFixedCount;
  }

  /**
   * Returns the number of items fixed at the bottom of the menu or popup menu.
   * 
   * @return the number of items
   */
  public int getBottomFixedCount() {
    return bottomFixedCount;
  }

  /**
   * Sets the number of items to fix at the bottom of the menu or popup menu.
   * 
   * @param bottomFixedCount the number of items
   */
  public void setBottomFixedCount(int bottomFixedCount) {
    this.bottomFixedCount = bottomFixedCount;
  }

  /**
   * Scrolls the specified item into view each time the menu is opened.  Call this method with
   * <code>null</code> to restore the default behavior, which is to show the menu as it last
   * appeared.
   *
   * @param item the item to keep visible
   * @see #keepVisible(int)
   */
  public void keepVisible(JMenuItem item) {
    if (item == null) {
      keepVisibleIndex = -1;
    } else {
      int index = menu.getComponentIndex(item);
      keepVisibleIndex = index;
    }
  }

  /**
   * Scrolls the item at the specified index into view each time the menu is opened.  Call this
   * method with <code>-1</code> to restore the default behavior, which is to show the menu as
   * it last appeared.
   *
   * @param index the index of the item to keep visible
   * @see #keepVisible(javax.swing.JMenuItem)
   */
  public void keepVisible(int index) {
    keepVisibleIndex = index;
  }

  /**
   * Removes this JMenuScroller from the associated menu and restores the
   * default behavior of the menu.
   */
  public void dispose() {
    if (menu != null) {
      menu.removePopupMenuListener(menuListener);
      menu = null;
    }
  }

  /**
   * Ensures that the <code>dispose</code> method of this JMenuScroller is
   * called when there are no more refrences to it.
   * 
   * @exception  Throwable if an error occurs.
   * @see JMenuScroller#dispose()
   */
  @Override
  public void finalize() throws Throwable {
    dispose();
  }

  private void refreshMenu() {
    if (menuItems != null && menuItems.length > 0) {
      firstIndex = Math.max(topFixedCount, firstIndex);
      firstIndex = Math.min(menuItems.length - bottomFixedCount - scrollCount, firstIndex);

      upItem.setEnabled(firstIndex > topFixedCount);
      downItem.setEnabled(firstIndex + scrollCount < menuItems.length - bottomFixedCount);

      menu.removeAll();
      for (int i = 0; i < topFixedCount; i++) {
        menu.add(menuItems[i]);
      }
      if (topFixedCount > 0) {
        menu.addSeparator();
      }

      menu.add(upItem);
      for (int i = firstIndex; i < scrollCount + firstIndex; i++) {
        menu.add(menuItems[i]);
      }
      menu.add(downItem);

      if (bottomFixedCount > 0) {
        menu.addSeparator();
      }
      for (int i = menuItems.length - bottomFixedCount; i < menuItems.length; i++) {
        menu.add(menuItems[i]);
      }

      JComponent parent = (JComponent) upItem.getParent();
      parent.revalidate();
      parent.repaint();
    }
  }

  private class MenuScrollListener implements PopupMenuListener {

    @Override
    public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
      if (fillMenu != null) {
        clearPopupMenu(menu);
        callF(fillMenu, menu);
      }
      setMenuItems();
    }

    @Override
    public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
      if (fillMenu != null) clearPopupMenu(menu);
      else restoreMenuItems();
    }

    @Override
    public void popupMenuCanceled(PopupMenuEvent e) {
      if (fillMenu != null) clearPopupMenu(menu);
      else restoreMenuItems();
    }

    private void setMenuItems() {
      menuItems = menu.getComponents();
      if (keepVisibleIndex >= topFixedCount
              && keepVisibleIndex <= menuItems.length - bottomFixedCount
              && (keepVisibleIndex > firstIndex + scrollCount
              || keepVisibleIndex < firstIndex)) {
        firstIndex = Math.min(firstIndex, keepVisibleIndex);
        firstIndex = Math.max(firstIndex, keepVisibleIndex - scrollCount + 1);
      }
      if (menuItems.length > topFixedCount + scrollCount + bottomFixedCount) {
        refreshMenu();
      }
    }

    private void restoreMenuItems() {
      menu.removeAll();
      for (Component component : menuItems) {
        menu.add(component);
      }
    }
  }

  private class MenuScrollTimer extends javax.swing.Timer {

    public MenuScrollTimer(final int increment, int interval) {
      super(interval, new ActionListener() {

        @Override
        public void actionPerformed(ActionEvent e) {
          firstIndex += increment;
          refreshMenu();
        }
      });
    }
  }

  private class MenuScrollItem extends JMenuItem
          implements ChangeListener {

    private MenuScrollTimer timer;

    public MenuScrollItem(MenuIcon icon, int increment) {
      setIcon(icon);
      setDisabledIcon(icon);
      timer = new MenuScrollTimer(increment, interval);
      addChangeListener(this);
    }

    public void setInterval(int interval) {
      timer.setDelay(interval);
    }

    @Override
    public void stateChanged(ChangeEvent e) {
      if (isArmed() && !timer.isRunning()) {
        timer.start();
      }
      if (!isArmed() && timer.isRunning()) {
        timer.stop();
      }
    }
  }

  static MenuIcon UP = new MenuIcon(9, 1, 9);
  static MenuIcon DOWN = new MenuIcon(1, 9, 1);
    
  private static class MenuIcon implements Icon {

    final int[] xPoints = {1, 5, 9};
    final int[] yPoints;

    MenuIcon(int... yPoints) {
      this.yPoints = yPoints;
    }

    @Override
    public void paintIcon(Component c, Graphics g, int x, int y) {
      Dimension size = c.getSize();
      Graphics g2 = g.create(size.width / 2 - 5, size.height / 2 - 5, 10, 10);
      g2.setColor(Color.GRAY);
      g2.drawPolygon(xPoints, yPoints, 3);
      if (c.isEnabled()) {
        g2.setColor(Color.BLACK);
        g2.fillPolygon(xPoints, yPoints, 3);
      }
      g2.dispose();
    }

    @Override
    public int getIconWidth() {
      return 0;
    }

    @Override
    public int getIconHeight() {
      return 10;
    }
  }
}
static interface ISetter<A> {
 void set(A a);
}

// takes ~70 ms to set up, do it only once if possible
static class ClassNameResolver {
   final public ClassNameResolver setByteCodePath(File byteCodePath){ return byteCodePath(byteCodePath); }
public ClassNameResolver byteCodePath(File byteCodePath) { this.byteCodePath = byteCodePath; return this; }  final public File getByteCodePath(){ return byteCodePath(); }
public File byteCodePath() { return byteCodePath; }
 File byteCodePath = byteCodePathForClass(getClass());
  List<String> importedPackages = itemPlusList("java.lang",
    endingWith_dropSuffix(standardImports(), ".*"));
  
   Set<String> allFullyQualifiedClassNames_cache;
 Set<String> allFullyQualifiedClassNames() { if (allFullyQualifiedClassNames_cache == null) allFullyQualifiedClassNames_cache = allFullyQualifiedClassNames_load(); return allFullyQualifiedClassNames_cache;}

 Set<String> allFullyQualifiedClassNames_load() {
    Set<String> set = new HashSet();
    assertNotNull(byteCodePath);
    set.addAll(classNamesInJarOrDir(byteCodePath));
    printVars("ClassNameResolver", "byteCodePath", byteCodePath, "classesFound" , l(set));
    set.addAll(classNamesInLoadedJigsawModules());
    return set;
  }
  
  ClassNameResolver init() { allFullyQualifiedClassNames(); return this; }

  String findClass(String name) {
    for (String pkg : importedPackages) {
      String fullName = pkg + "." + name;
      if (allFullyQualifiedClassNames().contains(fullName))
        return fullName;
    }
    return null;
  }
  
  void printMe() {
    printVars("ClassNameResolver", "byteCodePath", byteCodePath);
    print("importedPackages", importedPackages);
  }
}
static interface IAutoCloseableF0<A> extends IF0<A>, AutoCloseable {}
/*
 * #!
 * Ontopia Engine
 * #-
 * Copyright (C) 2001 - 2013 The Ontopia Project
 * #-
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * !#
 */

// modified by Stefan Reich

// Implements the Set interface more compactly than
// java.util.HashSet by using a closed hashtable.

// Note: equals is always called on the _stored_ object, not the one
// passed as an argument to find(), contains() etc.
// (In case you want to put special magic in your equals() function)

static class UnsynchronizedCompactHashSet<A> extends java.util.AbstractSet<A> {
  protected final static int INITIAL_SIZE = 3;
  public final static double LOAD_FACTOR = 0.75;

  protected final static Object nullObject = new Object();
  protected final static Object deletedObject = new Object();
  protected int elements;
  protected int freecells;
  protected A[] objects;
  
  protected int modCount;
  
  
  UnsynchronizedCompactHashSet() {
    this(INITIAL_SIZE);
  }

  UnsynchronizedCompactHashSet(int size) {
    // NOTE: If array size is 0, we get a
    // "java.lang.ArithmeticException: / by zero" in add(Object).
    objects = (A[]) new Object[(size==0 ? 1 : size)];
    elements = 0;
    freecells = objects.length;
    
      modCount = 0;
    
  }

  UnsynchronizedCompactHashSet(Collection<A> c) {
    this(c.size());
    addAll(c);
  }

  @Override
  public Iterator<A> iterator() {
    return new CompactHashIterator<A>();
  }

  @Override
  public int size() {
    return elements;
  }

  @Override
  public boolean isEmpty() {
    return elements == 0;
  }

  @Override
  public boolean contains(Object o) {
    return find(o) != null;
  }
  
  A find(Object o) {
    if (o == null) o = nullObject;
    
    int hash = o.hashCode();
    int index = (hash & 0x7FFFFFFF) % objects.length;
    int offset = 1;

    // search for the object (continue while !null and !this object)
    while(objects[index] != null &&
          !(objects[index].hashCode() == hash &&
            objects[index].equals(o))) {
      index = ((index + offset) & 0x7FFFFFFF) % objects.length;
      offset = offset*2 + 1;

      if (offset == -1)
        offset = 2;
    }

    return objects[index];
  }
  
  boolean removeIfSame(Object o) {
    A value = find(o);
    if (value == o) {
      remove(value);
      return true;
    }
    return false;
  }

  @Override
  public boolean add(Object o) {
    if (o == null) o = nullObject;

    int hash = o.hashCode();
    int index = (hash & 0x7FFFFFFF) % objects.length;
    int offset = 1;
    int deletedix = -1;
    
    // search for the object (continue while !null and !this object)
    while(objects[index] != null &&
          !(objects[index].hashCode() == hash &&
            objects[index].equals(o))) {

      // if there's a deleted object here we can put this object here,
      // provided it's not in here somewhere else already
      if (objects[index] == deletedObject)
        deletedix = index;
      
      index = ((index + offset) & 0x7FFFFFFF) % objects.length;
      offset = offset*2 + 1;

      if (offset == -1)
        offset = 2;
    }
    
    if (objects[index] == null) { // wasn't present already
      if (deletedix != -1) // reusing a deleted cell
        index = deletedix;
      else
        freecells--;

      
        modCount++;
      
      elements++;

      // here we face a problem regarding generics:
      // add(A o) is not possible because of the null Object. We cant do 'new A()' or '(A) new Object()'
      // so adding an empty object is a problem here
      // If (! o instanceof A) : This will cause a class cast exception
      // If (o instanceof A) : This will work fine

      objects[index] = (A) o;
      
      // do we need to rehash?
      if (1 - (freecells / (double) objects.length) > LOAD_FACTOR)
        rehash();
      return true;
    } else // was there already 
      return false;
  }
  
  @Override
  public boolean remove(Object o) {
    if (o == null) o = nullObject;
    
    int hash = o.hashCode();
    int index = (hash & 0x7FFFFFFF) % objects.length;
    int offset = 1;
    
    // search for the object (continue while !null and !this object)
    while(objects[index] != null &&
          !(objects[index].hashCode() == hash &&
            objects[index].equals(o))) {
      index = ((index + offset) & 0x7FFFFFFF) % objects.length;
      offset = offset*2 + 1;

      if (offset == -1)
        offset = 2;
    }

    // we found the right position, now do the removal
    if (objects[index] != null) {
      // we found the object

      // same problem here as with add
      objects[index] = (A) deletedObject;
      
        modCount++;
      
      elements--;
      return true;
    } else
      // we did not find the object
      return false;
  }
  
  @Override
  public void clear() {
    elements = 0;
    for (int ix = 0; ix < objects.length; ix++)
      objects[ix] = null;
    freecells = objects.length;
    
      modCount++;
    
  }

  @Override
  public Object[] toArray() {
    Object[] result = new Object[elements];
    Object[] objects = this.objects;
    int pos = 0;
    for (int i = 0; i < objects.length; i++)
      if (objects[i] != null && objects[i] != deletedObject) {
        if (objects[i] == nullObject)
          result[pos++] = null;
        else
          result[pos++] = objects[i];
      }
    // unchecked because it should only contain A
    return result;
  }

  // not sure if this needs to have generics
  @Override
  public <T> T[] toArray(T[] a) {
    int size = elements;
    if (a.length < size)
      a = (T[])java.lang.reflect.Array.newInstance(
                                 a.getClass().getComponentType(), size);
    A[] objects = this.objects;
    int pos = 0;
    for (int i = 0; i < objects.length; i++)
      if (objects[i] != null && objects[i] != deletedObject) {
        if (objects[i] == nullObject)
          a[pos++] = null;
        else
          a[pos++] = (T) objects[i];
      }
    return a;
  }
  
  protected void rehash() {
    int garbagecells = objects.length - (elements + freecells);
    if (garbagecells / (double) objects.length > 0.05)
      // rehash with same size
      rehash(objects.length);
    else
      // rehash with increased capacity
      rehash(objects.length*2 + 1);
  }
  
  protected void rehash(int newCapacity) {
    int oldCapacity = objects.length;
    @SuppressWarnings("unchecked")
    A[] newObjects = (A[]) new Object[newCapacity];

    for (int ix = 0; ix < oldCapacity; ix++) {
      Object o = objects[ix];
      if (o == null || o == deletedObject)
        continue;
      
      int hash = o.hashCode();
      int index = (hash & 0x7FFFFFFF) % newCapacity;
      int offset = 1;

      // search for the object
      while(newObjects[index] != null) { // no need to test for duplicates
        index = ((index + offset) & 0x7FFFFFFF) % newCapacity;
        offset = offset*2 + 1;

        if (offset == -1)
          offset = 2;
      }

      newObjects[index] = (A) o;
    }

    objects = newObjects;
    freecells = objects.length - elements;
  }
  
  private class CompactHashIterator<T> implements Iterator<T> {
    private int index;
    private int lastReturned = -1;

    
      private int expectedModCount;
    

    @SuppressWarnings("empty-statement")
    public CompactHashIterator() {
        for (index = 0; index < objects.length &&
                        (objects[index] == null ||
                        objects[index] == deletedObject); index++)
          ;
        
          expectedModCount = modCount;
        
    }

    @Override
    public boolean hasNext() {
        return index < objects.length;
    }

    @SuppressWarnings("empty-statement")
    @Override
    public T next() {
        /*if (modCount != expectedModCount)
          throw new ConcurrentModificationException();*/
        int length = objects.length;
        if (index >= length) {
          lastReturned = -2;
          throw new NoSuchElementException();
        }
  
        lastReturned = index;
        for (index += 1; index < length &&
                         (objects[index] == null ||
                          objects[index] == deletedObject); index++)
          ;
        if (objects[lastReturned] == nullObject)
          return null;
        else
          return (T) objects[lastReturned];
    }

    @Override
    public void remove() {
        
          if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        
        if (lastReturned == -1 || lastReturned == -2)
          throw new IllegalStateException();
        // delete object
        if (objects[lastReturned] != null && objects[lastReturned] != deletedObject) {
          objects[lastReturned] = (A) deletedObject;
          elements--;
          
            modCount++;
            expectedModCount = modCount; // this is expected; we made the change
          
        }
    }
  }
  
  int capacity() { return objects.length; }
  
  // returns true if there was a shrink
  boolean shrinkToFactor(double factor) {
    if (factor > LOAD_FACTOR)
      throw fail("Shrink factor must be equal to or smaller than load factor: " + factor + " / " + LOAD_FACTOR);
    int newCapacity = max(INITIAL_SIZE, iround(size()/factor));
    if (newCapacity >= capacity()) return false;
    rehash(newCapacity);
    return true;
  }
}
interface G22ProjectActions {
  void openObjectInProject(long id);
  void openPathInProject(String path);
  void editProjectStory();
  void editScripts();
}
static class ImageSurface extends Surface {
  BufferedImage image;
  double zoomX = 1, zoomY = 1, zoomFactor = 1.5;
  private Rectangle selection;
  List<AutoCloseable> tools = new ArrayList();
  
  // use overlays now
  Object overlay; // voidfunc(Graphics2D)
  List<G2Drawable> overlays = syncL();
  
  Runnable onSelectionChange;
   final public ImageSurface setVerbose(boolean verbose){ return verbose(verbose); }
public ImageSurface verbose(boolean verbose) { this.verbose = verbose; return this; }  final public boolean getVerbose(){ return verbose(); }
public boolean verbose() { return verbose; }
 boolean verbose = false;
  boolean noMinimumSize = true;
  String titleForUpload;
  Object onZoom;
  boolean specialPurposed = false; // true = don't show image changing commands in popup menu
   final public ImageSurface setZoomable(boolean zoomable){ return zoomable(zoomable); }
public ImageSurface zoomable(boolean zoomable) { this.zoomable = zoomable; return this; }  final public boolean getZoomable(){ return zoomable(); }
public boolean zoomable() { return zoomable; }
 boolean zoomable = true;
  boolean noAlpha = false; // set to true to speed up drawing if you don't use alpha
  Object interpolationMode = RenderingHints.VALUE_INTERPOLATION_BILINEAR;
  Object onNewImage;
  BufferedImage imageToDraw; // if you want to draw a different image
  File file; // where image was loaded from
  boolean autoZoomToDisplay = false; // only works 100% when not in scrollpane
   final public ImageSurface setRepaintInThread(boolean repaintInThread){ return repaintInThread(repaintInThread); }
public ImageSurface repaintInThread(boolean repaintInThread) { this.repaintInThread = repaintInThread; return this; }  final public boolean getRepaintInThread(){ return repaintInThread(); }
public boolean repaintInThread() { return repaintInThread; }
 boolean repaintInThread = false; // after setImage, repaint in same thread
  BoolVar showingVar;
  
  Pt mousePosition;
  transient Set<Runnable> onMousePositionChanged;
public ImageSurface onMousePositionChanged(Runnable r) { onMousePositionChanged = createOrAddToSyncLinkedHashSet(onMousePositionChanged, r); return this; }
public ImageSurface removeMousePositionChangedListener(Runnable r) { main.remove(onMousePositionChanged, r); return this; }
public void mousePositionChanged() {  pcallFAll(onMousePositionChanged); }
 
  public ImageSurface() {
    this(dummyImage());
  }
  
  static BufferedImage dummyImage() {
    return whiteImage(1);
  }

  ImageSurface(MakesBufferedImage image) {
    this(image != null ? image.getBufferedImage() : dummyImage());
  }
  
  ImageSurface(BufferedImage image) {
    setImage(image);
    clearSurface = false;

    onResize(this, () -> performAutoZoom());
    bindToComponent(this, () -> performAutoZoom(), null);
    
    componentPopupMenu2(this, ImageSurface_popupMenuMaker());
    new ImageSurfaceSelector(this);
    
    jHandleFileDrop(this, new VF1<File>() { public void get(File f) { try {  setImage(loadBufferedImage(f)) ; } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "setImage(loadBufferedImage(f))"; }});
    
    imageSurfaceOnHover(this, p -> {
      mousePosition = p;
      mousePositionChanged();
    });
  }

  public ImageSurface(RGBImage image, double zoom) {
    this(image);
    setZoom(zoom);
  }

  // point is already in image coordinates
  protected void fillPopupMenu(JPopupMenu menu, final Point point) {
    if (zoomable) {
      JMenuItem miZoomReset = new JMenuItem("Zoom 100%");
      miZoomReset.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent evt) {
          setZoom(1.0);
          centerPoint(point);
        }
      });
      menu.add(miZoomReset);
  
      JMenuItem miZoomIn = new JMenuItem("Zoom in");
      miZoomIn.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent evt) {
          zoomIn(zoomFactor);
          centerPoint(point);
        }
      });
      menu.add(miZoomIn);
  
      JMenuItem miZoomOut = new JMenuItem("Zoom out");
      miZoomOut.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent evt) {
          zoomOut(zoomFactor);
          centerPoint(point);
        }
      });
      menu.add(miZoomOut);
  
      /*JMenuItem miZoomToWindow = new JMenuItem("Zoom to window");
      miZoomToWindow.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent evt) {
          zoomToDisplaySize();
        }
      });
      menu.add(miZoomToWindow);*/
      
      menu.add(jCheckBoxMenuItem_dyn(/*"Auto-zoom to window"*/"Zoom to window",
        () -> autoZoomToDisplay,
        b -> { setAutoZoomToDisplay(b); }));
      
      addMenuItem(menu, "Show full screen", new Runnable() {  public void run() { try {  showFullScreen() ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "showFullScreen()"; }});
      
      addMenuItem(menu, "Point: " + point.x + "," + point.y + " (image: " + w() + "*" + h() + ")", null);
  
      menu.addSeparator();
    }

    if (!specialPurposed)
      addMenuItem(menu, "Load image...", new Runnable() {  public void run() { try {  selectFile("Load image",
        new VF1<File>() { public void get(File f) { try {  setImage(loadImage2(f)) ; } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "setImage(loadImage2(f))"; }}) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "selectFile(\"Load image\",\r\n        new VF1<File>() { public void get(File f) c..."; }});
    addMenuItem(menu, "Save image...", new Runnable() {  public void run() { try {  saveImage() ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "saveImage()"; }});
    addMenuItem(menu, "Upload image...", new Runnable() {  public void run() { try {  uploadTheImage() ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "uploadTheImage()"; }});
    addMenuItem(menu, "Copy image to clipboard", new Runnable() {  public void run() { try {  copyImageToClipboard(getImage()) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "copyImageToClipboard(getImage())"; }});
    if (!specialPurposed) {
      addMenuItem(menu, "Paste image from clipboard", new Runnable() {  public void run() { try {  loadFromClipboard() ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "loadFromClipboard()"; }});
      addMenuItem(menu, "Load image snippet...", new Runnable() {  public void run() { try { 
        selectImageSnippet(new VF1<String>() { public void get(String imageID) { try { 
          setImage(loadImage2(imageID))
        ; } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "setImage(loadImage2(imageID))"; }});
      
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "selectImageSnippet(new VF1<String>() { public void get(String imageID) ctex {..."; }});
    }
    if (selection != null)
      addMenuItem(menu, "Crop", new Runnable() {  public void run() { try {  crop() ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "crop()"; }});
    if (!specialPurposed)
      addMenuItem(menu, "No image", new Runnable() {  public void run() { try {  noImage() ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "noImage()"; }});
  }
  
  void noImage() { setImage((BufferedImage) null); }
  
  void crop() {
    if (selection == null) return;
    BufferedImage img = cloneClipBufferedImage(getImage(), selection);
    selection = null;
    setImage(img);
  }
  
  void loadFromClipboard() {
    BufferedImage img = getImageFromClipboard();
    if (img != null)
      setImage(img);
  }

  void saveImage() {
    var image = getImage();
    JFileChooser fileChooser = new JFileChooser(getProgramDir());
    if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
      try {
        main.saveImage(image, file = fileChooser.getSelectedFile());
      } catch (Throwable e) {
        popup(e);
      }
    }
  }
  
  void drawImageItself(int w, int h, Graphics2D g) {
    int iw = getZoomedWidth(), ih = getZoomedHeight();
    if (interpolationMode == RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR || zoomX >= 1 || zoomY >= 1) {
      // faster
      g.drawImage(image, 0, 0, iw, ih, null);
    } else
      g.drawImage(resizeImage(image, iw, ih), 0, 0, null); // smoother
  }

  public void render(int w, int h, Graphics2D g) {
    if (verbose) _print("render");
    g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, interpolationMode);
    g.setColor(Color.white);
    BufferedImage image = or(imageToDraw, this.image);
    if (!hasImage())
      g.fillRect(0, 0, w, h);
    else {
      boolean alpha = !noAlpha && hasTransparency(image);
      if (alpha) g.fillRect(0, 0, w, h);
    
      drawImageItself(w, h, g);
      
      int iw = getZoomedWidth(), ih = getZoomedHeight();
      if (!alpha) {
        g.fillRect(iw, 0, w-iw, h);
        g.fillRect(0, ih, iw, h-ih);
      }
    }

    if (overlay != null) {
      if (verbose) _print("render overlay");
      pcallF(overlay, g);
    }
    
    for (var overlay : cloneList(overlays)) { try {
      overlay.drawOn(g);
    } catch (Throwable __e) { printStackTrace(__e); }}

    if (selection != null) {
      if (verbose) _print("render selection");
      // drawRect is inclusive, selection is exclusive, so... whatever, tests show it's cool.
      drawSelectionRect(g, selection, Color.green, Color.white);
    }
  }

  public void drawSelectionRect(Graphics2D g, Rectangle selection, Color green, Color white) {
    drawSelectionRect(g, selection, green, white, zoomX, zoomY);
  }
  
  public void drawSelectionRect(Graphics2D g, Rectangle selection, Color green, Color white, double zoomX, double zoomY) {
    g.setColor(green);
    int top = (int) (selection.y * zoomY);
    int bottom = (int) ((selection.y+selection.height) * zoomY);
    int left = (int) (selection.x * zoomX);
    int right = (int) ((selection.x+selection.width) * zoomX);
    g.drawRect(left-1, top-1, right-left+1, bottom-top+1);
    g.setColor(white);
    g.drawRect(left - 2, top - 2, right - left + 3, bottom - top + 3);
  }

  public ImageSurface setZoom(double zoom) {
    setZoom(zoom, zoom);
    return this;
  }

  public void setZoom(double zoomX, double zoomY) {
    autoZoomToDisplay = false;
    setZoom_dontChangeAutoZoom(zoomX, zoomY);
  }
  
  public void setZoom_dontChangeAutoZoom(double zoomX) { setZoom_dontChangeAutoZoom(zoomX, zoomX); }
public void setZoom_dontChangeAutoZoom(double zoomX, double zoomY) {
    if (this.zoomX == zoomX && this.zoomY == zoomY) return;
    if (verbose) _print("Setting zoom");
    this.zoomX = zoomX;
    this.zoomY = zoomY;
    revalidateMe();
    repaint();
    centerPoint(new Point(getImage().getWidth()/2, getImage().getHeight()/2));

    pcallF(onZoom);
  }

  public Dimension getMinimumSize() {
if (metaGet("scaffolding") != null) scaffoldCalled(this, "getMinimumSize");
    if (noMinimumSize) return new Dimension(1, 1);
    int w = getZoomedWidth();
    int h = getZoomedHeight();
    Dimension min = super.getMinimumSize();
    return printIfScaffoldingEnabled(this, new Dimension(Math.max(w, min.width), Math.max(h, min.height)));
  }

  int getZoomedHeight() {
    return (int) (h() * zoomY);
  }

  int getZoomedWidth() {
    return (int) (w() * zoomX);
  }

  boolean isShowing_quick() {
    if (showingVar == null) { swing(() -> { 
      if (showingVar == null) showingVar = componentShowingVar(ImageSurface.this);
    }); }
    
    return showingVar.get();
  }
  
  public void setImageIfShowing_thisThread(MakesBufferedImage image) { setImageIfShowing_thisThread(toBufferedImage(image)); }

public void setImageIfShowing_thisThread(BufferedImage image) {
    if (isShowing_quick())
      setImage_thisThread(image);
  }

  public void setImage(MakesBufferedImage image) { swing(() -> { 
    setImage_thisThread(image);
  }); }
  
  public void setImage(BufferedImage img) { swing(() -> { 
    setImage_thisThread(img);
  }); }
    
  public void setImage_thisThread(MakesBufferedImage img) { setImage_thisThread(toBufferedImage(img)); }

public void setImage_thisThread(BufferedImage img) {
    BufferedImage newImage = img != null ? img : dummyImage();
    BufferedImage oldImage = image;
    image = newImage;
    if (verbose) print("Old image size:" + imageSize(oldImage) + ", new image size: " + imageSize(newImage));
    boolean sameSize = !imagesHaveSameSize(oldImage, newImage);
    if (!sameSize) {
      if (verbose) _print("New image size");
      revalidateMe();
    }
    quickRepaint();
    pcallF(onNewImage);
    if (!sameSize && autoZoomToDisplay) zoomToDisplaySize();
  }
  
  void setImageAndZoomToDisplay(BufferedImage img) {
    setImage(img);
    zoomToDisplaySize();
  }

  public BufferedImage getImage() {
    return image;
  }

  public double getZoomX() {
    return zoomX;
  }

  public double getZoomY() {
    return zoomY;
  }

  public Dimension getPreferredSize() {
if (metaGet("scaffolding") != null) scaffoldCalled(this, "getPreferredSize");
    return printIfScaffoldingEnabled(this, new Dimension(getZoomedWidth(), getZoomedHeight()));
  }

  /** returns a scrollpane with the scroll-mode prevent-garbage-drawing fix applied */
  public JScrollPane makeScrollPane() {
    JScrollPane scrollPane = new JScrollPane(this);
    scrollPane.getViewport().setScrollMode(JViewport.BACKINGSTORE_SCROLL_MODE);
    return scrollPane;
  }

  public void zoomToWindow() { zoomToDisplaySize(); }
  public void zoomToDisplaySize() { swing(() -> { 
    if (!hasImage()) return;
    Dimension display = getDisplaySize();
    if (display.width == 0 || display.height == 0) return;
    int w = w(), h = h();
    double xRatio = (display.width-5)/(double) w;
    double yRatio = (display.height-5)/(double) h;
    if (scaffoldingEnabled(this))
      printVars("zoomToDisplaySize", "display", display, "w", w, "h", h, "xRatio", xRatio, "yRatio", yRatio);
    setZoom_dontChangeAutoZoom(min(xRatio, yRatio));
    revalidateMe();
  }); }

  /** tricky magic to get parent scroll pane */
  private Dimension getDisplaySize() {
if (metaGet("scaffolding") != null) scaffoldCalled(this, "getDisplaySize");
    Container c = getParent();
    while (c != null) {
      if (c instanceof JScrollPane)
        return c.getSize();
      c = c.getParent();
    }
    return getSize();
  }
  
  public void setSelection(Rect r) {
    setSelection(toRectangle(r));
  }

  public void setSelection(Rectangle r) {
    if (neq(selection, r)) {
      selection = r;
      pcallF(onSelectionChange);
      quickRepaint();
    }
  }

  public Rectangle getSelection() {
    return selection;
  }

  public RGBImage getRGBImage() {
    return new RGBImage(getImage());
  }
  
  // p is in image coordinates
  void centerPoint(Point p) {
    JScrollPane sp = enclosingScrollPane(this);
    if (sp == null) return;
      
    p = new Point((int) (p.x*getZoomX()), (int) (p.y*getZoomY()));
    final JViewport viewport = sp.getViewport();
    Dimension viewSize = viewport.getExtentSize();
    
    //_print("centerPoint " + p);
    int x = max(0, p.x-viewSize.width/2);
    int y = max(0, p.y-viewSize.height/2);
    
    //_print("centerPoint " + p + " => " + x + "/" + y);
    p = new Point(x,y);
    //_print("centerPoint " + p);
    final Point _p = p;
    awtLater(new Runnable() {  public void run() { try { 
      viewport.setViewPosition(_p);
    
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "viewport.setViewPosition(_p);"; }});
  }
  
  Pt pointFromEvent(MouseEvent e) {
    return pointFromComponentCoordinates(new Pt(e.getX(), e.getY()));
  }
  
  Pt pointFromComponentCoordinates(Pt p) {
    return new Pt((int) (p.x/zoomX), (int) (p.y/zoomY));
  }
  
  Pt pointToComponentCoordinates(double x, double y) {
    return new Pt((int) (x*zoomX), (int) (y*zoomY));
  }
  
  void uploadTheImage() {
    call(hotwire(/*#1007313*/"#1016427"), "go", getImage(), titleForUpload);
  }
  
  void showFullScreen() {
    showFullScreenImageSurface(getImage());
  }
  
  void zoomIn(double f) { setZoom(getZoomX()*f, getZoomY()*f); }
  void zoomOut(double f) { setZoom(getZoomX()/f, getZoomY()/f); }
  
  ImageSurface setFile(File f) { file = f; return this; }
  
  void setOverlay(IVF1<Graphics2D> overlay) {
    this.overlay = overlay;
  }
  
  boolean hasImage() { return image != null; }
  public int w() { return image.getWidth(); }
  public int h() { return image.getHeight(); }
  
  void setPixelated(boolean b) {
    assertTrue(b);
    imageSurface_pixelated(this);
  }
  
  ImageSurface setAutoZoomToDisplay(boolean b) {
    if (autoZoomToDisplay = b)
      zoomToDisplaySize();
    return this;
  }
  
  void quickRepaint() {
    if (repaintInThread)
      paintImmediately(0, 0, getWidth(), getHeight());
    else
      repaint();
  }
  
  void setTool(ImageSurfaceMouseHandler tool) { swing(() -> { 
    removeAllTools();
    addTool(tool);
  }); }
  
  boolean hasTool(AutoCloseable tool) {
    return swing(() -> tools.contains(tool));
  }
  
  void addTool(ImageSurfaceMouseHandler tool) { swing(() -> { 
    if (!tools.contains(tool))
      tool.register(this);
  }); }
  
  void removeTool(AutoCloseable tool) { swing(() -> { 
    if (tools.contains(tool)) {
      close(tool);
      tools.remove(tool);
    }
  }); }
  
  void removeAllTools() {
    closeAllAndClear(tools);
  }
  
  void performAutoZoom() {
    if (autoZoomToDisplay) zoomToDisplaySize();
  }
  
  void revalidateMe() {
    revalidateIncludingFullCenterContainer(this);
  }
  
  void addOverlay(G2Drawable overlay) {
    overlays.add(overlay);
    repaint();
  }
  
  void clearOverlays() {
    if (nempty(overlays)) {
      overlays.clear();
      repaint();
    }
  }
  
  void loadImage(File f) {
    setImage(loadImage2(f));
  }
  
  JComponent visualize() {
    return jscroll_center_borderless(this);
  }
  
  
<A> A print(A a) { return print("", a); }
<A> A print(String msg, A a) { return _print(msg, a); }
} // end of ImageSurface

// static function allows garbage collection 
static VF2<ImageSurface, JPopupMenu> ImageSurface_popupMenuMaker() {
  return new VF2<ImageSurface, JPopupMenu>() { public void get(ImageSurface is, JPopupMenu menu) { try { 
    Point p = is.pointFromEvent(componentPopupMenu_mouseEvent.get()).getPoint();
    is.fillPopupMenu(menu, p);
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "Point p = is.pointFromEvent(componentPopupMenu_mouseEvent.get()).getPoint();\r..."; }};
}
static class CombinedStringifier<A> implements IStringifier<A> {
  CopyOnWriteArrayList<IPartialStringifier<A>> stringifiers = new CopyOnWriteArrayList();
  IStringifier<A> defaultStringifier = new Stringifier_ToString();
  
  CombinedStringifier(IPartialStringifier<A>... stringifiers) {
    addAll(this.stringifiers, stringifiers);
  }
  
  public String toString(A o) {
    for (var stringifier : stringifiers) {
      String s = stringifier.toStringOpt(o);
      if (s != null) return s;
    }
    return defaultStringifier.toString(o);
  }
  
  public String toString() {
    return formatFunctionCall("CombinedStringifier", 
      listPlus((List) stringifiers, defaultStringifier)
    );
  }
}
static interface MakesBufferedImage extends WidthAndHeight {
  BufferedImage getBufferedImage();
  
  public default void drawAt(Graphics2D g, int x, int y) {
    g.drawImage(getBufferedImage(), x, y, null);
  }
}
static class G22AutoStarter extends MetaWithChangeListeners implements AutoCloseable , IFieldsToList{
  G22Utils g22utils;
  G22AutoStarter() {}
  G22AutoStarter(G22Utils g22utils) {
  this.g22utils = g22utils;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + g22utils + ")"; }public Object[] _fieldsToList() { return new Object[] {g22utils}; }

  public transient FieldVar<Boolean> varEnabled_cache;
public FieldVar<Boolean> varEnabled() { if (varEnabled_cache == null) varEnabled_cache = varEnabled_load(); return varEnabled_cache;}

public FieldVar<Boolean> varEnabled_load() {
        return new FieldVar<Boolean>(this, "enabled", () -> enabled(), enabled -> enabled(enabled)); }
 final public G22AutoStarter setEnabled(boolean enabled){ return enabled(enabled); }
public G22AutoStarter enabled(boolean enabled) { if (!eq(this.enabled, enabled)) { this.enabled = enabled; change(); } return this; }
 final public boolean getEnabled(){ return enabled(); }
public boolean enabled() { return enabled; }
volatile boolean enabled = true;
   final public G22AutoStarter setInitialDelay(int initialDelay){ return initialDelay(initialDelay); }
public G22AutoStarter initialDelay(int initialDelay) { this.initialDelay = initialDelay; return this; }  final public int getInitialDelay(){ return initialDelay(); }
public int initialDelay() { return initialDelay; }
 int initialDelay = 3;
  public transient FieldVar<Integer> varNScriptsRun_cache;
public FieldVar<Integer> varNScriptsRun() { if (varNScriptsRun_cache == null) varNScriptsRun_cache = varNScriptsRun_load(); return varNScriptsRun_cache;}

public FieldVar<Integer> varNScriptsRun_load() {
        return new FieldVar<Integer>(this, "nScriptsRun", () -> nScriptsRun(), nScriptsRun -> nScriptsRun(nScriptsRun)); }
 final public G22AutoStarter setNScriptsRun(int nScriptsRun){ return nScriptsRun(nScriptsRun); }
public G22AutoStarter nScriptsRun(int nScriptsRun) { if (!eq(this.nScriptsRun, nScriptsRun)) { this.nScriptsRun = nScriptsRun; change(); } return this; }
 final public int getNScriptsRun(){ return nScriptsRun(); }
public int nScriptsRun() { return nScriptsRun; }
 int nScriptsRun;

  Flag started = new Flag();
  Flag waited = new Flag();
  Flag done = new Flag();
  Flag ctrlPressed = new Flag();
  List<AutoStartScript> scripts = syncL();
  AutoCloseable ctrlListener;

  class AutoStartScript extends RunResultWithTimestamps implements IFieldsToList{
  G22LeftArrowScript script;
  AutoStartScript() {}
  AutoStartScript(G22LeftArrowScript script) {
  this.script = script;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + script + ")"; }public Object[] _fieldsToList() { return new Object[] {script}; }

    public void run() { try {
      LASCompileResult cr = script.compileForAutoRun();
      if (cr == null) cr = script.compileSaved();
      var _cr_2 = cr; 

      run(() -> {
        G22AutoStarter.this.change();
        if (_cr_2 == null) throw fail("Script is not saved: " + script);
        var parsed = _cr_2.parsedScriptMandatory();
        return parsed.get();
      });
      nScriptsRun(nScriptsRun+1);
    } catch (Exception __e) { throw rethrow(__e); } }
  }
  
  void init() {
    if (ctrlListener != null) return;
    
    scripts.clear();
    
    var scripts = conceptsWhere(g22utils.concepts, G22LeftArrowScript.class, "runOnProjectOpen" , true);
    for (var script : scripts)
      this.scripts.add(new AutoStartScript(script));
    change();
    
    ctrlListener = tempAddGlobalCtrlKeyListener(b -> {
      if (b) {
        ctrlPressed.raise();
        cancel();
      }
    });
  }
  
  void start() {
    print("G22AutoStarter: Start");
    if (enabled && started.raise()) { startThread(new Runnable() {  public void run() { try {  try {
      change();
       AutoCloseable __1 = g22utils.backgroundProcessesUI.tempAdd("Auto-Start Scripts"); try {
      print("G22AutoStarter: Sleeping");
      while (initialDelay > 0) {
        sleepSeconds(1);
        initialDelay(initialDelay-1);
      }
      waited.raise();
      change();
      print("G22AutoStarter: Slept");
      
      for (var script : scripts) { try {
        if (!enabled) break;
        script.run();
      } catch (Throwable __e) { printStackTrace(__e); }}
      
      done.raise();
      change();
      close();
    } finally { _close(__1); }} catch (Throwable __e) { printStackTrace(__e); }
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "try {\r\n      change();\r\n       AutoCloseable __1 = g22utils.backgroundProcess..."; }}); }
    print("G22AutoStarter: Started");
  }
  
  void cancel() { setEnabled(false); }
  
  String status() {
    return !enabled ?
      ctrlPressed.get()
        ? "Cancelled due to ctrl key"
        : "Cancelled"
      : done.get() ? "Done"
      : waited.get() ? "Started" 
      : started.get() ? "Pre-start wait (" + formatDouble1(initialDelay) + "s)"
      : "Not started";
  }
  
  String stats() {
    return status() + ". " + scriptsRunStats();
  }
  
  String scriptsRunStats() {
    return "Scripts run: " + nScriptsRun() + "/" + l(scripts);
  }
  
  public void close() { try { { cleanUp(ctrlListener); ctrlListener = null; } } catch (Exception __e) { throw rethrow(__e); } }
  
  boolean done() { return done.get(); }
}
/* e.g.

  overlay <- ScreenOverlay
  bounds <- rightScreenBounds
  overlay bounds bounds
  overlay show

Can separate commands with ";" also.
For how to define functions in a script see #1033988.

Note: LeftArrowScriptAutoCompleter uses a lot of this class's internals

*/

static class GazelleV_LeftArrowScriptParser extends SimpleLeftToRightParser {
  
  
  
  
  
   final public GazelleV_LeftArrowScriptParser setG22utils(G22Utils g22utils){ return g22utils(g22utils); }
public GazelleV_LeftArrowScriptParser g22utils(G22Utils g22utils) { this.g22utils = g22utils; return this; }  final public G22Utils getG22utils(){ return g22utils(); }
public G22Utils g22utils() { return g22utils; }
 G22Utils g22utils = new G22Utils();
  List functionContainers = new ArrayList();
  
  LinkedHashMap<String, LASValueDescriptor> knownVars = new LinkedHashMap();
  BuildingScript currentReturnableScript;
  boolean inParens = false;
  
  
  
  
  // object can be a class
  static class MethodOnObject implements IFieldsToList{
  static final String _fieldOrder = "object method";
  Object object;
  String method;
  MethodOnObject() {}
  MethodOnObject(Object object, String method) {
  this.method = method;
  this.object = object;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + object + ", " + method + ")"; }

public boolean equals(Object o) {
if (!(o instanceof MethodOnObject)) return false;
    MethodOnObject __3 =  (MethodOnObject) o;
    return eq(object, __3.object) && eq(method, __3.method);
}

  public int hashCode() {
    int h = 791808543;
    h = boostHashCombine(h, _hashCode(object));
    h = boostHashCombine(h, _hashCode(method));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {object, method}; }
}
  
  class BuildingScript {
    boolean returnable = false;
    GazelleV_LeftArrowScript.Script script = new GazelleV_LeftArrowScript.Script();
    List<GazelleV_LeftArrowScript.Evaluable> steps = new ArrayList();
    Map<String, GazelleV_LeftArrowScript.FunctionDef> functionDefs = new HashMap();
    
    BuildingScript(boolean returnable) {
  this.returnable = returnable;}
    
    void add(GazelleV_LeftArrowScript.Evaluable step) { if (step != null) steps.add(step); }

    GazelleV_LeftArrowScript.Evaluable get() {
      // if the last command is a return from THIS script,
      // convert into a simple expression
      
      var lastStep = last(steps);
      if (lastStep instanceof GazelleV_LeftArrowScript.ReturnFromScript)
        if (((GazelleV_LeftArrowScript.ReturnFromScript) lastStep).script == script)
          replaceLast(steps, ((GazelleV_LeftArrowScript.ReturnFromScript) lastStep).value);
        
      // if the script is only step, is not returnable
      // and defines no functions, replace it with its only step
      
      if (!returnable && l(steps) == 1 && empty(functionDefs))
        return first(steps);
        
      // make and return actual script
      
      script.functionDefs = functionDefs;
      script.steps = toTypedArray(GazelleV_LeftArrowScript.Evaluable.class, steps);
      return script;
    }
    
    String toStringLong() { return pnlToLines(steps); }
    public String toString() { return formatRecordVars("BuildingScript", "script", script, "returnable", returnable); }
  }
  
  GazelleV_LeftArrowScript.Script parse(String text) {
    setText(text);
    return (GazelleV_LeftArrowScript.Script) parseScript(true);
  }
  
  // if returnable is true, it's always a Script
  GazelleV_LeftArrowScript.Evaluable parseScript(boolean returnable) {
    BuildingScript script = new BuildingScript(returnable);
    var lastReturnableScript = currentReturnableScript;
    if (returnable) currentReturnableScript = script;
    try {
      parseScript_2(script);
      var builtScript = script.get();
      currentReturnableScript = lastReturnableScript;
      return builtScript;
    } catch (Throwable e) {
      if (scaffoldingEnabled()) print("Parsed so far:\n" + script);
      
      throw rethrowAndAppendToMessage(e, squareBracketed(str(lineAndColumn())));
    }
  }
  
  void parseScript_2(BuildingScript script) {
    while (mainLoop()) {
      if (is(";")) { next(); continue; }
      if (is("}")) break;
      
      if (is("def"))
        { parseFunctionDefinition(); continue; }
        
      if (is("param")) {
        consume();
        String var = assertIdentifier(tpp());
        knownVars.put(var, new LASValueDescriptor());
        continue;
      }
        
      if (is("return")) {
        consume();
        var expr = parseAssignmentOrExpr();
        { script.add(new GazelleV_LeftArrowScript.ReturnFromScript(currentReturnableScript.script, expr)); continue; }
      }

      script.add(parseAssignmentOrExpr());
    }
  }
  
  GazelleV_LeftArrowScript.Evaluable parseAssignmentOrExpr() {
    String t = token();
    if (scaffoldingEnabled()) print("next tokens: " + quote(token(1)) + " " + quote(token(2)));
    if (isIdentifier(t) && eq(token(1), "<") && eq(token(2), "-")) {
      if (scaffoldingEnabled()) print("Found assignment");
      next(3);
      knownVars.put(t, new LASValueDescriptor());
      return new GazelleV_LeftArrowScript.Assignment(t, parseExpr());
    } else
      return parseExpr();
  }
  
  GazelleV_LeftArrowScript.Evaluable parseOptionalInnerExpression() {
    if (scaffoldingEnabled()) printVars("parseOptionalInnerExpression", "token", token());
    if (atCmdEnd() || is("{")) return null;
    return parseInnerExpr();
  }
  
  GazelleV_LeftArrowScript.Evaluable _const(Object o) { return new GazelleV_LeftArrowScript.Const(o); }
  
  GazelleV_LeftArrowScript.Evaluable parseInnerExpr() { return parseExpr(true); }
  
  GazelleV_LeftArrowScript.Evaluable parseExpr() { return parseExpr(false); }
GazelleV_LeftArrowScript.Evaluable parseExpr(boolean inner) {
    if (atEnd()) return null;
    
    String t = token();
    if (scaffoldingEnabled()) printVars("parseExpr", "token" , t);
    if (is(";")) return null; // empty command
    
    // int or double literal
    if (is("-") && empty(nextSpace()) && startsWithDigit(token(1))
      || startsWithDigit(t)) {
      t = consumeMultiTokenLiteral();
      if (isInteger(t)) return _const(parseInt(t));
      if (endsWith(t, "f")) return _const(parseFloat(t));
      return _const(parseDouble(t));
    }

    if (isQuoted(t)) {
      consume();
      return _const(unquote(t));
    }
      
    if (isIdentifier(t)) {
      if (is("while"))
        return parseWhileLoop();
  
      if (is("for"))
        return parseForEach();
  
      if (is("if"))
        return parseIfStatement();
        
      consume();
      if (scaffoldingEnabled()) print("Consumed identifier " + t + ", next token: " + token() + ", inner: " + inner);
      return parseExprStartingWithIdentifier(t, inner);
    }
    
    // nested expression
    
    if (eq(t, "(")) {
      boolean inParensOld = inParens;
      inParens = true;
      consume();
      //print("Consumed opening parens (level " + parenLevels + ")");
      var e = parseExpr();
      //print("Consuming closing parens for expr: " + e + " (closing paren level " + parenLevels + ")");
      consume(")");
      inParens = inParensOld;
      
      return inner ? e : parseCall(e);
    }
      
    throw fail("Identifier, literal or opening parentheses expected (got: " + quote(t));
  }
  
  // t is last consumed token (the identifier the expression starts with)
  GazelleV_LeftArrowScript.Evaluable parseExprStartingWithIdentifier(String t, boolean inner) {
    if (eq(t, "true")) return _const(true);
    if (eq(t, "false")) return _const(false);
    if (eq(t, "null")) return _const(null);
    
    if (eq(t, "new")) {
      String className = assertIdentifier(tpp());
      Object o = findExternalObject(className);
      if (o instanceof Class)
        return new GazelleV_LeftArrowScript.NewObject(((Class) o), parseArguments());
      throw fail("Class not found: " + className);
    }

    var type = knownVars.get(t);
    if (type != null) {
      var e = new GazelleV_LeftArrowScript.GetVar(t).returnType(type);
      if (scaffoldingEnabled()) print("Found var acccess: " + e + ", " + (!inner ? "Checking for call" : "Returning expression"));
      return inner ? e : parseCall(e);
    }
    
    if (!inner) {
      var fdef = currentReturnableScript.functionDefs.get(t);
      if (fdef != null)
        return new GazelleV_LeftArrowScript.CallFunction(fdef, parseArguments());
    }
    
    if (eq(t, "_context"))
      return new GazelleV_LeftArrowScript.GetVarContext();
      
    Object o = findExternalObject(t);
    if (o == null)
      throw fail("Unknown object: " + t);
    else if (inner)
      return _const(o);
    else if (o instanceof Class) {
      /*if (atCmdEnd())
        ret new Toolbox.NewObject(o);*/
        
      /* old object creation syntax (e.g. Pair new a b)
      if (is("new")) {
        next();
        ret new Toolbox.NewObject(o, parseArguments());
      } else*/ if (isIdentifier()) {
        String name = tpp();
        
        // look for method first
        
        if (hasMethodNamed(((Class) o), name))
          return new GazelleV_LeftArrowScript.CallMethod(_const((Class) o), name, parseArguments());
          
        // look for field second
        
        var field = getField(((Class) o), name);
        if (field != null) {
          assertCmdEnd();
          return new GazelleV_LeftArrowScript.GetStaticField(field);
        }
        
        throw fail(name + " not found in " + ((Class) o) + " (looked for method or field)");
      } else
        throw fail("Method name expected: " + token());
    } else if (o instanceof MethodOnObject) {
      if (inner) throw fail("Can't call methods in arguments");
      return new GazelleV_LeftArrowScript.CallMethod(_const(((MethodOnObject) o).object), ((MethodOnObject) o).method, parseArguments());
    } else
      return parseCall(_const(o));
  }
  
  List<GazelleV_LeftArrowScript.Evaluable> parseArguments() {
    //ret collectWhileNotNull(-> parseOptionalInnerExpression());
    
    List<GazelleV_LeftArrowScript.Evaluable> l = new ArrayList();
    try {
      while (true) {
        GazelleV_LeftArrowScript.Evaluable a = parseOptionalInnerExpression();
        if (a == null) break;
        l.add(a);
      }
      return l;
    } catch (Throwable _e) {
      if (scaffoldingEnabled()) print("Arguments parsed so far: " + l);
    
throw rethrow(_e); }
  }
  
  String consumeMultiTokenLiteral() {
    return consumeUntilSpaceOr(() -> atCmdEnd());
  }
  
  boolean atCmdEnd() {
    return
      !inParens && atEndOrLineBreak()
      || is(";") || is("}") || is(")");
  }
  
  void assertCmdEnd() { if (!atCmdEnd()) throw fail("Expected end of command"); }
  
  GazelleV_LeftArrowScript.Evaluable parseCall(GazelleV_LeftArrowScript.Evaluable target) {
    if (atCmdEnd() || !isIdentifier()) return target;
    
    var start = ptr();
    String methodName = tpp();
    var args = parseArguments();
    
    if (nempty(args))
      return new GazelleV_LeftArrowScript.CallMethod(target, methodName, args);
    else
      return src(start, new GazelleV_LeftArrowScript.CallMethodOrGetField(target, methodName));
  }
  
  <A> A src(ListAndIndex<String> start, A a) {
    if (a instanceof IHasTokenRangeWithSrc)
      ((IHasTokenRangeWithSrc) a).setTokenRangeWithSrc(new TokenRangeWithSrc(start, ptr()));
    return a;
  }
  
  // can return MethodOnObject
  transient  IF1<String, Object> findExternalObject;
Object findExternalObject(String name) { return findExternalObject != null ? findExternalObject.get(name) : findExternalObject_base(name); }
final Object findExternalObject_fallback(IF1<String, Object> _f, String name) { return _f != null ? _f.get(name) : findExternalObject_base(name); }
Object findExternalObject_base(String name) { try {
    //try object findClassThroughDefaultClassFinder(name);
    //try object findClassInStandardImports(name);
      
    String fullName = globalClassNames().get(name);
    if (fullName != null)
      return Class.forName(fullName);
      
    for (var container : unnullForIteration(functionContainers))
      if (hasMethodNamed(container, name))
        return new MethodOnObject(container, name);
    return null;
  } catch (Exception __e) { throw rethrow(__e); } }
  
  GazelleV_LeftArrowScriptParser allowTheWorld() { return allowTheWorld(mc()); }
  
  GazelleV_LeftArrowScriptParser allowTheWorld(Object... functionContainers) {
    for (Object o : unnullForIteration(reversed(functionContainers)))
      if (!contains(this.functionContainers, o)) {
        this.functionContainers.add(0, o);
        globalClassNames_cache = null; // recalculate
      }
    return this;
  }
  
  void printFunctionDefs(GazelleV_LeftArrowScript.Script script) {
    if (scaffoldingEnabled()) print(values(script.functionDefs));
  }
  
  void parseFunctionDefinition() {
    consume("def");
    String functionName = assertIdentifier(tpp());
    List<String> args = new ArrayList();
    while (isIdentifier())
      args.add(tpp());
     AutoCloseable __1 = tempMapPutAll(knownVars, mapWithSingleValue(args, new LASValueDescriptor())); try {
    var functionBody = parseCurlyBlock(true);
    
    currentReturnableScript.functionDefs.put(functionName,
      new GazelleV_LeftArrowScript.FunctionDef(functionName, args, functionBody));
  } finally { _close(__1); }}
  
  GazelleV_LeftArrowScript.Evaluable parseCurlyBlock(boolean returnable) {
    //print(+knownVars);
    consume("{");
    boolean inParensOld = inParens;
    inParens = false;
    var script = parseScript(returnable);
    consume("}");
    inParens = inParensOld;
    return script;
  }
  
  GazelleV_LeftArrowScript.Evaluable parseWhileLoop() {
    consume("while");
    var condition = parseExpr();
    var body = parseCurlyBlock(false);
    return new GazelleV_LeftArrowScript.While(condition, body);
  }
  
  GazelleV_LeftArrowScript.Evaluable parseForEach() {
    consume("for");
    String var = assertIdentifier(tpp());
    if (scaffoldingEnabled()) print("for var", var);
    consume("in");
    var collection = parseExpr();
    if (scaffoldingEnabled()) print("collection", collection);
     AutoCloseable __2 = tempMapPut(knownVars, var, new LASValueDescriptor()); try {
    var body = parseCurlyBlock(false);
    return new GazelleV_LeftArrowScript.ForEach(collection, var, body);
  } finally { _close(__2); }}
  
  GazelleV_LeftArrowScript.Evaluable parseIfStatement() {
    consume("if");
    var condition = parseExpr();
    var body = parseCurlyBlock(false);
    GazelleV_LeftArrowScript.Evaluable elseBranch = null;
    if (is("else")) {
      consume();
      if (is("if"))
        elseBranch = parseIfStatement();
      else
        elseBranch = parseCurlyBlock(false);
    }
    return new GazelleV_LeftArrowScript.IfThen(condition, body, elseBranch);
  }
  
  // declare an external variable with optional type info
  void addVar(String var) { addVar(var, new LASValueDescriptor()); }
void addVar(String var, LASValueDescriptor type) { knownVars.put(var, type); }
  void addVar(String var, Class type, boolean canBeNull) { addVar(var, new LASValueDescriptor.NonExact(type, canBeNull)); }
  
  // short name to full name
   Map<String, String> globalClassNames_cache;
 Map<String, String> globalClassNames() { if (globalClassNames_cache == null) globalClassNames_cache = globalClassNames_load(); return globalClassNames_cache;}

 Map<String, String> globalClassNames_load() {
    var packages = mapToTreeSet(importedPackages(), pkg -> pkg + ".");
    
    // add inner classes of function containers
    var classContainers = classContainerPrefixes();
    
    Map<String, String> out = new HashMap();
    for (var className : g22utils.classNameResolver().allFullyQualifiedClassNames()) {
      if (isAnonymousClassName(className)) continue;
      if (!contains(className, '$')) {
        String pkg = longestPrefixInTreeSet(className, packages);
        if (pkg != null) {
          String shortName = dropPrefix(pkg, className);
          if (!shortName.contains("."))
            out.put(shortName, className);
        }
      }
        
      String container = longestPrefixInTreeSet(className, classContainers);
      if (container != null)
        out.put(dropPrefix(container, className), className);
    }
        
    return out;
  }
  
  transient  IF0<Collection<String>> importedPackages;
Collection<String> importedPackages() { return importedPackages != null ? importedPackages.get() : importedPackages_base(); }
final Collection<String> importedPackages_fallback(IF0<Collection<String>> _f) { return _f != null ? _f.get() : importedPackages_base(); }
Collection<String> importedPackages_base() {
    return itemPlus("java.lang", standardImports_fullyImportedPackages());
  }
  
  TreeSet<String> classContainerPrefixes() {
    return mapToTreeSet(functionContainers, fc -> className(fc) + "$");
  }
}

static class LetterLayout implements LayoutManager {
  private String[] lines;
  private Map<String, Component> map = new TreeMap();
  private Map<Component, String> constraints = new HashMap();
  private RC[] rows;
  private RC[] cols;
  private Cell[][] cells;
  private int spacingX = 10, spacingY = 10;
  private int insetTop, insetBottom, insetLeft, insetRight;
  private int template;
  private boolean formWideLeftSide, formWideRightSide;

  private static final int STALACTITE = 1, LEFT_ALIGNED_ROW = 2, CENTERED_ROW = 3, FORM = 4, RIGHT_ALIGNED_ROW = 5;
  private boolean debug = false;

  public void setLeftBorder(int border) {
    insetLeft = border;
  }

  public void setRightBorder(int border) {
    insetRight = border;
  }

  public static JComponent withBorder(JComponent component, int border) {
    JPanel panel = new JPanel(new LetterLayout("C").setBorder(border));
    panel.add("C", component);
    return panel;
  }

  public static JPanel panel(String... lines) {
    return new JPanel(new LetterLayout(lines));
  }

  public static JPanel stalactitePanel() {
    return new JPanel(stalactite());
  }

  static class DummyComponent extends JComponent {
  }

  /**
   * info about one matrix cell
   */
  static class Cell {
    boolean aux = false; // part of a larger cell, but not top-left corner
    int minWidth, minHeight;
    Component component;
    int colspan, rowspan;
    double weightX, weightY;
  }

  /**
   * info about one matrix row / column
   */
  static class RC {
    int min;
    double weightSum;
    int start;
    int minEnd;
  }

  private LetterLayout(int template) {
    this.template = template;
  }

  public LetterLayout(String... lines) {
    this.lines = lines;
  }

  public void layoutContainer(Container container) {
    prepareLayout(container);

    // do layout

    if (debug)
      System.out.println("Container size: " + container.getSize());

    Insets insets = getInsets(container);
    for (int r = 0; r < rows.length; r++) {
      for (int i = 0; i < cols.length;) {
        Cell cell = cells[i][r];
        if (cell.aux)
          ++i;
        else {
          if (cell.component != null) {
            int x1 = cols[i].start;
            int y1 = rows[r].start;
            int x2 = i + cell.colspan < cols.length ? cols[i + cell.colspan].start - spacingX : container.getWidth() - insets.right;
            int y2 = r + cell.rowspan < rows.length ? rows[r + cell.rowspan].start - spacingY : container.getHeight() - insets.bottom;

            if (debug)
              System.out.println("Layouting ("+i+", "+r+", " + cell.component.getClass().getName() + "): "+x1+" "+y1+" "+x2+" "+y2);

            cell.component.setBounds(x1, y1, x2 - x1, y2 - y1);
          }
          i += cells[i][r].colspan;
        }
      }
    }
  }

  private void prepareLayout(Container container) {
    applyTemplate(container);

    int numRows = lines.length, numCols = lines[0].length();
    for (int i = 1; i < numRows; i++) if (lines[i].length() != numCols)
      throw new IllegalArgumentException("Lines have varying length");
    cells = new Cell[numCols][numRows];
    rows = new RC[numRows];
    cols = new RC[numCols];

    for (int r = 0; r < numRows; r++) rows[r] = new RC();
    for (int i = 0; i < numCols; i++) cols[i] = new RC();
    for (int r = 0; r < numRows; r++) for (int i = 0; i < numCols; i++) cells[i][r] = new Cell();

    // define cells

    for (int r = 0; r < numRows; r++) {
      String line = lines[r];
      for (int i = 0; i < numCols;) {
        Cell cell = cells[i][r];
        if (cell.aux) {
          ++i;
          continue;
        }
        char ch = line.charAt(i);
        int iNext = i;
        do ++iNext; while (iNext < numCols && ch == line.charAt(iNext));
        int rNext = r;
        do ++rNext; while (rNext < numRows && ch == lines[rNext].charAt(i));

        cell.weightX = numCols == 1 || iNext > i + 1 ? 1.0 : 0.0;
        cell.weightY = numRows == 1 || rNext > r + 1 ? 1.0 : 0.0;

        Component c = map.get(String.valueOf(ch));
        cell.component = c;
        if (c != null) {
          cell.minWidth = c.getMinimumSize().width + spacingX;
          cell.minHeight = getMinimumHeight(c) + spacingY;
        }
        cell.colspan = iNext - i;
        cell.rowspan = rNext - r;

        if (cell.colspan == 1)
          cols[i].min = Math.max(cols[i].min, cell.minWidth);
        if (cell.rowspan == 1)
          rows[r].min = Math.max(rows[r].min, cell.minHeight);

        for (int r2 = r; r2 < rNext; r2++)
          for (int i2 = i; i2 < iNext; i2++)
            if (r2 != r || i2 != i)
              cells[i2][r2].aux = true;

        i = iNext;
      }
    }

    // determine minStarts, weightSums

    while (true) {
      for (int i = 0; i < numCols; i++) {
        int minStart = i == 0 ? 0 : cols[i - 1].minEnd;
        double weightStart = i == 0 ? 0.0 : cols[i - 1].weightSum;
        for (int r = 0; r < numRows; r++) {
          Cell cell = cells[i][r];
          if (!cell.aux) {
            RC rc = cols[i + cell.colspan - 1];
            rc.minEnd = Math.max(rc.minEnd, minStart + cell.minWidth);
            rc.weightSum = Math.max(rc.weightSum, weightStart + cell.weightX);
          }
        }
      }

      for (int r = 0; r < numRows; r++) {
        int minStart = r == 0 ? 0 : rows[r - 1].minEnd;
        double weightStart = r == 0 ? 0.0 : rows[r - 1].weightSum;
        for (int i = 0; i < numCols; i++) {
          Cell cell = cells[i][r];
          if (!cell.aux) {
            RC rc = rows[r + cell.rowspan - 1];
            rc.minEnd = Math.max(rc.minEnd, minStart + cell.minHeight);
            rc.weightSum = Math.max(rc.weightSum, weightStart + cell.weightY);
          }
        }
      }

      if (allWeightsZero(cols)) {
        for (int r = 0; r < numRows; r++)
          for (int i = 0; i < numCols; i++)
            cells[i][r].weightX = 1.0;
        continue;
      }

      if (allWeightsZero(rows)) {
        for (int r = 0; r < numRows; r++)
          for (int i = 0; i < numCols; i++)
            cells[i][r].weightY = 1.0;
        continue;
      }

      break;
    }

    // determine row, col starts

    Insets insets = getInsets(container);
    determineStarts(cols, insets.left, container.getWidth() - insets.left - insets.right + spacingX, spacingX);
    determineStarts(rows, insets.top, container.getHeight() - insets.top - insets.bottom + spacingY, spacingY);
  }

  private boolean allWeightsZero(RC[] rcs) {
    for (int i = 0; i < rcs.length; i++)
      if (rcs[i].weightSum != 0.0)
        return false;
    return true;
  }

  private static int getMinimumHeight(Component c) {
    /*if (c instanceof JTextArea) {
      return (int) ((JTextArea) c).getUI().getRootView((JTextArea) c).getPreferredSpan(javax.swing.text.View.Y_AXIS);
    }*/
    return c.getMinimumSize().height;
  }

  private void applyTemplate(Container container) {
    if (template == STALACTITE) {
      Component[] components = container.getComponents();

      lines = new String[components.length + 2];
      map.clear();
      for (int i = 0; i < components.length; i++) {
        String s = String.valueOf(makeIndexChar(i));
        map.put(s, components[i]);
        lines[i] = s;
      }
      lines[components.length] = lines[components.length + 1] = " ";
    } else if (template == FORM) {
      /* old method of calculating numRows:
      int numRows = 0;
      for (String key : map.keySet()) {
        if (key.length() == 1)
          numRows = Math.max(numRows, Character.toLowerCase(key.charAt(0))-'a');
      }*/
      Component[] components = container.getComponents();
      int numRows = components.length/2;

      lines = new String[numRows+2];
      map.clear();
      for (int row = 0; row < numRows; row++) {
        String lower = String.valueOf(makeIndexChar(row));
        String upper = String.valueOf(makeAlternateIndexChar(row));
        Component rightComponent = components[row * 2 + 1];
        if (rightComponent instanceof DummyComponent)
          upper = lower;
        lines[row] = (formWideLeftSide ? lower + lower : lower) + (formWideRightSide ? upper + upper : upper);
        map.put(lower, components[row*2]);
        if (!(rightComponent instanceof DummyComponent))
          map.put(upper, rightComponent);
      }
      lines[numRows] = lines[numRows+1] = (formWideLeftSide ? "  " : " ") + (formWideRightSide ? "  " : " ");
    } else if (template == LEFT_ALIGNED_ROW) {
      lines = new String[] { makeSingleRow(container) + RIGHT_CHAR + RIGHT_CHAR };
    } else if (template == CENTERED_ROW) {
      // LEFT_CHAR + LEFT_CHAR is a 2-character repeating sequence
      // which indicates a wide component. So you can theoretically
      // use LetterLayout.LEFT_CHAR as your "main" component's...
      // but it doesn't seem to work. Just use centerAndEast etc
      lines = new String[] { "" + LEFT_CHAR + LEFT_CHAR + makeSingleRow(container) + RIGHT_CHAR + RIGHT_CHAR };
    } else if (template == RIGHT_ALIGNED_ROW) {
      lines = new String[] { "" + LEFT_CHAR + LEFT_CHAR + makeSingleRow(container) };
    }
  }

  private String makeSingleRow(Container container) {
    Component[] components = container.getComponents();
    StringBuffer buf = new StringBuffer();
    map.clear();
    for (int i = 0; i < components.length; i++) {
      var c = components[i];
      String s = constraints.get(c);
      if (isOneOfSingleChars(s, LEFT_CHAR, RIGHT_CHAR)) continue;
      s = str(makeAlternateIndexChar(i));
      setConstraints(c, s);
      buf.append(s);
    }
    return buf.toString();
  }

  private static void determineStarts(RC[] rcs, int start, int totalSize, int spacing) {
    int minTotal = rcs[rcs.length - 1].minEnd;
    double weightSum = rcs[rcs.length - 1].weightSum;
    //System.out.println("totalSize="+totalSize+",minTotal="+minTotal+",weightSum="+weightSum);
    int spare = (int) ((totalSize - minTotal) / (weightSum == 0.0 ? 1.0 : weightSum));
    int x = start, minSum = 0;
    double prevWeightSum = 0.0;
    for (int i = 0; i < rcs.length; i++) {
      int width = rcs[i].minEnd - minSum + (int) ((rcs[i].weightSum - prevWeightSum) * spare) - spacing;
      //System.out.println("i="+i+",prevws="+prevWeightSum+",ws="+rcs[i].weightSum+",min="+rcs[i].min+",width="+width);
      rcs[i].start = x;
      x += width + spacing;
      prevWeightSum = rcs[i].weightSum;
      minSum = rcs[i].minEnd;
    }
  }

  public void addLayoutComponent(String s, Component component) {
    setConstraints(component, s);
  }

  void setConstraints(Component component, String s) {
    mapPutOrRemove(map, s, component);
    mapPutOrRemove(constraints, component, s);
  }

  public void removeLayoutComponent(Component component) {
    map.values().remove(component);
    constraints.remove(component);
  }

  public Dimension minimumLayoutSize(Container container) {
    prepareLayout(container);
    Insets insets = getInsets(container);
    Dimension result = new Dimension(
      insets.left + cols[cols.length - 1].minEnd + insets.right - spacingX,
      insets.top + rows[rows.length - 1].minEnd + insets.bottom - spacingY);
    return result;
  }

  private Insets getInsets(Container container) {
    Insets insets = container.getInsets();
    return new Insets(insets.top + insetTop,
      insets.left + insetLeft,
      insets.bottom + insetBottom,
      insets.right + insetRight);
  }

  public Dimension preferredLayoutSize(Container container) {
    return minimumLayoutSize(container);
  }

  public LetterLayout setSpacing(int x, int y) {
    spacingX = x;
    spacingY = y;
    return this;
  }

  public LetterLayout setSpacing(int spacing) {
    return setSpacing(spacing, spacing);
  }

  public LetterLayout setBorder(int top, int left, int bottom, int right) {
    insetTop = top;
    insetLeft = left;
    insetBottom = bottom;
    insetRight = right;
    return this;
  }

  public LetterLayout setBorder(int inset) {
    return setBorder(inset, inset, inset, inset);
  }

  public LetterLayout setTopBorder(int inset) {
    insetTop = inset;
    return this;
  }

  /**
   * layout components from top to bottom; add components without letters!
   */
  public static LetterLayout stalactite() {
    return new LetterLayout(STALACTITE);
  }

  /**
   * layout components from left to right; add components without letters!
   */
  public static LetterLayout leftAlignedRow() {
    return new LetterLayout(LEFT_ALIGNED_ROW);
  }

  public static LetterLayout leftAlignedRow(int spacing) {
    return leftAlignedRow().setSpacing(spacing);
  }

  /**
   * layout components from left to right, center in container; add components without letters!
   */
  public static LetterLayout centeredRow() {
    return new LetterLayout(CENTERED_ROW);
  }

  public static LetterLayout rightAlignedRow() {
    return new LetterLayout(RIGHT_ALIGNED_ROW);
  }

  public static JPanel rightAlignedRowPanel(JComponent... components) {
    return makePanel(new LetterLayout(RIGHT_ALIGNED_ROW), components);
  }

  private static JPanel makePanel(LetterLayout letterLayout, JComponent[] components) {
    JPanel panel = new JPanel(letterLayout);
    for (JComponent component : components) {
      panel.add(component);
    }
    return panel;
  }

  /**
   * layout components from top to bottom; two components per row
   */
  public static LetterLayout form() {
    LetterLayout letterLayout = new LetterLayout(FORM);
    letterLayout.formWideLeftSide = true;
    letterLayout.formWideRightSide = true;
    return letterLayout;
  }

  /**
   * layout components from top to bottom; two components per row
   * left column is small, right column is wide
   */
  public static LetterLayout formWideRightSide() {
    LetterLayout letterLayout = new LetterLayout(FORM);
    letterLayout.formWideRightSide = true;
    return letterLayout;
  }

  public static Component getDummyComponent() {
    return new DummyComponent();
  }

  public static JPanel newPanel(String... lines) {
    return new JPanel(new LetterLayout(lines));
  }

  public boolean isDebug() {
    return debug;
  }

  public void setDebug(boolean debug) {
    this.debug = debug;
  }

  public static char makeIndexChar(int idx) {
    return (char) ('a' + idx*2);
  }

  public static char makeAlternateIndexChar(int idx) {
    return (char) ('b' + idx*2);
  }

  public static char LEFT_CHAR = ',', RIGHT_CHAR = '.';

  public static void main(String[] args) {
    System.out.println((int) makeIndexChar(0));
    System.out.println((int) makeAlternateIndexChar(0));
    System.out.println((int) makeIndexChar(32000));
    System.out.println((int) makeAlternateIndexChar(32000));
    System.out.println((int) LEFT_CHAR);
    System.out.println((int) RIGHT_CHAR);
  }
}

static class G22LeftArrowScript extends ConceptWithChangeListeners {
  static final String _fieldOrder = "description text clearedForAutoRun editingText runOnProjectOpen runCount lastResultByMode compileResultForAutoRun compileResultForSaved";
  public transient FieldVar<String> varDescription_cache;
public FieldVar<String> varDescription() { if (varDescription_cache == null) varDescription_cache = varDescription_load(); return varDescription_cache;}

public FieldVar<String> varDescription_load() {
        return new FieldVar<String>(this, "description", () -> description(), description -> description(description)); }
 final public G22LeftArrowScript setDescription(String description){ return description(description); }
public G22LeftArrowScript description(String description) { if (!eq(this.description, description)) { this.description = description; change(); } return this; }
 final public String getDescription(){ return description(); }
public String description() { return description; }
 String description;
  
  // the script (last "saved" version)
  public transient FieldVar<String> varText_cache;
public FieldVar<String> varText() { if (varText_cache == null) varText_cache = varText_load(); return varText_cache;}

public FieldVar<String> varText_load() {
        return new FieldVar<String>(this, "text", () -> text(), text -> text(text)); }
 final public G22LeftArrowScript setText(String text){ return text(text); }
public G22LeftArrowScript text(String text) { if (!eq(this.text, text)) { this.text = text; change(); } return this; }
 final public String getText(){ return text(); }
public String text() { return text; }
 String text;
  
  // the latest code that has been cleared to auto-run (if any)
  public transient FieldVar<ClearForAutoRun<String>> varClearedForAutoRun_cache;
public FieldVar<ClearForAutoRun<String>> varClearedForAutoRun() { if (varClearedForAutoRun_cache == null) varClearedForAutoRun_cache = varClearedForAutoRun_load(); return varClearedForAutoRun_cache;}

public FieldVar<ClearForAutoRun<String>> varClearedForAutoRun_load() {
        return new FieldVar<ClearForAutoRun<String>>(this, "clearedForAutoRun", () -> clearedForAutoRun(), clearedForAutoRun -> clearedForAutoRun(clearedForAutoRun)); }
 final public G22LeftArrowScript setClearedForAutoRun(ClearForAutoRun<String> clearedForAutoRun){ return clearedForAutoRun(clearedForAutoRun); }
public G22LeftArrowScript clearedForAutoRun(ClearForAutoRun<String> clearedForAutoRun) { if (!eq(this.clearedForAutoRun, clearedForAutoRun)) { this.clearedForAutoRun = clearedForAutoRun; change(); } return this; }
 final public ClearForAutoRun<String> getClearedForAutoRun(){ return clearedForAutoRun(); }
public ClearForAutoRun<String> clearedForAutoRun() { return clearedForAutoRun; }
 ClearForAutoRun<String> clearedForAutoRun;

  // text currently being modified in an editor (or null)
  // Note that "" doesn't count as null.
  public transient FieldVar<String> varEditingText_cache;
public FieldVar<String> varEditingText() { if (varEditingText_cache == null) varEditingText_cache = varEditingText_load(); return varEditingText_cache;}

public FieldVar<String> varEditingText_load() {
        return new FieldVar<String>(this, "editingText", () -> editingText(), editingText -> editingText(editingText)); }
 final public G22LeftArrowScript setEditingText(String editingText){ return editingText(editingText); }
public G22LeftArrowScript editingText(String editingText) { if (!eq(this.editingText, editingText)) { this.editingText = editingText; change(); } return this; }
 final public String getEditingText(){ return editingText(); }
public String editingText() { return editingText; }
 String editingText;
  
  // auto-run when project is opened
  boolean runOnProjectOpen = false;
  
  // run stats
  long runCount;
  
  // info about last result (or error) of script run by mode
  Map<G22ScriptMode, WithTimestamp<PersistableOKOrError<G22TypeDesc>>> lastResultByMode;
  
  @Override void _doneLoading2() {
    //print("_doneLoading " + this);
    cMigrateField(this, "code", "text");
  }
  
  String myType() { return dropPrefix("G22", shortClassName(this)); }
  
  public String toString() { return or2(description, myType() + " " + id); }
  
  void initEditingText() {
    editingText(unnull(or(editingText, text))); 
  }
  
  void receiveEditingText(String text) {
    printVars_shorten("receiveEditingText", "text", text);
    editingText(text);
  }
  
  String stableText() { return text; }
  
  final void save(){ completeEdit(); }
void completeEdit() {
    String t = editingText;
    printVars_shorten("completeEdit", "t", t);
    if (t != null) {
      setTextWithHistory(t);
      printVars_shorten("completeEdit", "editingText", editingText);
    }
  }
  
  void setTextWithHistory(String text) {
    if (eq(this.text, text)) return;
    text(text);
    saveTextToHistory();
  }
  
  String textForEditing() {
    initEditingText();
    return editingText;
  }
  
  File historyFile() {
    return fileInConceptsDir("History/" + shortDynName(this) + id + ".history");
  }
  
  void saveTextToHistory() {
    saveFieldToHistory("text", text);
  }
  
  void saveFieldToHistory(String field, String value) {
    File historyFile = historyFile();
    if (historyFile == null) return;
    String contents = value == null ? " empty" : " (" + nLines(value) + ", " + nChars(value) + "):\n" +
      indentx(value) + "\n" +
      "\n";
    appendToTextFile(historyFile,
      "\n===\n" +
      "Concept ID: " + id + "\n" +
      "Date: " + dateWithMSUTC() + "\n" +
      firstToUpper(field) + contents +
      "===" + "\n");
  }
  
  boolean isSaved() { return text != null; }
  boolean isSavedDistinctFromAutoRunVersion() { return isSaved() && !eq(text, codeForAutoRun()); }
  boolean isEditing() { return editedText() != null; }
  boolean isClearForAutoRun() { return clearedForAutoRun != null; }
  
  String editedText() { return eq(editingText, text) ? null : editingText; }
  
  String codeForAutoRun() { return getVar(clearedForAutoRun()); }
  
  // return code clear for auto run if available
  // otherwise returns saved code if available
  // in all other cases returns null
  String safestCode() { return or(codeForAutoRun(), stableText()); }
  
  void clearForAutoRun() {
    if (!isSaved()) return;
    String text = text();
    saveFieldToHistory("Auto-runnable code", text);
    clearedForAutoRun(new ClearForAutoRun(text));
  }
  
  void forgetAutoRunCode() {
    if (!isClearForAutoRun()) return;
    saveFieldToHistory("Auto-runnable code", null);
    clearedForAutoRun(null);
    compileResultForAutoRun = null;
  }
  
  GazelleV_LeftArrowScriptParser makeParser() { return g22utils(_concepts).leftArrowParser(); }
  
  LASCompileResult newCompileResult() { return new LASCompileResult(); }
  
  transient LASCompileResult compileResultForAutoRun;
  LASCompileResult compileForAutoRun() {
    String code = codeForAutoRun();
    if (code == null) return null;
    var cr = compileResultForAutoRun;
    if (cr != null && eq(cr.script, code))
      return cr;
    cr = newCompileResult();
    cr.script(code);
    var g22utils = assertNotNull("g22utils", g22utils(_concepts()));
    var parser = makeParser();
    cr.parser(parser);
    cr.compile();
    return compileResultForAutoRun = cr;
  }
  
  transient LASCompileResult compileResultForSaved;
  LASCompileResult compileSaved() {
    String code = stableText();
    if (code == null) return null;
    var cr = compileResultForSaved;
    if (cr != null && eq(cr.script, code))
      return cr;
    cr = newCompileResult();
    cr.script(code);
    var g22utils = assertNotNull("g22utils", g22utils(_concepts()));
    var parser = makeParser();
    cr.parser(parser);
    cr.compile();
    return compileResultForSaved = cr;
  }
}
static class RSyntaxTextAreaWithSearch implements SearchListener, Swingable {
  JPanel panel;
   final public RSyntaxTextArea getTextArea(){ return textArea(); }
public RSyntaxTextArea textArea() { return textArea; }
 RSyntaxTextArea textArea;
  CollapsibleSectionPanel csp;
  
  FindDialog findDialog;
  ReplaceDialog replaceDialog;
  
  FindToolBar findToolBar;
  ReplaceToolBar replaceToolBar;
  
  StatusBar statusBar;
  
  RSyntaxTextAreaWithSearch() { init(); }
  RSyntaxTextAreaWithSearch(IF1<JComponent, JComponent> wrapStatusLabel) {
  this.wrapStatusLabel = wrapStatusLabel; init(); }
  RSyntaxTextAreaWithSearch(String text) { init(); setText(text); }
  RSyntaxTextAreaWithSearch(RSyntaxTextArea textArea) {
  this.textArea = textArea; init(); }
  
  public JComponent visualize() { return panel; }
  
  void init() { swing(() -> { 
    rsyntaxTextArea_fixNumPad();
    initSearchDialogs();
  
    panel = new JPanel(new BorderLayout());
    csp = new CollapsibleSectionPanel();
    panel.add(csp);
  
    if (textArea == null) textArea = javaxSyntaxTextArea();
    RTextScrollPane sp = new RTextScrollPane(textArea);
    csp.add(sp);
  
    ErrorStrip errorStrip = new ErrorStrip(textArea);
    panel.add(errorStrip, BorderLayout.LINE_END);
  
    statusBar = new StatusBar();
    panel.add(statusBar, BorderLayout.SOUTH);
  }); }
  
  transient  IF1<JComponent, JComponent> wrapStatusLabel;
JComponent wrapStatusLabel(JComponent label) { return wrapStatusLabel != null ? wrapStatusLabel.get(label) : wrapStatusLabel_base(label); }
final JComponent wrapStatusLabel_fallback(IF1<JComponent, JComponent> _f, JComponent label) { return _f != null ? _f.get(label) : wrapStatusLabel_base(label); }
JComponent wrapStatusLabel_base(JComponent label) { return label; }
  
  void addItem(Action a, ButtonGroup bg, JMenu menu) {
    JRadioButtonMenuItem item = new JRadioButtonMenuItem(a);
    bg.add(item);
    menu.add(item);
  }
  
  // register keystrokes if we don't have a menu bar
  void menuLessOperation() { swing(() -> { 
    //print("menuLessOperation");
    var mb = createMenuBar();
    for (var menu : getMenus(mb))
      for (var menuItem : getMenuItems(menu)) {
        var ks = menuItem.getAccelerator();
        //printVars(+ks, +menuItem);
        if (ks != null) {
          var action = menuItem.getAction();
          //print("Registering keystroke: " + ks + " => " + action);
          textArea.getInputMap().put(ks, action);
        }
      }
  }); }
  
  JMenuBar createMenuBar() {
    JMenuBar mb = new JMenuBar();
    JMenu menu = new JMenu("Search");
    menu.add(new JMenuItem(new ShowFindDialogAction()));
    menu.add(new JMenuItem(new ShowReplaceDialogAction()));
    menu.add(new JMenuItem(new GoToLineAction()));
    menu.addSeparator();
  
    int ctrl = getToolkit().getMenuShortcutKeyMask();
    int shift = InputEvent.SHIFT_MASK;
    KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_F, ctrl|shift);
    Action a = csp.addBottomComponent(ks, findToolBar);
    a.putValue(Action.NAME, "Show Find Search Bar");
    menu.add(new JMenuItem(a));
    ks = KeyStroke.getKeyStroke(KeyEvent.VK_H, ctrl|shift);
    a = csp.addBottomComponent(ks, replaceToolBar);
    a.putValue(Action.NAME, "Show Replace Search Bar");
    menu.add(new JMenuItem(a));
  
    mb.add(menu);
    return mb;
  }
  
  public String getSelectedText() {
    return textArea.getSelectedText();
  }
  
  /**
   * Creates our Find and Replace dialogs.
   */
  public void initSearchDialogs() {
  
    findDialog = new FindDialog((java.awt.Dialog) null, this);
    replaceDialog = new ReplaceDialog((java.awt.Dialog) null, this);
  
    // This ties the properties of the two dialogs together (match case,
    // regex, etc.).
    SearchContext context = findDialog.getSearchContext();
    replaceDialog.setSearchContext(context);
  
    // Create tool bars and tie their search contexts together also.
    findToolBar = new FindToolBar(this);
    findToolBar.setSearchContext(context);
    replaceToolBar = new ReplaceToolBar(this);
    replaceToolBar.setSearchContext(context);
  
  }
  
  
  /**
   * Listens for events from our search dialogs and actually does the dirty
   * work.
   */
  @Override
  public void searchEvent(SearchEvent e) {
  
    SearchEvent.Type type = e.getType();
    SearchContext context = e.getSearchContext();
    SearchResult result = null;
  
    switch (type) {
      default: // Prevent FindBugs warning later
      case MARK_ALL:
        result = SearchEngine.markAll(textArea, context);
        break;
       case FIND:
            result = SearchEngine.find(textArea, context);
            
            if (!result.wasFound())
            {
                //Try to wrap the result...
                Caret c = textArea.getCaret();
                int pos = c.getDot();
                if (context.getSearchForward())
                {
                    c.setDot(0);
                }
                else
                {
                    c.setDot(textArea.getDocument().getLength());
                }
                result = SearchEngine.find(textArea, context);
                if (!result.wasFound())
                {
                    c.setDot(pos);
                    UIManager.getLookAndFeel().provideErrorFeedback(textArea);
                }
            }
            break;
      case REPLACE:
        result = SearchEngine.replace(textArea, context);
        if (!result.wasFound()) {
          UIManager.getLookAndFeel().provideErrorFeedback(textArea);
        }
        break;
      case REPLACE_ALL:
        result = SearchEngine.replaceAll(textArea, context);
        JOptionPane.showMessageDialog(null, result.getCount() +
            " occurrences replaced.");
        break;
    }
  
    String text = null;
    if (result.wasFound()) {
      text = "Text found; occurrences marked: " + result.getMarkedCount();
    }
    else if (type==SearchEvent.Type.MARK_ALL) {
      if (result.getMarkedCount()>0) {
        text = "Occurrences marked: " + result.getMarkedCount();
      }
      else {
        text = "";
      }
    }
    else {
      text = "Text not found";
    }
    setStatus(text);
  }
  
  class GoToLineAction extends AbstractAction {
    GoToLineAction() {
      super("Go To Line...");
      int c = getToolkit().getMenuShortcutKeyMask();
      putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_L, c));
    }
  
    public void actionPerformed(ActionEvent e) {
      findDialog.setVisible(false);
      replaceDialog.setVisible(false);
      GoToDialog dialog = new GoToDialog((java.awt.Dialog) null);
      dialog.setMaxLineNumberAllowed(textArea.getLineCount());
      showDialogOnSameScreen(dialog, panel);
      int line = dialog.getLineNumber();
      if (line>0) {
        try {
          textArea.setCaretPosition(textArea.getLineStartOffset(line-1));
        } catch (BadLocationException ble) { // Never happens
          UIManager.getLookAndFeel().provideErrorFeedback(textArea);
          ble.printStackTrace();
        }
      }
    }
  }
  
  class ShowFindDialogAction extends AbstractAction {
    
    ShowFindDialogAction() {
      super("Find...");
      int c = getToolkit().getMenuShortcutKeyMask();
      putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_F, c));
    }
  
    public void actionPerformed(ActionEvent e) {
      replaceDialog.setVisible(false);
      showDialogOnSameScreen(findDialog, panel);
    }
  }
  
  class ShowReplaceDialogAction extends AbstractAction {
    ShowReplaceDialogAction() {
      super("Replace...");
      int c = getToolkit().getMenuShortcutKeyMask();
      putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_H, c));
    }
  
    public void actionPerformed(ActionEvent e) {
      findDialog.setVisible(false);
      showDialogOnSameScreen(replaceDialog, panel);
    }
  }
  
  class StatusBar extends JPanel {
    JLabel label;
  
    StatusBar() {
      label = new JLabel(" ");
      setLayout(new BorderLayout());
      add(wrapStatusLabel(label), BorderLayout.CENTER);
      add(new JLabel(new SizeGripIcon()), BorderLayout.EAST);
    }
  
    public void setText(String label) {
      main.setText(this.label, label);
    }
  }
  
  void setStatus(String text) {
    statusBar.setText(text);
  }
  
  void setText(String text) {
    main.setText(textArea, text);
  }
  
  String getText() {
    return main.getText(textArea);
  }
   
  void setEditorFont(final Font font) { swing(() -> { 
    SyntaxScheme ss = textArea.getSyntaxScheme();
    ss = (SyntaxScheme) ss.clone();
    for (int i = 0; i < ss.getStyleCount(); i++)
      if (ss.getStyle(i) != null)
        ss.getStyle(i).font = font;
     textArea.setSyntaxScheme(ss);
     textArea.setFont(font);
  }); }
  
  Font getEditorFont() {
    return swing(new F0<Font>() { public Font get() { try {  return textArea.getFont();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return textArea.getFont();"; }});
  }
  
  int getEditorFontSize() {
    return getEditorFont().getSize();
  }
  
  void setEditorFontSize(int size) {
    setEditorFont(deriveFont(getEditorFont(), size));
  }
  
  RSyntaxDocument getDocument() {
    return (RSyntaxDocument) textArea.getDocument();
  }
  
  Toolkit getToolkit() { return panel.getToolkit(); }
}
// A database system for only slightly modified Java objects.

// Recent changes:
// -minimal crash recovery disabled for now (could reenable)
// -idCounter.structure file no longer used
// -allDynamic/safeLoad disabled, it doesn't really make any sense
// -conceptsFile can be in any directory now

// Functions that should always be there for child processes:
static int concepts_internStringsLongerThan = 10;

static ThreadLocal<Boolean> concepts_unlisted = new ThreadLocal();

// BREAKING CHANGE 2021/6/7 set to true
static boolean concepts_unlistedByDefault = true; // true = we can create instances of concepts with "new" without registering them automatically

interface IConceptIndex {
  void update(Concept c); // also for adding
  void remove(Concept c);
}

interface IFieldIndex<A extends Concept, Val> {
  Collection<A> getAll(Val val);
  List<Val> allValues(); // returns a cloned list
  MultiSet<Val> allValues_multiSet();
  IterableIterator<A> objectIterator();
}

// Approach to persisting the Concepts object itself (in normal
// DB operation, this is not done): For simplification, speed and
// compactness, we make almost all the fields transient and store only // the concepts and the idCounter. To unstructure the Concepts object,
// use unstructureConcepts() or postUnstructureConcepts(), then
// re-set up any indices, listeners etc.

// base class indicating nothing
static class ConceptsChange {}

// change of a single concept
static class ConceptCreate extends ConceptsChange implements IFieldsToList{
  Concept c;
  ConceptCreate() {}
  ConceptCreate(Concept c) {
  this.c = c;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + c + ")"; }

public boolean equals(Object o) {
if (!(o instanceof ConceptCreate)) return false;
    ConceptCreate __3 =  (ConceptCreate) o;
    return eq(c, __3.c);
}

  public int hashCode() {
    int h = -1751266972;
    h = boostHashCombine(h, _hashCode(c));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {c}; }
}

// change of a single concept
static class ConceptChange extends ConceptsChange implements IFieldsToList{
  Concept c;
  ConceptChange() {}
  ConceptChange(Concept c) {
  this.c = c;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + c + ")"; }

public boolean equals(Object o) {
if (!(o instanceof ConceptChange)) return false;
    ConceptChange __4 =  (ConceptChange) o;
    return eq(c, __4.c);
}

  public int hashCode() {
    int h = -1760609256;
    h = boostHashCombine(h, _hashCode(c));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {c}; }
}

// removal of a single concept
// c.id is going to become 0 at some point, so we pass the
// id separately
static class ConceptDelete extends ConceptsChange implements IFieldsToList{
  static final String _fieldOrder = "id c";
  long id;
  Concept c;
  ConceptDelete() {}
  ConceptDelete(long id, Concept c) {
  this.c = c;
  this.id = id;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + id + ", " + c + ")"; }

public boolean equals(Object o) {
if (!(o instanceof ConceptDelete)) return false;
    ConceptDelete __5 =  (ConceptDelete) o;
    return id == __5.id && eq(c, __5.c);
}

  public int hashCode() {
    int h = -1734431213;
    h = boostHashCombine(h, _hashCode(id));
    h = boostHashCombine(h, _hashCode(c));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {id, c}; }
}

// unknown change anywhere in concepts; consider it all dirty
// (this one should not be used except for batch jobs)
static class FullChange extends ConceptsChange implements IFieldsToList{
  FullChange() {}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + ")"; }

public boolean equals(Object o) {
return o instanceof FullChange;
}

  public int hashCode() {
    int h = 733452095;
    return h;
  }
  public Object[] _fieldsToList() { return null; }
}

static class Concepts implements AutoCloseable {
  SortedMap<Long, Concept> concepts = synchroTreeMap();
  long idCounter;
  
  transient HashMap<Class, Object> perClassData;
  transient Map miscMap; // don't use directly, call miscMap... methods to access
  
  // set to "-" for non-persistent (possibly not implemented)
  // also, can include a case ID ("#123/1")
  // TODO: phase out (we have conceptsFile field now)
  transient String programID;
  transient File conceptsFile;
  
  transient Concepts parent; // new mechanism
  transient volatile long changes, changesWritten, lastChange;
  transient volatile java.util.Timer autoSaver;
  transient volatile boolean dontSave = false;
  transient volatile boolean savingConcepts, noXFullGrab;
  transient boolean vmBusSend = true;
  transient boolean initialSave = false; // set to false to avoid initial useless saving
  transient int autoSaveInterval = -1000; // 1 second + wait logic
  transient boolean useGZIP = true, quietSave;
  transient ReentrantLock lock = new ReentrantLock(true);
  transient ReentrantLock saverLock = new ReentrantLock(true);
  transient long lastSaveTook = -1, lastSaveWas, loadTook, uncompressedSize;
  transient float maxAutoSavePercentage = 10;
  transient List<IConceptIndex> conceptIndices;
  transient Map<Class<? extends Concept>, Map<String, IFieldIndex>> fieldIndices;
  transient Map<Class<? extends Concept>, Map<String, IFieldIndex>> ciFieldIndices;
  //transient L saveActions = synchroList();
  transient List<Runnable> preSave;
  transient Object classFinder = _defaultClassFinder();
  transient List onAllChanged = synchroList(); // list of runnables
  transient Set<IVF1> onChange = new HashSet();
  transient Object saveWrapper; // VF1<Runnable>, to profile saving
  transient boolean modifyOnCreate = false; // set _modified == created initially
  transient boolean modifyOnBackRef = false; // set modified if back refs change
  transient boolean useFileLock = true; // instead of locking by bot
  // OLD - not done anymore. transient bool collectForwardRefs
  transient FileBasedLock fileLock;
  transient boolean storeBaseClassesInStructure = false; // helps with schema evolution when concept subclasses disappear
  transient boolean useBackRefsForSearches = false; // assume backRefs are sane in order to speed up searches
  transient boolean defunct = false;
  transient int newBackupEveryXMinutes = 60;
  
  // add more fields for Concepts here
  
  Concepts() {}
  Concepts(String programID) {
  this.programID = programID;}
  Concepts(File conceptsFile) {
  this.conceptsFile = conceptsFile;}
  
  synchronized long internalID() {
    do {
      ++idCounter;
    } while (hasConcept(idCounter));
    return idCounter;
  }
  
  synchronized HashMap<Class, Object> perClassData() {
    if (perClassData == null) perClassData = new HashMap();
    return perClassData;
  }
  
  void initProgramID() {
    if (programID == null)
      programID = getDBProgramID();
  }
  
  // load from structure
  Concepts load(String structure) { return load(structure, false); }
Concepts load(String structure, boolean allDynamic) {
    clearConcepts();
    Map<Long, Concept> map = unstructureMap(structure, allDynamic, classFinder);
    concepts.putAll(map);
    assignConceptsToUs();
    calcIdCounter();
    return this;
  }
  
  Concepts load() {
    initProgramID();
    
    // try custom grabber
    Object dbGrabber = miscMapGet("dbGrabber");
    if (dbGrabber != null && !isFalse(callF(dbGrabber)))
      return this;

    try {
      if (tryToGrab()) return this;
    } catch (Throwable e) {
      if (!exceptionMessageContains(e, "no xfullgrab"))
        printShortException(e);
      print("xfullgrab failed - loading DB of " + programID + " from disk");
    }
    return loadFromDisk();
  }
  
  Concepts loadFromDisk() {
    if (nempty(concepts)) clearConcepts();
    
    // minimal crash recovery (disabled for now)
    //restoreLatestBackupIfConceptsFileEmpty(programID, doIt := true);

    long time = now();
    Map<Long, Concept> _concepts =  (Map<Long, Concept>) (unstructureGZFile(conceptsFile(), toIF1(classFinder)));
    putAll(concepts, _concepts);
    assignConceptsToUs();
    loadTook = now()-time;
    done("Loaded " + n2(l(concepts), "concept"), time);
    calcIdCounter();
    return this;
  }
  
  Concepts loadConcepts() { return load(); }
  
  boolean tryToGrab() {
    if (sameSnippetID(programID, getDBProgramID())) return false;
     RemoteDB db = connectToDBOpt(programID); try {
    if (db != null) {
      loadGrab(db.fullgrab());
      return true;
    }
    return false;
  } finally { _close(db); }}
  
  Concepts loadGrab(String grab) {
    clearConcepts();
    DynamicObject_loading.set(true);
    try {
      Map<Long, Concept> map = (Map) unstructure(grab, false, classFinder);
      concepts.putAll(map);
      assignConceptsToUs();
      for (long l : map.keySet())
        idCounter = max(idCounter, l);
    } finally {
      DynamicObject_loading.set(null);
    }
    //XXX allChanged(); // Nobody is listening at this point anyway
    return this;
  }
  
  void assignConceptsToUs() {
    // fix unstructure bugs
    
    for (Pair<Long, Object> p: mapToPairs((Map<Long, Object>) (Map) concepts))
      if (!(p.b instanceof Concept)) {
       print("DROPPING non-existant concept " + p.a + ": " + dynShortName(p.b));
       concepts.remove(p.a);
    }
    
    for (Concept c : values(concepts)) c._concepts = this;
    for (Concept c : values(concepts))
      c._doneLoading2(); // doneLoading2 is called on all concepts after all concepts are loaded
  }

  String progID() {
    return programID == null ? getDBProgramID() : programID;
  }
  
  Concept getConcept(String id) {
    return empty(id) ? null : getConcept(parseLong(id));
  }
  
  Concept getConcept(long id) {
    return (Concept) concepts.get((long) id);
  }
  
  Concept getConcept(RC ref) {
    return ref == null ? null : getConcept(ref.longID());
  }
  
  boolean hasConcept(long id) {
    return concepts.containsKey((long) id);
  }
  
  void deleteConcept(long id) {
    Concept c = getConcept(id);
    if (c == null)
      print("Concept " + id + " not found");
    else
      c.delete();
  }
  
  void calcIdCounter() {
    Long lastID = lastKey(concepts);
    idCounter = lastID == null ? 1 : lastID+1;
  }
  
  File conceptsDir() { return dirOfFile(conceptsFile()); }
  
  Concepts conceptsFile(File conceptsFile) { this.conceptsFile = conceptsFile; return this; }
  
  File conceptsFile() {
    if (conceptsFile != null) return conceptsFile;
    
    return getProgramFile(programID, useGZIP ? "concepts.structure.gz" : "concepts.structure");
  }
  
  // used for locking when useFileLock is activated
  File lockFile() {
    return newFile(conceptsDir(), "concepts.lock");
  }
  
  FileBasedLock fileLock() {
    if (fileLock == null)
      fileLock = new FileBasedLock(lockFile());
    return fileLock;
  }
  
  void saveConceptsIfDirty() { saveConcepts(); }
  void save() { saveConcepts(); }

  void saveConcepts() {
    vmBus_send("saveConceptsCalled", Concepts.this);
    if (dontSave) return;
    initProgramID();
    saverLock.lock();
    savingConcepts = true;
    long start = now(), time;
    try {
      String s = null;
      //synchronized(main.class) {
      long _changes = changes;
      if (_changes == changesWritten) return;
      
      File f = conceptsFile();
      
      lock.lock();
      long fullTime = now();
      try {
        if (useGZIP) {
          vmBus_send("callingSaveWrapper", Concepts.this, saveWrapper);
          callRunnableWithWrapper(saveWrapper, new Runnable() {  public void run() { try { 
            vmBus_send("callingPreSave", Concepts.this, preSave);
            callFAll(preSave);
            vmBus_send("writingFile", Concepts.this, f);
            uncompressedSize = saveGZStructureToFile(f, cloneMap(concepts), makeStructureData());
            vmBus_send("gzFileSaved", Concepts.this, f, uncompressedSize);
          
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "vmBus_send callingPreSave(Concepts.this, preSave);\r\n            callFAll(preS..."; }});
          newFile(conceptsDir(), "concepts.structure").delete();
        } else
          s = fullStructure();
      } finally {
        lock.unlock();
      }
      
      /*while (nempty(saveActions))
        pcallF(popFirst(saveActions));*/

      changesWritten = _changes; // only update when structure didn't fail
      
      if (!useGZIP) {
        time = now()-start;
        if (!quietSave)
          print("Saving " + toM(l(s)) + "M chars (" /*+ changesWritten + ", "*/ + time + " ms)");
        start = now();
        saveTextFile(f, javaTokWordWrap(s));
        newFile(conceptsDir(), "concepts.structure.gz").delete();
      }
      
      File conceptsFile = conceptsFile();
      File backupFile = newFile(conceptsDir(), "backups/" + fileName(conceptsFile) + ".backup" + ymd() + "-" + formatInt(hours(), 2)
        + (newBackupEveryXMinutes >= 60 ? "" : formatInt(roundDownTo_rev(minutes(), newBackupEveryXMinutes), 2)));
      // TODO: get rid of this
      copyFile(f, backupFile);
      
      time = now()-start;
      if (!quietSave)
        print("Saved " + toK(f.length()) + " K, " + n(concepts, "concepts") + " (" + time + " ms)");
      lastSaveWas = fullTime;
      lastSaveTook = now()-fullTime;
    } finally {
      savingConcepts = false;
      saverLock.unlock();
    }
  }
  
  void _autoSaveConcepts() {
    if (autoSaveInterval < 0 && maxAutoSavePercentage != 0) {
      long pivotTime = Math.round(lastSaveWas+lastSaveTook*100.0/maxAutoSavePercentage);
      if (now() < pivotTime) {
        //print("Skipping auto-save (last save took " + lastSaveTook + ")");
        return;
      }
    }
    try {
      saveConcepts();
    } catch (Throwable e) {
      print("Concept save failed, will try again");
      printStackTrace(e);
    }
  }
  
  String fullStructure() {
    return structure(cloneMap(concepts), makeStructureData());
  }
  
  transient  IF0<structure_Data> makeStructureData;
structure_Data makeStructureData() { return makeStructureData != null ? makeStructureData.get() : makeStructureData_base(); }
final structure_Data makeStructureData_fallback(IF0<structure_Data> _f) { return _f != null ? _f.get() : makeStructureData_base(); }
structure_Data makeStructureData_base() {
    return finishStructureData(new structure_Data());
  }
  
  structure_Data finishStructureData(structure_Data data) {
    if (storeBaseClassesInStructure)
      data.storeBaseClasses = true;
    return data;
  }
  
  void clearConcepts() {
    for (Concept c : allConcepts()) c.delete();
    //concepts.clear();
    //allChanged();
  }
  
  void fireLegacyChangeEvent() {
    synchronized(this) { ++changes; lastChange = sysNow(); }
    if (vmBusSend) vmBus_send("conceptsChanged", this);
    pcallFAll(onAllChanged);
  }
  
  // auto-save every second if dirty
  synchronized void autoSaveConcepts() {
    if (autoSaver == null) {
      if (isTransient()) throw fail("Can't persist transient database");
      autoSaver = doEvery_daemon("Concepts Saver for " + conceptsDir(),
        abs(autoSaveInterval), new Runnable() {  public void run() { try {  _autoSaveConcepts() ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "_autoSaveConcepts()"; }});
      // print("Installed auto-saver (" + autoSaveInterval + " ms, " + progID() + ")");
    }
  }
  
  public void close() { cleanMeUp(); }
  
  void cleanMeUp() {
    try {
      defunct = true;
      boolean shouldSave = autoSaver != null;
      if (autoSaver != null) {
        autoSaver.cancel();
        autoSaver = null;
      }
      while (savingConcepts) sleepInCleanUp(10);
      if (shouldSave)
        saveConceptsIfDirty();
    } catch (Throwable __e) { printStackTrace(__e); }
    { cleanUp(fileLock); fileLock = null; }
  }
  
  Map<Long, String> getIDsAndNames() {
    Map<Long, String> map = new HashMap();
    Map<Long, Concept> cloned = cloneMap(concepts);
    for (long id : keys(cloned)) 
      map.put(id, cloned.get(id).className);
    return map;
  }
  
  void deleteConcepts(List l) {
    ping();
    if (l != null) for (Object o : cloneList(l))
      if (o instanceof Long) {
        Concept c = concepts.get(o);
        if (c != null) c.delete();
      } else if (o instanceof Concept)
        ((Concept) o).delete();
      else
        warn("Can't delete " + getClassName(o));
  }
  
  <A extends Concept> A conceptOfType(Class<A> type) {
    IConceptCounter counter = conceptCounterForClass(type);
    if (counter != null) return (A) first(counter.allConcepts());
    return firstOfType(allConcepts(), type);
  }
  
  <A extends Concept> List<A> conceptsOfType(Class<A> type) {
    List<A> l = conceptsOfType_noParent(type);
    if (parent == null) return l;
    return concatLists_conservative(l, parent.conceptsOfType(type));
  }
  
  <A extends Concept> List<A> conceptsOfType_noParent(Class<A> type) {
    ping();
    IConceptCounter counter = conceptCounterForClass(type);
    if (counter != null) return (List<A>) cloneList(counter.allConcepts());
    return filterByType(allConcepts(), type);
  }
  
  <A extends Concept> List<A> listConcepts(Class<A> type) {
    return conceptsOfType(type);
  }
  
  <A extends Concept> List<A> list(Class<A> type) {
    return conceptsOfType(type);
  }
  
  <A extends Concept> List<A> list_noParent(Class<A> type) {
    return conceptsOfType_noParent(type);
  }
  
  // TODO: would be better to make this Cl (indices may return sets)
  List<Concept> list(String type) {
    return conceptsOfType(type);
  }
  
  List<Concept> conceptsOfType(String type) {
    return filterByDynamicType(allConcepts(), "main$" + type);
  }
  
  boolean hasConceptOfType(Class<? extends Concept> type) {
    return hasType(allConcepts(), type);
  }
  
  void persistConcepts() {
    loadConcepts();
    autoSaveConcepts();
  }
  
  // We love synonyms
  void conceptPersistence() { persistConcepts(); }
  
  Concepts persist() { persistConcepts(); return this; }
  void persist(Integer interval) {
    if (interval != null) autoSaveInterval = interval;
    persist();
  }
    
  // Runs r if there is no concept of that type
  <A extends Concept> A ensureHas(Class<A> c, Runnable r) {
    A a = conceptOfType(c);
    if (a == null) {
      r.run();
      a = conceptOfType(c);
      if (a == null)
        throw fail("Concept not made by " + r + ": " + shortClassName(c));
    }
    return a;
  }
  
  // Ensures that every concept of type c1 is ref'd by a concept of
  // type c2.
  // Type of func: voidfunc(concept)
  void ensureHas(Class<? extends Concept> c1, Class<? extends Concept> c2, Object func) {
    for (Concept a : conceptsOfType(c1)) {
      Concept b = findBackRef(a, c2);
      if (b == null) {
        callF(func, a);
        b = findBackRef(a, c2);
        if (b == null)
          throw fail("Concept not made by " + func + ": " + shortClassName(c2));
      }
    }
  }
  
  // Type of func: voidfunc(concept)
  void forEvery(Class<? extends Concept> type, Object func) {
    for (Concept c : conceptsOfType(type))
      callF(func, c);
  }
  
  int deleteAll(Class<? extends Concept> type) {
    List<Concept> l = (List) conceptsOfType(type);
    for (Concept c : l) c.delete();
    return l(l);
  }
  
  // always returns a new list (callers depend on this)
  Collection<Concept> allConcepts() {
    synchronized(concepts) {
      return new ArrayList(values(concepts));
    }
  }
  
  IConceptCounter conceptCounterForClass(Class<? extends Concept> c) {
    for (IFieldIndex idx : values(mapGet(fieldIndices, c)))
      if (idx instanceof IConceptCounter) return ((IConceptCounter) idx);
    for (IFieldIndex idx : values(mapGet(ciFieldIndices, c)))
      if (idx instanceof IConceptCounter) return ((IConceptCounter) idx);
    return null;
  }
  

  <A extends Concept> int countConcepts(Class<A> c, Object... params) {
    int n = countConcepts_noParent(c, params);
    if (parent == null) return n;
    return n+parent.countConcepts(c, params);
  }
  
  <A extends Concept> int countConcepts_noParent(Class<A> c, Object... params) {
    ping();
    if (empty(params)) {
      IConceptCounter counter = conceptCounterForClass(c);
      if (counter != null) return counter.countConcepts();
      return l(list_noParent(c));
    }
    int n = 0;
    for (A x : list_noParent(c)) if (checkConceptFields(x, params)) ++n;
    return n;
  }

  int countConcepts(String c, Object... params) {
    ping();
    if (empty(params)) return l(list(c));
    int n = 0;
    for (Concept x : list(c)) if (checkConceptFields(x, params)) ++n;
    return n;
  }

  int countConcepts() {
    return l(concepts);
  }
  
  synchronized List<IConceptIndex> clonedConceptIndices() {
    return cloneList(conceptIndices);
  }
  
  synchronized void addConceptIndex(IConceptIndex index) {
    if (conceptIndices == null)
      conceptIndices = new ArrayList();
    conceptIndices.add(index);
  }
  
  synchronized void removeConceptIndex(IConceptIndex index) {
    if (conceptIndices == null) return;
    conceptIndices.remove(index);
    if (empty(conceptIndices)) conceptIndices = null;
  }
  
  synchronized void addFieldIndex(Class<? extends Concept> c, String field, IFieldIndex index) {
    if (fieldIndices == null)
      fieldIndices = new HashMap();
    Map<String, IFieldIndex> map = fieldIndices.get(c);
    if (map == null)
      fieldIndices.put(c, map = new HashMap());
    map.put(field, index);
  }
  
  synchronized void removeFieldIndex(Class<? extends Concept> c, String field, IFieldIndex index) {
    Map<String, IFieldIndex> map = mapGet(fieldIndices, c);
    mapRemove(map, field);
  }
  
  synchronized IFieldIndex getFieldIndex(Class<? extends Concept> c, String field) {
    if (fieldIndices == null) return null;
    Map<String, IFieldIndex> map = fieldIndices.get(c);
    return map == null ? null : map.get(field);
  }
  
  synchronized IFieldIndex getAnyIndexForClass(Class<? extends Concept> c) {
    return firstValue(fieldIndices == null ? null : fieldIndices.get(c));
  }
  
  synchronized void addCIFieldIndex(Class<? extends Concept> c, String field, IFieldIndex index) {
    if (ciFieldIndices == null)
      ciFieldIndices = new HashMap();
    Map<String, IFieldIndex> map = ciFieldIndices.get(c);
    if (map == null)
      ciFieldIndices.put(c, map = new HashMap());
    map.put(field, index);
  }
  
  synchronized void removeCIFieldIndex(Class<? extends Concept> c, String field) {
    Map<String, IFieldIndex> map = mapGet(ciFieldIndices, c);
    mapRemove(map, field);
  }
  
  synchronized IFieldIndex getCIFieldIndex(Class<? extends Concept> c, String field) {
    if (ciFieldIndices == null) return null;
    Map<String, IFieldIndex> map = ciFieldIndices.get(c);
    return map == null ? null : map.get(field);
  }
  
  // inter-process methods
  
  RC xnew(String name, Object... values) {
    return new RC(cnew(name, values));
  }
  
  void xset(long id, String field, Object value) {
    xset(new RC(id), field, value);
  }
  
  void xset(RC c, String field, Object value) {
    if (value instanceof RC)
      value = getConcept((RC) value);
    cset(getConcept(c), field, value);
  }
  
  Object xget(long id, String field) {
    return xget(new RC(id), field);
  }
  
  Object xget(RC c, String field) {
    return xgetPost(cget(getConcept(c), field));
  }
  
  Object xgetPost(Object o) {
    o = deref(o);
    if (o instanceof Concept)
      return new RC((Concept) o);
    return o;
  }
  
  void xdelete(long id) {
    xdelete(new RC(id));
  }
  
  void xdelete(RC c) {
    getConcept(c).delete();
  }
  
  void xdelete(List<RC> l) {
    for (RC c : l)
      xdelete(c);
  }
  
  List<RC> xlist() {
    return map("toPassRef", allConcepts());
  }
  
  List<RC> xlist(String className) {
    return map("toPassRef", conceptsOfType(className));
  }
  
  boolean isTransient() { return eq(programID, "-"); }
  
  String xfullgrab() {
    if (noXFullGrab) throw fail("no xfullgrab (DB too large)");
    Lock __1 = lock(); lock(__1); try {
    if (changes == changesWritten && !isTransient())
      return loadConceptsStructure(programID);
    return fullStructure();
  } finally { unlock(__1); } }
  
  /* dev.
  Either<File, byte[]> xfullgrabGZipped() {
    lock lock();
    if (changes == changesWritten && !isTransient())
      ret loadConceptsStructure(programID);
    ret fullStructure();
  }*/
  
  void xshutdown() {
    // Killing whole VM if someone wants this DB to shut down
    cleanKillVM();
  }
  
  long xchangeCount() { return changes; }
  int xcount() { return countConcepts(); }
  
  void register(Concept c) {
    ping();
    if (c._concepts == this) return;
    if (c._concepts != null) throw fail("Can't re-register");
    c.id = internalID();
    c.created = now();
    if (modifyOnCreate) c._setModified(c.created);
    register_phase2(c);
    vmBus_send("conceptCreated", c);
    fireChange(new ConceptCreate(c));
  }
  
  // also called by replaceConceptAndUpdateRefs
  void register_phase2(Concept c) {
    c._concepts = this;
    concepts.put((long) c.id, c);
    for (Concept.Ref r : c._refs())
      r.index();
    c.change();
    c._onRegistered();
  }
  
  void registerKeepingID(Concept c) {
    if (c._concepts == this) return;
    if (c._concepts != null) throw fail("Can't re-register");
    c._concepts = this;
    concepts.put((long) c.id, c);
    c.change();
  }
  
  void conceptChanged(Concept c) {
    fireChange(new ConceptChange(c));
    if (conceptIndices != null)
      for (IConceptIndex index : clonedConceptIndices())
        index.update(c);
  }
  
  boolean hasUnsavedData() {
    return changes != changesWritten || savingConcepts;
  }
  
  synchronized Object miscMapGet(Object key) {
    return mapGet(miscMap, key);
  }
  
  synchronized Object miscMapPut(Object key, Object value) {
    if (miscMap == null) miscMap = new HashMap();
    return miscMap.put(key, value);
  }
  
  synchronized void miscMapRemove(Object key) {
    mapRemove(miscMap, key);
  }
  
  // Note: auto-typing can fool you, make sure create returns
  // a wide enough type
  synchronized <A> A miscMapGetOrCreate(Object key, IF0<A> create) {
    if (containsKey(miscMap, key)) return (A) miscMap.get(key);
    A value = create.get();
    miscMapPut(key, value);
    return value;
  }
  
  void setParent(Concepts parent) {
    this.parent = parent;
  }
  
  void fireChange(ConceptsChange change) {
    if (change == null) return;
    pcallFAll(onChange, change);
    fireLegacyChangeEvent();
  }
  
  final void onChange(IVF1<ConceptsChange> l){ addChangeListener(l); }
void addChangeListener(IVF1<ConceptsChange> l) {
    syncAdd(onChange, l);
  }
  
  void removeChangeListener(IVF1<ConceptsChange> l) {
    syncRemove(onChange, l);
  }
  
  void addPreSave(Runnable r) {
    preSave = syncAddOrCreate(preSave, r);
  }
  
  public String toString() {
    return nConcepts(concepts) + " (" + conceptsDir() + ", hash: " + identityHashCode(this) + ")";
  }
} // end of Concepts

// TODO: Move everyone over to this so we can have concepts that
// don't subclass Concept. It's hard though because of Concept.Ref
interface IConcept {
  public long _conceptID();
  public Concepts concepts();
}

static class Concept extends DynamicObject implements IConcept, ChangeTriggerable {
  transient Concepts _concepts; // Where we belong
  long id;
  long created, _modified;
  
  List<Ref> backRefs;
  
  // used only internally (cnew)
  Concept(String className) {
    super(className);
    _created();
  }
  
  Concept() {
    if (!_loading()) {
      //className = shortClassName(this); // XXX - necessary?
      //print("New concept of type " + className);
      _created();
    }
  }
  
  Concept(boolean unlisted) {
    if (!unlisted) _created();
  }
  
  boolean includeZeroIDInToString() { return false; }
  
  public String toString() {
    String s = shortDynamicClassName(this);
    long id = this.id;
    if (id != 0 || includeZeroIDInToString())
      s += " " + id;
    return s;
  }
  
  static boolean loading() { return _loading(); }
  static boolean _loading() { return dynamicObjectIsLoading(); }

  void _created() {
    if (!concepts_unlistedByDefault && !eq(concepts_unlisted.get(), true))
      db_mainConcepts().register(this);
  }
  
  // base class + required interface. experimental
  class TypedRef<A extends Concept, B> extends Ref<A> {
  TypedRef() {}
    //Class<B> aType;
    Class<B> bType;
    
    TypedRef(Class<B> bType) {
  this.bType = bType;}
    TypedRef(Class<B> bType, B value) {
  this.bType = bType; set((A) value); }
    TypedRef(B value) { set((A) value); }
    
    public boolean set(A a) {
      return super.set(checkValue(a));
    }
    
    void check() { checkValue(get()); }
    
    <C> C checkValue(C a) {
      if (bType != null && a != null)
        assertIsInstance(a, bType);
      return a;
    }
    
    B b() { return (B) value; }
  }
  
  class Ref<A extends Concept> implements IRef<A> {
    A value;
    
    Ref() {
      if (!dynamicObjectIsLoading())
        registerRef();
    }
    
    void registerRef() {
      vmBus_send("registeringConceptRef", this);
    }
    
    Ref(A value) {
  this.value = value;
      registerRef();
      index();
    }
    
    // get owning concept (source)
    Concept concept() {
      return Concept.this;
    }
    
    // get target
    public A get() { return value; }
    public boolean has() { return value != null; }
    
    boolean set(A a) {
      if (a == value) return false;
      unindex();
      value = a;
      index();
      change();
      return true;
    }
    
    void setIfEmpty(A a) {
      if (!has()) set(a);
    }
    
    public void set(Ref<A> ref) { set(ref.get()); }
    public void clear() { set((A) null); }
    
    boolean validRef() {
      return value != null && _concepts != null && _concepts == value._concepts;
    }
    
    // TODO: sync all the indexing and unindexing!?
    void index() { 
      if (validRef()) {
        value._addBackRef(this);
        change();
      }
    }

    Ref<A> unindex() {
      if (validRef()) {
        value._removeBackRef(this);
        change();
      }
      return this;
    }
    
    void unindexAndDrop() {
      unindex();
      _removeRef(this);
    }
    
    void change() {
      Concept.this.change();
    }
    
    public String toString() { return 
    str(value); }
  }
  
  class RefL<A extends Concept> extends AbstractList<A> {
    List<Ref<A>> l = new ArrayList();
    
    RefL() {}
    RefL(List<A> l) { replaceWithList(l); }
    
    public void clear() {
      while (!isEmpty()) removeLast(this);
    }
    
    public void replaceWithList(List<A> l) {
      clear();
      for (A a : unnullForIteration(l)) add(a);
    }
    
    public A set(int i, A o) {
      Ref<A> ref = syncGet(l, i);
      A prev = ref.get();
      ref.set(o);
      return prev;
    }
    
    public void add(int i, A o) {
      syncAdd(l, i, new Ref(o));
    }
    
    public A get(int i) {
      return syncGet(l, i).get();
    }
    
    public A remove(int i) {
      return syncRemove(l, i).get();
    }
    
    public int size() {
      return syncL(l);
    }
    
    public boolean contains(Object o) {
      if (o instanceof Concept)
        for (Ref<A> r : l) if (eq(r.get(), o)) return true;
      return super.contains(o);
    }
  }
  
  void delete() {
    //name = "[defunct " + name + "]";
    //defunct = true;
    //energy = 0;
    
    // clean refs
    
    for (Ref r : unnullForIteration(_refs()))
      r.unindex();
    
    
    // set back refs to null
    
    for (Ref r : cloneList(backRefs))
      r.set((Concept) null);
    backRefs = null;
    
    var _concepts = this._concepts;
    if (_concepts != null) {
      _concepts.concepts.remove(id);
      _concepts.fireChange(new ConceptDelete(id, this));
      if (_concepts.conceptIndices != null)
        for (IConceptIndex index : _concepts.conceptIndices)
          index.remove(this);
      this._concepts = null;
    }
    id = 0;
  }
  
  BaseXRef export() {
    return new BaseXRef(_concepts.progID(), id);
  }
  
  // notice system of a change in this object
  final public void _change(){ change(); }
public void change() {
    _setModified(now());
    _change_withoutUpdatingModifiedField();
  }
  
  void _setModified(long modified) {
    _modified = modified;
  }
  
  final void _change_withoutUpdatingModifiedField() {
    _onChange();
    if (_concepts != null) _concepts.conceptChanged(this);
  }
  
  // overridable
  void _onChange() {}
  
  String _programID() {
    return _concepts == null ? getDBProgramID() : _concepts.progID();
  }
  
  // overridable
  
  void _addBackRef(Concept.Ref ref) {
    backRefs = addDyn_quickSync(backRefs, ref);
    _backRefsModified();
  }
  
  void _backRefsModified() {
    if (_concepts != null && _concepts.modifyOnBackRef) change();
  }
  
  void _removeBackRef(Concept.Ref ref) {
    backRefs = removeDyn_quickSync(backRefs, ref);
    _backRefsModified();
  }
  
  void _removeRef(Concept.Ref ref) {
    
  }
  
  int _backRefCount() { return syncL(backRefs); }
  
  // convenience methods
  
  final void setField(String field, Object value){ _setField(field, value); }
void _setField(String field, Object value) {
    cset(this, field, value);
  }
  
  boolean setField_trueIfChanged(String field, Object value) {
    return cset(this, field, value) != 0;
  }
  
  <A> A setFieldAndReturn(String field, A value) {
    setField(field, value);
    return value;
  }
  
  final void setFields(Object... values){ _setFields(values); }
void _setFields(Object... values) {
    cset(this, values);
  }
  
  public Concepts concepts() { return _concepts; }
  
  boolean isDeleted() { return id == 0; }
  
  void _doneLoading2() {
    Map<String, FieldMigration> map = _fieldMigrations();
    if (map != null) for (Map.Entry<? extends String, ? extends FieldMigration> __0 : _entrySet( map))
      { String oldField = __0.getKey(); FieldMigration m = __0.getValue();  crenameField_noOverwrite(this, oldField, m.newField); }
  }
  
  static class FieldMigration implements IFieldsToList{
  String newField;
  FieldMigration() {}
  FieldMigration(String newField) {
  this.newField = newField;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + newField + ")"; }

public boolean equals(Object o) {
if (!(o instanceof FieldMigration)) return false;
    FieldMigration __6 =  (FieldMigration) o;
    return eq(newField, __6.newField);
}

  public int hashCode() {
    int h = 558692372;
    h = boostHashCombine(h, _hashCode(newField));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {newField}; }
}
  
  // value is 
  Map<String, FieldMigration> _fieldMigrations() { return null; }
  
  // new wrapper to get a copy of the refs list
  // so we can eventually drop the refs field
  Collection<Ref> _refs() {
    return scanConceptForRefs(this);
  }
  
  
  
  Concepts _concepts() { return _concepts; }
  
  boolean _conceptsDefunct() { return _concepts != null && _concepts.defunct; }
  boolean _conceptsDefunctOrUnregistered() { return _concepts == null || _concepts.defunct; }
  
  // allow refs to do magic stuff?
  void _onRegistered() {
    /*for (Ref ref : _refs())
      refs._onRegistered();*/
  }
  
  
<A> boolean addAndChange(Collection<A> cl, A a) {
  if (cl == null || !cl.add(a)) return false;
  change();
  return true;
}

<A> void clearAndChange(Collection<A> cl) {
  if (cl == null) return;
  cl.clear();
  change();
}


File conceptsDir() {
    var concepts = concepts();
    return concepts == null ? null : concepts.conceptsDir();
  }
  
  File fileInConceptsDir(String name) {
    var dir = conceptsDir();
    return dir == null ? null : newFile(dir, name);
  }
  
  public long _conceptID() { return id; }
} // end of Concept

// remote reference (for inter-process communication or
// external databases). Formerly "PassRef".
// prepared for string ids if we do them later
static class RC {
  transient Object owner;
  String id;
  
  RC() {} // make serialisation happy
  RC(long id) { this.id = str(id); }
  RC(Object owner, long id) { this.id = str(id); this.owner = owner; }
  RC(Concept c) { this(c.id); }
  long longID() { return parseLong(id); }
  
  public String toString() {
    return id;
  }

  transient RemoteDB db;
  
  String getString(String field) { return db.xS(this, field); }
  Object get(String field) { return db.xget(this, field); }
  void set(String field, Object value) { db.xset(this, field, value); }

} // end of RC

// Reference to a concept in another program
static class BaseXRef {
  String programID;
  long id;
    
  BaseXRef() {}
  BaseXRef(String programID, long id) {
  this.id = id;
  this.programID = programID;}
  
  public boolean equals(Object o) {
    if (!(o instanceof BaseXRef)) return false;
    BaseXRef r =  (BaseXRef) o;
    return eq(programID, r.programID) && eq(id, r.id);
  }
  
  public int hashCode() {
    return programID.hashCode() + (int) id;
  }
}

// BaseXRef as a concept
static class XRef extends Concept {
  BaseXRef ref;
  
  XRef() {}
  XRef(BaseXRef ref) {
  this.ref = ref; _doneLoading2(); }
  
  // after we have been added to concepts
  void _doneLoading2() {
    getIndex().put(ref, this);
  }
    
  HashMap<BaseXRef, XRef> getIndex() {
    return getXRefIndex(_concepts);
  }
}

static synchronized HashMap<BaseXRef, XRef> getXRefIndex(Concepts concepts) {
  HashMap cache = (HashMap) concepts.perClassData().get(XRef.class);
  if (cache == null)
    concepts.perClassData.put(XRef.class, cache = new HashMap());
  return cache;
}

// uses mainConcepts
static XRef lookupOrCreateXRef(BaseXRef ref) {
  XRef xref = getXRefIndex(db_mainConcepts()).get(ref);
  if (xref == null)
    xref = new XRef(ref);
  return xref;
}

// define standard concept functions to use main concepts

// Now in db_mainConcepts()
/*static void cleanMeUp_concepts() {
  if (db_mainConcepts() != null) db_mainConcepts().cleanMeUp();
  // mainConcepts = null; // TODO
}*/

static void loadAndAutoSaveConcepts() {
  db_mainConcepts().persist();
}

static void loadAndAutoSaveConcepts(int interval) {
  db_mainConcepts().persist(interval);
}

static RC toPassRef(Concept c) {
  return new RC(c);
}

// so we can instantiate the program to run as a bare DB bot
static void concepts_setUnlistedByDefault(boolean b) {
  concepts_unlistedByDefault = b;
}
static class G22Label extends ConceptWithGlobalID {
  String name;
  //new RefL examples;
  
  public String toString() { return /*"Label " +*/ name; }
  
  final boolean textIs(String text){ return nameIs(text); }
boolean nameIs(String text) { return eqic(name, text); }
}

static class ImageSurface_PositionToolTip extends ImageSurfaceMouseHandler {
  ImageSurface_PositionToolTip(ImageSurface is) {
    if (containsInstance(is.tools, ImageSurface_PositionToolTip.class)) return;
    register(is);
  }

  final public void mouseDragged(MouseEvent e){ mouseMoved(e); }
public void mouseMoved(MouseEvent e) {
    setToolTip(is, getPt(e));
  }
}

interface IDoublePt {
  public double x_double();
  public double y_double();
}
static interface G22MasterStuff {
  void openDatabase(File dir);
  void closeDatabase(File dir);
  Collection<File> openConceptDirs();
  G22MasterStuff onLoadedDBsChange(Runnable r);
  G22MasterStuff removeLoadedDBsChangeListener(Runnable r);
  IF1<String, Class> makeClassFinder();
}


// Describes a value during script optimization/type inference.

// In the best case, the exact value is known.
// Next best case is we're knowing its precise type (value of object.getClass()).
// Third best case is we're knowing it's castable to a certain type.
// A completely unknown value is described simply by "new LASValueDescriptor".

static class LASValueDescriptor {
  LASValueDescriptor() {}
  boolean knownValue() { return false; }
  Object value() { return null; }
  
  Class javaClass() { return null; }
  boolean javaClassIsExact() { return false; }
  boolean canBeNull() { return true; }
  
  boolean canFail() { return false; }
  boolean willFail() { return false; }
  
  static class Exact extends LASValueDescriptor implements IFieldsToList{
  Class c;
  boolean canBeNull = false;
  Exact() {}
  Exact(Class c, boolean canBeNull) {
  this.canBeNull = canBeNull;
  this.c = c;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + c + ", " + canBeNull + ")"; }

public boolean equals(Object o) {
if (!(o instanceof Exact)) return false;
    Exact __1 =  (Exact) o;
    return eq(c, __1.c) && eq(canBeNull, __1.canBeNull);
}

  public int hashCode() {
    int h = 67394271;
    h = boostHashCombine(h, _hashCode(c));
    h = boostHashCombine(h, _hashCode(canBeNull));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {c, canBeNull}; }

    Class javaClass() { return c; }
    boolean javaClassIsExact() { return true; }
    boolean canBeNull() { return canBeNull; }
  }
  
  static class NonExact extends LASValueDescriptor implements IFieldsToList{
  Class c;
  boolean canBeNull = false;
  NonExact() {}
  NonExact(Class c, boolean canBeNull) {
  this.canBeNull = canBeNull;
  this.c = c;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + c + ", " + canBeNull + ")"; }

public boolean equals(Object o) {
if (!(o instanceof NonExact)) return false;
    NonExact __2 =  (NonExact) o;
    return eq(c, __2.c) && eq(canBeNull, __2.canBeNull);
}

  public int hashCode() {
    int h = 1445514322;
    h = boostHashCombine(h, _hashCode(c));
    h = boostHashCombine(h, _hashCode(canBeNull));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {c, canBeNull}; }

    Class javaClass() { return c; }
    boolean javaClassIsExact() { return false; }
    boolean canBeNull() { return canBeNull; }
  }
  
  static class KnownValue extends LASValueDescriptor implements IFieldsToList{
  Object value;
  KnownValue() {}
  KnownValue(Object value) {
  this.value = value;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + value + ")"; }

public boolean equals(Object o) {
if (!(o instanceof KnownValue)) return false;
    KnownValue __3 =  (KnownValue) o;
    return eq(value, __3.value);
}

  public int hashCode() {
    int h = -1456305138;
    h = boostHashCombine(h, _hashCode(value));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {value}; }

    boolean knownValue() { return true; }
    Object value() { return value; }
  
    Class javaClass() { return value == null ? null : value.getClass(); }
    boolean javaClassIsExact() { return value != null; }
    boolean canBeNull() { return value == null; }
  }
  
  // Indicates an exception can be thrown instead of returning a value.
  // Probably we don't need this.
  static class WillFail extends LASValueDescriptor {
    boolean canFail() { return true; }
    boolean willFail() { return true; }
  }
  
  static LASValueDescriptor fromClass(Class c) {
    return new NonExact(c, true);
  }
}
static class TokenRangeWithSrc extends TokenRange {
  List<String> tok;
  
  TokenRangeWithSrc() {}
  TokenRangeWithSrc(List<String> tok, int start) {
  this.start = start; end = start; }
  TokenRangeWithSrc(List<String> tok, int start, int end) {
  this.end = end;
  this.start = start;}
  TokenRangeWithSrc(ListAndIndex<String> startPtr, ListAndIndex<String> endPtr) {
    assertSame(tok = startPtr.list(), endPtr.list());
    start = startPtr.idx();
    end = endPtr.idx();
  }
  
  ListAndIndex<String> startPtr() { return new ListAndIndex(tok, start); }
  ListAndIndex<String> endPtr() { return new ListAndIndex(tok, end); }
  
  LineAndColumn startLineAndCol() { return tokenToLineAndColumn(startPtr()); }
  LineAndColumn endLineAndCol() { return tokenToLineAndColumn(endPtr()); }
  
  public String toString() {
    var start = startLineAndCol();
    if (eq(start, end)) return str(start);
    return start + " to " + endLineAndCol();
  }
}
static abstract class VF2<A, B> {
  abstract void get(A a, B b);
}
static class ConceptWithGlobalID extends Concept implements IHasGlobalID {
  
GlobalID globalID =
  
   aGlobalIDObjUnlessLoading(); 

public GlobalID globalID() { return globalID; }
public String globalIDStr() { return strOrNull(globalID); }

String prependGlobalID(String s) { return globalID == null ? s : "[" + globalID + "] " + s; }
}
static class LASCompileResult {
   final public LASCompileResult setScript(String script){ return script(script); }
public LASCompileResult script(String script) { this.script = script; return this; }  final public String getScript(){ return script(); }
public String script() { return script; }
 String script;
   final public LASCompileResult setParser(GazelleV_LeftArrowScriptParser parser){ return parser(parser); }
public LASCompileResult parser(GazelleV_LeftArrowScriptParser parser) { this.parser = parser; return this; }  final public GazelleV_LeftArrowScriptParser getParser(){ return parser(); }
public GazelleV_LeftArrowScriptParser parser() { return parser; }
 GazelleV_LeftArrowScriptParser parser;
   final public LASCompileResult setCompileError(Throwable compileError){ return compileError(compileError); }
public LASCompileResult compileError(Throwable compileError) { this.compileError = compileError; return this; }  final public Throwable getCompileError(){ return compileError(); }
public Throwable compileError() { return compileError; }
 Throwable compileError;
  /*settable broken*/ GazelleV_LeftArrowScript.Script parsedScript;

  public String toString() {
    return compileError != null ? exceptionToStringShorter_dontDropOuterExceptions(compileError) : "Compiled OK";
  }
  
  boolean runnable() { return parsedScript != null; }

  void compile() {  
    try {
      parsedScript = parser.parse(script);
      print(parsedScript);
    } catch (Throwable e) {
      print(e);
      compileError(e);
    }
  }
  
  GazelleV_LeftArrowScript.Script parsedScript() { return parsedScript; }
  
  GazelleV_LeftArrowScript.Script parsedScriptMandatory() {
    if (compileError != null) throw fail(compileError);
    return parsedScript;
  }
}
// the lockFile must be a file separate from any data files.
// It is created & deleted by this class, and will always have
// size 0.
static class FileBasedLock implements AutoCloseable {
  File lockFile;
  double timeout = 60.0; // in seconds. refresh happens twice as often
  boolean verbose = false;
  boolean haveLock = false;
  java.util.Timer touchTimer;
  
  FileBasedLock() {}
  FileBasedLock(File lockFile) {
  this.lockFile = lockFile;}
  FileBasedLock(File lockFile, double timeout) {
  this.timeout = timeout;
  this.lockFile = lockFile;}
  
  // returns true iff lock was acquired (or kept)
  synchronized boolean tryToLock() {
    if (haveLock) return true;
    
    if (fileExists(lockFile)) {
      double age = fileAgeInSeconds(lockFile);
      double remaining = timeout-age;
      print("Lock file age: " + lockFile + ": " + iround(age) + " s"
        + (remaining <= 0 ? " - old, deleting" : "- please start again in " + nSeconds(iceil(remaining))));
        
      if (remaining <= 0) {
        print("Deleting old lock file (program crashed?): " + lockFile + " (age: " + iround(age) + " seconds)");
        deleteFile(lockFile);
      }
    }
    
    try {
      mkdirsForFile(lockFile);
      java.nio.file.Files.createFile(toPath(lockFile));
      acquired();
      return true;
    } catch (Throwable e) {
      printExceptionShort("Can't lock", e);
      return false;
    }
  }
  
  private void acquired() {
    haveLock = true;
    startTouchTimer();
  }
  
  void forceLock() { try {
    print("Force-locking " + lockFile);
    touchFile(lockFile); // make or touch file
    acquired();
  } catch (Exception __e) { throw rethrow(__e); } }
  
  String lockError() {
    return "Couldn't aquire lock file: " + lockFile;
  }
  
  void lockOrFail() {
    if (!tryToLock())
      throw fail(lockError());
  }
  
  synchronized void startTouchTimer() {
    if (touchTimer != null) return;
    double interval = timeout/2;
    touchTimer = doEvery(interval, new Runnable() {  public void run() { try {  doTouch(); 
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "doTouch();"; }});
    if (verbose) print("Touch timer started for " + lockFile +  " (" + interval + "s)");
  }

  synchronized void doTouch() { try {
    if (haveLock) {
      if (verbose) print("Touching lock file: " + lockFile);
      touchExistingFile(lockFile);
    }
  } catch (Throwable __e) { printStackTrace(__e); }}
     
  public synchronized void close() { try {
    { cleanUp(touchTimer); touchTimer = null; }
    if (haveLock) {
      haveLock = false;
      if (verbose) print("Deleting lock file: " + lockFile);
      deleteFile(lockFile);
    }
  } catch (Throwable __e) { printStackTrace(__e); }}
  
  synchronized void _simulateCrash() {
    { cleanUp(touchTimer); touchTimer = null; }
  }
  
  void deleteOnExit() {
    if (haveLock)
      lockFile.deleteOnExit();
  }
}
static class WithTimestamp<A> extends Var<A> {
  long timestamp;
  
  final Timestamp timestamp(){ return getTimestamp(); }
final Timestamp timeStamp(){ return getTimestamp(); }
Timestamp getTimestamp() { return timestamp == 0 ? null : new Timestamp(timestamp); }
  
  WithTimestamp() {}
  WithTimestamp(long timestamp, A value) { super(value);
  this.timestamp = timestamp; }
  WithTimestamp(A value) { this(now(), value); }
  
  Timestamp timestampObj() { return timestamp == 0 ? null : new Timestamp(timestamp); }
  
  public String toString() { return toStringWithTimestamp(super.toString()); }
  String toStringQuoted() { return toStringWithTimestamp(quote(super.toString())); }
  
  String toStringWithTimestamp(String s) { return s + " at " + localDateWithSeconds(timestamp); }
  
  boolean olderThanMinutes(double minutes) { return elapsedMinutes_timestamp(timestamp) >= minutes; }
  boolean olderThanSeconds(double seconds) { return elapsedSeconds_timestamp(timestamp) >= seconds; }
}
interface IStringifier<A> {
  public String toString(A o);
}
abstract static class Surface extends JPanel implements IMeta {
  public boolean clearSurface = true;
  private boolean clearOnce = false;
  
  
// Meta - a "minimal" approach to adding meta-level to Java objects
// (implementing the interface IMeta)

// We allocate one extra field for each Java object to make it
// reasoning-compatible (reasoning-compatible = extensible with
// fields of any name at runtime).
//
// We couldn't go for 0 extra fields (meta values must be linked
// directly from the object) and there are no half fields in
// Java... so there you go.
//
// Also, if you don't use any meta data, you are probably not
// reasoning about anything. The point of reasoning in JavaX is
// to attach information to objects directly used in the program.

// Possible information contained in the meta field:
//   Origin, destination, security level, sender, cost center,
//   purpose, list of reifications, ...

// So here it is. THE FIELD YOU HAVE BEEN WAITING FOR!

// [We also have IMeta to retrofit foreign classes (rare but
// probably useful).]

//////////////////////
// The "meta" field //
//////////////////////

// Generic meta value of any kind, but the typical case is it's a
// Map with extra field values for the object etc.
// "meta" is volatile to avoid synchronization; but you can also synchronize on
// _tempMetaMutex() which is usually the object itself. Collections
// and maps are exempt from using the collections's monitor as the meta
// mutex because their monitor tends to be held for long operations
// (e.g. cloneList). For those we use a substantially more complex
// algorithm using a weakMap. Probably overkill. I may reconsider.

volatile Object meta;

// The meta field is not transient, thus by default it will be
// persisted like anything else unless you customize your object
// to suppress or modulate this.

// ...and the interface methods

public void _setMeta(Object meta) { this.meta = meta; }
public Object _getMeta() { return meta; }

// MOST functions are implemented in IMeta (default implementations)

// Scaffolding convenience functions

final boolean scaffolding(){ return scaffoldingEnabled(); }
boolean scaffoldingEnabled() { return main.scaffoldingEnabled(this); }
boolean scaffoldingEnabled(Object o) { return main.scaffoldingEnabled(o); }
Surface() {
    setDoubleBuffered(false);
  }

  Graphics2D createGraphics2D(int width, int height, Graphics g) {
    Graphics2D g2 = (Graphics2D) g;
    g2.setBackground(getBackground());
    if (clearSurface || clearOnce) {
      g2.clearRect(0, 0, width, height);
      clearOnce = false;
    }
    return g2;
  }

  public abstract void render(int w, int h, Graphics2D g);

  public void paintImmediately(int x,int y,int w, int h) {
    RepaintManager repaintManager = null;
    boolean save = true;
    if (!isDoubleBuffered()) {
      repaintManager = RepaintManager.currentManager(this);
      save = repaintManager.isDoubleBufferingEnabled();
      repaintManager.setDoubleBufferingEnabled(false);
    }
    super.paintImmediately(x, y, w, h);

    if (repaintManager != null)
      repaintManager.setDoubleBufferingEnabled(save);
  }

  public void paint(Graphics g) {
    Dimension d = getSize();
    Graphics2D g2 = createGraphics2D(d.width, d.height, g);
    render(d.width, d.height, g2);
    g2.dispose();
  }
}

static class ConceptWithChangeListeners extends Concept implements IHasChangeListeners, ChangeTriggerable {
  transient Set<Runnable> onChange;
public ConceptWithChangeListeners onChange(Runnable r) { onChange = createOrAddToSyncLinkedHashSet(onChange, r); return this; }
public ConceptWithChangeListeners removeChangeListener(Runnable r) { main.remove(onChange, r); return this; }
public void fireChange() {  pcallFAll(onChange); }
  
  void _onChange() { super._onChange();
    fireChange();
  }
  
  public void change() { super.change(); }
}
static class SimpleLiveValue<A> extends LiveValue<A> implements IVarWithNotify<A> {
  transient Set<Runnable> onChange;
public SimpleLiveValue<A> onChange(Runnable r) { onChange = createOrAddToSyncLinkedHashSet(onChange, r); return this; }
public SimpleLiveValue<A> removeChangeListener(Runnable r) { main.remove(onChange, r); return this; }
public void change() {  pcallFAll(onChange); }
  Class<A> type;
  volatile A value;
  
  SimpleLiveValue(Class<A> type) {
  this.type = type;}
  SimpleLiveValue(Class<A> type, A value) {
  this.value = value;
  this.type = type;}
  
  public Class<A> getType() { return type; }
  public A get() { return value; }

  void fireChanged() { change(); }
  public void set(A a) { if (neq(value, a)) { value = a; fireChanged(); } }
}
static class RunResultWithTimestamps<A> {
   final public RunResultWithTimestamps<A> setStarted(Timestamp started){ return started(started); }
public RunResultWithTimestamps<A> started(Timestamp started) { this.started = started; return this; }  final public Timestamp getStarted(){ return started(); }
public Timestamp started() { return started; }
 Timestamp started;
   final public RunResultWithTimestamps<A> setEnded(Timestamp ended){ return ended(ended); }
public RunResultWithTimestamps<A> ended(Timestamp ended) { this.ended = ended; return this; }  final public Timestamp getEnded(){ return ended(); }
public Timestamp ended() { return ended; }
 Timestamp ended;
   final public RunResultWithTimestamps<A> setResult(OKOrError<A> result){ return result(result); }
public RunResultWithTimestamps<A> result(OKOrError<A> result) { this.result = result; return this; }  final public OKOrError<A> getResult(){ return result(); }
public OKOrError<A> result() { return result; }
 OKOrError<A> result;
   final public RunResultWithTimestamps<A> setPrintOutput(CharSequence printOutput){ return printOutput(printOutput); }
public RunResultWithTimestamps<A> printOutput(CharSequence printOutput) { this.printOutput = printOutput; return this; }  final public CharSequence getPrintOutput(){ return printOutput(); }
public CharSequence printOutput() { return printOutput; }
 CharSequence printOutput;
  
  void run(IF0<A> f) {
    printOutput(hijackPrint_tee(new Runnable() {  public void run() { try { 
      started(tsNow());
      result(okOrError(f));
      ended(tsNow());
    
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "started(tsNow());\r\n      result(okOrError(f));\r\n      ended(tsNow());"; }}));
  }
}
static class FieldVar<A> extends VarWithNotify<A> {
  IHasChangeListeners containingObject;
  String fieldName;
  IF0<A> getter;
  IVF1<A> setter;

  FieldVar(IHasChangeListeners containingObject,
    String fieldName, IF0<A> getter, IVF1<A> setter) {
  this.setter = setter;
  this.getter = getter;
  this.fieldName = fieldName;
  this.containingObject = containingObject;
    containingObject.onChangeAndNow(() -> _updateFromObject());
  }
  
  void _updateFromObject() {
    set(getter.get());
  }
  
  public void fireChange() {
    setter.get(get());
  super.fireChange(); }
  
  public FieldVar<A> onChange(IVF1<A> r) {
    if (r != null) onChange(() -> r.get(get()));
    return this;
  }
}
static class SimpleLeftToRightParser extends Meta {
  String text;
  List<String> tok;
   final public ListAndIndex<String> getPtr(){ return ptr(); }
public ListAndIndex<String> ptr() { return ptr; }
 ListAndIndex<String> ptr;
  ListAndIndex<String> mainLoopPtr;
  String currentToken;
  boolean caseInsensitive = false;
  List warnings = new ArrayList();
  
  SimpleLeftToRightParser() {}
  SimpleLeftToRightParser(String text) {
  this.text = text;}
  SimpleLeftToRightParser(List<String> tok) {
  this.tok = tok;}
  
  final String token(){ return t(); }
String t() { return currentToken; }
  String token(int i) { return get(tok, ptr.idx()+i*2); }
  String tpp() { var t = t(); next(); return t; }
  String lastSpace() { return get(tok, ptr.idx()-1); }
  String nextSpace() { return get(tok, ptr.idx()+1); }
  
  boolean is(String t) { return eqOrEqic(caseInsensitive, currentToken, t); }
  boolean was(String t) { return eqOrEqic(caseInsensitive, token(-1), t); }
  
  
    boolean isInteger() { return isInteger(t()); }
    boolean isInteger(String s) { return main.isInteger(s); }
  

    boolean isIdentifier() { return isIdentifier(t()); }
    boolean isIdentifier(String s) { return main.isIdentifier(s); }
  

  
  final void consume(){ next(); }
void next() { if (!endOfText()) ptr(ptr.plus(2)); }
  
  void consume(String token) {
    if (!is(token))
      throw fail("Expected " + quote(token) + ", got " + quote(token()));
    consume();
  }
  
  void ptr(ListAndIndex<String> ptr) { this.ptr = ptr; fetch(); }
  int idx() { return ptr.idx(); }
  int lTok() { return l(tok); }

  final boolean endOfText(){ return atEnd(); }
boolean atEnd() { return ptr.atEnd(); }
    
  void fetch() { currentToken = ptr.get(); }
  
  boolean lineBreak() { return containsLineBreak(get(tok, ptr.idx()-1)); }
  boolean atEndOrLineBreak() { return atEnd() || lineBreak(); }
  
  void init() {
    if (tok == null) tok = javaTok(text);
    if (ptr == null) ptr(new ListAndIndex(tok, 1));
  }
  
  boolean mainLoop() {
    init();
    if (atEnd()) return false;
    if (eq(mainLoopPtr, ptr))
      throw fail("main loop didn't advance (current token: " + quote(token()) + ")");
    mainLoopPtr = ptr;
    return true;
  }
  
  void unknownToken() {
    warn("Unknown token: " + t());
  }
  
  void warn(String msg) {
    warnings.add(print(msg));
  }
  
  void next(int n) { 
    ptr(ptr.idx()+n*2);
  }
  
  // if i points to an N token, it is incremented
  void ptr(int i) {
    ptr(new ListAndIndex(tok, min(i | 1, l(tok))));
  }
  
  LineAndColumn lineAndColumn() {
    return tokenToLineAndColumn(ptr);
  }
  
  String consumeUntilSpaceOr(IF0<Boolean> pred) {
    int i = idx();
    do next(); while (!atEnd() && empty(lastSpace()) && !pred.get());
    return joinSubList(tok, i, idx()-1);
  }
  
  void setText(String text) {
    this.text = text;
    tok = null;
    ptr = null;
  }
}
interface G2Drawable {
  void drawOn(Graphics2D g);
  
  default void drawOn(BufferedImage img) {
    drawOn(img.createGraphics());
  }
}
// uses HashMap by default
static class MultiSet<A> implements IMultiSet<A> {
  Map<A, Integer> map = new HashMap();
  int size; // now maintaining a size counter
  
  MultiSet(boolean useTreeMap) {
    if (useTreeMap) map = new TreeMap();
  }
  MultiSet(TreeMap map) {
  this.map = map;}
  
  MultiSet() {}
  MultiSet(Iterable<A> c) { addAll(c); }
  MultiSet(MultiSet<A> ms) { synchronized(ms) {
    for (A a : ms.keySet()) add(a, ms.get(a));
  }}
  
  // returns new count
  public synchronized int add(A key) { return add(key, 1); }
  
  synchronized void addAll(Iterable<A> c) {
    if (c != null) for (A a : c) add(a);
  }

  synchronized void addAll(MultiSet<A> ms) {
    for (A a : ms.keySet()) add(a, ms.get(a));
  }
  
  synchronized int add(A key, int count) {
    if (count <= 0) return 0; // don't calculate return value in this case
    size += count;
    Integer i = map.get(key);
    map.put(key, i != null ? (count += i) : count);
    return count;
  }

  synchronized void put(A key, int count) {
    int oldCount = get(key);
    if (count == oldCount) return;
    size += count-oldCount;
    if (count != 0)
      map.put(key, count);
    else
      map.remove(key);
  }

  public synchronized int get(A key) {
    Integer i = map.get(key);
    return i != null ? i : 0;
  }
  
  synchronized boolean contains(A key) {
    return map.containsKey(key);
  }

  synchronized void remove(A key) {
    Integer i = map.get(key);
    if (i != null) {
      --size;
      if (i > 1)
        map.put(key, i - 1);
      else
        map.remove(key);
    }
  }

  synchronized List<A> topTen() { return getTopTen(); }
  
  synchronized List<A> getTopTen() { return getTopTen(10); }
  synchronized List<A> getTopTen(int maxSize) {
    List<A> list = getSortedListDescending();
    return list.size() > maxSize ? list.subList(0, maxSize) : list;
  }
  
  synchronized List<A> highestFirst() {
    return getSortedListDescending();
  }

  synchronized List<A> lowestFirst() {
    return reversedList(getSortedListDescending());
  }

  synchronized List<A> getSortedListDescending() {
    List<A> list = new ArrayList<A>(map.keySet());
    Collections.sort(list, new Comparator<A>() {
      public int compare(A a, A b) {
        return map.get(b).compareTo(map.get(a));
      }
    });
    return list;
  }

  synchronized int getNumberOfUniqueElements() {
    return map.size();
  }
  
  synchronized int uniqueSize() {
    return map.size();
  }

  synchronized Set<A> asSet() {
    return map.keySet();
  }

  synchronized NavigableSet<A> navigableSet() {
    return navigableKeys((NavigableMap) map);
  }

  synchronized Set<A> keySet() {
    return map.keySet();
  }
  
  synchronized A getMostPopularEntry() {
    int max = 0;
    A a = null;
    for (Map.Entry<A,Integer> entry : map.entrySet()) {
      if (entry.getValue() > max) {
        max = entry.getValue();
        a = entry.getKey();
      }
    }
    return a;
  }

  synchronized void removeAll(A key) {
    size -= get(key);
    map.remove(key);
  }

  synchronized int size() {
    return size;
  }

  synchronized MultiSet<A> mergeWith(MultiSet<A> set) {
    MultiSet<A> result = new MultiSet<A>();
    for (A a : set.asSet()) {
      result.add(a, set.get(a));
    }
    return result;
  }
  
  synchronized boolean isEmpty() {
    return map.isEmpty();
  }
  
  synchronized public String toString() { // hmm. sync this?
    return str(map);
  }
  
  synchronized void clear() {
    map.clear();
    size = 0;
  }
  
  final  Map<A, Integer> toMap(){ return asMap(); }
synchronized Map<A, Integer> asMap() {
    return cloneMap(map);
  }
}
static class ImageSurfaceSelector extends ImageSurfaceMouseHandler {
  Point startingPoint;
  boolean enabled = true;
  static boolean verbose = false;

  ImageSurfaceSelector() {}
  ImageSurfaceSelector(ImageSurface is) {
    if (containsInstance(is.tools, ImageSurfaceSelector.class)) return;
    this.register(is);
  }

  public void mousePressed(MouseEvent evt) {
    if (verbose) print("mousePressed");
    if (evt.getButton() != MouseEvent.BUTTON1) return;
    if (enabled)
      startingPoint = getPoint(evt);
  }

  public void mouseDragged(MouseEvent e) {
    if (verbose) print("mouseDragged");
    if (startingPoint != null) {
      Point endPoint = getPoint(e);
      Rectangle r = new Rectangle(startingPoint, new Dimension(endPoint.x-startingPoint.x+1, endPoint.y-startingPoint.y+1));
      normalize(r);
      r.width = min(r.width, is.getImage().getWidth()-r.x);
      r.height = min(r.height, is.getImage().getHeight()-r.y);
      is.setSelection(r);
    }
    if (verbose) print("mouseDragged done");
  }

  public static void normalize(Rectangle r) {
    if (r.width < 0) {
      r.x += r.width;
      r.width = -r.width;
    }
    if (r.height < 0) {
      r.y += r.height;
      r.height = -r.height;
    }
  }

  public void mouseReleased(MouseEvent e) {
    if (verbose) print("mouseReleased");
    mouseDragged(e);
    if (getPoint(e).equals(startingPoint))
      is.setSelection((Rectangle) null);
    startingPoint = null;
  }
}
abstract static class ImageSurfaceMouseHandler extends MouseAdapter implements AutoCloseable {
  ImageSurface is;

  void register(ImageSurface is) {
    this.is = is;
    is.tools.add(this);
    is.addMouseListener(this);
    is.addMouseMotionListener(this);
  }
  
  public void close() { try {
    if (is == null) return;
    is.tools.remove(this);
    is.removeMouseListener(this);
    is.removeMouseMotionListener(this);
    is = null;
  } catch (Exception __e) { throw rethrow(__e); } }
  
  Pt getPt(MouseEvent e) {
    return toPt(getPoint(e));
  }
  
  Point getPoint(MouseEvent e) {
    return new Point((int) (e.getX()/is.getZoomX()), (int) (e.getY()/is.getZoomY()));
  }
  
  ImageSurface getImageSurface() { return is; }
}
enum G22ScriptMode {
  edit, saved, autoRunnable
}
// script that recognizes images or performs some kind of analysis on an image
static class G22Analyzer extends G22LeftArrowScript {
  Ref<G22GalleryImage> exampleImage = new Ref();
  
  @Override GazelleV_LeftArrowScriptParser makeParser() {
    var parser = super.makeParser();
    parser.addVar("image", BufferedImage.class, false);
    parser.addVar("context", G22AnalysisContext.class, false);
    return parser;
  }
  
  class CompiledAnalyzer extends LASCompileResult {
     final public CompiledAnalyzer setTimeout(double timeout){ return timeout(timeout); }
public CompiledAnalyzer timeout(double timeout) { this.timeout = timeout; return this; }  final public double getTimeout(){ return timeout(); }
public double timeout() { return timeout; }
 double timeout = 10;
    
    Object get(BufferedImage image) {
      G22AnalysisContext context = new G22AnalysisContext(g22utils(_concepts()), image);
      return get(context);
    }
    
    Object get(G22AnalysisContext context) {
      if (context.analyzerResults.containsKey(analyzer())) {
        Object value = context.analyzerResults.get(analyzer());
        if (value == G22AnalysisContext.calculating)
          throw fail("Recursive call to analyzer " + this);
        return ((OKOrError) value).getMandatory();
      }
      context.analyzerResults.put(analyzer(), G22AnalysisContext.calculating);
      
      var result = okOrError(() -> {
        if (parsedScript == null) rethrow(compileError);
        var image = context.image;
        if (image == null) throw fail("Need image");
        
        return evalWithTimeoutOrFail(timeout, () -> {
          VarContext ctx = new VarContext();
          ctx.set("image", image);
          ctx.set("context", context);
          return parsedScript.get(ctx);
        });
      });
      context.analyzerResults.put(analyzer(), result);
      return result.getMandatory();
    }
  }
  
  G22Analyzer analyzer() { return this; }
  
  LASCompileResult newCompileResult() { return new CompiledAnalyzer(); }
  
  CompiledAnalyzer compileForAutoRun() {
    return (CompiledAnalyzer) super.compileForAutoRun();
  }
}
static class PersistableOKOrError<A> implements IF0<A> {
  A value;
   final public PersistableThrowable getError(){ return error(); }
public PersistableThrowable error() { return error; }
 PersistableThrowable error;
  
  PersistableOKOrError() {}
  PersistableOKOrError(A value) {
  this.value = value;}
  PersistableOKOrError(boolean dummy, PersistableThrowable error) {
  this.error = error; assertNotNull(error); }
  
  final boolean isOK(){ return ok(); }
boolean ok() { return error == null; }
  
  public String toString() {
    return ok()
      ? /*"OK: " +*/ str(value)
      : "Error: " + str(error);
  }
  
  public A get() { return !ok() ? null : value; }
  
  public A getMandatory() { if (!ok()) throw error.asRuntimeException(); return value; }
  
  static <A> PersistableOKOrError<A> ok(A a) { return new PersistableOKOrError(a); }
  static PersistableOKOrError error(PersistableThrowable error) { return new PersistableOKOrError(false, error); }
}
// could almost add IVar<A> here - but void set() and bool set() are incompatible in some places
static interface IRef<A> extends IF0<A> {
  // called by the referencee to atomically replace itself
  // Passing oldValue to avoid race conditions
  public default void replaceValue(A oldValue, A newValue) {}
}
static interface IConceptCounter {
  Class<? extends Concept> conceptClass();
  int countConcepts();
  Collection<Concept> allConcepts();
}
static class RemoteDB implements AutoCloseable {
  DialogIO db;
  String name;
  
  // s = bot name or snippet ID
  RemoteDB(String s) {
    this(s, false);
  }
  
  RemoteDB(String s, boolean autoStart) {
    name = s;
    if (isSnippetID(s)) name = dbBotName(s);
    db = findBot(name);
    if (db == null)
      if (autoStart) {
        nohupJavax(fsI(s));
        waitForBotStartUp(name);
        assertNotNull("Weird problem", db = findBot(s));
      } else
        throw fail("DB " + s + " not running");
  }

  boolean functional() { return db != null; } // now always true
  
  List<RC> list() { return adopt((List<RC>) rpc(db, "xlist")); }
  List<RC> list(String className) { return adopt((List<RC>) rpc(db, "xlist", className)); }
  List<RC> xlist() { return list(); }
  List<RC> xlist(String className) { return list(className); }
  
  // adopt is an internal method
  List<RC> adopt(List<RC> l) {
    if (l != null) for (RC rc : l) adopt(rc);
    return l;
  }
  
  RC adopt(RC rc) { if (rc != null) rc.db = this; return rc; }
  
  Object adopt(Object o) {
    if (o instanceof RC) return adopt((RC) o);
    return o;
  }
  
  String xclass(RC o) {
    return (String) rpc(db, "xclass", o);
  }
  
  Object xget(RC o, String field) {
    return adopt(rpc(db, "xget", o, field));
  }
  
  String xS(RC o, String field) {
    return (String) xget(o, field);
  }
  
  RC xgetref(RC o, String field) {
    return adopt((RC) xget(o, field));
  }
  
  void xset(RC o, String field, Object value) {
    rpc(db, "xset", o, field, value);
  }
  
  RC uniq(String className) {
    RC ref = first(list(className));
    if (ref == null)
      ref = xnew(className);
    return ref;
  }
  RC xuniq(String className) { return uniq(className); }
  
  RC xnew(String className, Object... values) {
    return adopt((RC) rpc(db, "xnew", className, values));
  }
  
  void xdelete(RC o) {
    rpc(db, "xdelete", o);
  }
  
  void xdelete(List<RC> l) {
    rpc(db, "xdelete", l);
  }

  public void close() {
    _close(db);
  }
  
  String fullgrab() { return (String) rpc(db, "xfullgrab"); }
  String xfullgrab() { return fullgrab(); }
  
  void xshutdown() { rpc(db, "xshutdown"); }
  
  long xchangeCount() { return (long) rpc(db, "xchangeCount"); }
  int xcount() { return (int) rpc(db, "xcount"); }

  void reconnect() {
    close();
    db = findBot(name);
  }
  
  RC rc(long id) { return new RC(this, id); }
}
/** this class is fully thread-safe */
static class Flag implements Runnable {
  private boolean up = false;

  /** returns true if flag was down before (i.e. flag was actually raised right now) */
  public synchronized boolean raise() {
    if (!up) {
      up = true;
      notifyAll();
      return true;
    } else
      return false;
  }

  public synchronized void waitUntilUp() {
    while (!up) {
      try {
        wait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  public boolean waitUntilUp(double timeout) {
    return waitUntilUp(toMS(timeout));
  }
  
  public synchronized boolean waitUntilUp(long timeout) {
    if (!up) {
      try {
        wait(timeout);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
    return isUp();
  }

  public synchronized boolean isUp() {
    return up;
  }
  
  boolean get() { return isUp(); }

  public String toString() {
    return isUp() ? "up" : "down";
  }

  // currently does a semi-active wait with latency = 50 ms
  public void waitForThisOr(Flag otherFlag) { try {
    while (!isUp() && !otherFlag.isUp())
      Thread.sleep(50);
  } catch (Exception __e) { throw rethrow(__e); } }
  
  public void run() { raise(); }
}
static class BoolVar {
  boolean a = false; // you can access this directly if you use one thread
  
  BoolVar() {}
  BoolVar(boolean a) {
  this.a = a;}
  
  public synchronized void set() { set(true); }
public synchronized void set(boolean v) {
    if (v != a) {
      a = v;
      notifyAll();
    }
  }
  
  public synchronized boolean get() { return a; }
  //public synchronized bool has() { true; }
  final public void unset(){ clear(); }
public void clear() { set(false); }
  
  boolean waitUntilTrue () { return waitForValue(true); }
  boolean waitUntilFalse() { return waitForValue(false); }
  
  synchronized boolean waitForValue(boolean wantedValue) { try {
    while (a != wantedValue)
      wait();
    return a;
  } catch (Exception __e) { throw rethrow(__e); } }
}
interface ChangeTriggerable {
  public void change();
}
static class ClearForAutoRun<A> extends Var<A> {
  ClearForAutoRun() {}
  ClearForAutoRun(A code) { super(code); }
}
static class Stringifier_ToString implements IStringifier {
  public String toString(Object o) { return str(o); }
}
interface IPartialStringifier<A> {
  // return null if not handled
  public String toStringOpt(A o);
}
static class G22TypeDesc {
  String exactClassName;
  Set<String> implementedClassNames; // also, superclasses
}
static List<String> _stickyLibs_1400546 = ll("#1400546", "#1400547", "#1400548");

 // RSyntaxTextArea
 // AutoComplete
 // RSTAUI




















static class MetaWithChangeListeners extends Meta implements IHasChangeListeners, ChangeTriggerable {
  transient Set<Runnable> onChange;
public MetaWithChangeListeners onChange(Runnable r) { onChange = createOrAddToSyncLinkedHashSet(onChange, r); return this; }
public MetaWithChangeListeners removeChangeListener(Runnable r) { main.remove(onChange, r); return this; }
public void change() {  pcallFAll(onChange); }
}
interface IHasTokenRangeWithSrc {
  void setTokenRangeWithSrc(TokenRangeWithSrc src);
  TokenRangeWithSrc tokenRangeWithSrc();
}
// See GazelleV_LeftArrowScriptParser

// TODO: decide whether we allow calling methods/getting fields on
// a null reference (just returning null), or whether we throw a
// NullPointerException. Currently we just return null. Probably
// that's pretty cool. Null propagation as a default, just like in
// JavaX. Just more automatic!

static class GazelleV_LeftArrowScript {
  // Base = any script element with a reference to its source code
  
  abstract static class Base implements IHasTokenRangeWithSrc {
    TokenRangeWithSrc src;
    
    public void setTokenRangeWithSrc(TokenRangeWithSrc src) { this.src = src; }
    public TokenRangeWithSrc tokenRangeWithSrc() { return src; }
    
    RuntimeException rethrowWithSrc(Throwable e) {
      if (src != null)
        throw rethrowAndAppendToMessage(e, squareBracketed(str(src)));
      else
        throw rethrow(e);
    }
  }
  
  // Evaluable = a script element that can be evaluated
  
  interface Evaluable extends IF0 {
    public default Object get() { return get(new VarContext()); }
public Object get(VarContext ctx);
    public default LASValueDescriptor returnType() { return null; }
    public default Evaluable optimize() { return this; }
  }
  
  // Base + Evaluable + explicitly stored return type
  
  abstract static class BaseEvaluable extends Base implements Evaluable {
     final public BaseEvaluable setReturnType(LASValueDescriptor returnType){ return returnType(returnType); }
public BaseEvaluable returnType(LASValueDescriptor returnType) { this.returnType = returnType; return this; }  final public LASValueDescriptor getReturnType(){ return returnType(); }
public LASValueDescriptor returnType() { return returnType; }
 LASValueDescriptor returnType;
  }
  
  /*interface Cmd {
    // returns true if a return was issued
    public bool run(VarContext ctx default new);
  }*/
  
  static AtomicLong scriptIDCounter = new AtomicLong();
  static long scriptID() { return incAtomicLong(scriptIDCounter); }
  
  static class Script extends Base implements Evaluable {
    long id = scriptID(); // just for printing
    Map<String, FunctionDef> functionDefs;
    Evaluable[] steps;
    
    public Object get(VarContext ctx) {
      Object result = null;
      for (var step : steps) {
        ping();
        result = step.get(ctx);
        
        // exiting from anything?
        
        var exiting = ctx.exitFromScript;
        if (exiting != null) {
           
            
          // we're the exit point
          if (exiting == this)
            ctx.exitFromScript = null;
            
          // exit further
          break;
        }
      }
      return result;
    }
    
    String toStringLong() { return pnlToLines(steps); }
    public String toString() { return "Script " + n2(id); }
    
    FunctionDef getFunction(String name) { return mapGet(functionDefs, name); }
  } // end of Script
  
  static class FunctionDef extends Base implements IFieldsToList{
  String name;
  List<String> args;
  Evaluable body;
  FunctionDef() {}
  FunctionDef(String name, List<String> args, Evaluable body) {
  this.body = body;
  this.args = args;
  this.name = name;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + name + ", " + args + ", " + body + ")"; }public Object[] _fieldsToList() { return new Object[] {name, args, body}; }

    public Object call(VarContext ctx, Object... args) {
      var ctx2 = new VarContext(ctx);
      int n = min(l(args), l(this.args));
      for (int i = 0; i < n; i++)
        ctx2.put(this.args.get(i), args[i]);
       
      return body.get(ctx2);
    }
  }
  
  static class Assignment extends Base implements Evaluable , IFieldsToList{
  String var;
  Evaluable expression;
  Assignment() {}
  Assignment(String var, Evaluable expression) {
  this.expression = expression;
  this.var = var;}public Object[] _fieldsToList() { return new Object[] {var, expression}; }

    public Object get(VarContext ctx) {
      Object o = expression.get(ctx);
      ctx.set(var, o);
      return o;
    }
    
    public String toString() { return var + " <- " + expression; }
  }
  
  static class NewObject extends Base implements Evaluable {
  NewObject() {}
    Class c;
    List<Evaluable> args;
    
    NewObject(Class c) {
  this.c = c;}
    NewObject(Class c, List<Evaluable> args) {
  this.args = args;
  this.c = c;}
    
    public Object get(VarContext ctx) {
      return callConstructor(c, mapToArray(args, arg -> arg.get(ctx)));
    }
    
    public String toString() { return "new " + formatFunctionCall(className(c), args); }
  }
  
  static class CallFunction extends Base implements Evaluable , IFieldsToList{
  FunctionDef f;
  List<Evaluable> args;
  CallFunction() {}
  CallFunction(FunctionDef f, List<Evaluable> args) {
  this.args = args;
  this.f = f;}public Object[] _fieldsToList() { return new Object[] {f, args}; }

    public Object get(VarContext ctx) {
      //ping();
      return f.call(ctx, mapToArray(args, a -> a.get(ctx)));
    }
    
    public String toString() { return formatFunctionCall(f.name, args); }
  }
  
  static class GetVar extends BaseEvaluable implements IFieldsToList{
  String var;
  GetVar() {}
  GetVar(String var) {
  this.var = var;}public Object[] _fieldsToList() { return new Object[] {var}; }

    public Object get(VarContext ctx) {
      return ctx.get(var);
    }
    
    public String toString() { return var; }
  }
  
  static class Const extends Base implements Evaluable , IFieldsToList{
  Object value;
  Const() {}
  Const(Object value) {
  this.value = value;}public Object[] _fieldsToList() { return new Object[] {value}; }

    public Object get(VarContext ctx) {
      return value;
    }
    
    public String toString() { return strOrClassName(value); }
    
    public LASValueDescriptor returnType() {
      return new LASValueDescriptor.KnownValue(value);
    }
  }
  
  static class GetStaticField extends Base implements Evaluable , IFieldsToList{
  Field field;
  GetStaticField() {}
  GetStaticField(Field field) {
  this.field = field;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + field + ")"; }public Object[] _fieldsToList() { return new Object[] {field}; }

    public Object get(VarContext ctx) { try {
      return field.get(null);
    } catch (Exception __e) { throw rethrow(__e); } }
  }
  
  static class CallMethodOrGetField extends Base implements Evaluable , IFieldsToList{
  Evaluable target;
  String name;
  CallMethodOrGetField() {}
  CallMethodOrGetField(Evaluable target, String name) {
  this.name = name;
  this.target = target;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + target + ", " + name + ")"; }public Object[] _fieldsToList() { return new Object[] {target, name}; }

    public Object get(VarContext ctx) {
      try {
        Object object = target.get(ctx);
        if (object == null)
          return null; // throw new NullPointerException();
          
        // could optimize more for sure
        if (canCallWithVarargs(object, name))
          return call(object, name);
          
        // TODO: better error message when neither field nor method found
        return _get(object, name);
      } catch (Throwable e) {
        throw rethrowWithSrc(e);
      }
    }
  }
  
  static class GetVarContext extends Base implements Evaluable {
    public Object get(VarContext ctx) { return ctx; }
  }
  
  static class ThrowMethodNotFoundException extends Base implements Evaluable , IFieldsToList{
  CallMethod instruction;
  ThrowMethodNotFoundException() {}
  ThrowMethodNotFoundException(CallMethod instruction) {
  this.instruction = instruction;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + instruction + ")"; }public Object[] _fieldsToList() { return new Object[] {instruction}; }

    public Object get(VarContext ctx) {
      throw fail("Method not found: " + instruction);
    }
  }
  
  static class ThrowNullPointerException extends Base implements Evaluable , IFieldsToList{
  CallMethod instruction;
  ThrowNullPointerException() {}
  ThrowNullPointerException(CallMethod instruction) {
  this.instruction = instruction;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + instruction + ")"; }public Object[] _fieldsToList() { return new Object[] {instruction}; }

    public Object get(VarContext ctx) {
      throw fail("Null pointer exception: " + instruction);
    }
  }
  
  static class CallMethod extends Base implements Evaluable , IFieldsToList{
  Evaluable target;
  String methodName;
  List<Evaluable> args;
  CallMethod() {}
  CallMethod(Evaluable target, String methodName, List<Evaluable> args) {
  this.args = args;
  this.methodName = methodName;
  this.target = target;}public Object[] _fieldsToList() { return new Object[] {target, methodName, args}; }

    public Object get(VarContext ctx) {
      return call(target.get(ctx), methodName, mapToArray(args, arg -> arg.get(ctx)));
    }
    
    public String toString() { return target + "." + formatFunctionCall(methodName, args); }
    
    public Evaluable optimize() {
      var targetType = target.returnType();
      if (targetType.knownValue()) {
        Object o = targetType.value();
        if (o == null) return new ThrowNullPointerException(this);
        
        Class[] argTypes = new Class[l(args)];
        for (int i = 0; i < l(args); i++) {
          var type = args.get(i).returnType();
          if (!type.javaClassIsExact())
            return this;
          argTypes[i] = type.javaClass();
        }
        
        // TODO: varargs
        var method = findMethod_precise_onTypes(o, methodName, argTypes);
        if (method == null) return new ThrowMethodNotFoundException(this);
        
        return new DirectMethodCallOnKnownTarget(o instanceof Class ? null : o, method, args);
      }
      
      return this;
    }
  }
  
  static class DirectMethodCallOnKnownTarget extends Base implements Evaluable , IFieldsToList{
  Object target;
  Method method;
  List<Evaluable> args;
  DirectMethodCallOnKnownTarget() {}
  DirectMethodCallOnKnownTarget(Object target, Method method, List<Evaluable> args) {
  this.args = args;
  this.method = method;
  this.target = target;}public Object[] _fieldsToList() { return new Object[] {target, method, args}; }

    public Object get(VarContext ctx) {
      return invokeMethod(method, target, mapToArray(args, arg -> arg.get(ctx)));
    }
    
    public String toString() { return (target == null ? "" : target + ".") + formatFunctionCall(str(method), args); }
    
    public LASValueDescriptor returnType() {
      return LASValueDescriptor.fromClass(method.getReturnType());
    }
  }
  
  static class While extends Base implements Evaluable , IFieldsToList{
  Evaluable condition;
  Evaluable body;
  While() {}
  While(Evaluable condition, Evaluable body) {
  this.body = body;
  this.condition = condition;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + condition + ", " + body + ")"; }public Object[] _fieldsToList() { return new Object[] {condition, body}; }

    public Object get(VarContext ctx) {
      while (ping() && (Boolean) condition.get(ctx)) {
        body.get(ctx);
      }
      
      // while loops don't return anything
      return null;
    }
  }
  
  static class ForEach extends Base implements Evaluable , IFieldsToList{
  Evaluable collection;
  String var;
  Evaluable body;
  ForEach() {}
  ForEach(Evaluable collection, String var, Evaluable body) {
  this.body = body;
  this.var = var;
  this.collection = collection;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + collection + ", " + var + ", " + body + ")"; }public Object[] _fieldsToList() { return new Object[] {collection, var, body}; }

    public Object get(VarContext ctx) {
      var coll = collection.get(ctx);
      Iterator iterator;
      List out = new ArrayList();
      if (coll instanceof Object[])
        for (var element : ((Object[]) coll)) {
          ping();
          ctx.set(var, element);
          out.add(body.get(ctx));
        }
      else if (coll instanceof Iterable) {
        for (var element : ((Iterable) coll)) {
          ping();
          ctx.set(var, element);
          out.add(body.get(ctx));
        }
      } else if (coll == null) {} // ok
      else
        throw fail("Not iterable: " + className(coll));

      ctx.unset(var);
      return out;
    }
  }
  
  static class IfThen extends Base implements Evaluable , IFieldsToList{
  Evaluable condition;
  Evaluable body;
  Evaluable elseBranch;
  IfThen() {}
  IfThen(Evaluable condition, Evaluable body, Evaluable elseBranch) {
  this.elseBranch = elseBranch;
  this.body = body;
  this.condition = condition;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + condition + ", " + body + ", " + elseBranch + ")"; }public Object[] _fieldsToList() { return new Object[] {condition, body, elseBranch}; }

    
    IfThen(Evaluable condition, Evaluable body) {
      this.condition = condition;
      this.body = body;
    }
    
    public Object get(VarContext ctx) {
      if ((Boolean) condition.get(ctx))
        return body.get(ctx);
      else if (elseBranch != null)
        return elseBranch.get(ctx);
      else
        return null;
    }
  }
  
  static class ReturnFromScript extends Base implements Evaluable , IFieldsToList{
  Script script;
  Evaluable value;
  ReturnFromScript() {}
  ReturnFromScript(Script script, Evaluable value) {
  this.value = value;
  this.script = script;}public Object[] _fieldsToList() { return new Object[] {script, value}; }

    public Object get(VarContext ctx) {
      Object result = value.get(ctx);
       
      ctx.exitFromScript(script);
      return result;
    }
    
    public String toString() {
      return formatFunctionCall("ReturnFromScript", script, value);
    }
  }
}


static class RGBImage implements MakesBufferedImage, IRGBImage {
  transient BufferedImage bufferedImage;
  int width, height;
  int[] pixels;

  RGBImage() {}

  RGBImage(BufferedImage image) {
    bufferedImage = image;
    width = image.getWidth();
    height = image.getHeight();
    pixels = new int[width*height];
    var gp = grabbableIntPixels_fastOrSlow(image);
    
    if (gp.scanlineStride == width && gp.offset == 0)
      pixels = gp.data;
    else {
      pixels = new int[width*height];
      int iIn = 0, iOut = 0;
      for (int y = 0; y < height; y++) {
        arrayCopy(gp.data, iIn, pixels, iOut, width);
        iIn += gp.scanlineStride;
        iOut += width;
      }
    }
    
    cleanPixels(); // set upper byte to 0
  }

  RGBImage(Dimension size, Color color) {
    this(size.width, size.height, color);
  }

  RGBImage(Dimension size, RGB color) {
    this(size.width, size.height, color);
  }

  private void cleanPixels() {
    var pixels = this.pixels;
    for (int i = 0; i < pixels.length; i++)
      pixels[i] &= 0xFFFFFF;
  }

  RGBImage(int width, int height, int[] pixels) {
    this.width = width;
    this.height = height;
    this.pixels = pixels;
  }

  RGBImage(int w, int h, RGB[] pixels) {
    this.width = w;
    this.height = h;
    this.pixels = asInts(pixels);
  }

  public static int[] asInts(RGB[] pixels) {
    int[] ints = new int[pixels.length];
    for (int i = 0; i < pixels.length; i++)
      ints[i] = pixels[i] == null ? 0 : pixels[i].getColor().getRGB();
    return ints;
  }

  public RGBImage(int w, int h) {
    this(w, h, Color.black);
  }
  
  RGBImage(int w, int h, RGB rgb) {
    this.width = w;
    this.height = h;
    this.pixels = new int[w*h];
    int col = rgb.asInt();
    if (col != 0)
      for (int i = 0; i < pixels.length; i++)
        pixels[i] = col;
  }

  RGBImage(RGBImage image) {
    this(image.width, image.height, copyPixels(image.pixels));
  }

  RGBImage(int width, int height, Color color) {
    this(width, height, new RGB(color));
  }

  RGBImage(MakesBufferedImage img) {
    this(toBufferedImage(img));
  }

  private static int[] copyPixels(int[] pixels) {
    int[] copy = new int[pixels.length];
    System.arraycopy(pixels, 0, copy, 0, pixels.length);
    return copy;
  }

  public int getIntPixel(int x, int y) {
    if (inRange(x, y))
      return pixels[y * width + x];
    else
      return 0xFFFFFF;
  }

  public static RGB asRGB(int packed) {
    int r = (packed >> 16) & 0xFF;
    int g = (packed >> 8) & 0xFF;
    int b = packed & 0xFF;
    return new RGB(r / 255f, g / 255f, b / 255f);
  }

  public RGB getRGB(int x, int y) {
    if (inRange(x, y))
      return asRGB(pixels[y * width + x]);
    else
      return new RGB(0xFFFFFF);
  }

  /** alias of getRGB - I kept typing getPixel instead of getRGB all the time, so I finally created it */
  RGB getPixel(int x, int y) {
    return getRGB(x, y);
  }
  
  RGB getPixel(Pt p) { return getPixel(p.x, p.y); }

  public int getWidth() { return width; }
  public int getHeight() { return height; }
  public int w() { return width; }
  public int h() { return height; }

  /** Attention: cached, i.e. does not change when image itself changes */
  /** @NotNull */
  public BufferedImage getBufferedImage() {
    if (bufferedImage == null) {
      bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
      //bufferedImage.setData(Raster.createRaster(new SampleModel()));
      for (int y = 0; y < height; y++)
        for (int x = 0; x < width; x++)
          bufferedImage.setRGB(x, y, pixels[y*width+x]);
    }
    return bufferedImage;
  }

  RGBImage clip(Rect r) {
    return r == null ? null : clip(r.getRectangle());
  }
  
  RGBImage clip(Rectangle r) {
    r = fixClipRect(r);
    if (r.x == 0 && r.y == 0 && r.width == width && r.height == height) return this;
    int[] newPixels;
    try {
      newPixels = new int[r.width*r.height];
    } catch (RuntimeException e) {
      System.out.println(r);
      throw e;
    }
    for (int y = 0; y < r.height; y++) {
      System.arraycopy(pixels, (y+r.y)*width+r.x, newPixels, y*r.width, r.width);
    }
    return new RGBImage(r.width, r.height, newPixels);
  }

  private Rectangle fixClipRect(Rectangle r) {
    r = r.intersection(new Rectangle(0, 0, width, height));
    if (r.isEmpty())
      r = new Rectangle(r.x, r.y, 0, 0);
    return r;
  }

  public int getInt(int x, int y) {
    return pixels[y * width + x];
  }

  public void save(File file) {
    saveImage(file, getBufferedImage());
  }

  public static RGBImage dummyImage() {
    return new RGBImage(1, 1, new int[] {0xFFFFFF});
  }

  public int[] getPixels() {
    return pixels;
  }
  
  void setPixel(int x, int y, int r, int g, int b) {
    if (x >= 0 && y >= 0 && x < width && y < height)
      pixels[y*width+x] = (limitToUByte(r) << 16) | (limitToUByte(g) << 8) | limitToUByte(b);
  }

  public void setPixel(int x, int y, RGB rgb) {
    if (x >= 0 && y >= 0 && x < width && y < height)
      pixels[y*width+x] = rgb.asInt();
  }

  final public void set(int x, int y, Color color){ setPixel(x, y, color); }
public void setPixel(int x, int y, Color color) {
    setPixel(x, y, new RGB(color));
  }
  
  void setInt(int x, int y, int rgb) {
    setPixel(x, y, rgb);
  }

  public void setPixel(int x, int y, int rgb) {
    if (x >= 0 && y >= 0 && x < width && y < height)
      pixels[y*width+x] = rgb;
  }
  
  void setPixel(Pt p, RGB rgb) { setPixel(p.x, p.y, rgb); }
  void setPixel(Pt p, Color color) { setPixel(p.x, p.y, color); }

  public RGBImage copy() {
    return new RGBImage(this);
  }

  public boolean inRange(int x, int y) {
    return x >= 0 && y >= 0 && x < width && y < height;
  }

  public Dimension getSize() {
    return new Dimension(width, height);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    RGBImage rgbImage = (RGBImage) o;

    if (height != rgbImage.height) return false;
    if (width != rgbImage.width) return false;
    if (!Arrays.equals(pixels, rgbImage.pixels)) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result = width;
    result = 31 * result + height;
    result = 31 * result + Arrays.hashCode(pixels);
    return result;
  }

  public String getHex(int x, int y) {
    return getPixel(x, y).getHexString();
  }

  public RGBImage clip(int x, int y, int width, int height) {
    return clip(new Rectangle(x, y, width, height));
  }

  public RGBImage clipLine(int y) {
    return clip(0, y, width, 1);
  }

  public int numPixels() {
    return width*height;
  }
  
  RGBImage uncacheBufferedImage() {
    bufferedImage = null;
    return this;
  }
}

static interface WidthAndHeight {
  default int w(){ return getWidth(); }
int getWidth();
  default int h(){ return getHeight(); }
int getHeight();
  
  public default Rect bounds() { return rect(0, 0, getWidth(), getHeight()); }
}


interface IVarWithNotify<A> extends IVar<A>, IF0WithChangeListeners<A> {
  default IVarWithNotify<A> onChange(IVF1<A> r) {
    if (r == null) return this;
    onChange(() -> r.get(get()));
    return this;
  }
  
  default IVarWithNotify<A> onChangeAndNow(IVF1<A> r) {
    if (r == null) return this;
    onChangeAndNow(() -> r.get(get()));
    return this;
  }
}
static interface IRGBImage extends MakesBufferedImage {
  int getIntPixel(int x, int y);
}
static interface IMultiSet<A> {
  // returns new count
  int add(A key);
  
  /*void addAll(Iterable<A> c) {
    if (c != null) for (A a : c) add(a);
  }

  void addAll(MultiSet<A> ms) {
    for (A a : ms.keySet()) add(a, ms.get(a));
  }*/
  
  //int add(A key, int count);

  //void put(A key, int count);

  int get(A key);
  //bool contains(A key);

  //void remove(A key);

  /*List<A> topTen();
  
  synchronized List<A> getTopTen() { ret getTopTen(10); }
  synchronized List<A> getTopTen(int maxSize) {
    List<A> list = getSortedListDescending();
    return list.size() > maxSize ? list.subList(0, maxSize) : list;
  }
  
  synchronized L<A> highestFirst() {
    ret getSortedListDescending();
  }

  synchronized L<A> lowestFirst() {
    ret reversedList(getSortedListDescending());
  }

  synchronized L<A> getSortedListDescending() {
    List<A> list = new ArrayList<A>(map.keySet());
    Collections.sort(list, new Comparator<A>() {
      public int compare(A a, A b) {
        return map.get(b).compareTo(map.get(a));
      }
    });
    ret list;
  }

  synchronized int getNumberOfUniqueElements() {
    return map.size();
  }
  
  synchronized int uniqueSize() {
    ret map.size();
  }

  synchronized Set<A> asSet() {
    return map.keySet();
  }

  synchronized NavigableSet<A> navigableSet() {
    return navigableKeys((NavigableMap) map);
  }

  synchronized Set<A> keySet() {
    return map.keySet();
  }
  
  synchronized A getMostPopularEntry() {
    int max = 0;
    A a = null;
    for (Map.Entry<A,Integer> entry : map.entrySet()) {
      if (entry.getValue() > max) {
        max = entry.getValue();
        a = entry.getKey();
      }
    }
    return a;
  }

  synchronized void removeAll(A key) {
    size -= get(key);
    map.remove(key);
  }

  synchronized int size() {
    ret size;
  }

  synchronized MultiSet<A> mergeWith(MultiSet<A> set) {
    MultiSet<A> result = new MultiSet<A>();
    for (A a : set.asSet()) {
      result.add(a, set.get(a));
    }
    return result;
  }
  
  synchronized boolean isEmpty() {
    return map.isEmpty();
  }
  
  synchronized public String toString() { // hmm. sync this?
    return str(map);
  }
  
  synchronized void clear() {
    map.clear();
    size = 0;
  }
  
  synchronized Map<A, Int> asMap() {
    ret cloneMap(map);
  }*/
}
static class OKOrError<A> implements IF0<A> {
  A value;
   final public Throwable getError(){ return error(); }
public Throwable error() { return error; }
 Throwable error;
  
  OKOrError() {}
  OKOrError(A value) {
  this.value = value;}
  OKOrError(boolean dummy, Throwable error) {
  this.error = error; assertNotNull(error); }
  
  final boolean isOK(){ return ok(); }
boolean ok() { return error == null; }
  
  public String toString() {
    return ok()
      ? /*"OK: " +*/ str(value)
      : "Error: " + str(error);
  }
  
  public A get() { return !ok() ? null : value; }
  
  public A getMandatory() { if (!ok()) throw rethrow(error); return value; }
  
  static <A> OKOrError<A> ok(A a) { return new OKOrError(a); }
  static OKOrError error(Throwable error) { return new OKOrError(false, error); }
}
static abstract class DialogIO implements AutoCloseable {
  String line;
  boolean eos, loud, noClose;
  Lock lock = lock();
  
  abstract String readLineImpl();
  abstract boolean isStillConnected();
  abstract void sendLine(String line);
  abstract boolean isLocalConnection();
  abstract Socket getSocket();

  int getPort() { Socket s = getSocket(); return s == null ? 0 : s.getPort(); }
  
  boolean helloRead = false;
  int shortenOutputTo = 500;
  
  String readLineNoBlock() {
    String l = line;
    line = null;
    return l;
  }
  
  boolean waitForLine() { try {
    ping();
    if (line != null) return true;
    //print("Readline");
    line = readLineImpl();
    //print("Readline done: " + line);
    if (line == null) eos = true;
    return line != null;
  } catch (Exception __e) { throw rethrow(__e); } }
  
  String readLine() {
    waitForLine();
    helloRead = true;
    return readLineNoBlock();
  }
  
  String ask(String s, Object... args) {
    if (loud) return askLoudly(s, args);
    if (!helloRead) readLine();
    if (args.length != 0) s = format3(s, args);
    sendLine(s);
    return readLine();
  }
  
  String askLoudly(String s, Object... args) {
    if (!helloRead) readLine();
    if (args.length != 0) s = format3(s, args);
    print("> " + shorten(s, shortenOutputTo));
    sendLine(s);
    String answer = readLine();
    print("< " + shorten(answer, shortenOutputTo));
    return answer;
  }
  
  void pushback(String l) {
    if (line != null)
      throw fail();
    line = l;
    helloRead = false;
  }
}

static abstract class DialogHandler {
  abstract void run(DialogIO io);
}
static class VarContext {
  VarContext parent;
  Map<String, Object> vars;
  
  VarContext() {}
  VarContext(VarContext parent) {
  this.parent = parent;}
  
  Object get(String name) {
    if (containsKey(vars, name))
      return mapGet(vars, name);
    if (parent != null)
      return parent.get(name);
    return null;
  }
  
  final void put(String name, Object value){ set(name, value); }
void set(String name, Object value) {
    vars = putOrCreate(vars, name, value);
  }
  
  void unset(String name) {
    remove(vars, name);
  }
  
  void printMe() {
    pnl(vars);
    print("parent", parent);
  }
  
  // mechanism for returning from a script
  
   final public VarContext setExitFromScript(Object exitFromScript){ return exitFromScript(exitFromScript); }
public VarContext exitFromScript(Object exitFromScript) { this.exitFromScript = exitFromScript; return this; }  final public Object getExitFromScript(){ return exitFromScript(); }
public Object exitFromScript() { return exitFromScript; }
 Object exitFromScript;
}
static class G22GalleryImage extends Concept {
  File path;

  public String toString() { return /*"[" + id + "] " +*/ fileName(path); }

  boolean imageExists() { return fileExists(path); }
  
  BufferedImage load() { return loadImage2(path); }
}
static abstract class LiveValue<A> implements IGetterWithNotify<A> {
  abstract Class<A> getType();
  public abstract A get();
}
// Both lines and columns are numbered from 1
static class LineAndColumn {
  int line, col;
  
  LineAndColumn(int line, int col) {
  this.col = col;
  this.line = line;}
  
  // get text of a line. may return null if no text available
  transient  IF1<Integer, String> getLineText;
String getLineText(int line) { return getLineText != null ? getLineText.get(line) : getLineText_base(line); }
final String getLineText_fallback(IF1<Integer, String> _f, int line) { return _f != null ? _f.get(line) : getLineText_base(line); }
String getLineText_base(int line) { return null; }
  
  public String toString() { return "Line " + n2(line) + ", col " + n2(col); }
}
static class FailedRule extends RuleWithParams {
  List<Exp> satisfiedConditions;
  Exp remainingCondition;
  
  FailedRule() {}
  FailedRule(IfThen rule, VarMatches matches, Exp remainingCondition) {
  this.remainingCondition = remainingCondition;
  this.matches = matches;
  this.rule = rule;}
  FailedRule(IfThen rule, VarMatches matches, List<Exp> satisfiedConditions, Exp remainingCondition) {
  this.remainingCondition = remainingCondition;
  this.satisfiedConditions = satisfiedConditions;
  this.matches = matches;
  this.rule = rule;}
}

static class RuleWithParams implements IFieldsToList{
  static final String _fieldOrder = "rule matches";
  IfThen rule;
  VarMatches matches;
  RuleWithParams() {}
  RuleWithParams(IfThen rule, VarMatches matches) {
  this.matches = matches;
  this.rule = rule;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + rule + ", " + matches + ")"; }

public boolean equals(Object o) {
if (!(o instanceof RuleWithParams)) return false;
    RuleWithParams __7 =  (RuleWithParams) o;
    return eq(rule, __7.rule) && eq(matches, __7.matches);
}

  public int hashCode() {
    int h = 1254104616;
    h = boostHashCombine(h, _hashCode(rule));
    h = boostHashCombine(h, _hashCode(matches));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {rule, matches}; }

  String ruleID() { return rule.globalID; }
}

static class IfThen implements IFieldsToList{
  static final String _fieldOrder = "in out globalID options originalText";
  Exp in;
  Exp out;
  IfThen() {}
  IfThen(Exp in, Exp out) {
  this.out = out;
  this.in = in;}

public boolean equals(Object o) {
if (!(o instanceof IfThen)) return false;
    IfThen __8 =  (IfThen) o;
    return eq(in, __8.in) && eq(out, __8.out);
}

  public int hashCode() {
    int h = -2108234502;
    h = boostHashCombine(h, _hashCode(in));
    h = boostHashCombine(h, _hashCode(out));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {in, out}; }

  String globalID;
  Set<String> options;
  String originalText;
  
  String text() {
    Collection<String> opt = options;
    if (nempty(globalID)) opt = concatLists(ll("id: " + globalID), opt);
    return (nempty(opt) ? "[" + joinWithComma(opt) + "] " : "")
      + (in == null ? "" : in.text() + "\n  => ") + nlLogic_text(out);
  }
  
  public String toString() { return text(); }
}

abstract static class Exp {
  abstract String text();
  public String toString() { return text(); }
}

static class Func extends Exp implements IFieldsToList{
  String name;
  Exp arg;
  Func() {}
  Func(String name, Exp arg) {
  this.arg = arg;
  this.name = name;}

public boolean equals(Object o) {
if (!(o instanceof Func)) return false;
    Func __0 =  (Func) o;
    return eq(name, __0.name) && eq(arg, __0.arg);
}

  public int hashCode() {
    int h = 2201316;
    h = boostHashCombine(h, _hashCode(name));
    h = boostHashCombine(h, _hashCode(arg));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {name, arg}; }

  List<String> options;
  
  Func(String name, List<String> options, Exp arg) {
  this.arg = arg;
  this.options = options;
  this.name = name;}
  
  String text() {
    return name
      + (empty(options) ? "" : "[" + joinWithComma(options) + "]")
      + "(" + arg.text() + ")";
  }
  
  String argText() { return nlLogic_text(arg); }
}

static class And extends Exp implements IFieldsToList{
  Exp a;
  Exp b;
  And() {}
  And(Exp a, Exp b) {
  this.b = b;
  this.a = a;}

public boolean equals(Object o) {
if (!(o instanceof And)) return false;
    And __1 =  (And) o;
    return eq(a, __1.a) && eq(b, __1.b);
}

  public int hashCode() {
    int h = 65975;
    h = boostHashCombine(h, _hashCode(a));
    h = boostHashCombine(h, _hashCode(b));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {a, b}; }

  String text() {
    return a.text() + "\n  && " + b.text();
  }
}

static class ExpNot extends Exp implements IFieldsToList{
  Exp a;
  ExpNot() {}
  ExpNot(Exp a) {
  this.a = a;}

public boolean equals(Object o) {
if (!(o instanceof ExpNot)) return false;
    ExpNot __2 =  (ExpNot) o;
    return eq(a, __2.a);
}

  public int hashCode() {
    int h = 2089649046;
    h = boostHashCombine(h, _hashCode(a));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {a}; }

  String text() {
    return "!" + a.text();
  }
}

abstract static class Literal extends Exp {}
static class Sentence extends Literal implements IFieldsToList{
  List<String> tok;
  Sentence() {}
  Sentence(List<String> tok) {
  this.tok = tok;}

public boolean equals(Object o) {
if (!(o instanceof Sentence)) return false;
    Sentence __3 =  (Sentence) o;
    return eq(tok, __3.tok);
}

  public int hashCode() {
    int h = 1327381123;
    h = boostHashCombine(h, _hashCode(tok));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {tok}; }
 String text() { return join(tok); } }
static class Sentence2 extends Literal implements IFieldsToList{
  String text;
  Sentence2() {}
  Sentence2(String text) {
  this.text = text;}

public boolean equals(Object o) {
if (!(o instanceof Sentence2)) return false;
    Sentence2 __4 =  (Sentence2) o;
    return eq(text, __4.text);
}

  public int hashCode() {
    int h = -1800858097;
    h = boostHashCombine(h, _hashCode(text));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {text}; }
 String text() { return text; } }

static class Eq extends Exp implements IFieldsToList{
  Exp left;
  Exp right;
  Eq() {}
  Eq(Exp left, Exp right) {
  this.right = right;
  this.left = left;}

public boolean equals(Object o) {
if (!(o instanceof Eq)) return false;
    Eq __5 =  (Eq) o;
    return eq(left, __5.left) && eq(right, __5.right);
}

  public int hashCode() {
    int h = 2252;
    h = boostHashCombine(h, _hashCode(left));
    h = boostHashCombine(h, _hashCode(right));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {left, right}; }

  String text() {
    return left.text() + " = " + right.text();
  }
}
static class RGB {
  // usually in range [0, 1]
  public float r, g, b; // can't be final cause persistence
  
  RGB() {}
  
  public RGB(float r, float g, float b) {
    this.r = r;
    this.g = g;
    this.b = b;
  }

  public RGB(double r, double g, double b) {
    this.r = (float) r;
    this.g = (float) g;
    this.b = (float) b;
  }
  
  public RGB(double[] rgb) {
    this(rgb[0], rgb[1], rgb[2]);
  }

  public RGB(int rgb) {
    this(new Color(rgb));
  }
  
  public RGB(double brightness) {
    this.r = this.g = this.b = max(0f, min(1f, (float) brightness));
  }

  public RGB(Color color) {
    this.r = color.getRed()/255f;
    this.g = color.getGreen()/255f;
    this.b = color.getBlue()/255f;
  }

  // TODO: 3-char version
  public RGB(String hex) {
    int i = l(hex)-6;
    r = Integer.parseInt(hex.substring(i, i+2), 16)/255f;
    g = Integer.parseInt(hex.substring(i+2, i+4), 16)/255f;
    b = Integer.parseInt(hex.substring(i+4, i+6), 16)/255f;
  }

  public float getComponent(int i) {
    return i == 0 ? r : i == 1 ? g : b;
  }

  public int getInt(int i) {
    return i == 0 ? redInt() : i == 1 ? greenInt() : blueInt();
  }
  
  public Color getColor() {
    return new Color(r, g, b);
  }

  public static RGB newSafe(float r, float g, float b) {
    return new RGB(Math.max(0, Math.min(1, r)), Math.max(0, Math.min(1, g)), Math.max(0, Math.min(1, b)));
  }

  int asInt() { return getColor().getRGB() & 0xFFFFFF; }
  int getInt() { return getColor().getRGB() & 0xFFFFFF; }
  int asIntWithAlpha() { return rgbInt(redInt(), greenInt(), blueInt()) | 0xFF000000; }

  public float getBrightness() {
    return (r+g+b)/3.0f;
  }

  public String getHexString() {
    return Integer.toHexString(asInt() | 0xFF000000).substring(2).toUpperCase();
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof RGB)) return false;

    RGB rgb = (RGB) o;

    if (Float.compare(rgb.b, b) != 0) return false;
    if (Float.compare(rgb.g, g) != 0) return false;
    if (Float.compare(rgb.r, r) != 0) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result = (r != +0.0f ? Float.floatToIntBits(r) : 0);
    result = 31 * result + (g != +0.0f ? Float.floatToIntBits(g) : 0);
    result = 31 * result + (b != +0.0f ? Float.floatToIntBits(b) : 0);
    return result;
  }

  public boolean isBlack() {
    return r == 0f && g == 0f && b == 0f;
  }

  public boolean isWhite() {
    return r == 1f && g == 1f && b == 1f;
  }

  public String toString() {
    return getHexString();
  }
  
  int redInt() { return iround(r*255); }
  int greenInt() { return iround(g*255); }
  int blueInt() { return iround(b*255); }
  
  static float brightnessToFloat(int brightness) { return brightness/255f; }
}
static class Timestamp implements Comparable<Timestamp> , IFieldsToList{
  long date;
  Timestamp(long date) {
  this.date = date;}

public boolean equals(Object o) {
if (!(o instanceof Timestamp)) return false;
    Timestamp __1 =  (Timestamp) o;
    return date == __1.date;
}

  public int hashCode() {
    int h = 2059094262;
    h = boostHashCombine(h, _hashCode(date));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {date}; }

  Timestamp() { date = now(); }
  
  long unixDate() { return date; }
  
  public String toString() { return formatLocalDateWithSeconds(date); }
  
  // Hmm. Should Timestamp(0) be equal to null? Question, questions...
  public int compareTo(Timestamp t) {
    return t == null ? 1 : cmp(date, t.date);
  }
  
  Timestamp plus(Seconds seconds) {
    return plus(seconds == null ? null : seconds.getDouble());
  }
  
  final Timestamp plusSeconds(double seconds){ return plus(seconds); }
Timestamp plus(double seconds) {
    return new Timestamp(date+toMS(seconds));
  }
  
  // returns milliseconds
  long minus(Timestamp ts) {
    return unixDate()-ts.unixDate();
  }
  
  long sysTime() {
    return clockTimeToSystemTime(date);
  }
}
static interface IHasGlobalID {
  public GlobalID globalID();
}
interface IHasChangeListeners {
  IHasChangeListeners onChange(Runnable r);
  IHasChangeListeners removeChangeListener(Runnable r);
  
  default IHasChangeListeners onChangeAndNow(Runnable l) {
    onChange(l);
    callF(l);
    return this;
  }
}
static class G22AnalysisContext implements IFieldsToList{
  G22Utils g22utils;
  BufferedImage image;
  G22AnalysisContext() {}
  G22AnalysisContext(G22Utils g22utils, BufferedImage image) {
  this.image = image;
  this.g22utils = g22utils;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + g22utils + ", " + image + ")"; }public Object[] _fieldsToList() { return new Object[] {g22utils, image}; }

  // value is OKOrError or calculating
  Map<G22Analyzer, Object> analyzerResults = syncMap();
  
  static Object calculating;
  
  Object callAnalyzer(int analyzerID) {
    return callAnalyzer((long) analyzerID);
  }
  
  Object callAnalyzer(long analyzerID) {
    var analyzer = getConcept(g22utils.concepts(), G22Analyzer.class, analyzerID);
    if (analyzer == null)
      throw fail("Analyzer with ID " + analyzerID + " not found");
    return callAnalyzer(analyzer);
  }
  
  Object callAnalyzer(G22Analyzer analyzer) {
    return analyzer.compileForAutoRun().get(this);
  }
}
  

static class ListAndIndex<A> implements IFieldsToList{
  static final String _fieldOrder = "list idx";
  List<A> list;
  int idx;
  ListAndIndex() {}
  ListAndIndex(List<A> list, int idx) {
  this.idx = idx;
  this.list = list;}

public boolean equals(Object o) {
if (!(o instanceof ListAndIndex)) return false;
    ListAndIndex __1 =  (ListAndIndex) o;
    return eq(list, __1.list) && idx == __1.idx;
}

  public int hashCode() {
    int h = 276903961;
    h = boostHashCombine(h, _hashCode(list));
    h = boostHashCombine(h, _hashCode(idx));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {list, idx}; }

  boolean atEnd() { return idx >= l(list); }
  
  A get() { return _get(list, idx); }
  int size() { return l(list); }
  
  public String toString() {
    return subList(list, 0, idx) + ", then " + subList(list, idx);
  }
  
  ListAndIndex<A> plus(int ofs) { return new ListAndIndex(list, idx+ofs); }
  
  List<A> list() { return list; }
  final int idx(){ return index(); }
int index() { return idx; }
}
static class VarWithNotify<A> extends Var<A> implements IVarWithNotify<A> {
  transient Set<Runnable> onChange;
public VarWithNotify<A> onChange(Runnable r) { onChange = createOrAddToSyncLinkedHashSet(onChange, r); return this; }
public VarWithNotify<A> removeChangeListener(Runnable r) { main.remove(onChange, r); return this; }
public void fireChange() {  pcallFAll(onChange); }
  
  VarWithNotify() {}
  VarWithNotify(A a) { super(a); }
  
  // clever way (I hope) to do eq() outside of synchronization block
  public void set(A a) {
    A v = get();
    if (eq(v, a)) {
       
      return;
    }
    
     
    synchronized(this) { this.v = a; }
    fireChange();
  }
}
static class TokenRange extends IntRange {
  TokenRange() {}
  TokenRange(int start, int end) {
  this.end = end;
  this.start = start;}
}


interface IGetterWithNotify<A> extends IF0<A>, IHasChangeListeners {
}
interface IF0WithChangeListeners<A> extends IF0<A>, IHasChangeListeners {}
// it's unclear whether the end is inclusive or exclusive
// (usually exclusive I guess)
static class IntRange {
  int start, end;
  
  IntRange() {}
  IntRange(int start, int end) {
  this.end = end;
  this.start = start;}
  IntRange(IntRange r) { start = r.start; end = r.end; }
  
  public boolean equals(Object o) { return stdEq2(this, o); }
public int hashCode() { return stdHash2(this); }
  
  final int length() { return end-start; }
  final boolean empty() { return start >= end; }
  final boolean isEmpty() { return start >= end; }
  
  static String _fieldOrder = "start end";
  
  public String toString() { return "[" + start + ";" + end + "]"; }
}
static class Seconds implements Comparable<Seconds> , IFieldsToList{
  double seconds;
  Seconds() {}
  Seconds(double seconds) {
  this.seconds = seconds;}

public boolean equals(Object o) {
if (!(o instanceof Seconds)) return false;
    Seconds __1 =  (Seconds) o;
    return seconds == __1.seconds;
}

  public int hashCode() {
    int h = -660217249;
    h = boostHashCombine(h, _hashCode(seconds));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {seconds}; }

  final double get(){ return seconds(); }
final double getDouble(){ return seconds(); }
double seconds() { return seconds; }
  
  public String toString() { return formatDouble(seconds, 3) + " s"; }
  
  public int compareTo(Seconds s) {
    return cmp(seconds, s.seconds);
  }
  
  Seconds div(double x) { return new Seconds(get()/x); }
  Seconds minus(Seconds x) { return new Seconds(get()-x.get()); }
}
static class VarMatches extends WrappedMap<String, String> {
  VarMatches() { super(new LinkedHashMap()); }
  VarMatches(Map<String, String> map) { this(); main.putAll(this, map); }
}
static class GlobalID implements Comparable<GlobalID> {
  // We need 76 bits for 26^16 IDs
  long a; // lower 64 bits
  int b;  // 76-64=12 higher bits; could be short. change to short when unstructure() is smarter
  
  GlobalID() {}
  GlobalID(String id) {
    assertGlobalID(id);
    BigInteger value = bigint(0);
    for (int i = 0; i < l(id); i++)
      value = plus(mul(value, 26), charDiff(id.charAt(i), 'a'));
      
    
    a = value.longValue();
    value = value.shiftRight(64);
    b = value.shortValue();
  }
  
  public String toString() {
    BigInteger value = bigint(b);
    value = value.shiftLeft(32);
    value = plus(value, (a >> 32) & 0xFFFFFFFFL);
    value = value.shiftLeft(32);
    value = plus(value, a & 0xFFFFFFFFL);
    
    
    
    return bigintToGlobalID(value);
  }
  
  public boolean equals(Object o) {
    if (!(o instanceof GlobalID)) return false;
    return ((GlobalID) o).a == a && ((GlobalID) o).b == b;
  }
  
  public int hashCode() {
    return (int) a;
  }
  
  // matches ID's lexical ordering
  public int compareTo(GlobalID id) {
    int diff = b-id.b;
    return diff != 0 ? diff : Long.compareUnsigned(a, id.a);
  }
}


static class WrappedMap<A, B> extends AbstractMap<A, B> {
  Map<A, B> map;

  WrappedMap() {}
  WrappedMap(Map<A, B> map) {
  this.map = map;}

  public B get(Object key) {
    return map.get(key);
  }
  
  public B put(A key, B value) {
    return map.put(key, value);
  }
  
  public boolean containsKey(Object key) {
    return map.containsKey(key);
  }
  
  public Set<Map.Entry<A,B>> entrySet() {
    return map.entrySet();
  }
}



static Lock dbLock() {
  return db_mainConcepts().lock;
}

static Lock dbLock(Concepts cc) {
  return cc == null ? null : cc.lock;
}

static Lock dbLock(Concept c) {
  return dbLock(c == null ? null : c._concepts);
}


static boolean bareDBMode_on = false;

static void bareDBMode() {
  bareDBMode(null); // default autoSaveInterval
}

static void bareDBMode(Integer autoSaveInterval) {
  bareDBMode_on = true;
  conceptsAndBot(autoSaveInterval);
}


static ThreadLocal<Boolean> DynamicObject_loading = or((ThreadLocal) get(getClass("x30_pkg.x30_util"), "DynamicObject_loading"), new ThreadLocal());

static ThreadLocal<Boolean> dynamicObjectIsLoading_threadLocal() { 
  return DynamicObject_loading;
}


static Class<?> getClass(String name) {
  return _getClass(name);
}

static Class getClass(Object o) {
  return _getClass(o);
}

static Class getClass(Object realm, String name) {
  return _getClass(realm, name);
}


static boolean classIsExportedTo(Class c, java.lang.Module destModule) {
  if (c == null || destModule == null) return false;
  
  java.lang.Module srcModule = c.getModule();
  String packageName = c.getPackageName();
  return srcModule.isExported(packageName, destModule);
}


static boolean isAbstract(Class c) {
  return (c.getModifiers() & Modifier.ABSTRACT) != 0;
}

static boolean isAbstract(Method m) {
  return (m.getModifiers() & Modifier.ABSTRACT) != 0;
}


static boolean reflection_isForbiddenMethod(Method m) {
  return m.getDeclaringClass() == Object.class
    && eqOneOf(m.getName(), "finalize", "clone", "registerNatives");
}


static Set<Class> allInterfacesImplementedBy(Class c) {
  if (c == null) return null;
  HashSet<Class> set = new HashSet();
  allInterfacesImplementedBy_find(c, set);
  return set;
}

static void allInterfacesImplementedBy_find(Class c, Set<Class> set) {
  if (c.isInterface() && !set.add(c)) return;
  do {
    for (Class intf : c.getInterfaces())
      allInterfacesImplementedBy_find(intf, set);
  } while ((c = c.getSuperclass()) != null);
}


static Method findMethod(Object o, String method, Object... args) {
  return findMethod_cached(o, method, args);
}

static boolean findMethod_checkArgs(Method m, Object[] args, boolean debug) {
  Class<?>[] types = m.getParameterTypes();
  if (types.length != args.length) {
    if (debug)
      System.out.println("Bad parameter length: " + args.length + " vs " + types.length);
    return false;
  }
  for (int i = 0; i < types.length; i++)
    if (!(args[i] == null || isInstanceX(types[i], args[i]))) {
      if (debug)
        System.out.println("Bad parameter " + i + ": " + args[i] + " vs " + types[i]);
      return false;
    }
  return true;
}


static Method findStaticMethod(Class c, String method, Object... args) {
  Class _c = c;
  while (c != null) {
    for (Method m : c.getDeclaredMethods()) {
      if (!m.getName().equals(method))
        continue;

      if ((m.getModifiers() & Modifier.STATIC) == 0 || !findStaticMethod_checkArgs(m, args))
        continue;

      return m;
    }
    c = c.getSuperclass();
  }
  return null;
}

static boolean findStaticMethod_checkArgs(Method m, Object[] args) {
  Class<?>[] types = m.getParameterTypes();
  if (types.length != args.length)
    return false;
  for (int i = 0; i < types.length; i++)
    if (!(args[i] == null || isInstanceX(types[i], args[i])))
      return false;
  return true;
}



static List<String> quoteAll(String[] l) {
  return quoteAll(asList(l));
}

static List<String> quoteAll(Collection<String> l) {
  List<String> x = new ArrayList();
  for (String s : l)
    x.add(quote(s));
  return x;
}


static int _hashCode(Object a) {
  return a == null ? 0 : a.hashCode();
}


static boolean arraysEqual(Object[] a, Object[] b) {
  if (a.length != b.length) return false;
  for (int i = 0; i < a.length; i++)
    if (neq(a[i], b[i])) return false;
  return true;
}


static Object metaGet(IMeta o, Object key) {
  return metaMapGet(o, key);
}

static Object metaGet(Object o, Object key) {
  return metaMapGet(o, key);
}

static Object metaGet(String key, IMeta o) {
  return metaMapGet(o, key);
}

static Object metaGet(String key, Object o) {
  return metaMapGet(o, key);
}


static Object metaMapGet(IMeta o, Object key) {
  return o == null ? null : o.metaGet(key); // We now let the object itself do it (overridable!)
}

static Object metaMapGet(Object o, Object key) {
  return metaMapGet(toIMeta(o), key);
}


static void metaPut(IMeta o, Object key, Object value) {
  metaMapPut(o, key, value);
}



static void metaPut(Object o, Object key, Object value) {
  metaMapPut(o, key, value);
}


static Map convertObjectMetaToMap(IMeta o) { return convertObjectMetaToMap(o, () -> makeObjectMetaMap()); }
static Map convertObjectMetaToMap(IMeta o, IF0<Map> createEmptyMap) {
  if (o == null) return null;
  
  // The following shortcut depends on the assumption that a meta field never reverts
  // to null when it was a map
  
    Object meta = o._getMeta();
    if (meta instanceof Map) return ((Map) meta);
  
  
  // non-shortcut path (create meta)
   var mutex = tempMetaMutex(o); try {
  var actualMutex = mutex.get();
  synchronized(actualMutex) {
    meta = o._getMeta();
    if (meta instanceof Map) return ((Map) meta);
    Map map = createEmptyMap.get();
    if (meta != null) map.put("previousMeta" , meta);
    o._setMeta(map);
    return map;
  }
} finally { _close(mutex); }}


static <A, B> void syncMapPutOrRemove(Map<A, B> map, A key, B value) {
  syncMapPut2(map, key, value);
}


static ImageSurface pixelatedImageSurface() {
  var is = imageSurface();
  imageSurface_pixelated(is);
  return is;
}

static ImageSurface pixelatedImageSurface(MakesBufferedImage img) { return pixelatedImageSurface(toBufferedImage(img)); }

static ImageSurface pixelatedImageSurface(BufferedImage img) {
  var is = pixelatedImageSurface();
  is.setImage(img);
  return is;
}


static BufferedImage toBufferedImage(Object o) {
  return toBufferedImageOpt(o);
}


static <A> A assertEquals(Object x, A y) {
  return assertEquals("", x, y);
}

static <A> A assertEquals(String msg, Object x, A y) {
  if (assertVerbose()) return assertEqualsVerbose(msg, x, y);
  if (!(x == null ? y == null : x.equals(y)))
    throw fail((msg != null ? msg + ": " : "") + y + " != " + x);
  return y;
}




static File getBytecodePathForClass(Object o) {
  return getBytecodePathForClass(_getClass(o));
}

static File getBytecodePathForClass(Class c) { try {
  return c == null ? null : new File(c.getProtectionDomain().getCodeSource().getLocation().toURI());
} catch (Exception __e) { throw rethrow(__e); } }


static void createOrRemoveFile(File f, boolean create) {
  if (create) {
    if (!fileExists(f)) touchFile(f);
  } else if (fileExists(f))
    removeFile(f);
}


static List<String> tlft(String s) { return toLinesFullTrim(s); }
static List<String> tlft(File f) { return toLinesFullTrim(f); }



static boolean sameFile(File a, File b) { try {
  return a == null ? b == null : b != null && eq(a.getCanonicalPath(), b.getCanonicalPath());
} catch (Exception __e) { throw rethrow(__e); } }


static String fileName(File f) {
  return f == null ? null : f.getName();
}


static <A> List<A> llNonNulls(A... a) {
  List<A> l = new ArrayList();
  for (A x : a) if (x != null) l.add(x);
  return l;
}


static <A extends Component> A jMinHeight(A c, int h) {
  return jMinHeight(h, c);
}

static <A extends Component> A jMinHeight(int h, A c) {
  Dimension size = c.getMinimumSize();
  c.setMinimumSize(new Dimension(size.width, max(h, size.height)));
  return jPreferHeight(h, c);
}


static LinkedHashMap litorderedmap(Object... x) {
  LinkedHashMap map = new LinkedHashMap();
  litmap_impl(map, x);
  return map;
}


static String or2_rev(String b, String a) {
  return or2(a, b);
}




static String joinNemptiesWithSpacedPlus(Object... strings) {
  return joinNempties(" + ", strings);
}

static String joinNemptiesWithSpacedPlus(Iterable strings) {
  return joinNempties(" + ", strings);
}


static String n2(long l) { return formatWithThousands(l); }
static String n2(AtomicLong l) { return n2(l.get()); }
static String n2(Collection l) { return n2(l(l)); }
static String n2(Map map) { return n2(l(map)); }

static String n2(double l, String singular) {
  return empty(singular) ? str(l) : n2(l, singular, singular + "s");
}

static String n2(double l, String singular, String plural) {
  if (fraction(l) == 0)
    return n2((long) l, singular, plural);
  else
    return l + " " + plural;
}

static String n2(long l, String singular, String plural) {
  return n_fancy2(l, singular, plural);
}

static String n2(long l, String singular) {
  return empty(singular) ? n2(l) : n_fancy2(l, singular, singular + "s");
}

static String n2(Collection l, String singular) {
  return n2(l(l), singular);
}

static String n2(Collection l, String singular, String plural) {
  return n_fancy2(l, singular, plural);
}

static String n2(Map m, String singular, String plural) {
  return n_fancy2(m, singular, plural);
}

static String n2(Map m, String singular) {
  return n2(l(m), singular);
}

static String n2(long[] a, String singular) { return n2(l(a), singular); }

static String n2(Object[] a, String singular) { return n2(l(a), singular); }
static String n2(Object[] a, String singular, String plural) { return n_fancy2(a, singular, plural); }


  static String n2(MultiSet ms, String singular, String plural) {
    return n_fancy2(ms, singular, plural);
  }



static int intMax(Collection c, String field) {
  int max = Integer.MIN_VALUE;
  for (Object o : c)
    max = Math.max(max, toInt(getOpt(o, field)));
  return max;
}

static int intMax(Iterable<Integer> l) {
  int max = Integer.MIN_VALUE;
  for (int i : unnullForIteration(l))
    max = Math.max(max, i);
  return max;
}

static int intMax(int... l) {
  int max = Integer.MIN_VALUE;
  if (l != null) for (int i : l)
    max = Math.max(max, i);
  return max;
}


static List mapLL(Object f, Object... data) {
  return map(f, ll(data));
}

static <A, B> List<B> mapLL(IF1<A, B> f, A... data) {
  return map(f, ll(data));
}


static int linesOfCode_javaTok(String text) {
  List<String> tok = javaTok(text);
  int lines = 0;
  boolean codeInCurrentLine = false;
  for (int i = 0; i < l(tok); i++) {
    String t = tok.get(i);
    if (odd(i)) {
      // code token
      int lTok = l(t);
      for (int j = 0; j < lTok; j++) {
        char c = t.charAt(j);
        if (c == '\n')
          codeInCurrentLine = false;
        else if (!isSpaceEtc(c) && !codeInCurrentLine) {
          codeInCurrentLine = true;
          ++lines;
        }
      }
    } else {
      // space token
      if (codeInCurrentLine && containsNewLine(t)) codeInCurrentLine = false;
    }
  }
  
  return lines;
}


static File appendToFileName(File f, String suffix) {
  return fileAppendToName(f, suffix);
}

static File appendToFileName(String suffix, File f) {
  return appendToFileName(f, suffix);
}


static String nLabels(long n) { return n2(n, "label"); }
static String nLabels(Collection l) { return nLabels(l(l)); }
static String nLabels(Map map) { return nLabels(l(map)); }


static <A extends Concept> A uniqCI(Class<A> c, Object... params) {
  return uniqueConcept(db_mainConcepts(), c, params);
}

static <A extends Concept> A uniqCI(Concepts cc, Class<A> c, Object... params) {
   AutoCloseable __1 = tempDBLock(cc); try {
  params = expandParams(c, params);
  A x = findConceptWhereCI(cc, c, params);
  if (x == null) {
    x = unlisted(c);
    csetAll(x, params);
    cc.register(x);
  }
  return x;
} finally { _close(__1); }}


static File dbDir() {
  return programDir(dbProgramID());
}


static File conceptsDir() { return conceptsDir(db_mainConcepts()); }
static File conceptsDir(Concepts cc) {
  return cc.conceptsDir();
}

static File conceptsDir(String subName) { return conceptsDir(db_mainConcepts(), subName); }
static File conceptsDir(Concepts cc, String subName) {
  return newFile(conceptsDir(cc), subName);
}


static int boostHashCombine(int a, int b) {
  return a ^ (b + 0x9e3779b9 + (a << 6) + (a >> 2));
}


static boolean containsFile(File dir, String name) {
  return new File(dir, name).exists();
}

static boolean containsFile(Iterable<File> l, File f) {
  return any(l, file -> sameFile(f, file));
}


static File conceptsFile(String progID) {
  return getProgramFile(progID, conceptsFileName());
}

static File conceptsFile() {
  return conceptsFile(dbProgramID());
}

static File conceptsFile(Concepts concepts) {
  return concepts.conceptsFile();
}


static File conceptsFileIn(File dir) {
  return newFile(dir, conceptsFileName());
}


static List<File> listDirsContainingFileNamed(File dir, String fileName) {
  return filter(listDirs(dir), d -> isFile(newFile(d, fileName)));
}


static void addSeparator(JMenu menu) {
  menu.addSeparator();
}

static void addSeparator(JPopupMenu menu) {
  menu.addSeparator();
}


static void truncateContainer(Container c, int n) { swing(() -> { 
  if (c != null)
    while (c.getComponentCount() > n)
      c.remove(c.getComponentCount()-1);
}); }


static <A extends Component> A packWindow(final A c) {
  { swing(() -> { 
    Window w = getWindow(c);
    if (w != null) w.pack();
  }); }
  return c;
}


static <A> VF1<A> toVF1(IVF1<A> f) {
  return ivf1ToVF1(f);
}


static PtInComponent ptInComponentFromEvent(MouseEvent evt) {
  return new PtInComponent(evt.getComponent(), pt(evt.getX(), evt.getY()));
}


static boolean hasParentOfType(Class<?> type, Component c) {
  return parentOfType(c, type) != null;
}


static <A, B> Map<A, B> putAll(Map<A, B> a, Map<? extends A,? extends B> b) {
  if (a != null && b != null) a.putAll(b);
  return a;
}


static <A, B> MultiMap<A, B> putAll(MultiMap<A, B> a, Map<? extends A,? extends B> b) {
  if (a != null) a.putAll((Map) b);
  return a;
}


static <A, B> Map<A, B> putAll(Map<A, B> a, Object... b) {
  if (a != null)
    litmap_impl(a, b);
  return a;
}



static <A> A getAndClear(IVar<A> v) {
  A a = v.get();
  v.set(null);
  return a;
}


static <A, B> Set<A> keySet(Map<A, B> map) {
  return map == null ? new HashSet() : map.keySet();
}

static Set keySet(Object map) {
  return keys((Map) map);
}


  static <A> Set<A> keySet(MultiSet<A> ms) {
    return ms.keySet();
  }



  static <A, B> Set<A> keySet(MultiMap<A, B> mm) {
    return mm.keySet();
  }





static <A, B> int keysSize(MultiMap<A, B> mm) {
  return lKeys(mm);
}


static <A> A reverseGet(List<A> l, int idx) {
  if (l == null || idx < 0) return null;
  int n = l(l);
  return idx < n ? l.get(n-1-idx) : null;
}


static <A, B> Map<A, B> cloneMap(Map<A, B> map) {
  if (map == null) return new HashMap();
  // assume mutex is equal to map
  synchronized(map) {
    return map instanceof TreeMap ? new TreeMap((TreeMap) map) // copies comparator
      : map instanceof LinkedHashMap ? new LinkedHashMap(map)
      : new HashMap(map);
  }
}

static <A, B> List<B> cloneMap(Iterable<A> l, IF1<A, B> f) {
  List x = emptyList(l);
  if (l != null) for (A o : cloneList(l))
    x.add(f.get(o));
  return x;
}


static <A, B> Collection<B> values(Map<A, B> map) {
  return map == null ? emptyList() : map.values();
}

// convenience shortcut for values_gen
static Collection values(Object map) {
  return values((Map) map);
}


static <A, B> Collection<B> values(MultiMap<A, B> mm) {
  return mm == null ? emptyList() : concatLists(values(mm.data));
}





static <A> List<A> concatLists(Iterable<A>... lists) {
  List<A> l = new ArrayList();
  if (lists != null) for (Iterable<A> list : lists)
    addAll(l, list);
  return l;
}

static <A> List<A> concatLists(Collection<? extends Iterable<A>> lists) {
  List<A> l = new ArrayList();
  if (lists != null) for (Iterable<A> list : lists)
    addAll(l, list);
  return l;
}



// TODO: use actualUserHome()?
// (there was a problem with onLocallyInferiorJavaX() always triggering inside #1013896)

static File pathToJavaxJar() {
  
    
    IResourceLoader rl = vm_getResourceLoader();
    if (rl != null)
      return rl.pathToJavaXJar();
    
    
    return pathToJavaxJar_noResourceLoader();
  
  
}
    
static File pathToJavaxJar_noResourceLoader() { try {
  
    int x = latestInstalledJavaX();
    File xfile = new File(userHome(), ".javax/x" + Math.max(x, 30) + ".jar");
    if (!xfile.isFile()) {
      print("Saving " + f2s(xfile));
      String url = x30JarServerURL();
      byte[] data = loadBinaryPage(url);
      if (data.length < 1000000)
        throw fail("Could not load " + url);
      saveBinaryFile(xfile.getPath(), data);
    }
    return xfile;
  
  
  
} catch (Exception __e) { throw rethrow(__e); } }


static <A> Iterator<A> iterator(Iterable<A> c) {
  return c == null ? emptyIterator() : c.iterator();
}


static Method hashMap_findKey_method;

static <A, B> A hashMap_findKey(HashMap<A, B> map, Object key) { try {
  if (hashMap_findKey_method == null)
    hashMap_findKey_method = findMethodNamed(HashMap.class, "getNode");
  Map.Entry<A, B> entry = (Map.Entry) hashMap_findKey_method.invoke(map, hashMap_internalHash(key), key);
  // java.util.Map.Entry<A, B> entry = (java.util.Map.Entry) call(hash, 'getNode, hashMap_internalHash(key), wkey);
  return entry == null ? null : entry.getKey();
} catch (Exception __e) { throw rethrow(__e); } }


static int syncL(Collection l) {
  if (l == null) return 0;
  synchronized(collectionMutex(l)) { return l.size(); }
}

static int syncL(Map map) {
  if (map == null) return 0;
  synchronized(collectionMutex(map)) { return map.size(); }
}

static <A> List<A> syncL() {
  return syncList();
}


static <A> List<A> syncGetAndClear(Collection<A> l) {
  return syncCloneAndClearList(l);
}


static void cancelAndInterruptThread(Thread t) {
  if (t == null) return;
  cancelThread(t);
  t.interrupt();
}


static String find(String pattern, String text) {
  Matcher matcher = Pattern.compile(pattern).matcher(text);
  if (matcher.find())
    return matcher.group(1);
  return null;
}

static <A> A find(Collection<A> c, Object... data) {
  for (A x : c)
    if (checkFields(x, data))
      return x;
  return null;
}


static void setComponent(SingleComponentPanel scp, Component c) {
  setSCPComponent(scp, c);
}

static void setComponent(SingleComponentPanel scp, IF0<Component> c) {
  if (scp != null)
    setComponent(scp, callF(c));
}


static <A> A set(A o, String field, Object value) {
  if (o == null) return null;
  if (o instanceof Class) set((Class) o, field, value);
  else try {
    Field f = set_findField(o.getClass(), field);
    makeAccessible(f);
    smartSet(f, o, value);
  } catch (Exception e) {
    throw new RuntimeException(e);
  }
  return o;
}

static void set(Class c, String field, Object value) {
  if (c == null) return;
  try {
    Field f = set_findStaticField(c, field);
    makeAccessible(f);
    smartSet(f, null, value);
  } catch (Exception e) {
    throw new RuntimeException(e);
  }
}
  
static Field set_findStaticField(Class<?> c, String field) {
  Class _c = c;
  do {
    for (Field f : _c.getDeclaredFields())
      if (f.getName().equals(field) && (f.getModifiers() & java.lang.reflect.Modifier.STATIC) != 0)
        return f;
    _c = _c.getSuperclass();
  } while (_c != null);
  throw new RuntimeException("Static field '" + field + "' not found in " + c.getName());
}

static Field set_findField(Class<?> c, String field) {
  Class _c = c;
  do {
    for (Field f : _c.getDeclaredFields())
      if (f.getName().equals(field))
        return f;
    _c = _c.getSuperclass();
  } while (_c != null);
  throw new RuntimeException("Field '" + field + "' not found in " + c.getName());
}

static void set(BitSet bs, int idx) {
  { if (bs != null) bs.set(idx); }
}


static <A> void removeAll(Collection<A> a, Collection<A> b) {
  if (a != null && b != null) a.removeAll(b);
}

static <A, B> void removeAll(Map<A, B> a, Collection<A> b) {
  if (a != null && b != null)
    for (A x : b)
      a.remove(x);
}

static <A, B extends A> boolean removeAll(Collection<A> c, B... b) {
  return c != null && b != null && c.removeAll(Arrays.asList(b));
}

static <A, B> void removeAll(Map<A, B> a, A... b) {
  if (a != null && b != null)
    for (A x : b)
      a.remove(x);
}


static void add(BitSet bs, int i) {
  bs.set(i);
}

static <A> boolean add(Collection<A> c, A a) {
  return c != null && c.add(a);
}


static void add(Container c, Component x) {
  addToContainer(c, x);
}


static long add(AtomicLong l, long b) {
  return l.addAndGet(b);
}


static <A extends Component> A _revalidate(A c) {
  return revalidate(c);
}

static void _revalidate(JFrame f) {
  revalidate(f);
}

static void _revalidate(JInternalFrame f) {
  revalidate(f);
}


static Container _getParent(Component c) {
  return getParent(c);
}


static String joinNemptiesWithColon(String... strings) {
  return joinNempties(": ", strings);
}

static String joinNemptiesWithColon(Collection<String> strings) {
  return joinNempties(": ", strings);
}


static String commaCombine(Object... l) {
  return joinNemptiesWithComma(flattenCollectionsAndArrays(ll(l)));
}


static int hashCodeFor(Object a) {
  return a == null ? 0 : a.hashCode();
}


static double sqrt(double x) {
  return Math.sqrt(x);
}


static <A> List<A> minus(Collection<A> a, Object... b) {
  Set set = asSet(b);
  List l = new ArrayList();
  for (Object s : unnull(a))
    if (!set.contains(s))
      l.add(s);
  return l;
}

static BigInteger minus(BigInteger a, BigInteger b) {
  return a.subtract(b);
}


static Complex minus(Complex c) {
  return c == null ? null : complex(-c.re(), -c.im());
}



  static int minus(int a, int b) {
    return a-b;
  }


  static double minus(double a, double b) {
    return a-b;
  }




static Pt ptMinus(Pt a, Pt b) {
  if (b == null) return a;
  return new Pt(a.x-b.x, a.y-b.y);
}


static UnsupportedOperationException unsupportedOperation() {
  throw new UnsupportedOperationException();
}


static long nanos() {
  return nanoTime();
}


static <A extends Component> A markVisualizer(Object visualizer, A a) {
  return setMetaSrc(a, visualizer);
}


static String formatElapsedTimeWithAppropriateUnit(double seconds) {
  if (seconds >= 1) return formatDouble(seconds, 3) + " s";
  double ms = seconds*1000;
  if (ms >= 1) return formatDouble(ms, 3) + " ms";
  double us = ms*1000;
  if (us >= 1) return formatDouble(us, 3) + " µs";
  double ns = us*1000;
  return formatDouble(ns, 3) + " ns";
}


static double nanosToSeconds(double nanos) {
  return nanoSecondsToSeconds(nanos);
}


static JPanel northAndCenterWithMargins(Component n, Component c) {
  return applyDefaultMargin(northAndCenter(withBottomMargin(n), c));
}

static JPanel northAndCenterWithMargins(int margin, Component n, Component c) {
  return applyMargin(margin, northAndCenter(withBottomMargin(margin, n), c));
}


static JPanel westAndEastWithMargin(Component w, Component e) {
  JPanel panel = new JPanel(new BorderLayout());
  panel.add(BorderLayout.WEST, wrap(w));
  panel.add(BorderLayout.CENTER, horizontalStrut(10));
  panel.add(BorderLayout.EAST, wrap(e));
  return panel;
}


static JComponent withLabel(String label, JComponent component) {
  return westAndCenter(jlabel(label + " "), component);
}

static JComponent withLabel(JComponent label, JComponent component) {
  return westAndCenterWithMargin(label, component);
}



static JPanel jpanel(LayoutManager layout) {
  return swingNu(JPanel.class, layout);
}

static JPanel jpanel() {
  return swingNu(JPanel.class);
}


static long toLong(Object o) {
  if (o instanceof Number)
    return ((Number) o).longValue();
  if (o instanceof String)
    return parseLong((String) o);
  return 0;
}


static <A extends JComponent> A fontSizePlus(final int delta, final A c) {
  if (c != null) { swing(() -> { 
    Font font = c.getFont();
    c.setFont(font.deriveFont(font.getSize2D()+delta));
  }); }
  return c;
}


static JScrollPane jscroll_centered_borderless(Component c) {
  return borderlessScrollPane(jscroll_centered(c));
}


static void addMenuItem(JPopupMenu menu, String text, Object action) {
  menu.add(jmenuItem(text, action));
}

static void addMenuItem(JPopupMenu menu, JMenuItem menuItem) {
  if (menu != null && menuItem != null)
    menu.add(menuItem);
}

static void addMenuItem(JMenu menu, String text, Object action) {
  menu.add(jmenuItem(text, action));
}

static void addMenuItem(Menu menu, String text, Object action) {
  menu.add(menuItem(text, action));
}

static void addMenuItem(JMenu menu, JMenuItem menuItem) {
  menu.add(menuItem);
}

static void addMenuItem(JMenuBar menuBar, String text, Runnable action) {
  addMenuItem(menuBar, jmenuItem(text, action));
}

static void addMenuItem(JMenuBar menuBar, JMenuItem menuItem) {
  addDirectMenuItem(menuBar, menuItem);
}




static String copyTextToClipboard(Object _text) {
  String text = str(_text);
  StringSelection selection = new StringSelection(text);
  Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, selection);
  vmBus_send("newClipboardContents", text);
  return text;
}


static <A, B> LinkedHashMap<A, B> cloneLinkedHashMap(Map<A, B> map) {
  return map == null ? new LinkedHashMap() : new LinkedHashMap(map);
}


static <A> A second(List<A> l) {
  return get(l, 1);
}

static <A> A second(Iterable<A> l) {
  if (l == null) return null;
  Iterator<A> it = iterator(l);
  if (!it.hasNext()) return null;
  it.next();
  return it.hasNext() ? it.next() : null;
}

static <A> A second(A[] bla) {
  return bla == null || bla.length <= 1 ? null : bla[1];
}


static <A, B> B second(Pair<A, B> p) {
  return p == null ? null : p.b;
}



static <A, B, C> B second(T3<A, B, C> t) {
  return t == null ? null : t.b;
}



static <A> A second(Producer<A> p) {
  if (p == null) return null;
  if (p.next() == null) return null;
  return p.next();
}


static char second(String s) {
  return charAt(s, 1);
}


static <A, B> B second(Either<A, B> e) {
  return e == null ? null : e.bOpt();
}



static <A> Set<A> setWithNotify(final Runnable onChange) {
  return setWithNotify(new HashSet(), onChange);
}

static <A> Set<A> setWithNotify(Set<A> set, final Runnable onChange) {
  return new NotifyingSet<A>(set) {
    void change() { onChange.run(); }
  };
}


static MenuItem menuItem(String text, final Object r) {
  MenuItem mi = new MenuItem(text);
  mi.addActionListener(actionListener(r));
  return mi;
}


static <A extends JComponent, B> A bindToolTipToTransformedLiveValue(IF1<B, Object> f, LiveValue<B> lv, A c) {
  bindLiveValueListenerToComponent(c, lv,
    new Runnable() {  public void run() { try {  setToolTipText(c, strOrNull(f.get(lv.get()))) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "setToolTipText(c, strOrNull(f.get(lv!)))"; }});
  return c;
}


static <A> JLabel simpleTransformedLiveValueLabel(IF1<A, Object> f, LiveValue<A> lv) {
  return bindJLabelToTransformedLiveValue(f, swing(() -> new JLabel()), lv);
}


static MouseAdapter onMouseDown_anyButton(Component c, Runnable r) {
  return onMouseDown(c, runnableToIVF1(r));
}

static MouseAdapter onMouseDown_anyButton(Component c, IVF1<MouseEvent> f) {
  return c == null || f == null ? null : swing(() -> {
    MouseAdapter ma = new MouseAdapter() {
      public void mousePressed(MouseEvent e) {
        f.get(e);
      }
    };
    c.addMouseListener(ma);
    return ma;
  });
}


static int componentCount(Container c) {
  return numberOfChildren(c);
}




static TitledBorder jRaisedSectionBorder(String title) {
  return swing(() -> {
    Border border = BorderFactory.createBevelBorder(BevelBorder.RAISED);
    return BorderFactory.createTitledBorder(border, title);
  });
}




static <A extends JComponent> A setBorder(Border border, A c) {
  if (c != null) { swing(() -> {  c.setBorder(border); }); }
  return c;
}



static <A extends JComponent> A setBorder(A c, Border border) {
  return setBorder(border, c);
}


static void showPopupMenu(JPopupMenu menu, MouseEvent e) {
  if (menu != null && e != null)
    menu.show(e.getComponent(), e.getX(), e.getY());
}


static Dimension dimensionPlus(int w, int h, Dimension d) {
  return d == null ? null : new Dimension(d.width+w, d.height+h);
}


// Use like this: printVars(+x, +y);
// Or: printVars("bla", +x);
// Or: printVars bla(, +x);
static void printVars(Object... params) {
  printVars_str(params);
}


static Dimension maxDimension(Dimension a, Dimension b) {
  return a == null ? b : b == null ? a
    : new Dimension(max(a.width, b.width), max(a.height, b.height));
}


static <A> AutoCloseable tempAdd(Collection<A> l, A a) {
  if (l == null || l.contains(a)) return null;
  l.add(a);
  return new AutoCloseable() { public String toString() { return "l.remove(a);"; } public void close() throws Exception { l.remove(a); }};
}


static void clearPopupMenu(final JPopupMenu menu) {
  if (menu != null) { swing(() -> {  menu.removeAll(); }); }
}


// finds the byte code path root (dir or jar)
// TODO: inner classes will not be found (need to replace . with $)
static File byteCodePathForClass(Class c) {
  if (c == null) return null;
  ClassLoader cl = getClassLoader(c);
  
  // handle JavaXClassLoader
  Collection<File> files =  (Collection<File>) (getOpt(cl, "files"));
  if (files != null) {
    String name = c.getName().replace('.', '/') + ".class";
    for (File location : files) 
      if (dirOrZipContainsPath(location, name))
        return location;
    throw fail(name + " not found in: " + files);
  }
  
  return null;
}

static File byteCodePathForClass(Object o) {
  return byteCodePathForClass(_getClass(o));
}


static <A> List<A> itemPlusList(A a, Collection<A> l) {
  return concatLists(ll(a), l);
}


static List<String> endingWith_dropSuffix(Collection<String> l, String suffix) {
  List<String> out = new ArrayList();
  for (String s : unnullForIteration(l))
    if (endsWith(s, suffix))
      out.add(dropLast(l(suffix), s));
  return out;
}

static List<String> endingWith_dropSuffix(String suffix, Collection<String> l) {
  return endingWith_dropSuffix(l, suffix);
}


static List<String> standardImports_cache;
static List<String> standardImports() { if (standardImports_cache == null) standardImports_cache = standardImports_load(); return standardImports_cache;}

static List<String> standardImports_load() {
  return ll(
    "java.util.*",
    "java.util.zip.*",
    "java.util.List",
    "java.util.regex.*",
    "java.util.concurrent.*",
    "java.util.concurrent.atomic.*",
    "java.util.concurrent.locks.*",
    "java.util.function.*",
    "javax.swing.*",
    "javax.swing.event.*",
    "javax.swing.text.*",
    "javax.swing.table.*",
    "java.io.*",
    "java.net.*",
    "java.lang.reflect.*",
    "java.lang.ref.*",
    "java.lang.management.*",
    "java.security.*",
    "java.security.spec.*",
    "java.awt.*",
    "java.awt.event.*",
    "java.awt.image.*",
    "java.awt.geom.*",
    "javax.imageio.*",
    "java.math.*");
}




static List<String> classNamesInJarOrDir(File dir) { return classNamesInJarOrDir(dir, ""); }
static List<String> classNamesInJarOrDir(File dir, String prefixInJar) {
  List<String> classes = new ArrayList();
  if (dir == null) {}
  else if (dir.isDirectory()) {
    for (File f : listFiles(dir)) { // TODO: subdirectories
      String s = f.getName();
      if (s.endsWith(".class"))
        classes.add(dropSuffix(".class", s));
    }
  } else if (dir.isFile()) { try {
    JarFile jarFile = new JarFile(dir);
    try {
      Enumeration<JarEntry> e = jarFile.entries();
      while (e.hasMoreElements()) {
        JarEntry je = e.nextElement();
        if (je.isDirectory()
          || je.getName().startsWith("META-INF/")
          || !je.getName().endsWith(".class"))
          continue;
        String className = dropSuffix(".class", je.getName());
        className = dropPrefixOrNull(prefixInJar, className);
        if (className == null) continue;
        if (className.contains("-")) continue;
        className = className.replace('/', '.');
        classes.add(className);
      }
    } finally {
      jarFile.close();
    }
  } catch (Throwable __e) { printStackTrace(__e); }}
  return classes;
}


static List<String> classNamesInLoadedJigsawModules() {
  return concatMap(loadedJigsawModuleNames(), moduleName
    -> classNamesInJigsawModule(moduleName));
}


static <A, B extends A> void addAll(Collection<A> c, Iterable<B> b) {
  if (c != null && b != null) for (A a : b) c.add(a);
}

static <A, B extends A> boolean addAll(Collection<A> c, Collection<B> b) {
  return c != null && b != null && c.addAll(b);
}

static <A, B extends A> boolean addAll(Collection<A> c, B... b) {
  return c != null && b != null && c.addAll(Arrays.asList(b));
}



static <A, B> Map<A, B> addAll(Map<A, B> a, Map<? extends A,? extends B> b) {
  if (a != null && b != null) a.putAll(b);
  return a;
}


static BufferedImage whiteImage(int w, int h) {
  return newBufferedImage(w, h, Color.white);
}

static BufferedImage whiteImage(int size) {
  return whiteImage(size, size);
}


static JLabel setImage(final BufferedImage img, final JLabel lbl) {
  if (lbl != null) { swing(() -> {  lbl.setIcon(imageIcon(img)); }); }
  return lbl;
}

static JLabel setImage(JLabel lbl, BufferedImage img) {
  return setImage(img, lbl);
}

static JLabel setImage(final String imageID, final JLabel lbl) {
  if (lbl != null) { swing(() -> {  lbl.setIcon(imageIcon(imageID)); }); }
  return lbl;
}

static JLabel setImage(JLabel lbl, String imageID) {
  return setImage(imageID, lbl);
}


static <A extends ImageSurface> A setImage(A is, BufferedImage img) {
  { if (is != null) is.setImage(img); }
  return is;
}



static <A extends Component> A onResize(A c, Runnable r) {
  if (c != null && r != null) { swing(() -> { 
    c.addComponentListener(new ComponentAdapter() {
      public void componentResized(ComponentEvent e) {
        pcallF(r);
      }
    });
  }); }
  return c;
}

static <A extends Component> A onResize(Runnable r, A c) {
  return onResize(c, r);
}


static <A extends JComponent> A bindToComponent(final A component, final Runnable onShow, final Runnable onUnShow) {
  { swing(() -> { 
    final Var < Boolean > flag = new Var<>(false);
    component.addAncestorListener(new AncestorListener() {
      public void ancestorAdded(AncestorEvent event) {
        if (flag.get()) print("Warning: bindToComponent logic failure");
        flag.set(true);
        pcallF(onShow);
      }
  
      public void ancestorRemoved(AncestorEvent event) {
        if (!flag.get()) print("Warning: bindToComponent logic failure");
        flag.set(false);
        pcallF(onUnShow);
      }
  
      public void ancestorMoved(AncestorEvent event) {
      }
    });
    if (component.isShowing()) { // Hopefully this matches the AncestorListener logic
      flag.set(true);
      pcallF(onShow);
    }
  }); }
  return component;
}

static <A extends JComponent> A bindToComponent(A component, Runnable onShow) {
  return bindToComponent(component, onShow, null);
}


static <A extends JComponent> void componentPopupMenu2(A component, final VF2<A, JPopupMenu> menuMaker) {
  final WeakReference < A > ref = new WeakReference<>(component);
  componentPopupMenu(component, new VF1<JPopupMenu>() { public void get(JPopupMenu menu) { try { 
    callF(menuMaker, ref.get(), menu);
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "callF(menuMaker, ref!, menu);"; }});
}





// onDrop: voidfunc(File), but may also return false
static <A extends JComponent> A jHandleFileDrop(A c, final Object onDrop) {
  new DropTarget(c, new DropTargetAdapter() {
    public void drop(DropTargetDropEvent e) {
      try {
        Transferable tr = e.getTransferable();
        DataFlavor[] flavors = tr.getTransferDataFlavors();
        for (DataFlavor flavor : flavors) {
          if (flavor.isFlavorJavaFileListType()) {
            e.acceptDrop(e.getDropAction());
            File file = first((List<File>) tr.getTransferData(flavor));
            if (file != null && !isFalse(callF(onDrop, file)))
              e.dropComplete(true);
            return;
          }
        }
      } catch (Throwable __e) { printStackTrace(__e); }
      e.rejectDrop();
    }
  });
  return c;
}

static <A extends JComponent> A jHandleFileDrop(Object onDrop, A c) {
  return jHandleFileDrop(c, onDrop);
}


// onHover is called with null value when mouse moves outside of image surface
// onHover receives image coordinates
static void imageSurfaceOnHover(ImageSurface is, VF1<Pt> onHover) {
  if (is == null || onHover == null) return;
  { swing(() -> { 
    MouseAdapter ma = new MouseAdapter() {
      public void mouseMoved(MouseEvent e) { pick(e); }
      public void mouseEntered(MouseEvent e) { pick(e); }
      public void mouseExited(MouseEvent e) { pick(null); }
      
      void pick(MouseEvent e) {
        callF(onHover, e == null ? (Pt) null : is.pointFromEvent(e));
      }
    };
    is.addMouseMotionListener(ma);
    is.addMouseListener(ma);
  }); }
}

static void imageSurfaceOnHover(ImageSurface is, IVF1<Pt> onHover) {
  imageSurfaceOnHover(is, toVF1(onHover));
}


// IMPORTANT: doesn't work with JTattoo (so not currently in Stefan's OS). listeners are only called on first show+hide
// r : runnable or voidfunc(bool)
static JCheckBoxMenuItem jCheckBoxMenuItem_dyn(String text, IF0<Boolean> checked, Object r) {
  JCheckBoxMenuItem mi = jCheckBoxMenuItem(text, false, r);
  if (checked != null) bindToComponent(mi, new Runnable() {  public void run() { try { 
    boolean b = isTrue(checked.get());
    
    setChecked(mi, b);
  
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "boolean b = isTrue(checked!);\r\n    ifdef jCheckBoxMenuItem_dyn_debug\r\n      p..."; }}, new Runnable() {  public void run() { try { 
    
  ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "ifdef jCheckBoxMenuItem_dyn_debug\r\n      print(\"jCheckBoxMenuItem_dyn: hiding..."; }});
  return mi;
}

static JCheckBoxMenuItem jCheckBoxMenuItem_dyn(String text, IF0<Boolean> checked, IVF1<Boolean> r) {
  return jCheckBoxMenuItem_dyn(text, checked, (Object) r);
}



static JFrame showFullScreen(JComponent c) {
  return showFullScreen(defaultFrameTitle(), c);
}

static JFrame showFullScreen(final String title, final JComponent c) {
  return (JFrame) swingAndWait(new F0<Object>() { public Object get() { try { 
    GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment()
      .getDefaultScreenDevice();
    if (!gd.isFullScreenSupported())
      throw fail("No full-screen mode supported!");
    boolean dec = JFrame.isDefaultLookAndFeelDecorated();
    if (dec) JFrame.setDefaultLookAndFeelDecorated(false);
    final JFrame window = new JFrame(title);
    window.setUndecorated(true);
    if (dec) JFrame.setDefaultLookAndFeelDecorated(true);
    registerEscape(window, new Runnable() {  public void run() { try {  disposeWindow(window) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "disposeWindow(window)"; }});
    window.add(wrap(c));
    gd.setFullScreenWindow(window);
    
    // Only this hides the task bar in Peppermint Linux w/Substance
    for (int i = 100; i <= 1000; i += 100)
      awtLater(i, new Runnable() {  public void run() { try {  window.toFront() ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "window.toFront()"; }});
    
    return window;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment()\r\n      ..."; }});
}


static void selectFile(final String msg, VF1<File> action) {
  selectFile(msg, userDir(), action);
}

static void selectFile(final String msg, final File defaultFile, VF1<File> action) {
  inputFilePath(msg, defaultFile, action);
}


static void saveImage(File f, BufferedImage img) {
  if (hasJPEGExtension(f))
    saveJPG(f, img);
  else
    savePNG(f, img);
}

static void saveImage(BufferedImage img, File f) {
  saveImage(f, img);
}


static <A extends Image> A copyImageToClipboard(A img) {
  TransferableImage trans = new TransferableImage(img);
  Toolkit.getDefaultToolkit().getSystemClipboard().setContents( trans, null);
  vmBus_send("newClipboardContents", img);
  print("Copied image to clipboard (" + img.getWidth(null) + "*" + img.getHeight(null) + " px)");
  return img;
}



static JComponent selectImageSnippet(VF1<String> onSelect) {
  return selectSnippetID_v1(onSelect);
}



static JComponent selectImageSnippet(String defaultID, VF1<String> onSelect) {
  return selectSnippetID_v1(defaultID, onSelect);
}


static BufferedImage cloneClipBufferedImage(BufferedImage src, Rectangle clip) {
  return cloneBufferedImage(clipBufferedImage(src, clip));
}

static BufferedImage cloneClipBufferedImage(BufferedImage src, Rect r) {
  return cloneBufferedImage(clipBufferedImage(src, r));
}

static BufferedImage cloneClipBufferedImage(BufferedImage src, int x, int y, int w, int h) {
  return cloneBufferedImage(clipBufferedImage(src, x, y, w, h));
}






static BufferedImage getImageFromClipboard() { try {
  Transferable t = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
  if (t == null) return null;
  
  List<File> l =  (List<File>) (getTransferData(t, DataFlavor.javaFileListFlavor));
  if (nempty(l))
    return loadImage2(first(l));
  
  if (t.isDataFlavorSupported(DataFlavor.imageFlavor))
    return (BufferedImage) t.getTransferData(DataFlavor.imageFlavor);
  return imageFromDataURL(getTextFromClipboard());
} catch (Exception __e) { throw rethrow(__e); } }


static void popup(final Throwable throwable) {
  popupError(throwable);
}

static void popup(final String msg) {
  print(msg);
  SwingUtilities.invokeLater(new Runnable() {
    public void run() {
      JOptionPane.showMessageDialog(null, msg);
    }
  });
}


static <A> A _print(String s, A a) {
  return print(s, a);
}

static <A> A _print(A a) {
  return print(a);
}

static void _print() {
  print();
}


static boolean hasTransparency(BufferedImage img) {
  return img.getColorModel().hasAlpha();
}


static <A extends Component> A repaint(A c) {
  if (c != null) c.repaint();
  return c;
}


static Dimension getMinimumSize(final Component c) {
  return c == null ? null : swing(new F0<Dimension>() { public Dimension get() { try {  return c.getMinimumSize();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return c.getMinimumSize();"; }});
}


static void scaffoldCalled(Object obj, Object function, Object... args) {
  printShortenedFunctionCall(200, (obj == null ? "" : shorten(20, str(obj)) + " :: ") + function, args);
}


static <A> A printIfScaffoldingEnabled(Object o, A a) { return printIfScaffoldingEnabled(o, "", a); }
static <A> A printIfScaffoldingEnabled(Object o, String s, A a) {
  return printIf(scaffoldingEnabled(o), s, a);
}


static BoolVar componentShowingVar(JComponent component) {
  return swing(() -> {
    BoolVar flag = new BoolVar(component.isShowing());
    
    component.addAncestorListener(new AncestorListener() {
      public void ancestorAdded(AncestorEvent event) {
        if (flag.get()) print("Warning: bindToComponent logic failure");
        flag.set(true);
      }
  
      public void ancestorRemoved(AncestorEvent event) {
        if (!flag.get()) print("Warning: bindToComponent logic failure");
        flag.set(false);
      }
  
      public void ancestorMoved(AncestorEvent event) {
      }
    });
    
    return flag;
  });
}


static WidthAndHeight imageSize(BufferedImage img) {
  return img == null ? null : widthAndHeight(img.getWidth(), img.getHeight());
}


static boolean imagesHaveSameSize(BufferedImage a, BufferedImage b) {
  return a != null && b != null && a.getWidth() == b.getWidth()
    && a.getHeight() == b.getHeight();
}


static Dimension getPreferredSize(final Component c) {
  return c == null ? null : swing(new F0<Dimension>() { public Dimension get() { try {  return c.getPreferredSize();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return c.getPreferredSize();"; }});
}


static boolean scaffoldingEnabled(Object o) {
  return metaGet(o, "scaffolding") != null;
}


static Container getParent(final Component c) {
  return c == null ? null : swing(new F0<Container>() { public Container get() { try {  return c.getParent();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return c.getParent();"; }});
}


static Rectangle toRectangle(Rect r) {
  return r == null ? null : r.getRectangle();
}


static JScrollPane enclosingScrollPane(Component c) {
  while (c.getParent() != null && !(c.getParent() instanceof JViewport) && c.getParent().getComponentCount() == 1) c = c.getParent(); // for jscroll_center
  if (!(c.getParent() instanceof JViewport)) return null;
  c = c.getParent().getParent();
  return c instanceof JScrollPane ? (JScrollPane) c : null;
}


// independent timer
static void awtLater(int delay, final Object r) {
  swingLater(delay, r);
}

static void awtLater(int delay, Runnable r) {
  swingLater(delay, r);
}

static void awtLater(Object r) {
  swingLater(r);
}

static void awtLater(double delaySeconds, Runnable r) {
  swingLater(toMS(delaySeconds), r);
}

// dependent timer (runs only when component is visible)
static void awtLater(JComponent component, int delay, Object r) {
  installTimer(component, r, delay, delay, false);
}

static void awtLater(JFrame frame, int delay, Object r) {
  awtLater(frame.getRootPane(), delay, r);
}


static ImageSurface showFullScreenImageSurface(BufferedImage img) {
  ImageSurface is = jImageSurface(img);
  showFullScreen(jscroll_centered(disposeFrameOnClick(is)));
  return is;
}


static void imageSurface_pixelated(ImageSurface imageSurface) {
  if (imageSurface == null) return;
  imageSurface.setDoubleBuffered(true); // solve flickering when partially obscured
  imageSurface.noAlpha = true;
  imageSurface.interpolationMode = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
  repaint(imageSurface);
}

static void imageSurface_pixelated(ImageSurface imageSurface, boolean pixelated) {
  if (pixelated)
    imageSurface_pixelated(imageSurface);
  else
    imageSurface_unpixelated(imageSurface);
}


static int getWidth(Component c) {
  return c == null ? 0 : (int) swingCall(c, "getWidth");
}


static int getHeight(Component c) {
  return c == null ? 0 : (int) swingCall(c, "getHeight");
}


static void close(AutoCloseable c) {
  _close(c);
}


static void closeAllAndClear(Collection<? extends AutoCloseable> l) {
  if (l == null) return;
  for (AutoCloseable c : cloneList(l)) { try { close(c); } catch (Throwable __e) { printStackTrace(__e); }}
  l.clear();
}


static void revalidateIncludingFullCenterContainer(Component c) {
  if (c == null) return;
  { swing(() -> { 
    c.revalidate();
    var parent = c.getParent();
    if (parent != null && parent.getLayout() instanceof GridBagLayout)
      parent.revalidate();
  }); }
}


static JScrollPane jscroll_center_borderless(Component c) {
  return jscroll_centered_borderless(c);
}


// binary legacy syntax
static String formatFunctionCall(String fname, Object... args) {
  return formatFunctionCall((Object) fname, args);
}
  
static String formatFunctionCall(Object fname, Object... args) {
  return fname + "(" + joinWithComma(allToString(args)) + ")";
}

static String formatFunctionCall(String fname, Iterable args) {
  return formatFunctionCall((Object) fname, args);
}

static String formatFunctionCall(Object fname, Iterable args) {
  return formatFunctionCall(fname, toObjectArray(args));
}


static <A> List<A> listPlus(Collection<A> l, A... more) {
  return concatLists(l, asList(more));
}


static <A extends JComponent> A setEnabled(A c, boolean enable) {
  if (c != null) { swing(() -> {  c.setEnabled(enable); }); }
  return c;
}

static <A extends JComponent> A setEnabled(boolean enable, A c) {
  return setEnabled(c, enable);
}

static void setEnabled(boolean enable, JComponent... l) {
  for (var c : unnullForIteration(l))
    setEnabled(c, enable);
}


static void change() {
  //mainConcepts.allChanged();
  // safe version for now cause function is sometimes included unnecessarily (e.g. by EGDiff)
  callOpt(getOptMC("mainConcepts"), "allChanged");
}


static Class run(String progID, String... args) {
  Class main = hotwire(progID);
  
  callMain(main, args);
  return main;
}


static <A extends Concept> Collection<A> conceptsWhere(Class<A> c, Object... params) {
  return findConceptsWhere(c, params);
}

static Collection<Concept> conceptsWhere(String c, Object... params) {
  return findConceptsWhere(c, params);
}

static <A extends Concept> Collection<A> conceptsWhere(Concepts concepts, Class<A> c, Object... params) {
  return findConceptsWhere(concepts, c, params);
}


// This works when the ctrl key is pressed/released when a Swing window
// is in focus or the mouse is inside a Swing window.
// If you press ctrl before the program starts and the mouse is never
// inside any of the windows, the key press will not be registered.

static AutoCloseable tempAddGlobalCtrlKeyListener(IVF1<Boolean> onStatusChanged) {
  AWTEventListener l = new AWTEventListener() {
    Boolean status;
    
    public void eventDispatched(AWTEvent evt) {
      if (evt instanceof InputEvent) {
        Boolean b = ((InputEvent) evt).isControlDown();
        if (!eq(b, status))
          pcallF_typed(onStatusChanged, status = b);
      }
    }
  };
  
  Toolkit.getDefaultToolkit().addAWTEventListener(l,
    AWTEvent.KEY_EVENT_MASK |
    AWTEvent.MOUSE_EVENT_MASK |
    AWTEvent.MOUSE_MOTION_EVENT_MASK);
  return () -> Toolkit.getDefaultToolkit().removeAWTEventListener(l);
}


static String formatDouble1(double d) {
  return formatDouble(d, 1);
}



static boolean cleanUp_interruptThreads = false; // experimental

static void cleanUp(Object c) {
  if (c == null) return;
  
  if (c instanceof AutoCloseable) { close_pcall((AutoCloseable) c); return; }
  
  if (c instanceof java.util.Timer) { ((java.util.Timer) c).cancel(); return; }
  
  if (c instanceof Collection) { cleanUp((Collection) c); return; }
  if (c instanceof Map) {
    for (Object o : keys((Map) c)) cleanUp(o);
    for (Object o : values((Map) c)) cleanUp(o);
    ((Map) c).clear();
    return;
  }
  //if (!(c instanceof Class)) ret;
  
  try {
    // revoke license
    
    preCleanUp(c);
    
    // unpause
    
    setOpt_raw(c, "ping_pauseAll", false);
    
    // call custom cleanMeUp() and cleanMeUp_*() functions
    
    innerCleanUp(c);
        
    // Java spec says finalize should only be called by GC,
    // but we care to differ.
    // Edit: Not anymore (illegal access warnings)
    /*if (isTrue(vmMap_get('callFinalize)))
      pcallOpt(c, "finalize");*/

    // remove all virtual bots (hope this works)
    
    List androids = (List) getOpt(c, "record_list");
    for (Object android : unnull(androids))
      pcallOpt(android, "dispose"); // heck we'll dispose anything

    // sub-cleanup
    
    List<WeakReference> classes =  (List<WeakReference>) (getOpt(c, "hotwire_classes"));
    if (classes != null)
      for (WeakReference cc : classes) { try {
        cleanUp(cc.get());
      } catch (Throwable __e) { printStackTrace(__e); }}
      
    // interrupt all threads (experimental, they might be doing cleanup?)
    
    if (cleanUp_interruptThreads) {
      List<Thread> threads = registeredThreads(c);
      if (nempty(threads)) {
        print("cleanUp: Interrupting " + n2(threads, "thread") + ": " + joinWithComma(allToString(threads)));
        interruptThreads(threads);
      }
    }
  } catch (Throwable __e) { printStackTrace(__e); }
  
  setOpt_raw(c, "cleaningUp_flag" , false);
  if (c instanceof Class && ((Class) c).getName().equals("main"))
    retireClassLoader(((Class) c).getClassLoader());
 }

static void cleanUp(Collection l) {
  if (l == null) return;
  for (Object c : l)
    cleanUp(c);
  l.clear();
}


static G22Utils g22utils(Concepts cc) {
  return (G22Utils) cc.miscMapGet(G22Utils.class);
}


static <A> A last(List<A> l) {
  return empty(l) ? null : l.get(l.size()-1);
}

static char last(String s) {
  return empty(s) ? '#' : s.charAt(l(s)-1);
}

static byte last(byte[] a) {
  return l(a) != 0 ? a[l(a)-1] : 0;
}

static int last(int[] a) {
  return l(a) != 0 ? a[l(a)-1] : 0;
}

static double last(double[] a) {
  return l(a) != 0 ? a[l(a)-1] : 0;
}

static <A> A last(A[] a) {
  return l(a) != 0 ? a[l(a)-1] : null;
}

static <A> A last(Iterator<A> it) {
  A a = null;
  while  (it.hasNext()) { ping(); a = it.next(); }
  return a;
}

static <A> A last(Collection<A> l) {
  if (l == null) return null;
  if (l instanceof List) return (A) last((List) l);
  if (l instanceof SortedSet) return (A) last((SortedSet) l);
  Iterator<A> it = iterator(l);
  A a = null;
  while  (it.hasNext()) { ping(); a = it.next(); }
  return a;
}

static <A> A last(SortedSet<A> l) {
  return l == null ? null : l.last();
}










static <A> A last(CompactLinkedHashSet<A> set) {
  return set == null ? null : set.last();
}



static <A> void replaceLast(List<A> l, A a) {
  replaceLastElement(l, a);
}


static <A> A[] toTypedArray(Class<A> type, Iterable<A> c) {
  return toArray(c, type);
}


static <A> String pnlToLines(String prefix, Iterable<A> l) {
  return pnlToString(prefix, l);
}



static <A> String pnlToLines(Iterable<A> l) {
  return pnlToString(l);
}



static <A> String pnlToLines(A[] l) {
  return pnlToString(l);
}



static <A, B> String pnlToLines(Map<A, B> map) {
  return pnlToString(map);
}


static <A, B> String pnlToLines(MultiMap<A, B> map) {
  return pnlToString(map);
}


// Use like this: renderRecordVars("MyRecord", +x, +y)
static String formatRecordVars(String recordName, Object... params) {
  return renderRecordVars(recordName, params);
}


static RuntimeException rethrowAndAppendToMessage(Throwable t, String msg) {
  String haveMsg = t.getMessage();
  if (empty(msg) || endsWith(haveMsg, " " + msg))
    throw rethrow(t);
  throw new RuntimeException(joinWithSpace(t.getMessage(), msg), t);
}


static String squareBracketed(String s) {
  return "[" + s + "]";
}


static boolean is(String a, String b) {
  
  return false;
}


static String assertIdentifier(String s) {
  return assertIsIdentifier(s);
}

static String assertIdentifier(String msg, String s) {
  return assertIsIdentifier(msg, s);
}


static boolean isIdentifier(String s) {
  return isJavaIdentifier(s);
}


static boolean startsWithDigit(String s) {
  return nempty(s) && isDigit(s.charAt(0));
}


static boolean isInteger(String s) {
  int n = l(s);
  if (n == 0) return false;
  int i = 0;
  if (s.charAt(0) == '-')
    if (++i >= n) return false;
  while (i < n) {
    char c = s.charAt(i);
    if (c < '0' || c > '9') return false;
    ++i;
  }
  return true;
}


static float parseFloat(String s) {
  return Float.parseFloat(s);
}


// This is a bit rough... finds static and non-static methods.
// It's usually just for error messages though, so no worries.

static boolean hasMethodNamed(Object obj, String method) {
  if (obj == null) return false;
  if (obj instanceof Class)
    return hasMethodNamed((Class) obj, method);
  return hasMethodNamed(obj.getClass(), method);
}

static boolean hasMethodNamed(Class c, String method) {
  while (c != null) {
    for (Method m : c.getDeclaredMethods())
      if (m.getName().equals(method))
        return true;
    c = c.getSuperclass();
  }
  return false;
}


static Field getField(Object o, String field) {
  if (o == null) return null;
  return setOpt_findField(_getClass(o), field);
}

static Object getField(Field field, Object o) {
  return fieldGet(field, o);
}


static <A> List<A> reversed(Iterable<A> l) {
  return reversedList(l);
}

static <A> List<A> reversed(A[] l) {
  return reversedList(asList(l));
}

static String reversed(String s) {
  return reversedString(s);
}


// assumes map never contains null values
static <A, B> AutoCloseable tempMapPutAll(Map<A, B> map, Map<A, B> toAdd) {
  if (map != null && nempty(toAdd)) {
    List<Pair<A, B>> toRestore = new ArrayList();
    for (var __0 : _entrySet( toAdd)) { var key = __0.getKey(); var value = __0.getValue(); 
      if (key != null && value != null) {
        B old = map.put(key, value);
        toRestore.add(pair(key, value));
      }
    }
    return () -> {
      for (var p : toRestore)
        mapPutOrRemove(map, p.a, p.b);
    };
  }
  return null;
}



static <A, B> Map<A, B> mapWithSingleValue(Collection<A> l, B b) {
  HashMap<A, B> map = new HashMap();
  for (A a : unnull(l))
    map.put(a, b);
  return map;
}


// assumes map never contains null values
static <A, B> AutoCloseable tempMapPut(Map<A, B> map, A key, B value) {
  if (map != null && key != null && value != null) {
    B old = map.put(key, value);
    return () -> mapPutOrRemove(map, key, old);
  }
  return null;
}



static TreeSet mapToTreeSet(Object f, Iterable l) {
  TreeSet x = new TreeSet();
  if (l != null) for (Object o : l)
    x.add(callF(f, o));
  return x;
}

static <A, B> TreeSet<B> mapToTreeSet(IF1<A, B> f, Iterable<A> l) {
  TreeSet<B> x = new TreeSet();
  if (l != null) for (var o : l)
    x.add(f.get(o));
  return x;
}

static <A, B> TreeSet<B> mapToTreeSet(Iterable<A> l, IF1<A, B> f) {
  return mapToTreeSet(f, l);
}


static boolean isAnonymousClassName(String s) {
  for (int i = 0; i < l(s); i++)
    if (s.charAt(i) == '$' && Character.isDigit(s.charAt(i+1)))
      return true;
  return false;
}


static String longestPrefixInTreeSet(String s, TreeSet<String> set) {
  return longestPrefixInNavigableSet(s, set);
}


static <A> List<A> itemPlus(A a, Collection<A> l) {
  return itemPlusList(a, l);
}


static List<String> standardImports_fullyImportedPackages() {
  return endingWith_dropSuffix(standardImports(), ".*");
}


static JPanel withBorder(javax.swing.border.Border border, final Component c) {
  return swing(new F0<JPanel>() { public JPanel get() { try { 
    JPanel p = new JPanel(new BorderLayout());
    p.setBorder(border);
    p.add(c);
    return p;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "JPanel p = new JPanel(new BorderLayout);\r\n    p.setBorder(border);\r\n    p.add..."; }});
}


static int getMinimumHeight(final Component c) {
  return getMinimumSize(c).height;
}


static boolean isOneOfSingleChars(String s, char... chars) {
  if (l(s) != 1) return false;
  char real = s.charAt(0);
  for (char c : unnullForIteration(chars))
    if (real == c)
      return true;
  return false;
}


static String form(Object contents, Object... params) {
  return hform(contents, params);
}


static void cMigrateField(Concept c, String oldField, String newField) {
  if (c == null) return;
  Object oldValue = cget(c, oldField);
  Object newValue = cget(c, newField);
  if (oldValue != null && newValue == null) { try {
    String name = shortDynName(c);
    print("Migrating " + name + "." + oldField + " -> " + name + "." + newField + " [id " + c.id + "]");
    cset(c, newField, oldValue);
    cset(c, oldField, null);
  } catch (Throwable __e) { printStackTrace(__e); }}
}


// Use like this: printVars_str(+x, +y);
// Or: printVars("bla", +x);
// Or: printVars bla(+x);
static void printVars_shorten(Object... params) {
  print(renderVars_stringifier(o -> str_shorten(o), params));
}


static String shortDynName(Object o) {
  return shortDynamicClassName(o);
}


static String nLines(long n) { return n2(n, "line"); }
static String nLines(Collection l) { return nLines(l(l)); }
static String nLines(String s) { return nLines(countLines(s)); }


static String nChars(long n) { return n2(n, "char"); }
static String nChars(String s) { return nChars(l(s)); }


static String indentx(String s) {
  return indentx(indent_default, s);
}

static String indentx(int n, String s) {
  return dropSuffix(repeat(' ', n), indent(n, s));
}

static String indentx(String indent, String s) {
  return dropSuffix(indent, indent(indent, s));
}


static void appendToTextFile(File file, String s) {
  appendToFile(file, s);
}

static void appendToTextFile(String file, String s) {
  appendToTextFile(programFile(file), s);
}




static String dateWithMSUTC() { return dateWithMSUTC(now()); }
static String dateWithMSUTC(long time) {
  var format = simpleDateFormat_UTC("yyyy/MM/dd");
  return format.format(time) + " " + formatUTCWithMS_24(time);
}


static String firstToUpper(String s) {
  if (empty(s)) return s;
  return Character.toUpperCase(s.charAt(0)) + s.substring(1);
}


static <A> A getVar(IF0<A> v) {
  return v == null ? null : v.get();
}

static <A> A getVar(Optional<A> v) {
  return v == null ? null : v.orElse(null);
}




static Map<Integer, Integer> rsyntaxTextArea_fixNumPad_map = litmap(
  KeyEvent.VK_UP, KeyEvent.VK_KP_UP,
  KeyEvent.VK_DOWN, KeyEvent.VK_KP_DOWN,
  KeyEvent.VK_LEFT, KeyEvent.VK_KP_LEFT,
  KeyEvent.VK_RIGHT, KeyEvent.VK_KP_RIGHT
);

static boolean rsyntaxTextArea_fixNumPad_done = false;

static void rsyntaxTextArea_fixNumPad() {
  if (rsyntaxTextArea_fixNumPad_done) return;
  RSyntaxTextAreaDefaultInputMap inputMap = new RSyntaxTextAreaDefaultInputMap();
  for (KeyStroke key : inputMap.keys()) {
    Integer code = rsyntaxTextArea_fixNumPad_map.get(key.getKeyCode());
    if (code != null)
      inputMap.put(KeyStroke.getKeyStroke(code, key.getModifiers()),
        inputMap.get(key));
  }
  UIManager.put("RSyntaxTextAreaUI.inputMap", inputMap);
  //set rsyntaxtextarea_done;
}


static RSyntaxTextArea javaxSyntaxTextArea() {
  return swing(new F0<RSyntaxTextArea>() { public RSyntaxTextArea get() { try { 
    RSyntaxTextArea textArea = new RSyntaxTextArea(1, 10);
    initSyntaxTextArea(textArea);
    textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
    return textArea;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "RSyntaxTextArea textArea = new RSyntaxTextArea(1, 10);\r\n    initSyntaxTextAre..."; }});
}


static List<JMenu> getMenus(JMenuBar mb) {
  if (mb == null) return null;
  return countIteratorAsList(mb.getMenuCount(), i -> mb.getMenu(i));
}


static List<JMenuItem> getMenuItems(JMenu menu) {
  if (menu == null) return null;
  return nonNulls(countIterator(menu.getItemCount(), i -> menu.getItem(i)));
}


static void showDialogOnSameScreen(JDialog dialog, Component c) { swing(() -> { 
  if (dialog == null) return;
  Window w = getWindow(c);
  if (w != null)
    centerWindowWithin(dialog, screenBounds_safe(screenNrOfWindow(w)));
  dialog.setVisible(true);
}); }



static String getText(final AbstractButton c) {
  return c == null ? "" : (String) swingAndWait(new F0<Object>() { public Object get() { try {  return c.getText();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return c.getText();"; }});
}

static String getText(final JTextComponent c) {
  return c == null ? "" : (String) swingAndWait(new F0<Object>() { public Object get() { try {  return c.getText();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return c.getText();"; }});
}

static String getText(final JLabel l) {
  return l == null ? "" : (String) swingAndWait(new F0<Object>() { public Object get() { try {  return l.getText();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return l.getText();"; }});
}

// returns the contents of text field for editable combo box
static String getText(final JComboBox cb) {
  if (cb == null) return null;
  if (isEditableComboBox(cb))
    return unnull((String) cb.getEditor().getItem());
  else
    return str(cb.getSelectedItem());
}





static Font deriveFont(Font font, float size) {
  return font == null ? null : font.deriveFont(size);
}


static <A, B> List<B> getAll(Map<A, B> map, Collection<A> l) {
  return lookupAllOpt(map, l);
}

static <A, B> List<B> getAll(Collection<A> l, Map<A, B> map) {
  return lookupAllOpt(map, l);
}

static <A, B extends IF0<A>> List<A> getAll(Iterable<B> l) {
  return getVars(l);
}


static NavigableMap synchroTreeMap() {
  return synchroNavigableMap(new TreeMap());
}



static Object _defaultClassFinder_value = defaultDefaultClassFinder();

static Object _defaultClassFinder() {
  return _defaultClassFinder_value;
}


static boolean hasConcept(Class<? extends Concept> c, Object... params) {
  return findConceptWhere(c, params) != null;
}


static String getDBProgramID_id;

static String getDBProgramID() {
  return nempty(getDBProgramID_id) ? getDBProgramID_id : programIDWithCase();
}


static Object load(String varName) {
  readLocally(varName);
  return get(mc(), varName);
}

static Object load(String progID, String varName) {
  readLocally(progID, varName);
  return get(mc(), varName);
}


static void clearConcepts() {
  db_mainConcepts().clearConcepts();
}

static void clearConcepts(Concepts concepts) {
  concepts.clearConcepts();
}


static Map unstructureMap(String s) {
  return (Map) unstructure(s);
}

static Map unstructureMap(String text, boolean allDynamic, Object classFinder) {
  return (Map) unstructure(text, allDynamic, classFinder);
}


static boolean exceptionMessageContains(Throwable e, String s) {
  return cic(getInnerMessage(e), s);
}


static void printShortException(Throwable e) {
  print(exceptionToStringShort(e));
}

static void printShortException(String s, Throwable e) {
  print(s, exceptionToStringShort(e));
}


static Object unstructureGZFile(File f) { return unstructureGZFile(f, null); }
static Object unstructureGZFile(File f, IF1<String, Class> classFinder) { try {
  if (!fileExists(f)) return null;
  BufferedReader reader = utf8BufferedReader(gzInputStream(f));
  return unstructure_tok(javaTokC_noMLS_onReader(reader), false, classFinder);
} catch (Exception __e) { throw rethrow(__e); } }


static IF1 toIF1(final Object f) {
  if (f == null) return null;
  if (f instanceof IF1) return (IF1) f;
  if (isString(f)) {
    
      throw fail("callF_legacy");
    
    
  }
  return new IF1() {
    public Object get(Object a) { return callF(f, a); }
  };
}


static int done_minPrint = 10;

static long done(long startTime, String desc) {
  long time = now()-startTime;
  if (time >= done_minPrint)
    print(desc + " [" + time + " ms]");
  return time;
}

static long done(String desc, long startTime) {
  return done(startTime, desc);
}

static long done(long startTime) {
  return done(startTime, "");
}


static RemoteDB connectToDBOpt(String dbNameOrID) { try {
  return new RemoteDB(dbNameOrID);
} catch (Throwable __e) { return null; } }




// TODO: cyclic structures involving certain lists & sets



static Object unstructure(String text) {
  return unstructure(text, false);
}

static Object unstructure(String text, boolean allDynamic) {
  return unstructure(text, allDynamic, null);
}

static Object unstructure(String text, IF1<String, Class> classFinder) {
  return unstructure(text, false, classFinder);
}

static int structure_internStringsLongerThan = 50;
static int unstructure_unquoteBufSize = 100;

static int unstructure_tokrefs; // stats

abstract static class unstructure_Receiver {
  abstract void set(Object o);
}

// classFinder: func(name) -> class (optional)
static Object unstructure(String text, boolean allDynamic,
  Object classFinder) {
  if (text == null) return null;
  return unstructure_tok(javaTokC_noMLS_iterator(text), allDynamic, classFinder);
}

static Object unstructure_reader(BufferedReader reader) {
  return unstructure_tok(javaTokC_noMLS_onReader(reader), false, null);
}

interface unstructure_Handler {
  void parse(int refID, int tokIndex, unstructure_Receiver out);
}
  
static Object unstructure_tok(final Producer<String> tok, final boolean allDynamic, final Object _classFinder) {
  final boolean debug = unstructure_debug;
  
  final class X {
    int i = -1;
    final Object classFinder = _classFinder != null ? _classFinder : _defaultClassFinder();
    String mcDollar = actualMCDollar();

    // use Eclipse primitive collection if possible (smaller & hopefully faster?)
    
    
    HashMap<Integer, Object> refs = new HashMap();
    HashMap<Integer, Object> tokrefs = new HashMap();
    
    
    HashSet<String> concepts = new HashSet();
    List<Runnable> stack = new ArrayList();
    Map<String, String> baseClassMap = new HashMap();
    HashMap<Class, Constructor> innerClassConstructors = new HashMap();
    String curT;
    char[] unquoteBuf = new char[unstructure_unquoteBufSize];
    
    // value is a class or a Handler
    final HashMap<String, Object> handlers = new HashMap();
    
    X() {
      try {
        Class mc =  (Class) (callF(_classFinder, "<main>"));
        if (mc != null) mcDollar = mc.getName() + "$";
      } catch (Throwable __e) { printStackTrace(__e); }
      
      makeHandlers();
    }
    
    
    
    void makeHandlers() {
      unstructure_Handler h;
      
      handlers.put("bigint", (unstructure_Handler) (refID, tokIndex, out)
        -> out.set(parseBigInt()));
        
      handlers.put("d", (unstructure_Handler) (refID, tokIndex, out)
        -> out.set(parseDouble()));
        
      handlers.put("fl", (unstructure_Handler) (refID, tokIndex, out)
        -> out.set(parseFloat()));
        
      handlers.put("sh", (unstructure_Handler) (refID, tokIndex, out) -> {
        consume();
        String t = tpp();
        if (t.equals("-")) {
          t = tpp();
          out.set((short) (-parseInt(t))); return;
        }
        out.set((short) parseInt(t));
      });
      
      handlers.put("enum", (unstructure_Handler) (refID, tokIndex, out) -> {
        consume();
        String t = tpp();
        assertTrue(isJavaIdentifier(t));
        String fullClassName = mcDollar + t;
        Class _c = findAClass(fullClassName);
        if (_c == null) throw fail("Enum class not found: " + fullClassName);
        int ordinal = parseInt(tpp());
        out.set(_c.getEnumConstants()[ordinal]);
      });

      handlers.put("false", h = (unstructure_Handler) (refID, tokIndex, out) -> {
        consume(); out.set(false);
      });
      handlers.put("f", h);
      
      handlers.put("true", h = (unstructure_Handler) (refID, tokIndex, out) -> {
        consume(); out.set(true);
      });
      handlers.put("t", h);
      
      handlers.put("{", (unstructure_Handler) (refID, tokIndex, out) -> parseMap(out));
      
      handlers.put("[", (unstructure_Handler) (refID, tokIndex, out) -> {
        ArrayList l = new ArrayList();
        if (refID >= 0) refs.put(refID, l);
        this.parseList(l, out);
      });
      
      handlers.put("bitset", (unstructure_Handler) (refID, tokIndex, out) -> parseBitSet(out));
      
      handlers.put("array", h = (unstructure_Handler) (refID, tokIndex, out) -> parseArray(out));
      handlers.put("intarray", h);
      handlers.put("dblarray", h);
      
    } // end of makeHandlers - add more handlers here

    Class findAClass(String fullClassName) { try {
      return classFinder != null ? (Class) callF(classFinder, fullClassName) : findClass_fullName(fullClassName);
    } catch (Throwable __e) { return null; } }
    
    String unquote(String s) {
      return unquoteUsingCharArray(s, unquoteBuf); 
    }

    // look at current token
    String t() {
      return curT;
    }
    
    // get current token, move to next
    String tpp() {
      String t = curT;
      consume();
      return t;
    }
    
    void parse(final unstructure_Receiver out) {
      String t = t();
      
      int refID;
      if (structure_isMarker(t, 0, l(t))) {
        refID = parseInt(t.substring(1));
        consume();
      } else refID = -1;
      
      // if (debug) print("parse: " + quote(t));
      
      final int tokIndex = i;  
      parse_inner(refID, tokIndex, new unstructure_Receiver() {
        void set(Object o) {
          if (refID >= 0)
            refs.put(refID, o);
          if (o != null)
            tokrefs.put(tokIndex, o);
          out.set(o);
        }
      });
    }
    
    void parse_inner(int refID, int tokIndex, unstructure_Receiver out) {
      String t = t();
      
      // if (debug) print("parse_inner: " + quote(t));
      
      Object handler = handlers.get(t);
      if (handler instanceof unstructure_Handler)
        { ((unstructure_Handler) handler).parse(refID, tokIndex, out); return; }
        
      Class c =  (Class) handler;
      if (c == null) {
        if (t.startsWith("\"")) {
          String s = internIfLongerThan(unquote(tpp()), structure_internStringsLongerThan);
          out.set(s); return;
        }
        
        if (t.startsWith("'")) {
          out.set(unquoteCharacter(tpp())); return;
        }
        if (t.equals("-")) {
          consume();
          t = tpp();
          out.set(isLongConstant(t) ? (Object) (-parseLong(t)) : (Object) (-parseInt(t))); return;
        }
        if (isInteger(t) || isLongConstant(t)) {
          consume();
          //if (debug) print("isLongConstant " + quote(t) + " => " + isLongConstant(t));
          if (isLongConstant(t)) {
            out.set(parseLong(t)); return;
          }
          long l = parseLong(t);
          boolean isInt = l == (int) l;
          
          out.set(isInt ? (Object) Integer.valueOf((int) l) : (Object) Long.valueOf(l)); return;
        }
        if (t.equals("-")) {
          consume();
          t = tpp();
          out.set(isLongConstant(t) ? (Object) (-parseLong(t)) : (Object) (-parseInt(t))); return;
        }
        if (isInteger(t) || isLongConstant(t)) {
          consume();
          //if (debug) print("isLongConstant " + quote(t) + " => " + isLongConstant(t));
          if (isLongConstant(t)) {
            out.set(parseLong(t)); return;
          }
          long l = parseLong(t);
          boolean isInt = l == (int) l;
          
          out.set(isInt ? (Object) Integer.valueOf((int) l) : (Object) Long.valueOf(l)); return;
        }
        
        if (t.equals("File")) {
          consume();
          File f = new File(unquote(tpp()));
          out.set(f); return;
        }
        
        if (t.startsWith("r") && isInteger(t.substring(1))) {
          consume();
          int ref = Integer.parseInt(t.substring(1));
          Object o = refs.get(ref);
          if (o == null)
            warn("unsatisfied back reference " + ref);
          out.set(o); return;
        }
      
        if (t.startsWith("t") && isInteger(t.substring(1))) {
          consume();
          int ref = Integer.parseInt(t.substring(1));
          Object o = tokrefs.get(ref);
          if (o == null)
            warn("unsatisfied token reference " + ref + " at " + tokIndex);
          out.set(o); return;
        }
        
        if (t.equals("hashset")) { parseHashSet(out); return; }
        if (t.equals("lhs")) { parseLinkedHashSet(out); return; }
        if (t.equals("treeset")) { parseTreeSet(out); return; }
        if (t.equals("ciset")) { parseCISet(out); return; }
        
        if (eqOneOf(t, "hashmap", "hm")) {
          consume();
          parseMap(new HashMap(), out);
          return;
        }
        if (t.equals("lhm")) {
          consume();
          parseMap(new LinkedHashMap(), out);
          return;
        }
        if (t.equals("tm")) {
          consume();
          parseMap(new TreeMap(), out);
          return;
        }
        if (t.equals("cimap")) {
          consume();
          parseMap(ciMap(), out);
          return;
        }
        
        if (t.equals("ll")) {
          consume();
          LinkedList l = new LinkedList();
          if (refID >= 0) refs.put(refID, l);
          { parseList(l, out); return; }
        }

        if (t.equals("syncLL")) { // legacy
          consume();
          { parseList(synchroLinkedList(), out); return; }
        }

        if (t.equals("sync")) {
          consume();
          { parse(new unstructure_Receiver() {
            void set(Object value) {
              if (value instanceof Map) {
                 // Java 7
                if (value instanceof NavigableMap)
                  { out.set(synchroNavigableMap((NavigableMap) value)); return; }
                
                if (value instanceof SortedMap)
                  { out.set(synchroSortedMap((SortedMap) value)); return; }
                { out.set(synchroMap((Map) value)); return; }
              } else
                { out.set(synchroList((List) value)); return; }
            }
          }); return; }
        }
        
        if (t.equals("ba")) {
          consume();
          String hex = unquote(tpp());
          out.set(hexToBytes(hex)); return;
        }
        if (t.equals("boolarray")) {
          consume();
          int n = parseInt(tpp());
          String hex = unquote(tpp());
          out.set(boolArrayFromBytes(hexToBytes(hex), n)); return;
        }
        if (t.equals("class")) {
          out.set(parseClass()); return;
        }
        if (t.equals("l")) {
          parseLisp(out); return;
        }
        if (t.equals("null")) {
          consume(); out.set(null); return;
        }
        
        if (eq(t, "c")) {
          consume();
          t = t();
          assertTrue(isJavaIdentifier(t));
          concepts.add(t);
        }
        
        // custom deserialization (new static method method)
        if (eq(t, "cu")) {
          consume();
          t = tpp();
          assertTrue(isJavaIdentifier(t));
          String fullClassName = mcDollar + t;
          Class _c = findAClass(fullClassName);
          if (_c == null) throw fail("Class not found: " + fullClassName);
          parse(new unstructure_Receiver() {
            void set(Object value) {
              
              out.set(call(_c, "_deserialize", value));
            }
          });
          return;
        }
      }
      
      if (eq(t, "j")) {
        consume();
        out.set(parseJava()); return;
      }
      
      if (eq(t, "bc")) {
        consume();
        String c1 = tpp();
        String c2 = tpp();
        baseClassMap.put(c1, c2);
        { parse_inner(refID, i, out); return; }
      }
      
      // add more tokens here

      // Now we want to find our target class c
      // Have we failed to look up the class before?
      //bool seenBefore = handlers.containsKey(cname);

      // If we have seen the class before, we skip all of this
      // and simply leave c as null
      // TODO - how do we fill className?
      //if (!seenBefore) {
        if (c == null && !isJavaIdentifier(t))
          throw new RuntimeException("Unknown token " + (i+1) + ": " + quote(t));
          
        // any other class name (or package name)
        consume();
        String className, fullClassName;
        
        // Is it a package name?
        if (eq(t(), ".")) {
          consume();
          className = fullClassName = t + "." + assertIdentifier(tpp());
        } else {
          className = t;
          fullClassName = mcDollar + t;
        }
        
        if (c == null && !allDynamic) {
          // First, find class
          c = findAClass(fullClassName);
          handlers.put(className, c);
        }
        
        // check for existing base class
        if (c == null && !allDynamic) {
          Set<String> seen = new HashSet();
          String parent = className;
          while (true) {
            String baseName = baseClassMap.get(parent);
            if (baseName == null)
              break;
            if (!seen.add(baseName))
              throw fail("Cyclic superclass info: " + baseName);
            c = findAClass(mcDollar + baseName);
            if (c == null)
              print("Base class " + baseName + " of " + parent +  " doesn't exist either");
            else if (isAbstract(c))
              print("Can't instantiate abstract base class: " + c);
            else {
              printVars_str("Reverting to base class", "className", className, "baseName", baseName, "c", c);
              handlers.put(className, c);
              break;
            }
            parent = baseName;
          }
        }
      //}
          
      // Check if it has an outer reference
      boolean hasBracket = eq(t(), "(");
      if (hasBracket) consume();
      boolean hasOuter = hasBracket && startsWith(t(), "this$");
      
      DynamicObject dO = null;
      Object o = null;
      final String thingName = t;
      if (c != null) {
        if (hasOuter) try {
          Constructor ctor = innerClassConstructors.get(c);
          if (ctor == null)
            innerClassConstructors.put(c, ctor = nuStubInnerObject_findConstructor(c, classFinder));
          o = ctor.newInstance(new Object[] {null});
        } catch (Exception e) {
          print("Error deserializing " + c + ": " + e);
          o = nuEmptyObject(c);
        } else
          o = nuEmptyObject(c);
        if (o instanceof DynamicObject) dO = (DynamicObject) o;
      } else {
        if (concepts.contains(t) && (c = findAClass(mcDollar + "Concept")) != null)
          o = dO = (DynamicObject) nuEmptyObject(c);
        else
          dO = new DynamicObject();
        dO.className = className;
        
      }
      
      // Save in references list early because contents of object
      // might link back to main object
      
      if (refID >= 0)
        refs.put(refID, o != null ? o : dO);
      tokrefs.put(tokIndex, o != null ? o : dO);
      
      // NOW parse the fields!
      
      HashMap<String, Object> fields = new HashMap(); // no longer preserving order (why did we do this?)
      Object _o = o;
      DynamicObject _dO = dO;
      if (hasBracket) {
        stack.add(new Runnable() {  public void run() { try { 
          
          if (eq(t(), ",")) consume();
          if (eq(t(), ")")) {
            consume(")");
            objRead(_o, _dO, fields, hasOuter);
            out.set(_o != null ? _o : _dO);
          } else {
            final String key = unquote(tpp());
            String t = tpp();
            if (!eq(t, "="))
              throw fail("= expected, got " + t + " after " + quote(key) + " in object " + thingName /*+ " " + sfu(fields)*/);
            stack.add(this);
            parse(new unstructure_Receiver() {
              void set(Object value) {
                fields.put(key, value);
                /*ifdef unstructure_debug
                  print("Got field value " + value + ", next token: " + t());
                endifdef*/
                //if (eq(t(), ",")) consume();
              }
            });
          }
        
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "ifdef unstructure_debug\r\n            print(\"in object values, token: \" + t())..."; }});
      } else {
        objRead(o, dO, fields, hasOuter);
        out.set(o != null ? o : dO);
      }
    }
    
    void objRead(Object o, DynamicObject dO, Map<String, Object> fields, boolean hasOuter) {
      
      
      // translate between diferent compilers (this$0 vs this$1)
      Object outer = fields.get("this$0");
      if (outer != null) fields.put("this$1", outer);
      else {
        outer = fields.get("this$1");
        if (outer != null) fields.put("this$0", outer);
      }
      
      if (o != null) {
        if (dO != null) {
          
          setOptAllDyn_pcall(dO, fields);
        } else {
          setOptAll_pcall(o, fields);
          
        }
        if (hasOuter)
          fixOuterRefs(o);
      } else for (Map.Entry<String, Object> e : fields.entrySet())
        setDynObjectValue(dO, intern(e.getKey()), e.getValue());

      if (o != null)
        pcallOpt_noArgs(o, "_doneLoading");
    }
    
    void parseSet(final Set set, final unstructure_Receiver out) {
      this.parseList(new ArrayList(), new unstructure_Receiver() {
        void set(Object o) {
          set.addAll((List) o);
          out.set(set);
        }
      });
    }
    
    void parseLisp(final unstructure_Receiver out) {
      
      
      throw fail("class Lisp not included");
    }
    
    void parseBitSet(final unstructure_Receiver out) {
      consume("bitset");
      consume("{");
      final BitSet bs = new BitSet();
      stack.add(new Runnable() {  public void run() { try { 
        if (eq(t(), "}")) {
          consume("}");
          out.set(bs);
        } else {
          stack.add(this);
          parse(new unstructure_Receiver() {
            void set(Object o) {
              bs.set((Integer) o);
              if (eq(t(), ",")) consume();
            }
          });
        }
      
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "if (eq(t(), \"}\")) {\r\n          consume(\"}\");\r\n          out.set(bs);\r\n       ..."; }});
    }
    
    void parseList(final List list, final unstructure_Receiver out) {
      tokrefs.put(i, list);
      consume("[");
      stack.add(new Runnable() {  public void run() { try { 
        if (eq(t(), "]")) {
          consume();
          
          out.set(list);
        } else {
          stack.add(this);
          parse(new unstructure_Receiver() {
            void set(Object o) {
              //if (debug) print("List element type: " + getClassName(o));
              list.add(o);
              if (eq(t(), ",")) consume();
            }
          });
        }
      
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "if (eq(t(), \"]\")) {\r\n          consume();\r\n          ifdef unstructure_debug\r..."; }});
    }
    
    void parseArray(unstructure_Receiver out) {
      String _type = tpp();
      int dims;

      if (eq(t(), "S")) { // string array
        _type = "S";
        consume();
      }
      
      if (eq(t(), "/")) { // multi-dimensional array
        consume();
        dims = parseInt(tpp());
      } else
        dims = 1;
      
      consume("{");
      List list = new ArrayList();
      String type = _type;
      
      stack.add(new Runnable() {  public void run() { try { 
        if (eq(t(), "}")) {
          consume("}");
          if (dims > 1) {
            Class atype;
            if (type.equals("intarray")) atype = int.class;
            else if (type.equals("S")) atype = String.class;
            else throw todo("multi-dimensional arrays of other types");
            
            out.set(list.toArray((Object[]) newMultiDimensionalOuterArray(atype, dims, l(list))));
          } else
            out.set(
              type.equals("intarray") ? toIntArray(list)
              : type.equals("dblarray") ? toDoubleArray(list)
              : type.equals("S") ? toStringArray(list)
              : list.toArray());
        } else {
          stack.add(this);
          parse(new unstructure_Receiver() {
            void set(Object o) {
              list.add(o);
              if (eq(t(), ",")) consume();
            }
          });
        }
      
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "if (eq(t(), \"}\")) {\r\n          consume(\"}\");\r\n          if (dims > 1) {\r\n    ..."; }});
    }
    
    Object parseClass() {
      consume("class");
      consume("(");
      String name = unquote(tpp());
      consume(")");
      Class c = allDynamic ? null : findAClass(name);
      if (c != null) return c;
      DynamicObject dO = new DynamicObject();
      dO.className = "java.lang.Class";
      name = dropPrefix(mcDollar, name);
      dO.fieldValues.put("name", name);
      return dO;
    }
    
    Object parseBigInt() {
      consume("bigint");
      consume("(");
      String val = tpp();
      if (eq(val, "-"))
        val = "-" + tpp();
      consume(")");
      return new BigInteger(val);
    }
    
    Object parseDouble() {
      consume("d");
      consume("(");
      String val = unquote(tpp());
      consume(")");
      return Double.parseDouble(val);
    }
    
    Object parseFloat() {
      consume("fl");
      String val;
      if (eq(t(), "(")) {
        consume("(");
        val = unquote(tpp());
        consume(")");
      } else {
        val = unquote(tpp());
      }
      return Float.parseFloat(val);
    }
    
    void parseHashSet(unstructure_Receiver out) {
      consume("hashset");
      parseSet(new HashSet(), out);
    }
    
    void parseLinkedHashSet(unstructure_Receiver out) {
      consume("lhs");
      parseSet(new LinkedHashSet(), out);
    }
    
    void parseTreeSet(unstructure_Receiver out) {
      consume("treeset");
      parseSet(new TreeSet(), out);
    }
    
    void parseCISet(unstructure_Receiver out) {
      consume("ciset");
      parseSet(ciSet(), out);
    }
    
    void parseMap(unstructure_Receiver out) {
      parseMap(new TreeMap(), out);
    }
    
    Object parseJava() {
      String j = unquote(tpp());
      Matches m = new Matches();
      if (jmatch("java.awt.Color[r=*,g=*,b=*]", j, m))
        return nuObject("java.awt.Color", parseInt(m.unq(0)), parseInt(m.unq(1)), parseInt(m.unq(2)));
      else {
        warn("Unknown Java object: " + j);
        return null;
      }
    }
    
    void parseMap(final Map map, final unstructure_Receiver out) {
      consume("{");
      stack.add(new Runnable() {
        boolean v = false;
        Object key;
        
        public void run() { 
          if (v) {
            v = false;
            stack.add(this);
            if (!eq(tpp(), "="))
              throw fail("= expected, got " + t() + " in map of size " + l(map));

            parse(new unstructure_Receiver() {
              void set(Object value) {
                map.put(key, value);
                
                if (eq(t(), ",")) consume();
              }
            });
          } else {
            if (eq(t(), "}")) {
              consume("}");
              out.set(map);
            } else {
              v = true;
              stack.add(this);
              parse(new unstructure_Receiver() {
                void set(Object o) {
                  key = o;
                }
              });
            }
          } // if v else
        } // run()
      });
    }
    
    /*void parseSub(unstructure_Receiver out) {
      int n = l(stack);
      parse(out);
      while (l(stack) > n)
        stack
    }*/
    
    void consume() { curT = tok.next(); ++i; }
    
    void consume(String s) {
      if (!eq(t(), s)) {
        /*S prevToken = i-1 >= 0 ? tok.get(i-1) : "";
        S nextTokens = join(tok.subList(i, Math.min(i+2, tok.size())));
        fail(quote(s) + " expected: " + prevToken + " " + nextTokens + " (" + i + "/" + tok.size() + ")");*/
        throw fail(quote(s) + " expected, got " + quote(t()));
      }
      consume();
    }
    
    // outer wrapper function getting first token and unwinding the stack
    void parse_initial(unstructure_Receiver out) {
      consume(); // get first token
      parse(out);
      while (nempty(stack))
        popLast(stack).run();
    }
  }
  
  ThreadLocal<Boolean> tlLoading = dynamicObjectIsLoading_threadLocal();
  Boolean b = tlLoading.get();
  tlLoading.set(true);
  try {
    final Var v = new Var();
    X x = new X();
    x.parse_initial(new unstructure_Receiver() {
      void set(Object o) { v.set(o); }
    });
    unstructure_tokrefs = x.tokrefs.size();
    return v.get();
  } finally {
    tlLoading.set(b);
  }
}

static boolean unstructure_debug = false;


static <A, B> List<Pair<A, B>> mapToPairs(Map<A, B> map) {
  List<Pair<A, B>> l = emptyList(l(map));
  if (map != null) for (Map.Entry<A, B> e : map.entrySet())
    l.add(pair(e.getKey(), e.getValue()));
  return l;
}


static String dynShortName(Object o) {
  return shortDynamicClassName(o);
}


static Concept getConcept(long id) {
  return db_mainConcepts().getConcept(id);
}

static Concept getConcept(Concepts concepts, long id) {
  return concepts.getConcept(id);
}
 
static <A extends Concept> A getConcept(Class<A> cc, long id) {
  return getConcept(db_mainConcepts(), cc, id);
}

static <A extends Concept> A getConcept(Concepts concepts, Class<A> cc, long id) {
  Concept c = concepts.getConcept(id);
  if (c == null) return null;
  if (!isInstance(cc, c))
    throw fail("Can't convert concept: " + getClassName(c) + " -> " + getClassName(cc) + " (" + id + ")");
  return (A) c;
}



static <A, B> A lastKey(SortedMap<A, B> map) {
  return empty(map) ? null : map.lastKey();
}


// wrapper: VF1<Runnable> or null
static void callRunnableWithWrapper(Object wrapper, Runnable r) {
  if (wrapper == null) callF(r);
  else callF(wrapper, r);
}



static List callFAll(Collection l, Object... args) {
  return callF_all(l, args);
}


static long saveGZStructureToFile(String file, Object o) {
  return saveGZStructureToFile(getProgramFile(file), o);
}
  
// returns number of uncompressed bytes written
static long saveGZStructureToFile(File file, Object o) { return saveGZStructureToFile(file, o, new structure_Data()); }
static long saveGZStructureToFile(File file, Object o, structure_Data data) { try {
  File parentFile = file.getParentFile();
  if (parentFile != null)
    parentFile.mkdirs();
  File tempFile = tempFileFor(file);
  if (tempFile.exists()) try {
    String saveName = tempFile.getPath() + ".saved." + now();
    copyFile(tempFile, new File(saveName));
  } catch (Throwable e) { printStackTrace(e); }
  
  FileOutputStream fileOutputStream = newFileOutputStream(tempFile.getPath());
  CountingOutputStream cos;
  try {
    GZIPOutputStream gos = new GZIPOutputStream(fileOutputStream);
    cos = new CountingOutputStream(gos);
    OutputStreamWriter outputStreamWriter = new OutputStreamWriter(cos, "UTF-8");
    PrintWriter printWriter = new PrintWriter(outputStreamWriter);
    structureToPrintWriter(o, printWriter, data);
    printWriter.close();
    gos.close();
    fileOutputStream.close();
  } catch (Throwable e) {
    fileOutputStream.close();
    tempFile.delete();
    throw rethrow(e);
  }
  
  if (file.exists() && !file.delete())
    throw new IOException("Can't delete " + file.getPath());

  if (!tempFile.renameTo(file))
    throw new IOException("Can't rename " + tempFile + " to " + file);
    
  return cos.getFilePointer();
} catch (Exception __e) { throw rethrow(__e); } }


static long toM(long l) {
  return (l+1024*1024-1)/(1024*1024);
}

static String toM(long l, int digits) {
  return formatDouble(toM_double(l), digits);
}


static String javaTokWordWrap(String s) {
  return javaTokWordWrap(120, s);
}

// TODO: complete trimming
static String javaTokWordWrap(int cols, String s) {
  int col = 0;
  List<String> tok = javaTok(s);
  for (int i = 0; i < l(tok); i++) {
    String t = tok.get(i);
    if (odd(i) && col >= cols && !containsNewLine(t))
      tok.set(i, t = rtrimSpaces(t) + "\n");
    int idx = t.lastIndexOf('\n');
    if (idx >= 0) col = l(t)-(idx+1);
    else col += l(t);
  }
  return join(tok);
}


static String ymd() {
  return ymd(now());
}

static String ymd(long now) {
  return year(now) + formatInt(month(now), 2) + formatInt(dayOfMonth(now), 2);
}

static String ymd(long now, TimeZone tz) {
  return year(now, tz) + formatInt(month(now, tz), 2) + formatInt(dayOfMonth(now, tz), 2);
}


static String formatInt(int i, int digits) {
  return padLeft(str(i), '0', digits);
}

static String formatInt(long l, int digits) {
  return padLeft(str(l), '0', digits);
}


static int hours() {
  return hours(java.util.Calendar.getInstance());
}

static int hours(java.util.Calendar c) {
  return c.get(java.util.Calendar.HOUR_OF_DAY);
}

static int hours(long time) {
  return hours(calendarFromTime(time));
}

static int hours(long time, TimeZone tz) {
  return hours(calendarFromTime(time, tz));
}


static int roundDownTo_rev(int x, int n) {
  return roundDownTo(n, x);
}

static long roundDownTo_rev(long x, long n) {
  return roundDownTo(n, x);
}


static int minutes() {
  return minutes(Calendar.getInstance());
}

static int minutes(Calendar c) {
  return c.get(Calendar.MINUTE);
}


static long toK(long l) {
  return (l+1023)/1024;
}


static String n(long l, String name) {
  return l + " " + trim(l == 1 ? singular(name) : getPlural(name));
}

static String n(Collection l, String name) {
  return n(l(l), name);
}

static String n(Map m, String name) {
  return n(l(m), name);
}

static String n(Object[] a, String name) {
  return n(l(a), name);
}


  static String n(MultiSet ms, String name) {
    return n(l(ms), name);
  }



static boolean structure_showTiming, structure_checkTokenCount;

static String structure(Object o) {
  return structure(o, new structure_Data());
}

static String structure(Object o, structure_Data d) {
  StringWriter sw = new StringWriter();
  d.out = new PrintWriter(sw);
  structure_go(o, d);
  String s = str(sw);
  if (structure_checkTokenCount) {
    print("token count=" + d.n);
    assertEquals("token count", l(javaTokC(s)), d.n);
  }
  return s;
}

static void structure_go(Object o, structure_Data d) {
  structure_1(o, d);
  while (nempty(d.stack))
    popLast(d.stack).run();
}

static void structureToPrintWriter(Object o, PrintWriter out) { structureToPrintWriter(o, out, new structure_Data()); }
static void structureToPrintWriter(Object o, PrintWriter out, structure_Data d) {
  d.out = out;
  structure_go(o, d);
}

// leave to false, unless unstructure() breaks
static boolean structure_allowShortening = false;

// info on how to serialize objects of a certain class
static class structure_ClassInfo {
  Class c;
  String shortName;
  List<Field> fields;
  Method customSerializer;
  IVF1<Object> serializeObject; // can be set by caller of structure function
  boolean special = false; // various special classes
  boolean nullInstances = false; // serialize all instances as null (e.g. lambdas/anonymous classes)
}

static class structure_Data {
  PrintWriter out;
  int stringSizeLimit;
  int shareStringsLongerThan = 20;
  boolean noStringSharing = false;
  boolean storeBaseClasses = false;
  boolean honorFieldOrder = true;
  String mcDollar = actualMCDollar();

  IdentityHashMap<Object, Integer> seen = new IdentityHashMap();
  //new BitSet refd;
  HashMap<String, Integer> strings = new HashMap();
  HashSet<String> concepts = new HashSet();
  HashMap<Class, structure_ClassInfo> infoByClass = new HashMap();
  
  // wrapper for _persistenceInfo field or _persistenceInfo method
  // by class (taking the object instance)
  HashMap<Class, IF1<Object, Map>> persistenceInfo = new HashMap();
  
  int n; // token count
  List<Runnable> stack = new ArrayList();
  
  // append single token
  structure_Data append(String token) { out.print(token); ++n; return this; }
  structure_Data append(int i) { out.print(i); ++n; return this; }
  
  // append multiple tokens
  structure_Data append(String token, int tokCount) { out.print(token); n += tokCount; return this; }
  
  // extend last token
  structure_Data app(String token) { out.print(token); return this; }
  structure_Data app(int i) { out.print(i); return this; }

  structure_ClassInfo infoForClass(Class c) {
    structure_ClassInfo info = infoByClass.get(c);
    if (info == null) info = newClass(c);
    return info;
  }
  
  // called when a new class is detected
  // can be overridden by clients
  structure_ClassInfo newClass(Class c) {
    structure_ClassInfo info = new structure_ClassInfo();
    info.c = c;
    infoByClass.put(c, info);
    
    String name = c.getName();
    String shortName = dropPrefix("loadableUtils.utils$", dropPrefix(mcDollar, name));
    if (startsWithDigit(shortName)) shortName = name; // for anonymous classes
    info.shortName = shortName;
    
    try {
      if (isSyntheticOrAnonymous(c)) {
        info.special = info.nullInstances = true;
        return info;
      }
      
      if (c.isEnum()) {
        info.special = true;
        return info;
      }
      
      if ((info.customSerializer = findMethodNamed(c, "_serialize"))
        != null) info.special = true;
        
      if (storeBaseClasses) {
        Class sup = c.getSuperclass();
        if (sup != Object.class) {
          append("bc ");
          append(shortDynClassNameForStructure(c));
          out.print(" ");
          append(shortDynClassNameForStructure(sup));
          out.print(" ");
          infoForClass(sup); // transitively write out superclass relations
        }
      }
      
      if (!isPersistableClass(c))
        warn("Class not persistable: " + c + " (anonymous or no default constructor)");
    } catch (Throwable e) { printStackTrace(e);
      info.nullInstances = true;
    }
    
    return info;
  }
  
  void setFields(structure_ClassInfo info, List<Field> fields) { 
    info.fields = fields;
  }
  
  void writeObject(Object o, String shortName, Map<String, Object> fv) {
    String singleField = fv.size() == 1 ? first(fv.keySet()) : null;
  
    append(shortName);
    n += countDots(shortName)*2; // correct token count
    
  
    int l = n;
    Iterator it = fv.entrySet().iterator();
    
    stack.add(new Runnable() {  public void run() { try { 
      if (!it.hasNext()) {
        if (n != l)
          append(")");
      } else {
        Map.Entry e = (Map.Entry) it.next();
        append(n == l ? "(" : ", ");
        append((String) e.getKey()).append("=");
        stack.add(this);
        structure_1(e.getValue(), structure_Data.this);
      }
    
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "if (!it.hasNext()) {\r\n        if (n != l)\r\n          append(\")\");\r\n      } el..."; }});
  }
}

static void structure_1(final Object o, final structure_Data d) { try {
  if (o == null) { d.append("null"); return; }
  
  Class c = o.getClass();
  boolean concept = false;
  
    concept = o instanceof Concept;
  
  structure_ClassInfo info = d.infoForClass(c);
  
  List<Field> lFields = info.fields;
  if (lFields == null) {
    // these are never back-referenced (for readability)
    
    if (o instanceof Number) {
      PrintWriter out = d.out;
if (o instanceof Integer) { int i = ((Integer) o).intValue(); out.print(i); d.n += i < 0 ? 2 : 1; return; }
      if (o instanceof Long) { long l = ((Long) o).longValue(); out.print(l); out.print("L"); d.n += l < 0 ? 2 : 1; return; }
      if (o instanceof Short) { short s = ((Short) o).shortValue(); d.append("sh "); out.print(s); d.n += s < 0 ? 2 : 1; return; }
      if (o instanceof Float) { d.append("fl ", 2); quoteToPrintWriter(str(o), out); return; }
      if (o instanceof Double) { d.append("d(", 3); quoteToPrintWriter(str(o), out); d.append(")"); return; }
      if (o instanceof BigInteger) { out.print("bigint("); out.print(o); out.print(")"); d.n += ((BigInteger) o).signum() < 0 ? 5 : 4; return; }
    }
  
    if (o instanceof Boolean) {
      d.append(((Boolean) o).booleanValue() ? "t" : "f"); return;
    }
      
    if (o instanceof Character) {
      d.append(quoteCharacter((Character) o)); return;
    }
      
    if (o instanceof File) {
      d.append("File ").append(quote(((File) o).getPath())); return;
    }
      
    // referencable objects follow
    
    Integer ref = d.seen.get(o);
    if (o instanceof String && ref == null) ref = d.strings.get((String) o);
    if (ref != null) { /*d.refd.set(ref);*/ d.append("t").app(ref); return; }

    if (!(o instanceof String))
      d.seen.put(o, d.n); // record token number
    else {
      String s = d.stringSizeLimit != 0 ? shorten((String) o, d.stringSizeLimit) : (String) o;
      if (!d.noStringSharing) {
        if (d.shareStringsLongerThan == Integer.MAX_VALUE)
          d.seen.put(o, d.n);
        if (l(s) >= d.shareStringsLongerThan)
          d.strings.put(s, d.n);
      }
      quoteToPrintWriter(s, d.out); d.n++; return;
    }
      
    if (o instanceof Set) {
      /*O set2 = unwrapSynchronizedSet(o);
      if (set2 != o) {
        d.append("sync");
        o = set2;
      } TODO */
      
      if (((Set) o) instanceof TreeSet) {
        d.append(isCISet_gen((Set) o) ? "ciset" : "treeset");
        structure_1(new ArrayList((Set) o), d);
        return;
      }
      
      // assume it's a HashSet or LinkedHashSet
      d.append(((Set) o) instanceof LinkedHashSet ? "lhs" : "hashset");
      structure_1(new ArrayList((Set) o), d);
      return;
    }
    
    String name = c.getName();
    
    if (o instanceof Collection
      && !isJavaXClassName(name)
      /* && neq(name, "main$Concept$RefL") */) {
      
      // it's a list
    
      if (name.equals("java.util.Collections$SynchronizedList")
        || name.equals("java.util.Collections$SynchronizedRandomAccessList")) {
        d.append("sync ");
        { structure_1(unwrapSynchronizedList(((List) o)), d); return; }
      }
      else if (name.equals("java.util.LinkedList")) d.append("ll");
      d.append("[");
      final int l = d.n;
      final Iterator it = cloneList((Collection) o).iterator();
      d.stack.add(new Runnable() {  public void run() { try { 
        if (!it.hasNext())
          d.append("]");
        else {
          d.stack.add(this);
          if (d.n != l) d.append(", ");
          structure_1(it.next(), d);
        }
      
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "if (!it.hasNext())\r\n          d.append(\"]\");\r\n        else {\r\n          d.sta..."; }});
      return;
    }
    
    

    
    if (o instanceof Map && !startsWith(name, d.mcDollar)) {
      if (o instanceof LinkedHashMap) d.append("lhm");
      else if (o instanceof HashMap) d.append("hm");
      else if (o instanceof TreeMap)
        d.append(isCIMap_gen((TreeMap) o) ? "cimap" : "tm");
      else if (name.equals("java.util.Collections$SynchronizedMap")
        || name.equals("java.util.Collections$SynchronizedSortedMap")
        || name.equals("java.util.Collections$SynchronizedNavigableMap")) {
        d.append("sync "); 
        { structure_1(unwrapSynchronizedMap(((Map) o)), d); return; }
      }
      
      d.append("{");
      final int l = d.n;
      final Iterator it = cloneMap((Map) o).entrySet().iterator();
      
      d.stack.add(new Runnable() {
        boolean v = false;
        Map.Entry e;
        
        public void run() {
          if (v) {
            d.append("=");
            v = false;
            d.stack.add(this);
            structure_1(e.getValue(), d);
          } else {
            if (!it.hasNext())
              d.append("}");
            else {
              e = (Map.Entry) it.next();
              v = true;
              d.stack.add(this);
              if (d.n != l) d.append(", ");
              structure_1(e.getKey(), d);
            }
          }
        }
      });
      return;
    }
    
    if (c.isArray()) {
      if (o instanceof byte[]) {
        d.append("ba ").append(quote(bytesToHex((byte[]) o))); return;
      }
  
      final int n = Array.getLength(o);
  
      if (o instanceof boolean[]) {
        String hex = boolArrayToHex((boolean[]) o);
        int i = l(hex);
        while (i > 0 && hex.charAt(i-1) == '0' && hex.charAt(i-2) == '0') i -= 2;
        d.append("boolarray ").append(n).app(" ").append(quote(substring(hex, 0, i))); return;
      }
      
      String atype = "array"/*, sep = ", "*/; // sep is not used yet
  
      if (o instanceof int[]) {
        //ret "intarray " + quote(intArrayToHex((int[]) o));
        atype = "intarray";
        //sep = " ";
      } else if (o instanceof double[]) {
        atype = "dblarray";
        //sep = " ";
      } else {
        Pair<Class, Integer> p = arrayTypeAndDimensions(c);
        if (p.a == int.class) atype = "intarray";
        else if (p.a == byte.class) atype = "bytearray";
        else if (p.a == boolean.class) atype = "boolarray";
        else if (p.a == double.class) atype = "dblarray";
        else if (p.a == String.class) { atype = "array S"; d.n++; }
        else atype = "array"; // fail("Unsupported array type: " + p.a);
        if (p.b > 1) {
          atype += "/" + p.b; // add number of dimensions
          d.n += 2; // 2 additional tokens will be written
        }
      }
      
      d.append(atype).append("{");
      d.stack.add(new Runnable() {
        int i;
        public void run() {
          if (i >= n)
            d.append("}");
          else {
            d.stack.add(this);
            if (i > 0) d.append(", ");
            structure_1(Array.get(o, i++), d);
          }
        }
      });
      return;
    }
  
    if (o instanceof Class) {
      d.append("class(", 2).append(quote(((Class) o).getName())).append(")"); return;
    }
      
    if (o instanceof Throwable) {
      d.append("exception(", 2).append(quote(((Throwable) o).getMessage())).append(")"); return;
    }
      
    if (o instanceof BitSet) {
      BitSet bs = (BitSet) o;
      d.append("bitset{", 2);
      int l = d.n;
      for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i+1)) {
        if (d.n != l) d.append(", ");
        d.append(i);
      }
      d.append("}"); return;
    }
      
    // Need more cases? This should cover all library classes...
    if (name.startsWith("java") || name.startsWith("sun")) {
      d.append("j ").append(quote(str(o))); return; // Hm. this is not unstructure-able except for java.awt.Color
    }
    
    
      
    /*if (name.equals("main$Lisp")) {
      fail("lisp not supported right now");
    }*/
    
    if (info.special) {
      if (c.isEnum()) {
        d.append("enum ");
        d.append(info.shortName);
        d.out.append(' ');
        d.append(((Enum) o).ordinal());
        return;
      }
      
      if (info.customSerializer != null) {
        // custom serialization (_serialize method)
        Object o2 = invokeMethod(info.customSerializer, o);
        d.append("cu ");
        String shortName = dropPrefix(d.mcDollar, name);
        d.append(shortName);
        d.out.append(' ');
        structure_1(o2, d);
        return;
      } else if (info.nullInstances) { d.append("null"); return; }
      else if (info.serializeObject != null)
        { info.serializeObject.get(o); return; }
      else throw fail("unknown special type");
    }
    
    String dynName = shortDynClassNameForStructure(o);
    if (concept && !d.concepts.contains(dynName)) {
      d.concepts.add(dynName);
      d.append("c ");
    }
    
    // serialize an object with fields.
    // first, collect all fields and values in fv.
    
    TreeSet<Field> fields = new TreeSet<Field>(new Comparator<Field>() {
      public int compare(Field a, Field b) {
        return stdcompare(a.getName(), b.getName());
      }
    });
    
    Class cc = c;
    while (cc != Object.class) {
      for (Field field : getDeclaredFields_cached(cc)) {
        String fieldName = field.getName();
        if (fieldName.equals("_persistenceInfo"))
          d.persistenceInfo.put(c, obj -> (Map) fieldGet(field, obj));
        if ((field.getModifiers() & (java.lang.reflect.Modifier.STATIC | java.lang.reflect.Modifier.TRANSIENT)) != 0)
          continue;

        fields.add(field);
        
        // put special cases here...?
      }
        
      cc = cc.getSuperclass();
    }
    
    Method persistenceInfoMethod = findInstanceMethod(c, "_persistenceInfo");
    if (persistenceInfoMethod != null)
      d.persistenceInfo.put(c, obj -> (Map) invokeMethod(persistenceInfoMethod, obj));
    
    lFields = asList(d.honorFieldOrder ? fieldObjectsInFieldOrder(c, fields) : fields);

    // Render this$0/this$1 first because unstructure needs it for constructor call.
    
    int n = l(lFields);
    for (int i = 0; i < n; i++) {
      Field f = lFields.get(i);
      if (f.getName().startsWith("this$")) {
        lFields.remove(i);
        lFields.add(0, f);
        break;
      }
    }
  
    
    d.setFields(info, lFields);
  } // << if (lFields == null)
  else { // ref handling for lFields != null
    Integer ref = d.seen.get(o);
    if (ref != null) { /*d.refd.set(ref);*/ d.append("t").app(ref); return; }
    d.seen.put(o, d.n); // record token number
  }

  // get _persistenceInfo from field and/or dynamic field
  IF1<Object, Map> piGetter = d.persistenceInfo.get(c);
  Map persistenceInfo = piGetter == null ? null : piGetter.get(o);
  
  if (piGetter == null && o instanceof DynamicObject)
    persistenceInfo = (Map) getOptDynOnly(((DynamicObject) o), "_persistenceInfo");

  
  LinkedHashMap<String, Object> fv = new LinkedHashMap();
  for (Field f : lFields) {
    Object value;
    try {
      value = f.get(o);
    } catch (Exception e) {
      value = "?";
    }
      
    if (value != null && (persistenceInfo == null
      || !Boolean.FALSE.equals(persistenceInfo.get(f.getName()))))
      fv.put(f.getName(), value);
    
  }
  
  String shortName = info.shortName;
    
  // Now we have fields & values. Process fieldValues if it's a DynamicObject.
  
  // omit field "className" if equal to class's name
  if (concept && eq(fv.get("className"), shortName))
    fv.remove("className");
          
  if (o instanceof DynamicObject) {
    putAll(fv, (Map) fv.get("fieldValues"));
    fv.remove("fieldValues");
    if (((DynamicObject) o).className != null) {
      // TODO: this probably doesn't work with inner classes
      shortName = shortDynClassNameForStructure((DynamicObject) o);
      fv.remove("className");
    }
  }
  
  d.writeObject(o, shortName, fv);
} catch (Exception __e) { throw rethrow(__e); } }



static Collection<Concept> allConcepts() {
  return db_mainConcepts().allConcepts();
}

static Collection<Concept> allConcepts(Concepts concepts) {
  return concepts.allConcepts();
}



static long sysNow() {
  ping();
  return System.nanoTime()/1000000;
}


static boolean isTransient(Field f) {
  return (f.getModifiers() & java.lang.reflect.Modifier.TRANSIENT) != 0;
}


// firstDelay = delay
static FixedRateTimer doEvery_daemon(long delay, final Object r) { return doEvery_daemon(defaultTimerName(), delay, r); }
static FixedRateTimer doEvery_daemon(String timerName, long delay, final Object r) {
  return doEvery_daemon(timerName, delay, delay, r);
}

static FixedRateTimer doEvery_daemon(long delay, long firstDelay, final Object r) { return doEvery_daemon(defaultTimerName(), delay, firstDelay, r); }
static FixedRateTimer doEvery_daemon(String timerName, long delay, long firstDelay, final Object r) {
  FixedRateTimer timer = new FixedRateTimer(true);
  timer.scheduleAtFixedRate(smartTimerTask(r, timer, delay), firstDelay, delay);
  return timer;
}

static FixedRateTimer doEvery_daemon(double delaySeconds, final Object r) {
  return doEvery_daemon(toMS(delaySeconds), r);
}


static float abs(float f) { return Math.abs(f); }
static int abs(int i) { return Math.abs(i); }
static double abs(double d) { return Math.abs(d); }


static double abs(Complex c) { return c.abs(); }



static void sleepInCleanUp(long ms) { try {
  if (ms < 0) return;
  Thread.sleep(ms);
} catch (Exception __e) { throw rethrow(__e); } }


static boolean warn_on = true;
static ThreadLocal<List<String>> warn_warnings = new ThreadLocal();

static void warn(String s) {
  if (warn_on)
    print("Warning: " + s);
}

static void warn(String s, List<String> warnings) {
  warn(s);
  if (warnings != null)
    warnings.add(s);
  addToCollection(warn_warnings.get(), s);
}


static <A> A firstOfType(Collection c, Class<A> type) {
  for (Object x : c)
    if (isInstanceX(type, x))
      return (A) x;
  return null;
}


static List<Concept> conceptsOfType(String type) {
  return db_mainConcepts().conceptsOfType(type);
}


static <A> List<A> concatLists_conservative(List<A> a, List<A> b) {
  if (empty(a)) return b;
  if (empty(b)) return a;
  return concatLists(a, b);
}

static <A> List<A> concatLists_conservative(Collection<A> a, Collection<A> b) {
  if (empty(a) && b instanceof List) return ((List) b);
  if (empty(b) && a instanceof List) return ((List) a);
  return concatLists(a, b);
}


static <A> List<A> filterByType(Iterable c, Class<A> type) {
  List<A> l = new ArrayList();
  if (c != null) for (Object x : c)
    if (isInstanceX(type, x))
      l.add((A) x);
  return l;
}

static <A> List<A> filterByType(Object[] c, Class<A> type) {
  return filterByType(asList(c), type);
}

static <A> List<A> filterByType(Class<A> type, Iterable c) {
  return filterByType(c, type);
}


static <A extends Concept> List<A> list(Class<A> type) { return list(type, db_mainConcepts()); }
static <A extends Concept> List<A> list(Class<A> type, Concepts cc) {
  return cc.list(type);
}

static <A extends Concept> List<A> list(Concepts concepts, Class<A> type) {
  return concepts.list(type);
}

static List<Concept> list(String type) {
  return db_mainConcepts().list(type);
}

static List<Concept> list(Concepts concepts, String type) {
  return concepts.list(type);
}

static List<Concept> list(Concepts concepts) {
  return asList(concepts.allConcepts());
}



static <A> List<A> filterByDynamicType(Collection<A> c, String type) {
  List<A> l = new ArrayList();
  for (A x : c)
    if (eq(dynamicClassName(x), type))
      l.add(x);
  return l;
}


static boolean hasType(Collection c, Class type) {
  for (Object x : c)
    if (isInstanceX(type, x))
      return true;
  return false;
}


static <A extends Concept> A findBackRef(Concept c, Class<A> type) {
  for (Concept.Ref r : c.backRefs)
    if (instanceOf(r.concept(), type))
      return (A) r.concept();
  return null;
}

static <A extends Concept> A findBackRef(Class<A> type, Concept c) {
  return findBackRef(c, type);
}


static boolean checkConceptFields(Concept x, Object... data) {
  for (int i = 0; i < l(data); i += 2)
    if (neq(cget(x, (String) data[i]), deref(data[i+1])))
      return false;
  return true;
}


static <A, B> void mapRemove(Map<A, B> map, A key) {
  if (map != null && key != null)
    map.remove(key);
}


static <A, B> B firstValue(Map<A, B> map) {
  return first(values(map));
}




static <A, B> B firstValue(MultiMap<A, B> map) {
  return map == null ? null : first(firstValue(map.data));
}



static Concept cnew(String name, Object... values) {
  return cnew(db_mainConcepts(), name, values);
}

static Concept cnew(Concepts concepts, String name, Object... values) {
  Class<? extends Concept> cc = findClass(name);
  concepts_unlisted.set(true);
  Concept c;
  try {
    c = cc != null ? nuObject(cc) : new Concept(name);
  } finally {
    concepts_unlisted.set(null);
  }
  csetAll(c, values);
  concepts.register(c);
  return c;
}

static <A extends Concept> A cnew(Class<A> cc, Object... values) {
  return cnew(db_mainConcepts(), cc, values);
}

static <A extends Concept> A cnew(Concepts concepts, Class<A> cc, Object... values) {
  concepts_unlisted.set(true);
  A c;
  try {
    c = nuObject(cc);
  } finally {
    concepts_unlisted.set(null);
  }
  csetAll(c, values);
  concepts.register(c);
  return c;
}



// returns number of changes
static int cset(Concept c, Object... values) { try {
  if (c == null) return 0;
  warnIfOddCount(values = expandParams(c.getClass(), values));
  int changes = 0;
  for (int i = 0; i+1 < l(values); i += 2)
    if (_csetField(c, (String) values[i], values[i+1])) ++changes;
  return changes;
} catch (Exception __e) { throw rethrow(__e); } }

static int cset(Iterable<? extends Concept> l, Object... values) {
  int changes = 0;
  for (Concept c : unnullForIteration(l))
    changes += cset(c, values);
  return changes;
}

static <A extends Concept> int cset(Concept.Ref<A> c, Object... values) {
  return cset(getVar(c), values);
}


static Object cget(Object c, String field) {
  c = derefRef(c);
  Object o = getOpt(c, field);
  return derefRef(o);
}

static Object cget(String field, Object c) {
  return cget(c, field);
}


static Object deref(Object o) {
  if (o instanceof IRef) return ((IRef) o).get();
  return o;
}



static String loadConceptsStructure(String progID) {
  return loadTextFilePossiblyGZipped(getProgramFile(progID, "concepts.structure"));
}

static String loadConceptsStructure() {
  return loadConceptsStructure(dbProgramID());
}


static void cleanKillVM() { try {
  ping();
  assertNotOnAWTThread();
  cleanKillVM_noSleep();
  Object o = new Object();
  synchronized(o) { o.wait(); }
} catch (Exception __e) { throw rethrow(__e); } }

static void cleanKillVM_noSleep() {
  call(getJavaX(), "cleanKill");
}


static <A extends Concept> int countConcepts(Concepts concepts, Class<A> c, Object... params) {
  return concepts.countConcepts(c, params);
}

static <A extends Concept> int countConcepts(Class<A> c, Object... params) {
  return db_mainConcepts().countConcepts(c, params);
}

static int countConcepts() {
  return db_mainConcepts().countConcepts();
}

static int countConcepts(String className) {
  return db_mainConcepts().countConcepts(className);
}

static <A extends Concept> int countConcepts(Concepts concepts, String className) {
  return concepts.countConcepts(className);
}

static int countConcepts(Concepts concepts) {
  return concepts.countConcepts();
}


static <A, B> boolean containsKey(Map<A, B> map, A key) {
  return map != null && map.containsKey(key);
}


static <A> boolean syncAdd(Collection<A> c, A b) {
  if (c == null) return false;
  synchronized(collectionMutex(c)) { return c.add(b); }
}

static <A> void syncAdd(List<A> l, int idx, A b) {
  if (l != null) synchronized(collectionMutex(l)) { l.add(idx, b); }
}


static <A> boolean syncRemove(Collection<A> c, A b) {
  if (c == null) return false;
  synchronized(collectionMutex(c)) { return c.remove(b); }
}

static <A> A syncRemove(List<A> l, int idx) {
  if (l == null) return null;
  synchronized(collectionMutex(l)) { return l.remove(idx); }
}

static <A, B> B syncRemove(Map<A, B> map, A key) {
  return map == null ? null : map.remove(key);
}


static <A> List<A> syncAddOrCreate(List<A> l, A a) {
  if (l == null) l = syncList();
  l.add(a);
  return l;
}


static String nConcepts(long n) { return n2(n, "concept"); }
static String nConcepts(Collection l) { return nConcepts(l(l)); }
static String nConcepts(Map map) { return nConcepts(l(map)); }


static int identityHashCode(Object o) {
  return System.identityHashCode(o);
}


static String shortDynamicClassName(Object o) {
 if (o instanceof DynamicObject && ((DynamicObject) o).className != null)
    return ((DynamicObject) o).className;
  return shortClassName(o);
}


static boolean dynamicObjectIsLoading() { 
  return isUnstructuring();
}


static volatile Concepts mainConcepts; // Where we create new concepts

static Concepts db_mainConcepts() {
  if (mainConcepts == null)
    mainConcepts = newConceptsWithClassFinder(getDBProgramID());
  return mainConcepts;
}

static void cleanMeUp_concepts() {
  if (db_mainConcepts() != null) db_mainConcepts().cleanMeUp();
  // mainConcepts = null; // TODO
}



static void assertIsInstance(Class type, Object o) {
  if (!isInstance(type, o))
    throw fail(_getClass(o) + " is not a subclass of " + type);
}

static void assertIsInstance(Object o, Class type) {
  assertIsInstance(type, o);
}


static String b(Object contents, Object... params) {
  return tag("b", contents, params);
}


static boolean has(String a, String b, String c) {
  
  return false;
}

static boolean has(T3<String, String, String> t) {
  
  return false;
}


static void removeLast(List l) {
  if (!l.isEmpty())
    l.remove(l(l)-1);
}

static void removeLast(List l, int n) {
  removeSubList(l, l(l)-n);
}

static void removeLast(int n, List l) {
  removeLast(l, n);
}


static <A> A syncGet(List<A> l, int idx) {
  if (l == null || idx < 0) return null;
  synchronized(l) {
    return idx < l(l) ? l.get(idx) : null;
  }
}

static <A, B> B syncGet(Map<A, B> map, A a) {
  if (map == null) return null;
  synchronized(map) {
    return map.get(a);
  }
}


static <A> List<A> addDyn_quickSync(List<A> l, A a) {
  if (l == null) l = new ArrayList();
  syncAdd(l, a);
  return l;
}


static <A> List<A> removeDyn_quickSync(List<A> l, A a) {
  if (l == null) return null;
  synchronized(collectionMutex(l)) {
    l.remove(a);
    return empty(l) ? null : l;
  }
}


static void crenameField_noOverwrite(Concept c, String oldField, String newField) {
  if (c == null || eq(oldField, newField)) return;
  Object value = cget(c, oldField);
  if (newField != null && cget(c, newField) == null) cset(c, newField, value);
  cset(c, oldField, null);
}


static Collection<Concept.Ref> scanConceptForRefs(Concept c) {
  Set<Concept.Ref> refs = new HashSet();
  if (c != null) for (Object o : values(objectToMap(c))) {
    if (o instanceof Concept.Ref)
      refs.add((Concept.Ref) o);
    else if (o instanceof Concept.RefL)
      addAll(refs, ((Concept.RefL) o).l);
  }
  return refs;
}


static boolean containsInstance(Iterable i, Class c) {
  if (i != null) for (Object o : i)
    if (isInstanceX(c, o))
      return true;
  return false;
}


static void register(Concept c) {
  registerConcept(c);
}


static void assertSame(Object a, Object b) { assertSame("", a, b); }
static void assertSame(String msg, Object a, Object b) {
  if (a != b)
    throw fail(joinNemptiesWithColon(msg, a + " != " + b + " (" + identityHash(a) + "/" + identityHash(b) + ")"));
}

static void assertSame(IF0<String> msg, Object a, Object b) {
  if (a != b)
    throw fail(joinNemptiesWithColon(msg.get(), a + " != " + b + " (" + identityHash(a) + "/" + identityHash(b) + ")"));
}



static LineAndColumn tokenToLineAndColumn(ListAndIndex<String> ptr) {
  return ptr == null ? null : tokenToLineAndColumn(ptr.list(), ptr.idx());
}


static LineAndColumn tokenToLineAndColumn(List<String> tok, int tokenIndex) {
  int line = 1, col = 1;
  tokenIndex = min(tokenIndex, l(tok));
  for (int i = 0; i < tokenIndex; i++) {
    String t = tok.get(i);
    int n = l(t);
    for (int j = 0; j < n; j++)
      if (t.charAt(j) == '\n') {
        ++line;
        col = 1;
      } else
        ++col;
  }
  return new LineAndColumn(line, col);
}


static String exceptionToStringShorter_dontDropOuterExceptions(Throwable e) {
  String msg = hideCredentials(unnull(e.getMessage()));
  String className = baseClassName(e);
  if (!eq(className, "RuntimeException")
    && (msg.indexOf("Error") < 0 && msg.indexOf("Exception") < 0))
    return className + prependIfNempty(": ", msg);
  else
    return msg;
}


static double fileAgeInSeconds(File f) {
  return f == null ? -1 : msToSeconds(now()-fileModificationTime(f));
}


static String nSeconds(long n) { return n2(n, "second"); }
static String nSeconds(Collection l) { return nSeconds(l(l)); }


static int iceil(double d) {
  return (int) Math.ceil(d);
}


static boolean deleteFile(File file) {
  return file != null && file.delete();
}




static Path toPath(File f) {
  return f == null ? null : f.toPath();
}


static void printExceptionShort(Throwable e) { printExceptionShort("", e); }
static void printExceptionShort(String prefix, Throwable e) {
  print(prefix, exceptionToStringShort(e));
}


// will create the file or update its last modified timestamp
static File touchFile(File file) { try {
  closeRandomAccessFile(newRandomAccessFile(mkdirsForFile(file), "rw"));
  return file;
} catch (Exception __e) { throw rethrow(__e); } }


// firstDelay = delay
static FixedRateTimer doEvery(long delay, final Object r) {
  return doEvery(delay, delay, r);
}

static FixedRateTimer doEvery(long delay, long firstDelay, final Object r) {
  FixedRateTimer timer = new FixedRateTimer(shorten(programID() + ": " + r, 80));
  timer.scheduleAtFixedRate(smartTimerTask(r, timer, toInt(delay)), toInt(firstDelay), toInt(delay));
  return vmBus_timerStarted(timer);
}

// reversed argument order for fun
static FixedRateTimer doEvery(double initialSeconds, double delaySeconds, final Object r) {
  return doEvery(toMS(delaySeconds), toMS(initialSeconds), r);
}

static FixedRateTimer doEvery(double delaySeconds, final Object r) {
  return doEvery(toMS(delaySeconds), r);
}



static void touchExistingFile(File file) { try {
  if (file == null) return;
  java.nio.file.Files.setLastModifiedTime(toPath(file),
    java.nio.file.attribute.FileTime.from(java.time.Instant.now()));
} catch (Exception __e) { throw rethrow(__e); } }




static String localDateWithSeconds(long time) {
  SimpleDateFormat format = simpleDateFormat_local("yyyy/MM/dd HH:mm:ss");
  return format.format(time);
}

static String localDateWithSeconds() {
  return localDateWithSeconds(now());
}


static double elapsedMinutes_timestamp(long time) {
  return toMinutes(now()-time);
}


static double elapsedSeconds_timestamp(long time) {
  return toSeconds(now()-time);
}


static <A extends JComponent> A setDoubleBuffered(A c, boolean b) {
  { swing(() -> {  { if (c != null) c.setDoubleBuffered(b); } }); }
  return c;
}


static Color getBackground(final Component c) {
  return c == null ? null : swing(new F0<Color>() { public Color get() { try {  return c.getBackground();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return c.getBackground();"; }});
}


static <A extends JSpinner> A onChange(A spinner, Object r) {
  return onChange(spinner, toRunnable(r));
}

static <A extends JSpinner> A onChange(A spinner, Runnable r) {
  if (r != null)
    { swing(() -> {  spinner.addChangeListener(changeListener(r)); }); }
  return spinner;
}

static <A extends AbstractButton> A onChange(A b, Object r) {
  { swing(() -> {  b.addItemListener(itemListener(r)); }); }
  return b;
}

static void onChange(JTextComponent tc, Object r) {
  onUpdate(tc, r);
}

static void onChange(JTextComponent tc, Runnable r) {
  onUpdate(tc, r);
}

static <A extends JSlider> A onChange(A slider, final Object r) {
  { swing(() -> {  slider.addChangeListener(changeListener(r)); }); }
  return slider;
}

static <A> JComboBox<A> onChange(JComboBox<A> cb, Runnable r) {
  addActionListener(cb, r);
  return cb;
}

static <A> JComboBox<A> onChange(JComboBox<A> cb, IVF1<A> f) {
  if (f != null) addActionListener(cb, () -> f.get(getSelectedItem_typed(cb)));
  return cb;
}

static JComboBox onChange(Object r, JComboBox cb) {
  return onChange(cb, r);
}

static JComboBox onChange(JComboBox cb, final Object r) {
  if (isEditableComboBox(cb))
    onChange(textFieldFromComboBox(cb), r);
  else
    onSelectedItem(cb, new VF1<String>() { public void get(String s) { try {  callF(r) ; } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "callF(r)"; }});
  return cb;
}

static <A extends JTabbedPane> A onChange(A tabs, Runnable r) {
  { swing(() -> {  tabs.addChangeListener(changeListener(r)); }); }
  return tabs;
}

static <A extends JColorChooser> A onChange(Runnable r, A cc) {
  if (cc != null && r != null) { swing(() -> { 
    cc.getSelectionModel().addChangeListener(changeListener(r));
  }); }
  return cc;
}

static void onChange(IHasChangeListeners a, ChangeTriggerable b) {
  if (a != null && b != null) a.onChange(new ChangeTrigger(b));
}


static String hijackPrint_tee(Runnable r) {
  final StringBuilder buf = new StringBuilder();
  Object old = interceptPrintInThisThread(new F1<String, Boolean>() {
    Boolean get(String s) {
      buf.append(s);
      return true;
    }
  });
  try {
    callF(r);
    return str(buf);
  } finally {
    interceptPrintInThisThread(old);
  }
}


static Timestamp tsNow() {
  return new Timestamp();
}


static <A> OKOrError<A> okOrError(IF0<A> f) {
  try {
    return OKOrError_ok(f.get());
  } catch (Throwable e) {
    printStackTrace(e);
    return OKOrError_error(e);
  }
}

static <A> OKOrError<A> okOrError(Runnable r) {
  return okOrError(runnableToIF0(r));
}


static boolean eqOrEqic(boolean caseInsensitive, String a, String b) {
  return caseInsensitive ? eqic(a, b) : eq(a, b);
}


static boolean containsLineBreak(String s) {
  return containsNewLine(s);
}


// TODO: extended multi-line strings

static int javaTok_n, javaTok_elements;
static boolean javaTok_opt = false;

static List<String> javaTok(String s) {
  ++javaTok_n;
  ArrayList<String> tok = new ArrayList();
  int l = s == null ? 0 : s.length();
  
  int i = 0;
  while (i < l) {
    int j = i;
    char c, d;
    
        // scan for whitespace
        while (j < l) {
          c = s.charAt(j);
          d = j+1 >= l ? '\0' : s.charAt(j+1);
          if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
            ++j;
          else if (c == '/' && d == '*') {
            do ++j; while (j < l && !regionMatches(s, j, "*/"));
            j = Math.min(j+2, l);
          } else if (c == '/' && d == '/') {
            do ++j; while (j < l && "\r\n".indexOf(s.charAt(j)) < 0);
          } else
            break;
        }
        
        tok.add(javaTok_substringN(s, i, j));
        i = j;
        if (i >= l) break;
        c = s.charAt(i);
        d = i+1 >= l ? '\0' : s.charAt(i+1);
    
        // scan for non-whitespace
        
        // Special JavaX syntax: 'identifier
        if (c == '\'' && Character.isJavaIdentifierStart(d) && i+2 < l && "'\\".indexOf(s.charAt(i+2)) < 0) {
          j += 2;
          while (j < l && Character.isJavaIdentifierPart(s.charAt(j)))
            ++j;
        } else if (c == '\'' || c == '"') {
          char opener = c;
          ++j;
          while (j < l) {
            int c2 = s.charAt(j);
            if (c2 == opener || c2 == '\n' && opener == '\'') { // allow multi-line strings, but not for '
              ++j;
              break;
            } else if (c2 == '\\' && j+1 < l)
              j += 2;
            else
              ++j;
          }
        } else if (Character.isJavaIdentifierStart(c))
          do ++j; while (j < l && (Character.isJavaIdentifierPart(s.charAt(j)) || s.charAt(j) == '\'')); // for stuff like "don't"
        else if (Character.isDigit(c)) {
          do ++j; while (j < l && Character.isDigit(s.charAt(j)));
          if (j < l && s.charAt(j) == 'L') ++j; // Long constants like 1L
        } else if (c == '[' && d == '[') {
          do ++j; while (j < l && !regionMatches(s, j, "]]"));
          j = Math.min(j+2, l);
        } else if (c == '[' && d == '=' && i+2 < l && s.charAt(i+2) == '[') {
          do ++j; while (j+2 < l && !regionMatches(s, j, "]=]"));
          j = Math.min(j+3, l);
        } else
          ++j;
      
    tok.add(javaTok_substringC(s, i, j));
    i = j;
  }
  
  if ((tok.size() % 2) == 0) tok.add("");
  javaTok_elements += tok.size();
  return tok;
}

static List<String> javaTok(List<String> tok) {
  return javaTokWithExisting(join(tok), tok);
}


static String joinSubList(List<String> l, int i, int j) {
  return join(subList(l, i, j));
}

static String joinSubList(List<String> l, int i) {
  return join(subList(l, i));
}


static String joinSubList(List<String> l, IntRange r) {
  return r == null ? null : joinSubList(l, r.start, r.end);
}



static <A> List<A> reversedList(Iterable<A> l) {
  List<A> x = cloneList(l);
  Collections.reverse(x);
  return x;
}


static Set asSet(Object[] array) {
  HashSet set = new HashSet();
  for (Object o : array)
    if (o != null)
      set.add(o);
  return set;
}

static Set<String> asSet(String[] array) {
  TreeSet<String> set = new TreeSet();
  for (String o : array)
    if (o != null)
      set.add(o);
  return set;
}

static <A> Set<A> asSet(Iterable<A> l) {
  if (l instanceof Set) return (Set) l;
  HashSet<A> set = new HashSet();
  for (A o : unnull(l))
    if (o != null)
      set.add(o);
  return set;
}


// Note: does not clone the set (keeps multiset alive)
static <A> Set<A> asSet(MultiSet<A> ms) {
  return ms == null ? null : ms.asSet();
}



static <A, B> NavigableSet<A> navigableKeys(NavigableMap<A, B> map) {
  return map == null ? new TreeSet() : map.navigableKeySet();
}


  static <A> NavigableSet<A> navigableKeys(MultiSet<A> ms) {
    return ((NavigableMap) ms.map).navigableKeySet();
  }



  static <A, B> NavigableSet<A> navigableKeys(MultiMap<A, B> mm) {
    return ((NavigableMap) mm.data).navigableKeySet();
  }



static <A, B> Map<A, List<B>> toMap(MultiMap<A, B> m) {
  return multiMapToMap(m);
}


static Pt toPt(Point p) {
  return p == null ? null : new Pt(p.x, p.y);
}

static Pt toPt(Dimension d) {
  return d == null ? null : new Pt(d.width, d.width);
}


static <A> A evalWithTimeoutOrFail(int timeoutMS, F0<A> f) {
  return evalWithTimeoutOrException(timeoutMS, f);
}

static <A> A evalWithTimeoutOrFail(double timeoutSeconds, F0<A> f) {
  return evalWithTimeoutOrException(timeoutSeconds, f);
}

static <A> A evalWithTimeoutOrFail(int timeoutMS, IF0<A> f) {
  return evalWithTimeoutOrException(timeoutMS, f);
}

static <A> A evalWithTimeoutOrFail(double timeoutSeconds, IF0<A> f) {
  return evalWithTimeoutOrException(timeoutSeconds, f);
}

static void evalWithTimeoutOrFail(int timeoutMS, Runnable r) {
  evalWithTimeoutOrException(timeoutMS, runnableToF0(r));
}

static void evalWithTimeoutOrFail(double timeoutSeconds, Runnable r) {
  evalWithTimeoutOrException(timeoutSeconds, runnableToF0(r));
}


  static RuntimeException error() {
    throw new RuntimeException("fail");
  }
  
  static RuntimeException error(String msg) {
    throw new RuntimeException(msg);
  }


static String ok(Object o) {
  return format("ok *", o);
}


static String dbBotName(String progIDWithCase) {
  return fsI_flex(progIDWithCase) + " Concepts";
}


static Map<String, Integer> findBot_cache = synchroHashMap();
static int findBot_timeout = 5000;

static DialogIO findBot(String searchPattern) {
  // first split off sub-bot suffix
  String subBot = null;
  int i = searchPattern.indexOf('/');
  if (i >= 0 && (isJavaIdentifier(searchPattern.substring(0, i)) || isInteger(searchPattern.substring(0, i)))) {
    subBot = searchPattern.substring(i+1);
    searchPattern = searchPattern.substring(0, i);
    if (!isInteger(searchPattern))
      searchPattern = "Multi-Port at " + searchPattern + ".";
  }
  
  // assume it's a port if it's an integer
  if (isInteger(searchPattern))
    return talkToSubBot(subBot, talkTo(parseInt(searchPattern)));
    
  if (eq(searchPattern, "remote"))
    return talkToSubBot(subBot, talkTo("second.tinybrain.de", 4999));
    
  Integer port = findBot_cache.get(searchPattern);
  if (port != null) try {
    DialogIO io = talkTo("localhost", port);
    io.waitForLine(/*findBot_timeout*/); // TODO: implement
    String line = io.readLineNoBlock();
    if (indexOfIgnoreCase(line, searchPattern) == 0) {
      call(io, "pushback", line); // put hello string back in
      return talkToSubBot(subBot, io);
    }
  } catch (Exception e) {
    e.printStackTrace();
  }
  
  List<ProgramScan.Program> bots = quickBotScan();
  
  // find top-level bots
  for (ProgramScan.Program p : bots) {
    if (indexOfIgnoreCase(p.helloString, searchPattern) == 0) { // strict matching - start of hello string only, but case-insensitive
      findBot_cache.put(searchPattern, p.port);
      return talkToSubBot(subBot, talkTo("localhost", p.port));
    }
  }
  
  // find sub-bots
  for (ProgramScan.Program p : bots) {
    String botName = firstPartOfHelloString(p.helloString);
    boolean isVM = startsWithIgnoreCase(p.helloString, "This is a JavaX VM.");
    boolean shouldRecurse = startsWithIgnoreCase(botName, "Multi-Port") || isVM;
        
    if (shouldRecurse) try {
      Map<Number, String> subBots = (Map) unstructure(sendToLocalBotQuietly(p.port, "list bots"));
      for (Number vport : subBots.keySet()) {
        String name = subBots.get(vport);
        if (startsWithIgnoreCase(name, searchPattern))
          return talkToSubBot(vport.longValue(), talkTo("localhost", p.port));
      }
    } catch (Throwable __e) { print(exceptionToStringShort(__e)); }
  }
        
  return null;
}


static long waitForBotStartUp_timeoutSeconds = 60;

// returns address or fails
static String waitForBotStartUp(String botName) {
  for (int i = 0; i < waitForBotStartUp_timeoutSeconds; i++) {
    sleepSeconds(i == 0 ? 0 : 1);
    String addr = getBotAddress(botName);
    if (addr != null)
      return addr;
  }
  throw fail("Bot not found: " + quote(botName));
}


static Object rpc(String botName, String method, Object... args) {
  return unstructure_matchOK2OrFail(
    sendToLocalBot(botName, rpc_makeCall(method, args)));
}

static Object rpc(DialogIO bot, String method, Object... args) {
  return unstructure_matchOK2OrFail(
    bot.ask(rpc_makeCall(method, args)));
}

static String rpc_makeCall(String method, Object... args) {
  if (empty(args))
    return "call " + method;
  return format("call *", concatLists((List) ll(method), asList(args)));
}


static <A extends Concept> A uniq(Class<A> c, Object... params) {
  return uniqueConcept(c, params);
}

static <A extends Concept> A uniq(Concepts cc, Class<A> c, Object... params) {
  return uniqueConcept(cc, c, params);
}


static long toMS(double seconds) {
  return (long) (seconds*1000);
}


static long incAtomicLong(AtomicLong l) {
  return l.incrementAndGet();
}


static Object callConstructor(String className, Object... args) {
  return nuObject(className, args);
}



// too ambiguous - maybe need to fix some callers
/*static O nuObject(O realm, S className, O... args) {
  ret nuObject(_getClass(realm, className), args);
}*/

static <A> A callConstructor(Class<A> c, Object... args) {
  return nuObject(c, args);
}


static Object[] mapToArray(Map map) {
  return mapToObjectArray(map);
}



static Object[] mapToArray(Object f, Collection l) {
  return mapToObjectArray(f, l);
}



static Object[] mapToArray(Object f, Object[] l) {
  return mapToObjectArray(f, l);
}



static <A> Object[] mapToArray(Collection<A> l, IF1<A, Object> f) {
  return mapToObjectArray(l, f);
}

static <A> Object[] mapToArray(A[] l, IF1<A, Object> f) {
  return mapToObjectArray(f, l);
}


static <A> Object[] mapToArray(IF1<A, Object> f, A[] l) {
  return mapToObjectArray(f, l);
}


static String strOrClassName(Object o) {
  if (o instanceof Class) return className((Class) o);
  return str(o);
}


static boolean canCallWithVarargs(Object o, String method, Object... args) {
  if (o == null) return false;
  
  if (o instanceof Class) {
    Class c = (Class) o;
    _MethodCache cache = callOpt_getCache(c);
    
    if (cache.findStaticMethod(method, args) != null) return true;

    // try varargs
    List<Method> methods = cache.cache.get(method);
    if (methods != null) methodSearch: for (Method m : methods) {
      { if (!(m.isVarArgs() && isStaticMethod(m))) continue; }
      if (massageArgsForVarArgsCall(m, args) != null) return true;
    }
  } else {
    Class c = o.getClass();
    _MethodCache cache = callOpt_getCache(c);

    if (cache.findMethod(method, args) != null) return true;
      
    // try varargs
    List<Method> methods = cache.cache.get(method);
    if (methods != null) methodSearch: for (Method m : methods) {
      { if (!(m.isVarArgs())) continue; }
      if (massageArgsForVarArgsCall(m, args) != null) return true;
    }
  }
  
  return false;
}


static Method findMethod_precise_onTypes(Object o, String method, Class... argTypes) { try {
  if (o instanceof Class) {
    _MethodCache cache = callOpt_getCache((Class) o);
    List<Method> methods = cache.cache.get(method);
    Lowest<Method> best = new Lowest();
    if (methods != null) for (Method m : methods) {
      { if (!(isStaticMethod(m))) continue; }
      int score = methodApplicabilityScore_onTypes(m, argTypes);
       
      if (score < Integer.MAX_VALUE)
        best.put(m, score);
    }
    return best.get();
  }
  
  if (o == null) return null;
  _MethodCache cache = callOpt_getCache(o.getClass());
  List<Method> methods = cache.cache.get(method);
  Lowest<Method> best = new Lowest();
  if (methods != null) for (Method m : methods) {
    int score = methodApplicabilityScore_onTypes(m, argTypes);
    if (score < Integer.MAX_VALUE)
      best.put(m, score);
  }
  return best.get();
} catch (Exception __e) { throw rethrow(__e); } }



static GrabbableIntPixels grabbableIntPixels_fastOrSlow(BufferedImage image) { try {
  try {
    GrabbableIntPixels gp = grabbableIntPixels(image);
    if (gp != null) return gp;
  } catch (Throwable __e) { printStackTrace(__e); }
    
  // Use pixelGrabber if quick method fails
  int w = image.getWidth(), h = image.getHeight();
  int[] data = new int[w*h];
  PixelGrabber pixelGrabber = new PixelGrabber(image, 0, 0, w, h, data, 0, w);
  if (!pixelGrabber.grabPixels())
    throw fail("Could not grab pixels");
      
  return new GrabbableIntPixels(data, w, h, 0, w);
} catch (Exception __e) { throw rethrow(__e); } }


static void arrayCopy(Object[] a, Object[] b) {
  arraycopy(a, b);
}



static void arrayCopy(Object src, int srcPos, Object dest, int destPos, int n) {
  arraycopy(src, srcPos, dest, destPos, n);
}


static boolean inRange(int x, int n) {
  return x >= 0 && x < n;
}

static boolean inRange(int x, List l) {
  return inRange(x, l(l));
}

static boolean inRange(int x, int a, int b) {
  return x >= a && x < b;
}


static int getPixel(BufferedImage img, int x, int y) {
  return img.getRGB(x, y);
}

static int getPixel(BufferedImage img, Pt p) {
  return img.getRGB(p.x, p.y);
}


static int limitToUByte(int i) {
  return max(0, min(255, i));
}


static void setPixel(BufferedImage img, Pt p, Color color) {
  { if (img != null) img.setRGB(p.x, p.y, colorToIntOpaque(color)); }
}


static String copy(String text) {
  return copyToClipboard(text);
}



static <A extends Image> A copy(A image) {
  return copyToClipboard(image);
}



static File copy(File f) {
  return copyToClipboard(f);
}


static Rect rect(int x, int y, int w, int h) {
  return new Rect(x, y, w, h);
}

static Rect rect(Pt p, int w, int h) {
  return new Rect(p.x, p.y, w, h);
}

static Rect rect(int w, int h) {
  return new Rect(0, 0, w, h);
}




// action = runnable or method name
static void onChangeAndNow(JComponent c, Object r) {
  onUpdateAndNow(c, r);
}

static void onChangeAndNow(List<? extends JComponent> l, Object r) {
  onUpdateAndNow(l, r);
}

static <A> JComboBox<A> onChangeAndNow(JComboBox<A> cb, IVF1<A> f) {
  onChange(cb, f);
  { if (f != null) f.get(getSelectedItem_typed(cb)); }
  return cb;
}

static <A extends JTabbedPane> A onChangeAndNow(A tabs, Runnable r) {
  if (r != null) {
    onChange(tabs, r);
    r.run();
  }
  return tabs;
}


static volatile boolean readLine_noReadLine = false;

static String readLine_lastInput;
static String readLine_prefix = "[] ";

static String readLine() {
  if (readLine_noReadLine) return null;
  String s = readLineHidden();
  if (s != null) {
    readLine_lastInput = s;
    print(readLine_prefix + s);
  }
  return s;
}


  static String format3(String pat, Object... args) {
    if (args.length == 0) return pat;
    
    List<String> tok = javaTokPlusPeriod(pat);
    int argidx = 0;
    for (int i = 1; i < tok.size(); i += 2)
      if (tok.get(i).equals("*"))
        tok.set(i, format3_formatArg(argidx < args.length ? args[argidx++] : "null"));
    return join(tok);
  }
  
  static String format3_formatArg(Object arg) {
    if (arg == null) return "null";
    if (arg instanceof String) {
      String s = (String) arg;
      return isIdentifier(s) || isNonNegativeInteger(s) ? s : quote(s);
    }
    if (arg instanceof Integer || arg instanceof Long) return String.valueOf(arg);
    return quote(structure(arg));
  }
  



static <A, B> Map<A, B> putOrCreate(Map<A, B> map, A key, B value) {
  if (map == null) map = new HashMap();
  map.put(key, value);
  return map;
}


static <A extends Iterable> A pnl(A l) { return pnl("", l); }
static <A extends Iterable> A pnl(String prefix, A l) {
  printNumberedLines(prefix, l);
  return l;
}

static <A> A[] pnl(A[] l) { return pnl("", l); }
static <A> A[] pnl(String prefix, A[] l) {
  printNumberedLines(prefix, l);
  return l;
}

static <A extends Map> A pnl(A map) {
  printNumberedLines(map);
  return map;
}

static <A extends Map> A pnl(String prefix, A map) {
  printNumberedLines(prefix, map);
  return map;
}

static String pnl(String s) {
  printNumberedLines(lines(s));
  return s;
}


static <A> MultiSet<A> pnl(MultiSet<A> ms) {
  pnl(ms == null ? null : ms.asMap());
  return ms;
}



static <A, B> MultiMap<A, B> pnl(MultiMap<A, B> mm) {
  pnl(mm == null ? null : mm.asMap());
  return mm;
}



static String nlLogic_text(Exp e) {
  return e == null ? null : e.text();
}

static String nlLogic_text(IfThen r) {
  return r == null ? null : r.text();
}


static Color getColor(BufferedImage img, int x, int y) {
  return colorFromRGBA(img.getRGB(x, y));
}

static Color getColor(BufferedImage img, Pt p) {
  return colorFromRGBA(img.getRGB(p.x, p.y));
}


static int rgbInt(int r, int g, int b) {
  return (clamp(r, 0, 255) << 16) | (clamp(g, 0, 255) << 8) | clamp(b, 0, 255);
}


static int asInt(Object o) {
  return toInt(o);
}




static String formatLocalDateWithSeconds(long time) {
  return localDateWithSeconds(time);
}



static String formatLocalDateWithSeconds() {
  return localDateWithSeconds();
}


static BigInteger plus(BigInteger a, BigInteger b) {
  return a.add(b);
}

static BigInteger plus(BigInteger a, long b) {
  return a.add(bigint(b));
}

static long plus(long a, long b) { return a+b; }
static int plus(int a, int b) { return a+b; }
static float plus(float a, float b) { return a+b; }
static double plus(double a, double b) { return a+b; }


static long clockTimeToSystemTime(long now) {
  return now == 0 ? 0 : now + clockToSysTimeDiff();
}


static List syncMap(Object f, Map map) {
  return syncMap(map, f);
}

// map: func(key, value) -> list element
static List syncMap(Map map, Object f) {
  return map(cloneLinkedHashMap(map), f); // TODO: use a temporary list instead
}

static <A, B> Map<A, B> syncMap() {
  return synchroHashMap();
}

static <A, B> Map<A, B> syncMap(Map map) {
  return synchronizedMap(map);
}


static boolean stdEq2(Object a, Object b) {
  if (a == null) return b == null;
  if (b == null) return false;
  if (a.getClass() != b.getClass()) return false;
  for (String field : allFields(a))
    if (neq(getOpt(a, field), getOpt(b, field)))
      return false;
  return true;
}


static int stdHash2(Object a) {
  if (a == null) return 0;
  return stdHash(a, toStringArray(allFields(a)));
}


static int seconds() {
  return seconds(Calendar.getInstance());
}

static int seconds(Calendar c) {
  return c.get(Calendar.SECOND);
}


static String formatDouble(double d, int digits) {
  String format = digits <= 0 ? "0" : "0." + rep(digits, '#');
  return decimalFormatEnglish(format, d);
}

static String formatDouble(double d) {
  return str(d);
}





static String div(Object contents, Object... params) {
  return hfulltag("div", contents, params);
}

static String div() {
  return div("");
}


static BigInteger div(BigInteger a, BigInteger b) {
  return a.divide(b);
}

static BigInteger div(BigInteger a, int b) {
  return a.divide(bigint(b));
}


static Complex div(Complex a, double b) {
  return new Complex(a.re/b, a.im/b);
}


static double div(double a, double b) { return a/b; }
static double div(double a, int b) { return a/b; }
static int div(int a, int b) { return a/b; }


static String assertGlobalID(String s) {
  return assertPossibleGlobalID(s);
}


static BigInteger bigint(String s) {
  return new BigInteger(s);
}

static BigInteger bigint(long l) {
  return BigInteger.valueOf(l);
}


static BigInteger mul(BigInteger a, BigInteger b) {
  return a.multiply(b);
}

static BigInteger mul(BigInteger a, long b) {
  return a.multiply(bigint(b));
}

static int mul(int a, int b) { return a*b; }


static String bigintToGlobalID(BigInteger value) {
  char[] buf = new char[16];
  for (int i = 16-1; i >= 0; i--) {
    buf[i] = charPlus('a', mod(value, 26).intValue());
    value = div(value, 26);
  }
  return str(buf);
}




volatile static boolean conceptsAndBot_running = false;
static boolean conceptsAndBot_thinOnStart = true;

static void conceptsAndBot() {
  conceptsAndBot(null);
}

static void conceptsAndBot(Integer autoSaveInterval) {
  if (conceptsAndBot_running) return;
  conceptsAndBot_running = true;
  
  Concepts cc = db_mainConcepts();
  cc.programID = getDBProgramID();
  
  try {
    if (cc.useFileLock) {
      if (!cc.fileLock().tryToLock()) {
        ensureDBNotRunning(dbBotStandardName());
        cc.fileLock().forceLock();
      }
    } else
      ensureDBNotRunning(dbBotStandardName());
  } catch (Throwable _e) {
    cc.dontSave = true; // SAFETY
  
throw rethrow(_e); }
  
  cc.persist(autoSaveInterval);
  dbBot(false);
  
  if (conceptsAndBot_thinOnStart) { try {
    thinAProgramsBackups(getDBProgramID(), true);
  } catch (Throwable __e) { printStackTrace(__e); }}
}


static boolean eqOneOf(Object o, Object... l) {
  for (Object x : l) if (eq(o, x)) return true; return false;
}


static Method findMethod_cached(Object o, String method, Object... args) { try {
  if (o == null) return null;
  if (o instanceof Class) {
    _MethodCache cache = callOpt_getCache((Class) o);
    List<Method> methods = cache.cache.get(method);
    if (methods != null) for (Method m : methods)
      if (isStaticMethod(m) && findMethod_checkArgs(m, args, false))
        return m;
    return null;
  } else {
    _MethodCache cache = callOpt_getCache(o.getClass());
    List<Method> methods = cache.cache.get(method);
    if (methods != null) for (Method m : methods)
      if (findMethod_checkArgs(m, args, false))
        return m;
    return null;
  }
} catch (Exception __e) { throw rethrow(__e); } }



static IMeta toIMeta(Object o) {
  if (o == null) return null;
  if (o instanceof IMeta) return ((IMeta) o);
  if (o instanceof JComponent) return initMetaOfJComponent((JComponent) o);
  if (o instanceof BufferedImage) return optCast(IMeta.class, ((BufferedImage) o).getProperty("meta"));
  return null;
}


static Map makeObjectMetaMap() {
  //ret synchroLinkedHashMap();
  return new CompactHashMap();
}


static IAutoCloseableF0 tempMetaMutex(IMeta o) {
  return o == null ? null : o._tempMetaMutex();
}


static <A, B> void syncMapPut2(Map<A, B> map, A key, B value) {
  if (map != null && key != null) synchronized(collectionMutex(map)) {
    if (value != null) map.put(key, value);
    else map.remove(key);
  }
}


static ImageSurface imageSurface(BufferedImage img) {
  return swingNu(ImageSurface.class, img);
}

static ImageSurface imageSurface(MakesBufferedImage img) {
  return swingNu(ImageSurface.class, img);
}

static ImageSurface imageSurface() {
  return swingNu(ImageSurface.class);
}


static BufferedImage toBufferedImageOpt(Object o) {
  if (o instanceof BufferedImage) return ((BufferedImage) o);
  if (o instanceof Image) return copyImage((Image) o);
  if (o instanceof MakesBufferedImage)
    return ((MakesBufferedImage) o).getBufferedImage();
  if (o instanceof File)
    if (isImageFile((File) o))
      return loadBufferedImageFile((File) o);
  String c = getClassName(o);
  
  // Keep this because it also works on imported objects
  if (eqOneOf(c, "main$BWImage", "main$RGBImage"))
    return (BufferedImage) call(o, "getBufferedImage");
    
  if (eq(c, "main$PNGFile"))
    return (BufferedImage) call(o, "getImage");
  return null;
}


static ThreadLocal<Boolean> assertVerbose_value = new ThreadLocal();

static void assertVerbose(boolean b) {
  assertVerbose_value.set(b);
}

static boolean assertVerbose() { return isTrue(assertVerbose_value.get()); }


static <A> A assertEqualsVerbose(Object x, A y) {
  assertEqualsVerbose((String) null, x, y);
  return y;
}

// x = expected, y = actual
static <A> A assertEqualsVerbose(String msg, Object x, A y) {
  if (!eq(x, y)) {
    

    throw fail((nempty(msg) ? msg + ": " : "") + "expected: "+ x + ", got: " + y);
  } else
    print("OK" + (empty(msg) ? "" : " " + msg) + ": " + /*sfu*/(x));
  return y;
}




static boolean removeFile(File file) {
  return deleteFile(file);
}


static List<String> toLinesFullTrim(String s) {
  
  List<String> l = new ArrayList();
  for (String line : toLines(s)) if (nempty(line = trim(line))) l.add(line);
  return l;
}

static List<String> toLinesFullTrim(File f) {
  List<String> l = new ArrayList();
  for (String line : linesFromFile(f)) if (nempty(line = trim(line))) l.add(line);
  return l;
}



static <A extends Component> A jPreferHeight(int h, A c) {
  Dimension size = c.getPreferredSize();
  c.setPreferredSize(new Dimension(size.width, max(h, size.height)));
  return c;
}


static HashMap litmap(Object... x) {
  HashMap map = new HashMap();
  litmap_impl(map, x);
  return map;
}

static void litmap_impl(Map map, Object... x) {
  if (x != null) for (int i = 0; i < x.length-1; i += 2)
    if (x[i+1] != null)
      map.put(x[i], x[i+1]);
}


static String joinNempties(String sep, Object... strings) {
  return joinStrings(sep, strings);
}

static String joinNempties(String sep, Iterable strings) {
  return joinStrings(sep, strings);
}


static String formatWithThousands(long l) {
  return formatWithThousandsSeparator(l);
}


static double fraction(double d) {
  return d % 1;
}


static String n_fancy2(long l, String singular, String plural) {
  return formatWithThousandsSeparator(l) + " " + trim(l == 1 ? singular : plural);
}

static String n_fancy2(Collection l, String singular, String plural) {
  return n_fancy2(l(l), singular, plural);
}

static String n_fancy2(Map m, String singular, String plural) {
  return n_fancy2(l(m), singular, plural);
}

static String n_fancy2(Object[] a, String singular, String plural) {
  return n_fancy2(l(a), singular, plural);
}


  static String n_fancy2(MultiSet ms, String singular, String plural) {
    return n_fancy2(l(ms), singular, plural);
  }



static boolean isSpaceEtc(char c) {
  return c == ' ' || c == '\t' || c == '\r' || c == '\n';
}


static File fileAppendToName(File f, String suffix) {
  return newFile(f.getPath() + suffix);
}


static <A extends Concept> A uniqueConcept(Class<A> c, Object... params) {
  return uniqueConcept(db_mainConcepts(), c, params);
}

static <A extends Concept> A uniqueConcept(Concepts cc, Class<A> c, Object... params) {
   AutoCloseable __1 = tempDBLock(cc); try {
  params = expandParams(c, params);
  A x = findConceptWhere(cc, c, params);
  if (x == null) {
    x = unlisted(c);
    
    csetAll(x, params);
    cc.register(x);
  } else {
    
  }
  return x;
} finally { _close(__1); }}


static AutoCloseable tempDBLock(Concepts concepts) {
  return tempLock(concepts.lock); // NO null propagation this time
}

static AutoCloseable tempDBLock() {
  return tempDBLock(db_mainConcepts());
}


static <A extends Concept> Object[] expandParams(Class<A> c, Object[] params) {
  if (l(params) == 1)
    params = new Object[] { singleFieldName(c), params[0] };
  else
    warnIfOddCount(params);
  return params;
}



static <A extends Concept> A findConceptWhereCI(Class<A> c, Object... params) {
  return findConceptWhereCI(db_mainConcepts(), c, params);
}

static <A extends Concept> A findConceptWhereCI(Concepts concepts, Class<A> c, Object... params) {
  params = expandParams(c, params);
  
  // find smallest index to use
  Lowest<IF0<Collection<A>>> bestIndex = new Lowest();
  
  if (concepts.ciFieldIndices != null)
    for  (int i = 0; i < l(params); i += 2) { ping(); 
      Object val = params[i+1];
      IFieldIndex<A, Object> index = concepts.getCIFieldIndex(c, (String) params[i]);
      if (index != null) {
        Collection<A> l = index.getAll(val);
        bestIndex.put(() -> l, l(l));
      }
      if (concepts.useBackRefsForSearches && val instanceof Concept) {
        // conservatively use full ref count as score until we store backRefs by class
        bestIndex.put(() -> findBackRefs(((Concept) val), c), ((Concept) val)._backRefCount());
      }
    }
  
  
  if (bestIndex.has()) {
    Collection<A> l = bestIndex.get().get();
    if (l(params) == 2) return first(l); // nothing to filter, index should suffice
    for  (A x : l)
      { ping(); if (checkConceptFieldsIC(x, params)) return x; }
    return null;
  } else {
    // table scan
    for  (A x : concepts.list(c))
      { ping(); if (checkConceptFieldsIC(x, params)) return x; }
    return null;
  }
}

static Concept findConceptWhereCI(Concepts concepts, String c, Object... params) {
  for  (Concept x : concepts.list(c))
    { ping(); if (checkConceptFieldsIC(x, params)) return x; }
  return null;
}



// make concept instance that is not connected to DB
static <A extends Concept> A unlisted(Class<A> c, Object... args) {
  concepts_unlisted.set(true);
  try {
    return nuObject(c, args);
  } finally {
    concepts_unlisted.set(null);
  }
}

static Concept unlisted(String name, Object... args) {
  Class<? extends Concept> cc = findClass(name);
  concepts_unlisted.set(true);
  try {
    return cc != null ? nuObject(cc) : new Concept(name);
  } finally {
    concepts_unlisted.set(null);
  }
}


static int csetAll(Concept c, Object... values) {
  return cset(c, values);
}

static int csetAll(Iterable<? extends Concept> l, Object... values) {
  int n = 0;
  for (Concept c : unnullForIteration(l))
    n += cset(c, values);
  return n;
}

static int csetAll(Concept c, Map<String, Object> values) {
  int n = 0;
  for (Map.Entry<? extends String, ? extends Object> __0 : _entrySet( values))
    { String field = __0.getKey(); Object value = __0.getValue();  n += cset(c, field, value); }
  return n;
}


static String dbProgramID() {
  return getDBProgramID();
}


static <A> boolean any(Object pred, Iterable<A> l) {
  if (l != null) for (A a : l)
    if (isTrue(callF(pred, a)))
      return true;
  return false;
}

static <A> boolean any(IF1<A, Boolean> pred, Iterable<A> l) {
  if (l != null) for (A a : l)
    if (pred.get(a))
      return true;
  return false;
}

static <A> boolean any(Iterable<A> l, IF1<A, Boolean> pred) {
  return any(pred, l);
}

static <A> boolean any(A[] l, IF1<A, Boolean> pred) {
  if (l != null) for (A a : l)
    if (pred.get(a))
      return true;
  return false;
}

static boolean any(Iterable<Boolean> l) {
  if (l != null) for (Boolean a : l)
    if (isTrue(a))
      return true;
  return false;
}


static String conceptsFileName() {
  return "concepts.structure.gz";
}


static <A> List<A> filter(Iterable<A> c, Object pred) {
  if (pred instanceof F1) return filter(c, (F1<A, Boolean>) pred);

  
  List x = new ArrayList();
  if (c != null) for (Object o : c)
    if (isTrue(callF(pred, o)))
      x.add(o);
  return x;
}

static List filter(Object pred, Iterable c) {
  return filter(c, pred);
}

static <A, B extends A> List<B> filter(Iterable<B> c, F1<A, Boolean> pred) {
  List x = new ArrayList();
  if (c != null) for (B o : c)
    if (pred.get(o))
      x.add(o);
  return x;
}

static <A, B extends A> List<B> filter(F1<A, Boolean> pred, Iterable<B> c) {
  return filter(c, pred);
}

//ifclass IF1
static <A, B extends A> List<B> filter(Iterable<B> c, IF1<A, Boolean> pred) {
  List x = new ArrayList();
  if (c != null) for (B o : c)
    if (pred.get(o))
      x.add(o);
  return x;
}

static <A, B extends A> List<B> filter(B[] c, IF1<A, Boolean> pred) {
  List x = new ArrayList();
  if (c != null) for (B o : c)
    if (pred.get(o))
      x.add(o);
  return x;
}

static <A, B extends A> List<B> filter(IF1<A, Boolean> pred, Iterable<B> c) {
  return filter(c, pred);
}
//endif


static List<File> listDirs(File dir) {
  if (dir == null) return emptyList();
  File[] files = dir.listFiles();
  List<File> l = new ArrayList();
  if (files != null)
    for (File f : files)
      if (f.isDirectory())
        l.add(f);
  return l;
}

static List<File> listDirs(String dir) {
  return listDirs(newFile(dir));
}


static <A> VF1<A> ivf1ToVF1(IVF1<A> f) {
  return f == null ? null : new VF1<A>() { public void get(A a) { try {  f.get(a) ; } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "f.get(a)"; }};
}


static Pt pt(int x, int y) {
  return new Pt(x, y);
}

static Pt pt(int x) {
  return new Pt(x, x);
}


static <A> A parentOfType(Component _c, Class<A> theClass) {
  return swing(() -> {
    Component c = _c;
    while (c != null) {
      if (isInstance(theClass, c)) return (A) c;
      c = c.getParent();
    }
    return null;
  });
}


static <A, B> int lKeys(MultiMap<A, B> mm) {
  return mm == null ? 0 : mm.keysSize();
}


static int latestInstalledJavaX() {
  File[] files = new File(userHome(), ".javax").listFiles();
  int v = 0;
  if (files != null) for (File f : files) {
    Matcher m = regexpMatcher("x(\\d\\d\\d?)\\.jar", f.getName());
    if (m.matches())
      v = Math.max(v, Integer.parseInt(m.group(1)));
  }
  return v;
}


static String x30JarServerURL() {
  return "http://botcompany.de:8081/x30.jar";
}


static Iterator emptyIterator() {
  return Collections.emptyIterator();
}


// This is a bit rough... finds static and non-static methods.

static Method findMethodNamed(Object obj, String method) {
  if (obj == null) return null;
  if (obj instanceof Class)
    return findMethodNamed((Class) obj, method);
  return findMethodNamed(obj.getClass(), method);
}

static Method findMethodNamed(Class c, String method) {
  while (c != null) {
    for (Method m : c.getDeclaredMethods())
      if (m.getName().equals(method)) {
        makeAccessible(m);
        return m;
      }
    c = c.getSuperclass();
  }
  return null;
}


static int hashMap_internalHash(Object key) {
  int h;
  return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}


static <A> List<A> syncList() {
  return synchroList();
}



static <A> List<A> syncList(List<A> l) {
  return synchroList(l);
}


static <A> List<A> syncCloneAndClearList(Collection<A> l) {
  if (l == null) return emptyList();
  synchronized(collectionMutex(l)) {
    List<A> l2 = cloneList(l);
    l.clear();
    return l2;
  }
}


static void cancelThread(Thread t) {
  if (t == null) return;
  ping();
  
  
  
  
  
    synchronized(ping_actions) {
      ping_actions.put(t, "cancelled");
      ping_anyActions = true;
    }
  
  
  
}


static boolean checkFields(Object x, Object... data) {
  for (int i = 0; i < l(data); i += 2)
    if (neq(getOpt(x, (String) data[i]), data[i+1]))
      return false;
  return true;
}


static void setSCPComponent(SingleComponentPanel scp, Component c) {
  if (scp != null) scp.setComponent(c);
}


static void addToContainer(Container a, Component... b) {
  if (a == null) return;
  { swing(() -> { 
    for (Component c : unnullForIteration(b))
      if (c != null) 
        a.add(c);
  }); }
}


static <A extends Component> A revalidate(final A c) {
  if (c == null || !c.isShowing()) return c;
  { swing(() -> { 
    // magic combo to actually relayout and repaint
    c.revalidate();
    c.repaint();
  }); }
  return c;
}

static void revalidate(JFrame f) { revalidate((Component) f); }
static void revalidate(JInternalFrame f) { revalidate((Component) f); }


static String joinNemptiesWithComma(Object... strings) {
  return joinNempties(", ", strings);
}

static String joinNemptiesWithComma(Iterable strings) {
  return joinNempties(", ", strings);
}


static List flattenCollectionsAndArrays(Iterable a) {
  List l = new ArrayList();
  for (Object x : a)
    if (x instanceof Collection)
      l.addAll(flattenCollectionsAndArrays((Collection) x));
    else if (x instanceof Object[])
      l.addAll(flattenCollectionsAndArrays(asList((Object[]) x)));
    else
      l.add(x);
  return l;
}


static Complex complex(double re, double im) {
  return new Complex(re, im);
}

static Complex complex(double re) {
  return new Complex(re, 0.0);
}

static Complex complex(double[] reIm) {
  if (empty(reIm)) return null;
  if (l(reIm) != 2) throw fail("Need 2 doubles to make complex number");
  return complex(reIm[0], reIm[1]);
}


static <A extends IMeta> A setMetaSrc(A a, Object src) {
  setMetaAndVerify(a, "src", src);
  return a;
}

static <A> A setMetaSrc(A a, Object src) {
  setMetaAndVerify(a, "src", src);
  return a;
}



static double nanoSecondsToSeconds(double nanos) {
  return nanos*1e-9;
}


static <A extends JComponent> A applyDefaultMargin(final A c) {
  if (c != null) { swing(() -> { 
    c.setBorder(BorderFactory.createEmptyBorder(withMargin_defaultWidth, withMargin_defaultWidth, withMargin_defaultWidth, withMargin_defaultWidth));
  }); }
  return c;
}


static JPanel northAndCenter(Component n, Component c) {
  return centerAndNorth(c, n);
}


static int withBottomMargin_defaultWidth = 6;

static JPanel withBottomMargin(Component c) {
  return withBottomMargin(withBottomMargin_defaultWidth, c);
}

static JPanel withBottomMargin(final int w, final Component c) {
  return swing(new F0<JPanel>() { public JPanel get() { try { 
    JPanel p = new JPanel(new BorderLayout());
    p.setBorder(BorderFactory.createEmptyBorder(0, 0, w, 0));
    p.add(c);
    return p;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "JPanel p = new JPanel(new BorderLayout);\r\n    p.setBorder(BorderFactory.creat..."; }});
}




static <A extends JComponent> A applyMargin(int top, int left, int bottom, int right, A c) {
  return addMargin(top, left, bottom, right, c);
}



static <A extends JComponent> A applyMargin(int w, A c) {
  return addMargin(w, c);
}


static Component horizontalStrut(int height) {
  return Box.createHorizontalStrut(height);
}


static JPanel westAndCenter(final Component w, final Component c) {
  return swing(new F0<JPanel>() { public JPanel get() { try { 
    JPanel panel = new JPanel(new BorderLayout());
    panel.add(BorderLayout.WEST, wrap(w));
    panel.add(BorderLayout.CENTER, wrap(c));
    return panel;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "JPanel panel = new JPanel(new BorderLayout);\r\n    panel.add(BorderLayout.WEST..."; }});
}


static JPanel westAndCenterWithMargin(Component w, Component c) {
  return westAndCenter(withRightMargin(w), c);
}

static JPanel westAndCenterWithMargin(int margin, Component w, Component c) {
  return westAndCenter(withRightMargin(margin, w), c);
}


static JScrollPane borderlessScrollPane(JScrollPane sp) {
  return setBorder(null, withoutViewportBorder(sp));
}


static JScrollPane jscroll_centered(Component c) {
  return jscroll(jFullCenter(c));
}


static void addDirectMenuItem(JMenuBar mb, String text, Object action) {
  if (mb != null) { swing(() -> { 
    addDirectMenuItem(mb, directJMenuItem(text, action));
  }); }
}

static void addDirectMenuItem(Component c, String text, Object action) {
  addDirectMenuItem(addMenuBar(c), text, action);
}

static void addDirectMenuItem(JMenuBar mb, JMenuItem menuItem) {
  if (mb != null) { swing(() -> { 
    mb.add(menuItem);
    revalidate(mb);
  }); }
}


static char charAt(String s, int i) {
  return s != null && i >= 0 && i < s.length() ? s.charAt(i) : '\0';
}


static <A extends JComponent> A bindLiveValueListenerToComponent(A component, final LiveValue lv, final Runnable listener) {
  if (lv != null)
    bindToComponent(component,
      new Runnable() {  public void run() { try { 
        
        lv.onChangeAndNow(listener);
      
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "ifdef bindLiveValueListenerToComponent_debug\r\n          print(\"bindLiveValueL..."; }},
      new Runnable() {  public void run() { try {  lv.removeChangeListener(listener) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "lv.removeChangeListener(listener)"; }});
  return component;
}


static <A extends JLabel, B> A bindJLabelToTransformedLiveValue(IF1<B, Object> f, A label, LiveValue<B> lv) {
  bindLiveValueListenerToComponent(label, lv,
    () -> { setText(label, strOrNull(f.get(lv.get()))); });
  return label;
}


static MouseAdapter onMouseDown(Component c, Runnable r) {
  return onMouseDown(c, runnableToIVF1(r));
}

static MouseAdapter onMouseDown(Component c, IVF1<MouseEvent> f) {
  return c == null || f == null ? null : swing(() -> {
    MouseAdapter ma = new MouseAdapter() {
      public void mousePressed(MouseEvent e) {
        if (e.getButton() == MouseEvent.BUTTON1)
          f.get(e);
      }
    };
    c.addMouseListener(ma);
    return ma;
  });
}


static <A> IVF1<A> runnableToIVF1(Runnable r) {
  return r == null ? null : a -> r.run();
}


static int numberOfChildren(final Container c) {
  return c == null ? 0 : swing(new F0<Integer>() { public Integer get() { try {  return c.getComponentCount();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return c.getComponentCount();"; }});
}


// Use like this: printVars_str(+x, +y);
// Or: printVars("bla", +x);
// Or: printVars bla(+x);
static void printVars_str(Object... params) {
  print(renderVars_str(params));
}


static ClassLoader getClassLoader(Object o) {
  return o == null ? null : _getClass(o).getClassLoader();
}


// subPath is relative and with slashes
static boolean dirOrZipContainsPath(File location, String subPath) { try {
  if (location.isDirectory()) {
    return new File(location, subPath).exists();
  } else if (location.isFile()) {
    return zipFileContains_falseOnError(location, subPath);
  }
  return false;
} catch (Exception __e) { throw rethrow(__e); } }


static File[] listFiles(File dir) {
  File[] files = dir.listFiles();
  return files == null ? new File[0] : files;
}

static File[] listFiles(String dir) {
  return listFiles(new File(dir));
}


static String dropPrefixOrNull(String prefix, String s) {
  return s != null && s.startsWith(prefix) ? s.substring(l(prefix)) : null;
}


// f must return a list
static List concatMap(Object f, Iterable l) {
  return concatLists(map(f, l));
}

static List concatMap(Iterable l, Object f) {
  return concatMap(f, l);
}

static List concatMap(Object f, Object[] l) {
  return concatLists(map(f, l));
}

static List concatMap(Object[] l, Object f) {
  return concatMap(f, l);
}

static <A, B, C extends Iterable<B>> List<B> concatMap(Iterable<A> l, IF1<A, C> f) {
  return concatMap(l, (Object) f);
}

static <A, B, C extends Iterable<B>> List<B> concatMap(IF1<A, C> f, Iterable<A> l) {
  return concatMap(l, f);
}

static <A, B, C extends Iterable<B>> List<B> concatMap(IF1<A, C> f, A[] l) {
  return concatMap((Object) f, l);
}


static List<String> loadedJigsawModuleNames() {
  var moduleLayer = ModuleLayer.boot();
  var modules = moduleLayer.modules();
  return map(modules, mod -> mod.getName());
}



// moduleName = e.g. "java.scripting"
static List<String> classNamesInJigsawModule(String moduleName) { return classNamesInJigsawModule(moduleName, javaHome()); }
static List<String> classNamesInJigsawModule(String moduleName, File javaHome) {
  return classNamesInJarOrDir(jigsawModuleFile(moduleName, javaHome), "classes/");
}


// undefined color, seems to be all black in practice
// This is without alpha?
static BufferedImage newBufferedImage(int w, int h) {
  return new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
}


static BufferedImage newBufferedImage(int w, int h, RGB rgb) {
  return newBufferedImage(w, h, rgb.getColor());
}


static BufferedImage newBufferedImage(int w, int h, Color color) {
  BufferedImage img = newBufferedImage(w, h);
  Graphics2D g = img.createGraphics();
  g.setColor(or(color, Color.white));
  g.fillRect(0, 0, w, h);
  return img;
}


static BufferedImage newBufferedImage(Pt p, Color color) {
  return newBufferedImage(p.x, p.y, color);
}


// This one is with alpha...
static BufferedImage newBufferedImage(int w, int h, int[] pixels) {
  return intArrayToBufferedImage(pixels, w, h);
}


static void setChecked(JCheckBox checkBox, boolean b) {
  if (checkBox != null) { swing(() -> { 
    if (isChecked(checkBox) != b)
      checkBox.setSelected(b);
  }); }
}

static void setChecked(JCheckBoxMenuItem mi, boolean b) {
  if (mi != null) { swing(() -> {  mi.setSelected(b); }); }
}


static String defaultFrameTitle() {
  return autoFrameTitle();
}

static void defaultFrameTitle(String title) {
  autoFrameTitle_value = title;
}


static void registerEscape(JFrame frame, final Runnable r) {
  registerEscape_rootPane(frame.getRootPane(), r);
}


static void disposeWindow(final Window window) {
  if (window != null) { swing(() -> { 
    window.dispatchEvent(new WindowEvent(window, WindowEvent.WINDOW_CLOSING)); // call listeners
    myFrames_list.remove(window);
    window.dispose();
  }); }
}

static void disposeWindow(final Component c) {
  disposeWindow(getWindow(c));
}

static void disposeWindow(Object o) {
  if (o != null) disposeWindow(((Component) o));
}

static void disposeWindow() {
  disposeWindow(heldInstance(Component.class));
}


static void inputFilePath(final String msg, final Object action) {
  inputFilePath(msg, userDir(), action);
}

// action: voidfunc(File)
static void inputFilePath(final String msg, final File defaultFile, final Object action) {
  swingLater(new Runnable() {  public void run() { try { 
    final JTextField tfPath = jtextfield(f2s(or(defaultFile, userDir())));
    String title = joinStrings(" | ", msg, programName());
    JComponent form = showFormTitled(title,
      unnull(msg), centerAndEast(tfPath, jbutton("Browse...", new Runnable() {  public void run() { try { 
        JFileChooser fileChooser = new JFileChooser(getTextTrim(tfPath));
        if (fileChooser.showOpenDialog(tfPath) == JFileChooser.APPROVE_OPTION) {
          tfPath.setText(fileChooser.getSelectedFile().getAbsolutePath());
          tfPath.requestFocus();
        }
      
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "JFileChooser fileChooser = new JFileChooser(getTextTrim(tfPath));\r\n        if..."; }})), new Runnable() {  public void run() { try { 
        callF(action, new File(getTextTrim(tfPath)))
      ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "callF(action, new File(getTextTrim(tfPath)))"; }});
    renameSubmitButton(form, "OK");
  
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "final JTextField tfPath = jtextfield(f2s(or(defaultFile, userDir())));\r\n    S..."; }});
}


static boolean hasJPEGExtension(File f) {
  return ewicOneOf(fileName(f), ".jpg", ".jpeg");
}


static void saveJPG(BufferedImage img, File file) { try {
  if (!ImageIO.write(img, "jpeg", mkdirsFor(file))) {
    print("Reconstructing image for saving JPEG");
    img = reconstructBufferedImage(img);
    if (!ImageIO.write(img, "jpeg", file))
      throw fail("Couldn't write JPEG: " + file + " (" + img + ")");
  }
  vmBus_send("wroteFile", file);
} catch (Exception __e) { throw rethrow(__e); } }

static void saveJPG(File file, BufferedImage img) { try {
  saveJPG(img, file);
} catch (Exception __e) { throw rethrow(__e); } }


static void savePNG(BufferedImage img, File file) { try {
  File tempFile = new File(file.getPath() + "_temp");
  CriticalAction ca = beginCriticalAction("Save " + f2s(file));
  try {
    ImageIO.write(img, "png", mkdirsFor(tempFile));
    file.delete();
    tempFile.renameTo(file);
  } finally {
    ca.done();
  }
} catch (Exception __e) { throw rethrow(__e); } }

// gotta love convenience & program-smartness
static void savePNG(String file, BufferedImage img) { savePNG(toFile(file), img); }

static void savePNG(File file, BufferedImage img) {
  savePNG(img, file);
}


static void savePNG(File file, RGBImage img) {
  savePNG(file, img.getBufferedImage());
}



static JComponent selectSnippetID_v1(final VF1<String> onSelect) {
  return selectSnippetID_v1("#", onSelect);
}

static JComponent selectSnippetID_v1(String defaultID, final VF1<String> onSelect) {
  final JTextField tfSnippetID = jtextfield(defaultID);
  if (eq(defaultID, "#")) moveCaretToEnd(tfSnippetID);
  
  JComponent panel;
  renameSubmitButton(panel = showTitledForm("Select Snippet",
    "Snippet ID:", tfSnippetID, runnableThread(new Runnable() {  public void run() { try { 
      callF(onSelect, fsI(getTextTrim(tfSnippetID)));
    
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "callF(onSelect, fsI(getTextTrim(tfSnippetID)));"; }})), "Select snippet");
  return panel;
}


static BufferedImage cloneBufferedImage(BufferedImage image) {
  return copyImage(image);
}


static BufferedImage clipBufferedImage(BufferedImage src, Rectangle clip) {
  return clipBufferedImage(src, new Rect(clip));
}

static BufferedImage clipBufferedImage(BufferedImage src, Rect r) {
  if (src == null || r == null) return null;
  // fixClipRect
  r = intersectRects(r, new Rect(0, 0, src.getWidth(), src.getHeight()));
  if (rectEmpty(r)) return null; // can't make zero-sized BufferedImage
    
  return src.getSubimage(r.x, r.y, r.w, r.h);
}

static BufferedImage clipBufferedImage(BufferedImage src, int x, int y, int w, int h) {
  return clipBufferedImage(src, new Rect(x, y, w, h));
}



static Object getTransferData(Transferable t, DataFlavor flavor) { try {
  return t != null && t.isDataFlavorSupported(flavor) ? t.getTransferData(flavor) : null;
} catch (Exception __e) { throw rethrow(__e); } }


static BufferedImage imageFromDataURL(String url) {
  return decodeImage(bytesFromDataURL(url));
}





static String getTextFromClipboard() { try {
  Transferable transferable = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
  if (transferable != null && transferable.isDataFlavorSupported(DataFlavor.stringFlavor))
    return (String) transferable.getTransferData(DataFlavor.stringFlavor);
  return null;
} catch (Exception __e) { throw rethrow(__e); } }


  static void popupError(final Throwable throwable) {
    throwable.printStackTrace(); // print stack trace to console for the experts
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        String text = throwable.toString();
        //text = cutPrefix(text, "java.lang.RuntimeException: ");
        JOptionPane.showMessageDialog(null, text);
      }
    });
  }


static void printShortenedFunctionCall(Object fname, Object... args) { printShortenedFunctionCall(100, fname, args); }
static void printShortenedFunctionCall(int len, Object fname, Object... args) {
  print(shorten(len, formatFunctionCall(fname, args)));
}


static <A> A printIf(boolean b, A a) {
  if (b) print(a);
  return a;
}

static <A> A printIf(boolean b, String s, A a) {
  if (b) print(s, a);
  return a;
}


static WidthAndHeight widthAndHeight(BufferedImage image) {
  return image == null ? null : widthAndHeight(image.getWidth(), image.getHeight());
}

static WidthAndHeightImpl widthAndHeight(int w, int h) {
  return new WidthAndHeightImpl(w, h);
}






// first delay = delay
static Timer installTimer(JComponent component, Object r, long delay) {
  return installTimer(component, r, delay, delay);
}

// first delay = delay
static Timer installTimer(RootPaneContainer frame, long delay, Object r) {
  return installTimer(frame.getRootPane(), r, delay, delay);
}

// first delay = delay
static Timer installTimer(JComponent component, long delay, Object r) {
  return installTimer(component, r, delay, delay);
}

static Timer installTimer(JComponent component, long delay, long firstDelay, Object r) {
  return installTimer(component, r, delay, firstDelay);
}

static Timer installTimer(final JComponent component, final Object r, final long delay, final long firstDelay) {
  return installTimer(component, r, delay, firstDelay, true);
}

static Timer installTimer(final JComponent component, final Object r, final long delay, final long firstDelay, final boolean repeats) {
  if (component == null) return null;
  return (Timer) swingAndWait(new F0<Object>() { public Object get() { try { 
    final Var<Timer> timer = new Var();
    timer.set(new Timer(toInt(delay), new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent _evt) { try {
       AutoCloseable __1 = tempActivity(r); try {
      try {
        if (!allPaused())
          if (isFalse(callF(r)))
            cancelTimer(timer.get());
      } catch (Throwable __e) { printStackTrace(__e); }
    } finally { _close(__1); }} catch (Throwable __e) { messageBox(__e); }}}));
    timer.get().setInitialDelay(toInt(firstDelay));
    timer.get().setRepeats(repeats);
    bindTimerToComponent(timer.get(), component);
    return timer.get();
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "final new Var<Timer> timer;\r\n    timer.set(new Timer(toInt(delay), actionList..."; }});
}

static Timer installTimer(RootPaneContainer frame, long delay, long firstDelay, Object r) {
  return installTimer(frame.getRootPane(), delay, firstDelay, r);
}



static ImageSurface jImageSurface() {
  return swingNu(ImageSurface.class);
}

// for BWImage
static ImageSurface jImageSurface(MakesBufferedImage img) {
  return swingNu(ImageSurface.class, img.getBufferedImage());
}

static ImageSurface jImageSurface(BufferedImage img) {
  return swingNu(ImageSurface.class, img);
}


static <A extends JComponent> A disposeFrameOnClick(final A c) {
  onClick(c, new Runnable() {  public void run() { try {  disposeFrame(c) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "disposeFrame(c)"; }});
  return c;
}


static void imageSurface_unpixelated(ImageSurface imageSurface) {
  if (imageSurface == null) return;
  imageSurface.interpolationMode = RenderingHints.VALUE_INTERPOLATION_BILINEAR;
  repaint(imageSurface);
}


static Object getOptMC(String field) {
  return getOpt(mc(), field);
}


static <A extends Concept> Collection<A> findConceptsWhere(Class<A> c, Object... params) {
  return findConceptsWhere(db_mainConcepts(), c, params);
}

static Collection<Concept> findConceptsWhere(String c, Object... params) {
  return findConceptsWhere(db_mainConcepts(), c, params);
}

static <A extends Concept> Collection<A> findConceptsWhere(Concepts concepts, Class<A> c, Object... params) {
  ping();
  params = expandParams(c, params);
  
  // indexed
  if (concepts.fieldIndices != null)
    for (int i = 0; i < l(params); i += 2) {
      IFieldIndex<A, Object> index = concepts.getFieldIndex(c, (String) params[i]);
      if (index != null) {
        Collection<A> rawList = index.getAll(params[i+1]);
        params = dropEntryFromParams(params, i);
        if (params == null) return rawList;
        List<A> l = new ArrayList();
        for (A x : rawList)
          if (checkConceptFields(x, params)) l.add(x);
        return l;
      }
    }
    
  // table scan
  return filterConcepts(concepts.list(c), params);
}

static Collection<Concept> findConceptsWhere(Concepts concepts, String c, Object... params) {
  return filterConcepts(concepts.list(c), params);
}



static <A> A pcallF_typed(F0<A> f) {
  try { return f == null ? null : f.get(); } catch (Throwable __e) { printStackTrace(__e); } return null;
}



static <A, B> B pcallF_typed(F1<A, B> f, A a) {
  try { return f == null ? null : f.get(a); } catch (Throwable __e) { printStackTrace(__e); } return null;
}



static <A> void pcallF_typed(VF1<A> f, A a) {
  try {
    { if (f != null) f.get(a); }
  } catch (Throwable __e) { printStackTrace(__e); }
}


static <A> void pcallF_typed(IVF1<A> f, A a) {
  try {
    { if (f != null) f.get(a); }
  } catch (Throwable __e) { printStackTrace(__e); }
}

static Object pcallF_typed(Runnable r) {
  try { { if (r != null) r.run(); } } catch (Throwable __e) { printStackTrace(__e); } return null;
}

static <A> A pcallF_typed(IF0<A> f) {
  try { return f == null ? null : f.get(); } catch (Throwable __e) { printStackTrace(__e); } return null;
}

static <A, B> B pcallF_typed(IF1<A, B> f, A a) {
  try { return f == null ? null : f.get(a); } catch (Throwable __e) { printStackTrace(__e); } return null;
}



static void close_pcall(AutoCloseable c) {
  if (c != null) { try { c.close(); } catch (Throwable __e) { printStackTrace(__e); }}
}


static void preCleanUp(Object c) {
  if (c instanceof Collection) { for (Object o : ((Collection) c)) preCleanUp(o); return; }
  callOpt(c, "licensed_off");
  setOpt_raw(c, "ping_anyActions" , true); // so ping notices
  setOpt_raw(c, "cleaningUp_flag" , true);
}


static void innerCleanUp(Object c) {
  // call custom cleanMeUp() and cleanMeUp_*() functions
  
  if (!isFalse(pcallOpt(c, "cleanMeUp")))
    for (String name : sorted(methodsStartingWith(c, "cleanMeUp_"))) try {
      callOpt(c, name);
    } catch (Throwable e) {
      print("Error cleaning up: " + programID(c));
      _handleException(e);
    }
}

static void innerCleanUp() {
  innerCleanUp(mc());
}


static Object pcallOpt(Object o, String method, Object... args) {
  try { return callOpt(o, method, args); } catch (Throwable __e) { printStackTrace(__e); }
  return null;
}


static List<Thread> registeredThreads(Object o) {
  Map<Thread, Boolean> map =  (Map<Thread, Boolean>) (getOpt(o, "_registerThread_threads"));
  if (map == null) return ll();
  map.size(); // force clean-up
  synchronized(map) { return asList(keys(map)); }
}

static List<Thread> registeredThreads() {
  _registerThread_threads.size(); // force clean-up
  return asList(keys(_registerThread_threads));
}


static void interruptThreads(Collection<Thread> threads) {
  for (Thread t : unnull(threads))
    interruptThread(t);
}

static void interruptThreads(Class mainClass) {
  interruptThreads(registeredThreads(mainClass));
}


static void retireClassLoader(ClassLoader cl) {
  if (isJavaXClassLoader(cl))
    setOptAll(cl, "retired" , true, "retiredMarker" , new DefunctClassLoader());
}



static <A> void replaceLastElement(List<A> l, A a) {
  if (nempty(l))
    l.set(l(l)-1, a);
}


static Object[] toArray(Collection c) {
  return toObjectArray(c);
}

static <A> A[] toArray(Class<A> type, Iterable<A> c) {
  return toArray(c, type);
}

static <A> A[] toArray(Iterable<A> c, Class<A> type) {
  A[] a = arrayOfType(l(c), type);
  if (a.length == 0) return a;
  asList(c).toArray(a);
  return a;
}

// array must have correct length and will be filled
static <A> A[] toArray(A[] array, Collection c) {
  if (array == null || c == null) return null;
  asList(c).toArray(array);
  return array;
}


static <A> String pnlToString(String prefix, Iterable<A> l) {
  return hijackPrint(new Runnable() {  public void run() { try {  pnl(prefix, l) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "pnl(prefix, l)"; }});
}

static <A> String pnlToString(final Iterable<A> l) {
  return hijackPrint(new Runnable() {  public void run() { try {  pnl(l) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "pnl(l)"; }});
}

static <A> String pnlToString(final A[] l) {
  return hijackPrint(new Runnable() {  public void run() { try {  pnl(l) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "pnl(l)"; }});
}

static <A, B> String pnlToString(final Map<A, B> map) {
  return hijackPrint(new Runnable() {  public void run() { try {  pnl(map) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "pnl(map)"; }});
}


static <A, B> String pnlToString(MultiMap<A, B> map) {
  return pnlToString(multiMapToMap(map));
}



// Use like this: renderRecordVars("MyRecord", +x, +y)
static String renderRecordVars(String recordName, Object... params) {
  List<String> l = new ArrayList();
  int i = 0;
  for (; i+1 < l(params); i += 2)
    l.add(params[i] + "=" + params[i+1]);
  return formatFunctionCall(recordName, l);
}


static String assertIsIdentifier(String s) {
  if (!isIdentifier(s))
    throw fail("Not an identifier: " + quote(s));
  return s;
}

static String assertIsIdentifier(String msg, String s) {
  if (!isIdentifier(s))
    throw fail(msg + " - Not an identifier: " + quote(s));
  return s;
}


static Object fieldGet(Field f, Object o) { try {
  return f == null ? null : f.get(o);
} catch (Exception __e) { throw rethrow(__e); } }


static String reversedString(String s) {
  return reverseString(s);
}


// return longest prefix of s actually contained in set
static String longestPrefixInNavigableSet(String s, NavigableSet<String> set) {
  if (set == null || s == null) return null;
  while (licensed()) {
    String key = set.floor(s);
    if (key == null) break; // s is in front of whole set => no prefix in there
    int n = lCommonPrefix(key, s);
    if (n == l(key)) return key; // found!
    s = takeFirst(s, n); // shorten and try again
  }
  return null; // not found
}


static String hform(Object contents, Object... params) {
  return htag("form", contents, params);
}


// Use like this: renderVars(+x, +y)
static String renderVars_stringifier(IStringifier stringifier, Object... params) {
  List<String> l = new ArrayList();
  int i = 0;
  if (odd(l(params))) {
    l.add(stringifier.toString(first(params)));
    ++i;
  }
  for (; i+1 < l(params); i += 2)
    l.add(params[i] + "=" + stringifier.toString(params[i+1]));
  return trim(joinWithComma(l));
}


static String str_shorten(Object o) {
  return shorten_str(o);
}



static String str_shorten(Object o, int max) {
  return shorten_str(o, max);
}



static String str_shorten(int max, Object o) {
  return shorten_str(max, o);
}


static int countLines(String s) {
  return l(toLines(s)); // yeah could be optimized :-)
}


static String repeat(char c, int n) {
  n = Math.max(n, 0);
  char[] chars = new char[n];
  for (int i = 0; i < n; i++)
    chars[i] = c;
  return new String(chars);
}

static <A> List<A> repeat(A a, int n) {
  n = Math.max(n, 0);
  List<A> l = new ArrayList(n);
  for (int i = 0; i < n; i++)
    l.add(a);
  return l;
}

static <A> List<A> repeat(int n, A a) {
  return repeat(a, n);
}


static int indent_default = 2;

static String indent(int indent) {
  return repeat(' ', indent);
}

static String indent(int indent, String s) {
  return indent(repeat(' ', indent), s);
}

static String indent(String indent, String s) {
  return indent + replace(unnull(s), "\n", "\n" + indent);
}

static String indent(String s) {
  return indent(indent_default, s);
}

static List<String> indent(String indent, List<String> lines) {
  List<String> l = new ArrayList();
  if (lines != null) for (String s : lines)
    l.add(indent + s);
  return l;
}


static Lock appendToFile_lock = lock();
static boolean appendToFile_keepOpen = false;
static HashMap<String, Writer> appendToFile_writers = new HashMap();

static void appendToFile(String path, String s) { try {
  Lock __0 = appendToFile_lock; lock(__0); try { // Let's just generally synchronize this to be safe.
  mkdirsForFile(new File(path));
  path = getCanonicalPath(path);
  Writer writer = appendToFile_writers.get(path);
  if (writer == null) {
    //print("[Logging to " + path + "]");
    writer = new BufferedWriter(new OutputStreamWriter(
      newFileOutputStream(path, true), "UTF-8"));
    if (appendToFile_keepOpen)
      appendToFile_writers.put(path, writer);
  }
  writer.write(s);
  if (!appendToFile_keepOpen)
    writer.close();
} finally { unlock(__0); } } catch (Exception __e) { throw rethrow(__e); } }
  
static void appendToFile(File path, String s) {
  if (path != null)
    appendToFile(path.getPath(), s);
}

static void cleanMeUp_appendToFile() {
   AutoCloseable __3 = tempCleaningUp(); try {
  Lock __1 = appendToFile_lock; lock(__1); try {
  closeAllWriters(values(appendToFile_writers));
  appendToFile_writers.clear();
} finally { unlock(__1); } } finally { _close(__3); }}


static File programFile(String name) {
  return prepareProgramFile(name);
}

static File programFile(String progID, String name) {
  return prepareProgramFile(progID, name);
}




static SimpleDateFormat simpleDateFormat_UTC(String format) {
  SimpleDateFormat sdf = new SimpleDateFormat(format);
  sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
  return sdf;
}




static String formatUTCWithMS_24() { return formatUTCWithMS_24(now()); }
static String formatUTCWithMS_24(long time) {
  var sdf = new SimpleDateFormat("HH:mm:ss.SSSS");
  sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
  return sdf.format(time) + " UTC";
}


static boolean initSyntaxTextArea_numPadFix = true;

static void initSyntaxTextArea(RSyntaxTextArea textArea) { swing(() -> { 
  if (initSyntaxTextArea_numPadFix)
    new NumPadFixingInputMap().replaceOn(textArea);
  textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
  textArea.setHighlightCurrentLine(false);
  textArea.setTabSize(2);
  textArea.setTabsEmulated(true);
  textArea.setCodeFoldingEnabled(false);
  textArea.setMarkOccurrences(true);
}); }


static List<Integer> countIteratorAsList(int b) { return countIteratorAsList(0, b); }
static List<Integer> countIteratorAsList(int a, int b) {
  return countIteratorToList(a, b);
}







static <A> List<A> countIteratorAsList(int b, IF1<Integer, A> f) { return countIteratorAsList(0, b, f); }
static <A> List<A> countIteratorAsList(int a, int b, IF1<Integer, A> f) {
  return countIteratorToList(a, b, f);
}





static List<Integer> countIteratorAsList(int a, int b, int step) {
  return countIteratorToList(a, b, step);
}





static <A> List<A> countIteratorAsList(double a, double b, double step, IF1<Double, A> f) {
  return countIteratorToList(a, b, step, f);
}





static <A> List<Double> countIteratorAsList(double a, double b, double step) {
  return countIteratorToList(a, b, step);
}





static <A> List<A> countIteratorAsList(IF1<Double, A> f, double a, double b, double step) {
  return countIteratorToList(f, a, b, step);
}



static <A> List<A> countIteratorAsList(IF1<Integer, A> f, int b) { return countIteratorAsList(f, 0, b); }
static <A> List<A> countIteratorAsList(IF1<Integer, A> f, int a, int b) {
  return countIteratorToList(f, a, b);
}


static <A> List<A> nonNulls(Iterable<A> l) {
  return withoutNulls(l);
}

static <A> List<A> nonNulls(A[] l) {
  return withoutNulls(l);
}

static <A, B> Map<A, B> nonNulls(Map<A, B> map) {
  return withoutNulls(map);
}


static IterableIterator<Integer> countIterator(int b) { return countIterator(0, b); }
static IterableIterator<Integer> countIterator(int a, int b) {
  return countIterator_exclusive(a, b);
}



static <A> IterableIterator<A> countIterator(int b, IF1<Integer, A> f) { return countIterator(0, b, f); }
static <A> IterableIterator<A> countIterator(int a, int b, IF1<Integer, A> f) {
  return countIterator_exclusive(a, b, f);
}

static IterableIterator<Integer> countIterator(int a, int b, int step) {
  return countIterator_exclusive_step(a, b, step);
}

static <A> IterableIterator<A> countIterator(double a, double b, double step, IF1<Double, A> f) {
  return countIterator_exclusive_step(a, b, step, f);
}

static <A> IterableIterator<Double> countIterator(double a, double b, double step) {
  return countIterator_exclusive_step(a, b, step);
}

static <A> IterableIterator<A> countIterator(IF1<Double, A> f, double a, double b, double step) {
  return countIterator(a, b, step, f);
}

static <A> IterableIterator<A> countIterator(IF1<Integer, A> f, int b) { return countIterator(f, 0, b); }
static <A> IterableIterator<A> countIterator(IF1<Integer, A> f, int a, int b) {
  return countIterator_exclusive(a, b, f);
}


static void centerWindowWithin(Window w, Rect r) {
  if (w != null) { swing(() -> { 
    w.setLocation(
      r.x+(r.w-w.getWidth())/2,
      r.y+(r.h-w.getHeight())/2);
  }); }
}


static int screenNrOfWindow(Window w) {
  if (w == null) return -1;
  Rect bounds = boundsAsRect(w);
  return indexOfMax(allScreenBounds(),
    screen -> area(intersectRects(bounds, screen)));
}


static boolean isEditableComboBox(final JComboBox cb) {
  return cb != null && swing(new F0<Boolean>() { public Boolean get() { try {  return cb.isEditable();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return cb.isEditable();"; }});
}


static <A, B> List<B> lookupAllOpt(Map<A, B> map, Collection<A> l) {
  List<B> out = new ArrayList();
  if (l != null) for (A a : l)
    addIfNotNull(out, map.get(a));
  return out; 
}

static <A, B> List<B> lookupAllOpt(Collection<A> l, Map<A, B> map) {
  return lookupAllOpt(map, l);
}


static <A, B extends IF0<A>> List<A> getVars(Iterable<B> l) {
  return lambdaMap(__60 -> getVar(__60), l);
}


static <A, B> NavigableMap<A, B> synchroNavigableMap(NavigableMap<A, B> map) {
  
  
    return Collections.synchronizedNavigableMap(map);
  
}


static Object defaultDefaultClassFinder() {
  return new F1<String, Class>() {
    public Class get(String name) {
      // Fix some buggy concepts files out there
      name = replacePrefix("main$main$", "main$", name);
      
      Class c = get2(name);
      
      return c;
    }
      
    Class get2(String name) {
      // special invocation to find main class irrelevant of name
      if (eq(name, "<main>")) return mc();
      
      { Class c = findClass_fullName(name); if (c != null) return c; }
      
      if (startsWithAny(name, "loadableUtils.utils$", "main$", mcDollar()))
        for (String pkg : ll("loadableUtils.utils$", mcDollar())) {
          String newName = pkg + afterDollar(name);
          
          { Class c = findClass_fullName(newName); if (c != null) return c; }
        }
      return null;
    }
  };
}


// TODO: if field is a Ref<>, you can thoretically use findBackRefs

static <A extends Concept> A findConceptWhere(Class<A> c, Object... params) {
  return findConceptWhere(db_mainConcepts(), c, params);
}

static <A extends Concept> A findConceptWhere(Concepts concepts, Class<A> c, Object... params) {
  ping();
  params = expandParams(c, params);
  
  // indexed
  if (concepts.fieldIndices != null)
    for (int i = 0; i < l(params); i += 2) {
      IFieldIndex<A, Object> index = concepts.getFieldIndex(c, (String) params[i]);
      if (index != null) {
        for (A x : index.getAll(params[i+1]))
          if (checkConceptFields(x, params)) return x;
        return null;
      }
    }
    
  // table scan
  for (A x : concepts.list(c)) if (checkConceptFields(x, params)) return x;
  return null;
}

static Concept findConceptWhere(Concepts concepts, String c, Object... params) {
  for (Concept x : concepts.list(c)) if (checkConceptFields(x, params)) return x;
  return null;
}



static String programIDWithCase() {
  return nempty(caseID())
    ? programID() + "/" + quoteUnlessIdentifierOrInteger(caseID())
    : programID();
}


static void readLocally(String progID, String varNames) {
  readLocally2(mc(), progID, varNames);
}

static void readLocally(String varNames) {
  readLocally2(mc(), programID(), varNames);
}

static void readLocally2(Object obj, String varNames) {
  readLocally2(obj, programID(), varNames);
}

static int readLocally_stringLength;

static ThreadLocal<Boolean> readLocally2_allDynamic = new ThreadLocal();
static ThreadLocal readLocally2_classFinder = new ThreadLocal();

// read a string variable from standard storage
// does not overwrite variable contents if there is no file
static void readLocally2(Object obj, String progID, String varNames) { try {
  boolean allDynamic = isTrue(getAndClearThreadLocal(readLocally2_allDynamic));
  for (String variableName : javaTokC(varNames)) {
    File textFile = new File(programDir(progID), variableName + ".text");
    
    String value = loadTextFile(textFile);
    if (value != null)
      set(main.class, variableName, value);
    else {
      File structureFile = new File(programDir(progID), variableName + ".structure");
      value = loadTextFile(structureFile);
      
      if (value == null) {
        File structureGZFile = new File(programDir(progID), variableName + ".structure.gz");
        if (!structureGZFile.isFile()) return;
        //value = loadGZTextFile(structureGZFile);
         InputStream fis = new FileInputStream(structureGZFile); try {
        GZIPInputStream gis = newGZIPInputStream(fis);
        InputStreamReader reader = new InputStreamReader(gis, "UTF-8");
        BufferedReader bufferedReader = new BufferedReader(reader);
        //O o = unstructure_reader(bufferedReader);
        Object o = unstructure_tok(javaTokC_noMLS_onReader(bufferedReader), allDynamic, readLocally2_classFinder.get());
        readLocally_set(obj, variableName, o);
        return;
      } finally { _close(fis); }}
      
      readLocally_stringLength = l(value);
      if (nempty(value))
        readLocally_set(obj, variableName, unstructure(value, allDynamic, readLocally2_classFinder.get()));
    }
  }
} catch (Exception __e) { throw rethrow(__e); } }

static void readLocally_set(Object c, String varName, Object value) {
  Object oldValue = get(c, varName);
  if (oldValue instanceof List && !(oldValue instanceof ArrayList) && value != null) {
    // Assume it's a synchroList.
    value = synchroList((List) value);
  }
  set(c, varName, value);
}



static boolean cic(Collection<String> l, String s) {
  return containsIgnoreCase(l, s);
}


static boolean cic(Collection<Symbol> l, Symbol s) {
  return contains(l, s);
}


static boolean cic(String[] l, String s) {
  return containsIgnoreCase(l, s);
}

static boolean cic(String s, char c) {
  return containsIgnoreCase(s, c);
}

static boolean cic(String a, String b) {
  return containsIgnoreCase(a, b);
}



static String getInnerMessage(Throwable e) {
  if (e == null) return null;
  return getInnerException(e).getMessage();
}


static BufferedReader utf8BufferedReader(InputStream in) {
  return utf8bufferedReader(in);
}

static BufferedReader utf8BufferedReader(File f) {
  return utf8bufferedReader(f);
}


static Producer<String> javaTokC_noMLS_onReader(final BufferedReader r) {
  final class X implements Producer<String> {
    StringBuilder buf = new StringBuilder(); // stores from "i"
    char c, d, e = 'x'; // just not '\0'
    
    X() {
      // fill c, d and e
      nc();
      nc();
      nc();
    }
    
    // get next character(s) into c, d and e
    void nc() { try {
      c = d;
      d = e;
      if (e == '\0') return;
      int i = r.read();
      e = i < 0 ? '\0'
        : i == '\0' ? '_' // shouldn't happen anymore
        : (char) i;
    } catch (Exception __e) { throw rethrow(__e); } }
    
    void ncSave() {
      if (c != '\0') {
        buf.append(c);
        nc();
      }
    }
    
    public String next() {
      // scan for whitespace
      while (c != '\0') {
        if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
          nc();
        else if (c == '/' && d == '*') {
          do nc(); while (c != '\0' && !(c == '*' && d == '/'));
          nc(); nc();
        } else if (c == '/' && d == '/') {
          do nc(); while (c != '\0' && "\r\n".indexOf(c) < 0);
        } else
          break;
      }
      
      if (c == '\0') return null;

      // scan for non-whitespace
      if (c == '\'' || c == '"') {
        char opener = c;
        ncSave();
        while (c != '\0') {
          if (c == opener || c == '\n') { // end at \n to not propagate unclosed string literal errors
            ncSave();
            break;
          } else if (c == '\\') {
            ncSave();
            ncSave();
          } else
            ncSave();
        }
      } else if (Character.isJavaIdentifierStart(c))
        do ncSave(); while (Character.isJavaIdentifierPart(c) || c == '\''); // for stuff like "don't"
      else if (Character.isDigit(c)) {
        do ncSave(); while (Character.isDigit(c));
        if (c == 'L') ncSave(); // Long constants like 1L
      } else
        ncSave();
        
      String t = buf.toString();
      buf.setLength(0);
      return t;
    }
  }
  
  return new X();
}



static boolean isString(Object o) {
  return o instanceof String;
}


static Producer<String> javaTokC_noMLS_iterator(final String s) {
  return javaTokC_noMLS_iterator(s, 0);
}

static Producer<String> javaTokC_noMLS_iterator(final String s, final int startIndex) {
  return new Producer<String>() {
    final int l = s.length();
    int i = startIndex;
    
    public String next() {
      if (i >= l) return null;
      
      int j = i;
      char c, d;
      
      // scan for whitespace
      while (j < l) {
        c = s.charAt(j);
        d = j+1 >= l ? '\0' : s.charAt(j+1);
        if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
          ++j;
        else if (c == '/' && d == '*') {
          do ++j; while (j < l && !s.substring(j, Math.min(j+2, l)).equals("*/"));
          j = Math.min(j+2, l);
        } else if (c == '/' && d == '/') {
          do ++j; while (j < l && "\r\n".indexOf(s.charAt(j)) < 0);
        } else
          break;
      }
      
      i = j;
      if (i >= l) return null;
      c = s.charAt(i);
      d = i+1 >= l ? '\0' : s.charAt(i+1);
  
      // scan for non-whitespace
      if (c == '\'' || c == '"') {
        char opener = c;
        ++j;
        while (j < l) {
          if (s.charAt(j) == opener || s.charAt(j) == '\n') { // end at \n to not propagate unclosed string literal errors
            ++j;
            break;
          } else if (s.charAt(j) == '\\' && j+1 < l)
            j += 2;
          else
            ++j;
        }
      } else if (Character.isJavaIdentifierStart(c))
        do ++j; while (j < l && Character.isJavaIdentifierPart(s.charAt(j)));
      else if (Character.isDigit(c)) {
        do ++j; while (j < l && Character.isDigit(s.charAt(j)));
        if (j < l && s.charAt(j) == 'L') ++j; // Long constants like 1L
      } else
        ++j;
        
      String t = quickSubstring(s, i, j);
      i = j;
      return t;
    }
  };
}



static String actualMCDollar() {
  return actualMC().getName() + "$";
}


static BigInteger parseBigInt(String s) {
  return new BigInteger(s);
}


static HashMap<String, Class> findClass_fullName_cache = new HashMap();

// returns null on not found
// this is the simple version that is not case-tolerant
static Class findClass_fullName(String name) {
  synchronized(findClass_fullName_cache) {
    if (findClass_fullName_cache.containsKey(name))
      return findClass_fullName_cache.get(name);
      
    Class c;
    try {
      c = Class.forName(name);
    } catch (ClassNotFoundException e) {
      c = null;
    }
    findClass_fullName_cache.put(name, c);
    return c;
  }
}


static String unquoteUsingCharArray(String s, char[] buf) {
  if (s == null) return null;
  if (startsWith(s, '[')) {
    int i = 1;
    while (i < s.length() && s.charAt(i) == '=') ++i;
    if (i < s.length() && s.charAt(i) == '[') {
      String m = s.substring(1, i);
      if (s.endsWith("]" + m + "]"))
        return s.substring(i+1, s.length()-i-1);
    }
  }
  
  if (s.length() > 1) {
    char c = s.charAt(0);
    if (c == '\"' || c == '\'') {
      int l = endsWith(s, c) ? s.length()-1 : s.length();
      if (l > buf.length) return unquote(s); // fallback
      int n = 0;
  
      for (int i = 1; i < l; i++) {
        char ch = s.charAt(i);
        if (ch == '\\') {
          char nextChar = (i == l - 1) ? '\\' : s.charAt(i + 1);
          // Octal escape?
          if (nextChar >= '0' && nextChar <= '7') {
              String code = "" + nextChar;
              i++;
              if ((i < l - 1) && s.charAt(i + 1) >= '0'
                      && s.charAt(i + 1) <= '7') {
                  code += s.charAt(i + 1);
                  i++;
                  if ((i < l - 1) && s.charAt(i + 1) >= '0'
                          && s.charAt(i + 1) <= '7') {
                      code += s.charAt(i + 1);
                      i++;
                  }
              }
              buf[n++] = (char) Integer.parseInt(code, 8);
              continue;
          }
          switch (nextChar) {
          case '\"': ch = '\"'; break;
          case '\\': ch = '\\'; break;
          case 'b': ch = '\b'; break;
          case 'f': ch = '\f'; break;
          case 'n': ch = '\n'; break;
          case 'r': ch = '\r'; break;
          case 't': ch = '\t'; break;
          case '\'': ch = '\''; break;
          // Hex Unicode: u????
          case 'u':
              if (i >= l - 5) {
                  ch = 'u';
                  break;
              }
              int code = Integer.parseInt(
                      "" + s.charAt(i + 2) + s.charAt(i + 3)
                         + s.charAt(i + 4) + s.charAt(i + 5), 16);
              char[] x = Character.toChars(code);
              int lx = x.length;
              for (int j = 0; j < lx; j++)
                buf[n++] = x[j];
              i += 5;
              continue;
          default:
            ch = nextChar; // added by Stefan
          }
          i++;
        }
        buf[n++] = ch;
      }
      return new String(buf, 0, n);
    }
  }
    
  return s; // not quoted - return original
}


static boolean structure_isMarker(String s, int i, int j) {
  if (i >= j) return false;
  if (s.charAt(i) != 'm') return false;
  ++i;
  while (i < j) {
    char c = s.charAt(i);
    if (c < '0' || c > '9') return false;
    ++i;
  }
  return true;
}


static String internIfLongerThan(String s, int l) {
  return s == null ? null : l(s) >= l ? intern(s) : s;
}


static char unquoteCharacter(String s) {
  assertTrue(s.startsWith("'") && s.length() > 1);
  return unquote("\"" + s.substring(1, s.endsWith("'") ? s.length()-1 : s.length()) + "\"").charAt(0);
}


static boolean isLongConstant(String s) {
  if (!s.endsWith("L")) return false;
  s = s.substring(0, l(s)-1);
  return isInteger(s);
}


static <A> TreeMap<String, A> ciMap() {
  return caseInsensitiveMap();
}


static List parseList(String s) {
  return (List) safeUnstructure(s);
}


static <A> List<A> synchroLinkedList() {
  return synchroList(new LinkedList<A>());
}



static <A, B> SortedMap<A, B> synchroSortedMap(SortedMap<A, B> map) {
  
  
    return Collections.synchronizedSortedMap(map);
  
}


static boolean[] boolArrayFromBytes(byte[] a, int n) {
  boolean[] b = new boolean[n];
  int m = min(n, l(a)*8);
  for (int i = 0; i < m; i++)
    b[i] = (a[i/8] & 1 << (i & 7)) != 0;
  return b;
}


static <A> Constructor nuStubInnerObject_findConstructor(Class<A> c) { return nuStubInnerObject_findConstructor(c, null); }
static <A> Constructor nuStubInnerObject_findConstructor(Class<A> c, Object classFinder) { try {
  Class outerType = getOuterClass(c, classFinder);
  Constructor m = c.getDeclaredConstructor(outerType);
  makeAccessible(m);
  return m;
} catch (Exception __e) { throw rethrow(__e); } }


static Map<Class, Constructor> nuEmptyObject_cache = newDangerousWeakHashMap();

static <A> A nuEmptyObject(Class<A> c) { try {
  Constructor ctr;
  
  synchronized(nuEmptyObject_cache) {
    ctr = nuEmptyObject_cache.get(c);
    if (ctr == null) {
      nuEmptyObject_cache.put(c, ctr = nuEmptyObject_findConstructor(c));
      makeAccessible(ctr);
    }
  }

  try {
    return (A) ctr.newInstance();
  } catch (InstantiationException e) {
    if (empty(e.getMessage()))
      if ((c.getModifiers() & Modifier.ABSTRACT) != 0)
        throw fail("Can't instantiate abstract class " + className(c), e);
      else
        throw fail("Can't instantiate " + className(c), e);  
    else throw rethrow(e);
  }
} catch (Exception __e) { throw rethrow(__e); } }

static Constructor nuEmptyObject_findConstructor(Class c) {
  for (Constructor m : c.getDeclaredConstructors())
    if (m.getParameterTypes().length == 0)
      return m;
  throw fail("No default constructor declared in " + c.getName());
}



static void setOptAllDyn_pcall(DynamicObject o, Map<String, Object> fields) {
  if (fields == null || o == null) return;
  HashMap<String, Field> fieldMap = instanceFieldsMap(o);
  for (Map.Entry<String, Object> e : fields.entrySet()) { try {
    String field = e.getKey();
    Object val = e.getValue();
    Field f = fieldMap.get(field);
    if (f != null)
      smartSet(f, o, val);
    else {
      dynamicObject_setRawFieldValue(o, intern(field), val);
      
    }
  } catch (Throwable __e) { printStackTrace(__e); }}
}


static void setOptAll_pcall(Object o, Map<String, Object> fields) {
  if (fields == null) return;
  for (String field : keys(fields))
    try { setOpt(o, field, fields.get(field)); } catch (Throwable __e) { print(exceptionToStringShort(__e)); }
}

static void setOptAll_pcall(Object o, Object... values) {
  //values = expandParams(c.getClass(), values);
  warnIfOddCount(values);
  for (int i = 0; i+1 < l(values); i += 2) {
    String field = (String) values[i];
    Object value = values[i+1];
    try { setOpt(o, field, value); } catch (Throwable __e) { print(exceptionToStringShort(__e)); }
  }
}


static void fixOuterRefs(Object o) { try {
  if (o == null) return;
  Field[] l = thisDollarOneFields(o.getClass());
  if (l.length <= 1) return;
  Object father = null;
  for (Field f : l) {
    father = f.get(o);
    if (father != null) break;
  }
  if (father == null) return;
  for (Field f : l)
    f.set(o, father);
} catch (Exception __e) { throw rethrow(__e); } }


static String intern(String s) {
  return fastIntern(s);
}


static void pcallOpt_noArgs(Object o, String method) {
  try { callOpt_noArgs(o, method); } catch (Throwable __e) { printStackTrace(__e); }
}


  static RuntimeException todo() {
    throw new RuntimeException("TODO");
  }
  
  static RuntimeException todo(Object msg) {
    throw new RuntimeException("TODO: " + msg);
  }


static Object newMultiDimensionalOuterArray(Class elementType, int dimensions, int length) {
  int[] dims = new int[dimensions];
  dims[0] = length;
  return Array.newInstance(elementType, dims);
}


static int[] toIntArray(Collection<Integer> l) {
  int[] a = new int[l(l)];
  int i = 0;
  if (a.length != 0) for (int x : l)
    a[i++] = x;
  return a;
}


static double[] toDoubleArray(Collection<Double> l) {
  double[] a = new double[l(l)];
  int i = 0;
  if (a.length != 0) for (double x : l)
    a[i++] = x;
  return a;
}




static TreeSet<String> ciSet() {
  return caseInsensitiveSet();
}


// DIFFERENCES to jfind: always ignores case, doesn't recognize <id> etc
// You probably want jmatch2

static boolean jmatch(String pat, String s) {
  return jmatch(pat, s, null);
}

static boolean jmatch(String pat, String s, Matches matches) {
  if (s == null) return false;
  return jmatch(pat, javaTok(s), matches);
}

static boolean jmatch(String pat, List<String> toks) {
  return jmatch(pat, toks, null);
}

static boolean jmatch(String pat, List<String> toks, Matches matches) {
  List<String> tokpat = javaTok(pat);
  String[] m = match2(tokpat, toks);
  //print(structure(tokpat) + " on " + structure(toks) + " => " + structure(m));
  if (m == null)
    return false;
  else {
    if (matches != null) matches.m = m;
    return true;
  }
}


static List callF_all(Collection l, Object... args) {
  return map(l, f -> callF(f, args));
}


static File tempFileFor(File f) {
  return new File(f.getPath() + "_temp");
}


static double toM_double(long l) {
  return l/(1024*1024.0);
}


public static String rtrimSpaces(String s) {
  if (s == null) return null;
  int i = s.length();
  while (i > 0 && " \t".indexOf(s.charAt(i-1)) >= 0)
    --i;
  return i < s.length() ? s.substring(0, i) : s;
}


static int year() {
  return localYear();
}

static int year(long now) {
  return localYear(now);
}

static int year(long now, TimeZone tz) {
  return parseInt(simpleDateFormat("y", tz).format(now));
}


static int month() {
  return localMonth();
}

static int month(long now) {
  return localMonth(now);
}

static int month(long now, TimeZone tz) {
  return parseInt(simpleDateFormat("M", tz).format(now));
}


static int dayOfMonth() {
  return localDayOfMonth();
}

static int dayOfMonth(long now) {
  return localDayOfMonth(now);
}

static int dayOfMonth(long now, TimeZone tz) {
  return parseInt(simpleDateFormat("d", tz).format(now));
}


static String padLeft(String s, char c, int n) {
  return rep(c, n-l(s)) + s;
}

// default to space
static String padLeft(String s, int n) {
  return padLeft(s, ' ', n);
}


static java.util.Calendar calendarFromTime(long time, TimeZone tz) {
  java.util.Calendar c = java.util.Calendar.getInstance(tz);
  c.setTimeInMillis(time);
  return c;
}

static java.util.Calendar calendarFromTime(long time) {
  java.util.Calendar c = java.util.Calendar.getInstance();
  c.setTimeInMillis(time);
  return c;
}


static Map<String, String> singular_specials = litmap(
  "children", "child", "images", "image", "chess", "chess");
  
static Set<String> singular_specials2 = litciset("time", "machine", "line", "rule");

static String singular(String s) {
  if (s == null) return null;
  { String __1 = singular_specials.get(s); if (!empty(__1)) return __1; }
  //try answer hippoSingulars().get(lower(s));
  if (singular_specials2.contains(dropSuffix("s", afterLastSpace(s))))
    return dropSuffix("s", s);
  if (s.endsWith("ness")) return s;
  if (s.endsWith("ges")) return dropSuffix("s", s);
  if (endsWith(s, "bases")) return dropLast(s);
  s = dropSuffix("es", s);
  s = dropSuffix("s", s);
  return s;
}


static Set<String> getPlural_specials = litciset("sheep", "fish");

static String getPlural(String s) {
  if (contains(getPlural_specials, s)) return s;
  if (ewic(s, "y")) return dropSuffixIgnoreCase("y", s) + "ies";
  if (ewicOneOf(s, "ss", "ch")) return s + "es";
  if (ewic(s, "s")) return s;
  return s + "s";
}


static List<String> javaTokC(String s) {
  if (s == null) return null;
  int l = s.length();
  ArrayList<String> tok = new ArrayList();
  
  int i = 0;
  while (i < l) {
    int j = i;
    char c, d;
    
    // scan for whitespace
    while (j < l) {
      c = s.charAt(j);
      d = j+1 >= l ? '\0' : s.charAt(j+1);
      if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
        ++j;
      else if (c == '/' && d == '*') {
        do ++j; while (j < l && !s.substring(j, Math.min(j+2, l)).equals("*/"));
        j = Math.min(j+2, l);
      } else if (c == '/' && d == '/') {
        do ++j; while (j < l && "\r\n".indexOf(s.charAt(j)) < 0);
      } else
        break;
    }
    
    i = j;
    if (i >= l) break;
    c = s.charAt(i);
    d = i+1 >= l ? '\0' : s.charAt(i+1);

    // scan for non-whitespace
    if (c == '\'' || c == '"') {
      char opener = c;
      ++j;
      while (j < l) {
        if (s.charAt(j) == opener || s.charAt(j) == '\n') { // end at \n to not propagate unclosed string literal errors
          ++j;
          break;
        } else if (s.charAt(j) == '\\' && j+1 < l)
          j += 2;
        else
          ++j;
      }
    } else if (Character.isJavaIdentifierStart(c))
      do ++j; while (j < l && (Character.isJavaIdentifierPart(s.charAt(j)) || "'".indexOf(s.charAt(j)) >= 0)); // for stuff like "don't"
    else if (Character.isDigit(c)) {
      do ++j; while (j < l && Character.isDigit(s.charAt(j)));
      if (j < l && s.charAt(j) == 'L') ++j; // Long constants like 1L
    } else if (c == '[' && d == '[') {
      do ++j; while (j+1 < l && !s.substring(j, j+2).equals("]]"));
      j = Math.min(j+2, l);
    } else if (c == '[' && d == '=' && i+2 < l && s.charAt(i+2) == '[') {
      do ++j; while (j+2 < l && !s.substring(j, j+3).equals("]=]"));
      j = Math.min(j+3, l);
    } else
      ++j;
      
    tok.add(javaTok_substringC(s, i, j));
    i = j;
  }
  
  return tok;
}


static boolean isSyntheticOrAnonymous(Class c) {
  return c != null && (c.isSynthetic() || isAnonymousClassName(c.getName()));
}


// keeps package names for dynamic code (package dyn.*)
static String shortDynClassNameForStructure(Object o) {
   if (o instanceof DynamicObject && ((DynamicObject) o).className != null)
    return ((DynamicObject) o).className;
  if (o == null) return null;
  Class c = o instanceof Class ? (Class) o : o.getClass();
  String name = c.getName();
  return name.startsWith("dyn.") ? classNameToVM(name) : shortenClassName(name);
}


static boolean isPersistableClass(Class c) {
  String name = c.getName();
  if (isAnonymousClassName(name)) return false;
  if (isBoxedType(c)) return true;
  if (isArrayType(c)) return true;
  if (c == Class.class || c == String.class || c == File.class || c == Color.class) return true;
  if (name.startsWith("java.util.Collections$Synchronized")) return true;
  
  if (hasThisDollarFields(c))
    return hasSingleArgumentConstructor(c);
  else
    return getDefaultConstructor(c) != null;
}


static int countDots(String s) {
  int n = l(s), count = 0;
  for (int i = 0; i < n; i++) if (s.charAt(i) == '.') ++count;
  return count;
}


static void quoteToPrintWriter(String s, PrintWriter out) {
  if (s == null) { out.print("null"); return; }
  out.print('"');
  int l = s.length();
  for (int i = 0; i < l; i++) {
    char c = s.charAt(i);
    if (c == '\\' || c == '"') {
      out.print('\\'); out.print(c);
    } else if (c == '\r')
      out.print("\\r");
    else if (c == '\n')
      out.print("\\n");
    else if (c == '\0')
      out.print("\\0");
    else
      out.print(c);
  }
  out.print('"');
}


static String quoteCharacter(char c) {
  if (c == '\'') return "'\\''";
  if (c == '\\') return "'\\\\'";
  if (c == '\r') return "'\\r'";
  if (c == '\n') return "'\\n'";
  if (c == '\t') return "'\\t'";
  return "'" + c + "'";
}



static boolean isCISet_gen(Iterable<String> l) {
  return l instanceof TreeSet && className(((TreeSet) l).comparator()).contains("CIComp");
}


static boolean isJavaXClassName(String s) {
  return startsWithOneOf(s, "main$", "loadableUtils.");
}


static <A> List<A> unwrapSynchronizedList(List<A> l) {
  
  if (eqOneOf(className(l),
    "java.util.Collections$SynchronizedList",
    "java.util.Collections$SynchronizedRandomAccessList"))
    return (List) get_raw(l, "list");
  return l;
}


static boolean isCIMap_gen(Map map) {
  return map instanceof TreeMap && className(((TreeMap) map).comparator()).contains("CIComp");
}


// works for both java.util-wrapped maps as well as our own
static <A, B> Map<A, B> unwrapSynchronizedMap(Map<A, B> map) {
  if (eqOneOf(shortClassName(map),
    "SynchronizedMap",
    "SynchronizedSortedMap",
    "SynchronizedNavigableMap"))
    return (Map) get_raw(map, "m");
  return map;
}


static String boolArrayToHex(boolean[] a) {
  return bytesToHex(boolArrayToBytes(a));
}


static Pair<Class, Integer> arrayTypeAndDimensions(Object o) {
  return arrayTypeAndDimensions(_getClass(o));
}

static Pair<Class, Integer> arrayTypeAndDimensions(Class c) {
  if (c == null || !c.isArray()) return null;
  Class elem = c.getComponentType();
  if (elem.isArray())
    return mapPairB(arrayTypeAndDimensions(elem), dim -> dim+1);
  return pair(elem, 1);
}


static int stdcompare(Number a, Number b) {
  return cmp(a, b);
}

static int stdcompare(String a, String b) {
  return cmp(a, b);
}

static int stdcompare(long a, long b) {
  return a < b ? -1 : a > b ? 1 : 0;
}

static int stdcompare(Object a, Object b) {
  return cmp(a, b);
}



static Map<Class, Field[]> getDeclaredFields_cache = newDangerousWeakHashMap();

static Field[] getDeclaredFields_cached(Class c) {
  Field[] fields;
  synchronized(getDeclaredFields_cache) {
    fields = getDeclaredFields_cache.get(c);
    if (fields == null) {
      getDeclaredFields_cache.put(c, fields = c.getDeclaredFields());
      for (Field f : fields)
        makeAccessible(f);
    }
  }
  return fields;
}


static Method findInstanceMethod(Class c, String method, Object... args) {
  while (c != null) {
    for (Method m : c.getDeclaredMethods())
      if (m.getName().equals(method) && findMethod_checkArgs(m, args, false))
        return m;
    c = c.getSuperclass();
  }
  return null;
}



static Set<Field> fieldObjectsInFieldOrder(Class c, Set<Field> fields) {
  try {
    var byName = mapToKey(f -> f.getName(), fields);
    LinkedHashSet<Field> out = new LinkedHashSet();
    for (String name : unnullForIteration(getFieldOrder(c))) {
      Field f = byName.get(name);
      if (f != null) {
        byName.remove(name);
        out.add(f);
      }
    }
    addAll(out, fields);
    return out;
  } catch (Throwable __0) { printStackTrace(__0);
    return fields;
  }
}


static String defaultTimerName_name;

static String defaultTimerName() {
  if (defaultTimerName_name == null)
    defaultTimerName_name = "A timer by " + programID();
  return defaultTimerName_name;
}


// r may return false to cancel timer
static TimerTask smartTimerTask(Object r, java.util.Timer timer, long delay) {
  return new SmartTimerTask(r, timer, delay, _threadInfo());
}

static class SmartTimerTask extends TimerTask implements IFieldsToList{
  Object r;
  java.util.Timer timer;
  long delay;
  Object threadInfo;
  SmartTimerTask() {}
  SmartTimerTask(Object r, java.util.Timer timer, long delay, Object threadInfo) {
  this.threadInfo = threadInfo;
  this.delay = delay;
  this.timer = timer;
  this.r = r;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + r + ", " + timer + ", " + delay + ", " + threadInfo + ")"; }public Object[] _fieldsToList() { return new Object[] {r, timer, delay, threadInfo}; }

  long lastRun;
  
  public void run() {
    if (!licensed())
      timer.cancel();
    else {
      _threadInheritInfo(threadInfo);
       AutoCloseable __1 = tempActivity(r); try {
      lastRun = fixTimestamp(lastRun);
      long now = now();
      if (now >= lastRun + delay*0.9) {
        lastRun = now;
        if (eq(false, pcallF(r)))
          timer.cancel();
      }
    } finally { _close(__1); }}
  }
}


static <A> boolean addToCollection(Collection<A> c, A a) {
  return c != null && c.add(a);
}


static String dynamicClassName(Object o) {
  if (o instanceof DynamicObject && ((DynamicObject) o).className != null)
    return "main$" + ((DynamicObject) o).className;
  return className(o);
}


// not a very good one
static boolean instanceOf(Object o, String className) {
  if (o == null) return false;
  String c = o.getClass().getName();
  return eq(c, className) || eq(c, "main$" + className);
}

// better
static boolean instanceOf(Object o, Class c) {
  if (c == null) return false;
  return c.isInstance(o);
}

static boolean instanceOf(Class c, Object o) {
  return instanceOf(o, c);
}


static HashMap<String, Class> findClass_cache = new HashMap();

// currently finds only inner classes of class "main"
// returns null on not found
// this is the simple version that is not case-tolerant
static Class findClass(String name) {
  synchronized(findClass_cache) {
    if (findClass_cache.containsKey(name))
      return findClass_cache.get(name);
      
    if (!isJavaIdentifier(name)) return null;
    
    Class c;
    try {
      c = Class.forName("main$" + name);
    } catch (ClassNotFoundException e) {
      c = null;
    }
    findClass_cache.put(name, c);
    return c;
  }
}


static void warnIfOddCount(Object... list) {
  if (odd(l(list)))
    printStackTrace("Odd list size: " + list);
}


// returns true if change
static boolean _csetField(Concept c, String field, Object value) { try {
  Field f = setOpt_findField(c.getClass(), field);
  //print("cset: " + c.id + " " + field + " " + struct(value) + " " + f);
  if (value instanceof RC) value = c._concepts.getConcept((RC) value);
  value = deref(value);
  
  if (value instanceof String && l((String) value) >= concepts_internStringsLongerThan) value = intern((String) value);
  
  if (f == null) {
    // dynamic field (undeclared)
    
    assertIdentifier(field);
    Object oldVal = mapGet(c.fieldValues, field);
    
    if (value instanceof Concept) {
      if (oldVal instanceof Concept.Ref)
        // change existing reference
        return ((Concept.Ref) oldVal).set((Concept) value);
      else {
        // overwrite non-reference value if any,
        // create new reference
        dynamicObject_setRawFieldValue(c, field, c.new Ref((Concept) value));
        c.change(); return true;
      }
    } else {
      // value is not a concept
      
      // if it was a reference, cleanly delete it
      if (oldVal instanceof Concept.Ref) ((Concept.Ref) oldVal).unindexAndDrop();
    
      if (eq(oldVal, value)) return false;
      
      if (isConceptList(value) && nempty(((List) value))) {
        // TODO: clean-up etc
        dynamicObject_setRawFieldValue(c, field, c.new RefL(((List) value)));
        c.change(); return true;
      }
      
      if (value == null) {
        // delete field
        dynamicObject_dropRawField(c, field);
      } else {
        // update or create field
        if (!isPersistable(value))
          throw fail("Can't persist: " + c + "." + field + " = "+ value);

        dynamicObject_setRawFieldValue(c, field, value);
      }
      c.change(); return true;
    }
  } else if (isSubtypeOf(f.getType(), Concept.Ref.class)) {
    // Concept.Ref magic
    ((Concept.Ref) f.get(c)).set((Concept) derefRef(value));
    c.change(); return true;
  } else if (isSubtypeOf(f.getType(), Concept.RefL.class)) {
    // Concept.RefL magic
    ((Concept.RefL) f.get(c)).replaceWithList(lmap(__61 -> derefRef(__61), (List) value));
    c.change(); return true;
  } else {
    Object old = f.get(c);
    if (neq(value, old)) {
      boolean isTransient = isTransient(f);
      if (!isTransient && !isPersistable(value))
        throw fail("Can't persist: " + c + "." + field + " = "+ value);
      f.set(c, value);
      if (!isTransient) c.change(); 
      return true;
    }
  }
  return false;
} catch (Exception __e) { throw rethrow(__e); } }


static Object derefRef(Object o) {
  if (o instanceof Concept.Ref) o = ((Concept.Ref) o).get();
  return o;
}

static <A extends Concept> A derefRef(Concept.Ref<A> r) {
  return r == null ? null : r.get();
}


static String loadTextFilePossiblyGZipped(String fileName) {
  return loadTextFilePossiblyGZipped(fileName, null);
}
  
static String loadTextFilePossiblyGZipped(String fileName, String defaultContents) {
  File gz = new File(fileName + ".gz");
  return gz.exists() ? loadGZTextFile(gz) : loadTextFile(fileName, defaultContents);
}

static String loadTextFilePossiblyGZipped(File fileName) {
  return loadTextFilePossiblyGZipped(fileName, null);
}

static String loadTextFilePossiblyGZipped(File fileName, String defaultContents) {
  return loadTextFilePossiblyGZipped(fileName.getPath(), defaultContents);
}



static void assertNotOnAWTThread() {
  assertFalse("Can't do this in AWT thread", isAWTThread());
}
  


static boolean isUnstructuring() {
  return isTrue(getTL(dynamicObjectIsLoading_threadLocal()));
}


static Concepts newConceptsWithClassFinder(String progID) {
  Concepts cc = new Concepts(progID);
  cc.classFinder = _defaultClassFinder();
  return cc;
}

static Concepts newConceptsWithClassFinder(File conceptsFile) {
  Concepts cc = new Concepts(assertNotNull(conceptsFile));
  cc.classFinder = _defaultClassFinder();
  return cc;
}

static Concepts newConceptsWithClassFinder(File conceptsFile, IF1<String, Class> classFinder) {
  Concepts cc = new Concepts(assertNotNull(conceptsFile));
  cc.classFinder = classFinder;
  return cc;
}


static String tag(String tag) {
  return htag(tag);
}

static String tag(String tag, Object contents, Object... params) {
  return htag(tag, str(contents), params);
}

static String tag(String tag, StringBuilder contents, Object... params) {
  return htag(tag, contents, params);
}

static String tag(String tag, StringBuffer contents, Object... params) {
  return htag(tag, contents, params);
}


// o is either a map already (string->object) or an arbitrary object,
// in which case its fields are converted into a map.
static Map<String, Object> objectToMap(Object o) { try {
  if (o instanceof Map) return (Map) o;
  
  TreeMap<String, Object> map = new TreeMap();
  Class c = o.getClass();
  while (c != Object.class) {
    Field[] fields = c.getDeclaredFields();
    for (final Field field : fields) {
      if ((field.getModifiers() & Modifier.STATIC) != 0)
        continue;
      field.setAccessible(true);
      final Object value = field.get(o);
      if (value != null)
        map.put(field.getName(), value);
    }
    c = c.getSuperclass();
  }
  
  // XXX NEW - hopefully this doesn't break anything
  if (o instanceof DynamicObject)
    putAll(map, ((DynamicObject) o).fieldValues);

  return map;
} catch (Exception __e) { throw rethrow(__e); } }

// same for a collection (convert each element)
static List<Map<String, Object>> objectToMap(Iterable l) {
  if (l == null) return null;
  List x = new ArrayList();
  for (Object o : l)
    x.add(objectToMap(o));
  return x;
}


static <A extends Concept> A registerConcept(A c) { return registerConcept(db_mainConcepts(), c); }
static <A extends Concept> A registerConcept(Concepts cc, A c) {
  { if (cc != null) cc.register(c); }
  return c;
}


static int identityHash(Object o) {
  return identityHashCode(o);
}


static double msToSeconds(long ms) {
  return toSeconds(ms);
}

static double msToSeconds(double ms) {
  return toSeconds(ms);
}


static long fileModificationTime(File f) {
  return f == null ? 0 : f.lastModified();
}


static void closeRandomAccessFile(RandomAccessFile f) {
  if (f != null) try {
    f.close();
    callJavaX("dropIO", f);
  } catch (Throwable e) {
    printStackTrace(e);
  }
}


static RandomAccessFile newRandomAccessFile(File path, String mode) { try {
  boolean forWrite = mode.indexOf('w') >= 0;
  if (forWrite) mkdirsForFile(path);
  RandomAccessFile f = new RandomAccessFile(path, mode);
  
  callJavaX("registerIO", f, path, forWrite);
  
  return f;
} catch (Exception __e) { throw rethrow(__e); } }


static <A> A vmBus_timerStarted(A timer) {
  vmBus_send("timerStarted", timer, costCenter());
  return timer;
}




static SimpleDateFormat simpleDateFormat_local(String format) {
  SimpleDateFormat sdf = new SimpleDateFormat(format);
  sdf.setTimeZone(localTimeZone());
  return sdf;
}


static double toMinutes(long ms) {
  return ms/60000.0;
}


static double toSeconds(long ms) {
  return ms/1000.0;
}

static String toSeconds(long ms, int digits) {
  return formatDouble(toSeconds(ms), digits);
}

static double toSeconds(double ms) {
  return ms/1000.0;
}

static String toSeconds(double ms, int digits) {
  return formatDouble(toSeconds(ms), digits);
}


static ChangeListener changeListener(final Object r) {
  return new ChangeListener() {
    public void stateChanged(ChangeEvent e) {
      pcallF(r);
    }
  };
}


static ItemListener itemListener(final Object r) {
  return new ItemListener() {
    public void itemStateChanged(ItemEvent e) {
      pcallF(r);
    }
  };
}


static void onUpdate(JComponent c, Runnable r) {
  onUpdate(c, (Object) r);
}

// legacy signature
static void onUpdate(JComponent c, Object r) {
  if (c instanceof JTextComponent)
    ((JTextComponent) c).getDocument().addDocumentListener(new DocumentListener() {
      public void insertUpdate(DocumentEvent e) {
        call(r);
      }
      public void removeUpdate(DocumentEvent e) {
        call(r);
      }
      public void changedUpdate(DocumentEvent e) {
        call(r);
      }
    });
  else if (c instanceof ItemSelectable) // JCheckBox and others
    ((ItemSelectable) c).addItemListener(new ItemListener() {
      public void itemStateChanged(ItemEvent e) {
        call(r);
      }
    });
  else
    print("Warning: onUpdate doesn't know " + getClassName(c));
}

static void onUpdate(List<? extends JComponent> l, Object r) {
  for (JComponent c : l)
    onUpdate(c, r);
}


static <A> A getSelectedItem_typed(JList<A> l) {
  return swing(() -> l.getSelectedValue());
}

static <A> A getSelectedItem_typed(JComboBox<A> cb) {
  return swing(() -> (A) cb.getSelectedItem());
}


static JTextField textFieldFromComboBox(JComboBox cb) {
  return (JTextField) cb.getEditor().getEditorComponent();
}


static JComboBox onSelectedItem(final JComboBox cb, final VF1<String> f) {
  addActionListener(cb, new Runnable() {  public void run() { try { 
    pcallF(f, selectedItem(cb))
  ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "pcallF(f, selectedItem(cb))"; }});
  return cb;
}

static JComboBox onSelectedItem(final JComboBox cb, IVF1<String> f) {
  addActionListener(cb, new Runnable() {  public void run() { try { 
    pcallF(f, selectedItem(cb))
  ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "pcallF(f, selectedItem(cb))"; }});
  return cb;
}


// f can return false to suppress regular printing
// call print_raw within f to actually print something
// f preferrably is F1<S, Bool>
static Object interceptPrintInThisThread(Object f) {
  Object old = print_byThread().get();
  print_byThread().set(f);
  return old;
}


static <A> OKOrError<A> OKOrError_ok(A a) {
  return new OKOrError(a);
}


static OKOrError OKOrError_error(Throwable e) {
  return new OKOrError(false, e);
}


static IF0 runnableToIF0(Runnable r) {
  return r == null ? null : () -> { r.run(); return null; };
}


static boolean regionMatches(String a, int offsetA, String b, int offsetB, int len) {
  return a != null && b != null && a.regionMatches(offsetA, b, offsetB, len);
}

static boolean regionMatches(String a, int offsetA, String b) {
  return regionMatches(a, offsetA, b, 0, l(b));
}


static String javaTok_substringN(String s, int i, int j) {
  if (i == j) return "";
  if (j == i+1 && s.charAt(i) == ' ') return " ";
  return s.substring(i, j);
}


static String javaTok_substringC(String s, int i, int j) {
  return s.substring(i, j);
}


static List<String> javaTokWithExisting(String s, List<String> existing) {
  ++javaTok_n;
  int nExisting = javaTok_opt && existing != null ? existing.size() : 0;
  ArrayList<String> tok = existing != null ? new ArrayList(nExisting) : new ArrayList();
  int l = s.length();
  
  int i = 0, n = 0;
  while (i < l) {
    int j = i;
    char c, d;
    
    // scan for whitespace
    while (j < l) {
      c = s.charAt(j);
      d = j+1 >= l ? '\0' : s.charAt(j+1);
      if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
        ++j;
      else if (c == '/' && d == '*') {
        do ++j; while (j < l && !s.substring(j, Math.min(j+2, l)).equals("*/"));
        j = Math.min(j+2, l);
      } else if (c == '/' && d == '/') {
        do ++j; while (j < l && "\r\n".indexOf(s.charAt(j)) < 0);
      } else
        break;
    }
    
    if (n < nExisting && javaTokWithExisting_isCopyable(existing.get(n), s, i, j))
      tok.add(existing.get(n));
    else
      tok.add(javaTok_substringN(s, i, j));
    ++n;
    i = j;
    if (i >= l) break;
    c = s.charAt(i);
    d = i+1 >= l ? '\0' : s.charAt(i+1);

    // scan for non-whitespace
    
    // Special JavaX syntax: 'identifier
    if (c == '\'' && Character.isJavaIdentifierStart(d) && i+2 < l && "'\\".indexOf(s.charAt(i+2)) < 0) {
      j += 2;
      while (j < l && Character.isJavaIdentifierPart(s.charAt(j)))
        ++j;
    } else if (c == '\'' || c == '"') {
      char opener = c;
      ++j;
      while (j < l) {
        if (s.charAt(j) == opener /*|| s.charAt(j) == '\n'*/) { // allow multi-line strings
          ++j;
          break;
        } else if (s.charAt(j) == '\\' && j+1 < l)
          j += 2;
        else
          ++j;
      }
    } else if (Character.isJavaIdentifierStart(c))
      do ++j; while (j < l && (Character.isJavaIdentifierPart(s.charAt(j)) || "'".indexOf(s.charAt(j)) >= 0)); // for stuff like "don't"
    else if (Character.isDigit(c)) {
      do ++j; while (j < l && Character.isDigit(s.charAt(j)));
      if (j < l && s.charAt(j) == 'L') ++j; // Long constants like 1L
    } else if (c == '[' && d == '[') {
      do ++j; while (j+1 < l && !s.substring(j, j+2).equals("]]"));
      j = Math.min(j+2, l);
    } else if (c == '[' && d == '=' && i+2 < l && s.charAt(i+2) == '[') {
      do ++j; while (j+2 < l && !s.substring(j, j+3).equals("]=]"));
      j = Math.min(j+3, l);
    } else
      ++j;
      
    if (n < nExisting && javaTokWithExisting_isCopyable(existing.get(n), s, i, j))
      tok.add(existing.get(n));
    else
      tok.add(javaTok_substringC(s, i, j));
    ++n;
    i = j;
  }
  
  if ((tok.size() % 2) == 0) tok.add("");
  javaTok_elements += tok.size();
  return tok;
}

static boolean javaTokWithExisting_isCopyable(String t, String s, int i, int j) {
  return t.length() == j-i
    && s.regionMatches(i, t, 0, j-i); // << could be left out, but that's brave
}


static <A, B> Map<A, List<B>> multiMapToMap(MultiMap<A, B> m) {
  return m == null ? null : m.data;
}


// Note: these first 3 syntaxes seem to be wrong
// (not failing on timeout)
static Object evalWithTimeoutOrException(final Object f, int timeoutMS) {
  return evalWithTimeoutOrException(timeoutMS, f);
}

static Object evalWithTimeoutOrException(int timeoutMS, final Object f) {
  return eitherAOpt(evalWithTimeout(timeoutMS, f));
}

static Object evalWithTimeoutOrException(double timeoutSeconds, final Object f) {
  return eitherAOpt(evalWithTimeout(timeoutSeconds, f));
}

// the following 4 syntaxes are ok
static <A> A evalWithTimeoutOrException(int timeoutMS, F0<A> f) {
  Either<Object, Thread> e = evalWithTimeout(timeoutMS, f);
  if (e.isA()) return (A) e.a();
  throw fail(trim("Timeout after " + timeoutMS + " ms. "
    + unnull(stackTraceForThread(e.b()))));
}

static <A> A evalWithTimeoutOrException(double timeoutSeconds, F0<A> f) {
  return evalWithTimeoutOrException(toMS_int(timeoutSeconds), f);
}

static <A> A evalWithTimeoutOrException(int timeoutMS, IF0<A> f) {
  return evalWithTimeoutOrException(timeoutMS, if0ToF0(f));
}

static <A> A evalWithTimeoutOrException(double timeoutSeconds, IF0<A> f) {
  return evalWithTimeoutOrException(timeoutSeconds, if0ToF0(f));
}


static F0 runnableToF0(Runnable r) {
  return r == null ? null : new F0<Object>() { public Object get() { try {  r.run(); return null;  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "r.run(); null;"; }};
}


  static String format(String pat, Object... args) {
    return format3(pat, args);
  }



static String fsI_flex(String s) {
  return startsWithDigit(s) ? "#" + s : s;
}


static DialogIO talkToSubBot(final long vport, final DialogIO io) {
  return talkToSubBot(String.valueOf(vport), io);
}

static DialogIO talkToSubBot(final String subBot, final DialogIO io) {
  if (subBot == null) return io;
  return new talkToSubBot_IO(subBot, io);
}

static class talkToSubBot_IO extends DialogIO {
  String subBot;
  DialogIO io;
  
  talkToSubBot_IO(String subBot, DialogIO io) {
  this.io = io;
  this.subBot = subBot;}
  
  // delegate all but sendLine
  boolean isStillConnected() { return io.isStillConnected(); }
  String readLineImpl() { return io.readLineImpl(); }
  boolean isLocalConnection() { return io.isLocalConnection(); }
  Socket getSocket() { return io.getSocket(); }
  public void close() { try { io.close(); } catch (Exception __e) { throw rethrow(__e); } }

  void sendLine(String line) {
    io.sendLine(format3("please forward to bot *: *", subBot, line));
  }
}


static DialogIO talkTo(int port) {
  return talkTo("localhost", port);
}

static int talkTo_defaultTimeout = 10000; // This is the CONNECT timeout
static int talkTo_timeoutForReads = 0; // Timeout waiting for answers (0 = no timeout)

static ThreadLocal<Map<String, DialogIO>> talkTo_byThread = new ThreadLocal();

static DialogIO talkTo(String ip, int port) { try {
  String full = ip + ":" + port;
  Map<String, DialogIO> map = talkTo_byThread.get();
  if (map != null && map.containsKey(full)) return map.get(full);
  
  if (isLocalhost(ip) && port == vmPort()) return talkToThisVM();

  return new talkTo_IO(ip, port);
} catch (Exception __e) { throw rethrow(__e); } }

static class talkTo_IO extends DialogIO { 
  String ip;
  int port;
  Socket s;
  Writer w;
  BufferedReader in;
  
  talkTo_IO(String ip, int port) {
  this.port = port;
  this.ip = ip; try {
    s = new Socket();
    try {
      if (talkTo_timeoutForReads != 0)
        s.setSoTimeout(talkTo_timeoutForReads);
      s.connect(new InetSocketAddress(ip, port), talkTo_defaultTimeout);
    } catch (Throwable e) {
      throw fail("Tried talking to " + ip + ":" + port, e);
    }
  
    w = new OutputStreamWriter(s.getOutputStream(), "UTF-8");
    in = new BufferedReader(new InputStreamReader(s.getInputStream(), "UTF-8"));
  } catch (Exception __e) { throw rethrow(__e); } }
  
  boolean isLocalConnection() {
    return s.getInetAddress().isLoopbackAddress();
  }
  
  boolean isStillConnected() {
    return !(eos || s.isClosed());
  }
  
  void sendLine(String line) { try {
    Lock __0 = lock; lock(__0); try {
    w.write(line + "\n");
    w.flush();
  } finally { unlock(__0); } } catch (Exception __e) { throw rethrow(__e); } }
  
  String readLineImpl() { try {
    return in.readLine();
  } catch (Exception __e) { throw rethrow(__e); } }
  
  public void close() {
    try {
      if (!noClose) s.close();
    } catch (IOException e) {
      // whatever
    }
  }
  
  Socket getSocket() {
    return s;
  }
}


// works on lists and strings and null

static int indexOfIgnoreCase(List<String> a, String b) {
  return indexOfIgnoreCase(a, b, 0);
}

static int indexOfIgnoreCase(List<String> a, String b, int i) {
  int n = a == null ? 0 : a.size();
  for (; i < n; i++)
    if (eqic(a.get(i), b)) return i;
  return -1;
}

static int indexOfIgnoreCase(String[] a, String b) { return indexOfIgnoreCase(a, b, 0); }
static int indexOfIgnoreCase(String[] a, String b, int i) {
  int n = a == null ? 0 : a.length;
  for (; i < n; i++)
    if (eqic(a[i], b)) return i;
  return -1;
}

static int indexOfIgnoreCase(String a, String b) {
  return indexOfIgnoreCase_manual(a, b);
  /*Matcher m = Pattern.compile(b, Pattern.CASE_INSENSITIVE + Pattern.LITERAL).matcher(a);
  if (m.find()) return m.start(); else ret -1;*/
}

static int indexOfIgnoreCase(String a, String b, int i) {
  return indexOfIgnoreCase_manual(a, b, i);
}


static List<ProgramScan.Program> quickBotScan() {
  return ProgramScan.quickBotScan();
}

static List<ProgramScan.Program> quickBotScan(int[] preferredPorts) {
  return ProgramScan.quickBotScan(preferredPorts);
}

static List<ProgramScan.Program> quickBotScan(String searchPattern) {
  List<ProgramScan.Program> l = new ArrayList<ProgramScan.Program>();
  for (ProgramScan.Program p : ProgramScan.quickBotScan())
    if (indexOfIgnoreCase(p.helloString, searchPattern) == 0)
      l.add(p);
  return l;
}



static String firstPartOfHelloString(String s) {
  int i = s.lastIndexOf('/');
  return i < 0 ? s : rtrim(s.substring(0, i));
}


static String sendToLocalBotQuietly(String bot, String text, Object... args) {
  text = format3(text, args);
  
   DialogIO channel = newFindBot2(bot); try {
  if (channel == null)
    throw fail(quote(bot) + " not found");
  try {
    channel.readLine();
    channel.sendLine(text);
    String s = channel.readLine();
    return s;
  } catch (Throwable e) {
    e.printStackTrace();
    return null;
  }
} finally { _close(channel); }}

static String sendToLocalBotQuietly(int port, String text, Object... args) {
  text = format3(text, args);
   DialogIO channel = talkTo(port); try {
  try {
    channel.readLine();
    channel.sendLine(text);
    String s = channel.readLine();
    return s;
  } catch (Throwable e) {
    e.printStackTrace();
    return null;
  }
} finally { _close(channel); }}


static String getBotAddress(String bot) {
  List<ScannedBot> l = fullBotScan(bot);
  return empty(l) ? null : first(l).address;
}


static Object unstructure_matchOK2OrFail(String s) {
  if (swic(s, "ok "))
    return unstructure_startingAtIndex(s, 3);
  else
    throw fail(s);
}


static String sendToLocalBot(String bot, String text, Object... args) {
  text = format3(text, args);
  
   DialogIO channel = findBot(bot); try {
  if (channel == null)
    throw fail(quote(bot) + " not found");
  try {
    channel.readLine();
    print(bot + "> " + shorten(text, 80));
    channel.sendLine(text);
    String s = channel.readLine();
    print(bot + "< " + shorten(s, 80));
    return s;
  } catch (Throwable e) {
    e.printStackTrace();
    return null;
  }
} finally { _close(channel); }}

static String sendToLocalBot(int port, String text, Object... args) {
  text = format3(text, args);
   DialogIO channel = talkTo(port); try {
  try {
    channel.readLine();
    print(port + "> " + shorten(text, 80));
    channel.sendLine(text);
    String s = channel.readLine();
    print(port + "< " + shorten(s, 80));
    return s;
  } catch (Throwable e) {
    e.printStackTrace();
    return null;
  }
} finally { _close(channel); }}


static Object[] mapToObjectArray(Map map) {
  List l = new ArrayList();
  for (Object o : keys(map)) {
    l.add(o);
    l.add(map.get(o));
  }
  return toObjectArray(l);
}

static Object[] mapToObjectArray(Object f, Collection l) {
  int n = l(l);
  Object[] array = new Object[n];
  if (n != 0) {
    Iterator it = iterator(l);
    for (int i = 0; i < n; i++)
      array[i] = callF(f, it.next());
  }
  return array;
}

static Object[] mapToObjectArray(Object f, Object[] l) {
  int n = l(l);
  Object[] array = new Object[n];
  for (int i = 0; i < n; i++)
    array[i] = callF(f, l[i]);
  return array;
}

static <A> Object[] mapToObjectArray(Collection<A> l, IF1<A, Object> f) {
  return mapToObjectArray(f, l);
}

static <A> Object[] mapToObjectArray(A[] l, IF1<A, Object> f) {
  return mapToObjectArray(f, l);
}

static <A> Object[] mapToObjectArray(IF1<A, Object> f, A[] l) {
  int n = l(l);
  Object[] array = new Object[n];
  for (int i = 0; i < n; i++)
    array[i] = f.get(l[i]);
  return array;
}

static <A> Object[] mapToObjectArray(IF1<A, Object> f, Collection<A> l) {
  int n = l(l);
  Object[] array = new Object[n];
  if (n != 0) {
    Iterator it = iterator(l);
    for (int i = 0; i < n; i++)
      array[i] = callF(f, it.next());
  }
  return array;
}


// return Int.MAX_VALUE if not applicable
// Lower score is better
static int methodApplicabilityScore_onTypes(Method m, Class[] argTypes) {
  return methodApplicabilityScore_onTypes((Executable) m, argTypes);
}

static int methodApplicabilityScore_onTypes(Executable m, Class[] argTypes) {
   Class<?>[] types = m.getParameterTypes();
   if (types.length != argTypes.length) return Integer.MAX_VALUE;
   int score = 0;
   for (int i = 0; i < types.length; i++) {
     Class a = argTypes[i];
     Class c = types[i];
     if (c == a) {} // perfect match
     else if (isSubclassOf(a, c)) ++score;
     else return Integer.MAX_VALUE;
  }
  return score;
}






static Boolean grabbableIntPixels_succeeded;
static boolean grabbableIntPixels_enable = true;

static GrabbableIntPixels grabbableIntPixels(BufferedImage img) {
  if (img == null || !grabbableIntPixels_enable) return null;
  try {
    var result = grabbableIntPixels_impl(img);
    grabbableIntPixels_succeeded = result != null;
    return result;
  } catch (Throwable _e) {
    grabbableIntPixels_succeeded = false;
  
throw rethrow(_e); }
}

static GrabbableIntPixels grabbableIntPixels_impl(BufferedImage img) {
  Raster raster = img.getRaster();
  SampleModel _sampleModel = raster.getSampleModel();
  if (!(_sampleModel instanceof SinglePixelPackedSampleModel)) return null;
  SinglePixelPackedSampleModel sampleModel =  (SinglePixelPackedSampleModel) _sampleModel;
  DataBufferInt dataBuffer =  (DataBufferInt) (raster.getDataBuffer());
  assertEquals(1, dataBuffer.getNumBanks());
  assertEquals(DataBuffer.TYPE_INT, dataBuffer.getDataType());

  // Let's at this point assume the raster data is what we
  // think it is... (TODO: test on unusual platforms)
  
  int w = img.getWidth(), h = img.getHeight();
  int scanlineStride = sampleModel.getScanlineStride();
  int[] pixels = dataBuffer.getData();
  int offset = dataBuffer.getOffset();
  int translateX = raster.getSampleModelTranslateX();
  int translateY = raster.getSampleModelTranslateY();
  offset += -translateX-translateY*scanlineStride;
  return new GrabbableIntPixels(pixels, w, h, offset, scanlineStride);
}


static int colorToIntOpaque(Color c) {
  return c.getRGB() | 0xFF000000;
}


static String copyToClipboard(String text) {
  return copyTextToClipboard(text);
}

static <A extends Image> A copyToClipboard(A image) {
  copyImageToClipboard(image);
  return image;
}

static File copyToClipboard(File f) {
  return copyFileToClipboard(f);
}


// action = runnable or method name
static void onUpdateAndNow(JComponent c, final Object r) {
  onUpdate(c, r);
  callF(r);
}

static void onUpdateAndNow(List<? extends JComponent> l, Object r) {
  for (JComponent c : l)
    onUpdate(c, r);
  callF(r);
}


static String readLineHidden() { try {
  if (get(javax(), "readLine_reader") == null)
    set(javax(), "readLine_reader" , new BufferedReader(new InputStreamReader(System.in, "UTF-8")));
  try {
    return ((BufferedReader) get(javax(), "readLine_reader")).readLine();
  } finally {
    consoleClearInput();
  }
} catch (Exception __e) { throw rethrow(__e); } }


// This is made for NL parsing.
// It's javaTok extended with "..." token, "$n" and "#n" and
// special quotes (which are converted to normal ones).

static List<String> javaTokPlusPeriod(String s) {
  List<String> tok = new ArrayList<String>();
  if (s == null) return tok;
  int l = s.length();
  
  int i = 0;
  while (i < l) {
    int j = i;
    char c; String cc;
    
    // scan for whitespace
    while (j < l) {
      c = s.charAt(j);
      cc = s.substring(j, Math.min(j+2, l));
      if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
        ++j;
      else if (cc.equals("/*")) {
        do ++j; while (j < l && !s.substring(j, Math.min(j+2, l)).equals("*/"));
        j = Math.min(j+2, l);
      } else if (cc.equals("//")) {
        do ++j; while (j < l && "\r\n".indexOf(s.charAt(j)) < 0);
      } else
        break;
    }
    
    tok.add(s.substring(i, j));
    i = j;
    if (i >= l) break;
    c = s.charAt(i);
    cc = s.substring(i, Math.min(i+2, l));

    // scan for non-whitespace
    if (c == (char) 0x201C || c == (char) 0x201D) c = '"'; // normalize quotes
    if (c == '\'' || c == '"') {
      char opener = c;
      ++j;
      while (j < l) {
        char _c = s.charAt(j);
        if (_c == (char) 0x201C || _c == (char) 0x201D) _c = '"'; // normalize quotes
        if (_c == opener) {
          ++j;
          break;
        } else if (s.charAt(j) == '\\' && j+1 < l)
          j += 2;
        else
          ++j;
      }
      if (j-1 >= i+1) {
        tok.add(opener + s.substring(i+1, j-1) + opener);
        i = j;
        continue;
      }
    } else if (Character.isJavaIdentifierStart(c))
      do ++j; while (j < l && (Character.isJavaIdentifierPart(s.charAt(j)) || s.charAt(j) == '\'')); // for things like "this one's"
    else if (Character.isDigit(c))
      do ++j; while (j < l && Character.isDigit(s.charAt(j)));
    else if (cc.equals("[[")) {
      do ++j; while (j+1 < l && !s.substring(j, j+2).equals("]]"));
      j = Math.min(j+2, l);
    } else if (cc.equals("[=") && i+2 < l && s.charAt(i+2) == '[') {
      do ++j; while (j+2 < l && !s.substring(j, j+3).equals("]=]"));
      j = Math.min(j+3, l);
    } else if (s.substring(j, Math.min(j+3, l)).equals("..."))
      j += 3;
    else if (c == '$' || c == '#')
      do ++j; while (j < l && Character.isDigit(s.charAt(j)));
    else
      ++j;

    tok.add(s.substring(i, j));
    i = j;
  }
  
  if ((tok.size() % 2) == 0) tok.add("");
  return tok;
}



static boolean isNonNegativeInteger(String s) {
  int n = l(s);
  if (n == 0) return false;
  int i = 0;
  while (i < n) {
    char c = s.charAt(i);
    if (c < '0' || c > '9') return false;
    ++i;
  }
  return true;
}


/*static <A> L<A> printNumberedLines(L<A> l) {
  printNumberedLines((Collection<A>) l);
  ret l;
}

static <A> L<A> printNumberedLines(S prefix, L<A> l) {
  printNumberedLines(prefix, (Collection<A>) l);
  ret l;
}*/

static void printNumberedLines(Map map) {
  printNumberedLines(mapToLines(map));
}

static void printNumberedLines(String prefix, Map map) {
  printNumberedLines(prefix, mapToLines(map));
}

static <A extends Iterable> A printNumberedLines(A l) {
  int i = 0;
  if (l != null) for (Object a : cloneList(l)) print((++i) + ". " + str(a));
  return l;
}

static <A extends Iterable> A printNumberedLines(String prefix, A l) {
  int i = 0;
  if (l != null) for (Object a : cloneList(l)) print(prefix + (++i) + ". " + str(a));
  return l;
}

static void printNumberedLines(Object[] l) { printNumberedLines("", l); }
static void printNumberedLines(String prefix, Object[] l) {
  printNumberedLines(prefix, wrapAsList(l));
}

static void printNumberedLines(Object o) {
  printNumberedLines(lines(str(o)));
}


static Color colorFromRGBA(int rgba) {
  return new Color(rgba, true);
}


static float clamp(float x, float a, float b) {
  return x < a ? a : x > b ? b : x;
}

static double clamp(double x, double a, double b) {
  return x < a ? a : x > b ? b : x;
}

static int clamp(int x, int a, int b) {
  return x < a ? a : x > b ? b : x;
}

static long clamp(long x, long a, long b) {
  return x < a ? a : x > b ? b : x;
}


static long clockToSysTimeDiff() {
  return sysNow()-now();
}


static Map<Class, Set<String>> allFields_cache = weakHashMap();

static Set<String> allFields(Object o) {
  if (o == null) return emptySet();
  Class _c = _getClass(o);
  Set<String> fields = allFields_cache.get(_c);
  if (fields == null)
    allFields_cache.put(_c, fields = asTreeSet(keys(getOpt_getFieldMap(o))));
  return fields;
}


static int stdHash(Object a, String... fields) {
  if (a == null) return 0;
  int hash = getClassName(a).hashCode();
  for (String field : fields)
    hash = boostHashCombine(hash, hashCode(getOpt(a, field)));
  return hash;
}


static String rep(int n, char c) {
  return repeat(c, n);
}

static String rep(char c, int n) {
  return repeat(c, n);
}

static <A> List<A> rep(A a, int n) {
  return repeat(a, n);
}

static <A> List<A> rep(int n, A a) {
  return repeat(n, a);
}



static String decimalFormatEnglish(String format, double d) {
  return decimalFormatEnglish(format).format(d);
}

static java.text.DecimalFormat decimalFormatEnglish(String format) {
  return new java.text.DecimalFormat(format, new java.text.DecimalFormatSymbols(Locale.ENGLISH));
}


static String hfulltag(String tag) {
  return hfulltag(tag, "");
}

static String hfulltag(String tag, Object contents, Object... params) {
  return hopeningTag(tag, params) + str(contents) + "</" + tag + ">";
}


static String assertPossibleGlobalID(String s) {
  if (!possibleGlobalID(s))
    throw fail("Not an acceptable global ID: " + s);
  return s;
}


static char charPlus(char a, int b) {
  return (char) (((int) a) + b);
}

static char charPlus(int b, char a) {
  return charPlus(a, b);
}


// better modulo that gives positive numbers always
static int mod(int n, int m) {
  return (n % m + m) % m;
}

static long mod(long n, long m) {
  return (n % m + m) % m;
}

static BigInteger mod(BigInteger n, int m) {
  return n.mod(bigint(m));
}

static double mod(double n, double m) {
  return (n % m + m) % m;
}





static int withMargin_defaultWidth = 6;

static JPanel withMargin(Component c) {
  return withMargin(withMargin_defaultWidth, c);
}

static JPanel withMargin(int w, Component c) {
  return withMargin(w, w, c);
}

static JPanel withMargin(int w, int h, Component c) {
  return withMargin(w, h, w, h, c);
}

static JPanel withMargin(final int top, final int left, final int bottom, final int right, final Component c) {
  return swing(new F0<JPanel>() { public JPanel get() { try { 
    JPanel p = marginPanel();
    p.setBorder(BorderFactory.createEmptyBorder(top, left, bottom, right));
    p.add(c);
    return p;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "JPanel p = marginPanel();\r\n    p.setBorder(BorderFactory.createEmptyBorder(to..."; }});
}


static void ensureDBNotRunning(String name) {
  if (hasBot(name)) {
    try {
      String framesBot = dropSuffix(".", name) + " Frames";
      print("Trying to activate frames of running DB: " + framesBot);
      if (isOK(sendOpt(framesBot, "activate frames")) && isMainProgram())
        cleanKill();
    } catch (Throwable __e) { printStackTrace(__e); }
    throw fail("Already running: " + name);
  }
}

static void ensureDBNotRunning() {
  ensureDBNotRunning(dbBotStandardName());
}


static String dbBotStandardName() {
  String home = userHome();
  String name = dbBotName(getDBProgramID());
  if (neq(home, actualUserHome()))
    name += " " + quote(home);
  return name + ".";
}


static volatile Android3 dbBot_instance;

static Android3 dbBot() { return dbBot(true); }
static Android3 dbBot(boolean ensureNotRunning) {
  return dbBot(dbBotStandardName(), ensureNotRunning);
}

static Android3 dbBot(String name) { return dbBot(name, true); }
static Android3 dbBot(String name, boolean ensureNotRunning) {
  if (ensureNotRunning)
    ensureDBNotRunning(name);
  return dbBot_instance = methodsBot2(name, assertNotNull(db_mainConcepts()), db_standardExposedMethods(), db_mainConcepts().lock);
}


static void thinAProgramsBackups(String progID, boolean doIt) {
  List<File> files = new ArrayList();
  Map<File, Double> ageMap = new HashMap();
  
  // minutes (last group) are optional
  java.util.regex.Pattern pat = regexp("^(.*)\\.backup(20\\d\\d)(\\d\\d)(\\d\\d)-(\\d\\d)(\\d*)$");
  //print("Processing backups of program " + progID);
  File dir = programDir(progID);
  for (File f : listFilesNotDirs(dir, newFile(dir, "backups"))) {
    String s = f.getName();
    java.util.regex.Matcher matcher = pat.matcher(s);
    { if (!(matcher.find())) continue; }
    String originalName = matcher.group(1);
    { if (!(eq(originalName, "concepts.structure.gz"))) continue; }
    //print("Found backup: " + sfu(matcherGroups(matcher)));
    int year = matcherInt(matcher, 2);
    int month = matcherInt(matcher, 3);
    int day = matcherInt(matcher, 4);
    int hour = matcherInt(matcher, 5);
    int minute = matcherInt(matcher, 6);
    long time = timestampFromYMDHM(year, month, day, hour, minute);
    double age = ((now()-time)/1000.0/60/60/24);
    //print("Age: " + age + " days");
    ageMap.put(f, age);
    files.add(f);
  }
  
  int numDeleted = 0;
  sortByMap_inPlace(files, ageMap);
  double lastAge = -1;
  for (File f : files) {
    double age = ageMap.get(f);
    if (!thinAProgramsBackups_shouldKeep(age, lastAge)) {
      //print("Deleting: " + f);
      ++numDeleted;
      if (doIt) {
        print("Deleting: " + f);
        f.delete();
      }
    } else {
      //print("Keeping: " + f);
      lastAge = age;
    }
  }
  if (numDeleted != 0)
    print((doIt ? "Deleted: " : "Would delete: ") + n(numDeleted, "file"));
}

// age = age in days
static boolean thinAProgramsBackups_shouldKeep(double age, double lastAge) {
  return defaultAgeBasedBackupRetentionStrategy_shouldKeep(age, lastAge);
}


// this copies to RGBA
static BufferedImage copyImage(Image img) {
  if (img == null) return null;
  if (img instanceof BufferedImage)
    return copyImage((BufferedImage) img);
  int w = img.getWidth(null), h = img.getHeight(null);
  BufferedImage bi = newBufferedImage(w, h);
  drawImage(bi, img);
  return bi;
}

// this one stays in color model. inconsistent i guess
static BufferedImage copyImage(BufferedImage bi) {
  if (bi == null) return null;
  ColorModel cm = bi.getColorModel();
  boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
  WritableRaster raster = bi.copyData(bi.getRaster().createCompatibleWritableRaster());
  return new BufferedImage(cm, raster, isAlphaPremultiplied, null);
}


static boolean isImageFile(File f) {
  return isImageFileName(fileName(f)) && isFile(f);
}


static String appendColonIfNempty(String s) {
  return empty(s) ? "" : s + ": ";
}


static String joinStrings(String sep, Object... strings) {
  return joinStrings(sep, Arrays.asList(strings));
}

static String joinStrings(String sep, Iterable strings) {
  StringBuilder buf = new StringBuilder();
  for (Object o : unnull(strings)) { 
    String s = strOrNull(o);
    if (nempty(s)) {
      if (nempty(buf)) buf.append(sep);
      buf.append(s);
    }
  }
  return str(buf);
}




static String formatWithThousandsSeparator(long l) {
  return NumberFormat.getInstance(new Locale("en_US")).format(l);
}


// accept purpose argument so we are a drop-in for tempVerboseLock
static AutoCloseable tempLock(Lock lock) { return tempLock("", lock); }
static AutoCloseable tempLock(String purpose, Lock lock) {
  if (lock == null) return null;
  lock(lock);
  return new AutoCloseable() { public String toString() { return "unlock(lock);"; } public void close() throws Exception { unlock(lock); }};
}


static String singleFieldName(Class c) {
  Set<String> l = listFields(c);
  if (l(l) != 1)
    throw fail("No single field found in " + c + " (have " + n(l(l), "fields") + ")");
  return first(l);
}


static <A extends Concept> List<A> findBackRefs(Collection<? extends Concept> concepts, Class<A> type) {
  IdentityHashMap<A, Boolean> l = new IdentityHashMap();
  for (Concept c : concepts)
    if (c.backRefs != null) for (Concept.Ref r : c.backRefs)
      if (instanceOf(r.concept(), type))
        l.put((A) r.concept(), true);
  return asList(keys(l));
}

// TODO: sort by ID?
static <A extends Concept> List<A> findBackRefs(Concept c, Class<A> type) {
  IdentityHashMap<A, Boolean> l = new IdentityHashMap();
  if (c != null && c.backRefs != null) for (Concept.Ref r : c.backRefs)
    if (instanceOf(r.concept(), type))
      l.put((A) r.concept(), true);
  return asList(keys(l));
}
  
static <A extends Concept> List<A> findBackRefs(Class<A> type, Concept c) {
  return findBackRefs(c, type);
}

static Collection<Concept> findBackRefs(Concept c) {
  return findBackRefs(c, Concept.class);
}



static boolean checkConceptFieldsIC(Concept x, Object... data) {
  for (int i = 0; i < l(data); i += 2)
    if (!eqicOrEq(cget(x, (String) data[i]), deref(data[i+1])))
      return false;
  return true;
}


static void setMetaAndVerify(IMeta o, Object key, Object value) {
  setMeta(o, key, value);
  assertSame(() -> str(key), value, metaGet(o, key));
}

static void setMetaAndVerify(Object o, Object key, Object value) {
  setMeta(o, key, value);
  assertSame(() -> str(key), value, metaGet(o, key));
}


static JPanel centerAndNorth(final Component c, final Component n) {
  return swing(new F0<JPanel>() { public JPanel get() { try { 
    JPanel panel = new JPanel(new BorderLayout());
    panel.add(BorderLayout.CENTER, wrap(c));
    panel.add(BorderLayout.NORTH, wrap(n));
    return panel;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "JPanel panel = new JPanel(new BorderLayout);\r\n    panel.add(BorderLayout.CENT..."; }});
}




static <A extends JComponent> A addMargin(final int top, final int left, final int bottom, final int right, final A c) {
  if (c != null) { swing(() -> { 
    Border margin = BorderFactory.createEmptyBorder(top, left, bottom, right);
    c.setBorder(jCompoundBorder(c.getBorder(), margin));
  }); }
  return c;
}

static <A extends JComponent> A addMargin(int w, A c) {
  return addMargin(w, w, w, w, c);
}



static int withRightMargin_defaultWidth = 6;

static JPanel withRightMargin(Component c) {
  return withRightMargin(withRightMargin_defaultWidth, c);
}

static JPanel withRightMargin(final int w, final Component c) {
  return swing(new F0<JPanel>() { public JPanel get() { try { 
    JPanel p = marginPanel();
    p.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, w));
    p.add(c);
    return p;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "JPanel p = marginPanel();\r\n    p.setBorder(BorderFactory.createEmptyBorder(0,..."; }});
}


static JScrollPane withoutViewportBorder(JScrollPane sp) {
  if (sp != null) { swing(() -> {  sp.setViewportBorder(null); }); }
  return sp;
}


static JPanel jFullCenter(final Component c) {
  return swing(new F0<JPanel>() { public JPanel get() { try { 
    JPanel panel = new JPanel(new GridBagLayout());
    panel.add(c);
    return panel;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "JPanel panel = new JPanel(new GridBagLayout);\r\n    panel.add(c);\r\n    ret panel;"; }});
}


static JMenuItem directJMenuItem(Action a) {
  return new JMenuItem(a) {
    public Dimension getMaximumSize() {
      return new Dimension(super.getPreferredSize().width, super.getMaximumSize().height);
    }
  };
}

static JMenuItem directJMenuItem(String text, Object action) {
  return directJMenuItem(abstractAction(text, action));
}



static JMenuBar addMenuBar(final Component c) {
  return swing(new F0<JMenuBar>() { public JMenuBar get() { try { 
    RootPaneContainer f = getPossiblyInternalFrame(c);
    if (f == null) return null;
    JMenuBar bar =  (JMenuBar) (call(f, "getJMenuBar"));
    if (bar == null) {
      setMenuBar(f, bar = new JMenuBar());
      revalidate((Component) f);
    }
    return bar;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "RootPaneContainer f = getPossiblyInternalFrame(c);\r\n    if (f == null) null;\r..."; }});
}


// Use like this: renderVars(+x, +y)
static String renderVars_str(Object... params) {
  List<String> l = new ArrayList();
  int i = 0;
  if (odd(l(params))) {
    l.add(strOrNull(first(params)));
    ++i;
  }
  for (; i+1 < l(params); i += 2)
    l.add(params[i] + "=" + params[i+1]);
  return trim(joinWithComma(l));
}


static boolean zipFileContains_falseOnError(File inZip, String fileName) {
  try {
    return zipFileContains(inZip, fileName);
  } catch (Throwable e) {
    return false;
  }
}


static File javaHome() {
  return envJavaHome();
}

static File javaHome(String sub) {
  return newFile(envJavaHome(), sub);
}


// moduleName = e.g. "java.scripting"
static File jigsawModuleFile(String moduleName) { return jigsawModuleFile(moduleName, javaHome()); }
static File jigsawModuleFile(String moduleName, File javaHome) {
  return new File(javaHome, "jmods/" + moduleName + ".jmod");
}


// from: https://stackoverflow.com/questions/14416107/int-array-to-bufferedimage
// pixels are RGB pixels
static BufferedImage intArrayToBufferedImage(int[] pixels, int w, int h) {
  int[] bitMasks = new int[]{0xFF0000, 0xFF00, 0xFF, 0xFF000000};
  SinglePixelPackedSampleModel sm = new SinglePixelPackedSampleModel(DataBuffer.TYPE_INT, w, h, bitMasks);
  DataBufferInt db = new DataBufferInt(pixels, pixels.length);
  WritableRaster wr = Raster.createWritableRaster(sm, db, new Point());
  return new BufferedImage(ColorModel.getRGBdefault(), wr, false, null);
}


static void registerEscape_rootPane(JComponent rootPane, final Runnable r) {
  String name = "Escape";
  Action action = abstractAction(name, r);
  KeyStroke keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
  rootPane.getActionMap().put(name, action);
  rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(keyStroke, name);
}


static <A> A heldInstance(Class<A> c) {
  List<Object> l = holdInstance_l.get();
  for (int i = l(l)-1; i >= 0; i--) {
    Object o = l.get(i);
    if (isInstanceOf(o, c))
      return (A) o;
  }
  throw fail("No instance of " + className(c) + " held");
}


static JTextField jtextfield() {
  return jTextField();
}

static JTextField jtextfield(String text) {
  return jTextField(text);
}

static JTextField jtextfield(Object o) {
  return jTextField(o);
}



static String programName() {
  return getProgramName();
}


static int showForm_defaultGap = 4;
static int showForm_gapBetweenColumns = 10;

static JPanel showFormTitled(final String title, final Object... _parts) {
  JDesktopPane desktop = mainDesktopPane();
  if (desktop != null) return showInternalFrameFormTitled(desktop, title, _parts);
  return swing(new F0<JPanel>() { public JPanel get() { try { 
    final Var<JFrame> frame = new Var();
    JPanel panel = showForm_makePanel(false, _parts);
    showForm_makeFrame(title, panel);
    frame.set(getFrame(panel));
    return panel;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "final new Var<JFrame> frame;\r\n    JPanel panel = showForm_makePanel(false, _p..."; }});
}

static JPanel showForm_makePanel(Boolean internalFrame, Object... _parts) {
  List<JComponent> out = showForm_arrange1(showForm_makeComponents(internalFrame, _parts));
  return vstackWithSpacing(out, showForm_defaultGap);
}


static JPanel centerAndEast(final Component c, final Component e) {
  return swing(new F0<JPanel>() { public JPanel get() { try { 
    JPanel panel = new JPanel(new BorderLayout());
    panel.add(BorderLayout.CENTER, wrap(c));
    panel.add(BorderLayout.EAST, wrap(e));
    return panel;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "JPanel panel = new JPanel(new BorderLayout);\r\n    panel.add(BorderLayout.CENT..."; }});
}


static String getTextTrim(JTextComponent c) {
  return trim(getText(c));
}

// tested for editable combo box - returns the contents of text field
static String getTextTrim(JComboBox cb) {
  return trim(getText(cb));
}

static String getTextTrim(JComponent c) {
  if (c instanceof JLabel) return trim(((JLabel) c).getText());
  if (c instanceof JComboBox) return getTextTrim((JComboBox) c);
  return getTextTrim((JTextComponent) c);
}


static <A extends JComponent> A renameSubmitButton(A form, String newName) {
  renameButton(form, showFormSubmitButtonName(), newName);
  return form;
}

static <A extends JComponent> A renameSubmitButton(String newName, A form) {
  return renameSubmitButton(form, newName);
}


static boolean ewicOneOf(String s, String... l) {
  if (s != null) for (String x : l) if (ewic(s, x)) return true; return false;
}


static BufferedImage reconstructBufferedImage(BufferedImage img) {
  if (img == null) return null;
  RGBImage rgb = new RGBImage(img);
  rgb.uncacheBufferedImage();
  return rgb.getBufferedImage();
}


static List<CriticalAction> beginCriticalAction_inFlight = synchroList();

static class CriticalAction implements AutoCloseable {
  String description;
  
  CriticalAction() {}
  CriticalAction(String description) {
  this.description = description;}
  
  final public void close(){ done(); }
public void done() {
    beginCriticalAction_inFlight.remove(this);
  }
}

static CriticalAction beginCriticalAction(String description) {
  ping();
  CriticalAction c = new CriticalAction(description);
  beginCriticalAction_inFlight.add(c);
  return c;
}

static void cleanMeUp_beginCriticalAction() {
  int n = 0;
  while (nempty(beginCriticalAction_inFlight)) {
    int m = l(beginCriticalAction_inFlight);
    if (m != n) {
      n = m;
      try {
        print("Waiting for " + n2(n, "critical actions") + ": " + join(", ", collect(beginCriticalAction_inFlight, "description")));
      } catch (Throwable __e) { printStackTrace(__e); }
    }
    sleepInCleanUp(10);
  }
}


static File toFile(Object o) {
  if (o instanceof File) return (File) o;
  if (o instanceof String) return new File((String) o);
  throw fail("Not a file: " + o);
}


static <A extends JTextComponent> A moveCaretToEnd(A ta) {
  setCaretPosition(ta, textAreaTextLength(ta));
  return ta;
}


static JComponent showTitledForm(String title, Object... _parts) {
  return showFormTitled(title, _parts);
}



static Runnable runnableThread(final Runnable r) {
  return new Runnable() {  public void run() { try {  startThread(r) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "startThread(r)"; }};
}


static Rect intersectRects(Rect a, Rect b) {
  int x = max(a.x, b.x), y = max(a.y, b.y);
  int x2 = min(a.x+a.w, b.x+b.w), y2 = min(a.y+a.h, b.y+b.h);
  return new Rect(x, y, x2-x, y2-y);
}

static Rect intersectRects(Rect a, int x1, int y1, int w, int h) {
  if (a == null
    || a.x >= x1 && a.y >= y1 && a.x2() < x1+w && a.y2() < y1+h)
    return a;
  return rectFromPoints(
    max(a.x, x1), max(a.y, y1),
    min(a.x2(), x1+w), min(a.y2(), y1+h));
}


static boolean rectEmpty(Rect r) {
  return r == null || r.w <= 0 || r.h <= 0;
}


static BufferedImage decodeImage(byte[] data) { try {
  if (empty(data)) return null;
  return ImageIO.read(new ByteArrayInputStream(data));
} catch (Exception __e) { throw rethrow(__e); } }


static byte[] bytesFromDataURL(String url) {
  String pref = "base64,";
  int i = indexOf(url, pref);
  if (i < 0) return null;
  return base64decode(substring(url, i+l(pref)));
}


static AutoCloseable tempActivity(Object r) {
  return null;
}


static boolean allPaused() {
  return ping_pauseAll;
}



static void cancelTimer(javax.swing.Timer timer) {
  if (timer != null) timer.stop();
}


static void cancelTimer(java.util.Timer timer) {
  if (timer != null) timer.cancel();
}

static void cancelTimer(Object o) {
  if (o instanceof java.util.Timer) cancelTimer((java.util.Timer) o);
  
  else if (o instanceof javax.swing.Timer) cancelTimer((javax.swing.Timer) o);
  
  else if (o instanceof AutoCloseable) { try { ((AutoCloseable) o).close(); } catch (Throwable __e) { printStackTrace(__e); }}
}




static void bindTimerToComponent(final Timer timer, JFrame f) {
  bindTimerToComponent(timer, f.getRootPane());
}

static void bindTimerToComponent(final Timer timer, JComponent c) {
  if (c.isShowing())
    timer.start();
  
  c.addAncestorListener(new AncestorListener() {
    public void ancestorAdded(AncestorEvent event) {
      timer.start();
    }

    public void ancestorRemoved(AncestorEvent event) {
      timer.stop();
    }

    public void ancestorMoved(AncestorEvent event) {
    }
  });
}


static <A extends JComponent> A onClick(final A c, final Object runnable) {
  if (c != null) { swing(() -> { 
    c.addMouseListener(new MouseAdapter() {
      public void mouseClicked(MouseEvent e) {
        callF(runnable, e);
      }
    });
  }); }
  return c;
}

// re-interpreted for buttons
static void onClick(JButton btn, final Object runnable) {
  onEnter(btn, runnable);
}


static void disposeFrame(final Component c) {
  disposeWindow(c);
}


static Object[] dropEntryFromParams(Object[] params, int i) {
  int n = l(params);
  if (i < 0 || i >= n) return params;
  if (n == 2) return null;
  Object[] p = new Object[n-2];
  System.arraycopy(params, 0, p, 0, i);
  System.arraycopy(params, i+2, p, i, n-i-2);
  return p;
}


static <A extends Concept> List<A> filterConcepts(List<A> list, Object... params) {
  if (empty(params)) return list;
  List<A> l = new ArrayList();
  for (A x : list)
    if (checkConceptFields(x, params))
      l.add(x);
  return l;
}


static <A> List<A> sorted(Collection<A> c, Object comparator) {
  List<A> l = cloneList(c);
  sort(l, makeComparator(comparator));
  return l;
}

static <A> List<A> sorted(Collection<A> c) {
  List<A> l = cloneList(c);
  sort(l);
  return l;
}

static <A> List<A> sorted(Comparator<A> comparator, Collection<A> c) {
  List<A> l = cloneList(c);
  sort(l, comparator);
  return l;
}


static List<String> methodsStartingWith(Object o, final String prefix) {
  return filter(allMethodNames(o), new F1<String, Object>() { public Object get(String s) { try {  return startsWith(s, prefix);  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "startsWith(s, prefix)"; }});
}


static volatile PersistableThrowable _handleException_lastException;
static List _handleException_onException = synchroList(ll((IVF1<Throwable>) (__1 -> printStackTrace2(__1))));
static boolean _handleException_showThreadCancellations = false;

static void _handleException(Throwable e) {
  _handleException_lastException = persistableThrowable(e);
  
  Throwable e2 = innerException(e);
  if (e2.getClass() == RuntimeException.class && eq(e2.getMessage(), "Thread cancelled.") || e2 instanceof InterruptedException) {
    if (_handleException_showThreadCancellations)
      System.out.println(getStackTrace_noRecord(e2));
    return;
  }

  for (Object f : cloneList(_handleException_onException)) try {
    callF(f, e);
  } catch (Throwable e3) {
    try {
      printStackTrace2(e3); // not using pcall here - it could lead to endless loops
    } catch (Throwable e4) {
      System.out.println(getStackTrace(e3));
      System.out.println(getStackTrace(e4));
    }
  }
}


static boolean interruptThread_verbose = false;

static void interruptThread(Thread t) {
  if (t == null) return;
  if (interruptThread_verbose) print("Interrupting thread " + t);
  
  // note reason in global map
  
  vm_threadInterruptionReasonsMap().put(t, getStackTrace());
  
  t.interrupt();
  URLConnection c =  (URLConnection) (vm_generalSubMap("URLConnection per thread").get(t));
  if (c != null) { try {
    print("Closing URLConnection of interrupted thread.");
    call(c, "disconnect");
  } catch (Throwable __e) { printStackTrace(__e); }}
}


static boolean isJavaXClassLoader(ClassLoader cl) {
  return startsWithOneOf(className(cl), "main$JavaXClassLoader", "x30$JavaXClassLoader");
}


static void setOptAll(Object o, Map<String, Object> fields) {
  if (fields == null) return;
  for (String field : keys(fields))
    setOpt/*_flex*/(o, field, fields.get(field));
}

static void setOptAll(Object o, Object... values) {
  //values = expandParams(c.getClass(), values);
  warnIfOddCount(values);
  for (int i = 0; i+1 < l(values); i += 2) {
    String field = (String) values[i];
    Object value = values[i+1];
    setOpt(o, field, value);
  }
}


static String hijackPrint(Runnable r) {
  StringBuilder buf = new StringBuilder();
  Object old = interceptPrintInThisThread(new F1<String, Boolean>() {
    public Boolean get(String s) {
      buf.append(s);
      return false;
    }
  });
  try {
    { if (r != null) r.run(); }
    return str(buf);
  } finally {
    interceptPrintInThisThread(old);
  }
}


static String reverseString(String s) {
  return empty(s) ? s : new StringBuilder(s).reverse().toString();
}


static int lCommonPrefix(String a, String b) {
  int i = 0, n = Math.min(l(a), l(b));
  while (i < n && a.charAt(i) == b.charAt(i))
    ++i;
  return i;
}



static String htag(String tag) {
  return htag(tag, "");
}

static String htag(String tag, Object contents, Object... params) {
  String openingTag = hopeningTag(tag, params);
  String s = str(contents);
  if (empty(s) && neqic(tag, "script"))
    return dropLast(openingTag) + "/>";
  return openingTag + s + "</" + tag + ">";
}


static String shorten_str(Object o) {
  return shorten(str(o));
}

static String shorten_str(Object o, int max) {
  return shorten(str(o), max);
}

static String shorten_str(int max, Object o) {
  return shorten_str(o, max);
}


static String getCanonicalPath(File f) { try {
  return f == null ? null : f.getCanonicalPath();
} catch (Exception __e) { throw rethrow(__e); } }

static String getCanonicalPath(String path) {
  return getCanonicalPath(newFile(path));
}


static AutoCloseable tempCleaningUp() {
  AutoCloseable result = null;
  
  
  result = tempSetTL(ping_isCleanUpThread, true);
  
  
  return result;
}


static void closeAllWriters(Collection<? extends Writer> l) {
  for (Writer w : unnull(l)) { try {
    w.close();
  } catch (Throwable __e) { printStackTrace(__e); }}
}


static File prepareProgramFile(String name) {
  return mkdirsForFile(getProgramFile(name));
}

static File prepareProgramFile(String progID, String name) {
  return mkdirsForFile(getProgramFile(progID, name));
}


static List<Integer> countIteratorToList(int b) { return countIteratorToList(0, b); }
static List<Integer> countIteratorToList(int a, int b) {
  return asList(countIterator(a, b));
}





static <A> List<A> countIteratorToList(int b, IF1<Integer, A> f) { return countIteratorToList(0, b, f); }
static <A> List<A> countIteratorToList(int a, int b, IF1<Integer, A> f) {
  return asList(countIterator(a, b, f));
}



static List<Integer> countIteratorToList(int a, int b, int step) {
  return asList(countIterator(a, b, step));
}



static <A> List<A> countIteratorToList(double a, double b, double step, IF1<Double, A> f) {
  return asList(countIterator(a, b, step, f));
}



static <A> List<Double> countIteratorToList(double a, double b, double step) {
  return asList(countIterator(a, b, step));
}



static <A> List<A> countIteratorToList(IF1<Double, A> f, double a, double b, double step) {
  return asList(countIterator(f, a, b, step));
}

static <A> List<A> countIteratorToList(IF1<Integer, A> f, int b) { return countIteratorToList(f, 0, b); }
static <A> List<A> countIteratorToList(IF1<Integer, A> f, int a, int b) {
  return asList(countIterator(f, a, b));
}


static <A> List<A> withoutNulls(Iterable<A> l) {
  if (l instanceof List)
    if (!containsNulls((List) l)) return ((List) l);
  List<A> l2 = new ArrayList();
  for (A a : l)
    if (a != null)
      l2.add(a);
  return l2;
}

static <A, B> Map<A, B> withoutNulls(Map<A, B> map) {
  Map<A, B> map2 = similarEmptyMap(map);
  for (A a : keys(map))
    if (a != null) {
      B b = map.get(a);
      if (b != null)
        map2.put(a, b);
    }
  return map2;
}

static <A> List<A> withoutNulls(A[] l) {
  List<A> l2 = new ArrayList();
  if (l != null) for (A a : l)
    if (a != null)
      l2.add(a);
  return l2;
}


static IterableIterator<Integer> countIterator_exclusive(int b) { return countIterator_exclusive(0, b); }
static IterableIterator<Integer> countIterator_exclusive(int a, int b) {
  return new IterableIterator<Integer>() {
    int i = a;
    
    public boolean hasNext() { return i < b; }
    public Integer next() { return i++; }
  };
}

static <A> IterableIterator<A> countIterator_exclusive(int b, IF1<Integer, A> f) { return countIterator_exclusive(0, b, f); }
static <A> IterableIterator<A> countIterator_exclusive(int a, int b, IF1<Integer, A> f) {
  return mapI_if1(f, countIterator_exclusive(a, b));
}


static IterableIterator<Integer> countIterator_exclusive_step(final int a, final int b, final int step) {
  assertTrue("step > 0", step > 0);
  
  return new IterableIterator<Integer>() {
    int i = a;
    
    public boolean hasNext() { return i < b; }
    public Integer next() { var j = i; i += step; return j; }
  };
}

static IterableIterator<Double> countIterator_exclusive_step(double a, double b, double step) {
  assertTrue("step > 0", step > 0);
  
  return new IterableIterator<Double>() {
    double i = a;
    
    public boolean hasNext() { return i < b; }
    public Double next() { var j = i; i += step; return j; }
  };
}

static <A> IterableIterator<A> countIterator_exclusive_step(double a, double b, double step, IF1<Double, A> f) {
  return mapI_if1(f, countIterator_exclusive_step(a, b, step));
}



static Rect boundsAsRect(Component c) {
  return boundsRect(c);
}


static <A> int indexOfMax(List<A> l, IF1<A, Comparable> f) {
  Best_comparable<Integer> best = new Best_comparable();
  for (int i = 0; i < l(l); i++)
    best.put(i, f.get(l.get(i)));
  return or(best.get(), -1);
}


static List<Rect> allScreenBounds() {
  return map(__62 -> screenBounds(__62), screenDevices());
}


static int area(Rect r) {
  return rectArea(r);
}

static double area(DoubleRect r) {
  return r == null ? 0 : r.w*r.h;
}


static <A> boolean addIfNotNull(Collection<A> l, A a) {
  return a != null && l != null & l.add(a);
}


static <A> void addIfNotNull(MultiSet<A> ms, A a) {
  if (a != null && ms != null) ms.add(a);
}



static <A, B> List<B> lambdaMap(IF1<A, B> f, Iterable<A> l) {
  return map(l, f);
}

static <A, B> List<B> lambdaMap(IF1<A, B> f, A[] l) {
  return map(l, f);
}


static Object get2(Object o, String field1, String field2) {
  return get(get(o, field1), field2);
}


static boolean startsWithAny(String a, Collection<String> b) {
  for (String prefix : unnullForIteration(b))
    if (startsWith(a, prefix))
      return true;
  return false;
}

static boolean startsWithAny(String a, String... b) {
  if (b != null)
    for (String prefix : unnullForIteration(b))
      if (startsWith(a, prefix))
        return true;
  return false;
}


static boolean startsWithAny(String a, Collection<String> b, Matches m) {
  for (String prefix : unnullForIteration(b))
    if (startsWith(a, prefix, m))
      return true;
  return false;
}



static String mcDollar() {
  return mcName() + "$";
}


static String afterDollar(String s) {
  return substring(s, smartIndexOf(s, '$')+1);
}


static String quoteUnlessIdentifierOrInteger(String s) {
  return quoteIfNotIdentifierOrInteger(s);
}


static boolean containsIgnoreCase(Collection<String> l, String s) {
  if (l != null) for (String x : l)
    if (eqic(x, s))
      return true;
  return false;
}

static boolean containsIgnoreCase(String[] l, String s) {
  if (l != null) for (String x : l)
    if (eqic(x, s))
      return true;
  return false;
}

static boolean containsIgnoreCase(String s, char c) {
  return indexOfIgnoreCase(s, String.valueOf(c)) >= 0;
}

static boolean containsIgnoreCase(String a, String b) {
  return indexOfIgnoreCase(a, b) >= 0;
}


static String quickSubstring(String s, int i, int j) {
  if (i >= j) return "";
  return s.substring(i, j);
}


static Class actualMC() {
  return or((Class) realMC(), mc());
}


static <A> TreeMap<String, A> caseInsensitiveMap() {
  return new TreeMap(caseInsensitiveComparator());
}


static Object safeUnstructure(String s) {
  return unstructure(s, true);
}

static Object safeUnstructure(File f) {
  return safeUnstructureGZFile(f);
}


static Class getOuterClass(Class c) { return getOuterClass(c, null); }
static Class getOuterClass(Class c, Object classFinder) { try {
  String s = c.getName();
  int i = s.lastIndexOf('$');
  String name = substring(s, 0, i);
  return classForName(name, classFinder);
} catch (Exception __e) { throw rethrow(__e); } }

static Class getOuterClass(Object o) { return getOuterClass(o, null); }
static Class getOuterClass(Object o, Object classFinder) {
  return getOuterClass(_getClass(o), classFinder);
}


static HashMap<String, Field> instanceFieldsMap(Object o) {
  return (HashMap) getOpt_getFieldMap(o);
}





static Map<Class, Field[]> thisDollarOneFields_cache = newDangerousWeakHashMap();

static Field[] thisDollarOneFields(Class c) {
  synchronized(thisDollarOneFields_cache) {
    Field[] l = thisDollarOneFields_cache.get(c);
    if (l == null)
      thisDollarOneFields_cache.put(c, l = thisDollarOneFields_uncached(c));
    return l;
  }
}

static Field[] thisDollarOneFields_uncached(Class c) {
  List<Field> fields = new ArrayList();
  do {
    for (Field f : c.getDeclaredFields())
      if (f.getName().startsWith("this$"))
        fields.add(makeAccessible(f));
    c = c.getSuperclass();
  } while (c != null);
  return toArray(new Field[l(fields)], fields);
}




static Method fastIntern_method;

static String fastIntern(String s) { try {
  if (s == null) return null;
  if (fastIntern_method == null) {
    fastIntern_method = findMethodNamed(javax(), "internPerProgram");
    if (fastIntern_method == null) upgradeJavaXAndRestart();
  }
    
  return (String) fastIntern_method.invoke(null, s);
} catch (Exception __e) { throw rethrow(__e); } }


static Map<Class, HashMap<String, Method>> callOpt_noArgs_cache = newDangerousWeakHashMap();

static Object callOpt_noArgs(Object o, String method) { try {
  if (o == null) return null;
  if (o instanceof Class)
    return callOpt(o, method); // not optimized
  
  Class c = o.getClass();
  HashMap<String, Method> map;
  synchronized(callOpt_noArgs_cache) {
    map = callOpt_noArgs_cache.get(c);
    if (map == null)
      map = callOpt_noArgs_makeCache(c);
  }

  Method m = map.get(method);
  return m != null ? m.invoke(o) : null;
} catch (Exception __e) { throw rethrow(__e); } }

// used internally - we are in synchronized block
static HashMap<String, Method> callOpt_noArgs_makeCache(Class c) {
  HashMap<String, Method> map = new HashMap();
  Class _c = c;
  do {
    for (Method m : c.getDeclaredMethods())
      if (m.getParameterTypes().length == 0 
        && !reflection_isForbiddenMethod(m)) {
        makeAccessible(m);
        String name = m.getName();
        if (!map.containsKey(name))
          map.put(name, m);
      }
    _c = _c.getSuperclass();
  } while (_c != null);
  callOpt_noArgs_cache.put(c, map);
  return map;
}


static TreeSet<String> caseInsensitiveSet() {
  return caseInsensitiveSet_treeSet();
}

static TreeSet<String> caseInsensitiveSet(Collection<String> c) {
  return caseInsensitiveSet_treeSet(c);
}


// match2 matches multiple "*" (matches a single token) wildcards and zero or one "..." wildcards (matches multiple tokens)

static String[] match2(List<String> pat, List<String> tok) {
  // standard case (no ...)
  int i = pat.indexOf("...");
  if (i < 0) return match2_match(pat, tok);
  
  pat = new ArrayList<String>(pat); // We're modifying it, so copy first
  pat.set(i, "*");
  while (pat.size() < tok.size()) {
    pat.add(i, "*");
    pat.add(i+1, ""); // doesn't matter
  }
  
  return match2_match(pat, tok);
}

static String[] match2_match(List<String> pat, List<String> tok) {
  List<String> result = new ArrayList<String>();
  if (pat.size() != tok.size()) {
    
    return null;
  }
  for (int i = 1; i < pat.size(); i += 2) {
    String p = pat.get(i), t = tok.get(i);
    
    if (eq(p, "*"))
      result.add(t);
    else if (!equalsIgnoreCase(unquote(p), unquote(t))) // bold change - match quoted and unquoted now. TODO: should remove
      return null;
  }
  return result.toArray(new String[result.size()]);
}



static int localYear() {
  return localYear(now());
}

static int localYear(long time) {
  return parseInt(simpleDateFormat_local("yyyy").format(time));
}


static java.text.SimpleDateFormat simpleDateFormat(String format, TimeZone timeZone) {
  java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat(format);
  sdf.setTimeZone(timeZone);
  return sdf;
}


static int localMonth(long time) {
  return parseInt(simpleDateFormat_local("MM").format(time));
}

static int localMonth() {
  return localMonth(now());
}


static int localDayOfMonth(long time) {
  return parseInt(simpleDateFormat_local("dd").format(time));
}

static int localDayOfMonth() {
  return localDayOfMonth(now());
}


static TreeSet<String> litciset(String... items) {
  TreeSet<String> set = caseInsensitiveSet();
  for (String a : items) set.add(a);
  return set;
}


static TreeSet<Symbol> litciset(Symbol... items) {
  TreeSet<Symbol> set = treeSet(); // HashSet would also do, but we might have the return type fixed somewhere, and they might want a NavigableMap.
  for (Symbol a : items) set.add(a);
  return set;
}



static String afterLastSpace(String s) {
  return s == null ? null : substring(s, s.lastIndexOf(' ')+1);
}


static String dropSuffixIgnoreCase(String suffix, String s) {
  return ewic(s, suffix) ? s.substring(0, l(s)-l(suffix)) : s;
}


static boolean isBoxedType(Class type) {
  return type == Boolean.class
    || type == Integer.class
    || type == Long.class
    || type == Float.class
    || type == Short.class
    || type == Character.class
    || type == Byte.class
    || type == Double.class;
}


static boolean isArrayType(Class type) {
  return type != null && type.isArray();
}


static boolean hasThisDollarFields(Object o) {
  Matches m = new Matches();
  for (var f : allFieldObjects_dontMakeAccessible(o))
    if (startsWith(f.getName(), "this$", m) && isInteger(m.rest()))
      return true;
  return false;
}


static boolean hasSingleArgumentConstructor(Class c) {
  if (c != null)
    for (Constructor m : c.getDeclaredConstructors())
      if (l(m.getParameterTypes()) == 1)
        return true;
  return false;
}


static Constructor getDefaultConstructor(Class c) {
  if (c != null)
    for (Constructor m : c.getDeclaredConstructors())
      if (empty(m.getParameterTypes()))
        return m;
  return null;
}


static byte[] boolArrayToBytes(boolean[] a) {
  byte[] b = new byte[(l(a)+7)/8];
  for (int i = 0; i < l(a); i++)
    if (a[i])
      b[i/8] |= 1 << (i & 7);
  return b;
}


static <A, B, C> List<Pair<A, C>> mapPairB(final Object f, Iterable<Pair<A, B>> l) {
  return map(l, new F1<Pair<A, B>, Pair<A, C>>() { public Pair<A, C> get(Pair<A, B> p) { try { 
    return p == null ? null : pair(p.a, (C) callF(f, p.b));
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "p == null ? null : pair(p.a, (C) callF(f, p.b))"; }});
}

static <A, B, C> List<Pair<A, C>> mapPairB(final F1<B, C> f, Iterable<Pair<A, B>> l) {
  return mapPairB((Object) f, l);
}

static <A, B, C> List<Pair<A, C>> mapPairB(final IF1<B, C> f, Iterable<Pair<A, B>> l) {
  return mapPairB((Object) f, l);
}

static <A, B, C> List<Pair<A, C>> mapPairB(Iterable<Pair<A, B>> l, IF1<B, C> f) {
  return mapPairB((Object) f, l);
}

static <A, B, C> Pair<A, C> mapPairB(IF1<B, C> f, Pair<A, B> p) {
  return pairMapB(f, p);
}

static <A, B, C> Pair<A, C> mapPairB(Pair<A, B> p, IF1<B, C> f) {
  return pairMapB(f, p);
}


static <A, B> Map<B, A> mapToKey(Iterable<A> l, IF1<A, B> f) {
  return mapToKeys(l, f);
}



static <A, B> Map<B, A> mapToKey(IF1<A, B> f, Iterable<A> l) {
  return mapToKeys(f, l);
}


static Map<Class, List<String>> getFieldOrder_cache = weakMap();

static List<String> getFieldOrder(Object o) {
  return getFieldOrder(_getClass(o));
}

static List<String> getFieldOrder(Class c) {
  if (c == null) return null;
  return getOrCreate(getFieldOrder_cache, c,
    () -> splitAtSpace(toStringOpt(getOpt(c, "_fieldOrder"))));
}


static long fixTimestamp(long timestamp) {
  return timestamp > now() ? 0 : timestamp;
}


static boolean isConceptList(Object o) {
  if (!(o instanceof List)) return false;
  List l =  (List) o;
  for (Object x : l) if (!(x instanceof Concept)) return false;
  return true;
}


static void dynamicObject_dropRawField(DynamicObject o, Object key) {
  if (o == null) return;
  
  // double sync, but should be OK here because of locking order o > o.fieldValues
  synchronized(o) {
    // can drop the inner synchronization when we migrated all users
    // of fieldValues to synchronizing on the object too
    o.fieldValues = (LinkedHashMap) syncMapRemove_deleteMapIfEmpty((Map) o.fieldValues, key);
  }
}


static boolean isPersistable(Object o) {
  return !isInAnonymousClass(o);
}


static <A, B> List<B> lmap(IF1<A, B> f, Iterable<A> l) {
  return lambdaMap(f, l);
}



static <A, B> List<B> lmap(IF1<A, B> f, A[] l) {
  return lambdaMap(f, l);
}


static String loadGZTextFile(File file) { try {
  if (!file.isFile()) return null;
  ping();
  ByteArrayOutputStream baos = new ByteArrayOutputStream();
   InputStream fis = new FileInputStream(file); try {
  GZIPInputStream gis = newGZIPInputStream(fis);
  byte[] buffer = new byte[1024];
  int len;
  while ((len = gis.read(buffer)) != -1) baos.write(buffer, 0, len);
  baos.close();
  return fromUtf8(baos.toByteArray()); // TODO: use a Reader
} finally { _close(fis); }} catch (Exception __e) { throw rethrow(__e); } }


static Object getTL(Object o, String name) {
  return getThreadLocal(o, name);
}

static <A> A getTL(ThreadLocal<A> tl) {
  return getThreadLocal(tl);
}

static <A> A getTL(ThreadLocal<A> tl, A defaultValue) {
  return getThreadLocal(tl, defaultValue);
}


static Object callJavaX(String method, Object... args) {
  return callOpt(getJavaX(), method, args);
}


static Object costCenter() { return mc(); }


static TimeZone localTimeZone() {
  return getTimeZone(standardTimeZone());
  // TimeZone.getDefault()?
}


static String selectedItem(JList l) {
  return getSelectedItem(l);
}

static String selectedItem(JComboBox cb) {
  return getSelectedItem(cb);
}


static <A, B> A eitherAOpt(Either<A, B> e) {
  return e != null && e.isA() ? e.a() : null;
}


static Set<Thread> evalWithTimeout_inTime = synchroSet();
static Set<Thread> evalWithTimeout_allThreads = newWeakHashSet();

static ThreadLocal<String> evalWithTimeout_threadName = new ThreadLocal();

static <A> Either<A, Thread> evalWithTimeout(double timeoutSeconds, IF0<A> r) {
  return (Either) evalWithTimeout(timeoutSeconds, (Object) r);
}

// Either calculation result or Thread with timed-out computation
// Tries to interrupt thread after timeout
// Errors are rethrown.
static Either<Object, Thread> evalWithTimeout(int timeoutMS, final Object r) {
  final Flag done = new Flag();
  final Flag doneWaiting = new Flag();
  final Var var = new Var();
  final Var<Throwable> error = new Var();
  
  Thread t = newThread(getAndClearThreadLocal(evalWithTimeout_threadName), new Runnable() {  public void run() { try { 
    try {
      try {
        var.set(callF(r));
      } finally {
        evalWithTimeout_allThreads.remove(currentThread());
      }
    } catch (Throwable e) {
      error.set(e);
      if (doneWaiting.isUp())
        printStackTrace_inPossiblyCancelledThread(e);
    } finally {
      done.raise();
    }
  
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "try {\r\n      try {\r\n        var.set(callF(r));\r\n      } finally {\r\n        ev..."; }});
  
  beforeDelegatingToThread(t);
  try {
    startThread(t);
    
    evalWithTimeout_inTime.add(t);
    evalWithTimeout_allThreads.add(t);
    try {
      done.waitUntilUp(timeoutMS);
      doneWaiting.raise();
    } finally {
      evalWithTimeout_inTime.remove(t);
    }
      
    // timeout! cancel/interrupt and return thread object
    if (!done.isUp()) {
      print("Cancelling thread (timeout)");
      cancelAndInterruptThread(t);
      return either2(t);
    }
    
    // thread ended with error
    if (error.get() != null) rethrow(error.get());
    
    // thread ended naturally
    return either1(var.get());
  } finally {
    afterDelegatingToThread(t);
  }
}

static Either<Object, Thread> evalWithTimeout(double timeoutSeconds, final Object r) {
  return evalWithTimeout(toMS_int(timeoutSeconds), r);
}


static String stackTraceForThread(Thread t) {
  return t == null ? null : lines(t.getStackTrace());
}


static int toMS_int(double seconds) {
  return toInt_checked((long) (seconds*1000));
}


static <A> F0<A> if0ToF0(IF0<A> f) {
  return f == null ? null : new F0<A>() { public A get() { try {  return f.get();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return f.get();"; }};
}


static boolean isLocalhost(String ip) {
  return isLoopbackIP(ip) || eqic(ip, "localhost");
}


static int vmPort() {
  return myVMPort();
}


static DialogIO talkToThisVM() {
  return new talkToThisVM_IO();
}

static class talkToThisVM_IO extends DialogIO { 
  List<String> answers = ll(thisVMGreeting());
  
  boolean isLocalConnection() { return true; }
  boolean isStillConnected() { return true; }
  int getPort() { return vmPort(); }
  
  void sendLine(String line) {
    answers.add(or2(sendToThisVM_newThread(line), "?"));
  }
  
  String readLineImpl() { try {
    return popFirst(answers);
  } catch (Exception __e) { throw rethrow(__e); } }
  
  public void close() {}
  Socket getSocket() { return null; }
}


static int indexOfIgnoreCase_manual(String a, String b) {
  return indexOfIgnoreCase_manual(a, b, 0);
}

static int indexOfIgnoreCase_manual(String a, String b, int i) {
  int la = strL(a), lb = strL(b);
  if (la < lb) return -1;
  int n = la-lb;
  
  loop: for (; i <= n; i++) {
    for (int j = 0; j < lb; j++) {
      char c1 = a.charAt(i+j), c2 = b.charAt(j);
      if (!eqic(c1, c2))
        continue loop;
    }
    return i;
  }
  return -1;
}


public static String rtrim(String s) {
  if (s == null) return null;
  int i = s.length();
  while (i > 0 && " \t\r\n".indexOf(s.charAt(i-1)) >= 0)
    --i;
  return i < s.length() ? s.substring(0, i) : s;
}


static Map<String, Integer> newFindBot2_cache = synchroHashMap();
static boolean newFindBot2_verbose = false;

static DialogIO newFindBot2(String name) {
  Integer port = newFindBot2_cache.get(name);
  if (port != null) {
    if (newFindBot2_verbose)
      print("newFindBot2: testing " + name + " => " + port);
    DialogIO io = talkTo(port);
    String q = format("has bot *", name);
    String s = io.ask(q);
    if (match("yes", s)) {
      io = talkToSubBot(name, io);
      call(io, "pushback", "?"); // put some hello string in (yes, this should be improved.)
      return io;
    }
    // bot not there anymore - remove cache entry
    newFindBot2_cache.remove(name);
    if (newFindBot2_verbose)
      print("newFindBot2: dropping " + name + " => " + port);
  }
  
  DialogIO io = findBot(name);
  if (io != null) {
    newFindBot2_cache.put(name, io.getPort());
    if (newFindBot2_verbose)
      print("newFindBot2: remembering " + name + " => " + port);
  }
  return io;
}


static class ScannedBot implements IFieldsToList{
  static final String _fieldOrder = "helloString address";
  String helloString;
  String address;
  ScannedBot() {}
  ScannedBot(String helloString, String address) {
  this.address = address;
  this.helloString = helloString;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + helloString + ", " + address + ")"; }

public boolean equals(Object o) {
if (!(o instanceof ScannedBot)) return false;
    ScannedBot __1 =  (ScannedBot) o;
    return eq(helloString, __1.helloString) && eq(address, __1.address);
}

  public int hashCode() {
    int h = 1660478935;
    h = boostHashCombine(h, _hashCode(helloString));
    h = boostHashCombine(h, _hashCode(address));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {helloString, address}; }
}

static List<ScannedBot> fullBotScan() {
  return fullBotScan("");
}

static List<ScannedBot> fullBotScan(String searchPattern) {
  List<ScannedBot> bots = new ArrayList();
  for (ProgramScan.Program p : quickBotScan()) {
    String botName = firstPartOfHelloString(p.helloString);
    boolean isVM = startsWithIgnoreCase(p.helloString, "This is a JavaX VM.");
    boolean shouldRecurse = swic(botName, "Multi-Port") || isVM;
    
    if (swic(botName, searchPattern)) bots.add(new ScannedBot(botName, "" + p.port));

    if (shouldRecurse) try {
      Map<Number, String> subBots = (Map) unstructure(sendToLocalBotQuietly(p.port, "list bots"));
      for (Number vport : subBots.keySet()) {
        botName = subBots.get(vport);
        if (swic(botName, searchPattern)) 
          bots.add(new ScannedBot(botName, p.port + "/" + vport));
      }
    } catch (Exception e) { e.printStackTrace(); }
  }
  return bots;
}


static Object unstructure_startingAtIndex(String s, int i) {
  return unstructure_tok(javaTokC_noMLS_iterator(s, i), false, null);
}


static boolean isSubclassOf(Class a, Class b) {
  return isSubclass(a, b);
}


static File copyFileToClipboard(File f) {
  if (f == null) return null;
  Toolkit.getDefaultToolkit().getSystemClipboard().setContents(
    new FileTransferable(f), null);
  vmBus_send("newClipboardContents", f);
  return f;
}


static void consoleClearInput() {
  consoleSetInput("");
}


static <A> List<A> wrapAsList(A[] a) {
  return wrapArrayAsList(a);
}


static Set emptySet() {
  return new HashSet();
}


static <A> TreeSet<A> asTreeSet(Collection<A> set) {
  return set == null ? null : set instanceof TreeSet ? (TreeSet) set : new TreeSet(set);
}


static int hashCode(Object a) {
  return a == null ? 0 : a.hashCode();
}

static int hashCode(long l) {
  return Long.hashCode(l);
}


static String hopeningTag(String tag, Map params) {
  return hopeningTag(tag, mapToParams(params));
}

static String hopeningTag(String tag, Object... params) {
  StringBuilder buf = new StringBuilder();
  buf.append("<" + tag);
  params = unrollParams(params);
  for (int i = 0; i < l(params); i += 2) {
    String name = (String) get(params, i);
    Object val = get(params, i+1);
    if (nempty(name) && val != null) {
      if (eqOneOf(val, html_valueLessParam(), true))
        buf.append(" " + name);
      else {
        String s = str(val);
        if (!empty(s))
          buf.append(" " + name + "=" + htmlQuote(s));
      }
    }
  }
  buf.append(">");
  return str(buf);
}


static boolean possibleGlobalID(String s) {
  return l(s) == globalIDLength() && allLowerCaseCharacters(s);
}




static JPanel marginPanel() {
  return jtransparent(borderLayoutPanel());
}


static boolean hasBot(String searchPattern) { try {
  DialogIO io = findBot(searchPattern);
  if (io != null) {
    io.close();
    return true;
  } else
    return false;
} catch (Exception __e) { throw rethrow(__e); } }


static boolean isOK(String s) {
  s = trim(s);
  return swic(s, "ok ") || eqic(s, "ok") || matchStart("ok", s);
}


static String sendOpt(String bot, String text, Object... args) {
  return sendToLocalBotOpt(bot, text, args);
}


static boolean isMainProgram() {
  return creator() == null;
}


static void cleanKill() {
  cleanKillVM();
}


static Android3 methodsBot2(String name, final Object receiver, final List<String> exposedMethods) {
  return methodsBot2(name, receiver, exposedMethods, null);
}

static Android3 methodsBot2(String name, final Object receiver, final List<String> exposedMethods, final Lock lock) {
  Android3 android = new Android3();
  android.greeting = name;
  android.console = false;
  android.responder = new Responder() {
    String answer(String s, List<String> history) {
      return exposeMethods2(receiver, s, exposedMethods, lock);
    }
  };
  return makeBot(android);
}


static List<String> db_standardExposedMethods_list = ll("xlist", "xnew", "xset", "xdelete", "xget", "xclass", "xfullgrab", "xshutdown", "xchangeCount", "xcount");

static List<String> db_standardExposedMethods() {
  return db_standardExposedMethods_list;
}


static Matcher regexp(String pat, String s) {
  return regexp(compileRegexp(pat), unnull(s));
}

static Matcher regexp(java.util.regex.Pattern pat, String s) {
  return pat.matcher(unnull(s));
}

static java.util.regex.Pattern regexp(String pat) {
  return compileRegexp(pat);
}


static List<File> listFilesNotDirs(String dir) {
  return listFilesOnly(dir);
}

static List<File> listFilesNotDirs(File... dirs) {
  return listFilesOnly(dirs);
}


static int matcherInt(Matcher m, int i) {
  return parseInt(m.group(i));
}


// month = 1 to 12
static long timestampFromYMDHM(int y, int m, int d, int h, int minutes) {
  return new GregorianCalendar(y, m-1, d, h, minutes).getTimeInMillis();
}


static <A> List<A> sortByMap_inPlace(List<A> l, Map<A, ?> map) {
  sort(l, mapComparator(map));
  return l;
}


// age = age in days, lastAge = age of last (more recent) file kept (also in days)
static boolean defaultAgeBasedBackupRetentionStrategy_shouldKeep(double age, double lastAge) {
  if (age <= 1/12.0) return true; // keep all backups within last hour
  if (age <= 0.5 && age >= lastAge+1/12.0) return true; // keep hourly backups within last 12 hours
  if (age <= 7 && age >= lastAge+1) return true; // keep every daily backup from this week
  if (age <= 28 && age >= lastAge+7) return true; // weekly backups for 3 more weeks
  if (age >= lastAge+365.0/12) return true; // after 4 weeks, switch to monthly (roundabout)
  return false;
}


// changes & returns canvas
static BufferedImage drawImage(BufferedImage canvas, Image img, Pt p) {
  return drawImageOnImage(img, canvas, p.x, p.y);
}

static void drawImage(BufferedImage g, Image img) { drawImage(graphics(g), img); }

static void drawImage(Graphics2D g, Image img) {
  drawImage(g, img, 0, 0);
}

static void drawImage(Graphics2D g, Image img, Pt p) {
  drawImage(g, img, p.x, p.y);
}

static void drawImage(Graphics2D g, Image img, int x, int y) {
  { if (g != null) g.drawImage(img, x, y, null); }
}


static boolean isImageFileName(String s) {
  return eqicOneOf(fileExtension(s), ".png", ".jpg", ".jpeg", ".gif");
}


// This is for main classes that are all static.
// (We don't go to base classes.)
static Set<String> listFields(Object c) {
  TreeSet<String> fields = new TreeSet();
  for (Field f : _getClass(c).getDeclaredFields())
    fields.add(f.getName());
  return fields;
}


static boolean eqicOrEq(Object a, Object b) {
  return a instanceof String && b instanceof String ? eqic((String) a, (String) b) : eq(a, b);
}


static void setMeta(IMeta o, Object key, Object value) {
  metaMapPut(o, key, value);
}

static void setMeta(Object o, Object key, Object value) {
  metaMapPut(o, key, value);
}


static Border jCompoundBorder(Border inner, Border outer) {
  if (inner == null) return outer;
  if (outer == null) return inner;
  return BorderFactory.createCompoundBorder(inner, outer);
}


static void setMenuBar(final JMenuBar mb, final RootPaneContainer f) {
  { swing(() -> { 
    call(f, "setJMenuBar", mb);
    revalidate((Component) f);
  }); }
}

static void setMenuBar(RootPaneContainer f, JMenuBar mb) {
  setMenuBar(mb, f);
}


static boolean zipFileContains(File inZip, String fileName) { try {
  ZipFile zip = new ZipFile(inZip);
  try {
    return zipFileContains(zip, fileName);
  } finally {
    zip.close();
  }
} catch (Exception __e) { throw rethrow(__e); } }
    
static boolean zipFileContains(ZipFile zip, String fileName) { try {
  return zip.getEntry(fileName) != null;
} catch (Exception __e) { throw rethrow(__e); } }


static File envJavaHome() {
  return newFile(System.getProperty("java.home"));
}


static boolean isInstanceOf(Object o, Class type) {
  return type.isInstance(o);
}


static JTextField jTextField() {
  return jTextField("");
}

static JTextField jTextField(final String text) {
  return swing(new F0<JTextField>() { public JTextField get() { try { 
    JTextField tf = new JTextField(unnull(text));
    standardTextFieldPopupMenu(tf);
    jenableUndoRedo(tf);
    tf.selectAll();
    return tf;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "JTextField tf = new JTextField(unnull(text));\r\n    standardTextFieldPopupMenu..."; }});
}

static JTextField jTextField(Object o) {
  return jTextField(strOrEmpty(o));
}



static JDesktopPane mainDesktopPane_value;

static JDesktopPane mainDesktopPane() {
  return mainDesktopPane_value;
}


static JPanel showInternalFrameFormTitled(final JDesktopPane desktop, final String title, final Object... _parts) {
  JPanel panel = showForm_makePanel(true, _parts);
  showForm_makeInternalFrame(desktop, title, panel);
  return panel;
}


static  IVF2<String, JPanel> showForm_makeFrame;
static void showForm_makeFrame(String title, JPanel panel) { if (showForm_makeFrame != null) showForm_makeFrame.get(title, panel); else showForm_makeFrame_base(title, panel); }
final static void showForm_makeFrame_fallback(IVF2<String, JPanel> _f, String title, JPanel panel) { if (_f != null) _f.get(title, panel); else showForm_makeFrame_base(title, panel); }
static void showForm_makeFrame_base(String title, JPanel panel) {
  handleEscapeKey(minFrameWidth(showPackedFrame(title, withMargin(panel)), 400));
}


static List<JComponent> showForm_arrange1(List<List<JComponent>> l) {
  int minW = showForm_leftWidth(l);

  List<JComponent> out = new ArrayList();
  for (List<JComponent> row : l)
    out.add(westAndCenter(withRightMargin(showForm_gapBetweenColumns, jMinWidthAtLeast(minW, first(row))), second(row)));
  return out;
}


static List<List<JComponent>> showForm_makeComponents(final Boolean internalFrame, Object... _parts) {
  List<List<JComponent>> l = new ArrayList();
  List parts = asList(_parts);
  JButton submitButton = null;
  for (int i = 0; i < l(parts); i++) {
    final Object o = parts.get(i), next = get(parts, i+1);
    if (o instanceof String && next instanceof Component)
      setComponentID((Component) next, (String) o);
    
    if (o instanceof Component || o instanceof String || next instanceof Component) { // smartAdd accepts strings
      l.add(mapLL(__63 -> wrapForSmartAdd_jComponent(__63), 
          o == null ? new JPanel()
        : o instanceof String ? humanizeFormLabel((String) o)
        : o, next));
      if (next instanceof JButton && submitButton == null)
        submitButton = (JButton) next;
      i++;
    } else if (isRunnable(o))
      l.add(mapLL(__64 -> wrapForSmartAdd_jComponent(__64), null, submitButton = jbutton(showFormSubmitButtonName(), new Runnable() {  public void run() { try { 
        Object result = call(o);
        print("Result of form runnable: " + result + ". Button:  " + heldInstance(JButton.class));
        if (neq(Boolean.FALSE, result)) {
          if (isTrue(internalFrame))
            disposeInternalFrame(heldInstance(JButton.class));
          else if (isFalse(internalFrame))
            disposeFrame(heldInstance(JButton.class));
        }
      
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "Object result = call(o);\r\n        print(\"Result of form runnable: \" + result ..."; }})));
    else print("showForm: Unknown element type: " + getClassName(o));
  }
  if (submitButton != null) {
    final JButton _submitButton = submitButton;
    onEnterInAllTextFields(concatLists(l), new Runnable() {  public void run() { try {  clickButton(_submitButton) ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "clickButton(_submitButton)"; }});
  }
  
  // massage labels
  for (List<JComponent> row : l) {
    JComponent left = first(row);
    if (left instanceof JLabel) makeBold((JLabel) left).setVerticalAlignment(JLabel.TOP);
  }
  
  return l;
}


static int vstackWithSpacing_default = 10;

static JPanel vstackWithSpacing(final List parts) {
  return vstackWithSpacing(parts, vstackWithSpacing_default);
}

static JPanel vstackWithSpacing(final List parts, final int spacing) {
  return swing(new F0<JPanel>() { public JPanel get() { try { 
    JPanel panel = new JPanel(new GridBagLayout());
    GridBagConstraints gbc = new GridBagConstraints();
    gbc.weightx = 1;
    gbc.fill = GridBagConstraints.HORIZONTAL;
    gbc.gridwidth = GridBagConstraints.REMAINDER;
    gbc.insets = new Insets(spacing/2, 0, (spacing+1)/2, 0); // well...
    smartAddWithLayout(panel, gbc, toObjectArray(nonNulls(parts)));
    //gbc = (GridBagConstraints) gbc.clone();
    //gbc.fill = GridBagConstraints.BOTH;
    gbc.weighty = 1;
    gbc.insets = new Insets(0, 0, 0, 0);
    panel.add(jrigid(), gbc);
    return panel;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "JPanel panel = new JPanel(new GridBagLayout);\r\n    new GridBagConstraints gbc..."; }});
}

static JPanel vstackWithSpacing(Component... parts) {
  return vstackWithSpacing(asList(parts), vstackWithSpacing_default);
}

static JPanel vstackWithSpacing(int spacing, Component... parts) {
  return vstackWithSpacing(asList(parts), spacing);
}


static JButton renameButton(JComponent c, String name) {
  JButton b = first(childrenOfType(c, JButton.class));
  if (b != null)
    b.setText(name);
  return b;
}

static JButton renameButton(JComponent c, String oldName, String newName) {
  JButton b = findButton(c, oldName);
  if (b != null) b.setText(newName);
  return b;
}


static String showFormSubmitButtonName() {
  return "Submit";
}


static List collect(Iterable c, String field) {
  return collectField(c, field);
}

static List collect(String field, Iterable c) {
  return collectField(c, field);
}

/*ifclass Concept
static L collect(Class c, S field) {
  ret collect(list(c), field);
}
endif
TODO: make translator ignore stuff in ifclass until resolved
*/


static void setCaretPosition(final JTextComponent c, final int pos) {
  if (c != null) { swing(() -> { 
    try {
      int _pos = max(0, min(l(c.getText()), pos));
      c.setCaretPosition(_pos);
    } catch (Throwable __e) { printStackTrace(__e); }
  }); }
}


static int textAreaTextLength(JTextComponent ta) {
  return l(getText(ta));
}


static Rect rectFromPoints(int x1, int y1, int x2, int y2) {
  return pointsRect(x1, y1, x2, y2);
}

static Rect rectFromPoints(Pt a, Pt b) {
  return pointsRect(a.x, a.y, b.x, b.y);
}


  static byte[] base64decode(String s) {
    byte[] alphaToInt = base64decode_base64toint;
    int sLen = s.length();
    int numGroups = sLen/4;
    if (4*numGroups != sLen)
      throw new IllegalArgumentException(
        "String length must be a multiple of four.");
    int missingBytesInLastGroup = 0;
    int numFullGroups = numGroups;
    if (sLen != 0) {
      if (s.charAt(sLen-1) == '=') {
        missingBytesInLastGroup++;
        numFullGroups--;
      }
      if (s.charAt(sLen-2) == '=')
        missingBytesInLastGroup++;
    }
    byte[] result = new byte[3*numGroups - missingBytesInLastGroup];

    // Translate all full groups from base64 to byte array elements
    int inCursor = 0, outCursor = 0;
    for (int i=0; i<numFullGroups; i++) {
      int ch0 = base64decode_base64toint(s.charAt(inCursor++), alphaToInt);
      int ch1 = base64decode_base64toint(s.charAt(inCursor++), alphaToInt);
      int ch2 = base64decode_base64toint(s.charAt(inCursor++), alphaToInt);
      int ch3 = base64decode_base64toint(s.charAt(inCursor++), alphaToInt);
      result[outCursor++] = (byte) ((ch0 << 2) | (ch1 >> 4));
      result[outCursor++] = (byte) ((ch1 << 4) | (ch2 >> 2));
      result[outCursor++] = (byte) ((ch2 << 6) | ch3);
    }

    // Translate partial group, if present
    if (missingBytesInLastGroup != 0) {
      int ch0 = base64decode_base64toint(s.charAt(inCursor++), alphaToInt);
      int ch1 = base64decode_base64toint(s.charAt(inCursor++), alphaToInt);
      result[outCursor++] = (byte) ((ch0 << 2) | (ch1 >> 4));

      if (missingBytesInLastGroup == 1) {
        int ch2 = base64decode_base64toint(s.charAt(inCursor++), alphaToInt);
        result[outCursor++] = (byte) ((ch1 << 4) | (ch2 >> 2));
      }
    }
    // assert inCursor == s.length()-missingBytesInLastGroup;
    // assert outCursor == result.length;
    return result;
  }

  static int base64decode_base64toint(char c, byte[] alphaToInt) {
    int result = alphaToInt[c];
    if (result < 0)
      throw new IllegalArgumentException("Illegal character " + c);
    return result;
  }

  static final byte base64decode_base64toint[] = {
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
    -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54,
    55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4,
    5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
    24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34,
    35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
  };




static <T> void sort(T[] a, Comparator<? super T> c) {
  if (a != null) Arrays.sort(a, c);
}

static <T> void sort(T[] a) {
  if (a != null) Arrays.sort(a);
}

static void sort(int[] a) { if (a != null) Arrays.sort(a); }

static <T> void sort(List<T> a, Comparator<? super T> c) {
  if (a != null) Collections.sort(a, c);
}

static void sort(List a) {
  if (a != null) Collections.sort(a);
}


static Comparator makeComparator(final Object f) {
  if (f instanceof Comparator) return (Comparator) f;
  return new Comparator() {
    public int compare(Object a, Object b) {
      return (Integer) callF(f, a, b);
    }
  };
}



static List<String> allMethodNames(Object o) {
  Class c = _getClass(o);
  TreeSet<String> names = new TreeSet();
  while (c != null) {
    for (Method m : c.getDeclaredMethods())
      names.add(m.getName());
    c = c.getSuperclass();
  }
  return asList(names);
}


static Throwable printStackTrace2(Throwable e) {
  // we go to system.out now - system.err is nonsense
  print(getStackTrace2(e));
  return e;
}

static void printStackTrace2() {
  printStackTrace2(new Throwable());
}

static void printStackTrace2(String msg) {
  printStackTrace2(new Throwable(msg));
}



static Throwable innerException(Throwable e) {
  return getInnerException(e);
}


static boolean neqic(String a, String b) {
  return !eqic(a, b);
}

static boolean neqic(char a, char b) {
  return !eqic(a, b);
}


static boolean containsNulls(Collection c) {
  return contains(c, null);
}


static Map similarEmptyMap(Map m) {
  if (m instanceof TreeMap) return new TreeMap(((TreeMap) m).comparator());
  if (m instanceof LinkedHashMap) return new LinkedHashMap();
  
  // default to a hash map
  return new HashMap();
}

static Map similarEmptyMap(Iterable m) {
  if (m instanceof TreeSet) return new TreeMap(((TreeSet) m).comparator());
  if (m instanceof LinkedHashSet) return new LinkedHashMap();
  
  return new HashMap();
}


static class mapI_if1_It<A, B> extends IterableIterator<B> {
  IF1<A, B> f;
  Iterator<A> i;
  
  mapI_if1_It() {}
  mapI_if1_It(IF1<A, B> f, Iterator<A> i) {
  this.i = i;
  this.f = f;}
  
  public boolean hasNext() {
    return i.hasNext();
  }
  
  public B next() {
    return f.get(i.next());
  }
  
  public String toString() {
    return formatFunctionCall("mapI_if1", f, i);
  }
}

static <A, B> IterableIterator<B> mapI_if1(IF1<A, B> f, Iterable<A> i) {
  return new mapI_if1_It(f, i.iterator());
}



static <A, B> IterableIterator<B> mapI_if1(Iterable<A> i, IF1<A, B> f) {
  return mapI_if1(f, i);
}


static Rect boundsRect(Component c) {
  return toRect(getBounds(c));
}


static int rectArea(Rect r) {
  return r == null ? 0 : r.w*r.h;
}


static String mcName() {
  return mc().getName();
}


// returns l(s) if not found
static int smartIndexOf(String s, String sub, int i) {
  if (s == null) return 0;
  i = s.indexOf(sub, min(i, l(s)));
  return i >= 0 ? i : l(s);
}

static int smartIndexOf(String s, int i, char c) {
  return smartIndexOf(s, c, i);
}

static int smartIndexOf(String s, char c, int i) {
  if (s == null) return 0;
  i = s.indexOf(c, min(i, l(s)));
  return i >= 0 ? i : l(s);
}

static int smartIndexOf(String s, String sub) {
  return smartIndexOf(s, sub, 0);
}

static int smartIndexOf(String s, char c) {
  return smartIndexOf(s, c, 0);
}

static <A> int smartIndexOf(List<A> l, A sub) {
  return smartIndexOf(l, sub, 0);
}

static <A> int smartIndexOf(List<A> l, int start, A sub) {
  return smartIndexOf(l, sub, start);
}

static <A> int smartIndexOf(List<A> l, A sub, int start) {
  int i = indexOf(l, sub, start);
  return i < 0 ? l(l) : i;
}


static String quoteIfNotIdentifierOrInteger(String s) {
  if (s == null) return null;
  return isJavaIdentifier(s) || isInteger(s) ? s : quote(s);
}


static Object realMC() {
  return getThreadLocal(realMC_tl());
}


static Comparator<String> caseInsensitiveComparator() {
  
  
  return betterCIComparator();
  
}


static Object safeUnstructureGZFile(File f) { try {
  if (!fileExists(f)) return null;
  BufferedReader reader = utf8BufferedReader(gzInputStream(f));
  return unstructure_tok(javaTokC_noMLS_onReader(reader), true, null);
} catch (Exception __e) { throw rethrow(__e); } }


static void upgradeJavaXAndRestart() {
  
    run("#1001639");
    restart();
    sleep();
  
  
}


static TreeSet<String> caseInsensitiveSet_treeSet() {
  return new TreeSet(caseInsensitiveComparator());
}

static TreeSet<String> caseInsensitiveSet_treeSet(Collection<String> c) {
  return toCaseInsensitiveSet_treeSet(c);
}


static boolean equalsIgnoreCase(String a, String b) {
  return eqic(a, b);
}

static boolean equalsIgnoreCase(char a, char b) {
  return eqic(a, b);
}


static <A> TreeSet<A> treeSet() {
  return new TreeSet();
}


static List<Field> allFieldObjects_dontMakeAccessible(Object o) {
  List<Field> fields = new ArrayList();
  Class _c = _getClass(o);
  do {
    addAll(fields, _c.getDeclaredFields());
    _c = _c.getSuperclass();
  } while (_c != null);
  return fields;
}


static Pair pairMapB(Object f, Pair p) {
  return p == null ? null : pair(p.a, callF(f, p.b));
}

static <A, B, C> Pair<A, C> pairMapB(IF1<B, C> f, Pair<A, B> p) {
  return p == null ? null : pair(p.a, f.get(p.b));
}

static Pair pairMapB(Pair p, Object f) {
  return pairMap(f, p);
}



static <A, B> Map<B, A> mapToKeys(Iterable<A> l, IF1<A, B> f) {
  if (l == null) return null;
  HashMap<B, A> map = new HashMap();
  for (A a : l)
    map.put(f.get(a), a);
  return map;
}

static <A, B> Map<B, A> mapToKeys(IF1<A, B> f, A[] l) { return mapToKeys(f, asList(l)); }
static <A, B> Map<B, A> mapToKeys(IF1<A, B> f, Iterable<A> l) {
  return mapToKeys(l, f);
}


static <A, B> Map<A, B> weakMap() {
  return newWeakHashMap();
}


// allows null keys but not null values

static <A, B> B getOrCreate(Map<A, B> map, A key, Class<? extends B> c) { try {
  B b = map.get(key);
  if (b == null)
    map.put(key, b = c.newInstance());
  return b;
} catch (Exception __e) { throw rethrow(__e); } }

// f : func -> B
static <A, B> B getOrCreate(Map<A, B> map, A key, Object f) { try {
  B b = map.get(key);
  if (b == null)
    map.put(key, b = (B) callF(f));
  return b;
} catch (Exception __e) { throw rethrow(__e); } }

static <A, B> B getOrCreate(IF0<B> f, Map<A, B> map, A key) {
  return getOrCreate(map, key, f);
}

static <A, B> B getOrCreate(Map<A, B> map, A key, IF0<B> f) {
  B b = map.get(key);
  if (b == null)
    map.put(key, b = f.get());
  return b;
}

static <A, B> B getOrCreate(Class<? extends B> c, Map<A, B> map, A key) {
  return getOrCreate(map, key, c);
}


static List<String> splitAtSpace(String s) {
  return empty(s) ? emptyList() : asList(s.split("\\s+"));
}


static String toStringOpt(Object o) {
  return o instanceof String ? ((String) o) : null;
}


static <A, B, C extends Map<A, B>> C syncMapRemove_deleteMapIfEmpty(C map, A key) {
  if (map != null && key != null)
    synchronized(collectionMutex(map)) {
      map.remove(key);
      if (map.isEmpty())
        return null;
    }
  return map;
}


static boolean isInAnonymousClass(Object o) {
  if (o == null) return false;
  return isAnonymousClassName(className(o));
}


static String fromUtf8(byte[] bytes) { try {
  return bytes == null ? null : new String(bytes, utf8charset());
} catch (Exception __e) { throw rethrow(__e); } }




static TimeZone getTimeZone(String name) {
  return TimeZone.getTimeZone(name);
}


static String standardTimeZone_name = "Europe/Berlin";

static String standardTimeZone() {
  return standardTimeZone_name;
}


static <A> Set<A> synchroSet() {
  return synchroHashSet();
}

static <A> Set<A> synchroSet(Set<A> set) {
  return Collections.synchronizedSet(set);
}


static void printStackTrace_inPossiblyCancelledThread(Throwable e) {
   AutoCloseable __1 = tempUncancelThread(); try {
  System.out.println(getStackTrace_noRecord(e));
} finally { _close(__1); }}


static List beforeDelegatingToThread_operations = synchroList();

static void beforeDelegatingToThread(Thread t) {
  for (Object op : cloneList(beforeDelegatingToThread_operations))
    pcallF(op, t);
}

static void beforeDelegatingToThread_do(Object f) {
  setAdd(beforeDelegatingToThread_operations, f);
}


static <A, B> Either<A, B> either2(B b) {
  return new Either(2, b);
}


static <A, B> Either<A, B> either1(A a) {
  return new Either(1, a);
}


static List afterDelegatingToThread_operations = synchroList();

static void afterDelegatingToThread(Thread t) {
  for (Object op : cloneList(afterDelegatingToThread_operations))
    pcallF(op, t);
}

static void afterDelegatingToThread_do(Object f) {
  setAdd(afterDelegatingToThread_operations, f);
}


static int toInt_checked(long l) {
  if (l != (int) l) throw fail("Too large for int: " + l);
  return (int) l;
}


static boolean isLoopbackIP(String ip) {
  return eq(ip, "127.0.0.1");
}


static int myVMPort() {
  List records =  (List) (get(getJavaX(), "record_list"));
  Object android = last(records);
  return or0((Integer) get(android, "port"));
}


static String thisVMGreeting() {
  List record_list =  (List) (get(getJavaX(), "record_list"));
  Object android = first(record_list); // Should be of class Android3
  return getString(android, "greeting");
}


static String sendToThisVM_newThread(String s, Object... args) {
  final String _s = format(s, args);
  try {
    return (String) evalInNewThread(new F0<Object>() { public Object get() { try { 
      return callStaticAnswerMethod(getJavaX(), _s);
     } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return callStaticAnswerMethod(getJavaX(), _s);"; }});
  } catch (Throwable e) {
    e = getInnerException(e);
    printStackTrace(e);
    return str(e);
  }
}


static <A> A popFirst(List<A> l) {
  if (empty(l)) return null;
  A a = first(l);
  l.remove(0);
  return a;
}

static <A> A popFirst(Collection<A> l) {
  if (empty(l)) return null;
  A a = first(l);
  l.remove(a);
  return a;
}

static <A, B> Pair<A, B> popFirst(Map<A, B> map) {
  if (map == null) return null;
  var it = map.entrySet().iterator();
  if (!it.hasNext()) return null;
  var p = mapEntryToPair(it.next());
  it.remove();
  return p;
}

static <A> List<A> popFirst(int n, List<A> l) {
  List<A> part = cloneSubList(l, 0, n);
  removeSubList(l, 0, n);
  return part;
}

static <A> AppendableChain<A> popFirst(AppendableChain<A> a) {
  return a == null ? null : a.popFirst();
}


static boolean match(String pat, String s) {
  return match3(pat, s);
}

static boolean match(String pat, String s, Matches matches) {
  return match3(pat, s, matches);
}

static boolean match(String pat, List<String> toks, Matches matches) {
  return match3(pat, toks, matches);
}


static boolean isSubclass(Class a, Class b) {
  return a != null && b != null && b.isAssignableFrom(a);
}


static void consoleSetInput(final String text) {
  
    if (headless()) return;
    setTextAndSelectAll(consoleInputField(), text);
    focusConsole();
  
}


static <A> List<A> wrapArrayAsList(A[] a) {
  return a == null ? null : Arrays.asList(a);
}


static Object[] mapToParams(Map map) {
  return mapToObjectArray(map);
}


static Object[] unrollParams(Object[] params) {
  if (l(params) == 1 && params[0] instanceof Map)
    return mapToParams((Map) params[0]);
  return params;
}


static Object html_valueLessParam_cache;
static Object html_valueLessParam() { if (html_valueLessParam_cache == null) html_valueLessParam_cache = html_valueLessParam_load(); return html_valueLessParam_cache;}

static Object html_valueLessParam_load() {
  return new Object();
}


static String htmlQuote(String s) {
  return "\"" + htmlencode_forParams(s) + "\"";
}


static int globalIDLength() {
  return 16;
}


static boolean allLowerCaseCharacters(String s) {
  for (int i = 0; i < l(s); i++)
    if (Character.getType(s.charAt(i)) != Character.LOWERCASE_LETTER) return false;
  return true;
}




static <A extends JComponent> A jtransparent(final A a) {
  { swing(() -> {  a.setOpaque(false); }); }
  return a;
}


static JPanel borderLayoutPanel() {
  return jpanel(new BorderLayout());
}


static boolean matchStart(String pat, String s) {
  return matchStart(pat, s, null);
}

// matches are as you expect, plus an extra item for the rest string
static boolean matchStart(String pat, String s, Matches matches) {
  if (s == null) return false;
  return matchStart(pat, parse3_cachedInput(s), matches);
}
  
static boolean matchStart(String pat, List<String> toks, Matches matches) {
  if (toks == null) return false;
  List<String> tokpat = parse3_cachedPattern(pat);
  if (toks.size() < tokpat.size()) return false;
  String[] m = match2(tokpat, toks.subList(0, tokpat.size()));
  //print(structure(tokpat) + " on " + structure(toks) + " => " + structure(m));
  if (m == null) return false;
  if (matches != null) {
    matches.m = new String[m.length+1];
    arraycopy(m, matches.m);
    matches.m[m.length] = joinSubList(toks, tokpat.size(), toks.size()); // for Matches.rest()
  }
  return true;
}


static String sendToLocalBotOpt(String bot, String text, Object... args) {
  if (bot == null) return null;
  text = format(text, args);
   DialogIO channel = findBot(bot); try {
  if (channel == null) {
    print(quote(bot) + " not found, skipping send: " + quote(text));
    return null;
  }
  try {
    channel.readLine();
    print(shorten(bot + "> " + text, 200));
    channel.sendLine(text);
    String s = channel.readLine();
    print(shorten(bot + "< " + s, 200));
    return s;
  } catch (Throwable e) {
    e.printStackTrace();
    return null;
  }
} finally { _close(channel); }}


static WeakReference<Object> creator_class;

static Object creator() {
  return creator_class == null ? null : creator_class.get();
}


static boolean exposeMethods2_debug = false;

static String exposeMethods2(Object receiver, String s, List<String> methodNames) {
  return exposeMethods2(receiver, s, methodNames, null);
}

static String exposeMethods2(Object receiver, String s, List<String> methodNames,
  Lock lock) {
  Matches m = new Matches();
  if (exposeMethods2_debug) print("Received: " + s);
  if (match("call *", s, m)) {
    List l;
    if (isIdentifier(m.unq(0)))
      l = ll(m.unq(0));
    else
      l = (List) unstructure(m.unq(0)); // we used to have safeUnstructure here
    String method = getString(l, 0);
    if (!contains(methodNames, method))
      throw fail("Method not allowed: " + method);
    if (lock != null) lock.lock();
    try {
      if (exposeMethods2_debug) print("Calling: " + method);
      Object o = call(receiver, method, asObjectArray(subList(l, 1)));
      if (exposeMethods2_debug) print("Got: " + getClassName(o));
      return ok2(structure(o));
    } finally {
      if (lock != null) lock.unlock();
    }
  }
  if (match("list methods", s))
    return ok2(structure(methodNames));
  return null;
}


static int makeBot(String greeting) {
  return makeAndroid3(greeting).port;
}

static Android3 makeBot(Android3 a) {
  makeAndroid3(a);
  return a;
}

static Android3 makeBot(String greeting, Object responder) {
  Android3 a = new Android3(greeting);
  a.responder = makeResponder(responder);
  makeBot(a);
  return a;
}

static Android3 makeBot() {
  return makeAndroid3(defaultBotName());
}


static List<File> listFilesOnly(String dir) {
  return listFilesOnly(new File(dir));
}

static List<File> listFilesOnly(File... dirs) {
  return concatMap(dir -> listFilesWithSuffix("", dir), dirs);
}


static <A, B> Comparator<A> mapComparator(final Map<A, B> map) {
  return new Comparator<A>() {
    public int compare(A a, A b) {
      return cmp(map.get(a), map.get(b));
    }
  };
}


// changes & returns canvas
static BufferedImage drawImageOnImage(Image img, BufferedImage canvas, int x, int y) {
  createGraphics(canvas).drawImage(img, x, y, null);
  return canvas;
}

static BufferedImage drawImageOnImage(Image img, BufferedImage canvas) {
  return drawImageOnImage(img, canvas, 0, 0);
}





static Graphics2D graphics(BufferedImage img) {
  return imageGraphics(img);
}


static JTextField standardTextFieldPopupMenu(final JTextField tf) {
  final WeakReference<JTextField> ref = weakRef(tf);
  componentPopupMenuItem(tf, "Copy text to clipboard", new Runnable() {  public void run() { try { 
    copyTextToClipboard(ref.get().getText())
  ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "copyTextToClipboard(ref.get().getText())"; }});
  componentPopupMenuItem(tf, "Paste", new Runnable() {  public void run() { try {  ref.get().paste() ;
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "ref.get().paste()"; }});
  return tf;
}


static String strOrEmpty(Object o) {
  return o == null ? "" : str(o);
}


static JInternalFrame showForm_makeInternalFrame(JDesktopPane desktop, String title, JPanel panel) {
  JInternalFrame f = addInternalFrame(desktop, title, withMargin(panel));
  minInternalFrameWidth(f, 400);
  packInternalFrameVertically(f);
  centerInternalFrame(f);
  // TODO: handleEscapeKey(f);
  return f;
}


static JFrame handleEscapeKey(final JFrame frame) {
  KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
  frame.getRootPane().registerKeyboardAction(new ActionListener() {
    public void actionPerformed(ActionEvent actionEvent) {
      frame.dispose();
    }
  }, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);
  return frame;
}



static JFrame minFrameWidth(JFrame frame, int w) {
  if (frame != null && frame.getWidth() < w)
    frame.setSize(w, frame.getHeight());
  return frame;
}

static JFrame minFrameWidth(int w, JFrame frame) {
  return minFrameWidth(frame, w);
}



static JFrame showPackedFrame(String title, Component contents) {
  return packFrame(showFrame(title, contents));
}

static JFrame showPackedFrame(Component contents) {
  return packFrame(showFrame(contents));
}


static int showForm_leftWidth_safetyMargin = 10;

static int showForm_leftWidth(List<List<JComponent>> l) {
  forEachLevel2(l, x -> vmBus_send("updateLayoutNow", x));
  int minW = 0;
  for (List<JComponent> row : l)
    minW = max(minW, getMinimumSize(first(row)).width);
  return minW + or((Integer) vmBus_query("formSafetyMargin"), showForm_leftWidth_safetyMargin);
}


static <A extends Component> A jMinWidthAtLeast(int w, final A c) {
  if (c == null) return null;
  return swing(new F0<A>() { public A get() { try { 
    Dimension size = c.getMinimumSize();
    Dimension d = new Dimension(max(w, size.width), size.height);
    c.setMinimumSize(d);
    return jPreferWidth(d.width, c);
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "Dimension size = c.getMinimumSize();\r\n    Dimension d = new Dimension(max(w, ..."; }});
}


static void setComponentID(Component c, String id) {
  if (c != null) componentID_map.put(c, id);
}


static JComponent wrapForSmartAdd_jComponent(Object o) {
  return componentToJComponent(wrapForSmartAdd(o));
}


static Map<String, String> humanizeFormLabel_replacements = litmap("id" , "ID", "md5" , "MD5");

static String humanizeFormLabel(String s) {
  if (containsSpace(s)) return s;
  return firstToUpper(
    joinWithSpace(replaceElementsUsingMap(splitCamelCase(s), humanizeFormLabel_replacements)).replace("I D", "ID")
  );
}


static boolean isRunnable(Object o) {
  return o instanceof Runnable || hasMethod(o, "get");
}


static void disposeInternalFrame(Component c) {
  final JInternalFrame f = getInternalFrame(c);
  if (f != null) { swing(() -> { 
    vmBus_send("disposingInternalFrame", f);
    f.dispose();
    // now illegal... setOpt(f, lastFocusOwner := null); // Help GC
  }); }
}


static void onEnterInAllTextFields(JComponent c, Runnable action) {
  if (action == null) return;
  for (Component tf : allChildren(c))
    onEnterIfTextField(tf, action);
}

static void onEnterInAllTextFields(List c, Runnable action) {
  for (Object o : unnull(c))
    if (o instanceof JComponent)
      onEnterInAllTextFields(((JComponent) o), action);
}


static <A extends JComponent> A makeBold(final A c) {
  if (c != null) { swing(() -> {  c.setFont(c.getFont().deriveFont(java.awt.Font.BOLD)); }); }
  return c;
}


static JPanel smartAddWithLayout(JPanel panel, Object layout, List parts) {
  for (Object o : parts)
    panel.add(wrapForSmartAdd(o), layout);
  return panel;
}

static JPanel smartAddWithLayout(JPanel panel, Object layout, Object... parts) {
  return smartAddWithLayout(panel, layout, asList(flattenArray2(parts)));
}



static Component jrigid() {
  return javax.swing.Box.createRigidArea(new Dimension(0, 0));
}


static <A> List<A> childrenOfType(Component c, Class<A> theClass) {
  List<A> l = new ArrayList();
  scanForComponents(c, theClass, l);
  return l;
}

static <A> List<A> childrenOfType(Class<A> theClass, Component c) {
  return childrenOfType(c, theClass);
}


static JButton findButton(Component c, String name) {
  for (JButton b : childrenOfType(c, JButton.class))
    if (eq(b.getText(), name)) return b;
  for (JButton b : childrenOfType(getFrame(c), JButton.class))
    if (eq(b.getText(), name)) return b;
  return null;
}

static JButton findButton(Component c) {
  return childOfType(c, JButton.class);
}


static List collectField(Iterable c, String field) {
  List l = new ArrayList();
  if (c != null) for (Object a : c)
    l.add(getOpt(a, field));
  return l;
}

static List collectField(String field, Iterable c) {
  return collectField(c, field);
}


static Rect pointsRect(int x1, int y1, int x2, int y2) {
  return new Rect(x1, y1, x2-x1, y2-y1);
}


static String getStackTrace2(Throwable e) {
  return hideCredentials(getStackTrace(unwrapTrivialExceptionWraps(e)) + replacePrefix("java.lang.RuntimeException: ", "FAIL: ",
    hideCredentials(str(innerException2(e)))) + "\n");
}


static Rectangle getBounds(final Component c) {
  return c == null ? null : swing(new F0<Rectangle>() { public Rectangle get() { try {  return c.getBounds();  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "return c.getBounds();"; }});
}


static ThreadLocal realMC_tl_tl = new ThreadLocal();

static ThreadLocal realMC_tl() {
  return realMC_tl_tl;
}


static betterCIComparator_C betterCIComparator_instance;

static betterCIComparator_C betterCIComparator() {
  if (betterCIComparator_instance == null)
    betterCIComparator_instance = new betterCIComparator_C();
  return betterCIComparator_instance;
}

final static class betterCIComparator_C implements Comparator<String> {
  public int compare(String s1, String s2) {
    if (s1 == null) return s2 == null ? 0 : -1;
    if (s2 == null) return 1;
  
    int n1 = s1.length();
    int n2 = s2.length();
    int min = Math.min(n1, n2);
    for (int i = 0; i < min; i++) {
        char c1 = s1.charAt(i);
        char c2 = s2.charAt(i);
        if (c1 != c2) {
            c1 = Character.toUpperCase(c1);
            c2 = Character.toUpperCase(c2);
            if (c1 != c2) {
                c1 = Character.toLowerCase(c1);
                c2 = Character.toLowerCase(c2);
                if (c1 != c2) {
                    // No overflow because of numeric promotion
                    return c1 - c2;
                }
            }
        }
    }
    return n1 - n2;
  }
}


static TreeSet<String> toCaseInsensitiveSet_treeSet(Iterable<String> c) {
  if (isCISet(c)) return (TreeSet) c;
  TreeSet<String> set = caseInsensitiveSet_treeSet();
  addAll(set, c);
  return set;
}

static TreeSet<String> toCaseInsensitiveSet_treeSet(String... x) {
  TreeSet<String> set = caseInsensitiveSet_treeSet();
  addAll(set, x);
  return set;
}


static Pair pairMap(Object f, Pair p) {
  return p == null ? null : pair(callF(f, p.a), callF(f, p.b));
}

static <A> Pair<A, A> pairMap(IF1<A, A> f, Pair<A, A> p) {
  return p == null ? null : pair(callF(f, p.a), callF(f, p.b));
}

static Pair pairMap(Pair p, Object f) {
  return pairMap(f, p);
}






  static AutoCloseable tempUncancelThread() {
    return tempRemove(ping_actions, Thread.currentThread());
  }



static <A> boolean setAdd(Collection<A> c, A a) {
  if (c == null || c.contains(a)) return false;
  c.add(a);
  return true;
}


static int or0(Integer i) { return i == null ? 0 : i; }
static long or0(Long l) { return l == null ? 0L : l; }
static double or0(Double d) { return d == null ? 0.0 : d; }


static String getString(Map map, Object key) {
  return map == null ? null : (String) map.get(key);
}

static String getString(List l, int idx) {
  return (String) get(l, idx);
}

static String getString(Object o, Object key) {
  if (o instanceof Map) return getString((Map) o, key);
  if (key instanceof String)
    return (String) getOpt(o, (String) key);
  throw fail("Not a string key: " + getClassName(key));
}

static String getString(String key, Object o) {
  return getString(o, (Object) key);
}


static Object evalInNewThread(final Object f) {
  final Flag flag = new Flag();
  final Var var = new Var();
  final Var<Throwable> exception = new Var();
  { startThread(new Runnable() {  public void run() { try {  try {
      var.set(callF(f));
    } catch (Throwable e) {
      exception.set(e);
    }
    flag.raise();
  
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "try {\r\n      var.set(callF(f));\r\n    } catch (Throwable e) {\r\n      exception..."; }}); }
  flag.waitUntilUp();
  if (exception.has()) throw rethrow(exception.get());
  return var.get();
}


static String callStaticAnswerMethod(List bots, String s) {
  for (Object c : bots) try {
    String answer = callStaticAnswerMethod(c, s);
    if (!empty(answer)) return answer;
  } catch (Throwable e) {
    print("Error calling " + getProgramID(c));
    e.printStackTrace();
  }
  return null;
}

static String callStaticAnswerMethod(Object c, String s) {
  String answer = (String) callOpt(c, "answer", s, litlist(s));
  if (answer == null)
    answer = (String) callOpt(c, "answer", s);
  return emptyToNull(answer);
}

static String callStaticAnswerMethod(String s) {
  return callStaticAnswerMethod(mc(), s);
}

static String callStaticAnswerMethod(String s, List<String> history) {
  return callStaticAnswerMethod(mc(), s, history);
}

static String callStaticAnswerMethod(Object c, String s, List<String> history) {
  String answer = (String) callOpt(c, "answer", s, history);
  if (answer == null)
    answer = (String) callOpt(c, "answer", s);
  return emptyToNull(answer);
}


static boolean match3(String pat, String s) {
  return match3(pat, s, null);
}

static boolean match3(String pat, String s, Matches matches) {
  if (pat == null || s == null) return false;
  return match3(pat, parse3_cachedInput(s), matches);
}
  
static boolean match3(String pat, List<String> toks, Matches matches) {
  List<String> tokpat = parse3_cachedPattern(pat);
  return match3(tokpat, toks, matches);
}

static boolean match3(List<String> tokpat, List<String> toks, Matches matches) {
  String[] m = match2(tokpat, toks);
  //print(structure(tokpat) + " on " + structure(toks) + " => " + structure(m));
  if (m == null) return false;
  if (matches != null) matches.m = m; return true;
}


static JTextField setTextAndSelectAll(final JTextField tf, final String text) {
  if (tf != null) { swing(() -> { 
    tf.setText(text);
    tf.selectAll();
  }); }
  return tf;
}


static JTextField consoleInputField() {
  Object console = get(getJavaX(), "console");
  return (JTextField) getOpt(console, "tfInput");
}


static void focusConsole(String s) {
  setConsoleInput(s);
  focusConsole();
}

static void focusConsole() {
  JComponent tf = consoleInputFieldOrComboBox();
  if (tf != null) {
    //print("Focusing console");
    tf.requestFocus();
  }
}


// this should be on by default now I think, but it may break
// legacy code...
static ThreadLocal<Boolean> htmlencode_forParams_useV2 = new ThreadLocal();

static String htmlencode_forParams(String s) {
  if (s == null) return "";
  if (isTrue(htmlencode_forParams_useV2.get()))
    return htmlencode_forParams_v2(s);
    
  StringBuilder out = new StringBuilder(Math.max(16, s.length()));
  for (int i = 0; i < s.length(); i++) {
      char c = s.charAt(i);
      if (c > 127 || c == '"' || c == '<' || c == '>') {
          out.append("&#");
          out.append((int) c);
          out.append(';');
      } else
          out.append(c);
  }
  return out.toString();
}




static Map<Component, String> componentID_map = weakHashMap();

static String componentID(Component c) {
  return c == null ? null : componentID_map.get(c);
}




static Map<String, List<String>> parse3_cachedInput_cache = synchronizedMRUCache(1000);

static List<String> parse3_cachedInput(String s) {
  List<String> tok = parse3_cachedInput_cache.get(s);
  if (tok == null) parse3_cachedInput_cache.put(s, tok = parse3(s));
  return tok;
}






static Map<String, List<String>> parse3_cachedPattern_cache = synchronizedMRUCache(1000);

static synchronized List<String> parse3_cachedPattern(String s) {
  List<String> tok = parse3_cachedPattern_cache.get(s);
  if (tok == null) parse3_cachedPattern_cache.put(s, tok = parse3(s));
  return tok;
}




static Object[] asObjectArray(Collection l) {
  return toObjectArray(l);
}


static String ok2(String s) {
  return "ok " + s;
}


// An "Android" is a program that accepts text questions (on console or TCP) and outputs one response text per question

//please include function myJavaSource. // for getting my known commands

static boolean makeAndroid3_disable = false; // disable all android making

static class Android3 implements AutoCloseable {
  String greeting;
  boolean publicOverride = false; // optionally set this in client
  int startPort = 5000; // optionally set this in client
  Responder responder;
  boolean console = true;
  boolean quiet = false; // no messages on console
  boolean daemon = false;
  boolean incomingSilent = false;
  int incomingPrintLimit = 200;
  boolean useMultiPort = true;
  boolean recordHistory = false;
  boolean verbose = false;
  int answerPrintLimit = 500;
  boolean newLineAboveAnswer, newLineBelowAnswer;
  
  // set by system
  int port;
  long vport;
  DialogHandler handler;
  ServerSocket server;
  
  Android3(String greeting) {
  this.greeting = greeting;}
  Android3() {}
  
  public void close() { dispose(); }
  
  synchronized void dispose() {
    if (server != null) {
      try {
        server.close();
      } catch (IOException e) {
        print("[internal] " + e);
      }
      server = null;
    }
    if (vport != 0) { try {
      print("Disposing " + this);
      removeFromMultiPort(vport);
      vport = 0;
    } catch (Throwable __e) { printStackTrace(__e); }}
  }
  
  public String toString() { return "Bot: " + greeting + " [vport " + vport + "]"; }
}

static abstract class Responder {
  abstract String answer(String s, List<String> history);
}

static Android3 makeAndroid3(final String greeting) {
  return makeAndroid3(new Android3(greeting));
}

static Android3 makeAndroid3(final String greeting, Responder responder) {
  Android3 android = new Android3(greeting);
  android.responder = responder;
  return makeAndroid3(android);
}

static Android3 makeAndroid3(final Android3 a) {
  if (makeAndroid3_disable) return a;
  
  if (a.responder == null)
    a.responder = new Responder() {
      String answer(String s, List<String> history) {
        return callStaticAnswerMethod(s, history);
      }
    };
    
  if (!a.quiet)
    print("[bot] " + a.greeting);
  
  if (a.console && (readLine_noReadLine || makeAndroid3_consoleInUse()))
    a.console = false;
  
  record(a);
  
  if (a.useMultiPort)
    a.vport = addToMultiPort(a.greeting,
      makeAndroid3_verboseResponder(a));
      
  if (a.console)
    makeAndroid3_handleConsole(a);

  if (a.useMultiPort) return a;

  a.handler = makeAndroid3_makeDialogHandler(a);
  if (a.quiet) startDialogServer_quiet.set(true);
  try {
    a.port = a.daemon
      ? startDialogServerOnPortAboveDaemon(a.startPort, a.handler)
      : startDialogServerOnPortAbove(a.startPort, a.handler);
  } finally {
    startDialogServer_quiet.set(null);
  }
  a.server = startDialogServer_serverSocket;

  return a;
}

static void makeAndroid3_handleConsole(final Android3 a) {
  // Console handling stuff
  if (!a.quiet)
    print("You may also type on this console.");
  { startThread(new Runnable() {  public void run() { try {  List<String> history = new ArrayList();
    while (licensed()) {
      String line;
      try {
        line = readLine();
      } catch (Throwable e) {
        print(getInnerMessage(e));
        break;
      }
      if (line == null) break;
      /*if (eq(line, "bye")) {
        print("> bye stranger");
        history = new ArrayList<S>();
      } else*/ {
        history.add(line);
        history.add(makeAndroid3_getAnswer(line, history, a)); // prints answer on console too
      }
    }
  
} catch (Exception __e) { throw rethrow(__e); } }  public String toString() { return "List<String> history = new ArrayList();\r\n    while (licensed()) {\r\n      Stri..."; }}); }
}

static DialogHandler makeAndroid3_makeDialogHandler(final Android3 a) {
  return new DialogHandler() {
public void run(final DialogIO io) {
    if (!a.publicOverride && !(publicCommOn() || io.isLocalConnection())) {
      io.sendLine("Sorry, not allowed");
      return;
    }
    
    String dialogID = randomID(8);
    
    io.sendLine(a.greeting + " / Your ID: " + dialogID);
    
    List<String> history = new ArrayList();
    
    while (io.isStillConnected()) {
      if (io.waitForLine()) {
        final String line = io.readLineNoBlock();
        String s = dialogID + " at " + now() + ": " + quote(line);
        if (!a.incomingSilent)
          print(shorten(s, a.incomingPrintLimit));
        if (eq(line, "bye")) {
          io.sendLine("bye stranger");
          return;
        }
        Matches m = new Matches();
        if (a.recordHistory)
          history.add(line);
        String answer;
        if (match3("this is a continuation of talk *", s, m)
          || match3("hello bot! this is a continuation of talk *", s, m)) {
          dialogID = unquote(m.m[0]);
          answer = "ok";
        } else try {
          makeAndroid3_io.set(io);
          answer = makeAndroid3_getAnswer(line, history, a);
        } finally {
          makeAndroid3_io.set(null);
        }
        if (a.recordHistory)
          history.add(answer);
        io.sendLine(answer);
        //appendToLog(logFile, s);
      }
    }
  }};
}

static String makeAndroid3_getAnswer(String line, List<String> history, Android3 a) {
  String answer, originalAnswer;
  try {
    originalAnswer = a.responder.answer(line, history);
    answer = makeAndroid3_fallback(line, history, originalAnswer);
  } catch (Throwable e) {
    e = getInnerException(e);
    printStackTrace(e);
    originalAnswer = answer = e.toString();
  }
  if (!a.incomingSilent) {
    if (originalAnswer == null) originalAnswer = "?";
    if (a.newLineAboveAnswer) print();
    print(">" + dropFirst(indentx(2, shorten(rtrim(originalAnswer), a.answerPrintLimit))));
    if (a.newLineBelowAnswer) print();
  }
  return answer;
}

static String makeAndroid3_fallback(String s, List<String> history, String answer) {
  // Now we only do the safe thing instead of VM inspection - give out our process ID
  if (answer == null && match3("what is your pid", s))
    return getPID();
    
  if (answer == null && match3("what is your program id", s)) // should be fairly safe, right?
    return getProgramID();
    
  if (match3("get injection id", s))
    return getInjectionID();
    
  if (answer == null) answer = "?";
  if (answer.indexOf('\n') >= 0 || answer.indexOf('\r') >= 0)
    answer = quote(answer);
  return answer;
}

static boolean makeAndroid3_consoleInUse() {
  if (isTrue(vm_generalMap_get("consoleInUse"))) return true;
  for (Object o : record_list)
    if (o instanceof Android3 && ((Android3) o).console)
      return true;
  return false;
}

static Responder makeAndroid3_verboseResponder(final Android3 a) {
  return new Responder() {
    String answer(String s, List<String> history) {
      if (a.verbose)
        print("> " + shorten(s, a.incomingPrintLimit));
      String answer = a.responder.answer(s, history);
      if (a.verbose)
        print("< " + shorten(answer, a.incomingPrintLimit));
      return answer;
    }
  };
}

static ThreadLocal<DialogIO> makeAndroid3_io = new ThreadLocal();

static Android3 makeAndroid3() {
  return makeAndroid3(getProgramTitle() + ".");
}


static String makeResponder_callAnswerMethod(Object bot, String s, List<String> history) {
  String answer = (String) callOpt(bot, "answer", s, history);
  if (answer == null)
    answer = (String) callOpt(bot, "answer", s);
  return answer;
}

static Responder makeResponder(final Object bot) {
  if (bot instanceof Responder) return (Responder) bot;
  
  if (bot instanceof String) {
    String f =  (String) bot;
    return new Responder() {
      String answer(String s, List<String> history) {
        String answer = (String) callOptMC((String) bot, s, history);
        if (answer == null)
          answer = (String) callOptMC((String) bot, s);
        return answer;
      }
    };
  }
  
  return new Responder() {
    String answer(String s, List<String> history) {
      return makeResponder_callAnswerMethod(bot, s, history);
    }
  };
}


static String defaultBotName() {
  return getProgramTitle() + ".";
}


static List<File> listFilesWithSuffix(File dir, String suffix) {
  List<File> l = new ArrayList();
  for (File f : listFiles(dir))
    if (!f.isDirectory() && (empty(suffix) || endsWithIgnoreCase(f.getName(), suffix)))
      l.add(f);
  return l;
}

static List<File> listFilesWithSuffix(String suffix, File dir) {
  return listFilesWithSuffix(dir, suffix);
}


static Map<BufferedImage, Object> createGraphics_modulators = synchroIdentityHashMap();

static Graphics2D createGraphics(BufferedImage img) {
  Graphics2D g = img.createGraphics();
  Object mod = createGraphics_modulators.get(img);
  if (mod != null)
    callF(mod, g);
  return g;
}

// mod: voidfunc(Graphics2D)
static void createGraphics_modulate(BufferedImage img, Object mod) {
  mapPut2(createGraphics_modulators, img, mod);
}


static ThreadLocal<Boolean> imageGraphics_antiAlias = new ThreadLocal();

static Graphics2D imageGraphics(BufferedImage img) {
  return !isFalse(imageGraphics_antiAlias.get()) ? antiAliasGraphics(img) : createGraphics(img);
}


static <A extends JComponent> A componentPopupMenuItem(A c, final String name, final Object action) {
  componentPopupMenu(c, new VF1<JPopupMenu>() { public void get(JPopupMenu menu) { try { 
    addMenuItem(menu, name, action);
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "addMenuItem(menu, name, action);"; }});
  return c;
}

static void componentPopupMenuItem(JComponent c, final JMenuItem menuItem) {
  componentPopupMenu(c, new VF1<JPopupMenu>() { public void get(JPopupMenu menu) { try { 
    addMenuItem(menu, menuItem);
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "addMenuItem(menu, menuItem);"; }});
}


static ThreadLocal<Boolean> addInternalFrame_dontSelect = new ThreadLocal();
static ThreadLocal<Integer> addInternalFrame_layer = new ThreadLocal();
static ThreadLocal<Boolean> addInternalFrame_toBack = new ThreadLocal();

static JInternalFrame addInternalFrame(final JDesktopPane desktop, final String title, final int x, final int y, final int w, final int h) {
  return addInternalFrame(desktop, title, x, y, w, h, null);
}

static JInternalFrame addInternalFrame(final JDesktopPane desktop, final String title, final int x, final int y, final int w, final int h, final Component contents) {
  return addInternalFrame(desktop, title, rect(x, y, w, h), contents);
}

static JInternalFrame addInternalFrame(final JDesktopPane desktop, final String title, final Component contents) {
  return addInternalFrame(desktop, title, null, contents);
}

static JInternalFrame addInternalFrame(final JDesktopPane desktop, final String title, final Rect r, final Component contents) {
  final boolean dontSelect = isTrue(optParam(addInternalFrame_dontSelect));
  final boolean toBack = isTrue(optParam(addInternalFrame_toBack));
  final Integer layer = optParam(addInternalFrame_layer);
  return swing(new F0<JInternalFrame>() { public JInternalFrame get() { try { 
    JInternalFrame frame;
    if (contents instanceof JInternalFrame)
      frame = (JInternalFrame) contents;
    else {
      frame = jInternalFrame(title);
      setInternalFrameContents(frame, contents);
    }
    frame.setVisible(true);
    desktop.add(frame, layer);
    if (r != null)
      setBounds(frame, r);
    else
      internalFrameDefaultPosition(frame);
    if (dontSelect)
      if (toBack)
        frame.toBack();
      else
        frame.toFront();
    else
      frame.setSelected(true);
    return fixInternalFrame(frame);
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "JInternalFrame frame;\r\n    if (contents instanceof JInternalFrame)\r\n      fra..."; }});
}

static JInternalFrame addInternalFrame(JDesktopPane desktop, String title) {
  return addInternalFrame(desktop, title, jpanel());
}


static JInternalFrame minInternalFrameWidth(final JInternalFrame frame, final int w) {
  { swing(() -> { 
    if (frame != null && frame.getWidth() < w)
      frame.setSize(w, frame.getHeight());
  }); }
  return frame;
}

static JInternalFrame minInternalFrameWidth(int w, JInternalFrame frame) {
  return minInternalFrameWidth(frame, w);
}



static <A extends Component> A packInternalFrameVertically(A c) {
  return packInternalFrameVertically(-1, c);
}

static <A extends Component> A packInternalFrameVertically(int width, A c) {
  final JInternalFrame win = getInternalFrame(c);
  if (win == null) return c;
  final int w = width < 0 ? win.getWidth() : width;
  { swing(() -> { 
    win.pack();
    win.setSize(w, win.getHeight());
    fixInternalFrame(win);
  }); }
  return c;
}


static JInternalFrame centerInternalFrame(final JInternalFrame f) {
  { swing(() -> { 
    Container c = f.getParent();
    if (c != null) {
      //print("Container type: " + className(c) + ", bounds: " + c.getBounds());
      f.setLocation((c.getWidth()-f.getWidth())/2, (c.getHeight()-f.getHeight())/2);
    }
  }); }
  return f;
}

static JInternalFrame centerInternalFrame(final int w, final int h, final JInternalFrame f) {
  { swing(() -> { 
    f.setSize(w, h);
  }); }
  return centerInternalFrame(f);
}


static int packFrame_minw = 150, packFrame_minh = 50;

static <A extends Component> A packFrame(final A c) {
  { swing(() -> { 
    Window w = getWindow(c);
    if (w != null) {
      w.pack();
      int maxW = getScreenWidth()-50, maxH = getScreenHeight()-50;
      w.setSize(
        min(maxW, max(w.getWidth(), packFrame_minw)),
        min(maxH, max(w.getHeight(), packFrame_minh)));
    }
  }); }
  return c;
}

static JFrame packFrame(ButtonGroup g) {
  return packFrame(getFrame(g));
}


static JFrame showFrame() {
  return makeFrame();
}

static JFrame showFrame(Object content) {
  return makeFrame(content);
}

static JFrame showFrame(String title) {
  return makeFrame(title);
}

static JFrame showFrame(String title, Object content) {
  return makeFrame(title, content);
}

static JFrame showFrame(final JFrame f) {
  if (f != null) { swing(() -> { 
    if (frameTooSmall(f)) frameStandardSize(f);
    if (!f.isVisible()) f.setVisible(true); // XXX
    if (f.getState() == Frame.ICONIFIED) f.setState(Frame.NORMAL);
  }); }
  return f;
}

// make or update frame
static JFrame showFrame(String title, Object content, JFrame frame) {
  if (frame == null)
    return showFrame(title, content);
  else {
    frame.setTitle(title);
    setFrameContents(frame, content);
    return frame;
  }
}


static <A, B extends Collection<A>> void forEachLevel2(Iterable<B> l, IVF1<A> f) {
  if (l != null) for (B b : l)
    forEach(b, f);
}

static <A, B extends Collection<A>> void forEachLevel2(IVF1<A> f, Iterable<B> l) {
  forEachLevel2(f, l);
}


static Object vmBus_query(String msg, Object... args) {
  Object arg = vmBus_wrapArgs(args);
  { var __1= pcallFAll_returnFirstNotNull(vm_busListeners_live(), msg, arg); if (__1 != null) return __1; }
  return pcallFAll_returnFirstNotNull(vm_busListenersByMessage_live().get(msg), msg, arg);
}

static Object vmBus_query(String msg) {
  return vmBus_query(msg, (Object) null);
}


static <A extends Component> A jPreferWidth(int w, A c) {
  { swing(() -> { 
    Dimension size = c.getPreferredSize();
    c.setPreferredSize(new Dimension(/*max(w, size.width) ??? */w, size.height));
  }); }
  return c;
}


static Component wrapForSmartAdd(Object o) {
  if (o == null) return jpanel();
  if (o instanceof String) return jlabel((String) o);
  return wrap(o);
}


static boolean containsSpace(String s) {
  return containsSpaces(s);
}


static <A> List<A> replaceElementsUsingMap(Iterable<A> l, final Map<A, A> map) {
  return map(l, new F1<A, A>() { public A get(A a) { try {  return getOrKeep(map, a);  } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "getOrKeep(map, a)"; }});
}


static List<String> splitCamelCase(String s) {
  return ai_splitCamelCase(s);
}


static boolean hasMethod(Object o, String method, Object... args) {
  return findMethod_cached(o, method, args) != null;
}


static List<Component> allChildren(Component c) {
  return childrenOfType(c, Component.class);
}


static void onEnterIfTextField(Component c, Runnable action) {
  if (action == null) return;
  if (c instanceof JTextField)
    onEnter(((JTextField) c), action);
  else if (c instanceof JComboBox)
    onEnter(((JComboBox) c), action);
}


static Object[] flattenArray2(Object... a) {
  List l = new ArrayList();
  if (a != null) for (Object x : a)
    if (x instanceof Object[])
      l.addAll(asList((Object[]) x));
    else if (x instanceof Collection)
      l.addAll((Collection) x);
    else
      l.add(x);
  return asObjectArray(l);
}


static <A> void scanForComponents(final Component c, final Class<A> theClass, final List<A> l) {
  if (theClass.isInstance(c))
    l.add((A) c);
  if (c instanceof Container) { swing(() -> { 
    for (Component comp : ((Container) c).getComponents())
      scanForComponents(comp, theClass, l);
  }); }
}


static <A> A childOfType(Component c, Class<A> theClass) {
  return first(childrenOfType(c, theClass));
}

static <A> A childOfType(Class<A> theClass, Component c) {
  return childOfType(c, theClass);
}


static Throwable unwrapTrivialExceptionWraps(Throwable e) {
  if (e == null) return e;
  while (e.getClass() == RuntimeException.class
    && e.getCause() != null && eq(e.getMessage(), str(e.getCause())))
    e = e.getCause();
  return e;
}


static boolean isCISet(Iterable<String> l) {
  return l instanceof TreeSet && ((TreeSet) l).comparator() == caseInsensitiveComparator();
}


static <A, B> AutoCloseable tempRemove(Map<A, B> map, A key) {
  if (map == null || !map.containsKey(key)) return null;
  B b = map.get(key);
  map.remove(key);
  return new AutoCloseable() { public String toString() { return "map.put(key, b);"; } public void close() throws Exception { map.put(key, b); }};
}


static String emptyToNull(String s) {
  return eq(s, "") ? null : s;
}

static <A, B> Map<A, B> emptyToNull(Map<A, B> map) {
  return empty(map) ? null : map;
}


static void setConsoleInput(String text) {
  consoleSetInput(text);
}


static JComponent consoleInputFieldOrComboBox() {
  Object console = get(getJavaX(), "console");
  JComboBox cb =  (JComboBox) (getOpt(console, "cbInput"));
  if (cb != null) return cb;
  return (JTextField) getOpt(console, "tfInput");
}


static String htmlencode_forParams_v2(String s) {
  if (s == null) return "";
  StringBuilder out = new StringBuilder(Math.max(16, s.length()));
  for (int i = 0; i < s.length(); i++) {
      char c = s.charAt(i);
      if (c > 127 || c == '"' || c == '<' || c == '>' || c == '&') {
          out.append("&#");
          out.append((int) c);
          out.append(';');
      } else
          out.append(c);
  }
  return out.toString();
}




static <A, B> Map<A, B> synchronizedMRUCache(int maxSize) {
  return synchroMap(new MRUCache(maxSize));
}


static List<String> parse3(String s) {
  return dropPunctuation(javaTokPlusPeriod(s));
}


static void removeFromMultiPort(long vport) {
  if (vport == 0) return;
  for (Object port : getMultiPorts())
    call(port, "removePort", vport);
}


static List<Object> record_list = synchroList();

static void record(Object o) {
  record_list.add(o);
}


static Object addToMultiPort_responder;

static long addToMultiPort(final String botName) {
  return addToMultiPort(botName, new Object() {
    public String answer(String s, List<String> history) {
      String answer =  (String) (callOpt(getMainClass(), "answer", s, history));
      if (answer != null) return answer;
      answer = (String) callOpt(getMainClass(), "answer", s);
      if (answer != null) return answer;
      if (match3("get injection id", s))
        return getInjectionID();
      return null;
    }
  });
}

static long addToMultiPort(final String botName, final Object responder) {
  //print(botName);
  addToMultiPort_responder = responder;
  startMultiPort();
  List ports = getMultiPorts();
  if (ports == null) return 0;
  if (ports.isEmpty())
    throw fail("No multiports!");
  if (ports.size() > 1)
    print("Multiple multi-ports. Using last one.");
  Object port = last(ports);
  Object responder2 = new Object() {
    public String answer(String s, List<String> history) {
      if (match3("get injection id", s))
        return getInjectionID();
      if (match3("your name", s))
        return botName;
      return (String) call(responder, "answer", s, history);
    }
  };
  record(responder2);
  return (Long) call(port, "addResponder", botName, responder2);
}



static AtomicInteger dialogServer_clients = new AtomicInteger();
static boolean dialogServer_printConnects = false;
static ThreadLocal<Boolean> startDialogServer_quiet = new ThreadLocal();

static Set<String> dialogServer_knownClients = synchroTreeSet();

static int startDialogServerOnPortAbove(int port, DialogHandler handler) {
  while (!forbiddenPort(port) && !startDialogServerIfPortAvailable(port, handler))
    ++port;
  return port;
}

static int startDialogServerOnPortAboveDaemon(int port, DialogHandler handler) {
  while (!forbiddenPort(port) && !startDialogServerIfPortAvailable(port, handler, true))
    ++port;
  return port;
}

static void startDialogServer(int port, DialogHandler handler) {
  if (!startDialogServerIfPortAvailable(port, handler))
    throw fail("Can't start dialog server on port " + port);
}

static boolean startDialogServerIfPortAvailable(int port, final DialogHandler handler) {
  return startDialogServerIfPortAvailable(port, handler, false);
}

static ServerSocket startDialogServer_serverSocket;
  
static boolean startDialogServerIfPortAvailable(int port, final DialogHandler handler, boolean daemon) {
  ServerSocket serverSocket = null;
  try {
    serverSocket = new ServerSocket(port);
  } catch (IOException e) {
    // probably the port number is used - let's assume there already is a chat server.
    return false;
  }
  final ServerSocket _serverSocket = serverSocket;
  startDialogServer_serverSocket = serverSocket;

  Thread thread = new Thread("Socket accept port " + port) { public void run() {
   try {
    while (true) {
      try {
        final Socket s = _serverSocket.accept();
        
        String client = s.getInetAddress().toString();
        if (!dialogServer_knownClients.contains(client) && neq(client, "/127.0.0.1")) {
          print("connect from " + client + " - clients: " + dialogServer_clients.incrementAndGet());
          dialogServer_knownClients.add(client);
        }
        
        String threadName = "Handling client " + s.getInetAddress();

        Thread t2 = new Thread(threadName) {
         public void run() {
          try {
            final Writer w = new OutputStreamWriter(s.getOutputStream(), "UTF-8");
            final BufferedReader in = new BufferedReader(
              new InputStreamReader(s.getInputStream(), "UTF-8"));
              
            DialogIO io = new DialogIO() {
            
              // This should be the same as #1001076 (talkTo)
            
  boolean isLocalConnection() {
    return s.getInetAddress().isLoopbackAddress();
  }
  
  boolean isStillConnected() {
    return !(eos || s.isClosed());
  }
  
  void sendLine(String line) { try {
    w.write(line + "\n");
    w.flush();
  } catch (Exception __e) { throw rethrow(__e); } }
  
  String readLineImpl() { try {
    return in.readLine();
  } catch (Exception __e) { throw rethrow(__e); } }
  
  public void close() {
    try {
      s.close();
    } catch (IOException e) {
      // whatever
    }
  }
  
  Socket getSocket() {
    return s;
  }
  };
            
            try {
              handler.run(io);
            } finally {
              if (!io.noClose)
                s.close();
            }
          } catch (IOException e) {
            print("[internal] " + e);
          } finally {
            //print("client disconnect - " + dialogServer_clients.decrementAndGet() + " remaining");
          }
         }
        }; // Thread t2
        t2.setDaemon(true); // ?
        t2.start();
      } catch (SocketTimeoutException e) {
      }
    }   
   } catch (IOException e) {
     print("[internal] " + e);
   }
  }};
  if (daemon) thread.setDaemon(true);
  thread.start();
 
  if (!isTrue(getAndClearThreadLocal(startDialogServer_quiet)))
    print("Dialog server on port " + port + " started."); 
  return true;
}


static boolean publicCommOn() {
  return "1".equals(loadTextFile(new File(userHome(), ".javax/public-communication")));
}


static int randomID_defaultLength = 12;

static String randomID(int length) {
  return makeRandomID(length);
}

static String randomID(Random r, int length) {
  return makeRandomID(r, length);
}

static String randomID() {
  return randomID(randomID_defaultLength);
}

static String randomID(Random r) {
  return randomID(r, randomID_defaultLength);
}


static String processID_cached;

// try to get our current process ID
static String getPID() {
  if (processID_cached == null) {
    String name = ManagementFactory.getRuntimeMXBean().getName();
    processID_cached = name.replaceAll("@.*", "");
  }
  return processID_cached;
}


static String getInjectionID() {
  return (String) call(getJavaX(), "getInjectionID", getMainClass());
}


static Object callOptMC(String method, Object... args) {
  return callOpt(mc(), method, args);
}


static <A, B> Map<A, B> synchroIdentityHashMap() {
  return synchroMap(new IdentityHashMap());
}


static <A, B> void mapPut2(Map<A, B> map, A key, B value) {
  if (map != null && key != null)
    if (value != null) map.put(key, value);
    else map.remove(key);
}


static Graphics2D antiAliasGraphics(BufferedImage img) {
  return antiAliasOn(createGraphics(img));
}


static boolean jInternalFrame_iconifiable = true;

static JInternalFrame jInternalFrame() {
  return jInternalFrame("");
}

static JInternalFrame jInternalFrame(final String title) {
  return swing(new F0<JInternalFrame>() { public JInternalFrame get() { try { 
    JInternalFrame f = new JInternalFrame(title, true, true, true, jInternalFrame_iconifiable);
    f.setVisible(true);
    return f;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "JInternalFrame f = new JInternalFrame(title, true, true, true, jInternalFrame..."; }});
}


static void setInternalFrameContents(final Component c, final Object contents) {
  { swing(() -> { 
    JInternalFrame frame = getInternalFrame(c);
    if (frame == null) return;
    frame.getContentPane().removeAll();
    frame.getContentPane().setLayout(new BorderLayout());
    if (contents != null) frame.getContentPane().add(wrap(contents));
    revalidate(frame);
  }); }
}


static <A extends Component> A setBounds(final int x, final int y, final int w, final int h, final A a) {
  if (a != null) { swing(() -> { 
    a.setBounds(x, y, w, h);
  }); }
  return a;
}

static <A extends Component> A setBounds(A a, Rect r) {
  if (a != null && r != null) { swing(() -> { 
    a.setBounds(toRectangle(r));
  }); }
  return a;
}

static <A extends Component> A setBounds(A a, Rectangle r) {
  if (a != null && r != null) { swing(() -> { 
    a.setBounds(r);
  }); }
  return a;
}

static <A extends Component> A setBounds(Rect r, A a) {
  return setBounds(a, r);
}

static <A extends Component> A setBounds(A a, int x, int y, int w, int h) {
  return setBounds(x, y, w, h, a);
}


static void internalFrameDefaultPosition(JInternalFrame f) {
  f.setSize(500, 300);
  centerInternalFrame(f);
}


static int fixInternalFrame_borderTopLeft = 0;
static int fixInternalFrame_borderBottomRight = 40; // for title bar

static JInternalFrame fixInternalFrame(final JInternalFrame f) {
  return swing(new F0<JInternalFrame>() { public JInternalFrame get() { try { 
    Container c = f.getParent();
    if (c == null) return f;
    Rect r = toRect(f.getBounds());
    int a = fixInternalFrame_borderTopLeft, b = fixInternalFrame_borderBottomRight;
    Rect outer = new Rect(a, a, c.getWidth()-b, c.getHeight()-b);
    if (!rectContains(outer, r))
      f.setLocation(
        max(a, min(r.x, outer.x2())),
        max(a, min(r.y, outer.y2())));
    if (r.w > c.getWidth() || r.h > c.getHeight())
      f.setSize(c.getWidth()-a, c.getHeight()-a);
    return f;
   } catch (Exception __e) { throw rethrow(__e); } }
  public String toString() { return "Container c = f.getParent();\r\n    if (c == null) ret f;\r\n    Rect r = toRect(..."; }});
}



static int getScreenWidth() {
  return getScreenSize().width;
}


static int getScreenHeight() {
  return getScreenSize().height;
}


static boolean frameTooSmall(JFrame frame) {
  return frame.getWidth() < 100 || frame.getHeight() < 50;
}


static void frameStandardSize(JFrame frame) {
  frame.setBounds(300, 100, 500, 400);
}


static void setFrameContents(final Component c, final Object contents) { swing(() -> { 
  JFrame frame = getFrame(c);
  if (frame == null) return;
  frame.getContentPane().removeAll();
  frame.getContentPane().setLayout(new BorderLayout());
  frame.getContentPane().add(wrap(contents));
  revalidate(frame);
}); }


static <A> void forEach(Iterable<A> l, IVF1<A> f) {
  if (f != null && l != null) for (A a : l)
    callF(f, a);
}

static <A> void forEach(IVF1<A> f, Iterable<A> l) {
  forEach(l, f);
}

static <A> void forEach(A[] l, IVF1<A> f) {
  if (f != null && l != null) for (A a : l)
    callF(f, a);
}

static <A> void forEach(IVF1<A> f, A[] l) {
  forEach(l, f);
}

static <A, B> void forEach(Map<A, B> map, IVF2<A, B> f) {
  for (Map.Entry<? extends A, ? extends B> __0 : _entrySet( map))
    { A a = __0.getKey(); B b = __0.getValue();  f.get(a, b); }
}


static Object pcallFAll_returnFirstNotNull(Collection l, Object... args) {
  if (l != null) for (Object f : cloneList(l))
    { var __1= pcallF(f, args); if (__1 != null) return __1; }
  return null;
}

static Object pcallFAll_returnFirstNotNull(Iterator it, Object... args) {
  while (it.hasNext())
    { var __2= pcallF(it.next(), args); if (__2 != null) return __2; }
  return null;
}


static boolean containsSpaces(String s) {
  return indexOf(s, ' ') >= 0;
}


static <A> A getOrKeep(Map<A, ? extends A> map, A a) {
  if (map == null) return a;
  A v = map.get(a);
  return v != null ? v : a;
}


static List<String> ai_splitCamelCase(String s) {
  int j = 0;
  List<String> l = new ArrayList();
  
  // new addition
  if (isAllUpperCase(s)) { l.add(s); return l; }
  
  for (int i = 0; i < l(s); i++)
    if (i > j && isUpperCaseLetter(s.charAt(i))) {
      l.add(substring(s, j, i));
      j = i;
    }
  if (j < l(s))
    l.add(substring(s, j));
  return l;
}




static List<String> dropPunctuation_keep = ll("*", "<", ">");

static List<String> dropPunctuation(List<String> tok) {
  tok = new ArrayList<String>(tok);
  for (int i = 1; i < tok.size(); i += 2) {
    String t = tok.get(i);
    if (t.length() == 1 && !Character.isLetter(t.charAt(0)) && !Character.isDigit(t.charAt(0)) && !dropPunctuation_keep.contains(t)) {
      tok.set(i-1, tok.get(i-1) + tok.get(i+1));
      tok.remove(i);
      tok.remove(i);
      i -= 2;
    }
  }
  return tok;
}

static String dropPunctuation(String s) {
  return join(dropPunctuation(nlTok(s)));
}


static List<Object> getMultiPorts() {
  return (List) callOpt(getJavaX(), "getMultiPorts");
}


// start multi-port if none exists in current VM.
static void startMultiPort() {
  List mp = getMultiPorts();
  if (mp != null && mp.isEmpty()) {
    nohupJavax("#1001639");
    throw fail("Upgrading JavaX, please restart this program afterwards.");
    //callMain(hotwire("#1001672"));
  }
}


static <A> Set<A> synchroTreeSet() {
  return Collections.synchronizedSet(new TreeSet<A>());
}

static <A> Set<A> synchroTreeSet(TreeSet<A> set) {
  return Collections.synchronizedSet(set);
}


static boolean forbiddenPort(int port) {
  return port == 5037; // adb
}



static Graphics2D antiAliasOn(Graphics2D g) {
  g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
  g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  return g;
}


static Dimension getScreenSize() {
  return Toolkit.getDefaultToolkit().getScreenSize();
}


static boolean isAllUpperCase(String s) {
  return hasLettersAllUpperCase(s);
}


static boolean isUpperCaseLetter(char c) {
  return Character.isUpperCase(c);
}




static List<String> nlTok(String s) {
  return javaTokPlusPeriod(s);
}


static boolean hasLettersAllUpperCase(String s) {
  return hasLetters(s) && !containsLowerCase(s);
}




static boolean hasLetters(String s) {
  for (int i = 0; i < s.length(); i++)
    if (Character.isLetter(s.charAt(i)))
      return true;
  return false;
}


static boolean containsLowerCase(String s) {
  for (int i = 0; i < l(s); i++)
    if (isLowerCase(s.charAt(i)))
      return true;
  return false;
}




static boolean isLowerCase(char c) {
  return Character.isLowerCase(c);
}




static class ProgramScan {
  static int threads = isWindows() ? 500 : 10;
  static int timeout = 5000; // hmm...
  static String ip = "127.0.0.1";
  
  // This range is not used anymore anyway
  static int quickScanFrom = 10000, quickScanTo = 10999;

  static int maxNumberOfVMs_android = 4; // Android will always only have one if we don't screw up
  static int maxNumberOfVMs_nonAndroid = 50; // 100;
  static int maxNumberOfVMs;
  
  static boolean verbose = false;
  
  static class Program {
    int port;
    String helloString;
    
    Program(int port, String helloString) {
  this.helloString = helloString;
  this.port = port;}
  }
  
  static List<Program> scan() { try {
    return scan(1, 65535);
  } catch (Exception __e) { throw rethrow(__e); } }
  
  static List<Program> scan(int fromPort, int toPort) {
    return scan(fromPort, toPort, new int[0]);
  }
  
  static List<Program> scan(int fromPort, int toPort, int[] preferredPorts) { try {
    Set<Integer> preferredPortsSet = new HashSet<Integer>(asList(preferredPorts));
    int scanSize = toPort-fromPort+1;
    String name = toPort < 10000 ? "bot" : "program";
    int threads = isWindows() ? min(500, scanSize) : min(scanSize, 10);
    final ExecutorService es = Executors.newFixedThreadPool(threads);
    if (verbose) print(firstToUpper(name) + "-scanning " + ip + " with timeout " + timeout + " ms in " + threads + " threads.");
    startTiming();
    List<Future<Program>> futures = new ArrayList();
    List<Integer> ports = new ArrayList();
    for (int port : preferredPorts) {
      futures.add(checkPort(es, ip, port, timeout));
      ports.add(port);
    }
    for (int port = fromPort; port <= toPort; port++)
      if (!preferredPortsSet.contains(port) && !forbiddenPort(port)) {
        futures.add(checkPort(es, ip, port, timeout));
        ports.add(port);
      }
    es.shutdown();
    List<Program> programs = new ArrayList();
    long time = now();
    int i = 0;
    for (final Future<Program> f : futures) {
      if (verbose) print("Waiting for port " + get(ports, i++) + " at time " + (now()-time));
      Program p = f.get();
      if (p != null)
        programs.add(p);
    }
    //stopTiming("Port Scan " + scanSize + ", " + n(threads, "threads") + ": ", 250);
    if (verbose) print("Found " + programs.size() + " " + name + "(s) on " + ip);
    return programs;
  } catch (Exception __e) { throw rethrow(__e); } }

  static Future<Program> checkPort(final ExecutorService es, final String ip, final int port, final int timeout) {
    return es.submit(new Callable<Program>() {
        @Override public Program call() {
          try {
            Socket socket = new Socket();
            try {
              socket.setSoTimeout(timeout);
              socket.connect(new InetSocketAddress(ip, port), timeout);
              //if (verbose) print("Connected to " + ip + ":" + port);
              BufferedReader in = new BufferedReader(
                new InputStreamReader(socket.getInputStream(), "UTF-8"));
              String hello = or(in.readLine(), "?");
              return new Program(port, hello);
            } finally {
              socket.close();
            }
          } catch (Exception ex) {
            return null;
          }
        }
     });
  }
  
  static List<Program> quickScan() {
    return scan(quickScanFrom, quickScanTo);
  }
  
  static List<Program> quickBotScan() {
    return quickBotScan(new int[0]);
  }
  
  static List<Program> quickBotScan(int[] preferredPorts) {
    if (maxNumberOfVMs == 0)
      maxNumberOfVMs = isAndroid() ? maxNumberOfVMs_android : maxNumberOfVMs_nonAndroid;
    return scan(4999, 5000+maxNumberOfVMs-1, preferredPorts);
  }
}
static class Best_comparable<A> {
  A best;
  Comparable score;
  boolean verboseNewBest, replaceIfSameScore;
  
  transient Object onChange;
  transient Object stringifier; // func(A) -> S
  
  synchronized boolean isNewBest(Comparable score) {
    return this.score == null
      || (replaceIfSameScore
        ? cmp(score, this.score) >= 0
        : cmp(score, this.score) > 0);
  }
  
  synchronized Comparable bestScore() {
    return score;
  }
  
  Comparable score() { return bestScore(); }
  Comparable getScore() { return bestScore(); }
  
  synchronized float floatScoreOr(float defaultValue) {
    return best == null ? defaultValue : (float) score;
  }
  
  boolean put(Pair<? extends A, Comparable> p) {
    return p != null && put(p.a, p.b);
  }
  
  boolean put(Best<? extends A> b) {
    return b != null && put(b.get(), b.score);
  }
  
  boolean put(A a, Comparable score) {
    ping();
    boolean change = false;
    if (a != null) synchronized(this) {
      if (isNewBest(score)) {
        best = a;
        this.score = score;
        change = true;
      }
    }
    if (change) {
      if (verboseNewBest) print("New best! " + this);
      pcallF(onChange);
    }
    return change;
  }
  
  synchronized A get() { return best; }
  synchronized boolean has() { return best != null; }
  
  synchronized Pair<A, Comparable> pair() { return main.pair(best, bestScore()); }

  synchronized A getIfScoreAbove(Comparable x) { return cmp(x, score()) >= 0 ? best : null; }
  
  public String toString() {
    return !has() ? "-" : "Score " + score + ": " + callStringifier(stringifier, best);
  }
  
  boolean putAndPrintIfNewBest(A a, Comparable score) {
    if (!put(a, score)) return false;
    { print(this); return true; }
  }
  
  synchronized void clear() { best = null; score = 0; }
}
// size:
// 64 bytes for 0 to 1 elements
// 96 bytes for 2 to 4 elements

/*
 * #!
 * Ontopia Engine
 * #-
 * Copyright (C) 2001 - 2013 The Ontopia Project
 * #-
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * !#
 */

static class CompactHashMap<K, V> extends CompactAbstractMap<K, V> {
  final static int INITIAL_SIZE = 3;
  final static double LOAD_FACTOR = 0.6;

  // This object is used to represent null, should clients use that as
  final static Object nullObject = new Object();
  
  /**
   * When a key is deleted this object is put into the hashtable in
   * its place, so that other entries with the same key (collisions)
   * further down the hashtable are not lost after we delete an object
   * in the collision chain.
   */
  final static Object deletedObject = new Object();
  int elements;
  int freecells;
  Object[] table; // key, value, key, value, ...
  //int modCount;

  CompactHashMap() {
    this(INITIAL_SIZE);
  }

  CompactHashMap(int size) {
    table = new Object[(size==0 ? 1 : size)*2];
    elements = 0;
    freecells = tableSize();
    //modCount = 0;
  }
  
  // TODO: allocate smarter
  CompactHashMap(Map<K, V> map) {
    this(0);
    if (map != null) putAll(map);
  }

  // ===== MAP IMPLEMENTATION =============================================

  /**
   * Returns the number of key/value mappings in this map.
   */
  public synchronized int size() {
    return elements;
  }
  
  /**
   * Returns <tt>true</tt> if this map contains no mappings.
   */
  public synchronized boolean isEmpty() {
    return elements == 0;
  }

  /**
   * Removes all key/value mappings in the map.
   */
  public synchronized void clear() {
    elements = 0;
    for (int ix = 0; ix < tableSize(); ix++) {
      key(ix, null);
      value(ix, null);
    }
    freecells = tableSize();
    //modCount++;
  }
  
  /**
   * Returns <tt>true</tt> if this map contains the specified key.
   */
  public synchronized boolean containsKey(Object k) {
    return key(findKeyIndex(k)) != null;
  }
  
  /**
   * Returns <tt>true</tt> if this map contains the specified value.
   */
  public synchronized boolean containsValue(Object v) {
    if (v == null)
      v = (V)nullObject;

    for (int ix = 0; ix < tableSize(); ix++)
      if (value(ix) != null && value(ix).equals(v))
        return true;

    return false;
  }

  /**
   * Returns a read-only set view of the map's keys.
   */
  public synchronized Set<Entry<K, V>> entrySet() {
    throw new UnsupportedOperationException();
  }

  /**
   * Removes the mapping with key k, if there is one, and returns its
   * value, if there is one, and null if there is none.
   */
  public synchronized V remove(Object k) {
    int index = findKeyIndex(k);

    // we found the right position, now do the removal
    if (key(index) != null) {
      // we found the object

      // same problem here as with put
      V v = value(index);
      key(index, deletedObject);
      value(index, deletedObject);
      //modCount++;
      elements--;
      return v;
    } else
      // we did not find the key
      return null;
  }

  /**
   * Adds the specified mapping to this map, returning the old value for
   * the mapping, if there was one.
   */
  public synchronized V put(K k, V v) {
    if (k == null)
      k = (K)nullObject;

    int hash = k.hashCode();
    int index = (hash & 0x7FFFFFFF) % tableSize();
    int offset = 1;
    int deletedix = -1;
    
    // search for the key (continue while !null and !this key)
    while(key(index) != null &&
          !(key(index).hashCode() == hash &&
            key(index).equals(k))) {

      // if there's a deleted mapping here we can put this mapping here,
      // provided it's not in here somewhere else already
      if (key(index) == deletedObject)
        deletedix = index;
      
      index = ((index + offset) & 0x7FFFFFFF) % tableSize();
      offset = offset*2 + 1;

      if (offset == -1)
        offset = 2;
    }
    
    if (key(index) == null) { // wasn't present already
      if (deletedix != -1) // reusing a deleted cell
        index = deletedix;
      else
        freecells--;

      //modCount++;
      elements++;

      key(index, k);
      value(index, v);
      
      // rehash with increased capacity
      if (1 - (freecells / (double) tableSize()) > LOAD_FACTOR)
        rehash(tableSize()*2 + 1);
      return null;
    } else { // was there already
      //modCount++;
      V oldv = value(index);
      value(index, v);
      return oldv;
    }
  }

  /**
   * INTERNAL: Rehashes the hashmap to a bigger size.
   */
  void rehash(int newCapacity) {
    int oldCapacity = tableSize();
    Object[] newTable = new Object[newCapacity*2];

    for (int ix = 0; ix < oldCapacity; ix++) {
      Object k = key(ix);
      if (k == null || k == deletedObject)
        continue;
      
      int hash = k.hashCode();
      int index = (hash & 0x7FFFFFFF) % newCapacity;
      int offset = 1;

      // search for the key
      while(newTable[index*2] != null) { // no need to test for duplicates
        index = ((index + offset) & 0x7FFFFFFF) % newCapacity;
        offset = offset*2 + 1;

        if (offset == -1)
          offset = 2;
      }

      newTable[index*2] = k;
      newTable[index*2+1] = value(ix);
    }

    table = newTable;
    freecells = tableSize() - elements;
  }

  /**
   * Returns the value for the key k, if there is one, and null if
   * there is none.
   */
  public synchronized V get(Object k) {
    return value(findKeyIndex(k));
  }

  /**
   * Returns a virtual read-only collection containing all the values
   * in the map.
   */
  public synchronized Collection<V> values() {
    return new ValueCollection();
  }

  /**
   * Returns a virtual read-only set of all the keys in the map.
   */
  public synchronized Set<K> keySet() {
    return new KeySet();
  }

  // --- Internal utilities

  final int findKeyIndex(Object k) {
    if (k == null)
      k = nullObject;

    int hash = k.hashCode();
    int index = (hash & 0x7FFFFFFF) % tableSize();
    int offset = 1;

    // search for the key (continue while !null and !this key)
    while(key(index) != null &&
          !(key(index).hashCode() == hash &&
            key(index).equals(k))) {
      index = ((index + offset) & 0x7FFFFFFF) % tableSize();
      offset = offset*2 + 1;

      if (offset == -1)
        offset = 2;
    }
    return index;
  }
  
  // --- Key set

  class KeySet<K> extends AbstractSet<K> {
    public synchronized int size() {
      return elements;
    }

    public synchronized boolean contains(Object k) {
      return containsKey(k);
    }

    public synchronized Iterator<K> iterator() {
      return new KeyIterator();
    }
  }

  class KeyIterator<K> implements Iterator<K> {
    private int ix;
    
    private KeyIterator() {
      // walk up to first value, so that hasNext() and next() return
      // correct results
      for (; ix < tableSize(); ix++)
        if (value(ix) != null && key(ix) != deletedObject)
          break;
    }

    public synchronized boolean hasNext() {
      return ix < tableSize();
    }

    public synchronized void remove() {
      throw new UnsupportedOperationException("Collection is read-only");
    }

    public synchronized K next() {
      if (ix >= tableSize())
        throw new NoSuchElementException();
      K key = (K) key(ix++);
      
      // walk up to next value
      for (; ix < tableSize(); ix++)
        if (key(ix) != null && key(ix) != deletedObject)
          break;
      
      // ix now either points to next key, or outside array (if no next)
      return key;
    }
  }
  
  // --- Value collection

  class ValueCollection<V> extends AbstractCollection<V> {
    public synchronized int size() {
      return elements;
    }

    public synchronized Iterator<V> iterator() {
      return new ValueIterator();
    }

    public synchronized boolean contains(Object v) {
      return containsValue(v);
    }
  }

  class ValueIterator<V> implements Iterator<V> {
    private int ix;
    
    private ValueIterator() {
      // walk up to first value, so that hasNext() and next() return
      // correct results
      for (; ix < table.length/2; ix++)
        if (value(ix) != null && value(ix) != deletedObject)
          break;
    }

    public synchronized boolean hasNext() {
      return ix < tableSize();
    }

    public synchronized void remove() {
      throw new UnsupportedOperationException("Collection is read-only");
    }

    public synchronized V next() {
      if (ix >= tableSize())
        throw new NoSuchElementException();
      V value = (V) value(ix++);
      
      // walk up to next value
      for (; ix < tableSize(); ix++)
        if (value(ix) != null && value(ix) != deletedObject)
          break;
      
      // ix now either points to next value, or outside array (if no next)
      return value;
    }
  }
  
  K key(int i) { return (K) table[i*2]; }
  void key(int i, Object key) { table[i*2] = key; }
  V value(int i) { return (V) table[i*2+1]; }
  void value(int i, Object value) { table[i*2+1] = value; }
  
  int tableSize() { return table.length/2; }
}
static class DefunctClassLoader {}
static class WidthAndHeightImpl extends Meta implements WidthAndHeight , IFieldsToList{
  int width;
  int height;
  WidthAndHeightImpl() {}
  WidthAndHeightImpl(int width, int height) {
  this.height = height;
  this.width = width;}public Object[] _fieldsToList() { return new Object[] {width, height}; }

  public int getWidth() { return width; }
  public int getHeight() { return height; }
  
  public String toString() { return n2(width) + "*" + n2(height) + " px"; }
}
static class DoubleRect {
  double x, y, w, h;
  
  DoubleRect() {}
  DoubleRect(Rectangle r) {
    x = r.x;
    y = r.y;
    w = r.width;
    h = r.height;
  }
  DoubleRect(double x, double y, double w, double h) {
  this.h = h;
  this.w = w;
  this.y = y;
  this.x = x;}
  
  // Huh. not implementing equals()/hashCode? Stefan is mysterious
  boolean eq(Object o) {
    if (!(o instanceof DoubleRect)) return false;
    if (o == this) return true;
    DoubleRect r =  (DoubleRect) o;
    return x == r.x && y == r.y && w == r.w && h == r.h;
  }
  
  public String toString() {
    return x + "," + y + " / " + w + "," + h;
  }
  
  double x1() { return x; }
  double y1() { return y; }
  double x2() { return x + w; }
  double y2() { return y + h; }
  
  boolean contains(Pt p) {
    return contains(p.x, p.y);
  }
  
  boolean contains(double _x, double _y) {
    return _x >= x && _y >= y && _x < x+w && _y < y+h;
  }
  
  boolean empty() { return w <= 0 || h <= 0; }
}
static class Complex implements IFieldsToList{
  static final String _fieldOrder = "re im";
  double re;
  double im;
  Complex() {}
  Complex(double re, double im) {
  this.im = im;
  this.re = re;}

public boolean equals(Object o) {
if (!(o instanceof Complex)) return false;
    Complex __1 =  (Complex) o;
    return re == __1.re && im == __1.im;
}

  public int hashCode() {
    int h = -1679819632;
    h = boostHashCombine(h, _hashCode(re));
    h = boostHashCombine(h, _hashCode(im));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {re, im}; }

  double abs() { return sqrt(re*re+im*im); }
  
  double re() { return re; }
  double im() { return im; }
  
  final double angle(){ return phase(); }
double phase() { return Math.atan2(im, re); }
  double fracAngle() { return fracNonNeg(angle()/twoPi()); } // angle as 0 to 1
  
  public String toString() {
    if (im != 0)
      return re == 0 ? im + "i" : re + plusPrefixUnlessMinus(str(im)) + "i";
    else
      return str(re);
  }
}
static class GrabbableIntPixels implements IFieldsToList{
  static final String _fieldOrder = "data w h offset scanlineStride";
  int[] data;
  int w;
  int h;
  int offset;
  int scanlineStride;
  GrabbableIntPixels() {}
  GrabbableIntPixels(int[] data, int w, int h, int offset, int scanlineStride) {
  this.scanlineStride = scanlineStride;
  this.offset = offset;
  this.h = h;
  this.w = w;
  this.data = data;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + data + ", " + w + ", " + h + ", " + offset + ", " + scanlineStride + ")"; }

public boolean equals(Object o) {
if (!(o instanceof GrabbableIntPixels)) return false;
    GrabbableIntPixels __1 =  (GrabbableIntPixels) o;
    return eq(data, __1.data) && w == __1.w && h == __1.h && offset == __1.offset && scanlineStride == __1.scanlineStride;
}

  public int hashCode() {
    int h = -1183022196;
    h = boostHashCombine(h, _hashCode(data));
    h = boostHashCombine(h, _hashCode(w));
    h = boostHashCombine(h, _hashCode(h));
    h = boostHashCombine(h, _hashCode(offset));
    h = boostHashCombine(h, _hashCode(scanlineStride));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {data, w, h, offset, scanlineStride}; }

  int[] asPackedArray() {
    if (offset == 0 && data.length == w*h) return data;
    int[] pixels = new int[w*h];
    for (int y = 0; y < h; y++)
      arrayCopy(data, offset+y*scanlineStride, pixels, y*h, w);
    return pixels;
  }
}




static class FileTransferable implements Transferable {
  List<File> files = new ArrayList();

  FileTransferable(File  f) { files.add(f); }
  FileTransferable(List<File> files) {
  this.files = files;}

  public DataFlavor[] getTransferDataFlavors() {
    return new DataFlavor[] { DataFlavor.javaFileListFlavor };
  }

  public boolean isDataFlavorSupported(DataFlavor flavor) {
    return eq(flavor, DataFlavor.javaFileListFlavor);
  }

  public Object getTransferData(DataFlavor flavor) {
    return files;
  }
}




static class TransferableImage implements Transferable {
  Image i;

  TransferableImage(Image i) {
  this.i = i;}

  public Object getTransferData( DataFlavor flavor )
  throws UnsupportedFlavorException, IOException {
      if ( flavor.equals( DataFlavor.imageFlavor ) && i != null ) {
          return i;
      }
      else {
          throw new UnsupportedFlavorException( flavor );
      }
  }

  public DataFlavor[] getTransferDataFlavors() {
      DataFlavor[] flavors = new DataFlavor[ 1 ];
      flavors[ 0 ] = DataFlavor.imageFlavor;
      return flavors;
  }

  public boolean isDataFlavorSupported( DataFlavor flavor ) {
      DataFlavor[] flavors = getTransferDataFlavors();
      for ( int i = 0; i < flavors.length; i++ ) {
          if ( flavor.equals( flavors[ i ] ) ) {
              return true;
          }
      }

      return false;
  }
}
static class CountingOutputStream extends FilterOutputStream {
  long counter;
  
  CountingOutputStream(OutputStream out) { super(out); }
  
  @Override
  public void write(int b) throws IOException {
    ++counter;
    out.write(b);
  }

  @Override
  public void write(byte[] b) throws IOException {
    counter += b.length;
    out.write(b, 0, b.length);
  }

  @Override
  public void write(byte[] b, int off, int len) throws IOException {
    if (len == 0) return;
    counter += len;
    out.write(b, off, len);
  }
  
  long getFilePointer() { return counter; }
}
static interface Producer<A> {
  public A next(); // null when end
}
// AppendableChain has one "smart" head element (with size counter
// and pointer to the chain's last element), all the other nodes are
// maximally simple (MinimalChain).
// This allows O(1) front insertion, front removal and back insertion
// (not removal at the back though) which is fine for what I need this
// for (event queues).
//
// Stefan Reich, Oct 21

static class AppendableChain<A> extends MinimalChain<A> implements Iterable<A> {
  MinimalChain<A> last; // pointer to last element in chain (which may be us)
  int size; // total length of chain

  AppendableChain() {} // only used internally
  AppendableChain(A element) {
  this.element = element; size = 1; last = this; }
  
  // intermediate constructor called by itemPlusChain()
  AppendableChain(A element, AppendableChain<A> next) {
  this.next = next;
  this.element = element;
    if (next == null) return;
    
    MinimalChain<A> b = new MinimalChain();
    b.element = next.element;
    b.next = next.next;
    this.next = b;
    last = next.last;
    size = next.size+1;
  }
  
  public String toString() { return str(toList()); }
  
  // append at the end
  boolean add(A a) {
    MinimalChain newLast = new MinimalChain(a);
    last.next = newLast;
    last = newLast;
    ++size;
    return true;
  }
  
  // drop first element
  AppendableChain<A> popFirst() {
    if (next == null) return null;
    element = next.element;
    if (last == next) last = this;
    next = next.next;
    --size;
    return this;
  }
  
  ArrayList<A> toList() {
    ArrayList<A> l = emptyList(size);
    MinimalChain<A> c = this;
    while (c != null) {
      l.add(c.element);
      c = c.next;
    }
    return l;
  }
  
  //public Iterator<A> iterator() { ret toList().iterator(); }
  
  class ACIt extends IterableIterator  < A > {
    MinimalChain<A> c = AppendableChain.this;
    
    public boolean hasNext() {
      return c != null;
    }
    
    public A next() {
      var a = c.element;
      c = c.next;
      return a;
    }
  }
  
  public IterableIterator<A> iterator() {
    return new ACIt();
  }
}
static class Lowest<A> {
  A best;
  double score;
  transient Object onChange;
  
  synchronized boolean isNewBest(double score) {
    return best == null || score < this.score;
  }
  
  synchronized double bestScore() {
    return best == null ? Double.NaN : score;
  }
  
  double score() { return bestScore(); }
  
  synchronized float floatScore() {
    return best == null ? Float.NaN : (float) score;
  }
  
  synchronized float floatScoreOr(float defaultValue) {
    return best == null ? defaultValue : (float) score;
  }
  
  boolean put(A a, double score) {
    boolean change = false;
    synchronized(this) {
      if (a != null && isNewBest(score)) {
        best = a;
        this.score = score;
        change = true;
      }
    }
    if (change)
      pcallF(onChange);
    return change;
  }
  
  synchronized void clear() { best = null; score = 0; }
  
  synchronized A get() { return best; }
  synchronized boolean has() { return best != null; }
  synchronized Pair<A, Double> pair() { return best == null ? null : new Pair(best, bestScore()); }
  
  public String toString() {
    return "Score " + formatDouble_significant2(score, 4) + ": " + best;
  }
}
static class Either<A, B> {
  byte which;
  Object value;
  
  Either() {}
  Either(int which, Object value) { this.which = (byte) which;
  this.value = value; }
  
  boolean isA() { return which == 1; }
  boolean isB() { return which == 2; }
  
  A a() { if (which != 1) _failMe(); return (A) value; }
  B b() { if (which != 2) _failMe(); return (B) value; }
  
  A aOpt() { return which != 1 ? null : (A) value; }
  B bOpt() { return which != 2 ? null : (B) value; }
  
  void _failMe() {
    throw fail("Either object is of wrong type: " + shortClassName(value));
  }
  
  public String toString() {
    return "Either" + (isA() ? "A" : "B") + "(" + value + ")";
  }
}
static class NumPadFixingInputMap extends InputMap {
  public void put(KeyStroke keyStroke, Object actionMapKey) {
    super.put(keyStroke, actionMapKey);
    Integer code = rsyntaxTextArea_fixNumPad_map.get(keyStroke.getKeyCode());
    if (code != null)
      put(KeyStroke.getKeyStroke(code, keyStroke.getModifiers()), actionMapKey);
  }
  
  void replaceOn(JComponent c) {
    InputMap im = c.getInputMap();
    setParent(im.getParent());
    for (KeyStroke key : im.keys())
      put(key, im.get(key));
    c.setInputMap(JComponent.WHEN_FOCUSED, this);
  }
}
static class FixedRateTimer extends java.util.Timer implements AutoCloseable {
  FixedRateTimer() { this(false); }
  FixedRateTimer(boolean daemon) { this(defaultTimerName(), daemon); }
  FixedRateTimer(String name) { this(name, false); }
  FixedRateTimer(String name, boolean daemon) {
    super(name, daemon);
    _registerTimer(this);
  }
  
  List<Entry> entries = synchroList();
  
  static class Entry implements IFieldsToList{
  TimerTask task;
  long firstTime;
  long period;
  Entry() {}
  Entry(TimerTask task, long firstTime, long period) {
  this.period = period;
  this.firstTime = firstTime;
  this.task = task;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + task + ", " + firstTime + ", " + period + ")"; }public Object[] _fieldsToList() { return new Object[] {task, firstTime, period}; }
}
  
  // Note: not all methods overridden; only use these ones
  
  public void scheduleAtFixedRate(TimerTask task, long delay, long period) {
    entries.add(new Entry(task, now()+delay, period));
    super.scheduleAtFixedRate(task, delay, period);
  }
  
  public void cancel() {
    entries.clear();
    super.cancel();
  }
  
  public int purge() {
    entries.clear();
    return super.purge();
  }
  
  FixedRateTimer changeRate(int newPeriod) {
    Object r = ((SmartTimerTask) first(entries).task).r;
    cancel();
    return doEvery(newPeriod, r);
  }
  
  public void close() { try { cancel(); } catch (Exception __e) { throw rethrow(__e); } }
}
abstract static class NotifyingSet<A> extends NotifyingCollection<A> implements Set<A> {
  Set<A> set;
  
  NotifyingSet() {} // for persistence
  NotifyingSet(Set<A> set) {
    super(set);
    this.set = set;
  }
}
// has equals semantics so passing it to onChange() etc is idempotent
static class ChangeTrigger implements Runnable , IFieldsToList{
  ChangeTriggerable target;
  ChangeTrigger() {}
  ChangeTrigger(ChangeTriggerable target) {
  this.target = target;}
  public String toString() { return shortClassName_dropNumberPrefix(this) + "(" + target + ")"; }

public boolean equals(Object o) {
if (!(o instanceof ChangeTrigger)) return false;
    ChangeTrigger __1 =  (ChangeTrigger) o;
    return eq(target, __1.target);
}

  public int hashCode() {
    int h = -1239972920;
    h = boostHashCombine(h, _hashCode(target));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {target}; }

  public void run() { try { { if (target != null) target.change(); } } catch (Exception __e) { throw rethrow(__e); } }
}
static class T3<A, B, C> {
  A a;
  B b;
  C c;
  
  T3() {}
  T3(A a, B b, C c) {
  this.c = c;
  this.b = b;
  this.a = a;}
  T3(T3<A, B, C> t) { a = t.a; b = t.b; c = t.c; }
  
  public int hashCode() {
    return _hashCode(a) + 2*_hashCode(b) - 4*_hashCode(c);
  }
  
  public boolean equals(Object o) {
    if (o == this) return true;
    if (!(o instanceof T3)) return false;
    T3 t = (T3) o;
    return eq(a, t.a) && eq(b, t.b) && eq(c, t.c);
  }
  
  public String toString() {
    return "(" + quoteBorderless(a) + ", " + quoteBorderless(b) + ", " + quoteBorderless(c) + ")";
  }
}


abstract static class NotifyingCollection<E> extends AbstractCollection<E> implements Collection<E> {
  Collection<E> c;  // Backing Collection

  NotifyingCollection() {} // for persistence
  NotifyingCollection(Collection<E> c) {
  this.c = c;}

  public int size() {
      synchronized (this) {return c.size();}
  }
  public boolean isEmpty() {
      synchronized (this) {return c.isEmpty();}
  }
  public boolean contains(Object o) {
      synchronized (this) {return c.contains(o);}
  }
  public Object[] toArray() {
      synchronized (this) {return c.toArray();}
  }
  public <T> T[] toArray(T[] a) {
      synchronized (this) {return c.toArray(a);}
  }

  public Iterator<E> iterator() {
      return c.iterator(); // caller should not use Iterator.remove()
  }

  public boolean add(E e) {
      synchronized (this) { beforeChange(); if (!c.add(e)) return false; } change(); return true;
  }
  public boolean remove(Object o) {
      synchronized (this) { beforeChange(); if (!c.remove(o)) return false;} change(); return true;
  }

  public boolean containsAll(Collection<?> coll) {
      synchronized (this) {return c.containsAll(coll);}
  }
  public boolean addAll(Collection<? extends E> coll) {
      synchronized (this) { beforeChange(); if (!c.addAll(coll)) return false;} change(); return true;
  }
  public boolean removeAll(Collection<?> coll) {
      synchronized (this) { beforeChange(); if (!c.removeAll(coll)) return false;} change(); return true;
  }
  public boolean retainAll(Collection<?> coll) {
      synchronized (this) { beforeChange(); if (!c.retainAll(coll)) return false;} change(); return true;
  }
  public void clear() {
      synchronized (this) { beforeChange(); c.clear();} change();
  }
  public String toString() {
      synchronized (this) {return c.toString();}
  }
  
  /*public void forEach(Consumer<? super E> consumer) {
      synchronized (this) {c.forEach(consumer);}
  }*/
 
  /*public boolean removeIf(Predicate<? super E> filter) {
      synchronized (this) {return c.removeIf(filter);}
  }*/
  @Override
  public Spliterator<E> spliterator() {
      return c.spliterator(); // Must be manually synched by user!
  }
  
  /*public Stream<E> stream() {
      return c.stream(); // Must be manually synched by user!
  }*/
  
  /*public Stream<E> parallelStream() {
      return c.parallelStream(); // Must be manually synched by user!
  }*/
  
  void beforeChange() {}
  abstract void change();
  
  Collection<E> unwrap() { return c; }
}
static class MinimalChain<A> implements Iterable<A> {
  A element;
  MinimalChain<A> next;

  MinimalChain() {}
  MinimalChain(A element) {
  this.element = element;}
  MinimalChain(A element, MinimalChain<A> next) {
  this.next = next;
  this.element = element;}
  
  public String toString() { return str(toList()); }
  
  ArrayList<A> toList() {
    ArrayList<A> l = new ArrayList();
    MinimalChain<A> c = this;
    while (c != null) {
      l.add(c.element);
      c = c.next;
    }
    return l;
  }
  
  void setElement(A a) { element = a; }
  void setNext(MinimalChain<A> next) { this.next = next; }
  
  // TODO: optimize
  public Iterator<A> iterator() { return toList().iterator(); }
  
  A get() { return element; }
}
abstract static class CompactAbstractMap<K, V> implements Map<K, V> {
  public int size() {
      return entrySet().size();
  }

  public boolean isEmpty() {
      return size() == 0;
  }

  public boolean containsValue(Object value) {
      Iterator<Entry<K, V>> i = entrySet().iterator();
      if (value == null) {
          while (i.hasNext()) {
              Entry<K, V> e = i.next();
              if (e.getValue() == null)
                  return true;
          }
      } else {
          while (i.hasNext()) {
              Entry<K, V> e = i.next();
              if (value.equals(e.getValue()))
                  return true;
          }
      }
      return false;
  }

  public boolean containsKey(Object key) {
      Iterator<Entry<K, V>> i = entrySet().iterator();
      if (key == null) {
          while (i.hasNext()) {
              Entry<K, V> e = i.next();
              if (e.getKey() == null)
                  return true;
          }
      } else {
          while (i.hasNext()) {
              Entry<K, V> e = i.next();
              if (key.equals(e.getKey()))
                  return true;
          }
      }
      return false;
  }

  public V get(Object key) {
      Iterator<Entry<K, V>> i = entrySet().iterator();
      if (key == null) {
          while (i.hasNext()) {
              Entry<K, V> e = i.next();
              if (e.getKey() == null)
                  return e.getValue();
          }
      } else {
          while (i.hasNext()) {
              Entry<K, V> e = i.next();
              if (key.equals(e.getKey()))
                  return e.getValue();
          }
      }
      return null;
  }

  public V put(K key, V value) {
      throw new UnsupportedOperationException();
  }

  public V remove(Object key) {
      Iterator<Entry<K, V>> i = entrySet().iterator();
      Entry<K, V> correctEntry = null;
      if (key == null) {
          while (correctEntry == null && i.hasNext()) {
              Entry<K, V> e = i.next();
              if (e.getKey() == null)
                  correctEntry = e;
          }
      } else {
          while (correctEntry == null && i.hasNext()) {
              Entry<K, V> e = i.next();
              if (key.equals(e.getKey()))
                  correctEntry = e;
          }
      }

      V oldValue = null;
      if (correctEntry != null) {
          oldValue = correctEntry.getValue();
          i.remove();
      }
      return oldValue;
  }

  public void putAll(Map<? extends K, ? extends V> m) {
      for (Entry<? extends K, ? extends V> e : m.entrySet())
          put(e.getKey(), e.getValue());
  }

  public void clear() {
      entrySet().clear();
  }

  public Set<K> keySet() {
      return new AbstractSet<K>() {
          public Iterator<K> iterator() {
              return new Iterator<K>() {
                  private Iterator<Entry<K, V>> i = entrySet().iterator();

                  public boolean hasNext() {
                      return i.hasNext();
                  }

                  public K next() {
                      return i.next().getKey();
                  }

                  public void remove() {
                      i.remove();
                  }
              };
          }

          public int size() {
              return CompactAbstractMap.this.size();
          }

          public boolean isEmpty() {
              return CompactAbstractMap.this.isEmpty();
          }

          public void clear() {
              CompactAbstractMap.this.clear();
          }

          public boolean contains(Object k) {
              return CompactAbstractMap.this.containsKey(k);
          }
      };
  }

  public Collection<V> values() {
      return new AbstractCollection<V>() {
          public Iterator<V> iterator() {
              return new Iterator<V>() {
                  private Iterator<Entry<K, V>> i = entrySet().iterator();

                  public boolean hasNext() {
                      return i.hasNext();
                  }

                  public V next() {
                      return i.next().getValue();
                  }

                  public void remove() {
                      i.remove();
                  }
              };
          }

          public int size() {
              return CompactAbstractMap.this.size();
          }

          public boolean isEmpty() {
              return CompactAbstractMap.this.isEmpty();
          }

          public void clear() {
              CompactAbstractMap.this.clear();
          }

          public boolean contains(Object v) {
              return CompactAbstractMap.this.containsValue(v);
          }
      };
  }

  public abstract Set<Entry<K, V>> entrySet();

  public boolean equals(Object o) {
      if (o == this)
          return true;

      if (!(o instanceof Map))
          return false;
      Map<?, ?> m = (Map<?, ?>) o;
      if (m.size() != size())
          return false;

      try {
          for (Entry<K, V> e : entrySet()) {
              K key = e.getKey();
              V value = e.getValue();
              if (value == null) {
                  if (!(m.get(key) == null && m.containsKey(key)))
                      return false;
              } else {
                  if (!value.equals(m.get(key)))
                      return false;
              }
          }
      } catch (ClassCastException unused) {
          return false;
      } catch (NullPointerException unused) {
          return false;
      }

      return true;
  }

  public int hashCode() {
      int h = 0;
      for (Entry<K, V> entry : entrySet())
          h += entry.hashCode();
      return h;
  }

  public String toString() {
      Iterator<Entry<K, V>> i = entrySet().iterator();
      if (!i.hasNext())
          return "{}";

      StringBuilder sb = new StringBuilder();
      sb.append('{');
      for (; ; ) {
          Entry<K, V> e = i.next();
          K key = e.getKey();
          V value = e.getValue();
          sb.append(key == this ? "(this Map)" : key);
          sb.append('=');
          sb.append(value == this ? "(this Map)" : value);
          if (!i.hasNext())
              return sb.append('}').toString();
          sb.append(',').append(' ');
      }
  }

  protected Object clone() throws CloneNotSupportedException {
      CompactAbstractMap<?, ?> result = (CompactAbstractMap<?, ?>) super.clone();
      return result;
  }

  public static class SimpleEntry<K, V>
          implements Entry<K, V>, java.io.Serializable {
      @java.io.Serial
      private static final long serialVersionUID = -8499721149061103585L;

      @SuppressWarnings("serial")
      private final K key;
      @SuppressWarnings("serial")
      private V value;

      public SimpleEntry(K key, V value) {
          this.key = key;
          this.value = value;
      }

      public SimpleEntry(Entry<? extends K, ? extends V> entry) {
          this.key = entry.getKey();
          this.value = entry.getValue();
      }

      public K getKey() {
          return key;
      }

      public V getValue() {
          return value;
      }

      public V setValue(V value) {
          V oldValue = this.value;
          this.value = value;
          return oldValue;
      }

      public boolean equals(Object o) {
          if (!(o instanceof Map.Entry))
              return false;
          Entry<?, ?> e = (Entry<?, ?>) o;
          return eq(key, e.getKey()) && eq(value, e.getValue());
      }

      public int hashCode() {
          return (key == null ? 0 : key.hashCode()) ^
                  (value == null ? 0 : value.hashCode());
      }

      public String toString() {
          return key + "=" + value;
      }

  }

  public static class SimpleImmutableEntry<K, V>
          implements Entry<K, V>, java.io.Serializable {
      @java.io.Serial
      private static final long serialVersionUID = 7138329143949025153L;

      @SuppressWarnings("serial")
      private final K key;
      @SuppressWarnings("serial")
      private final V value;

      public SimpleImmutableEntry(K key, V value) {
          this.key = key;
          this.value = value;
      }

      public SimpleImmutableEntry(Entry<? extends K, ? extends V> entry) {
          this.key = entry.getKey();
          this.value = entry.getValue();
      }

      public K getKey() {
          return key;
      }

      public V getValue() {
          return value;
      }

      public V setValue(V value) {
          throw new UnsupportedOperationException();
      }

      public boolean equals(Object o) {
          if (!(o instanceof Map.Entry))
              return false;
          Entry<?, ?> e = (Entry<?, ?>) o;
          return eq(key, e.getKey()) && eq(value, e.getValue());
      }

      public int hashCode() {
          return (key == null ? 0 : key.hashCode()) ^
                  (value == null ? 0 : value.hashCode());
      }

      public String toString() {
          return key + "=" + value;
      }
  }
}
static class Best<A> {
  A best;
  double score;
  boolean verboseNewBest, replaceIfSameScore;
  
  transient Object onChange;
  transient Object stringifier; // func(A) -> S
  
  synchronized boolean isNewBest(double score) {
    return best == null || !isNaN(score)
      && (replaceIfSameScore
        ? score >= this.score
        : score > this.score);
  }
  
  synchronized double bestScore() {
    return best == null ? minusInfinity() : score;
  }
  
  double score() { return bestScore(); }
  double getScore() { return bestScore(); }
  
  synchronized float floatScoreOr(float defaultValue) {
    return best == null ? defaultValue : (float) score;
  }
  
  boolean put(Pair<? extends A, Double> p) {
    return p != null && put(p.a, p.b);
  }
  
  boolean put(Best<? extends A> b) {
    return b != null && put(b.get(), b.score);
  }
  
  boolean put(A a, double score) {
    ping();
    boolean change = false;
    if (a != null) synchronized(this) {
      if (isNewBest(score)) {
        best = a;
        this.score = score;
        change = true;
      }
    }
    if (change) {
      if (verboseNewBest) print("New best! " + this);
      pcallF(onChange);
    }
    return change;
  }
  
  synchronized A get() { return best; }
  synchronized boolean has() { return best != null; }
  
  synchronized Pair<A, Double> pair() { return main.pair(best, bestScore()); }
  synchronized Scored<A> scored() { return best == null ? null : new Scored<A>(best, bestScore()); }
  
  synchronized A getIfScoreAbove(double x) { return score() >= x ? best : null; }
  
  public String toString() {
    return "Score " + formatDouble_significant2(score, 4) + ": " + callStringifier(stringifier, best);
  }
  
  boolean putAndPrintIfNewBest(A a, double score) {
    if (!put(a, score)) return false;
    { print(this); return true; }
  }
  
  synchronized void clear() { best = null; score = 0; }
}


static class Scored<A> extends Var<A> {
  float score;
  
  Scored() {}
  Scored(A a, float score) { super(a); this.score = score; }
  Scored(A a, double score) { super(a); this.score = (float) score; }
  
  float score() { return score; }
  
  public String toString() {
    return toIntPercent(score) + "%: " + str(get());
  }
}


static GlobalID aGlobalIDObjUnlessLoading() {
  return dynamicObjectIsLoading() ? null : aGlobalIDObj();
}


static long stopTiming_defaultMin = 10;

static long startTiming_startTime;
static void startTiming() {
  startTiming_startTime = now();
}

static void stopTiming() {
  stopTiming(null);
}

static void stopTiming(String text) {
  stopTiming(text, stopTiming_defaultMin);
}

static void stopTiming(String text, long minToPrint) {
  long time = now()-startTiming_startTime;
  if (time >= minToPrint) {
    text = or2(text, "Time: ");
    print(text + time + " ms");
  }
}


static float score(Scored s) {
  return s == null ? 0 : s.score();
}


static <A> float getScore(Scored<A> s) {
  return s == null ? 0 : s.score;
}


static String callStringifier(Object stringifier, Object o) {
  return stringifier != null ? str(callF(stringifier, o)) : str(o);
}


static <A> Value<A> value(A a) {
  return new Value<A>(a);
}


static double fracNonNeg(double d) {
  return frac_nonNeg(d);
}


static double twoPi() {
  return Math.PI*2;
}


static String plusPrefixUnlessMinus(String s) {
  return startsWith(s, "-") ? s : "+" + s;
}


static String formatDouble_significant2(double d, int digits) {
  try {
    digits -= max(0, Math.floor(Math.log10(abs(d))+1));
    return formatDouble(d, digits);
  } catch (Throwable _e) {
    print("Had number: " + d + ", digits: " + digits);
  
throw rethrow(_e); }
}


static <A, B> boolean isA(Either<A, B> e) {
  return eitherIsA(e);
}


static Set<java.util.Timer> _registerTimer_list = newWeakHashSet();

static void _registerTimer(java.util.Timer timer) {
  _registerTimer_list.add(timer);
}

static void cleanMeUp__registerTimer() {
  cancelTimers(getAndClearList(_registerTimer_list));
}


static String quoteBorderless(Object o) {
  if (o == null) return "null";
  return quoteBorderless(str(o));
}

static String quoteBorderless(String s) {
  if (s == null) return "null";
  StringBuilder out = new StringBuilder((int) (l(s)*1.5));
  quoteBorderless_impl(s, out);
  return out.toString();
}
  
static void quoteBorderless_impl(String s, StringBuilder out) {
  int l = s.length();
  for (int i = 0; i < l; i++) {
    char c = s.charAt(i);
    if (c == '\\' || c == '"')
      out.append('\\').append(c);
    else if (c == '\r')
      out.append("\\r");
    else if (c == '\n')
      out.append("\\n");
    else
      out.append(c);
  }
}


static JComponent unwrap(JComponent c) {
  return c instanceof JScrollPane ? unwrapScrollPane((JScrollPane) c) : c;
}


static boolean isNaN(double d) {
  return Double.isNaN(d);
}

static boolean isNaN(float f) {
  return Float.isNaN(f);
}


static double minusInfinity() {
  return negativeInfinity();
}


static <A> Scored<A> scored(A a, float score) {
  return new Scored(a, score);
}

static <A> Scored<A> scored(A a, double score) {
  return new Scored(a, (float) score);
}

static <A> Scored<A> scored(A a, Scored scored) {
  return new Scored(a, getScore(scored));
}

static <A> Scored<A> scored(double score, A a) {
  return scored(a, score);
}


static int toIntPercent(double ratio) {
  return roundToInt(ratio*100);
}

// when used with map function
static int toIntPercent(float ratio) {
  return toIntPercent((double) ratio);
}




static GlobalID aGlobalIDObj() {
  return asGlobalID(randomID(16));
}

static GlobalID aGlobalIDObj(Random random) {
  return asGlobalID(randomID(random, 16));
}


static double frac_nonNeg(double d) {
  return mod(d, 1);
}


static <A, B> boolean eitherIsA(Either<A, B> e) {
  return e != null && e.isA();
}


static void cancelTimers(Collection timers) {
  for (Object timer : timers) cancelTimer(timer);
}


static <A> List<A> getAndClearList(Collection<A> l) {
  if (l == null) return emptyList();
  synchronized(collectionMutex(l)) {
    List<A> out = cloneList(l);
    l.clear();
    return out;
  }
}


static JComponent unwrapScrollPane(JScrollPane sp) {
  return sp == null ? null : (JComponent) sp.getViewport().getView();
}


static double negativeInfinity() {
  return Double.NEGATIVE_INFINITY;
}


static int roundToInt(double d) {
  return (int) Math.round(d);
}




static GlobalID asGlobalID(String id) {
  return id == null ? null : new GlobalID(id);
}




static class Value<A> implements IF0<A> , IFieldsToList{
  A value;
  Value() {}
  Value(A value) {
  this.value = value;}

public boolean equals(Object o) {
if (!(o instanceof Value)) return false;
    Value __1 =  (Value) o;
    return eq(value, __1.value);
}

  public int hashCode() {
    int h = 82420049;
    h = boostHashCombine(h, _hashCode(value));
    return h;
  }
  public Object[] _fieldsToList() { return new Object[] {value}; }

  public A get() { return value; }
  
  public String toString() { return str(get()); }
}

}