package processing.core;

import java.awt.Paint;
import java.awt.PaintContext;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.util.HashMap;

import processing.xml.XMLElement;


/**
 * SVG stands for Scalable Vector Graphics, a portable graphics format. It is
 * a vector format so it allows for infinite resolution and relatively small
 * file sizes. Most modern media software can view SVG files, including Adobe
 * products, Firefox, etc. Illustrator and Inkscape can edit SVG files.
 * <p>
 * We have no intention of turning this into a full-featured SVG library.
 * The goal of this project is a basic shape importer that is small enough
 * to be included with applets, meaning that its download size should be
 * in the neighborhood of 25-30k. Starting with release 0149, this library
 * has been incorporated into the core via the loadShape() command, because
 * vector shape data is just as important as the image data from loadImage().
 * <p>
 * For more sophisticated import/export, consider the
 * <A HREF="http://xmlgraphics.apache.org/batik/">Batik</A>
 * library from the Apache Software Foundation. Future improvements to this
 * library may focus on this properly supporting a specific subset of SVG,
 * for instance the simpler SVG profiles known as
 * <A HREF="http://www.w3.org/TR/SVGMobile/">SVG Tiny or Basic</A>,
 * although we still would not support the interactivity options.
 *
 * <p> <hr noshade> <p>
 *
 * A minimal example program using SVG:
 * (assuming a working moo.svg is in your data folder)
 *
 * <PRE>
 * PShape moo;
 *
 * void setup() {
 *   size(400, 400);
 *   moo = loadShape("moo.svg");
 * }
 * void draw() {
 *   background(255);
 *   shape(moo, mouseX, mouseY);
 * }
 * </PRE>
 *
 * This code is based on the Candy library written by Michael Chang, which was
 * later revised and expanded for use as a Processing core library by Ben Fry.
 * Thanks to Ricard Marxer Pinon for help with better Inkscape support in 0154.
 *
 * <p> <hr noshade> <p>
 *
 * Late October 2008 revisions from ricardmp, incorporated by fry (0154)
 * <UL>
 * <LI>Better style attribute handling, enabling better Inkscape support.
 * </UL>
 *
 * October 2008 revisions by fry (Processing 0149, pre-1.0)
 * <UL>
 * <LI> Candy is no longer a separate library, and is instead part of core.
 * <LI> Loading now works through loadShape()
 * <LI> Shapes are now drawn using the new PGraphics shape() method.
 * </UL>
 *
 * August 2008 revisions by fry (Processing 0149)
 * <UL>
 * <LI> Major changes to rework around PShape.
 * <LI> Now implementing more of the "transform" attribute.
 * </UL>
 *
 * February 2008 revisions by fry (Processing 0136)
 * <UL>
 * <LI> Added support for quadratic curves in paths (Q, q, T, and t operators)
 * <LI> Support for reading SVG font data (though not rendering it yet)
 * </UL>
 *
 * Revisions for "Candy 2" November 2006 by fry
 * <UL>
 * <LI> Switch to the new processing.xml library
 * <LI> Several bug fixes for parsing of shape data
 * <LI> Support for linear and radial gradients
 * <LI> Support for additional types of shapes
 * <LI> Added compound shapes (shapes with interior points)
 * <LI> Added methods to get shapes from an internal table
 * </UL>
 *
 * Revision 10/31/06 by flux
 * <UL>
 * <LI> Now properly supports Processing 0118
 * <LI> Fixed a bunch of things for Casey's students and general buggity.
 * <LI> Will now properly draw #FFFFFFFF colors (were being represented as -1)
 * <LI> SVGs without <g> tags are now properly caught and loaded
 * <LI> Added a method customStyle() for overriding SVG colors/styles
 * <LI> Added a method SVGStyle() to go back to using SVG colors/styles
 * </UL>
 *
 * Some SVG objects and features may not yet be supported.
 * Here is a partial list of non-included features
 * <UL>
 * <LI> Rounded rectangles
 * <LI> Drop shadow objects
 * <LI> Typography
 * <LI> <STRIKE>Layers</STRIKE> added for Candy 2
 * <LI> Patterns
 * <LI> Embedded images
 * </UL>
 *
 * For those interested, the SVG specification can be found
 * <A HREF="http://www.w3.org/TR/SVG">here</A>.
 */
public class PShapeSVG extends PShape {
  XMLElement element;

  float opacity;
  float strokeOpacity;
  float fillOpacity;


  Gradient strokeGradient;
  Paint strokeGradientPaint;
  String strokeName;  // id of another object, gradients only?

  Gradient fillGradient;
  Paint fillGradientPaint;
  String fillName;  // id of another object


  /**
   * Initializes a new SVG Object with the given filename.
   */
  public PShapeSVG(PApplet parent, String filename) {
    // this will grab the root document, starting <svg ...>
    // the xml version and initial comments are ignored
    this(new XMLElement(parent, filename));
  }


  /**
   * Initializes a new SVG Object from the given XMLElement.
   */
  public PShapeSVG(XMLElement svg) {
    this(null, svg);

    if (!svg.getName().equals("svg")) {
      throw new RuntimeException("root is not <svg>, it's <" + svg.getName() + ">");
    }

    // not proper parsing of the viewBox, but will cover us for cases where
    // the width and height of the object is not specified
    String viewBoxStr = svg.getStringAttribute("viewBox");
    if (viewBoxStr != null) {
      int[] viewBox = PApplet.parseInt(PApplet.splitTokens(viewBoxStr));
      width = viewBox[2];
      height = viewBox[3];
    }

    // TODO if viewbox is not same as width/height, then use it to scale
    // the original objects. for now, viewbox only used when width/height
    // are empty values (which by the spec means w/h of "100%"
    String unitWidth = svg.getStringAttribute("width");
    String unitHeight = svg.getStringAttribute("height");
    if (unitWidth != null) {
      width = parseUnitSize(unitWidth);
      height = parseUnitSize(unitHeight);
    } else {
      if ((width == 0) || (height == 0)) {
        //throw new RuntimeException("width/height not specified");
        PGraphics.showWarning("The width and/or height is not " +
                              "readable in the <svg> tag of this file.");
        // For the spec, the default is 100% and 100%. For purposes
        // here, insert a dummy value because this is prolly just a
        // font or something for which the w/h doesn't matter.
        width = 1;
        height = 1;
      }
    }

    //root = new Group(null, svg);
    parseChildren(svg);  // ?
  }


  public PShapeSVG(PShapeSVG parent, XMLElement properties) {
    //super(GROUP);

    if (parent == null) {
      // set values to their defaults according to the SVG spec
      stroke = false;
      strokeColor = 0xff000000;
      strokeWeight = 1;
      strokeCap = PConstants.SQUARE;  // equivalent to BUTT in svg spec
      strokeJoin = PConstants.MITER;
      strokeGradient = null;
      strokeGradientPaint = null;
      strokeName = null;

      fill = true;
      fillColor = 0xff000000;
      fillGradient = null;
      fillGradientPaint = null;
      fillName = null;

      //hasTransform = false;
      //transformation = null; //new float[] { 1, 0, 0, 1, 0, 0 };

      strokeOpacity = 1;
      fillOpacity = 1;
      opacity = 1;

    } else {
      stroke = parent.stroke;
      strokeColor = parent.strokeColor;
      strokeWeight = parent.strokeWeight;
      strokeCap = parent.strokeCap;
      strokeJoin = parent.strokeJoin;
      strokeGradient = parent.strokeGradient;
      strokeGradientPaint = parent.strokeGradientPaint;
      strokeName = parent.strokeName;

      fill = parent.fill;
      fillColor = parent.fillColor;
      fillGradient = parent.fillGradient;
      fillGradientPaint = parent.fillGradientPaint;
      fillName = parent.fillName;

      //hasTransform = parent.hasTransform;
      //transformation = parent.transformation;

      opacity = parent.opacity;
    }

    element = properties;
    name = properties.getStringAttribute("id");

    String displayStr = properties.getStringAttribute("display", "inline");
    visible = !displayStr.equals("none");

    String transformStr = properties.getStringAttribute("transform");
    if (transformStr != null) {
      matrix = parseMatrix(transformStr);
    }

    parseColors(properties);
    parseChildren(properties);
  }


  protected void parseChildren(XMLElement graphics) {
    XMLElement[] elements = graphics.getChildren();
    children = new PShape[elements.length];
    childCount = 0;

    for (XMLElement elem : elements) {
      PShape kid = parseChild(elem);
      if (kid != null) {
        addChild(kid);
      }
    }
  }


  /**
   * Parse a child XML element.
   * Override this method to add parsing for more SVG elements.
   */
  protected PShape parseChild(XMLElement elem) {
    String name = elem.getName();
    PShapeSVG shape = null;

    if (name.equals("g")) {
      //return new BaseObject(this, elem);
      shape = new PShapeSVG(this, elem);

    } else if (name.equals("defs")) {
      // generally this will contain gradient info, so may
      // as well just throw it into a group element for parsing
      //return new BaseObject(this, elem);
      shape = new PShapeSVG(this, elem);

    } else if (name.equals("line")) {
      //return new Line(this, elem);
      //return new BaseObject(this, elem, LINE);
      shape = new PShapeSVG(this, elem);
      shape.parseLine();

    } else if (name.equals("circle")) {
      //return new BaseObject(this, elem, ELLIPSE);
      shape = new PShapeSVG(this, elem);
      shape.parseEllipse(true);

    } else if (name.equals("ellipse")) {
      //return new BaseObject(this, elem, ELLIPSE);
      shape = new PShapeSVG(this, elem);
      shape.parseEllipse(false);

    } else if (name.equals("rect")) {
      //return new BaseObject(this, elem, RECT);
      shape = new PShapeSVG(this, elem);
      shape.parseRect();

    } else if (name.equals("polygon")) {
      //return new BaseObject(this, elem, POLYGON);
      shape = new PShapeSVG(this, elem);
      shape.parsePoly(true);

    } else if (name.equals("polyline")) {
      //return new BaseObject(this, elem, POLYGON);
      shape = new PShapeSVG(this, elem);
      shape.parsePoly(false);

    } else if (name.equals("path")) {
      //return new BaseObject(this, elem, PATH);
      shape = new PShapeSVG(this, elem);
      shape.parsePath();

    } else if (name.equals("radialGradient")) {
      return new RadialGradient(this, elem);

    } else if (name.equals("linearGradient")) {
      return new LinearGradient(this, elem);

    } else if (name.equals("text")) {
      PGraphics.showWarning("Text in SVG files is not currently supported, " +
                            "convert text to outlines instead.");

    } else if (name.equals("filter")) {
      PGraphics.showWarning("Filters are not supported.");

    } else if (name.equals("mask")) {
      PGraphics.showWarning("Masks are not supported.");

    } else {
      PGraphics.showWarning("Ignoring  <" + name + "> tag.");
    }
    return shape;
  }


  protected void parseLine() {
    kind = LINE;
    family = PRIMITIVE;
    params = new float[] {
      element.getFloatAttribute("x1"),
      element.getFloatAttribute("y1"),
      element.getFloatAttribute("x2"),
      element.getFloatAttribute("y2"),
    };
    //    x = params[0];
    //    y = params[1];
    //    width = params[2];
    //    height = params[3];
  }


  /**
   * Handles parsing ellipse and circle tags.
   * @param circle true if this is a circle and not an ellipse
   */
  protected void parseEllipse(boolean circle) {
    kind = ELLIPSE;
    family = PRIMITIVE;
    params = new float[4];

    params[0] = element.getFloatAttribute("cx");
    params[1] = element.getFloatAttribute("cy");

    float rx, ry;
    if (circle) {
      rx = ry = element.getFloatAttribute("r");
    } else {
      rx = element.getFloatAttribute("rx");
      ry = element.getFloatAttribute("ry");
    }
    params[0] -= rx;
    params[1] -= ry;

    params[2] = rx*2;
    params[3] = ry*2;
  }


  protected void parseRect() {
    kind = RECT;
    family = PRIMITIVE;
    params = new float[] {
      element.getFloatAttribute("x"),
      element.getFloatAttribute("y"),
      element.getFloatAttribute("width"),
      element.getFloatAttribute("height"),
    };
  }


  /**
   * Parse a polyline or polygon from an SVG file.
   * @param close true if shape is closed (polygon), false if not (polyline)
   */
  protected void parsePoly(boolean close) {
    family = PATH;
    this.close = close;

    String pointsAttr = element.getStringAttribute("points");
    if (pointsAttr != null) {
      String[] pointsBuffer = PApplet.splitTokens(pointsAttr);
      vertexCount = pointsBuffer.length;
      vertices = new float[vertexCount][2];
      for (int i = 0; i < vertexCount; i++) {
        String pb[] = PApplet.split(pointsBuffer[i], ',');
        vertices[i][X] = Float.valueOf(pb[0]).floatValue();
        vertices[i][Y] = Float.valueOf(pb[1]).floatValue();
      }
    }
  }


  protected void parsePath() {
    family = PATH;
    kind = 0;

    String pathData = element.getStringAttribute("d");
    if (pathData == null) return;
    char[] pathDataChars = pathData.toCharArray();

    StringBuffer pathBuffer = new StringBuffer();
    boolean lastSeparate = false;

    for (int i = 0; i < pathDataChars.length; i++) {
      char c = pathDataChars[i];
      boolean separate = false;

      if (c == 'M' || c == 'm' ||
          c == 'L' || c == 'l' ||
          c == 'H' || c == 'h' ||
          c == 'V' || c == 'v' ||
          c == 'C' || c == 'c' ||  // beziers
          c == 'S' || c == 's' ||
          c == 'Q' || c == 'q' ||  // quadratic beziers
          c == 'T' || c == 't' ||
          c == 'Z' || c == 'z' ||  // closepath
          c == ',') {
        separate = true;
        if (i != 0) {
          pathBuffer.append("|");
        }
      }
      if (c == 'Z' || c == 'z') {
        separate = false;
      }
      if (c == '-' && !lastSeparate) {
        pathBuffer.append("|");
      }
      if (c != ',') {
        pathBuffer.append(c); //"" + pathDataBuffer.charAt(i));
      }
      if (separate && c != ',' && c != '-') {
        pathBuffer.append("|");
      }
      lastSeparate = separate;
    }

    // use whitespace constant to get rid of extra spaces and CR or LF
    String[] pathDataKeys =
      PApplet.splitTokens(pathBuffer.toString(), "|" + WHITESPACE);
    vertices = new float[pathDataKeys.length][2];
    vertexCodes = new int[pathDataKeys.length];

    float cx = 0;
    float cy = 0;

    int i = 0;
    while (i < pathDataKeys.length) {
      char c = pathDataKeys[i].charAt(0);
      switch (c) {

      case 'M':  // M - move to (absolute)
        cx = PApplet.parseFloat(pathDataKeys[i + 1]);
        cy = PApplet.parseFloat(pathDataKeys[i + 2]);
        parsePathMoveto(cx, cy);
        i += 3;
        break;

      case 'm':  // m - move to (relative)
        cx = cx + PApplet.parseFloat(pathDataKeys[i + 1]);
        cy = cy + PApplet.parseFloat(pathDataKeys[i + 2]);
        parsePathMoveto(cx, cy);
        i += 3;
        break;

      case 'L':
        cx = PApplet.parseFloat(pathDataKeys[i + 1]);
        cy = PApplet.parseFloat(pathDataKeys[i + 2]);
        parsePathLineto(cx, cy);
        i += 3;
        break;

      case 'l':
        cx = cx + PApplet.parseFloat(pathDataKeys[i + 1]);
        cy = cy + PApplet.parseFloat(pathDataKeys[i + 2]);
        parsePathLineto(cx, cy);
        i += 3;
        break;

        // horizontal lineto absolute
      case 'H':
        cx = PApplet.parseFloat(pathDataKeys[i + 1]);
        parsePathLineto(cx, cy);
        i += 2;
        break;

        // horizontal lineto relative
      case 'h':
        cx = cx + PApplet.parseFloat(pathDataKeys[i + 1]);
        parsePathLineto(cx, cy);
        i += 2;
        break;

      case 'V':
        cy = PApplet.parseFloat(pathDataKeys[i + 1]);
        parsePathLineto(cx, cy);
        i += 2;
        break;

      case 'v':
        cy = cy + PApplet.parseFloat(pathDataKeys[i + 1]);
        parsePathLineto(cx, cy);
        i += 2;
        break;

        // C - curve to (absolute)
      case 'C': {
        float ctrlX1 = PApplet.parseFloat(pathDataKeys[i + 1]);
        float ctrlY1 = PApplet.parseFloat(pathDataKeys[i + 2]);
        float ctrlX2 = PApplet.parseFloat(pathDataKeys[i + 3]);
        float ctrlY2 = PApplet.parseFloat(pathDataKeys[i + 4]);
        float endX = PApplet.parseFloat(pathDataKeys[i + 5]);
        float endY = PApplet.parseFloat(pathDataKeys[i + 6]);
        parsePathCurveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY);
        cx = endX;
        cy = endY;
        i += 7;
      }
        break;

        // c - curve to (relative)
      case 'c': {
        float ctrlX1 = cx + PApplet.parseFloat(pathDataKeys[i + 1]);
        float ctrlY1 = cy + PApplet.parseFloat(pathDataKeys[i + 2]);
        float ctrlX2 = cx + PApplet.parseFloat(pathDataKeys[i + 3]);
        float ctrlY2 = cy + PApplet.parseFloat(pathDataKeys[i + 4]);
        float endX = cx + PApplet.parseFloat(pathDataKeys[i + 5]);
        float endY = cy + PApplet.parseFloat(pathDataKeys[i + 6]);
        parsePathCurveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY);
        cx = endX;
        cy = endY;
        i += 7;
      }
        break;

        // S - curve to shorthand (absolute)
      case 'S': {
        float ppx = vertices[vertexCount-2][X];
        float ppy = vertices[vertexCount-2][Y];
        float px = vertices[vertexCount-1][X];
        float py = vertices[vertexCount-1][Y];
        float ctrlX1 = px + (px - ppx);
        float ctrlY1 = py + (py - ppy);
        float ctrlX2 = PApplet.parseFloat(pathDataKeys[i + 1]);
        float ctrlY2 = PApplet.parseFloat(pathDataKeys[i + 2]);
        float endX = PApplet.parseFloat(pathDataKeys[i + 3]);
        float endY = PApplet.parseFloat(pathDataKeys[i + 4]);
        parsePathCurveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY);
        cx = endX;
        cy = endY;
        i += 5;
      }
        break;

        // s - curve to shorthand (relative)
      case 's': {
        float ppx = vertices[vertexCount-2][X];
        float ppy = vertices[vertexCount-2][Y];
        float px = vertices[vertexCount-1][X];
        float py = vertices[vertexCount-1][Y];
        float ctrlX1 = px + (px - ppx);
        float ctrlY1 = py + (py - ppy);
        float ctrlX2 = cx + PApplet.parseFloat(pathDataKeys[i + 1]);
        float ctrlY2 = cy + PApplet.parseFloat(pathDataKeys[i + 2]);
        float endX = cx + PApplet.parseFloat(pathDataKeys[i + 3]);
        float endY = cy + PApplet.parseFloat(pathDataKeys[i + 4]);
        parsePathCurveto(ctrlX1, ctrlY1, ctrlX2, ctrlY2, endX, endY);
        cx = endX;
        cy = endY;
        i += 5;
      }
        break;

        // Q - quadratic curve to (absolute)
      case 'Q': {
        float ctrlX = PApplet.parseFloat(pathDataKeys[i + 1]);
        float ctrlY = PApplet.parseFloat(pathDataKeys[i + 2]);
        float endX = PApplet.parseFloat(pathDataKeys[i + 3]);
        float endY = PApplet.parseFloat(pathDataKeys[i + 4]);
        parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY);
        cx = endX;
        cy = endY;
        i += 5;
      }
        break;

        // q - quadratic curve to (relative)
      case 'q': {
        float ctrlX = cx + PApplet.parseFloat(pathDataKeys[i + 1]);
        float ctrlY = cy + PApplet.parseFloat(pathDataKeys[i + 2]);
        float endX = cx + PApplet.parseFloat(pathDataKeys[i + 3]);
        float endY = cy + PApplet.parseFloat(pathDataKeys[i + 4]);
        parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY);
        cx = endX;
        cy = endY;
        i += 5;
      }
        break;

        // T - quadratic curve to shorthand (absolute)
        // The control point is assumed to be the reflection of the
        // control point on the previous command relative to the
        // current point. (If there is no previous command or if the
        // previous command was not a Q, q, T or t, assume the control
        // point is coincident with the current point.)
      case 'T': {
        float ppx = vertices[vertexCount-2][X];
        float ppy = vertices[vertexCount-2][Y];
        float px = vertices[vertexCount-1][X];
        float py = vertices[vertexCount-1][Y];
        float ctrlX = px + (px - ppx);
        float ctrlY = py + (py - ppy);
        float endX = PApplet.parseFloat(pathDataKeys[i + 1]);
        float endY = PApplet.parseFloat(pathDataKeys[i + 2]);
        parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY);
        cx = endX;
        cy = endY;
        i += 3;
      }
        break;

        // t - quadratic curve to shorthand (relative)
      case 't': {
        float ppx = vertices[vertexCount-2][X];
        float ppy = vertices[vertexCount-2][Y];
        float px = vertices[vertexCount-1][X];
        float py = vertices[vertexCount-1][Y];
        float ctrlX = px + (px - ppx);
        float ctrlY = py + (py - ppy);
        float endX = cx + PApplet.parseFloat(pathDataKeys[i + 1]);
        float endY = cy + PApplet.parseFloat(pathDataKeys[i + 2]);
        parsePathQuadto(cx, cy, ctrlX, ctrlY, endX, endY);
        cx = endX;
        cy = endY;
        i += 3;
      }
        break;

      case 'Z':
      case 'z':
        close = true;
        i++;
        break;

      default:
        String parsed =
          PApplet.join(PApplet.subset(pathDataKeys, 0, i), ",");
        String unparsed =
          PApplet.join(PApplet.subset(pathDataKeys, i), ",");
        System.err.println("parsed: " + parsed);
        System.err.println("unparsed: " + unparsed);
        if (pathDataKeys[i].equals("a") || pathDataKeys[i].equals("A")) {
          String msg = "Sorry, elliptical arc support for SVG files " +
            "is not yet implemented (See bug #996 for details)";
          throw new RuntimeException(msg);
        }
        throw new RuntimeException("shape command not handled: " + pathDataKeys[i]);
      }
    }
  }


//      private void parsePathCheck(int num) {
//        if (vertexCount + num-1 >= vertices.length) {
//          //vertices = (float[][]) PApplet.expand(vertices);
//          float[][] temp = new float[vertexCount << 1][2];
//          System.arraycopy(vertices, 0, temp, 0, vertexCount);
//          vertices = temp;
//        }
//      }

  private void parsePathVertex(float x, float y) {
    if (vertexCount == vertices.length) {
      //vertices = (float[][]) PApplet.expand(vertices);
      float[][] temp = new float[vertexCount << 1][2];
      System.arraycopy(vertices, 0, temp, 0, vertexCount);
      vertices = temp;
    }
    vertices[vertexCount][X] = x;
    vertices[vertexCount][Y] = y;
    vertexCount++;
  }


  private void parsePathCode(int what) {
    if (vertexCodeCount == vertexCodes.length) {
      vertexCodes = PApplet.expand(vertexCodes);
    }
    vertexCodes[vertexCodeCount++] = what;
  }


  private void parsePathMoveto(float px, float py) {
    if (vertexCount > 0) {
      parsePathCode(BREAK);
    }
    parsePathCode(VERTEX);
    parsePathVertex(px, py);
  }


  private void parsePathLineto(float px, float py) {
    parsePathCode(VERTEX);
    parsePathVertex(px, py);
  }


  private void parsePathCurveto(float x1, float y1,
                                float x2, float y2,
                                float x3, float y3) {
    parsePathCode(BEZIER_VERTEX);
    parsePathVertex(x1, y1);
    parsePathVertex(x2, y2);
    parsePathVertex(x3, y3);
  }

  private void parsePathQuadto(float x1, float y1, 
                               float cx, float cy,
                               float x2, float y2) {
    parsePathCode(BEZIER_VERTEX);
    // x1/y1 already covered by last moveto, lineto, or curveto
    parsePathVertex(x1 + ((cx-x1)*2/3.0f), y1 + ((cy-y1)*2/3.0f));
    parsePathVertex(x2 + ((cx-x2)*2/3.0f), y2 + ((cy-y2)*2/3.0f));
    parsePathVertex(x2, y2);
  }


  /**
   * Parse the specified SVG matrix into a PMatrix2D. Note that PMatrix2D
   * is rotated relative to the SVG definition, so parameters are rearranged
   * here. More about the transformation matrices in
   * <a href="http://www.w3.org/TR/SVG/coords.html#TransformAttribute">this section</a>
   * of the SVG documentation.
   * @param matrixStr text of the matrix param.
   * @return a good old-fashioned PMatrix2D
   */
  static protected PMatrix2D parseMatrix(String matrixStr) {
    String[] pieces = PApplet.match(matrixStr, "\\s*(\\w+)\\((.*)\\)");
    if (pieces == null) {
      System.err.println("Could not parse transform " + matrixStr);
      return null;
    }
    float[] m = PApplet.parseFloat(PApplet.splitTokens(pieces[2], ", "));
    if (pieces[1].equals("matrix")) {
      return new PMatrix2D(m[0], m[2], m[4], m[1], m[3], m[5]);

    } else if (pieces[1].equals("translate")) {
      float tx = m[0];
      float ty = (m.length == 2) ? m[1] : m[0];
      //return new float[] { 1, 0, tx,  0, 1, ty };
      return new PMatrix2D(1, 0, tx, 0, 1, ty);

    } else if (pieces[1].equals("scale")) {
      float sx = m[0];
      float sy = (m.length == 2) ? m[1] : m[0];
      //return new float[] { sx, 0, 0, 0, sy, 0 };
      return new PMatrix2D(sx, 0, 0,  0, sy, 0);

    } else if (pieces[1].equals("rotate")) {
      float angle = m[0];

      if (m.length == 1) {
        float c = PApplet.cos(angle);
        float s = PApplet.sin(angle);
        // SVG version is cos(a) sin(a) -sin(a) cos(a) 0 0
        return new PMatrix2D(c, -s, 0, s, c, 0);

      } else if (m.length == 3) {
        PMatrix2D mat = new PMatrix2D(0, 1, m[1],  1, 0, m[2]);
        mat.rotate(m[0]);
        mat.translate(-m[1], -m[2]);
        return mat; //.get(null);
      }

    } else if (pieces[1].equals("skewX")) {
      return new PMatrix2D(1, 0, 1,  PApplet.tan(m[0]), 0, 0);

    } else if (pieces[1].equals("skewY")) {
      return new PMatrix2D(1, 0, 1,  0, PApplet.tan(m[0]), 0);
    }
    return null;
  }


  protected void parseColors(XMLElement properties) {
    if (properties.hasAttribute("opacity")) {
      String opacityText = properties.getStringAttribute("opacity");
      setOpacity(opacityText);
    }

    if (properties.hasAttribute("stroke")) {
      String strokeText = properties.getStringAttribute("stroke");
      setStroke(strokeText);
    }

    if (properties.hasAttribute("stroke-width")) {
      // if NaN (i.e. if it's 'inherit') then default back to the inherit setting
      String lineweight = properties.getStringAttribute("stroke-width");
      setStrokeWeight(lineweight);
    }

    if (properties.hasAttribute("stroke-linejoin")) {
      String linejoin = properties.getStringAttribute("stroke-linejoin");
      setStrokeJoin(linejoin);
    }

    if (properties.hasAttribute("stroke-linecap")) {
      String linecap = properties.getStringAttribute("stroke-linecap");
      setStrokeCap(linecap);
    }


    // fill defaults to black (though stroke defaults to "none")
    // http://www.w3.org/TR/SVG/painting.html#FillProperties
    if (properties.hasAttribute("fill")) {
      String fillText = properties.getStringAttribute("fill");
      setFill(fillText);

    }

    if (properties.hasAttribute("style")) {
      String styleText = properties.getStringAttribute("style");
      String[] styleTokens = PApplet.splitTokens(styleText, ";");

      //PApplet.println(styleTokens);
      for (int i = 0; i < styleTokens.length; i++) {
        String[] tokens = PApplet.splitTokens(styleTokens[i], ":");
        //PApplet.println(tokens);

        tokens[0] = PApplet.trim(tokens[0]);

        if (tokens[0].equals("fill")) {
          setFill(tokens[1]);

        } else if(tokens[0].equals("fill-opacity")) {
          setFillOpacity(tokens[1]);

        } else if(tokens[0].equals("stroke")) {
          setStroke(tokens[1]);

        } else if(tokens[0].equals("stroke-width")) {
          setStrokeWeight(tokens[1]);

        } else if(tokens[0].equals("stroke-linecap")) {
          setStrokeCap(tokens[1]);

        } else if(tokens[0].equals("stroke-linejoin")) {
          setStrokeJoin(tokens[1]);

        } else if(tokens[0].equals("stroke-opacity")) {
          setStrokeOpacity(tokens[1]);

        } else if(tokens[0].equals("opacity")) {
          setOpacity(tokens[1]);

        } else {
          // Other attributes are not yet implemented
        }
      }
    }
  }

  void setOpacity(String opacityText) {
    opacity = PApplet.parseFloat(opacityText);
    strokeColor = ((int) (opacity * 255)) << 24 | strokeColor & 0xFFFFFF;
    fillColor = ((int) (opacity * 255)) << 24 | fillColor & 0xFFFFFF;
  }

  void setStrokeWeight(String lineweight) {
    strokeWeight = PApplet.parseFloat(lineweight);
  }

  void setStrokeOpacity(String opacityText) {
    strokeOpacity = PApplet.parseFloat(opacityText);
    strokeColor = ((int) (strokeOpacity * 255)) << 24 | strokeColor & 0xFFFFFF;
  }

  void setStroke(String strokeText) {
    int opacityMask = strokeColor & 0xFF000000;
    if (strokeText.equals("none")) {
      stroke = false;
    } else if (strokeText.startsWith("#")) {
      stroke = true;
      strokeColor = opacityMask |
        (Integer.parseInt(strokeText.substring(1), 16)) & 0xFFFFFF;
    } else if (strokeText.startsWith("rgb")) {
      stroke = true;
      strokeColor = opacityMask | parseRGB(strokeText);
    } else if (strokeText.startsWith("url(#")) {
      strokeName = strokeText.substring(5, strokeText.length() - 1);
      Object strokeObject = findChild(strokeName);
      if (strokeObject instanceof Gradient) {
        strokeGradient = (Gradient) strokeObject;
        strokeGradientPaint = calcGradientPaint(strokeGradient); //, opacity);
      } else {
        System.err.println("url " + strokeName + " refers to unexpected data");
      }
    }
  }

  void setStrokeJoin(String linejoin) {
    if (linejoin.equals("inherit")) {
      // do nothing, will inherit automatically

    } else if (linejoin.equals("miter")) {
      strokeJoin = PConstants.MITER;

    } else if (linejoin.equals("round")) {
      strokeJoin = PConstants.ROUND;

    } else if (linejoin.equals("bevel")) {
      strokeJoin = PConstants.BEVEL;
    }
  }

  void setStrokeCap(String linecap) {
    if (linecap.equals("inherit")) {
      // do nothing, will inherit automatically

    } else if (linecap.equals("butt")) {
      strokeCap = PConstants.SQUARE;

    } else if (linecap.equals("round")) {
      strokeCap = PConstants.ROUND;

    } else if (linecap.equals("square")) {
      strokeCap = PConstants.PROJECT;
    }
  }

  void setFillOpacity(String opacityText) {
    fillOpacity = PApplet.parseFloat(opacityText);
    fillColor = ((int) (fillOpacity * 255)) << 24 | fillColor & 0xFFFFFF;
  }

  void setFill(String fillText) {
    int opacityMask = fillColor & 0xFF000000;
    if (fillText.equals("none")) {
      fill = false;
    } else if (fillText.startsWith("#")) {
      fill = true;
      fillColor = opacityMask |
        (Integer.parseInt(fillText.substring(1), 16)) & 0xFFFFFF;
      //System.out.println("hex for fill is " + PApplet.hex(fillColor));
    } else if (fillText.startsWith("rgb")) {
      fill = true;
      fillColor = opacityMask | parseRGB(fillText);
    } else if (fillText.startsWith("url(#")) {
      fillName = fillText.substring(5, fillText.length() - 1);
      //PApplet.println("looking for " + fillName);
      Object fillObject = findChild(fillName);
      //PApplet.println("found " + fillObject);
      if (fillObject instanceof Gradient) {
        fill = true;
        fillGradient = (Gradient) fillObject;
        fillGradientPaint = calcGradientPaint(fillGradient); //, opacity);
        //PApplet.println("got filla " + fillObject);
      } else {
        System.err.println("url " + fillName + " refers to unexpected data");
      }
    }
  }


  static protected int parseRGB(String what) {
    int leftParen = what.indexOf('(') + 1;
    int rightParen = what.indexOf(')');
    String sub = what.substring(leftParen, rightParen);
    int[] values = PApplet.parseInt(PApplet.splitTokens(sub, ", "));
    return (values[0] << 16) | (values[1] << 8) | (values[2]);
  }


  static protected HashMap<String, String> parseStyleAttributes(String style) {
    HashMap<String, String> table = new HashMap<String, String>();
    String[] pieces = style.split(";");
    for (int i = 0; i < pieces.length; i++) {
      String[] parts = pieces[i].split(":");
      table.put(parts[0], parts[1]);
    }
    return table;
  }


  /**
   * Parse a size that may have a suffix for its units.
   * Ignoring cases where this could also be a percentage.
   * The <A HREF="http://www.w3.org/TR/SVG/coords.html#Units">units</A> spec:
   * <UL>
   * <LI>"1pt" equals "1.25px" (and therefore 1.25 user units)
   * <LI>"1pc" equals "15px" (and therefore 15 user units)
   * <LI>"1mm" would be "3.543307px" (3.543307 user units)
   * <LI>"1cm" equals "35.43307px" (and therefore 35.43307 user units)
   * <LI>"1in" equals "90px" (and therefore 90 user units)
   * </UL>
   */
  static protected float parseUnitSize(String text) {
    int len = text.length() - 2;

    if (text.endsWith("pt")) {
      return PApplet.parseFloat(text.substring(0, len)) * 1.25f;
    } else if (text.endsWith("pc")) {
      return PApplet.parseFloat(text.substring(0, len)) * 15;
    } else if (text.endsWith("mm")) {
      return PApplet.parseFloat(text.substring(0, len)) * 3.543307f;
    } else if (text.endsWith("cm")) {
      return PApplet.parseFloat(text.substring(0, len)) * 35.43307f;
    } else if (text.endsWith("in")) {
      return PApplet.parseFloat(text.substring(0, len)) * 90;
    } else if (text.endsWith("px")) {
      return PApplet.parseFloat(text.substring(0, len));
    } else {
      return PApplet.parseFloat(text);
    }
  }


  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


  static class Gradient extends PShapeSVG {
    AffineTransform transform;

    float[] offset;
    int[] color;
    int count;

    public Gradient(PShapeSVG parent, XMLElement properties) {
      super(parent, properties);

      XMLElement elements[] = properties.getChildren();
      offset = new float[elements.length];
      color = new int[elements.length];

      // <stop  offset="0" style="stop-color:#967348"/>
      for (int i = 0; i < elements.length; i++) {
        XMLElement elem = elements[i];
        String name = elem.getName();
        if (name.equals("stop")) {
          offset[count] = elem.getFloatAttribute("offset");
          String style = elem.getStringAttribute("style");
          HashMap<String, String> styles = parseStyleAttributes(style);

          String colorStr = styles.get("stop-color");
          if (colorStr == null) colorStr = "#000000";
          String opacityStr = styles.get("stop-opacity");
          if (opacityStr == null) opacityStr = "1";
          int tupacity = (int) (PApplet.parseFloat(opacityStr) * 255);
          color[count] = (tupacity << 24) |
            Integer.parseInt(colorStr.substring(1), 16);
          count++;
        }
      }
      offset = PApplet.subset(offset, 0, count);
      color = PApplet.subset(color, 0, count);
    }
  }


  class LinearGradient extends Gradient {
    float x1, y1, x2, y2;

    public LinearGradient(PShapeSVG parent, XMLElement properties) {
      super(parent, properties);

      this.x1 = properties.getFloatAttribute("x1");
      this.y1 = properties.getFloatAttribute("y1");
      this.x2 = properties.getFloatAttribute("x2");
      this.y2 = properties.getFloatAttribute("y2");

      String transformStr =
        properties.getStringAttribute("gradientTransform");

      if (transformStr != null) {
        float t[] = parseMatrix(transformStr).get(null);
        this.transform = new AffineTransform(t[0], t[3], t[1], t[4], t[2], t[5]);

        Point2D t1 = transform.transform(new Point2D.Float(x1, y1), null);
        Point2D t2 = transform.transform(new Point2D.Float(x2, y2), null);

        this.x1 = (float) t1.getX();
        this.y1 = (float) t1.getY();
        this.x2 = (float) t2.getX();
        this.y2 = (float) t2.getY();
      }
    }
  }


  class RadialGradient extends Gradient {
    float cx, cy, r;

    public RadialGradient(PShapeSVG parent, XMLElement properties) {
      super(parent, properties);

      this.cx = properties.getFloatAttribute("cx");
      this.cy = properties.getFloatAttribute("cy");
      this.r = properties.getFloatAttribute("r");

      String transformStr =
        properties.getStringAttribute("gradientTransform");

      if (transformStr != null) {
        float t[] = parseMatrix(transformStr).get(null);
        this.transform = new AffineTransform(t[0], t[3], t[1], t[4], t[2], t[5]);

        Point2D t1 = transform.transform(new Point2D.Float(cx, cy), null);
        Point2D t2 = transform.transform(new Point2D.Float(cx + r, cy), null);

        this.cx = (float) t1.getX();
        this.cy = (float) t1.getY();
        this.r = (float) (t2.getX() - t1.getX());
      }
    }
  }



  class LinearGradientPaint implements Paint {
    float x1, y1, x2, y2;
    float[] offset;
    int[] color;
    int count;
    float opacity;

    public LinearGradientPaint(float x1, float y1, float x2, float y2,
                               float[] offset, int[] color, int count,
                               float opacity) {
      this.x1 = x1;
      this.y1 = y1;
      this.x2 = x2;
      this.y2 = y2;
      this.offset = offset;
      this.color = color;
      this.count = count;
      this.opacity = opacity;
    }

    public PaintContext createContext(ColorModel cm,
                                      Rectangle deviceBounds, Rectangle2D userBounds,
                                      AffineTransform xform, RenderingHints hints) {
      Point2D t1 = xform.transform(new Point2D.Float(x1, y1), null);
      Point2D t2 = xform.transform(new Point2D.Float(x2, y2), null);
      return new LinearGradientContext((float) t1.getX(), (float) t1.getY(),
                                       (float) t2.getX(), (float) t2.getY());
    }

    public int getTransparency() {
      return TRANSLUCENT;  // why not.. rather than checking each color
    }

    public class LinearGradientContext implements PaintContext {
      int ACCURACY = 2;
      float tx1, ty1, tx2, ty2;

      public LinearGradientContext(float tx1, float ty1, float tx2, float ty2) {
        this.tx1 = tx1;
        this.ty1 = ty1;
        this.tx2 = tx2;
        this.ty2 = ty2;
      }

      public void dispose() { }

      public ColorModel getColorModel() { return ColorModel.getRGBdefault(); }

      public Raster getRaster(int x, int y, int w, int h) {
        WritableRaster raster =
          getColorModel().createCompatibleWritableRaster(w, h);

        int[] data = new int[w * h * 4];

        // make normalized version of base vector
        float nx = tx2 - tx1;
        float ny = ty2 - ty1;
        float len = (float) Math.sqrt(nx*nx + ny*ny);
        if (len != 0) {
          nx /= len;
          ny /= len;
        }

        int span = (int) PApplet.dist(tx1, ty1, tx2, ty2) * ACCURACY;
        if (span <= 0) {
          //System.err.println("span is too small");
          // annoying edge case where the gradient isn't legit
          int index = 0;
          for (int j = 0; j < h; j++) {
            for (int i = 0; i < w; i++) {
              data[index++] = 0;
              data[index++] = 0;
              data[index++] = 0;
              data[index++] = 255;
            }
          }

        } else {
          int[][] interp = new int[span][4];
          int prev = 0;
          for (int i = 1; i < count; i++) {
            int c0 = color[i-1];
            int c1 = color[i];
            int last = (int) (offset[i] * (span-1));
            //System.out.println("last is " + last);
            for (int j = prev; j <= last; j++) {
              float btwn = PApplet.norm(j, prev, last);
              interp[j][0] = (int) PApplet.lerp((c0 >> 16) & 0xff, (c1 >> 16) & 0xff, btwn);
              interp[j][1] = (int) PApplet.lerp((c0 >> 8) & 0xff, (c1 >> 8) & 0xff, btwn);
              interp[j][2] = (int) PApplet.lerp(c0 & 0xff, c1 & 0xff, btwn);
              interp[j][3] = (int) (PApplet.lerp((c0 >> 24) & 0xff, (c1 >> 24) & 0xff, btwn) * opacity);
              //System.out.println(j + " " + interp[j][0] + " " + interp[j][1] + " " + interp[j][2]);
            }
            prev = last;
          }

          int index = 0;
          for (int j = 0; j < h; j++) {
            for (int i = 0; i < w; i++) {
              //float distance = 0; //PApplet.dist(cx, cy, x + i, y + j);
              //int which = PApplet.min((int) (distance * ACCURACY), interp.length-1);
              float px = (x + i) - tx1;
              float py = (y + j) - ty1;
              // distance up the line is the dot product of the normalized
              // vector of the gradient start/stop by the point being tested
              int which = (int) ((px*nx + py*ny) * ACCURACY);
              if (which < 0) which = 0;
              if (which > interp.length-1) which = interp.length-1;
              //if (which > 138) System.out.println("grabbing " + which);

              data[index++] = interp[which][0];
              data[index++] = interp[which][1];
              data[index++] = interp[which][2];
              data[index++] = interp[which][3];
            }
          }
        }
        raster.setPixels(0, 0, w, h, data);

        return raster;
      }
    }
  }


  class RadialGradientPaint implements Paint {
    float cx, cy, radius;
    float[] offset;
    int[] color;
    int count;
    float opacity;

    public RadialGradientPaint(float cx, float cy, float radius,
                               float[] offset, int[] color, int count,
                               float opacity) {
      this.cx = cx;
      this.cy = cy;
      this.radius = radius;
      this.offset = offset;
      this.color = color;
      this.count = count;
      this.opacity = opacity;
    }

    public PaintContext createContext(ColorModel cm,
                                      Rectangle deviceBounds, Rectangle2D userBounds,
                                      AffineTransform xform, RenderingHints hints) {
      return new RadialGradientContext();
    }

    public int getTransparency() {
      return TRANSLUCENT;
    }

    public class RadialGradientContext implements PaintContext {
      int ACCURACY = 5;

      public void dispose() {}

      public ColorModel getColorModel() { return ColorModel.getRGBdefault(); }

      public Raster getRaster(int x, int y, int w, int h) {
        WritableRaster raster =
          getColorModel().createCompatibleWritableRaster(w, h);

        int span = (int) radius * ACCURACY;
        int[][] interp = new int[span][4];
        int prev = 0;
        for (int i = 1; i < count; i++) {
          int c0 = color[i-1];
          int c1 = color[i];
          int last = (int) (offset[i] * (span - 1));
          for (int j = prev; j <= last; j++) {
            float btwn = PApplet.norm(j, prev, last);
            interp[j][0] = (int) PApplet.lerp((c0 >> 16) & 0xff, (c1 >> 16) & 0xff, btwn);
            interp[j][1] = (int) PApplet.lerp((c0 >> 8) & 0xff, (c1 >> 8) & 0xff, btwn);
            interp[j][2] = (int) PApplet.lerp(c0 & 0xff, c1 & 0xff, btwn);
            interp[j][3] = (int) (PApplet.lerp((c0 >> 24) & 0xff, (c1 >> 24) & 0xff, btwn) * opacity);
          }
          prev = last;
        }

        int[] data = new int[w * h * 4];
        int index = 0;
        for (int j = 0; j < h; j++) {
          for (int i = 0; i < w; i++) {
            float distance = PApplet.dist(cx, cy, x + i, y + j);
            int which = PApplet.min((int) (distance * ACCURACY), interp.length-1);

            data[index++] = interp[which][0];
            data[index++] = interp[which][1];
            data[index++] = interp[which][2];
            data[index++] = interp[which][3];
          }
        }
        raster.setPixels(0, 0, w, h, data);

        return raster;
      }
    }
  }


  protected Paint calcGradientPaint(Gradient gradient) {
    if (gradient instanceof LinearGradient) {
      LinearGradient grad = (LinearGradient) gradient;
      return new LinearGradientPaint(grad.x1, grad.y1, grad.x2, grad.y2,
                                     grad.offset, grad.color, grad.count,
                                     opacity);

    } else if (gradient instanceof RadialGradient) {
      RadialGradient grad = (RadialGradient) gradient;
      return new RadialGradientPaint(grad.cx, grad.cy, grad.r,
                                     grad.offset, grad.color, grad.count,
                                     opacity);
    }
    return null;
  }


//  protected Paint calcGradientPaint(Gradient gradient,
//                                    float x1, float y1, float x2, float y2) {
//    if (gradient instanceof LinearGradient) {
//      LinearGradient grad = (LinearGradient) gradient;
//      return new LinearGradientPaint(x1, y1, x2, y2,
//                                     grad.offset, grad.color, grad.count,
//                                     opacity);
//    }
//    throw new RuntimeException("Not a linear gradient.");
//  }


//  protected Paint calcGradientPaint(Gradient gradient,
//                                    float cx, float cy, float r) {
//    if (gradient instanceof RadialGradient) {
//      RadialGradient grad = (RadialGradient) gradient;
//      return new RadialGradientPaint(cx, cy, r,
//                                     grad.offset, grad.color, grad.count,
//                                     opacity);
//    }
//    throw new RuntimeException("Not a radial gradient.");
//  }


  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


  protected void styles(PGraphics g) {
    super.styles(g);

    if (g instanceof PGraphicsJava2D) {
      PGraphicsJava2D p2d = (PGraphicsJava2D) g;

      if (strokeGradient != null) {
        p2d.strokeGradient = true;
        p2d.strokeGradientObject = strokeGradientPaint;
      } else {
        // need to shut off, in case parent object has a gradient applied
        //p2d.strokeGradient = false;
      }
      if (fillGradient != null) {
        p2d.fillGradient = true;
        p2d.fillGradientObject = fillGradientPaint;
      } else {
        // need to shut off, in case parent object has a gradient applied
        //p2d.fillGradient = false;
      }
    }
  }


  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


  //public void drawImpl(PGraphics g) {
  // do nothing
  //}


  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


  /**
   * Get a particular element based on its SVG ID. When editing SVG by hand,
   * this is the id="" tag on any SVG element. When editing from Illustrator,
   * these IDs can be edited by expanding the layers palette. The names used
   * in the layers palette, both for the layers or the shapes and groups
   * beneath them can be used here.
   * <PRE>
   * // This code grabs "Layer 3" and the shapes beneath it.
   * SVG layer3 = svg.getChild("Layer 3");
   * </PRE>
   */
  public PShape getChild(String name) {
    PShape found = super.getChild(name);
    if (found == null) {
      // Otherwise try with underscores instead of spaces
      // (this is how Illustrator handles spaces in the layer names).
      found = super.getChild(name.replace(' ', '_'));
    }
    // Set bounding box based on the parent bounding box
    if (found != null) {
//      found.x = this.x;
//      found.y = this.y;
      found.width = this.width;
      found.height = this.height;
    }
    return found;
  }


  /**
   * Prints out the SVG document. Useful for parsing.
   */
  public void print() {
    PApplet.println(element.toString());
  }
}
