/* -*- mode: jde; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
  Part of the Processing project - http://processing.org

  Copyright (c) 2005-06 Ben Fry and Casey Reas

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General
  Public License along with this library; if not, write to the
  Free Software Foundation, Inc., 59 Temple Place, Suite 330,
  Boston, MA  02111-1307  USA
*/

package processing.core;

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;


/**
 * Subclass for PGraphics that implements the graphics API
 * in Java 1.3+ using Java 2D.
 *
 * <p>Pixel operations too slow? As of release 0085 (the first beta),
 * the default renderer uses Java2D. It's more accurate than the renderer
 * used in alpha releases of Processing (it handles stroke caps and joins,
 * and has better polygon tessellation), but it's super slow for handling
 * pixels. At least until we get a chance to get the old 2D renderer
 * (now called P2D) working in a similar fashion, you can use
 * <TT>size(w, h, P3D)</TT> instead of <TT>size(w, h)</TT> which will
 * be faster for general pixel flipping madness. </p>
 *
 * <p>To get access to the Java 2D "Graphics2D" object for the default
 * renderer, use:
 * <PRE>Graphics2D g2 = ((PGraphicsJava2D)g).g2;</PRE>
 * This will let you do Java 2D stuff directly, but is not supported in
 * any way shape or form. Which just means "have fun, but don't complain
 * if it breaks."</p>
 */
public class PGraphicsJava2D extends PGraphics {

  public Graphics2D g2;
  GeneralPath gpath;

  int transformCount;
  AffineTransform transformStack[] =
    new AffineTransform[MATRIX_STACK_DEPTH];
  double transform[] = new double[6];

  Line2D.Float line = new Line2D.Float();
  Ellipse2D.Float ellipse = new Ellipse2D.Float();
  Rectangle2D.Float rect = new Rectangle2D.Float();
  Arc2D.Float arc = new Arc2D.Float();

  protected Color tintColorObject;

  protected Color fillColorObject;
  public boolean fillGradient;
  public Paint fillGradientObject;

  protected Color strokeColorObject;
  public boolean strokeGradient;
  public Paint strokeGradientObject;



  //////////////////////////////////////////////////////////////

  // INTERNAL


  /**
   * Constructor for the PGraphicsJava object.
   * This prototype only exists because of annoying
   * java compilers, and should not be used.
   */
  //public PGraphicsJava2D() { }


  /**
   * Constructor for the PGraphics object. Use this to ensure that
   * the defaults get set properly. In a subclass, use this(w, h)
   * as the first line of a subclass' constructor to properly set
   * the internal fields and defaults.
   *
   * @param iwidth  viewport width
   * @param iheight viewport height
   */
  public PGraphicsJava2D(int iwidth, int iheight, PApplet parent) {
    super(iwidth, iheight, parent);
    //resize(iwidth, iheight);
  }


  /**
   * Called in repsonse to a resize event, handles setting the
   * new width and height internally, as well as re-allocating
   * the pixel buffer for the new size.
   *
   * Note that this will nuke any cameraMode() settings.
   */
  public void resize(int iwidth, int iheight) {  // ignore
    //System.out.println("resize " + iwidth + " " + iheight);
    insideDrawWait();
    insideResize = true;

    width = iwidth;
    height = iheight;
    width1 = width - 1;
    height1 = height - 1;

    allocate();

    // ok to draw again
    insideResize = false;
  }


  // broken out because of subclassing for opengl
  protected void allocate() {
    image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
    g2 = (Graphics2D) image.getGraphics();
    // can't un-set this because this may be only a resize (Bug #463)
    //defaultsInited = false;
  }



  //////////////////////////////////////////////////////////////

  // FRAME


  public void beginDraw() {
    insideResizeWait();
    insideDraw = true;

    // need to call defaults(), but can only be done when it's ok
    // to draw (i.e. for opengl, no drawing can be done outside
    // beginDraw/endDraw).
    if (!defaultsInited) defaults();

    resetMatrix(); // reset model matrix

    // reset vertices
    vertexCount = 0;
  }


  public void endDraw() {
    // hm, mark pixels as changed, because this will instantly do a full
    // copy of all the pixels to the surface.. so that's kind of a mess.
    //updatePixels();

    if (!mainDrawingSurface) {
      loadPixels();
    }
    modified = true;
    insideDraw = false;
  }



  //////////////////////////////////////////////////////////////

  // SHAPES


  public void beginShape(int kind) {
    //super.beginShape(kind);
    shape = kind;
    vertexCount = 0;
    splineVertexCount = 0;

    // set gpath to null, because when mixing curves and straight
    // lines, vertexCount will be set back to zero, so vertexCount == 1
    // is no longer a good indicator of whether the shape is new.
    // this way, just check to see if gpath is null, and if it isn't
    // then just use it to continue the shape.
    gpath = null;
  }


  public void textureMode(int mode) {
    unavailableError("textureMode(mode)");
  }


  public void texture(PImage image) {
    unavailableError("texture(image)");
  }


  public void vertex(float x, float y) {
    splineVertexCount = 0;
    //float vertex[];

    if (vertexCount == vertices.length) {
      float temp[][] = new float[vertexCount<<1][VERTEX_FIELD_COUNT];
      System.arraycopy(vertices, 0, temp, 0, vertexCount);
      vertices = temp;
      //message(CHATTER, "allocating more vertices " + vertices.length);
    }
    // not everyone needs this, but just easier to store rather
    // than adding another moving part to the code...
    vertices[vertexCount][MX] = x;
    vertices[vertexCount][MY] = y;
    vertexCount++;

    switch (shape) {

    case POINTS:
      point(x, y);
      break;

    case LINES:
      if ((vertexCount % 2) == 0) {
        line(vertices[vertexCount-2][MX],
             vertices[vertexCount-2][MY], x, y);
      }
      break;

/*
    case LINE_STRIP:
    case LINE_LOOP:
      if (gpath == null) {
        gpath = new GeneralPath();
        gpath.moveTo(x, y);
      } else {
        gpath.lineTo(x, y);
      }
      break;
*/

    case TRIANGLES:
      if ((vertexCount % 3) == 0) {
        triangle(vertices[vertexCount - 3][MX],
                 vertices[vertexCount - 3][MY],
                 vertices[vertexCount - 2][MX],
                 vertices[vertexCount - 2][MY],
                 x, y);
      }
      break;

    case TRIANGLE_STRIP:
      if (vertexCount >= 3) {
        triangle(vertices[vertexCount - 2][MX],
                 vertices[vertexCount - 2][MY],
                 vertices[vertexCount - 1][MX],
                 vertices[vertexCount - 1][MY],
                 vertices[vertexCount - 3][MX],
                 vertices[vertexCount - 3][MY]);
      }
      break;

    case TRIANGLE_FAN:
      if (vertexCount == 3) {
        triangle(vertices[0][MX], vertices[0][MY],
                 vertices[1][MX], vertices[1][MY],
                 x, y);
      } else if (vertexCount > 3) {
        gpath = new GeneralPath();
        // when vertexCount > 3, draw an un-closed triangle
        // for indices 0 (center), previous, current
        gpath.moveTo(vertices[0][MX],
                     vertices[0][MY]);
        gpath.lineTo(vertices[vertexCount - 2][MX],
                    vertices[vertexCount - 2][MY]);
        gpath.lineTo(x, y);
        draw_shape(gpath);
      }
      break;

    case QUADS:
      if ((vertexCount % 4) == 0) {
        quad(vertices[vertexCount - 4][MX],
             vertices[vertexCount - 4][MY],
             vertices[vertexCount - 3][MX],
             vertices[vertexCount - 3][MY],
             vertices[vertexCount - 2][MX],
             vertices[vertexCount - 2][MY],
             x, y);
      }
      break;

    case QUAD_STRIP:
      // 0---2---4
      // |   |   |
      // 1---3---5
      if ((vertexCount >= 4) && ((vertexCount % 2) == 0)) {
        quad(vertices[vertexCount - 4][MX],
             vertices[vertexCount - 4][MY],
             vertices[vertexCount - 2][MX],
             vertices[vertexCount - 2][MY],
             x, y,
             vertices[vertexCount - 3][MX],
             vertices[vertexCount - 3][MY]);
      }
      break;

    case POLYGON:
      if (gpath == null) {
        gpath = new GeneralPath();
        gpath.moveTo(x, y);
      } else if (breakShape) {
        gpath.moveTo(x, y);
        breakShape = false;
      } else {
        gpath.lineTo(x, y);
      }
      break;
    }
  }


  public void vertex(float x, float y, float u, float v) {
    unavailableError("vertex(x, y, u, v");
  }


  public void vertex(float x, float y, float z) {
    depthErrorXYZ("vertex");
  }


  public void vertex(float x, float y, float z, float u, float v) {
    depthErrorXYZ("vertex");
  }


  public void bezierVertex(float x1, float y1,
                           float x2, float y2,
                           float x3, float y3) {
    if (gpath == null) {
      throw new RuntimeException("Must call vertex() at least once " +
                                 "before using bezierVertex()");
    }

    switch (shape) {
      //case LINE_LOOP:
      //case LINE_STRIP:
      case POLYGON:
        gpath.curveTo(x1, y1, x2, y2, x3, y3);
        break;

    default:
      throw new RuntimeException("bezierVertex() can only be used with " +
                                 "LINE_STRIP, LINE_LOOP, or POLYGON");
    }
  }


  float curveX[] = new float[4];
  float curveY[] = new float[4];

  public void curveVertex(float x, float y) {
    //if ((shape != LINE_LOOP) && (shape != LINE_STRIP) && (shape != POLYGON)) {
    if (shape != POLYGON) {
      throw new RuntimeException("curveVertex() can only be used with " +
                                 "POLYGON shapes");
                                 //"LINE_LOOP, LINE_STRIP, and POLYGON shapes");
    }

    if (!curve_inited) curve_init();
    vertexCount = 0;

    if (splineVertices == null) {
      splineVertices = new float[DEFAULT_SPLINE_VERTICES][VERTEX_FIELD_COUNT];
    }

    // if more than 128 points, shift everything back to the beginning
    if (splineVertexCount == DEFAULT_SPLINE_VERTICES) {
      System.arraycopy(splineVertices[DEFAULT_SPLINE_VERTICES - 3], 0,
                       splineVertices[0], 0, VERTEX_FIELD_COUNT);
      System.arraycopy(splineVertices[DEFAULT_SPLINE_VERTICES - 2], 0,
                       splineVertices[1], 0, VERTEX_FIELD_COUNT);
      System.arraycopy(splineVertices[DEFAULT_SPLINE_VERTICES - 1], 0,
                       splineVertices[2], 0, VERTEX_FIELD_COUNT);
      splineVertexCount = 3;
    }

    // this new guy will be the fourth point (or higher),
    // which means it's time to draw segments of the curve
    if (splineVertexCount >= 3) {
      curveX[0] = splineVertices[splineVertexCount-3][MX];
      curveY[0] = splineVertices[splineVertexCount-3][MY];

      curveX[1] = splineVertices[splineVertexCount-2][MX];
      curveY[1] = splineVertices[splineVertexCount-2][MY];

      curveX[2] = splineVertices[splineVertexCount-1][MX];
      curveY[2] = splineVertices[splineVertexCount-1][MY];

      curveX[3] = x;
      curveY[3] = y;

      curveToBezierMatrix.mult(curveX, curveX);
      curveToBezierMatrix.mult(curveY, curveY);

      // since the paths are continuous,
      // only the first point needs the actual moveto
      if (gpath == null) {
        gpath = new GeneralPath();
        gpath.moveTo(curveX[0], curveY[0]);
      }

      gpath.curveTo(curveX[1], curveY[1],
                    curveX[2], curveY[2],
                    curveX[3], curveY[3]);
    }

    // add the current point to the list
    splineVertices[splineVertexCount][MX] = x;
    splineVertices[splineVertexCount][MY] = y;
    splineVertexCount++;
  }


  boolean breakShape;
  public void breakShape() {
    breakShape = true;
  }


  public void endShape(int mode) {
    if (gpath != null) {  // make sure something has been drawn
      if (shape == POLYGON) {
        if (mode == CLOSE) {
          gpath.closePath();
        }
        draw_shape(gpath);
      }
    }
    shape = 0;
  }



  //////////////////////////////////////////////////////////////


  /*
  protected void fillGradient(Paint paint) {
    fillGradient = true;
    fillGradientObject = paint;
  }


  protected void noFillGradient() {
    fillGradient = false;
  }
  */


  //////////////////////////////////////////////////////////////


  protected void fill_shape(Shape s) {
    if (fillGradient) {
      g2.setPaint(fillGradientObject);
      g2.fill(s);
    } else if (fill) {
      g2.setColor(fillColorObject);
      g2.fill(s);
    }
  }

  protected void stroke_shape(Shape s) {
    if (strokeGradient) {
      g2.setPaint(strokeGradientObject);
      g2.draw(s);
    } else if (stroke) {
      g2.setColor(strokeColorObject);
      g2.draw(s);
    }
  }

  protected void draw_shape(Shape s) {
    if (fillGradient) {
      g2.setPaint(fillGradientObject);
      g2.fill(s);
    } else if (fill) {
      g2.setColor(fillColorObject);
      g2.fill(s);
    }
    if (strokeGradient) {
      g2.setPaint(strokeGradientObject);
      g2.draw(s);
    } else if (stroke) {
      g2.setColor(strokeColorObject);
      g2.draw(s);
    }
  }


  //////////////////////////////////////////////////////////////


  public void point(float x, float y) {
    line(x, y, x, y);
  }


  public void line(float x1, float y1, float x2, float y2) {
    //graphics.setColor(strokeColorObject);
    //graphics.drawLine(x1, y1, x2, y2);
    line.setLine(x1, y1, x2, y2);
    stroke_shape(line);
  }


  public void triangle(float x1, float y1, float x2, float y2,
                       float x3, float y3) {
    gpath = new GeneralPath();
    gpath.moveTo(x1, y1);
    gpath.lineTo(x2, y2);
    gpath.lineTo(x3, y3);
    gpath.closePath();

    draw_shape(gpath);
  }


  public void quad(float x1, float y1, float x2, float y2,
                   float x3, float y3, float x4, float y4) {
    GeneralPath gp = new GeneralPath();
    gp.moveTo(x1, y1);
    gp.lineTo(x2, y2);
    gp.lineTo(x3, y3);
    gp.lineTo(x4, y4);
    gp.closePath();

    draw_shape(gp);
  }


  //////////////////////////////////////////////////////////////


  protected void rectImpl(float x1, float y1, float x2, float y2) {
    rect.setFrame(x1, y1, x2-x1, y2-y1);
    draw_shape(rect);
  }


  protected void ellipseImpl(float x, float y, float w, float h) {
    ellipse.setFrame(x, y, w, h);
    draw_shape(ellipse);
  }


  protected void arcImpl(float x, float y, float w, float h,
                         float start, float stop) {
    // 0 to 90 in java would be 0 to -90 for p5 renderer
    // but that won't work, so -90 to 0?

    if (stop - start >= TWO_PI) {
      start = 0;
      stop = 360;

    } else {
      start = -start * RAD_TO_DEG;
      stop = -stop * RAD_TO_DEG;

      // ok to do this because already checked for NaN
      while (start < 0) {
        start += 360;
        stop += 360;
      }
      if (start > stop) {
        float temp = start;
        start = stop;
        stop = temp;
      }
    }
    float span = stop - start;

    // stroke as Arc2D.OPEN, fill as Arc2D.PIE
    if (fill) {
      //System.out.println("filla");
      arc.setArc(x, y, w, h, start, span, Arc2D.PIE);
      fill_shape(arc);
    }
    if (stroke) {
      //System.out.println("strokey");
      arc.setArc(x, y, w, h, start, span, Arc2D.OPEN);
      stroke_shape(arc);
    }
  }


  //////////////////////////////////////////////////////////////


  /** Ignored (not needed) in Java 2D. */
  public void bezierDetail(int detail) {
  }


  /** Ignored (not needed) in Java 2D. */
  public void curveDetail(int detail) {
  }


  //////////////////////////////////////////////////////////////


  /**
   * Handle renderer-specific image drawing.
   */
  protected void imageImpl(PImage who,
                           float x1, float y1, float x2, float y2,
                           int u1, int v1, int u2, int v2) {
    if (who.cache != null) {
      if (!(who.cache instanceof ImageCache)) {
        // this cache belongs to another renderer.. fix me later,
        // because this is gonna make drawing *really* inefficient
        //who.cache = null;
      }
    }

    if (who.cache == null) {
      //System.out.println("making new image cache");
      who.cache = new ImageCache(who);
      who.updatePixels();  // mark the whole thing for update
      who.modified = true;
    }

    ImageCache cash = (ImageCache) who.cache;
    // if image previously was tinted, or the color changed
    // or the image was tinted, and tint is now disabled
    if ((tint && !cash.tinted) ||
        (tint && (cash.tintedColor != tintColor)) ||
        (!tint && cash.tinted)) {
      // for tint change, mark all pixels as needing update
      who.updatePixels();
    }

    if (who.modified) {
      cash.update(tint, tintColor);
      who.modified = false;
    }

    g2.drawImage(((ImageCache) who.cache).image,
                 (int) x1, (int) y1, (int) x2, (int) y2,
                 u1, v1, u2, v2, null);
  }


  class ImageCache {
    PImage source;
    boolean tinted;
    int tintedColor;
    int tintedPixels[];
    BufferedImage image;

    public ImageCache(PImage source) {
      this.source = source;
      // even if RGB, set the image type to ARGB, because the
      // image may have an alpha value for its tint().
      int type = BufferedImage.TYPE_INT_ARGB;
      //System.out.println("making new buffered image");
      image = new BufferedImage(source.width, source.height, type);
    }

    // for rev 0124, passing the tintColor in here. the problem is that
    // the 'parent' PGraphics object of this inner class may not be
    // the same one that's used when drawing. for instance, if this
    // is a font used by the main drawing surface, then it's later
    // used in an offscreen PGraphics, the tintColor value from the
    // original PGraphics will be used.
    public void update(boolean tint, int tintColor) {
      if (tintedPixels == null) {
        //System.out.println("tinted pixels null");
        tintedPixels = new int[source.width * source.height];
      }

      if ((source.format == ARGB) || (source.format == RGB)) {
        if (tint) {
          // create tintedPixels[] if necessary
          //if (tintedPixels == null) {
          //  tintedPixels = new int[source.width * source.height];
          //}

          int a2 = (tintColor >> 24) & 0xff;
          int r2 = (tintColor >> 16) & 0xff;
          int g2 = (tintColor >> 8) & 0xff;
          int b2 = (tintColor) & 0xff;

          // multiply each of the color components into tintedPixels
          // if straight RGB image, don't bother multiplying
          // (also avoids problems if high bits not set)
          if (source.format == RGB) {
            int alpha = a2 << 24;

            for (int i = 0; i < tintedPixels.length; i++) {
              int argb1 = source.pixels[i];
              int r1 = (argb1 >> 16) & 0xff;
              int g1 = (argb1 >> 8) & 0xff;
              int b1 = (argb1) & 0xff;

              tintedPixels[i] = alpha |
                (((r2 * r1) & 0xff00) << 8) |
                ((g2 * g1) & 0xff00) |
                (((b2 * b1) & 0xff00) >> 8);
            }

          } else {
            for (int i = 0; i < tintedPixels.length; i++) {
              int argb1 = source.pixels[i];
              int a1 = (argb1 >> 24) & 0xff;
              int r1 = (argb1 >> 16) & 0xff;
              int g1 = (argb1 >> 8) & 0xff;
              int b1 = (argb1) & 0xff;

              tintedPixels[i] =
                (((a2 * a1) & 0xff00) << 16) |
                (((r2 * r1) & 0xff00) << 8) |
                ((g2 * g1) & 0xff00) |
                (((b2 * b1) & 0xff00) >> 8);
            }
          }

          tinted = true;
          tintedColor = tintColor;

          // finally, do a setRGB based on tintedPixels
          //image.setRGB(0, 0, source.width, source.height,
          //             tintedPixels, 0, source.width);
          WritableRaster raster = ((BufferedImage) image).getRaster();
          raster.setDataElements(0, 0, source.width, source.height,
                                 tintedPixels);

        } else {  // no tint
          // just do a setRGB like before
          // (and we'll just hope that the high bits are set)
          //image.setRGB(0, 0, source.width, source.height,
          //             source.pixels, 0, source.width);
          WritableRaster raster = ((BufferedImage) image).getRaster();
          raster.setDataElements(0, 0, source.width, source.height,
                                 source.pixels);
        }

      } else if (source.format == ALPHA) {
        int lowbits = tintColor & 0x00ffffff;
        if (((tintColor >> 24) & 0xff) >= 254) {
          //PApplet.println("  no alfa " + PApplet.hex(tintColor));
          // no actual alpha to the tint, set the image's alpha
          // as the high 8 bits, and use the color as the low 24 bits
          for (int i = 0; i < tintedPixels.length; i++) {
            // don't bother with the math if value is zero
            tintedPixels[i] = (source.pixels[i] == 0) ?
              0 : (source.pixels[i] << 24) | lowbits;
          }

        } else {
          //PApplet.println("  yes alfa " + PApplet.hex(tintColor));
          // multiply each image alpha by the tint alpha
          int alphabits = (tintColor >> 24) & 0xff;
          for (int i = 0; i < tintedPixels.length; i++) {
            tintedPixels[i] = (source.pixels[i] == 0) ?
              0 : (((alphabits * source.pixels[i]) & 0xFF00) << 16) | lowbits;
          }
        }

        // mark the pixels for next time
        tinted = true;
        tintedColor = tintColor;

        // finally, do a setRGB based on tintedPixels
        //image.setRGB(0, 0, source.width, source.height,
        //             tintedPixels, 0, source.width);
        WritableRaster raster = ((BufferedImage) image).getRaster();
        raster.setDataElements(0, 0, source.width, source.height, tintedPixels);
      }
    }
  }


  //////////////////////////////////////////////////////////////


  public float textAscent() {
    if (textFontNative == null) {
      return super.textAscent();
    }
    return textFontNativeMetrics.getAscent();
  }


  public float textDescent() {
    if (textFontNative == null) {
      return super.textDescent();
    }
    return textFontNativeMetrics.getDescent();
  }


  /**
   * Same as parent, but override for native version of the font.
   * <p/>
   * Also gets called by textFont, so the metrics
   * will get recorded properly.
   */
  public void textSize(float size) {
    // if a native version available, subset this font
    if (textFontNative != null) {
      textFontNative = textFontNative.deriveFont(size);
      g2.setFont(textFontNative);
      textFontNativeMetrics = g2.getFontMetrics(textFontNative);
    }

    // take care of setting the textSize and textLeading vars
    // this has to happen second, because it calls textAscent()
    // (which requires the native font metrics to be set)
    super.textSize(size);
  }


  protected float textWidthImpl(char buffer[], int start, int stop) {
    if (textFontNative == null) {
      //System.out.println("native is null");
      return super.textWidthImpl(buffer, start, stop);
    }
    // maybe should use one of the newer/fancier functions for this?
    int length = stop - start;
    return textFontNativeMetrics.charsWidth(buffer, start, length);
  }


  protected void textLinePlacedImpl(char buffer[], int start, int stop,
                                    float x, float y) {
    if (textFontNative == null) {
      super.textLinePlacedImpl(buffer, start, stop, x, y);
      return;
    }

    /*
    // save the current setting for text smoothing. note that this is
    // different from the smooth() function, because the font smoothing
    // is controlled when the font is created, not now as it's drawn.
    // fixed a bug in 0116 that handled this incorrectly.
    Object textAntialias =
      g2.getRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING);

    // override the current text smoothing setting based on the font
    // (don't change the global smoothing settings)
    g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                        textFont.smooth ?
                        RenderingHints.VALUE_ANTIALIAS_ON :
                        RenderingHints.VALUE_ANTIALIAS_OFF);
    */

    Object antialias =
      g2.getRenderingHint(RenderingHints.KEY_ANTIALIASING);
    if (antialias == null) {
      // if smooth() and noSmooth() not called, this will be null (0120)
      antialias = RenderingHints.VALUE_ANTIALIAS_DEFAULT;
    }

    // override the current smoothing setting based on the font
    // also changes global setting for antialiasing, but this is because it's
    // not possible to enable/disable them independently in some situations.
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                        textFont.smooth ?
                        RenderingHints.VALUE_ANTIALIAS_ON :
                        RenderingHints.VALUE_ANTIALIAS_OFF);


    g2.setColor(fillColorObject);
    // better to use drawString(float, float)?
    int length = stop - start;
    g2.drawChars(buffer, start, length, (int) (x + 0.5f), (int) (y + 0.5f));

    // this didn't seem to help the scaling issue
    // and creates garbage because of the new temporary object
    //java.awt.font.GlyphVector gv = textFontNative.createGlyphVector(g2.getFontRenderContext(), new String(buffer, start, stop));
    //g2.drawGlyphVector(gv, x, y);

    // return to previous smoothing state if it was changed
    //g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, textAntialias);
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, antialias);

    textX = x + textWidthImpl(buffer, start, stop);
    textY = y;
    textZ = 0;  // this will get set by the caller if non-zero
  }


  //////////////////////////////////////////////////////////////


  public void translate(float tx, float ty) {
    g2.translate(tx, ty);
  }


  public void rotate(float angle) {
    g2.rotate(angle);
  }


  public void scale(float s) {
    g2.scale(s, s);
  }


  public void scale(float sx, float sy) {
    g2.scale(sx, sy);
  }


  //////////////////////////////////////////////////////////////


  public void pushMatrix() {
    if (transformCount == transformStack.length) {
      throw new RuntimeException("pushMatrix() cannot use push more than " +
                                 transformStack.length + " times");
    }
    transformStack[transformCount] = g2.getTransform();
    transformCount++;
  }


  public void popMatrix() {
    if (transformCount == 0) {
      throw new RuntimeException("missing a popMatrix() " +
                                 "to go with that pushMatrix()");
    }
    transformCount--;
    g2.setTransform(transformStack[transformCount]);
  }


  public void resetMatrix() {
    g2.setTransform(new AffineTransform());
  }


  public void applyMatrix(float n00, float n01, float n02,
                          float n10, float n11, float n12) {
    g2.transform(new AffineTransform(n00, n10, n01, n11, n02, n12));
  }


  public void loadMatrix() {
    g2.getTransform().getMatrix(transform);

    m00 = (float) transform[0];
    m01 = (float) transform[2];
    m02 = (float) transform[4];

    m10 = (float) transform[1];
    m11 = (float) transform[3];
    m12 = (float) transform[5];
  }


  public float screenX(float x, float y) {
    loadMatrix();
    return super.screenX(x, y);
    //g2.getTransform().getMatrix(transform);
    //return (float)transform[0]*x + (float)transform[2]*y + (float)transform[4];
  }


  public float screenY(float x, float y) {
    loadMatrix();
    return super.screenY(x, y);
    //g2.getTransform().getMatrix(transform);
    //return (float)transform[1]*x + (float)transform[3]*y + (float)transform[5];
  }


  //////////////////////////////////////////////////////////////


  protected void tintFromCalc() {
    super.tintFromCalc();
    // TODO actually implement tinted images
    tintColorObject = new Color(tintColor, true);
  }

  protected void fillFromCalc() {
    super.fillFromCalc();
    fillColorObject = new Color(fillColor, true);
    fillGradient = false;
  }

  protected void strokeFromCalc() {
    super.strokeFromCalc();
    strokeColorObject = new Color(strokeColor, true);
    strokeGradient = false;
  }


  //////////////////////////////////////////////////////////////


  public void strokeWeight(float weight) {
    super.strokeWeight(weight);
    set_stroke();
  }


  public void strokeJoin(int join) {
    super.strokeJoin(join);
    set_stroke();
  }


  public void strokeCap(int cap) {
    super.strokeCap(cap);
    set_stroke();
  }


  protected void set_stroke() {
    int cap = BasicStroke.CAP_BUTT;
    if (strokeCap == ROUND) {
      cap = BasicStroke.CAP_ROUND;
    } else if (strokeCap == PROJECT) {
      cap = BasicStroke.CAP_SQUARE;
    }

    int join = BasicStroke.JOIN_BEVEL;
    if (strokeJoin == MITER) {
      join = BasicStroke.JOIN_MITER;
    } else if (strokeJoin == ROUND) {
      join = BasicStroke.JOIN_ROUND;
    }

    g2.setStroke(new BasicStroke(strokeWeight, cap, join));
  }


  //////////////////////////////////////////////////////////////


  public void background(PImage image) {
    if ((image.width != width) || (image.height != height)) {
      throw new RuntimeException("background image must be " +
                                 "the same size as your application");
    }
    if ((image.format != RGB) && (image.format != ARGB)) {
      throw new RuntimeException("background images should be RGB or ARGB");
    }
    // draw the image to screen without any transformations
    set(0, 0, image);
  }


  int[] clearPixels;

  public void clear() {
    // the only way to properly clear the screen is to re-allocate
    if (backgroundAlpha) {
      // clearRect() doesn't work because it just makes everything black.
      // instead, just wipe out the canvas to its transparent original
      //allocate();

      // allocate also won't work, because all the settings
      // (like smooth) will be completely reset.
      // Instead, create a small array that can be used to set the pixels
      // several times. Using a single-pixel line of length 'width' is a
      // tradeoff between speed (setting each pixel individually is too slow)
      // and memory (an array for width*height would waste lots of memory
      // if it stayed resident, and would terrify the gc if it were
      // re-created on each trip to background().
      WritableRaster raster = ((BufferedImage) image).getRaster();
      if ((clearPixels == null) || (clearPixels.length < width)) {
        clearPixels = new int[width];
      }
      for (int i = 0; i < width; i++) {
        clearPixels[i] = backgroundColor;
      }
      for (int i = 0; i < height; i++) {
        raster.setDataElements(0, i, width, 1, clearPixels);
      }
    } else {
      // in case people do transformations before background(),
      // need to handle this with a push/reset/pop
      pushMatrix();
      resetMatrix();
      g2.setColor(new Color(backgroundColor, backgroundAlpha));
      g2.fillRect(0, 0, width, height);
      popMatrix();
    }
  }



  //////////////////////////////////////////////////////////////

  // FROM PIMAGE


  public void smooth() {
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                        RenderingHints.VALUE_INTERPOLATION_BICUBIC);
  }


  public void noSmooth() {
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_OFF);
    g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                        RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
  }



  //////////////////////////////////////////////////////////////


  public void beginRaw(PGraphics recorderRaw) {
    throw new RuntimeException("beginRaw() not available with this renderer");
  }


  public void endRaw() {
  }


  //////////////////////////////////////////////////////////////


  public void loadPixels() {
    if ((pixels == null) || (pixels.length != width * height)) {
      pixels = new int[width * height];
    }
    //((BufferedImage) image).getRGB(0, 0, width, height, pixels, 0, width);
    WritableRaster raster = ((BufferedImage) image).getRaster();
    raster.getDataElements(0, 0, width, height, pixels);
  }


  /**
   * Update the pixels[] buffer to the PGraphics image.
   * <P>
   * Unlike in PImage, where updatePixels() only requests that the
   * update happens, in PGraphicsJava2D, this will happen immediately.
   */
  public void updatePixels() {
    //updatePixels(0, 0, width, height);
    WritableRaster raster = ((BufferedImage) image).getRaster();
    raster.setDataElements(0, 0, width, height, pixels);
  }


  /**
   * Update the pixels[] buffer to the PGraphics image.
   * <P>
   * Unlike in PImage, where updatePixels() only requests that the
   * update happens, in PGraphicsJava2D, this will happen immediately.
   */
  public void updatePixels(int x, int y, int c, int d) {
    if ((x == 0) && (y == 0) && (c == width) && (d == height)) {
      updatePixels();
    } else {
      throw new RuntimeException("updatePixels(x, y, c, d) not implemented");
    }
    /*
    ((BufferedImage) image).setRGB(x, y,
                                   (imageMode == CORNER) ? c : (c - x),
                                   (imageMode == CORNER) ? d : (d - y),
                                   pixels, 0, width);
    WritableRaster raster = ((BufferedImage) image).getRaster();
    raster.setDataElements(x, y,
                           (imageMode == CORNER) ? c : (c - x),
                           (imageMode == CORNER) ? d : (d - y),
                           pixels);
  */
  }


  //////////////////////////////////////////////////////////////


  static int getset[] = new int[1];


  public int get(int x, int y) {
    if ((x < 0) || (y < 0) || (x >= width) || (y >= height)) return 0;
    //return ((BufferedImage) image).getRGB(x, y);
    WritableRaster raster = ((BufferedImage) image).getRaster();
    raster.getDataElements(x, y, getset);
    return getset[0];
  }


  public PImage get(int x, int y, int w, int h) {
    if (imageMode == CORNERS) {  // if CORNER, do nothing
      // w/h are x2/y2 in this case, bring em down to size
      w = (w - x);
      h = (h - x);
    }

    if (x < 0) {
      w += x; // clip off the left edge
      x = 0;
    }
    if (y < 0) {
      h += y; // clip off some of the height
      y = 0;
    }

    if (x + w > width) w = width - x;
    if (y + h > height) h = height - y;

    PImage output = new PImage(w, h);
    output.parent = parent;

    // oops, the last parameter is the scan size of the *target* buffer
    //((BufferedImage) image).getRGB(x, y, w, h, output.pixels, 0, w);
    WritableRaster raster = ((BufferedImage) image).getRaster();
    raster.getDataElements(x, y, w, h, output.pixels);

    return output;
  }


  /**
   * Grab a copy of the current pixel buffer.
   */
  public PImage get() {
    /*
    PImage outgoing = new PImage(width, height);
    ((BufferedImage) image).getRGB(0, 0, width, height,
                                   outgoing.pixels, 0, width);
    return outgoing;
    */
    return get(0, 0, width, height);
  }


  public void set(int x, int y, int argb) {
    if ((x < 0) || (y < 0) || (x >= width) || (y >= height)) return;
    //((BufferedImage) image).setRGB(x, y, argb);
    getset[0] = argb;
    WritableRaster raster = ((BufferedImage) image).getRaster();
    raster.setDataElements(x, y, getset);
  }


  protected void setImpl(int dx, int dy, int sx, int sy, int sw, int sh,
                         PImage src) {
    WritableRaster raster = ((BufferedImage) image).getRaster();
    if ((sx == 0) && (sy == 0) && (sw == src.width) && (sh == src.height)) {
      raster.setDataElements(dx, dy, src.width, src.height, src.pixels);
    } else {
      int mode = src.imageMode;
      src.imageMode = CORNER;
      // TODO Optimize, incredibly inefficient to reallocate this much memory
      PImage temp = src.get(sx, sy, sw, sh);
      src.imageMode = mode;
      raster.setDataElements(dx, dy, temp.width, temp.height, temp.pixels);
    }
  }


  //////////////////////////////////////////////////////////////


  public void mask(int alpha[]) {
    throw new RuntimeException("mask() cannot be used with JAVA2D");
  }


  public void mask(PImage alpha) {
    throw new RuntimeException("mask() cannot be used with JAVA2D");
  }


  //////////////////////////////////////////////////////////////


  public void filter(int kind) {
    loadPixels();
    super.filter(kind);
    updatePixels();
  }


  public void filter(int kind, float param) {
    loadPixels();
    super.filter(kind, param);
    updatePixels();
  }


  //////////////////////////////////////////////////////////////


  public void copy(int sx, int sy, int sw, int sh,
                   int dx, int dy, int dw, int dh) {
    if ((sw != dw) || (sh != dh)) {
      // use slow version if changing size
      copy(this, sx, sy, sw, sh, dx, dy, dw, dh);

    } else {
      if (imageMode == CORNERS) {
        sw -= sx;
        sh -= sy;
      }
      dx = dx - sx;  // java2d's "dx" is the delta, not dest
      dy = dy - sy;
      g2.copyArea(sx, sy, sw, sh, dx, dy);
    }
  }


  public void copy(PImage src,
                   int sx1, int sy1, int sx2, int sy2,
                   int dx1, int dy1, int dx2, int dy2) {
    loadPixels();
    super.copy(src, sx1, sy1, sx2, sy2, dx1, dy1, dx2, dy2);
    updatePixels();
  }


  //////////////////////////////////////////////////////////////


  /*
  public void blend(PImage src, int sx, int sy, int dx, int dy, int mode) {
    loadPixels();
    super.blend(src, sx, sy, dx, dy, mode);
    updatePixels();
  }


  public void blend(int sx, int sy, int dx, int dy, int mode) {
    loadPixels();
    super.blend(sx, sy, dx, dy, mode);
    updatePixels();
  }
  */


  public void blend(int sx1, int sy1, int sx2, int sy2,
                    int dx1, int dy1, int dx2, int dy2, int mode) {
    loadPixels();
    super.blend(sx1, sy1, sx2, sy2, dx1, dy1, dx2, dy2, mode);
    updatePixels();
  }


  public void blend(PImage src, int sx1, int sy1, int sx2, int sy2,
                    int dx1, int dy1, int dx2, int dy2, int mode) {
    loadPixels();
    super.blend(src, sx1, sy1, sx2, sy2, dx1, dy1, dx2, dy2, mode);
    updatePixels();
  }


  //////////////////////////////////////////////////////////////


  public void save(String filename) {
    //System.out.println("start load");
    loadPixels();
    //System.out.println("end load, start save");
    super.save(filename);
    //System.out.println("done with save");
  }
}