import javax.imageio.*;
import java.awt.image.*;
import java.awt.event.*;
import java.awt.*;
import java.security.NoSuchAlgorithmException;
import java.security.MessageDigest;
import java.lang.management.*;
import java.lang.ref.*;
import java.lang.reflect.*;
import java.net.*;
import java.io.*;
import javax.swing.table.*;
import javax.swing.text.*;
import javax.swing.event.*;
import javax.swing.*;
import java.util.concurrent.atomic.*;
import java.util.concurrent.*;
import java.util.regex.*;
import java.util.List;
import java.util.zip.*;
import java.util.*;

class RGB {
  public final float r, g, b;

  public RGB(float r, float g, float b) {
    this.r = r;
    this.g = g;
    this.b = b;
  }

  public RGB(double r, double g, double b) {
    this.r = (float) r;
    this.g = (float) g;
    this.b = (float) b;
  }

  public RGB(double brightness) {
    this.r = this.g = this.b = (float) brightness;
  }

  public RGB(Color color) {
    this.r = color.getRed()/255f;
    this.g = color.getGreen()/255f;
    this.b = color.getBlue()/255f;
  }

  public RGB(String hex) {
    r = Integer.parseInt(hex.substring(0, 2), 16)/255f;
    g = Integer.parseInt(hex.substring(2, 4), 16)/255f;
    b = Integer.parseInt(hex.substring(4, 6), 16)/255f;
  }

  public float getComponent(int i) {
    return i == 0 ? r : i == 1 ? g : b;
  }

  public Color getColor() {
    return new Color(r, g, b);
  }

  public static RGB newSafe(float r, float g, float b) {
    return new RGB(Math.max(0, Math.min(1, r)), Math.max(0, Math.min(1, g)), Math.max(0, Math.min(1, b)));
  }

  public int asInt() {
    return getColor().getRGB() & 0xFFFFFF;
  }

  public float getBrightness() {
    return (r+g+b)/3.0f;
  }

  public String getHexString() {
    return Integer.toHexString(asInt() | 0xFF000000).substring(2).toUpperCase();
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof RGB)) return false;

    RGB rgb = (RGB) o;

    if (Float.compare(rgb.b, b) != 0) return false;
    if (Float.compare(rgb.g, g) != 0) return false;
    if (Float.compare(rgb.r, r) != 0) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result = (r != +0.0f ? Float.floatToIntBits(r) : 0);
    result = 31 * result + (g != +0.0f ? Float.floatToIntBits(g) : 0);
    result = 31 * result + (b != +0.0f ? Float.floatToIntBits(b) : 0);
    return result;
  }

  public boolean isBlack() {
    return r == 0f && g == 0f && b == 0f;
  }

  public boolean isWhite() {
    return r == 1f && g == 1f && b == 1f;
  }

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

class RGBImage {
  private BufferedImage bufferedImage;
  private File file;
  private int width, height;
  private int[] pixels;

  // color returned when getPixel is called with out-of-bounds position
  private int background = 0xFFFFFF;

  public RGBImage(BufferedImage image) {
    this(image, null);
  }

  public RGBImage(BufferedImage image, File file) {
    this.file = file;
    bufferedImage = image;
    width = image.getWidth();
    height = image.getHeight();
    pixels = new int[width*height];
    PixelGrabber pixelGrabber = new PixelGrabber(image, 0, 0, width, height, pixels, 0, width);
    try {
      if (!pixelGrabber.grabPixels())
        throw new RuntimeException("Could not grab pixels");
      cleanPixels(); // set upper byte to 0
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
  }

  /** We assume it's a file name to load from */
  public RGBImage(String file) throws IOException {
    this(new File(file));
  }

  public RGBImage(Dimension size, Color color) {
    this(size.width, size.height, color);
  }

  public RGBImage(Dimension size, RGB color) {
    this(size.width, size.height, color);
  }

  private void cleanPixels() {
    for (int i = 0; i < pixels.length; i++)
      pixels[i] &= 0xFFFFFF;
  }

  public RGBImage(int width, int height, int[] pixels) {
    this.width = width;
    this.height = height;
    this.pixels = pixels;
  }

  public RGBImage(int w, int h, RGB[] pixels) {
    this.width = w;
    this.height = h;
    this.pixels = asInts(pixels);
  }

  public static int[] asInts(RGB[] pixels) {
    int[] ints = new int[pixels.length];
    for (int i = 0; i < pixels.length; i++)
      ints[i] = pixels[i] == null ? 0 : pixels[i].getColor().getRGB();
    return ints;
  }

  public RGBImage(int w, int h) {
    this(w, h, Color.black);
  }
  
  public RGBImage(int w, int h, RGB rgb) {
    this.width = w;
    this.height = h;
    this.pixels = new int[w*h];
    int col = rgb.asInt();
    if (col != 0)
      for (int i = 0; i < pixels.length; i++)
        pixels[i] = col;
  }

  public RGBImage(RGBImage image) {
    this(image.width, image.height, copyPixels(image.pixels));
  }

  public RGBImage(int width, int height, Color color) {
    this(width, height, new RGB(color));
  }

  public RGBImage(File file) throws IOException {
    this(javax.imageio.ImageIO.read(file));
  }

  private static int[] copyPixels(int[] pixels) {
    int[] copy = new int[pixels.length];
    System.arraycopy(pixels, 0, copy, 0, pixels.length);
    return copy;
  }

  public int getIntPixel(int x, int y) {
    if (inRange(x, y))
      return pixels[y * width + x];
    else
      return background;
  }

  public static RGB asRGB(int packed) {
    int r = (packed >> 16) & 0xFF;
    int g = (packed >> 8) & 0xFF;
    int b = packed & 0xFF;
    return new RGB(r / 255f, g / 255f, b / 255f);
  }

  public RGB getRGB(int x, int y) {
    if (inRange(x, y))
      return asRGB(pixels[y * width + x]);
    else
      return new RGB(background);
  }

  /** alias of getRGB - I kept typing getPixel instead of getRGB all the time, so I finally created it */
  public RGB getPixel(int x, int y) {
    return getRGB(x, y);
  }

  public int getWidth() {
    return width;
  }

  public int getHeight() {
    return height;
  }

  /** Attention: cached, i.e. does not change when image itself changes */
  /** @NotNull */
  public BufferedImage getBufferedImage() {
    if (bufferedImage == null) {
      bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
      //bufferedImage.setData(Raster.createRaster(new SampleModel()));
      for (int y = 0; y < height; y++)
        for (int x = 0; x < width; x++)
          bufferedImage.setRGB(x, y, pixels[y*width+x]);
    }
    return bufferedImage;
  }

  public RGBImage clip(Rectangle r) {
    r = fixClipRect(r);
    int[] newPixels;
    try {
      newPixels = new int[r.width*r.height];
    } catch (RuntimeException e) {
      System.out.println(r);
      throw e;
    }
    for (int y = 0; y < r.height; y++) {
      System.arraycopy(pixels, (y+r.y)*width+r.x, newPixels, y*r.width, r.width);
    }
    return new RGBImage(r.width, r.height, newPixels);
  }

  private Rectangle fixClipRect(Rectangle r) {
    r = r.intersection(new Rectangle(0, 0, width, height));
    if (r.isEmpty())
      r = new Rectangle(r.x, r.y, 0, 0);
    return r;
  }

  public File getFile() {
    return file;
  }

  /** can now also do GIF (not just JPEG) */
  public static RGBImage load(String fileName) {
    return load(new File(fileName));
  }

  /** can now also do GIF (not just JPEG) */
  public static RGBImage load(File file) {
    try {
      BufferedImage bufferedImage = javax.imageio.ImageIO.read(file);
      return new RGBImage(bufferedImage);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  public int getInt(int x, int y) {
    return pixels[y * width + x];
  }

  public void save(File file) throws IOException {
    String name = file.getName().toLowerCase();
    String type;
    if (name.endsWith(".png")) type = "png";
    else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) type = "jpeg";
    else throw new IOException("Unknown image extension: " + name);
    javax.imageio.ImageIO.write(getBufferedImage(), type, file);
  }

  public static RGBImage dummyImage() {
    return new RGBImage(1, 1, new int[] {0xFFFFFF});
  }

  public int[] getPixels() {
    return pixels;
  }

  public void setPixel(int x, int y, RGB rgb) {
    if (x >= 0 && y >= 0 && x < width && y < height)
      pixels[y*width+x] = rgb.asInt();
  }

  public void setPixel(int x, int y, Color color) {
    setPixel(x, y, new RGB(color));
  }

  public void setPixel(int x, int y, int rgb) {
    if (x >= 0 && y >= 0 && x < width && y < height)
      pixels[y*width+x] = rgb;
  }

  public RGBImage copy() {
    return new RGBImage(this);
  }

  public boolean inRange(int x, int y) {
    return x >= 0 && y >= 0 && x < width && y < height;
  }

  public int getBackground() {
    return background;
  }

  public void setBackground(int background) {
    this.background = background;
  }

  public Dimension getSize() {
    return new Dimension(width, height);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    RGBImage rgbImage = (RGBImage) o;

    if (height != rgbImage.height) return false;
    if (width != rgbImage.width) return false;
    if (!Arrays.equals(pixels, rgbImage.pixels)) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result = width;
    result = 31 * result + height;
    result = 31 * result + Arrays.hashCode(pixels);
    return result;
  }

  public String getHex(int x, int y) {
    return getPixel(x, y).getHexString();
  }

  public RGBImage clip(int x, int y, int width, int height) {
    return clip(new Rectangle(x, y, width, height));
  }

  public RGBImage clipLine(int y) {
    return clip(0, y, width, 1);
  }

  public int numPixels() {
    return width*height;
  }
}

abstract class Surface extends JPanel {


  public Object AntiAlias = RenderingHints.VALUE_ANTIALIAS_ON;
  public Object Rendering = RenderingHints.VALUE_RENDER_SPEED;
  public AlphaComposite composite;
  public Paint texture;
  public BufferedImage bimg;
  public int imageType;
  public String name;
  public boolean clearSurface = true;
  // Demos using animated gif's that implement ImageObserver set dontThread.
  public boolean dontThread;

  protected long sleepAmount = 50; // max20 fps

  private long orig, start, frame;
  private Toolkit toolkit;
  private boolean perfMonitor, outputPerf;
  private int biw, bih;
  private boolean clearOnce;
  private boolean toBeInitialized = true;


  public Surface() {
    setDoubleBuffered(false);
    toolkit = getToolkit();
    name = this.getClass().getName();
    name = name.substring(name.indexOf(".", 7)+1);
    setImageType(0);

    // To launch an individual demo with the performance str output  :
    //    java -Djava2demo.perf= -cp Java2Demo.jar demos.Clipping.ClipAnim
    try {
      if (System.getProperty("java2demo.perf") != null) {
        perfMonitor = outputPerf = true;
      }
    } catch (Exception ex) { }
  }


    /*protected Image getImage(String name) {
        return DemoImages.getImage(name, this);
    }


    protected Font getFont(String name) {
        return DemoFonts.getFont(name);
    }*/


  public int getImageType() {
    return imageType;
  }


  public void setImageType(int imgType) {
    if (imgType == 0) {
      imageType = 1;
    } else {
      imageType = imgType;
    }
    bimg = null;
  }


  public void setAntiAlias(boolean aa) {
    AntiAlias = aa
      ? RenderingHints.VALUE_ANTIALIAS_ON
      : RenderingHints.VALUE_ANTIALIAS_OFF;
  }


  public void setRendering(boolean rd) {
    Rendering = rd
      ? RenderingHints.VALUE_RENDER_QUALITY
      : RenderingHints.VALUE_RENDER_SPEED;
  }


  public void setTexture(Object obj) {
    if (obj instanceof GradientPaint) {
      texture = new GradientPaint(0, 0, Color.white,
        getSize().width*2, 0, Color.green);
    } else {
      texture = (Paint) obj;
    }
  }


  public void setComposite(boolean cp) {
    composite = cp
      ? AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f)
      : null;
  }


  public void setMonitor(boolean pm) {
    perfMonitor = pm;
  }


  public void setSleepAmount(long amount) {
    sleepAmount = amount;
  }


  public long getSleepAmount() {
    return sleepAmount;
  }


  public BufferedImage createBufferedImage(Graphics2D g2,
                                           int w,
                                           int h,
                                           int imgType) {
    BufferedImage bi = null;
    if (imgType == 0) {
      bi = (BufferedImage) g2.getDeviceConfiguration().
        createCompatibleImage(w, h);
    } else if (imgType > 0 && imgType < 14) {
      bi = new BufferedImage(w, h, imgType);
    } else if (imgType == 14) {
      bi = createBinaryImage(w, h, 2);
    } else if (imgType == 15) {
      bi = createBinaryImage(w, h, 4);
    } else if (imgType == 16) {
      bi = createSGISurface(w, h, 32);
    } else if (imgType == 17) {
      bi = createSGISurface(w, h, 16);
    }
    biw = w;
    bih = h;
    return bi;
  }


  // Lookup tables for BYTE_BINARY 1, 2 and 4 bits.
  static byte[] lut1Arr = new byte[] {0, (byte)255 };
  static byte[] lut2Arr = new byte[] {0, (byte)85, (byte)170, (byte)255};
  static byte[] lut4Arr = new byte[] {0, (byte)17, (byte)34, (byte)51,
    (byte)68, (byte)85,(byte) 102, (byte)119,
    (byte)136, (byte)153, (byte)170, (byte)187,
    (byte)204, (byte)221, (byte)238, (byte)255};


  private BufferedImage createBinaryImage(int w, int h, int pixelBits) {
    int bytesPerRow = w * pixelBits / 8;
    if (w * pixelBits % 8 != 0) {
      bytesPerRow++;
    }
    byte[] imageData = new byte[h * bytesPerRow];
    IndexColorModel cm = null;
    switch (pixelBits) {
      case 1:
        cm = new IndexColorModel(pixelBits, lut1Arr.length,
          lut1Arr, lut1Arr, lut1Arr);
        break;
      case 2:
        cm = new IndexColorModel(pixelBits, lut2Arr.length,
          lut2Arr, lut2Arr, lut2Arr);
        break;
      case 4:
        cm = new IndexColorModel(pixelBits, lut4Arr.length,
          lut4Arr, lut4Arr, lut4Arr);
        break;
      default:
      {new Exception("Invalid # of bit per pixel").printStackTrace();}
    }

    DataBuffer db = new DataBufferByte(imageData, imageData.length);
    WritableRaster r = Raster.createPackedRaster(db, w, h, pixelBits, null);
    return new BufferedImage(cm, r, false, null);
  }

  private BufferedImage createSGISurface(int w, int h, int pixelBits) {
    int rMask32 = 0xFF000000;
    int rMask16 = 0xF800;
    int gMask32 = 0x00FF0000;
    int gMask16 = 0x07C0;
    int bMask32 = 0x0000FF00;
    int bMask16 = 0x003E;

    DirectColorModel dcm = null;
    DataBuffer db = null;
    WritableRaster wr = null;
    switch (pixelBits) {
      case 16:
        short[] imageDataUShort = new short[w * h];
        dcm = new DirectColorModel(16, rMask16, gMask16, bMask16);
        db = new DataBufferUShort(imageDataUShort, imageDataUShort.length);
        wr = Raster.createPackedRaster(db, w, h, w,
          new int[] {rMask16, gMask16, bMask16},
          null);
        break;
      case 32:
        int[] imageDataInt = new int[w * h];
        dcm = new DirectColorModel(32, rMask32, gMask32, bMask32);
        db = new DataBufferInt(imageDataInt, imageDataInt.length);
        wr = Raster.createPackedRaster(db, w, h, w,
          new int[] {rMask32, gMask32, bMask32},
          null);
        break;
      default:
      {new Exception("Invalid # of bit per pixel").printStackTrace();}
    }

    return new BufferedImage(dcm, wr, false, null);
  }

  public Graphics2D createGraphics2D(int width,
                                     int height,
                                     BufferedImage bi,
                                     Graphics g) {

    Graphics2D g2 = null;

    if (bi != null) {
      g2 = bi.createGraphics();
    } else {
      g2 = (Graphics2D) g;
    }

    g2.setBackground(getBackground());
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, AntiAlias);
    g2.setRenderingHint(RenderingHints.KEY_RENDERING, Rendering);

    if (clearSurface || clearOnce) {
      g2.clearRect(0, 0, width, height);
      clearOnce = false;
    }

    if (texture != null) {
      // set composite to opaque for texture fills
      g2.setComposite(AlphaComposite.SrcOver);
      g2.setPaint(texture);
      g2.fillRect(0, 0, width, height);
    }

    if (composite != null) {
      g2.setComposite(composite);
    }

    return g2;
  }

  public abstract void render(int w, int h, Graphics2D g);


  /**
   * It's possible to turn off double-buffering for just the repaint
   * calls invoked directly on the non double buffered component.
   * This can be done by overriding paintImmediately() (which is called
   * as a result of repaint) and getting the current RepaintManager and
   * turning off double buffering in the RepaintManager before calling
   * super.paintImmediately(g).
   */
  public void paintImmediately(int x,int y,int w, int h) {
    RepaintManager repaintManager = null;
    boolean save = true;
    if (!isDoubleBuffered()) {
      repaintManager = RepaintManager.currentManager(this);
      save = repaintManager.isDoubleBufferingEnabled();
      repaintManager.setDoubleBufferingEnabled(false);
    }
    super.paintImmediately(x, y, w, h);

    if (repaintManager != null) {
      repaintManager.setDoubleBufferingEnabled(save);
    }
  }


  public void paint(Graphics g) {

    Dimension d = getSize();

    if (imageType == 1)
      bimg = null;
    else if (bimg == null || biw != d.width || bih != d.height) {
      bimg = createBufferedImage((Graphics2D)g,
        d.width, d.height, imageType-2);
      clearOnce = true;
      toBeInitialized = true;
    }

    if (toBeInitialized) {
      toBeInitialized = false;
      startClock();
    }

    Graphics2D g2 = createGraphics2D(d.width, d.height, bimg, g);
    render(d.width, d.height, g2);
    g2.dispose();

    if (bimg != null)  {
      g.drawImage(bimg, 0, 0, null);
      toolkit.sync();
    }

  }


  public void startClock() {
    orig = System.currentTimeMillis();
    start = orig;
    frame = 0;
  }

  private static final int REPORTFRAMES = 30;


  public static void setAlpha(Graphics2D g, float alpha) {
    g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
  }
}

class ImageSurface extends Surface {
  private BufferedImage image;
  private double zoomX = 1, zoomY = 1;
  private Rectangle selection;

  public ImageSurface() {
    this(new RGBImage(1, 1, new int[] { 0xFFFFFF }));
  }

  public ImageSurface(RGBImage image) {
    this(image.getBufferedImage());
  }

  public ImageSurface(BufferedImage image) {
    clearSurface = false;
    this.image = image;

    /*addMouseMotionListener(new MouseAdapter() {
      public void mouseMoved(MouseEvent e) {
        getMousePosition()

      }
    });*/
  }

  public ImageSurface(RGBImage image, double zoom) {
    this(image);
    setZoom(zoom);
  }

  public void render(int w, int h, Graphics2D g) {
    g.setColor(Color.white);
    g.fillRect(0, 0, w, h);
    if (image != null)
      g.drawImage(image, 0, 0, getZoomedWidth(), getZoomedHeight(), null);

    if (selection != null) {
      // drawRect is inclusive, selection is exclusive, so... whatever, tests show it's cool.
      drawSelectionRect(g, selection, Color.green, Color.white);
    }
  }

  public void drawSelectionRect(Graphics2D g, Rectangle selection, Color green, Color white) {
    g.setColor(green);
    int top = (int) (selection.y * zoomY);
    int bottom = (int) ((selection.y+selection.height) * zoomY);
    int left = (int) (selection.x * zoomX);
    int right = (int) ((selection.x+selection.width) * zoomX);
    g.drawRect(left-1, top-1, right-left+1, bottom-top+1);
    g.setColor(white);
    g.drawRect(left - 2, top - 2, right - left + 3, bottom - top + 3);
  }

  public void setZoom(double zoom) {
    setZoom(zoom, zoom);
  }

  public void setZoom(double zoomX, double zoomY) {
    this.zoomX = zoomX;
    this.zoomY = zoomY;
    revalidate();
    repaint();
  }

  public Dimension getMinimumSize() {
    int w = getZoomedWidth();
    int h = getZoomedHeight();
    Dimension min = super.getMinimumSize();
    return new Dimension(Math.max(w, min.width), Math.max(h, min.height));
  }

  private int getZoomedHeight() {
    return (int) (image.getHeight() * zoomY);
  }

  private int getZoomedWidth() {
    return (int) (image.getWidth() * zoomX);
  }

  public void setImage(RGBImage image) {
    setImage(image.getBufferedImage());
  }

  public void setImage(BufferedImage image) {
    this.image = image;
    revalidate();
    repaint();
  }

  public BufferedImage getImage() {
    return image;
  }

  public double getZoomX() {
    return zoomX;
  }

  public double getZoomY() {
    return zoomY;
  }

  public Dimension getPreferredSize() {
    return new Dimension(getZoomedWidth(), getZoomedHeight());
  }

  /** returns a scrollpane with the scroll-mode prevent-garbage-drawing fix applied */
  public JScrollPane makeScrollPane() {
    JScrollPane scrollPane = new JScrollPane(this);
    scrollPane.getViewport().setScrollMode(JViewport.BACKINGSTORE_SCROLL_MODE);
    return scrollPane;
  }

  public void zoomToDisplaySize() {
    if (image == null) return;
    Dimension display = getDisplaySize();
    double xRatio = display.width/(double) image.getWidth();
    double yRatio = display.height/(double) image.getHeight();
    setZoom(Math.min(xRatio, yRatio));
    revalidate();
  }

  /** tricky magic to get parent scroll pane */
  private Dimension getDisplaySize() {
    Container c = getParent();
    while (c != null) {
      if (c instanceof JScrollPane)
        return c.getSize();
      c = c.getParent();
    }
    return getSize();
  }

  public void setSelection(Rectangle r) {
    selection = r;
    repaint();
  }

  public Rectangle getSelection() {
    return selection;
  }

  public RGBImage getRGBImage() {
    return new RGBImage(getImage());
  }
}

public class main {

static class ShowBigText {
  String fontID = "#1000993";  // Brother Deluxe Font
  Color background = Color.white;
  Color foreground = Color.black;
  String text = "test";
  ImageSurface is;
  Font font;
  String title = "JavaX";

  // thread-safe
  void showText(final String text) {
    SwingUtilities.invokeLater(new Runnable() {
public void run() {
try {

      ShowBigText.this.text = text;
      if (is == null) {
        font = loadFont(fontID);
        
        BufferedImage img = makeImage();
        is = showImage(img, title);
        moveToTopRightCorner(getFrame(is));
      }
  
      is.setImage(makeImage());
    } catch (Exception _e) {
  throw _e instanceof RuntimeException ? (RuntimeException) _e : new RuntimeException(_e); } }
});
  }

  BufferedImage makeImage() {  
    // TODO: dispose old image?
    BufferedImage img = new BufferedImage(200, 60, BufferedImage.TYPE_INT_RGB);
    
    Graphics g = img.getGraphics();
    int w = img.getWidth(), h = img.getHeight();
    g.setColor(background);
    g.fillRect(0, 0, w, h);
    g.setColor(foreground);
    g.setFont(font.deriveFont(30f));
    FontMetrics fm = g.getFontMetrics();
    int y = fm.getAscent() + (h-fm.getHeight())/2;
    int x = (w-fm.stringWidth(text))/2;
    g.drawString(text, x, y);
    g.dispose();
    
    return img;
  }
} // ShowBigText
  
  static String os;
  static boolean wtf;
  
  public static void main(String[] args) throws Exception {
    readLocally("os");
    ShowBigText sbt = new ShowBigText();
    sbt.title = "OS Sensor";
    if (os != null) sbt.showText(os + "!");
    
    while (true) {
      String theOS = getOS();
      if (os != null && !eq(os, theOS)) {
        for (int i = 0; i < 3; i++) {
          wtf = true;
          sbt.showText("WTF!");
          sleepSeconds(1);
          sbt.showText(os + "!");
          sleepSeconds(1);
          sbt.showText(theOS + "!");
          sleepSeconds(1);
        }
        wtf = false;
      }
      
      if (!eq(os, theOS)) {
        os = theOS;
        saveLocally("os");
        sbt.showText(os + "!");
      }
      
      sleepSeconds(5);
    }
  }
  
  static String getOS() {
    if (isAndroid()) return "Android";
    if (isMac()) return "Mac OS";
    if (isWindows()) return "Windows";
    return "Linux"; // Everything else is Linux... Yeah it's generous :)
  }

  static boolean isMac() {
    return System.getProperty("os.name").toLowerCase().contains("mac");
	}

static void moveToTopRightCorner(JFrame frame) {
  if (frame == null) return;
  int x = getScreenSize().width - frame.getWidth() - 10;
  int y = 10;
  frame.setLocation(x, y);
}

static JFrame getFrame(Component c) {
  while (c != null) {
    if (c instanceof JFrame) return (JFrame) c;
    c = c.getParent();
  }
  return null;
}

  static ImageSurface showImage(BufferedImage img, String title) {
    ImageSurface is = showImage(img);
    getFrame(is).setTitle(title);
    return is;
  }
  
  static ImageSurface showImage(BufferedImage img) {
    JFrame frame = new JFrame("JavaX");
    ImageSurface is = new ImageSurface(img);
    frame.getContentPane().add(new JScrollPane(is));
    frame.pack();
    centerFrame(frame);
    frame.setVisible(true);
    return is;
  }
  
  static ImageSurface showImage(RGBImage img) {
    return showImage(img.getBufferedImage());
  }
  
  static ImageSurface showImage(String imageID) {
    return showImage(loadImage(imageID));
  }


static void sleepSeconds(long s) {
  if (s > 0) sleep(s*1000);
}

static void showText(String title, String text) {
  JTextArea textArea = new JTextArea(text);
  makeFrame(title, new JScrollPane(textArea));
}


static String programID;

static synchronized void saveLocally(String variableName) {
  File textFile = new File(programDir(programID), variableName + ".text");
  File structureFile = new File(programDir(programID), variableName + ".structure");
  Object x = get(main.class, variableName);
  
  if (x == null) {
    textFile.delete();
    structureFile.delete();
  } else if (x instanceof String) {
    structureFile.delete();
    saveTextFile(textFile, (String) x);
  } else {
    textFile.delete();
    saveTextFile(structureFile, structure(x));
  }
}
static synchronized void readLocally(String varNames) {
  for (String variableName : codeTokensOnly(javaTok(varNames))) {
  File textFile = new File(programDir(programID), variableName + ".text");
  File structureFile = new File(programDir(programID), variableName + ".structure");
  
  String value = loadTextFile(textFile);
  if (value != null)
    set(main.class, variableName, value);
  else {
    value = loadTextFile(structureFile);
    if (value != null)
    set(main.class, variableName, unstructure(value));
  }
  }
}


static boolean eq(Object a, Object b) {
  return a == null ? b == null : a.equals(b);
}

static Font loadFont(String snippetID) { try {
 
  return loadFont(snippetID, 12f);

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

static Font loadFont(String snippetID, float fontSize) { try {
 
  return Font.createFont(Font.TRUETYPE_FONT, loadLibrary(snippetID)).deriveFont(fontSize);

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

static boolean isAndroid() { return System.getProperty("java.vendor").toLowerCase().indexOf("android") >= 0; }

static boolean equals(Object a, Object b) {
  return a == null ? b == null : a.equals(b);
}

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

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

static Dimension getScreenSize() {
  return Toolkit.getDefaultToolkit().getScreenSize();
}

static String programID() {
  return getProgramID();
}

static void centerFrame(JFrame frame) {
  frame.setLocationRelativeTo(null); // magic trick
}

  static List<String> codeTokensOnly(List<String> tok) {
    List<String> l = new ArrayList<String>();
    for (int i = 1; i < tok.size(); i += 2)
      l.add(tok.get(i));
    return l;
  }

static class DynamicObject {
  String className;
  Map<String, Object> fieldValues = new TreeMap<String, Object>();
}

// actually it's now almost the same as jsonDecode :)
static Object unstructure(String text) {
  final List<String> tok = javaTok(text);
  
  class X {
    int i = 1;

    Object parse() {
      String t = tok.get(i);
      if (t.startsWith("\"")) {
        String s = unquote(tok.get(i));
        i += 2;
        return s;
      }
      if (t.equals("{"))
        return parseMap();
      if (t.equals("["))
        return parseList();
      if (t.equals("null")) {
        i += 2; return null;
      }
      if (t.equals("false")) {
        i += 2; return false;
      }
      if (t.equals("true")) {
        i += 2; return true;
      }
      if (isInteger(t)) {
        i += 2; return Long.parseLong(t);
      }
      if (isJavaIdentifier(t)) {
        Class c = findClass(t);
        DynamicObject dO = null;
        Object o = null;
        if (c != null)
          o = nuObject(c);
        else {
          dO = new DynamicObject();
          dO.className = t;
        }
        i += 2;
        if (i < tok.size() && tok.get(i).equals("(")) {
          consume("(");
          while (!tok.get(i).equals(")")) {
            // It's like parsing a map.
            //Object key = parse();
            //if (tok.get(i).equals(")"))
            //  key = onlyField();
            String key = unquote(tok.get(i));
            i += 2;
            consume("=");
            Object value = parse();
            if (o != null)
              set(o, key, value);
            else
              dO.fieldValues.put(key, value);
            if (tok.get(i).equals(",")) i += 2;
          }
          consume(")");
        }
        return o != null ? o : dO;
      }
      throw new RuntimeException("Unknown token " + (i+1) + ": " + t);
    }
    
    Object parseList() {
      consume("[");
      List list = new ArrayList();
      while (!tok.get(i).equals("]")) {
        list.add(parse());
        if (tok.get(i).equals(",")) i += 2;
      }
      consume("]");
      return list;
    }
    
    Object parseMap() {
      consume("{");
      Map map = new TreeMap();
      while (!tok.get(i).equals("}")) {
        String key = unquote(tok.get(i));
        i += 2;
        consume("=");
        Object value = parse();
        map.put(key, value);
        if (tok.get(i).equals(",")) i += 2;
      }
      consume("}");
      return map;
    }
    
    void consume(String s) {
      if (!tok.get(i).equals(s)) {
        String prevToken = i-2 >= 0 ? tok.get(i-2) : "";
        String nextTokens = join(tok.subList(i, Math.min(i+4, tok.size())));
        fail(quote(s) + " expected: " + prevToken + " " + nextTokens + " (" + i + "/" + tok.size() + ")");
      }
      i += 2;
    }
  }
  
  return new X().parse();
}

static File programDir() {
  return programDir(getProgramID());
}

static File programDir(String snippetID) {
  return new File(userHome(), "JavaX-Data/" + formatSnippetID(snippetID));
}


static File loadLibrary(String snippetID) {
  return loadBinarySnippet(snippetID);
}

static void makeFrame(Component content) {
  // TODO: get program ID
  makeFrame("A JavaX Frame", content);
}

static void makeFrame(String title, Component content) {
  JFrame frame = new JFrame(title);
  frame.getContentPane().add(content);
  frame.setBounds(300, 100, 500, 400);
  frame.setVisible(true);
  exitOnFrameClose(frame);
}

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

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

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

// 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) {
          ++j;
          break;
        } else if (s.charAt(j) == '\\' && j+1 < l)
          j += 2;
        else
          ++j;
      }
    } else if (Character.isJavaIdentifierStart(c))
      do ++j; while (j < l && Character.isJavaIdentifierPart(s.charAt(j)));
    else if (Character.isDigit(c))
      do ++j; while (j < l && Character.isDigit(s.charAt(j)));
    else if (cc.equals("[[")) {
      do ++j; while (j+1 < l && !s.substring(j, j+2).equals("]]"));
      j = Math.min(j+2, l);
    } else
      ++j;

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


  /** 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);
    }
  }


  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 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);
      f.setAccessible(true);
      f.set(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);
      f.setAccessible(true);
      f.set(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 boolean loadImage_useImageCache = true;
  
  static RGBImage loadImage(String snippetID) {
   try {
    // TODO: androidify
    File dir = new File(System.getProperty("user.home"), ".tinybrain/image-cache");
    if (loadImage_useImageCache) {
      dir.mkdirs();
      File file = new File(dir, snippetID + ".png");
      if (file.exists() && file.length() != 0)
        try {
          return new RGBImage(ImageIO.read(file));
        } catch (Throwable e) {
          e.printStackTrace();
          // fall back to loading from sourceforge
        }
    }

    String imageURL = loadImage_getImageURL(parseSnippetID(snippetID));
    System.err.println("Loading image: " + imageURL);
    BufferedImage image = ImageIO.read(new URL(imageURL));

    if (loadImage_useImageCache) {
      File tempFile = new File(dir, snippetID + ".tmp." + System.currentTimeMillis());
      ImageIO.write(image, "png", tempFile);
      tempFile.renameTo(new File(dir, snippetID + ".png"));
      //Log.info("Cached image.");
    }

    //Log.info("Loaded image.");
    return new RGBImage(image);
   } catch (IOException e) {
    throw new RuntimeException(e);
   }
  }

  static String loadImage_getImageURL(long snippetID) throws IOException {
    String url;
    if (snippetID == 1000010 || snippetID == 1000012)
      url = "http://tinybrain.de:8080/tb/show-blobimage.php?id=" + snippetID;
    else
      url = "http://eyeocr.sourceforge.net/filestore/filestore.php?cmd=serve&file=blob_" + snippetID
        + "&contentType=image/png";
    return url;
  }

static Object get(Object o, String field) {
  if (o instanceof Class) return get((Class) o, 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) {
  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 Field get_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 String structure(Object o) {
  return structure(o, 0);
}

// leave to false, unless unstructure() breaks
static boolean structure_allowShortening = false;
  
static String structure(Object o, int stringSizeLimit) {
  if (o == null) return "null";
  String name = o.getClass().getName();
  
  StringBuilder buf = new StringBuilder();
  
  if (o instanceof Collection) { // TODO: store the type (e.g. HashSet/TreeSet)
    for (Object x : (Collection) o) {
      if (buf.length() != 0) buf.append(", ");
      buf.append(structure(x, stringSizeLimit));
    }
    return "[" + buf + "]";
  }
  
  if (o instanceof Map) {
    for (Object e : ((Map) o).entrySet()) {
      if (buf.length() != 0) buf.append(", ");
      buf.append(structure(((Map.Entry) e).getKey(), stringSizeLimit));
      buf.append("=");
      buf.append(structure(((Map.Entry) e).getValue(), stringSizeLimit));
    }
    return "{" + 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(Array.get(o, i), stringSizeLimit));
    }
    return "array{" + buf + "}";
  }

  if (o instanceof String)
    return quote(stringSizeLimit != 0 ? shorten((String) o, stringSizeLimit) : (String) o);
  
  // Need more cases? This should cover all library classes...
  if (name.startsWith("java.") || name.startsWith("javax."))
    return String.valueOf(o);
    
  String shortName = o.getClass().getName().replaceAll("^main\\$", "");
  
  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(value, stringSizeLimit));
      }
      ++numFields;
   }
  } else {
    // regular class
    // TODO: go to superclasses too
    Field[] fields = o.getClass().getDeclaredFields();
    for (Field field : fields) {
      if ((field.getModifiers() & Modifier.STATIC) != 0)
        continue;
      Object value;
      try {
        field.setAccessible(true);
        value = field.get(o);
      } catch (Exception e) {
        value = "?";
      }
      
      fieldName = field.getName();
      
      // put special cases here...
  
      if (value != null) {
        if (buf.length() != 0) buf.append(", ");
        buf.append(fieldName + "=" + structure(value, stringSizeLimit));
      }
      ++numFields;
    }
  }
  
  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 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 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;
}

// currently finds only inner classes of class "main"
// returns null on not found
// this is the simple version that is not case-tolerant
static Class findClass(String name) {
  try {
    return Class.forName("main$" + name);
  } catch (ClassNotFoundException e) {
    return null;
  }
}

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 getProgramID() {
  return programID;
}

static File loadBinarySnippet(String snippetID) { try {
 
  long id = parseSnippetID(snippetID);
  File f = DiskSnippetCache_getLibrary(id);
  if (f == null) {
    byte[] data = loadDataSnippetImpl(snippetID);
    DiskSnippetCache_putLibrary(id, data);
    f = DiskSnippetCache_getLibrary(id);
  }
  return f;

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

  static void exitOnFrameClose(JFrame frame) {
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  }


  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.length()-1);
      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;
          }
          i++;
        }
        sb.append(ch);
      }
      return sb.toString();      
    } else
      return s; // return original
  }

  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 RuntimeException fail() {
    throw new RuntimeException("fail");
  }
  
  static RuntimeException fail(Object msg) {
    throw new RuntimeException(String.valueOf(msg));
  }

static boolean isInteger(String s) {
  return Pattern.matches("\\-?\\d+", s);
}

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


static String shorten(String s, int max) {
  return s.length() <= max ? s : s.substring(0, Math.min(s.length(), max)) + "...";
}

public static long parseSnippetID(String snippetID) {
  return Long.parseLong(shortenSnippetID(snippetID));
}

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

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

// 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;
  return type.isInstance(arg);
}

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

public static void DiskSnippetCache_putLibrary(long snippetID, byte[] data) throws IOException {
  saveBinaryFile(new File(getGlobalCache(), "data_" + snippetID).getPath() + ".jar", data);
}

static byte[] loadDataSnippetImpl(String snippetID) throws IOException {
  byte[] data;
  try {
    URL url = new URL("http://eyeocr.sourceforge.net/filestore/filestore.php?cmd=serve&file=blob_"
      + parseSnippetID(snippetID) + "&contentType=application/binary");
    System.err.println("Loading library: " + url);
    try {
      data = loadBinaryPage(url.openConnection());
    } catch (IOException e) {
      url = new URL("http://data.tinybrain.de/blobs/"
        + parseSnippetID(snippetID));
      System.err.println("Loading library: " + url);
      data = loadBinaryPage(url.openConnection());
    }
    System.err.println("Bytes loaded: " + data.length);
  } catch (FileNotFoundException e) {
    throw new IOException("Binary snippet #" + snippetID + " not found or not public");
  }
  return data;
}

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

static File getGlobalCache() {
  File file = new File(userHome(), ".tinybrain/snippet-cache");
  file.mkdirs();
  return file;
}


static byte[] loadBinaryPage(String url) throws IOException {
  return loadBinaryPage(new URL(url).openConnection());
}

public static byte[] loadBinaryPage(URLConnection con) throws IOException {
  //setHeaders(con);
  ByteArrayOutputStream buf = new ByteArrayOutputStream();
  InputStream inputStream = con.getInputStream();
  int n = 0;
  while (true) {
    int ch = inputStream.read();
    if (ch < 0)
      break;
    buf.write(ch);
    if (++n % 100000 == 0)
      System.err.println("  " + n + " bytes loaded.");
  }
  inputStream.close();
  return buf.toByteArray();
}


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

    if (!new File(tempFileName).renameTo(file))
      throw new IOException("Can't rename " + tempFileName + " to " + fileName);
  }

  static void saveBinaryFile(File fileName, byte[] contents) {
    try {
      saveBinaryFile(fileName.getPath(), contents);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }
}