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 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 javax.imageio.*;
import java.math.*;
public class main {
static class Chat {
  JTextArea chatArea = wrappedTextArea();
  JTextField chatField = new JTextField();
  Runnable onEnter_pre, onEnter_post;
  boolean showTimestamps = true;
  Object timeFormatter;
  File logFile;
  
  Chat() {
    onEnter(chatField, new Runnable() { public void run() { try { 
      pcallFunction(onEnter_pre);
      append(formatLine(chatField.getText()) + "\n");
      chatField.selectAll();
      pcallFunction(onEnter_post);
    } catch (Exception __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}});
  }
  
  void requestFocus() {
    chatField.requestFocus();
  }
  
  JComponent swing() {
    appendToFile(logFile, "\n");
    return withTitle("Chat", centerAndSouth(chatArea, chatField));
  }
  
  // override me if you freaking like
  String formatLine(String line) {
   return "Usr: " + line;
  }
  
  void append(String s) {
    if (showTimestamps)
      s = formatTime() + " " + s;
    chatArea.append(s);
    if (logFile != null)
      appendToFile(logFile, s);
  }
  
  String formatTime() {
    if (timeFormatter != null) return (String) callFunction(timeFormatter);
    return "[" + formatInt(month(), 2) + "/" + formatInt(days(), 2) + " " + formatInt(hours(), 2) + ":" + formatInt(minutes(), 2) + ":" + formatInt(seconds(), 2) + "]";
  }
  
  String input() {
    return chatField.getText().trim();
  }
  
  void setInput(String s) {
    chatField.setText(s);
    chatField.selectAll();
  }
} // Chat

static class BotChat {
  Chat chat;
  Object dialogBot, backend;
  JTextArea thoughtsArea;

  BotChat(Object dialogBot) { this(dialogBot, null); }
  
  BotChat(Object _dialogBot, Object backend) {
    if (backend instanceof String) backend = run((String) backend);
    this.backend = backend;
    dialogBot = _dialogBot;
    //callOpt(dialogBot, "noRewind");
    
    swingAndWait(new Runnable() { public void run() { try { 
      thoughtsArea = new JTextArea();
      
      chat = new Chat();
      chat.logFile = getProgramFile("log.txt");
  
      chat.onEnter_post = new Runnable() { public void run() { try { 
        try {
          fire(E.q(chat.input()));
        } catch (Throwable e) {
          postError(e);
        }
      } catch (Exception __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}};
      
      updateThoughts();
    
      JFrame frame = showFrame("Chat with " + programTitle(),
        hgrid(chat, withTitle("Thoughts", thoughtsArea)));
      setFrameWidth(frame, 800);
      chat.requestFocus();
    } catch (Exception __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}});
  }

  void postIt(final List<E> l) {
    try {
      for (int i = 0; i < l(l); i++) {
        E e = l.get(i);
        if (e.a())
          chatAppend("Bot: " + e.a + "\n");
        else if (e.state()) {
          chatAppend("[" + e.state + "]\n");
          final int _i = i+1;
          final Runnable l8r = new Runnable() { public void run() { try {  SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {
 postIt(subList(l, _i)); } catch (Exception _e) {
  throw _e instanceof RuntimeException ? (RuntimeException) _e : new RuntimeException(_e); } }
}); } catch (Exception __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}};
          if (isNewStyleBackend(backend)) {
            class TheBF implements BF {
              boolean working, unknown;
              String value;
              
              public void working() { working = true; }
              public void done() { l8r.run(); }
              public void value(Object o) { value = str(o); }
              public void unknown() { unknown = true; }
            };
            TheBF bf = new TheBF();
            Object exportedBF = proxy(_getClass(backend, "main.BF"), bf);
            print("Calling backend");
            call(backend, "action", e.state, exportedBF);
            if (bf.unknown) {
              print("Warning: Action unknown to backend: " + e.state);
            }
            if (bf.working) {
              print("Backend working");
              break;
            }
            if (bf.value != null)
              fire(E.state("result is " + bf.value));
          } else {
            if ((boolean) call(backend, "action", e.state, l8r))
              break;
          }
        }
      }
      updateThoughts();
    } catch (Throwable e) {
      postError(e);
    }
  }
  
  void chatAppend(String s) {
    if (hideLine(s)) return;
    chat.append(s);
  }
  
  // hide boring stuff
  boolean hideLine(String s) {
    return s.startsWith("[")
      && (matchStart("bot pauses", s)
       || matchStart("get", s));
  }
  
  void postError(Throwable e) {
    chat.append("= ERROR =\n" + getStackTrace(e) + "= ERROR =\n");
  }
  
  void updateThoughts() {
    String s;
    try {
      s = (String) callOpt(dialogBot, "thoughts");
    } catch (Throwable e) {
      s = getStackTrace(e);
    }
    if (empty(s)) s = "No thoughts in " + getClassName(dialogBot);   
    //print("Thoughts: " + s);
    thoughtsArea.setText(s);
  }
  
  void preset(String line) {
    chat.setInput(line);
  }
  
  void fire(E e) {
    call(dialogBot, "take", e);
    postIt(slurp(dialogBot, "getSingleOutput"));
  }
} // BotChat (new)
// adapters are immutable! (plus returns a modified clone)
abstract static class Adapter {
  abstract boolean canMatch(String in, String out);
  Adapter plus(String in, String out) { return this; }
  String get(String in) { return in; }
  abstract double size();
}

// node in flight (+ optional adapter)
static class InFlight {
  OccTree2 node;
  Adapter adapter;
  
  InFlight(OccTree2 node) {
  this.node = node;}
  InFlight(OccTree2 node, Adapter adapter) {
  this.adapter = adapter;
  this.node = node;}
  
  E adapted() {
    // TODO: security when rewriting states
    return adapter == null ? node.e : new E(adapter.get(node.e.text()), node.e.type());
  }
}

static class LooseBot {
  static boolean debug, repeat = true;
  OccTree2 tree;
  List<InFlight> nodes = new ArrayList<InFlight>();
  List<List> rewindStack = new ArrayList<List>();
  Object newAdapter;
  Object grabber; // func(S -> E)

  // WordAdapter now default
  LooseBot(OccTree2 tree) { this(tree, new Object() { Object get() { return  new WordAdapter(); }}); }
  LooseBot(OccTree2 tree, Object newAdapter) {
  this.newAdapter = newAdapter;
  this.tree = tree;}
  LooseBot(String theme) { this(makeOccTree(theme)); }
  LooseBot(String theme, Object newAdapter) { this(makeOccTree(theme), newAdapter); }
  
  Adapter makeAdapter() {
    return newAdapter == null ? null : (Adapter) call(newAdapter);
  }
  
  void take(E in) {
    remember();
    
    // try starting over from beginning of tree too
    if (repeat) setAdd(nodes, new InFlight(tree, makeAdapter()));
    
    List<InFlight> l = new ArrayList<InFlight>();
    
    // try grabber
    if (grabber != null && in.q()) try {
      E grabbed = (E) ( callFunction(grabber, in.text()));
      if (grabbed != null) {
        if (debug)
          print("Grabber worked! " + in.text() + " => " + grabbed);
          
        // make a mini tree with Q + A so it is matched in
        // next call to getSingleOutput()
        OccTree2 node = new OccTree2(in);
        node.next.add(new OccTree2(grabbed));
        
        l.add(new InFlight(node));
      } else
        if (debug)
          print("Grabber didnt't return anything. " + in.text());
    } catch (Throwable e) {
      if (debug) printStackTrace(e);
    }
    
    for (InFlight flight : nodes)
      for (OccTree2 n : flight.node.next)
        if (eq(n.e.type(), in.type()) && (flight.adapter != null
          ? flight.adapter.canMatch(n.e.text(), in.text())
          : matchE(n.e, in))) {
          l.add(new InFlight(n, flight.adapter == null ? null
            : flight.adapter.plus(n.e.text(), in.text())));
        }
    nodes = l;
  }
  
  void remember() {
    if (rewindStack != null)
      rewindStack.add(cloneList(nodes));
  }
  
  E getSingleOutput() {
    remember();
    List<InFlight> l = new ArrayList<InFlight>();
    for (InFlight flight : nodes)
      for (OccTree2 n : flight.node.next)
        if (n.e.a() || isCommand(n.e))
          l.add(new InFlight(n, flight.adapter));
    if (empty(l)) { rewind(); return null; }
    nodes = l;
    return first(nodes).adapted();
  }
  
  void rewind() {
    nodes = popLast(rewindStack);
  }
  
  boolean isCommand(E e) {
    return e.state() && matchStart("bot", e.state);
  }
  
  void noRewind() {
    rewindStack = null;
  }
  
  boolean matchE(E a, E b) {
    // standard NL matching
    return eq(a.type(), b.type()) && match(a.text(), b.text());
  }
  
  String nodesToString(List<InFlight> nodes) {
    List<String> l = new ArrayList<String>();
    l.add(n(l(nodes), "node") + "\n");
    for (InFlight flight : nodes) {
      l.add(str(flight.node.e));
      if (flight.adapter != null)
        l.add("  " + flight.adapter);
    }
    return rtrim(fromLines(l));
  }
  
  String thoughts() {
    List<String> l = new ArrayList<String>();
    l.add(nodesToString(nodes));
    if (rewindStack != null) {
      l.add("\nHistory\n");
      for (List<InFlight> nodes : reversedList(rewindStack))
        l.add(indent(nodesToString(nodes)));
    }
    return fromLines(l);
  }
}

static class WordAdapter extends Adapter {
  Map<String, String> wordMap = new TreeMap<String, String>();
  
  List<String> tok(String s) {
    return nlTok2(dropPunctuation2(s));
  }
  
  boolean canMatch(String in, String out) {
    List<String> t1 = tok(in), t2 = tok(out);
    return l(t1) == l(t2);
  }
  
  Adapter plus(String in, String out) {
    List<String> t1 = tok(in), t2 = tok(out);
    if (l(t1) != l(t2)) return this;
    
    WordAdapter a = (WordAdapter) ( nuObject(getClass()));
    a.wordMap = cloneMap(wordMap);
    for (int i = 1; i < l(t1); i += 2) {
      String w1 = t1.get(i), w2 = t2.get(i);
      if (!eqic(w1, w2))
        // just overwrite - be flexible!
        a.wordMap.put(w1.toLowerCase(), w2.toLowerCase());
    }
    return a;
  }
  
  String get(String s) {
    List<String> tok = nlTok2(s);
    for (int i = 1; i < l(tok); i += 2) {
      String w = lookupToken(tok.get(i));
      if (nempty(w))
        tok.set(i, w);
    }
    return join(tok);
  }
  
  String lookupToken(String s) {
    return wordMap.get(s.toLowerCase());
  }
  
  double size() {
    return l(wordMap);
  }
  
  public String toString() {
    return structure(wordMap);
  }
}

static class WordAdapter2 extends WordAdapter {
  Map<String, String> wordMap = new TreeMap<String, String>();
  
  List<String> tok(String s) {
    return nlTok2(s);
  }
} // LooseBot v5

static class DateAdapter extends WordAdapter2 {
  static List<String> months = splitAtSpace("___ January February March April May June July August September October November December");

  String lookupToken(String s) {
    int i = indexOf(months, s);
    if (i >= 0) try { 
      String j = lookupToken(str(i));
      print("Month found! " + s + " => " + i + " => " + j);
      if (nempty(j))
        return months.get(parseInt(j));
    } catch (Throwable __e) { printStackTrace(__e); }
    return super.lookupToken(s);
  }
  
  String get(String s) {
    String o = countFixer(super.get(s));
    print("count fixed: " + o);
    return o;
  }
}

static Object makeBot = new Object() { Object get() { 
  LooseBot bot = new LooseBot(botTree, new Object() { Object get() { return  new DateAdapter(); }}) {
    boolean isCommand(E e) {
      return !isInput(e);
    }
    
    // see Dialog Optimizer
    boolean isInput(E e) {
      return e.q()
        || e.state() && matchStart("result is ", e.text());
      }
    };
  bot.debug = true;
  return bot;
 }};

static void expandDialogs(List<Dialog> dialogs) {
  for (Dialog d : dialogs)
    for (int i = 0; i < l(d.poem); i++) {
      E e = d.poem.get(i);
      if (!e.state()) continue;
      int idx = e.text().indexOf('=');
      if (idx < 0) continue;
      d.poem.set(i, E.state("get " + e.text().substring(0, idx)));
      d.poem.add(i+1, E.state("result is " + e.text().substring(idx+1)));
    }
}
 // Engine

static OccTree2 botTree;

static Object makeBot(String theme) {
  List<Dialog> dialogs = loadDialogs(theme);
  expandDialogs(dialogs);
  botTree = makeOccTree(dialogs);
  return callF(makeBot);
}

public static void main(String[] args) throws Exception {
  substanceLAF("BusinessBlueSteel");
  new BotChat(makeBot("What's the date"), "#1003490").preset("What's the date, bot?");
}

static Class<?> getClass(String name) {
  try {
    return Class.forName(name);
  } catch (ClassNotFoundException e) {
    return null;
  }
}

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

static Class getClass(Object realm, String name) { try {
 
  return getClass(realm).getClassLoader().loadClass(classNameToVM(name));

} catch (Throwable __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}
static String formatInt(int i, int digits) {
  return padLeft(str(i), '0', digits);
}

static List<String> splitAtSpace(String s) {
  return asList(s.split("\\s+"));
}
static Object callF(Object f, Object... args) {
  return callFunction(f, args);
}
static Object callFunction(Object f, Object... args) {
  if (f == null) return null;
  if (f instanceof Runnable) {
    ((Runnable) f).run();
    return null;
  } else if (f instanceof String)
    return call(mc(), (String) f, args);
  else
    return call(f, "get", args);
  //else throw fail("Can't call a " + getClassName(f));
}
static Class<?> _getClass(String name) {
  try {
    return Class.forName(name);
  } catch (ClassNotFoundException e) {
    return null;
  }
}

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

static Class _getClass(Object realm, String name) { try {
 
  return getClass(realm).getClassLoader().loadClass(classNameToVM(name));

} catch (Throwable __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}
static Class run(String progID) {
  Class main = hotwire(progID);
  callMain(main);
  return main;
}
// reduced NL parsing without quoted strings and some other stuff

static List<String> nlTok2(String s) {
  List<String> tok = new ArrayList<String>();
  int l = s.length();
  
  int i = 0;
  while (i < l) {
    int j = i;
    char c;
    
    // scan for whitespace
    while (j < l) {
      c = s.charAt(j);
      if (c == ' ' || c == '\t' || c == '\r' || c == '\n')
        ++j;
      else
        break;
    }
    
    tok.add(s.substring(i, j));
    i = j;
    if (i >= l) break;
    c = s.charAt(i);

    // scan for non-whitespace
    if (Character.isJavaIdentifierStart(c))
      do ++j; while (j < l && (Character.isJavaIdentifierPart(s.charAt(j)) /*|| s.charAt(j) == '\''*/));
    else if (Character.isDigit(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 empty(Collection c) {
  return isEmpty(c);
}

static boolean empty(String s) {
  return isEmpty(s);
}

static boolean empty(Map map) {
  return map == null || map.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);
  return false;
}
static int seconds() {
  return Calendar.getInstance().get(Calendar.SECOND);
}
static void setFrameWidth(JFrame frame, int w) {
  frame.setSize(w, frame.getHeight());
}
static String getStackTrace(Throwable throwable) {
  StringWriter writer = new StringWriter();
  throwable.printStackTrace(new PrintWriter(writer));
  return writer.toString();
}
 // Let's just generally synchronize this to be safe.
 static synchronized void appendToFile(String path, String s) { try {
 
    new File(path).getParentFile().mkdirs();
    //print("[Logging to " + path + "]");
    Writer writer = new BufferedWriter(new OutputStreamWriter(
      new FileOutputStream(path, true), "UTF-8"));
    writer.write(s);
    writer.close();
  
} catch (Throwable __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}
  
  static void appendToFile(File path, String s) {
    if (path != null)
      appendToFile(path.getPath(), s);
  }

  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;
    List<String> tokpat = parse3(pat), toks = parse3(s);
    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;
    else {
      if (matches != null) {
        matches.m = new String[m.length+1];
        arraycopy(m, matches.m);
        matches.m[m.length] = join(toks.subList(tokpat.size(), toks.size())); // for Matches.rest()
      }
      return true;
    }
  }
  public static String join(String glue, Iterable<String> strings) {
    StringBuilder buf = new StringBuilder();
    Iterator<String> 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(Iterable<String> strings) {
    return join("", strings);
  }
  
  public static String join(String[] strings) {
    return join("", strings);
  }

static JTextArea wrappedTextArea() {
  JTextArea ta = new JTextArea();
  ta.setLineWrap(true);
  ta.setWrapStyleWord(true);
  return ta;
}
static <A> int indexOf(List<A> l, A a, int startIndex) {
  if (l == null) return -1;
  for (int i = startIndex; i < l(l); i++)
    if (eq(l.get(i), a))
      return i;
  return -1;
}

static <A> int indexOf(List<A> l, A a) {
  if (l == null) return -1;
  return l.indexOf(a);
}
static void swingAndWait(Runnable r) { try {
 
  EventQueue.invokeAndWait(r);

} catch (Throwable __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}
static void printStackTrace(Throwable e) {
  // we go to system.out now - system.err is nonsense
  print(getStackTrace(e));
}

static void printStackTrace() {
  printStackTrace(new Throwable());
}
static int minutes() {
  return Calendar.getInstance().get(Calendar.MINUTE);
}
static <A> List<A> cloneList(Collection<A> l) {
  //O mutex = getOpt(l, "mutex");
  /*if (mutex != null)
    synchronized(mutex) {
      ret new ArrayList<A>(l);
    }
  else
    ret new ArrayList<A>(l);*/
  // assume mutex is equal to collection, which will be true unless you explicitly pass a mutex to synchronizedList() which no one ever does.
  synchronized(l) {
    return new ArrayList<A>(l);
  }
}
static boolean eq(Object a, Object b) {
  if (a == null) return b == null;
  if (a.equals(b)) return true;
  if (a instanceof BigInteger) {
    if (b instanceof Integer) return a.equals(BigInteger.valueOf((Integer) b));
    if (b instanceof Long) return a.equals(BigInteger.valueOf((Long) b));
  }
  return false;
}
static String str(Object o) {
  return String.valueOf(o);
}
static List<String> dropPunctuation2(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)) /* && !dropPunctuation2_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 dropPunctuation2(String s) {
  return join(dropPunctuation(nlTok2(s)));
}
static void onEnter(JTextField tf, final Runnable action) {
  tf.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent evt) {
      try {
        action.run();
      } catch (Throwable e) {
        e.printStackTrace();
      }
    }
  });
}
static Object proxy(Class intrface, final Object target) {
   return java.lang.reflect.Proxy.newProxyInstance(intrface.getClassLoader(),
     new Class[] { intrface },
     new InvocationHandler() {
       public Object invoke(Object proxy, Method method, Object[] args) {
         return call(target, method.getName(), unnull(args));
       }
     });
}
static int month() {
  return Calendar.getInstance().get(Calendar.MONTH)+1;
}
static Object nuObject(String className, Object... args) { try {
 
  return nuObject(Class.forName(className), args);

} catch (Throwable __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}

static Object nuObject(Object realm, String className, Object... args) {
  return nuObject(getClass(realm, className), args);
}

static Object nuObject(Class c, Object... args) { try {
 
  Constructor m = nuObject_findConstructor(c, args);
  m.setAccessible(true);
  return m.newInstance(args);

} catch (Throwable __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__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 new RuntimeException("Constructor with " + args.length + " matching parameter(s) not found in " + c.getName());
}

 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 String getClassName(Object o) {
  return o == null ? "null" : o.getClass().getName();
}
static OccTree2 makeOccTree(String theme) {
  return makeOccTree(theme, true);
}

static OccTree2 makeOccTree(String theme, boolean print) {
  OccTree2 tree = goodDialogs2occTree2(loadDialogs(theme));
  if (print)
    printOccTree2(tree);
  return tree;
}

static OccTree2 makeOccTree(List<Dialog> dialogs) {
  OccTree2 tree = goodDialogs2occTree2(dialogs);
  printOccTree2(tree);
  return tree;
}
static JPanel hgrid(Object... parts) {
  JPanel panel = new JPanel();
  panel.setLayout(new GridLayout(1, parts.length));
  smartAdd(panel, parts);
  return panel;
}
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 + s.replace("\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<String>();
  for (String s : lines)
    l.add(indent + s);
  return l;
}
  static Object callOpt(Object o, String method, Object... args) {
    try {
      if (o == null) return null;
      if (o instanceof Class) {
        Method m = callOpt_findStaticMethod((Class) o, method, args, false);
        if (m == null) return null;
        m.setAccessible(true);
        return m.invoke(null, args);
      } else {
        Method m = callOpt_findMethod(o, method, args, false);
        if (m == null) return null;
        m.setAccessible(true);
        return m.invoke(o, args);
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  static Method callOpt_findStaticMethod(Class c, String method, Object[] args, boolean debug) {
    Class _c = c;
    while (c != null) {
      for (Method m : c.getDeclaredMethods()) {
        if (debug)
          System.out.println("Checking method " + m.getName() + " with " + m.getParameterTypes().length + " parameters");;
        if (!m.getName().equals(method)) {
          if (debug) System.out.println("Method name mismatch: " + method);
          continue;
        }

        if ((m.getModifiers() & Modifier.STATIC) == 0 || !callOpt_checkArgs(m, args, debug))
          continue;

        return m;
      }
      c = c.getSuperclass();
    }
    return null;
  }

  static Method callOpt_findMethod(Object o, String method, Object[] args, boolean debug) {
    Class c = o.getClass();
    while (c != null) {
      for (Method m : c.getDeclaredMethods()) {
        if (debug)
          System.out.println("Checking method " + m.getName() + " with " + m.getParameterTypes().length + " parameters");;
        if (m.getName().equals(method) && callOpt_checkArgs(m, args, debug))
          return m;
      }
      c = c.getSuperclass();
    }
    return null;
  }

  private static boolean callOpt_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 JPanel centerAndSouth(Component c, Component s) {
  JPanel panel = new JPanel(new BorderLayout());
  panel.add(BorderLayout.CENTER, wrap(c));
  panel.add(BorderLayout.SOUTH, wrap(s));
  return panel;
}
  public static String fromLines(List<String> lines) {
    StringBuilder buf = new StringBuilder();
    if (lines != null)
      for (String line : lines)
        buf.append(line).append('\n');
    return buf.toString();
  }
static void substanceLAF() {
  substanceLAF(null);
}

static void substanceLAF(String skinName) {
  try { 
    if (!substanceLookAndFeelEnabled()) {
      Object x = hotwire("#1003448");
      if (skinName != null)
        set(x, "skinName", skinName);
      runMain(x);
      JFrame.setDefaultLookAndFeelDecorated(substanceLookAndFeelEnabled());
    }
  } catch (Throwable __e) { printStackTrace(__e); }
}
static List<String> countFixer_them = splitAtSpace("nd st rd th");

static String countFixer(String s) {
  List<String> tok = nlTok2(s);
  for (int i = 1; i+2 < l(tok); i += 2)
    if (isInteger(tok.get(i)) && contains(countFixer_them, tok.get(i+2))) try { 
      // Is it correct?
      long l = parseLong(tok.get(i));
      long d = l % 10, h = l % 100;
      tok.set(i+2, h-d == 10 ? "th"
        : d == 1 ? "st"
        : d == 2 ? "nd"
        : d == 3 ? "rd"
        : "th");
    } catch (Throwable __e) { printStackTrace(__e); }
  return join(tok);
}
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 <A> boolean setAdd(Collection<A> c, A a) {
  if (c.contains(a)) return false;
  c.add(a);
  return true;
}
static File getProgramFile(String progID, String fileName) {
  return new File(getProgramDir(progID), fileName);
}

static File getProgramFile(String fileName) {
  return getProgramFile(getProgramID(), fileName);
}

static <A> List<A> reversedList(List<A> l) {
  List<A> x = cloneList(l);
  Collections.reverse(x);
  return x;
}
static int hours() {
  return Calendar.getInstance().get(Calendar.HOUR_OF_DAY);
}
static <A> List<A> subList(List<A> l, int startIndex) {
  return subList(l, startIndex, l(l));
}

static <A> List<A> subList(List<A> l, int startIndex, int endIndex) {
  startIndex = max(0, min(l(l), startIndex));
  endIndex = max(0, min(l(l), endIndex));
  if (startIndex > endIndex) return litlist();
  return l.subList(startIndex, endIndex);
}
static <A, B> Map<A, B> cloneMap(Map<A, B> map) {
  // assume mutex is equal to collection, which will be true unless you explicitly pass a mutex to synchronizedList() which no one ever does.
  synchronized(map) {
    return new HashMap(map);
  }
}
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);
}

public static String rtrim(String s) {
  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 boolean nempty(Collection c) {
  return !isEmpty(c);
}

static boolean nempty(CharSequence s) {
  return !isEmpty(s);
}

static boolean nempty(Object[] o) {
  return !isEmpty(o);
}

static boolean eqic(String a, String b) {
  if ((a == null) != (b == null)) return false;
  if (a == null) return true;
  return a.equalsIgnoreCase(b);
}
static int l(Object[] array) {
  return array == null ? 0 : array.length;
}

static int l(byte[] array) {
  return array == null ? 0 : array.length;
}

static int l(int[] array) {
  return array == null ? 0 : array.length;
}

static int l(char[] array) {
  return array == null ? 0 : array.length;
}

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

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

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


static String n(long l, String name) {
  return l + " " + (l == 1 ? name : getPlural(name));
}
static String structure(Object o) {
  HashSet refd = new HashSet();
  return structure_2(structure_1(o, 0, new IdentityHashMap(), refd), refd);
}

// leave to false, unless unstructure() breaks
static boolean structure_allowShortening = false;

static String structure_1(Object o, int stringSizeLimit, IdentityHashMap<Object, Integer> seen, HashSet<Integer> refd) {
  if (o == null) return "null";
  
  // these are never back-referenced (for readability)
  
  if (o instanceof String)
    return quote(stringSizeLimit != 0 ? shorten((String) o, stringSizeLimit) : (String) o);
    
  if (o instanceof BigInteger)
    return "bigint(" + o + ")";
  
  if (o instanceof Double)
    return "d(" + quote(str(o)) + ")";
    
  if (o instanceof Long)
    return o + "L";
  
  if (o instanceof Integer)
    return str(o);
    
  if (o instanceof Boolean)
    return ((Boolean) o).booleanValue() ? "t" : "f";
    
  if (o instanceof Character)
    return quoteCharacter((Character) o);
  
  Integer ref = seen.get(o);
  if (ref != null) {
    refd.add(ref);
    return "r" + ref;
  }
      
  ref = seen.size()+1;
  seen.put(o, ref);
  String r = "m" + ref + " "; // marker

  String name = o.getClass().getName();

  StringBuilder buf = new StringBuilder();
  
  if (o instanceof HashSet)
    return r + "hashset " + structure_1(new ArrayList((Set) o), stringSizeLimit, seen, refd);

  if (o instanceof TreeSet)
    return r + "treeset " + structure_1(new ArrayList((Set) o), stringSizeLimit, seen, refd);
  
  if (o instanceof Collection) {
    for (Object x : (Collection) o) {
      if (buf.length() != 0) buf.append(", ");
      buf.append(structure_1(x, stringSizeLimit, seen, refd));
    }
    return r + "[" + buf + "]";
  }
  
  if (o instanceof Map) {
    for (Object e : ((Map) o).entrySet()) {
      if (buf.length() != 0) buf.append(", ");
      buf.append(structure_1(((Map.Entry) e).getKey(), stringSizeLimit, seen, refd));
      buf.append("=");
      buf.append(structure_1(((Map.Entry) e).getValue(), stringSizeLimit, seen, refd));
    }
    return r + (o instanceof HashMap ? "hashmap" : "") + "{" + buf + "}";
  }
  
  if (o.getClass().isArray()) {
    int n = Array.getLength(o);
    for (int i = 0; i < n; i++) {
      if (buf.length() != 0) buf.append(", ");
      buf.append(structure_1(Array.get(o, i), stringSizeLimit, seen, refd));
    }
    return r + "array{" + buf + "}";
  }

  if (o instanceof Class)
    return r + "class(" + quote(((Class) o).getName()) + ")";
    
  if (o instanceof Throwable)
    return r + "exception(" + quote(((Throwable) o).getMessage()) + ")";
    
  if (o instanceof BitSet) {
    BitSet bs = (BitSet) o;
    for (int i = bs.nextSetBit(0); i >= 0; i = bs.nextSetBit(i+1)) {
      if (buf.length() != 0) buf.append(", ");
       buf.append(i);
    }
    return "bitset{" + buf + "}";
  }
    
  // Need more cases? This should cover all library classes...
  if (name.startsWith("java.") || name.startsWith("javax."))
    return r + String.valueOf(o);
    
  String shortName = o.getClass().getName().replaceAll("^main\\$", "");
  
  if (shortName.equals("Lisp")) {
    buf.append("l(" + structure_1(getOpt(o, "head"), stringSizeLimit, seen, refd));
    List args = (List) ( getOpt(o, "args"));
    if (nempty(args))
      for (int i = 0; i < l(args); i++) {
        buf.append(", ");
        Object arg = args.get(i);
        
        // sweet shortening
        if (arg != null && eq(arg.getClass().getName(), "main$Lisp") && isTrue(call(arg, "isEmpty")))
          arg = get(arg, "head");
          
        buf.append(structure_1(arg, stringSizeLimit, seen, refd));
      }
    buf.append(")");
    return r + str(buf);
  }
    
  int numFields = 0;
  String fieldName = "";
  if (shortName.equals("DynamicObject")) {
    shortName = (String) get(o, "className");
    Map<String, Object> fieldValues = (Map) get(o, "fieldValues");
    
    for (String _fieldName : fieldValues.keySet()) {
      fieldName = _fieldName;
      Object value = fieldValues.get(fieldName);
      if (value != null) {
        if (buf.length() != 0) buf.append(", ");
        buf.append(fieldName + "=" + structure_1(value, stringSizeLimit, seen, refd));
      }
      ++numFields;
   }
  } else {
    // regular class
    
    Class c = o.getClass();
    while (c != Object.class) {
      Field[] fields = c.getDeclaredFields();
      for (Field field : fields) {
        if ((field.getModifiers() & Modifier.STATIC) != 0)
          continue;
        fieldName = field.getName();
        
        // skip outer object reference
        if (fieldName.indexOf("$") >= 0) continue;
  
        Object value;
        try {
          field.setAccessible(true);
          value = field.get(o);
        } catch (Exception e) {
          value = "?";
        }
        
        // put special cases here...
    
        if (value != null) {
          if (buf.length() != 0) buf.append(", ");
          buf.append(fieldName + "=" + structure_1(value, stringSizeLimit, seen, refd));
        }
        ++numFields;
      }
      c = c.getSuperclass();
    }
  }
  
  String b = buf.toString();
  
  if (numFields == 1 && structure_allowShortening)
    b = b.replaceAll("^" + fieldName + "=", ""); // drop field name if only one
  String s = shortName;
  if (buf.length() != 0)
    s += "(" + b + ")";
  return r + s;
}

// drop unused markers
static String structure_2(String s, HashSet<Integer> refd) {
  List<String> tok = javaTok(s);
  StringBuilder out = new StringBuilder();
  for (int i = 1; i < l(tok); i += 2) {
    String t = tok.get(i);
    if (t.startsWith("m") && isInteger(t.substring(1))
      && !refd.contains(parseInt(t.substring(1))))
      continue;
    out.append(t).append(tok.get(i+1));
  }
  return str(out);
}

static String programTitle() {
  return getProgramName();
}
  static Object call(Object o) {
    return callFunction(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) {
    try {
      if (o instanceof Class) {
        Method m = call_findStaticMethod((Class) o, method, args, false);
        m.setAccessible(true);
        return m.invoke(null, args);
      } else {
        Method m = call_findMethod(o, method, args, false);
        m.setAccessible(true);
        return m.invoke(o, args);
      }
    } catch (Exception e) {
      throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);
    }
  }

  static Method call_findStaticMethod(Class c, String method, Object[] args, boolean debug) {
    Class _c = c;
    while (c != null) {
      for (Method m : c.getDeclaredMethods()) {
        if (debug)
          System.out.println("Checking method " + m.getName() + " with " + m.getParameterTypes().length + " parameters");;
        if (!m.getName().equals(method)) {
          if (debug) System.out.println("Method name mismatch: " + method);
          continue;
        }

        if ((m.getModifiers() & Modifier.STATIC) == 0 || !call_checkArgs(m, args, debug))
          continue;

        return m;
      }
      c = c.getSuperclass();
    }
    throw new RuntimeException("Method '" + method + "' (static) with " + args.length + " parameter(s) not found in " + _c.getName());
  }

  static Method call_findMethod(Object o, String method, Object[] args, boolean debug) {
    Class c = o.getClass();
    while (c != null) {
      for (Method m : c.getDeclaredMethods()) {
        if (debug)
          System.out.println("Checking method " + m.getName() + " with " + m.getParameterTypes().length + " parameters");;
        if (m.getName().equals(method) && call_checkArgs(m, args, debug))
          return m;
      }
      c = c.getSuperclass();
    }
    throw new RuntimeException("Method '" + method + "' (non-static) with " + args.length + " parameter(s) not found in " + o.getClass().getName());
  }

  private static boolean call_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 int parseInt(String s) {
  return empty(s) ? 0 : Integer.parseInt(s);
}
static List<Dialog> loadDialogs(String coreName) {
  List<Snippet> snippets = sortSnippetsByID(findDialogsNamed(coreName));
  //psl(snippets);
  List<Dialog> dialogs = new ArrayList<Dialog>();
  for (Snippet s : snippets) {
    Dialog d = new Dialog();
    d.snippet = s;
    d.good = !cic(javaTok(s.title), "bad");
    d.poem = parsePoem(s.id);
    dialogs.add(d);
  }
  //psl(dialogs);
  return dialogs;
}
static volatile StringBuffer local_log = new StringBuffer(); // not redirected
static volatile StringBuffer 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 void print() {
  print("");
}

// slightly overblown signature to return original object...
static <A> A print(A o) {
  String s = String.valueOf(o) + "\n";
  StringBuffer loc = local_log;
  StringBuffer 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);
  System.out.print(s);
  return o;
}

static void print(long l) {
  print(String.valueOf(l));
}

static void print(char c) {
  print(String.valueOf(c));
}

static void print_append(StringBuffer buf, String s, int max) {
  synchronized(buf) {
    buf.append(s);
    max /= 2;
    if (buf.length() > max) 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);
    }
  }
}
static JComponent withTitle(String title, Component c) {
  return withTitle(withTitle_titlePanel(title), c);
}

static JComponent withTitle_titlePanel(String title) {
  return new JLabel(title);
}

static JPanel withTitle(JComponent titleComponent, Component c) {
  titleComponent.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, UIManager.getColor("Button.borderColor")));
  JPanel panel = new JPanel(new BorderLayout());
  panel.setBackground(Color.WHITE);
  panel.add(BorderLayout.NORTH, titleComponent);
  panel.add(BorderLayout.CENTER, wrap(c));
  return panel;
}

static <A> A popLast(List<A> l) {
  return liftLast(l);
}
static void pcallFunction(Object f, Object... args) {
  try {  callFunction(f, args); } catch (Throwable __e) { printStackTrace(__e); }
}
static boolean isNewStyleBackend_debug;

static boolean isNewStyleBackend(Object backend) {
  String n = methodArgumentType(backend, "action", 1).getName();
  if (isNewStyleBackend_debug)
    print("n=" + n);
  return eq("main$BF", n);
}
static int days() {
  return Calendar.getInstance().get(Calendar.DAY_OF_MONTH);
}
static List slurp(Object o, String function) {
  List l = new ArrayList();
  Object x;
  while ((x = call(o, function)) != null)
    l.add(x);
  return l;
}
static Object first(Object list) {
  return ((List) list).isEmpty() ? null : ((List) list).get(0);
}

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

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

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(String title, Object content, boolean showIt) {
  JFrame frame = new JFrame(title);
  if (content != null)
    frame.getContentPane().add(wrap(content));
  frame.setBounds(300, 100, 500, 400);
  if (showIt)
    frame.setVisible(true);
  //callOpt(content, "requestFocus");
  //exitOnFrameClose(frame);
  return frame;
}
static <A> ArrayList<A> asList(A[] a) {
  return new ArrayList<A>(Arrays.asList(a));
}

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

static <A> ArrayList<A> asList(Collection<A> s) {
  return s == null ? new ArrayList() 
    : s instanceof ArrayList ? (ArrayList) s : new ArrayList(s);
}
  static String quote(String s) {
    if (s == null) return "null";
    return "\"" + s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\r", "\\r").replace("\n", "\\n") + "\"";
  }
  
  static String quote(long l) {
    return quote("" + l);
  }
  
  static String quote(char c) {
    return quote("" + c);
  }

static String shorten(String s, int max) {
  if (s == null) return "";
  return s.length() <= max ? s : s.substring(0, Math.min(s.length(), max)) + "...";
}
static String quoteCharacter(char c) {
  if (c == '\'') return "'\\''";
  if (c == '\\') return "'\\\\'";
  return "'" + c + "'";
}

static boolean substanceLookAndFeelEnabled() {
  return startsWith(getLookAndFeel(), "org.pushingpixels.");
}
static String getPlural(String s) {
  if (s.endsWith("y")) return dropSuffix("y", s) + "ies";
  return s + "s";
}
static void smartAdd(JPanel panel, Object... parts) {
  for (Object o : parts) {
    Component c;
    if (o instanceof String)
      c = new JLabel((String) o);
    else
      c = wrap(o);
    panel.add(c);
  }
}
static boolean contains(Collection c, Object o) {
  return c != null && c.contains(o);
}

static boolean contains(Object[] x, Object o) {
  if (x != null)
    for (Object a : x)
      if (eq(a, o))
        return true;
  return false;
}
static <A> A liftLast(List<A> l) {
  if (l.isEmpty()) return null;
  int i = l(l)-1;
  A a = l.get(i);
  l.remove(i);
  return a;
}
static void printOccTree2(OccTree2 t) {
  printOccTree2_sub(t, "");
}

static void printOccTree2_sub(OccTree2 t, String indent) {
  print(indent + "[" + t.count + "]" + (t.e == null ? "" : " " + t.e));
  for (OccTree2 t2 : t.next)
    printOccTree2_sub(t2, indent + "  ");
}
static String programID;

static String getProgramID() {
  return nempty(programID) ? formatSnippetID(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 String unnull(String s) {
    return s == null ? "" : s;
  }
  
  static <A> List<A> unnull(List<A> l) {
    return l == null ? emptyList() : l;
  }
  
  static Object[] unnull(Object[] a) {
    return a == null ? new Object[0] : a;
  }
// replacement for class JavaTok
// maybe incomplete, might want to add floating point numbers
// todo also: extended multi-line strings

static List<String> javaTok(String s) {
  List<String> tok = new ArrayList<String>();
  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 is not needed in rest of loop body
    cc = s.substring(i, Math.min(i+2, l));

    // 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 (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
      ++j;

    tok.add(s.substring(i, j));
    i = j;
  }
  
  if ((tok.size() % 2) == 0) tok.add("");
  return tok;
}

static List<String> javaTok(List<String> tok) {
  return javaTok(join(tok));
}
  static List<String> parse3(String s) {
    return dropPunctuation(javaTokPlusPeriod(s));
  }
static int min(int a, int 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 byte min(byte[] c) {
  byte x = 127;
  for (byte d : c) if (d < x) x = d;
  return x;
}
static void arraycopy(Object[] a, Object[] b) {
  int n = min(a.length, b.length);
  for (int i = 0; i < n; i++)
    b[i] = a[i];
}

static void arraycopy(Object src, int srcPos, Object dest, int destPos, int n) {
  System.arraycopy(src, srcPos, dest, destPos, n);
}
static <A> ArrayList<A> litlist(A... a) {
  return new ArrayList<A>(Arrays.asList(a));
}
// get purpose 1: access a list/array (safer version of x.get(y))

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

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

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

static Object get(Object o, String field) {
  if (o instanceof Class) return get((Class) o, field);
  
  if (o instanceof Map)
    return ((Map) o).get(field);
    
  if (o.getClass().getName().equals("main$DynamicObject"))
    return call(get_raw(o, "fieldValues"), "get", field);
    
  return get_raw(o, field);
}

static Object get_raw(Object o, String field) {
  try {
    Field f = get_findField(o.getClass(), field);
    f.setAccessible(true);
    return f.get(o);
  } catch (Exception e) {
    throw new RuntimeException(e);
  }
}

static Object get(Class c, String field) {
  try {
    Field f = get_findStaticField(c, field);
    f.setAccessible(true);
    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() & 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 void set(Object o, String field, Object value) {
    if (o instanceof Class) set((Class) o, field, value);
    else try {
      Field f = set_findField(o.getClass(), field);
      smartSet(f, o, value);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  
  static void set(Class c, String field, Object value) {
    try {
      Field f = set_findStaticField(c, field);
      smartSet(f, null, value);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  
  static Field set_findField(Class<?> c, String field) {
    for (Field f : c.getDeclaredFields())
      if (f.getName().equals(field))
        return f;
    throw new RuntimeException("Field '" + field + "' not found in " + c.getName());
  }
  
  static Field set_findStaticField(Class<?> c, String field) {
    for (Field f : c.getDeclaredFields())
      if (f.getName().equals(field) && (f.getModifiers() & Modifier.STATIC) != 0)
        return f;
    throw new RuntimeException("Static field '" + field + "' not found in " + c.getName());
  }
static int max(int a, int b) {
  return Math.max(a, b);
}

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 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 byte max(byte[] c) {
  byte x = -128;
  for (byte d : c) if (d > x) x = d;
  return x;
}
static List<Snippet> findDialogsNamed(String name) {
  List<Snippet> snippets = listSnippetsOfType(48); // TODO: optimize
  List<Snippet> dialogs = new ArrayList<Snippet>();
  for (Snippet s : snippets)
    if (swic(s.title.trim(), name))
      dialogs.add(s);
  return dialogs;
}
static Class methodArgumentType(Object o, String method, int pos) {
  return findMethodNamed(o, method).getParameterTypes()[pos];
}
static File getProgramDir() {
  return programDir();
}

static File getProgramDir(String snippetID) {
  return programDir(snippetID);
}
static boolean isTrue(Object o) {
  return booleanValue(o);
}
static long parseLong(String s) {
  if (s == null) return 0;
  return Long.parseLong(dropSuffix("L", s));
}

static long parseLong(Object s) {
  return Long.parseLong((String) s);
}
// c = Component or something implementing swing()
static Component wrap(Object swingable) {
  Component c = (Component) ( swingable instanceof Component ? swingable : call(swingable, "swing"));
  if (c instanceof JTable || c instanceof JList || c instanceof JTextArea)
    return new JScrollPane(c);
  return 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()) {
    /*if (debug)
      print("Size mismatch: " + structure(pat) + " vs " + structure(tok));*/
    return null;
  }
  for (int i = 1; i < pat.size(); i += 2) {
    String p = pat.get(i), t = tok.get(i);
    /*if (debug)
      print("Checking " + p + " against " + t);*/
    if (eq(p, "*"))
      result.add(t);
    else if (!equalsIgnoreCase(unquote(p), unquote(t))) // bold change - match quoted and unquoted now
      return null;
  }
  return result.toArray(new String[result.size()]);
}

static List<E> parsePoem(String input) {
  if (isSnippetID(input))
    return loadPoemAndParse(input);
  return parsePoem(toLinesFullTrim(input));
}

static List<E> parsePoem(List<String> lines) {
  char mode = ' ';
  List<E> entries = new ArrayList<E>();
  for (String line : lines) {
    E e = new E();
    if (line.startsWith("!")) {
      e.test = true;
      line = line.substring(1).trim();
    }
    if (swicAny(line, litlist("Q:", "U:", "Usr:"))) {
      mode = 'q';
      line = line.substring(line.indexOf(':')+1).trim();
    } else if (swicAny(line, litlist("A:", "B:", "Bot:"))) {
      mode = 'a';
      line = line.substring(line.indexOf(':')+1).trim();
    } else if (startsWith(line, "["))
      mode = 's';
    if (mode == 'q')
      e.q = line;
    else if (mode == 'a')
      e.a = line;
    else {
      String s = javaDropComments(line).trim();
      e.state = dropPrefix("[", dropSuffix("]", s)).trim();
    }
    entries.add(e);
  }
  return entries;
}
  // class Matches is added by #752
 
  static boolean match3(String pat, String s) {
    return match3(pat, s, null);
  }
  
  static boolean match3(String pat, String s, Matches matches) {
    if (s == null) return false;
    return match3(pat, parse3(s), matches);
  }
    
  static boolean match3(String pat, List<String> toks, Matches matches) {
    List<String> tokpat = parse3(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;
    else {
      if (matches != null) matches.m = m;
      return true;
    }
  }
static void callMain(Object c, String... args) {
  callOpt(c, "main", new Object[] {args});
}
static boolean isInteger(String s) {
  return s != null && Pattern.matches("\\-?\\d+", s);
}
static Class mc() {
  return getMainClass();
}
// extended over Class.isInstance() to handle primitive types
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 String classNameToVM(String name) {
  return name.replace(".", "$");
}
static OccTree2 goodDialogs2occTree2(List<Dialog> dialogs) {
  OccTree2 tree = new OccTree2();
  for (Dialog d : dialogs)
    if (d.good)
      tree.addScript(d.poem);
  return tree;
}
static void runMain(Object c, String... args) {
  callMain(c, args);
}
static boolean cic(List<String> l, String s) {
  return containsIgnoreCase(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 repeat(char c, int n) {
    n = 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) {
    List<A> l = new ArrayList<A>();
    for (int i = 0; i < n; i++)
      l.add(a);
    return l;
  }

  // compile JavaX source, load classes & return main class
  // src can be a snippet ID or actual source code
  // TODO: record injection?
  
  static Class<?> hotwire(String src) {
    try {
      Class j = getJavaX();
      
      synchronized(j) { // hopefully this goes well...
        List<File> libraries = new ArrayList<File>();
        File srcDir = (File) call(j, "transpileMain", src, libraries);
        if (srcDir == null)
          fail("transpileMain returned null (src=" + quote(src) + ")");
        
        Object androidContext = get(j, "androidContext");
        if (androidContext != null)
          return (Class) call(j, "loadx2android", srcDir, src);
          
        File classesDir = (File) call(j, "TempDirMaker_make");
        String javacOutput = (String) call(j, "compileJava", srcDir, libraries, classesDir);
        System.out.println(javacOutput);
        
        URL[] urls = new URL[libraries.size()+1];
        urls[0] = classesDir.toURI().toURL();
        for (int i = 0; i < libraries.size(); i++)
          urls[i+1] = libraries.get(i).toURI().toURL();
  
        // make class loader
        URLClassLoader classLoader = new URLClassLoader(urls);
    
        // load & return main class
        Class<?> theClass = classLoader.loadClass("main");
        
        callOpt(j, "registerSourceCode", theClass, loadTextFile(new File(srcDir, "main.java")));

        call(j, "setVars", theClass, isSnippetID(src) ? src: null);
        
        if (isSnippetID(src))
          callOpt(j, "addInstance", src, theClass);
          
        if (!_inCore())
          hotwire_copyOver(theClass);
  
        return theClass;
      }
    } catch (Exception e) {
      throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e);
    }
  }
static String padLeft(String s, char c, int n) {
  return rep(c, n-l(s)) + s;
}
static List<String> dropPunctuation_keep = litlist("*", "<", ">");

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 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(Map map) {
  return map == null || map.isEmpty();
}
static Object getOpt(Object o, String field) {
  if (o instanceof String) o = getBot ((String) o);
  if (o == null) return null;
  if (o instanceof Class) return getOpt((Class) o, field);
  
  if (o.getClass().getName().equals("main$DynamicObject"))
    return call(getOpt_raw(o, "fieldValues"), "get", field);
  
  if (o instanceof Map) return ((Map) o).get(field);
  
  return getOpt_raw(o, field);
}

static Object getOpt_raw(Object o, String field) {
  try {
    Field f = getOpt_findField(o.getClass(), field);
    if (f == null) return null;
    f.setAccessible(true);
    return f.get(o);
  } catch (Exception e) {
    throw new RuntimeException(e);
  }
}

static Object getOpt(Class c, String field) {
  try {
    Field f = getOpt_findStaticField(c, field);
    if (f == null) return null;
    f.setAccessible(true);
    return f.get(null);
  } catch (Exception e) {
    throw new RuntimeException(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() & Modifier.STATIC) != 0)
        return f;
    _c = _c.getSuperclass();
  } while (_c != null);
  return null;
}

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 String getProgramName_cache;

static synchronized String getProgramName() {
  if (getProgramName_cache == null)
    getProgramName_cache = getSnippetTitle(getProgramID());
  return getProgramName_cache;
}
static List<Snippet> sortSnippetsByID(List<Snippet> l) {
  return sorted(l, new Object() { Object get(Snippet a, Snippet b) { return  stdcompare(parseSnippetID(a.id), parseSnippetID(b.id)) ; }});
}

// 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)) {
        m.setAccessible(true);
        return m;
      }
    c = c.getSuperclass();
  }
  return null;
}
static Object getBot(String botID) {
  return callOpt(getMainBot(), "getBot", botID);
}

static boolean equalsIgnoreCase(String a, String b) {
  return a == null ? b == null : a.equalsIgnoreCase(b);
}
static String dropPrefix(String prefix, String s) {
  return s.startsWith(prefix) ? s.substring(l(prefix)) : s;
}
static List<E> loadPoemAndParse(String snippetID) {
  return parsePoem(loadPoem(snippetID).lines);
}
  static RuntimeException fail() {
    throw new RuntimeException("fail");
  }
  
  static RuntimeException fail(Object msg) {
    throw new RuntimeException(String.valueOf(msg));
  }
  
  static RuntimeException fail(String msg) {
    throw new RuntimeException(unnull(msg));
  }
    
  static RuntimeException fail(String msg, Object... args) {
    throw new RuntimeException(format(msg, args));
  }
static List sorted(List l, final Object comparator) {
  sort(l = cloneList(l), new Comparator() {
    public int compare(Object a, Object b) {
      return (int) callFunction(comparator, a, b);
    }
  });
  return l;
}
static List<String> toLinesFullTrim(String s) {
  List<String> l = toLines(s);
  for (ListIterator<String> i = l.listIterator(); i.hasNext(); ) {
    String line = i.next().trim();
    if (line.length() == 0)
      i.remove();
    else
      i.set(line);
  }
  return l;
}
static boolean swic(String a, String b) {
  return startsWithIgnoreCase(a, b);
}
static boolean _inCore() {
  return false;
}
  public static boolean isSnippetID(String s) {
    try {
      parseSnippetID(s);
      return true;
    } catch (RuntimeException e) {
      return false;
    }
  }
static Class getMainClass() { try {
 
  return Class.forName("main");

} catch (Throwable __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}

static Class getMainClass(Object o) { try {
 
  return (o instanceof Class ? (Class) o : o.getClass()).getClassLoader().loadClass("main");

} catch (Throwable __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}
static Class __javax;

static Class getJavaX() {
  return __javax;
}
static String javaDropComments(String s) {
  List<String> tok = javaTok(s);
  replaceLastElement(tok, "");
  return join(tok);
}
static String getSnippetTitle(String id) { try {
 
  return loadPage(new URL("http://tinybrain.de:8080/tb-int/getfield.php?id=" + parseSnippetID(id) + "&field=title"));

} catch (Throwable __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}
static void hotwire_copyOver(Class c) {
  synchronized(StringBuffer.class) {
    Object print_log = get(getMainClass(), "print_log");
    if (print_log != null)
      setOpt(c, "print_log", print_log);
      
    Object mainBot = getMainBot();
    if (mainBot != null)
      setOpt(c, "mainBot", mainBot);
  }
}
static String getLookAndFeel() {
  return getClassName(UIManager.getLookAndFeel());
}
static File programDir() {
  return programDir(getProgramID());
}

static File programDir(String snippetID) {
  return new File(javaxDataDir(), formatSnippetID(snippetID));
}

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

static String formatSnippetID(long id) {
  return "#" + id;
}
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);
}

// 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>();
  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 == '\u201C' || c == '\u201D') c = '"'; // normalize quotes
    if (c == '\'' || c == '"') {
      char opener = c;
      ++j;
      while (j < l) {
        char _c = s.charAt(j);
        if (_c == '\u201C' || _c == '\u201D') _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 booleanValue(Object o) {
  return eq(true, o);
}
static List emptyList() {
  return new ArrayList();
  //ret Collections.emptyList();
}
public static long parseSnippetID(String snippetID) {
  long id = Long.parseLong(shortenSnippetID(snippetID));
  if (id == 0) fail("0 is not a snippet ID");
  return id;
}
  public static String unquote(String s) {
    if (s.startsWith("[")) {
      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.startsWith("\"") /*&& s.endsWith("\"")*/ && s.length() > 1) {
      String st = s.substring(1, s.endsWith("\"") ? s.length()-1 : s.length());
      StringBuilder sb = new StringBuilder(st.length());
  
      for (int i = 0; i < st.length(); i++) {
        char ch = st.charAt(i);
        if (ch == '\\') {
          char nextChar = (i == st.length() - 1) ? '\\' : st
                  .charAt(i + 1);
          // Octal escape?
          if (nextChar >= '0' && nextChar <= '7') {
              String code = "" + nextChar;
              i++;
              if ((i < st.length() - 1) && st.charAt(i + 1) >= '0'
                      && st.charAt(i + 1) <= '7') {
                  code += st.charAt(i + 1);
                  i++;
                  if ((i < st.length() - 1) && st.charAt(i + 1) >= '0'
                          && st.charAt(i + 1) <= '7') {
                      code += st.charAt(i + 1);
                      i++;
                  }
              }
              sb.append((char) Integer.parseInt(code, 8));
              continue;
          }
          switch (nextChar) {
          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;
          case '\'':
              ch = '\'';
              break;
          // Hex Unicode: u????
          case 'u':
              if (i >= st.length() - 5) {
                  ch = 'u';
                  break;
              }
              int code = Integer.parseInt(
                      "" + st.charAt(i + 2) + st.charAt(i + 3)
                              + st.charAt(i + 4) + st.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();      
    } else
      return s; // return original
  }
  public static String loadTextFile(String fileName) {
    try {
      return loadTextFile(fileName, null);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }
  
  public static String loadTextFile(String fileName, String defaultContents) throws IOException {
    if (!new File(fileName).exists())
      return defaultContents;

    FileInputStream fileInputStream = new FileInputStream(fileName);
    InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, "UTF-8");
    return loadTextFile(inputStreamReader);
  }
  
  public static String loadTextFile(File fileName) {
    try {
      return loadTextFile(fileName, null);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  public static String loadTextFile(File fileName, String defaultContents) throws IOException {
    try {
      return loadTextFile(fileName.getPath(), defaultContents);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }
  
  public 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 builder.toString();
  }
static String dropSuffix(String suffix, String s) {
  return s.endsWith(suffix) ? s.substring(0, l(s)-l(suffix)) : s;
}
static void smartSet(Field f, Object o, Object value) throws Exception {
  f.setAccessible(true);
  
  // take care of common case (long to int)
  if (f.getType() == int.class && value instanceof Long)
    value = ((Long) value).intValue();
    
  f.set(o, value);
}
static boolean containsIgnoreCase(List<String> l, String s) {
  for (String x : l)
    if (eqic(x, s))
      return true;
  return false;
}

static boolean containsIgnoreCase(String[] l, String s) {
  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 class TableFinder {
  List<String> tok; // list of tokens in HTML document
  List<String> table; // list of tokens in table
  List<List<String>> rows; // for every row, list of tokens in row
  List<List<String>> data; // for every row, for every cell, inner data
  int i;
  boolean debug;
  
  void go(String html) {
    tok = htmlcoarsetok(html);
    i = 1;
      findTable();
  }

  boolean findTable() {
    if (debug) print("Finding table.");
    for (; i < tok.size(); i += 2)
      if (isTag(tok.get(i), "table"))
        for (int j = i+2; j < tok.size(); j += 2)
          if (isTag(tok.get(j), "/table")) {
            if (debug) print("Table found!");
            table = tok.subList(i-1, j+2);
            findRows();
            i = j;
            return true;
          }
    return false;
  }
  
  void findRows() {
    List<String> tok = table;
    rows = new ArrayList<List<String>>();
    data = new ArrayList<List<String>>();
    int rowStart = 0;
    
    for (int i = 1; i < table.size(); i += 2) {
      //print(tok.get(i));
      if (isTag(tok.get(i), "tr"))
        rowStart = i;
      else if (isTag(tok.get(i), "/tr") && rowStart != 0) {
        List<String> row = table.subList(rowStart-1, i+2);
        rows.add(row);
        data.add(getData(row));
      }
    }
    
    if (debug) print(rows.size() + " row(s)");
    if (debug) print("Top left cell: " + data.get(0).get(0));
  }
  
  boolean isTag(String token, String tag) {
    return token.regionMatches(true, 0, "<" + tag + " ", 0, tag.length()+2)
      || token.regionMatches(true, 0, "<" + tag + ">", 0, tag.length()+2);
  }
  
  // called internally
  List<String> getData(List<String> row) {
    int colStart = 0;
    List<String> cols = new ArrayList<String>();
    
    for (int i = 1; i < row.size(); i += 2) {
      String t = row.get(i);
      if (isTag(t, "td") || isTag(t, "th"))
        colStart = i;
      else if ((isTag(t, "/td") || isTag(t, "/th")) && colStart != 0)
        cols.add(join(row.subList(colStart+1, i)));
    }
    return cols;
  }
  
  // for clients
  List<String> getRow(int row) {
    return data.get(row);
  }
}
 // TableFinder

static class Snippet {
  String id, title, md5;
}

static List<Snippet> listSnippetsOfType(int type) {
  String html = loadPage("http://tinybrain.de:8080/tb/snippets.php?type=" + type + "&order=changed&reverse=1&longlist=1");
  
  TableFinder finder = new TableFinder();
  finder.go(html);
  
  List<Snippet> l = new ArrayList<Snippet>();
  for (int i = 1; i < finder.data.size(); i++) {
    List<String> row = finder.getRow(i);
    Snippet s = new Snippet();
    s.title = dropAllTags(htmldecode(row.get(0)));
    s.id = dropAllTags(row.get(1));
    s.md5 = dropAllTags(row.get(2));
    l.add(s);
  }
  return l;
}

static List<String> nlTok(String s) {
  return javaTokPlusPeriod(s);
}
static boolean swicAny(String a, List<String> l) {
  for (String b : l)
    if (swic(a, b)) return true; return false;
}
static int stdcompare(String a, String b) {
  return a == null ? b == null ? 0 : -1 : a.compareTo(b);
}

static int stdcompare(long a, long b) {
  return a < b ? -1 : a > b ? 1 : 0;
}
static boolean startsWith(String a, String b) {
  return a != null && a.startsWith(b);
}

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

static Text loadPoem(String snippetID) {
  return first(loadPoems(snippetID));
}

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 Object mainBot;

static Object getMainBot() {
  return mainBot;
}
static List<String> toLines(File f) {
  return toLines(loadTextFile(f));
}

  public 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;
  }

  private static int toLines_nextLineBreak(String s, int start) {
    for (int i = start; i < s.length(); i++) {
      char c = s.charAt(i);
      if (c == '\r' || c == '\n')
        return i;
    }
    return -1;
  }
static String htmldecode(final String input) {
  if (input == null) return null;
  
  final int MIN_ESCAPE = 2;
  final int MAX_ESCAPE = 6;

  StringWriter writer = null;
  int len = input.length();
  int i = 1;
  int st = 0;
  while (true) {
      // look for '&'
      while (i < len && input.charAt(i-1) != '&')
          i++;
      if (i >= len)
          break;

      // found '&', look for ';'
      int j = i;
      while (j < len && j < i + MAX_ESCAPE + 1 && input.charAt(j) != ';')
          j++;
      if (j == len || j < i + MIN_ESCAPE || j == i + MAX_ESCAPE + 1) {
          i++;
          continue;
      }

      // found escape 
      if (input.charAt(i) == '#') {
          // numeric escape
          int k = i + 1;
          int radix = 10;

          final char firstChar = input.charAt(k);
          if (firstChar == 'x' || firstChar == 'X') {
              k++;
              radix = 16;
          }

          try {
              int entityValue = Integer.parseInt(input.substring(k, j), radix);

              if (writer == null) 
                  writer = new StringWriter(input.length());
              writer.append(input.substring(st, i - 1));

              if (entityValue > 0xFFFF) {
                  final char[] chrs = Character.toChars(entityValue);
                  writer.write(chrs[0]);
                  writer.write(chrs[1]);
              } else {
                  writer.write(entityValue);
              }

          } catch (NumberFormatException ex) { 
              i++;
              continue;
          }
      }
      else {
          // named escape
          CharSequence value = htmldecode_lookupMap.get(input.substring(i, j));
          if (value == null) {
              i++;
              continue;
          }

          if (writer == null) 
              writer = new StringWriter(input.length());
          writer.append(input.substring(st, i - 1));

          writer.append(value);
      }

      // skip escape
      st = j + 1;
      i = st;
  }

  if (writer != null) {
      writer.append(input.substring(st, len));
      return writer.toString();
  }
  return input;
}

private static final String[][] htmldecode_ESCAPES = {
    {"\"",     "quot"}, // " - double-quote
    {"&",      "amp"}, // & - ampersand
    {"<",      "lt"}, // < - less-than
    {">",      "gt"}, // > - greater-than

    // Mapping to escape ISO-8859-1 characters to their named HTML 3.x equivalents.
    {"\u00A0", "nbsp"}, // non-breaking space
    {"\u00A1", "iexcl"}, // inverted exclamation mark
    {"\u00A2", "cent"}, // cent sign
    {"\u00A3", "pound"}, // pound sign
    {"\u00A4", "curren"}, // currency sign
    {"\u00A5", "yen"}, // yen sign = yuan sign
    {"\u00A6", "brvbar"}, // broken bar = broken vertical bar
    {"\u00A7", "sect"}, // section sign
    {"\u00A8", "uml"}, // diaeresis = spacing diaeresis
    {"\u00A9", "copy"}, // copyright sign
    {"\u00AA", "ordf"}, // feminine ordinal indicator
    {"\u00AB", "laquo"}, // left-pointing double angle quotation mark = left pointing guillemet
    {"\u00AC", "not"}, // not sign
    {"\u00AD", "shy"}, // soft hyphen = discretionary hyphen
    {"\u00AE", "reg"}, // registered trademark sign
    {"\u00AF", "macr"}, // macron = spacing macron = overline = APL overbar
    {"\u00B0", "deg"}, // degree sign
    {"\u00B1", "plusmn"}, // plus-minus sign = plus-or-minus sign
    {"\u00B2", "sup2"}, // superscript two = superscript digit two = squared
    {"\u00B3", "sup3"}, // superscript three = superscript digit three = cubed
    {"\u00B4", "acute"}, // acute accent = spacing acute
    {"\u00B5", "micro"}, // micro sign
    {"\u00B6", "para"}, // pilcrow sign = paragraph sign
    {"\u00B7", "middot"}, // middle dot = Georgian comma = Greek middle dot
    {"\u00B8", "cedil"}, // cedilla = spacing cedilla
    {"\u00B9", "sup1"}, // superscript one = superscript digit one
    {"\u00BA", "ordm"}, // masculine ordinal indicator
    {"\u00BB", "raquo"}, // right-pointing double angle quotation mark = right pointing guillemet
    {"\u00BC", "frac14"}, // vulgar fraction one quarter = fraction one quarter
    {"\u00BD", "frac12"}, // vulgar fraction one half = fraction one half
    {"\u00BE", "frac34"}, // vulgar fraction three quarters = fraction three quarters
    {"\u00BF", "iquest"}, // inverted question mark = turned question mark
    {"\u00C0", "Agrave"}, // ? - uppercase A, grave accent
    {"\u00C1", "Aacute"}, // ? - uppercase A, acute accent
    {"\u00C2", "Acirc"}, // ? - uppercase A, circumflex accent
    {"\u00C3", "Atilde"}, // ? - uppercase A, tilde
    {"\u00C4", "Auml"}, // ? - uppercase A, umlaut
    {"\u00C5", "Aring"}, // ? - uppercase A, ring
    {"\u00C6", "AElig"}, // ? - uppercase AE
    {"\u00C7", "Ccedil"}, // ? - uppercase C, cedilla
    {"\u00C8", "Egrave"}, // ? - uppercase E, grave accent
    {"\u00C9", "Eacute"}, // ? - uppercase E, acute accent
    {"\u00CA", "Ecirc"}, // ? - uppercase E, circumflex accent
    {"\u00CB", "Euml"}, // ? - uppercase E, umlaut
    {"\u00CC", "Igrave"}, // ? - uppercase I, grave accent
    {"\u00CD", "Iacute"}, // ? - uppercase I, acute accent
    {"\u00CE", "Icirc"}, // ? - uppercase I, circumflex accent
    {"\u00CF", "Iuml"}, // ? - uppercase I, umlaut
    {"\u00D0", "ETH"}, // ? - uppercase Eth, Icelandic
    {"\u00D1", "Ntilde"}, // ? - uppercase N, tilde
    {"\u00D2", "Ograve"}, // ? - uppercase O, grave accent
    {"\u00D3", "Oacute"}, // ? - uppercase O, acute accent
    {"\u00D4", "Ocirc"}, // ? - uppercase O, circumflex accent
    {"\u00D5", "Otilde"}, // ? - uppercase O, tilde
    {"\u00D6", "Ouml"}, // ? - uppercase O, umlaut
    {"\u00D7", "times"}, // multiplication sign
    {"\u00D8", "Oslash"}, // ? - uppercase O, slash
    {"\u00D9", "Ugrave"}, // ? - uppercase U, grave accent
    {"\u00DA", "Uacute"}, // ? - uppercase U, acute accent
    {"\u00DB", "Ucirc"}, // ? - uppercase U, circumflex accent
    {"\u00DC", "Uuml"}, // ? - uppercase U, umlaut
    {"\u00DD", "Yacute"}, // ? - uppercase Y, acute accent
    {"\u00DE", "THORN"}, // ? - uppercase THORN, Icelandic
    {"\u00DF", "szlig"}, // ? - lowercase sharps, German
    {"\u00E0", "agrave"}, // ? - lowercase a, grave accent
    {"\u00E1", "aacute"}, // ? - lowercase a, acute accent
    {"\u00E2", "acirc"}, // ? - lowercase a, circumflex accent
    {"\u00E3", "atilde"}, // ? - lowercase a, tilde
    {"\u00E4", "auml"}, // ? - lowercase a, umlaut
    {"\u00E5", "aring"}, // ? - lowercase a, ring
    {"\u00E6", "aelig"}, // ? - lowercase ae
    {"\u00E7", "ccedil"}, // ? - lowercase c, cedilla
    {"\u00E8", "egrave"}, // ? - lowercase e, grave accent
    {"\u00E9", "eacute"}, // ? - lowercase e, acute accent
    {"\u00EA", "ecirc"}, // ? - lowercase e, circumflex accent
    {"\u00EB", "euml"}, // ? - lowercase e, umlaut
    {"\u00EC", "igrave"}, // ? - lowercase i, grave accent
    {"\u00ED", "iacute"}, // ? - lowercase i, acute accent
    {"\u00EE", "icirc"}, // ? - lowercase i, circumflex accent
    {"\u00EF", "iuml"}, // ? - lowercase i, umlaut
    {"\u00F0", "eth"}, // ? - lowercase eth, Icelandic
    {"\u00F1", "ntilde"}, // ? - lowercase n, tilde
    {"\u00F2", "ograve"}, // ? - lowercase o, grave accent
    {"\u00F3", "oacute"}, // ? - lowercase o, acute accent
    {"\u00F4", "ocirc"}, // ? - lowercase o, circumflex accent
    {"\u00F5", "otilde"}, // ? - lowercase o, tilde
    {"\u00F6", "ouml"}, // ? - lowercase o, umlaut
    {"\u00F7", "divide"}, // division sign
    {"\u00F8", "oslash"}, // ? - lowercase o, slash
    {"\u00F9", "ugrave"}, // ? - lowercase u, grave accent
    {"\u00FA", "uacute"}, // ? - lowercase u, acute accent
    {"\u00FB", "ucirc"}, // ? - lowercase u, circumflex accent
    {"\u00FC", "uuml"}, // ? - lowercase u, umlaut
    {"\u00FD", "yacute"}, // ? - lowercase y, acute accent
    {"\u00FE", "thorn"}, // ? - lowercase thorn, Icelandic
    {"\u00FF", "yuml"}, // ? - lowercase y, umlaut
    
    {"'", "apos"}, // the controversial (but who cares!) &apos;
      // stackoverflow.com/questions/2083754/why-shouldnt-apos-be-used-to-escape-single-quotes
  };

private static final HashMap<String, CharSequence> htmldecode_lookupMap;
static {
    htmldecode_lookupMap = new HashMap<String, CharSequence>();
    for (final CharSequence[] seq : htmldecode_ESCAPES) 
        htmldecode_lookupMap.put(seq[1].toString(), seq[0]);
}
  static ThreadLocal<String> loadPage_charset = new ThreadLocal<String>();
  static boolean loadPage_allowGzip = true, loadPage_debug;

  public static String loadPageSilently(String url) {
    try {
      return loadPageSilently(new URL(loadPage_preprocess(url)));
    } catch (IOException e) { throw new RuntimeException(e); }
  }

  public static String loadPageSilently(URL url) {
    try {
      IOException e = null;
      for (int tries = 0; tries < 60; tries++)
        try {
          URLConnection con = url.openConnection();
          return loadPage(con, url);
        } catch (IOException _e) {
          e = _e;
          print("Retrying because of: " + e);
          sleepSeconds(1);
        }
      throw e;
    } catch (IOException e) { throw new RuntimeException(e); }
  }

  static String loadPage_preprocess(String url) {  
    if (url.startsWith("tb/"))
      url = "tinybrain.de:8080/" + url;
    if (url.indexOf("://") < 0)
      url = "http://" + url;
    return url;
  }
  
  public static String loadPage(String url) {
    try {
      return loadPage(new URL(loadPage_preprocess(url)));
    } catch (IOException e) { throw new RuntimeException(e); }
  }
  
  public static String loadPage(URL url) {
    print("Loading: " + url.toExternalForm());
    return loadPageSilently(url);
  }

  public static String loadPage(URLConnection con, URL url) throws IOException {
    String computerID = getComputerID();
    try {
      if (computerID != null)
        con.setRequestProperty("X-ComputerID", computerID);
      if (loadPage_allowGzip)
        con.setRequestProperty("Accept-Encoding", "gzip");
    } catch (Throwable e) {} // fails if within doPost
    String contentType = con.getContentType();
    if (contentType == null)
      throw new IOException("Page could not be read: " + url);
    //print("Content-Type: " + contentType);
    String charset = loadPage_charset == null ? null : loadPage_charset.get();
    if (charset == null) charset = loadPage_guessCharset(contentType);
    
    InputStream in = con.getInputStream();
    if ("gzip".equals(con.getContentEncoding())) {
      if (loadPage_debug)
        print("loadPage: Using gzip.");
      in = new GZIPInputStream(in);
    }
    Reader r = new InputStreamReader(in, charset);
    
    StringBuilder buf = new StringBuilder();
    while (true) {
      int ch = r.read();
      if (ch < 0)
        break;
      //Log.info("Chars read: " + buf.length());
      buf.append((char) ch);
    }
    return buf.toString();
  }
  
  static String loadPage_guessCharset(String contentType) {
    Pattern p = Pattern.compile("text/[a-z]+;\\s+charset=([^\\s]+)\\s*");
    Matcher m = p.matcher(contentType);
    /* If Content-Type doesn't match this pre-conception, choose default and hope for the best. */
    return m.matches() ? m.group(1) : "ISO-8859-1";
  }

// tok should be the output of htmlcoarsetok
static List<String> dropAllTags(List<String> tok) {
  List<String> list = new ArrayList<String>();
  for (int i = 0; i < tok.size(); i++) {
    String t = tok.get(i);
    if (t.startsWith("<")) {
      list.set(list.size()-1, list.get(list.size()-1) + tok.get(i+1));
      ++i;
    } else
      list.add(tok.get(i));
  }
  return list;
}

// alternatively, call this convenient function
static String dropAllTags(String html) {
  return join(dropAllTags(htmlcoarsetok(html)));
}
// works on lists and strings and null

static int indexOfIgnoreCase(Object a, Object b) {
  if (a == null) return -1;
  if (a instanceof String) {
     Matcher m = Pattern.compile((String) b, Pattern.CASE_INSENSITIVE + Pattern.LITERAL).matcher((String) a);
     if (m.find()) return m.start(); else return -1;
  }
  if (a instanceof List) {
    for (int i = 0; i < ((List) a).size(); i++) {
      Object o = ((List) a).get(i);
      if (o != null && ((String) o).equalsIgnoreCase((String) b))
        return i;
    }
    return -1;
  }
  throw fail("Unknown type: " + a);
}
static boolean startsWithIgnoreCase(String a, String b) {
  return a != null && a.regionMatches(true, 0, b, 0, b.length());
}
static boolean neq(Object a, Object b) {
  return !eq(a, b);
}
  static String format(String pat, Object... args) {
    return format3(pat, args);
  }

static <T> void sort(T[] a, Comparator<? super T> c) {
  Arrays.sort(a, c);
}

static <T> void sort(List<T> a, Comparator<? super T> c) {
  Collections.sort(a, c);
}
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 <A> void replaceLastElement(List<A> l, A a) {
  if (nempty(l))
    l.set(l(l)-1, a);
}
  static void setOpt(Object o, String field, Object value) {
    if (o instanceof Class) setOpt((Class) o, field, value);
    else try {
      Field f = setOpt_findField(o.getClass(), field);
      if (f != null)
        smartSet(f, o, value);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  
  static void setOpt(Class c, String field, Object value) {
    try {
      Field f = setOpt_findStaticField(c, field);
      if (f != null)
        smartSet(f, null, value);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
  
  static Field setOpt_findField(Class<?> c, String field) {
    for (Field f : c.getDeclaredFields())
      if (f.getName().equals(field))
        return f;
    return null;
  }
  
  static Field setOpt_findStaticField(Class<?> c, String field) {
    for (Field f : c.getDeclaredFields())
      if (f.getName().equals(field) && (f.getModifiers() & Modifier.STATIC) != 0)
        return f;
    return null;
  }

static String getComputerID() { try {
 
  return computerID();

} catch (Throwable __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}
static String _userHome;
static String userHome() {
  if (_userHome == null) {
    if (isAndroid())
      _userHome = "/storage/sdcard0/";
    else
      _userHome = System.getProperty("user.home");
    //System.out.println("userHome: " + _userHome);
  }
  return _userHome;
}

// TODO: process CDATA?

static List<String> htmlcoarsetok(String s) {
  List<String> tok = new ArrayList<String>();
  int l = s.length();
  
  int i = 0;
  while (i < l) {
    int j = i;
    char c;
    
    // scan for non-tags
    while (j < l) {
      if (s.charAt(j) != '<')
        // regular character
        ++j;
      else if (s.substring(j, Math.min(j+4, l)).equals("<!--")) {
        // HTML comment
        j = j+4;
        do ++j; while (j < l && !s.substring(j, Math.min(j+3, l)).equals("-->"));
        j = Math.min(j+3, l);
      } else
        // it's a tag
        break;
    }
    tok.add(s.substring(i, j));
    i = j;
    if (i >= l) break;
    c = s.charAt(i);

    // scan for tags
    if (c == '<') {
      ++j;
      
      while (j < l && s.charAt(j) != '>') ++j; // TODO: strings?
      if (j < l) ++j;
    }

    tok.add(s.substring(i, j));
    i = j;
  }
  
  if ((tok.size() % 2) == 0) tok.add("");
  return tok;
}
  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 void sleepSeconds(long s) {
  if (s > 0) sleep(s*1000);
}
static class Text {
  String title;
  List<String> lines = new ArrayList<String>();
}

static List<Text> loadPoems() {
  List<Text> texts = new ArrayList<Text>();

  List<Snippet> snippets = listSnippetsOfType(48); // "Natural Language"
  
  for (Snippet sn : snippets) try { 
    if (parseSnippetID(sn.id) < 1003000) break;
    texts.addAll(loadPoems(sn.id));
  } catch (Throwable __e) { printStackTrace(__e); }
  
  print("Got " + n(l(texts), "text") + " (" + structure(collect(texts, "title")) + ")");
  return texts;
}

static List<Text> loadPoems(String snippetID) {
  List<Text> texts = new ArrayList<Text>();

  List<String> lines = toLinesFullTrim(loadSnippet(snippetID));

  int i = 0;
  while (i < l(lines)) {
    Text text = new Text();
    
    // title
    if (i+1 < l(lines) && lines.get(i+1).startsWith("-")) {
      text.title = lines.get(i);
      i += 2;
    } else
      text.title = "";
      
    // text
    while (i < l(lines) &&
      (i+1 >= l(lines) || !lines.get(i+1).startsWith("-")))
      text.lines.add(lines.get(i++));
    texts.add(text);
  }
  
  return texts;
}

static void sleep(long ms) {
  try {
    Thread.sleep(ms);
  } catch (Exception e) { throw new RuntimeException(e); }
}

static void sleep() { try {
 
  print("Sleeping.");
  synchronized(main.class) { main.class.wait(); }

} catch (Throwable __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}
static boolean isIdentifier(String s) {
  return isJavaIdentifier(s);
}
static String _computerID;
public static String computerID() { try {
 
  if (_computerID == null) {
    File file = new File(userHome(), ".tinybrain/computer-id");
    _computerID = loadTextFile(file.getPath(), null);
    if (_computerID == null) {
      _computerID = makeRandomID(12);
      saveTextFile(file.getPath(), _computerID);
    }
  }
  return _computerID;

} catch (Throwable __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}
static boolean isNonNegativeInteger(String s) {
  return s != null && Pattern.matches("\\d+", s);
}
static List collect(Collection c, String field) {
  return collectField(c, field);
}
  static boolean preferCached = false;

  public static String loadSnippet(String snippetID) {
    try {
      return loadSnippet(parseSnippetID(snippetID), preferCached);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  public static String loadSnippet(String snippetID, boolean preferCached) throws IOException {
    return loadSnippet(parseSnippetID(snippetID), preferCached);
  }

  public static String loadSnippet(long snippetID, boolean preferCached) throws IOException {
    String text;
    
    // boss bot disabled for now for shorter transpilations
    
    /*text = getSnippetFromBossBot(snippetID);
    if (text != null) return text;*/
    
    initSnippetCache();
    text = DiskSnippetCache_get(snippetID);
    
    if (preferCached && text != null)
      return text;
    
    try {
      if (text != null) System.err.println("md5: " + md5(text));
      URL url = new URL("http://tinybrain.de:8080/getraw.php?id=" + snippetID + "&utf8=1");
      text = loadPage(url);
    } catch (RuntimeException e) {
      e.printStackTrace();
      throw new IOException("Snippet #" + snippetID + " not found or not public");
    }

    try {
      initSnippetCache();
      DiskSnippetCache_put(snippetID, text);
    } catch (IOException e) {
      System.err.println("Minor warning: Couldn't save snippet to cache ("  + DiskSnippetCache_getDir() + ")");
    }

    return text;
  }

  static File DiskSnippetCache_dir;

  public static void initDiskSnippetCache(File dir) {
    DiskSnippetCache_dir = dir;
    dir.mkdirs();
  }

  public static synchronized String DiskSnippetCache_get(long snippetID) throws IOException {
    return loadTextFile(DiskSnippetCache_getFile(snippetID).getPath(), null);
  }

  private static File DiskSnippetCache_getFile(long snippetID) {
    return new File(DiskSnippetCache_dir, "" + snippetID);
  }

  public static synchronized void DiskSnippetCache_put(long snippetID, String snippet) throws IOException {
    saveTextFile(DiskSnippetCache_getFile(snippetID).getPath(), snippet);
  }

  public static File DiskSnippetCache_getDir() {
    return DiskSnippetCache_dir;
  }

  public static void initSnippetCache() {
    if (DiskSnippetCache_dir == null)
      initDiskSnippetCache(new File(System.getProperty("user.home"), ".tinybrain/snippet-cache"));
  }
  
static boolean isAndroid() { return System.getProperty("java.vendor").toLowerCase().indexOf("android") >= 0; }

static boolean isJavaIdentifier(String s) {
  if (s.length() == 0 || !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;
}
static List collectField(Collection c, String field) {
  List l = new ArrayList();
  for (Object a : c)
    l.add(getOpt(a, field));
  return l;
}
static String md5(String text) { try {
 
  if (text == null) return "-";
  return bytesToHex(md5_impl(text.getBytes("UTF-8"))); // maybe different than the way PHP does it...

} catch (Throwable __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}

static String md5(byte[] data) {
  return bytesToHex(md5_impl(data));
}

static byte[] md5_impl(byte[] data) {
 try {
  return MessageDigest.getInstance("MD5").digest(data);
 } catch (Exception e) { throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e); }
}

static String md5(File file) { try {
 
  return md5(loadBinaryFile(file));

} catch (Throwable __e) { throw __e instanceof RuntimeException ? (RuntimeException) __e : new RuntimeException(__e); }}
  /** writes safely (to temp file, then rename) */
  public static void saveTextFile(String fileName, String contents) throws IOException {
    File file = new File(fileName);
    File parentFile = file.getParentFile();
    if (parentFile != null)
      parentFile.mkdirs();
    String tempFileName = fileName + "_temp";
    if (contents != null) {
      FileOutputStream fileOutputStream = new FileOutputStream(tempFileName);
      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 (!new File(tempFileName).renameTo(file))
        throw new IOException("Can't rename " + tempFileName + " to " + fileName);
  }
  
  public static void saveTextFile(File fileName, String contents) {
    try {
      saveTextFile(fileName.getPath(), contents);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

static String makeRandomID(int length) {
  Random random = new 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);
}

  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();
  }

public static byte[] loadBinaryFile(String fileName) throws IOException {
  if (!new File(fileName).exists())
    return null;

  FileInputStream in = new FileInputStream(fileName);

  byte buf[] = new byte[1024];
  ByteArrayOutputStream out = new ByteArrayOutputStream();
  int l;
  while (true) {
    l = in.read(buf);
    if (l <= 0) break;
    out.write(buf, 0, l);
  }
  in.close();
  return out.toByteArray();
}

public static byte[] loadBinaryFile(File file) throws IOException {
  return loadBinaryFile(file.getPath());
}


static interface BF {
  void working();
  void done();
  void value(Object o);
  void unknown();
}

// union-type entry
static class E {
  String q, a, state;
  boolean test; // not used anymore probably
  String comment; // not checked for comparison
  
  E() {}
  E(String text, String type) {
    if (eq(type, "q")) q = text;
    else if (eq(type, "a")) a = text;
    else state = text;
  }
  
  // does not compare the "test" bit
  boolean eqTo(E y) {
    E x = this;
    return eq/*ic*/(x.q, y.q) && eq/*ic*/(x.a, y.a) && eq/*ic*/(x.state, y.state);
  }
  
  public boolean equals(Object o) {
    return o instanceof E && eqTo((E) o);
  }
  
  public int hashCode() {
    return stdHash(this, "q", "a", "state");
  }
  
  static E state(String state) {
    if (state == null) return null; E e = new E(); e.state = state; return e;
  }
  
  static E q(String q) {
    if (q == null) return null; E e = new E(); e.q = q; return e;
  }
  
  static E a(String a) {
    if (a == null) return null; E e = new E(); e.a = a; return e;
  }
  
  public String toString() {
    return unparsePoemLine(this);
  }
  
  boolean q() { return q != null; }
  boolean a() { return a != null; }
  boolean state() { return state != null; }
  String type() { return q() ? "q" : a() ? "a" : "state"; }
  String text() { return q() ? q : a() ? a : state; }
}

static class Dialog {
  boolean good;
  Snippet snippet;
  List<E> poem;
}

static class OccTree2 {
  OccTree2 parent;
  E e;
  List<OccTree2> next = new ArrayList<OccTree2>();
  int count;
  
  OccTree2() {}
  OccTree2(E e) {
  this.e = e;}
  
  void addScript(List<E> script) {
    OccTree2 node = this;
    ++count;
    for (E c : script)
      (node = node.getOrMakeFollowUp(c)).count++;
  }
  
  OccTree2 getOrMakeFollowUp(E e) {
    OccTree2 node = followUp(e);
    if (node == null)
      next.add(node = newChild(e));
    return node;
  }
  
  OccTree2 followUp(E e) {
    for (OccTree2 t : next)
      if (eq(t.e, e))
        return t; return null;
  }
  
  List<E> followUpKeys() {
    return collect(next, "e");
  }
  
  OccTree2 newChild(E e) {
    OccTree2 child = new OccTree2(e);
    child.parent = this;
    return child;
  }
  
  // size including all nodes
  int size() {
    int n = 1;
    for (OccTree2 t : next)
      n += t.size();
    return n;
  }
  
  List<OccTree2> allLeaves() { List l = new ArrayList(); collectLeaves(l); return l; }
  
  // does not include root node as it has no input line associated
  List<OccTree2> allNodes() { List l = new ArrayList(); collectNodes(l); return l; }
  
  void collectLeaves(List<OccTree2> l) {
    if (isLeaf())
      l.add(this);
    else
      for (OccTree2 t : next)
        t.collectLeaves(l);
  }
  
  void collectNodes(List<OccTree2> l) {
    if (!isRoot())
      l.add(this);
    for (OccTree2 t : next)
      t.collectNodes(l);
  }
  
  boolean isRoot() { return e == null; }
  
  boolean isLeaf() {
    return !isRoot() && isEmpty(next);
  }
  
  int level() {
    int n = 0;
    OccTree2 node = this;
    while (node.parent != null) {
      ++n;
      node = node.parent;
    }
    return n;
  }
}

  static class Matches {
    String[] m;
    String get(int i) { return m[i]; }
    String unq(int i) { return unquote(m[i]); }
    String fsi(int i) { return formatSnippetID(unq(i)); }
    String fsi() { return fsi(0); }
    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)); }
  }


static boolean equals(Object a, Object b) {
  return a == null ? b == null : a.equals(b);
}
static int stdHash(Object a, String... fields) {
  if (a == null) return 0;
  int hash = 0;
  for (String field : fields)
    hash = hash*2+hashCode(getOpt(a, field));
  return hash;
}
static boolean isTag(String token, String tag) {
  return token.regionMatches(true, 0, "<" + tag + " ", 0, tag.length()+2)
    || token.regionMatches(true, 0, "<" + tag + ">", 0, tag.length()+2);
}
static String unparsePoemLine(E e) {
  return trim(unparsePoem(litlist(e)));
}

static String unparsePoem(List<E> poem) {
  List<String> l = new ArrayList<String>();
  for (E e : poem)
    if (e.q != null)
      l.add("Usr: " + trim(e.q));
    else if (e.a != null)
      l.add("Bot: " + trim(e.a));
    else if (e.state != null)
      l.add("[" + trim(e.state) + "]");
    else
      l.add("?");
  return joinLines(l);
}
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 int hashCode(Object a) {
  return a == null ? 0 : a.hashCode();
}

  public static String joinLines(List<String> lines) {
    return fromLines(lines);
  }
}