import javafx.application.*;
import javafx.animation.*;
import javafx.util.*;
import javafx.stage.*;
import javafx.scene.*;
import javafx.scene.shape.*;
import javafx.scene.paint.*;
import javafx.scene.transform.*;
import javafx.event.*;
import javafx.scene.shape.StrokeType;
import com.sun.javafx.perf.PerformanceTracker;

public class BenchShapes extends Application {
    public static enum ShapeType {
        RECT, RRECT, CIRCLE, OVAL,
        TRIANGLE, TRI_QUAD, TRI_CUBIC,
        RECT_PATH, RECT_QUAD, RECT_CUBIC,
        POLYGON, POLY_QUAD, POLY_CUBIC, SUPER_POLY,
    };
    static int NSHAPES = 200;
    static double DIM = 200;
    static double HDIM = DIM/2.0;

    static ShapeType shapetype = ShapeType.RECT;
    static StrokeType stroketype = StrokeType.CENTERED;
    static boolean domarkup = false;
    static boolean dograd = false;
    static boolean dodashes = false;
    static boolean randomize = false;
    static double degoff = 0.0;

    Group root;
    Rotate rotator;

    public void start(Stage stage) {
        stage.setTitle("Inner/Outer stroke test");

        root = new Group();
        double dist = (900 - DIM*1.5) / 2.0;
        if (dist < 0) dist = 0;
        double sdim = dist*2 + DIM * 1.5;
        if (sdim < 900) sdim = 900;
        Scene scene = new Scene(root, sdim, sdim);
        scene.setFill(Color.WHITE);

        for (int i = 0; i < NSHAPES; i++) {
            double deg = degoff + i * 360f / NSHAPES;
            double rad = Math.toRadians(deg);
            double cx = sdim/2.0 + Math.cos(rad) * dist;
            double cy = sdim/2.0 + Math.sin(rad) * dist;
            Paint p;
            if (dograd) {
                Color c1 = Color.hsb(deg, 1.0, 0.2);
                Color c2 = Color.hsb(deg, 1.0, 1.0);
                Color c3 = Color.hsb(deg, 1.0, 0.2);
                p = new LinearGradient(0, 0, 1, 1, true,
                                       CycleMethod.REFLECT,
                                       new Stop[] {
                                           new Stop(0.0f, c1),
                                           new Stop(0.5f, c2),
                                           new Stop(1.0f, c3),
                                       });
            } else {
                p = Color.hsb(deg, 1.0, 1.0);
            }
            double dim = randomize ? DIM + Math.random() : DIM;
            double hdim = dim/2.0;
            Node n = makeShape(cx, cy, hdim, dim, 10, p);
            n.setRotate(-deg);
            root.getChildren().add(n);
        }
        rotator = Transform.rotate(0, sdim/2.0, sdim/2.0);
        root.getTransforms().add(rotator);


        stage.setScene(scene);
        stage.setVisible(true);

        perfTracker = PerformanceTracker.getSceneTracker(scene);
        initTimelines();
    }

    public void initTimelines() {
        new AnimationTimer() {
            public void handle(long now) {
                rotator.setAngle(rotator.getAngle()+1);
            }
        }.start();


        Timeline fpsTimeline = new Timeline();
        fpsTimeline.setCycleCount(Timeline.INDEFINITE);
        EventHandler<ActionEvent> handler =
            new EventHandler<ActionEvent>() {
                @Override public void handle(ActionEvent e) {
                    updateFPS();
                }
            };
        KeyFrame fpsKeyFrame =
            new KeyFrame(Duration.valueOf(2000), handler);
        fpsTimeline.getKeyFrames().add(fpsKeyFrame);

        fpsTimeline.play();
    }

    private PerformanceTracker perfTracker;

    void updateFPS() {
        String fpsText =
            String.format("FPS: %.2f", perfTracker.getInstantFPS());
        //fpsLabel.setText(fpsText);
        System.out.println(fpsText);
    }

    public Node makeShape(double cx, double cy, double hdim, double dim,
                          float lw, Paint p)
    {
        switch (shapetype) {
            case RECT:
                return makeRect(cx-hdim, cy-hdim, dim, dim,
                                0, 0, lw, p);
            case RRECT:
                return makeRect(cx-hdim, cy-hdim, dim, dim,
                                hdim, hdim, lw, p);
            case CIRCLE:
                return makeCircle(cx, cy, hdim, lw, p);
            case OVAL:
                return makeEllipse(cx, cy, hdim, hdim, lw, p);
            case RECT_PATH:
                return makeRectPath(cx-hdim, cy-hdim, dim, dim, lw, p);
            case RECT_QUAD:
                return makeQuadRect(cx-hdim, cy-hdim, dim, dim, lw, p);
            case RECT_CUBIC:
                return makeCubicRect(cx-hdim, cy-hdim, dim, dim, lw, p);
            case TRIANGLE:
                return makeTriangle(cx-hdim, cy-hdim, dim, dim, lw, p);
            case TRI_QUAD:
                return makeQuadTriangle(cx-hdim, cy-hdim, dim, dim, lw, p);
            case TRI_CUBIC:
                return makeCubicTriangle(cx-hdim, cy-hdim, dim, dim, lw, p);
            case POLYGON:
                return makePolygon(cx-hdim, cy-hdim, dim, dim, lw, p, 90 + 9);
            case POLY_QUAD:
                return makeQuadPoly(cx-hdim, cy-hdim, dim, dim, lw, p);
            case POLY_CUBIC:
                return makeCubicPoly(cx-hdim, cy-hdim, dim, dim, lw, p);
            case SUPER_POLY:
                return makePolygon(cx-hdim, cy-hdim, dim, dim, lw, p, 131);
            default:
                throw new IllegalArgumentException("Unrecognized shape type: "+
                                                   shapetype);
        }
    }

    public Node makeRect(double x, double y, double w, double h,
                         double aw, double ah, float lw, Paint p)
    {
        Rectangle r = new Rectangle();
        r.setX(x);
        r.setY(y);
        r.setWidth(w);
        r.setHeight(h);
        if (aw > 0 && ah > 0) {
            r.setArcWidth(aw);
            r.setArcHeight(ah);
        }

        return finish(r, x, y, w, h, lw, p);
    }

    public Node makeCircle(double cx, double cy, double r,
                           float lw, Paint p)
    {
        Circle c = new Circle();
        c.setCenterX(cx);
        c.setCenterY(cy);
        c.setRadius(r);

        return finish(c, cx - r, cy - r, r*2, r*2, lw, p);
    }

    public Node makeEllipse(double cx, double cy, double rx, double ry,
                            float lw, Paint p)
    {
        Ellipse e = new Ellipse();
        e.setCenterX(cx);
        e.setCenterY(cy);
        e.setRadiusX(rx);
        e.setRadiusY(ry);

        return finish(e, cx - rx, cy - ry, rx*2, ry*2, lw, p);
    }

    public Node makeRectPath(double x, double y, double w, double h,
                             float lw, Paint p)
    {
        double hw = w / 2.0;
        double hh = h / 2.0;
        double qw = hw / 2.0;
        double qh = hh / 2.0;
        Path path = new Path();
        path.getElements().add(new MoveTo(x     , y     ));
        path.getElements().add(new LineTo(x+hw  , y+qh  ));
        path.getElements().add(new LineTo(x+w   , y     ));
        path.getElements().add(new LineTo(x+w-qw, y+hh  ));
        path.getElements().add(new LineTo(x+w   , y+h   ));
        path.getElements().add(new LineTo(x+hw  , y+h-qh));
        path.getElements().add(new LineTo(x     , y+h   ));
        path.getElements().add(new LineTo(x+qw  , y+hh  ));
        path.getElements().add(new ClosePath());

        return finish(path, x, y, w, h, lw, p);
    }

    public Node makeQuadRect(double x, double y, double w, double h,
                             float lw, Paint p)
    {
        double hw = w / 2.0;
        double hh = h / 2.0;
        double qw = hw / 2.0;
        double qh = hh / 2.0;
        Path curve = new Path();
        curve.getElements().add(new MoveTo(x, y));
        curve.getElements().add(new QuadCurveTo(x  +hw, y  +qh, x+w, y  ));
        curve.getElements().add(new QuadCurveTo(x+w-qw, y  +hh, x+w, y+h));
        curve.getElements().add(new QuadCurveTo(x  +hw, y+h-qh, x  , y+h));
        curve.getElements().add(new QuadCurveTo(x  +qw, y  +hh, x  , y  ));
        curve.getElements().add(new ClosePath());

        return finish(curve, x, y, w, h, lw, p);
    }

    public Node makeCubicRect(double x, double y, double w, double h,
                              float lw, Paint p)
    {
        double hw = w / 2.0;
        double hh = h / 2.0;
        double qw = hw / 2.0;
        double qh = hh / 2.0;
        Path curve = new Path();
        curve.getElements().add(new MoveTo(x, y));
        curve.getElements().add(new CubicCurveTo(x  +qw, y  +qh, x+w-qw, y  +qh, x+w, y  ));
        curve.getElements().add(new CubicCurveTo(x+w-qw, y  +qh, x+w-qw, y+h-qh, x+w, y+h));
        curve.getElements().add(new CubicCurveTo(x+w-qw, y+h-qh, x  +qw, y+h-qh, x  , y+h));
        curve.getElements().add(new CubicCurveTo(x  +qw, y+h-qh, x  +qw, y  +qh, x  , y  ));
        curve.getElements().add(new ClosePath());

        return finish(curve, x, y, w, h, lw, p);
    }

    public Node makeTriangle(double x, double y, double w, double h,
                             float lw, Paint p)
    {
        Path tri = new Path();
        tri.getElements().add(new MoveTo(x + w/2.0, y));
        tri.getElements().add(new LineTo(x + w,     y + h));
        tri.getElements().add(new LineTo(x,         y + h));
        tri.getElements().add(new ClosePath());

        return finish(tri, x, y, w, h, lw, p);
    }

    public Node makeQuadTriangle(double x, double y, double w, double h,
                                 float lw, Paint p)
    {
        Path tri = new Path();
        double cx = x + w/2.0;
        double cy = y + h/2.0;
        tri.getElements().add(new MoveTo(cx,    y));
        tri.getElements().add(new QuadCurveTo(cx, cy, x + w, y + h));
        tri.getElements().add(new QuadCurveTo(cx, cy, x,     y + h));
        tri.getElements().add(new QuadCurveTo(cx, cy, cx,    y));
        tri.getElements().add(new ClosePath());

        return finish(tri, x, y, w, h, lw, p);
    }

    public Node makeCubicTriangle(double x, double y, double w, double h,
                                  float lw, Paint p)
    {
        Path tri = new Path();
        double cx = x + w/2.0;
        double cy = y + h/2.0;
        double qw = w / 4.0;
        double qh = h / 4.0;
        tri.getElements().add(new MoveTo(cx,    y));
        tri.getElements().add(new CubicCurveTo(x+w,    y,      cx,   cy,     x + w, y + h));
        tri.getElements().add(new CubicCurveTo(x+w-qw, y+h-qh, x+qw, y+h-qw, x,     y + h));
        tri.getElements().add(new CubicCurveTo(cx,     cy,     x,    y,      cx,    y));
        tri.getElements().add(new ClosePath());

        return finish(tri, x, y, w, h, lw, p);
    }

    public Node makePolygon(double x, double y, double w, double h,
                            float lw, Paint p, int inc)
    {
        Path poly = new Path();
        double hw = w/2.0;
        double hh = h/2.0;
        double cx = x + hw;
        double cy = y + hh;
        int angle = inc;
        poly.getElements().add(new MoveTo(cx+hw, cy));
        while (true) {
            double cos = Math.cos(Math.toRadians(angle));
            double sin = Math.sin(Math.toRadians(angle));
            double vx = cx + cos * hw;
            double vy = cy + sin * hh;
            poly.getElements().add(new LineTo(vx, vy));
            angle += inc;
            if (angle >= 360) {
                angle -= 360;
                if (angle == 0) {
                    break;
                }
            }
        }
        poly.getElements().add(new ClosePath());

        return finish(poly, x, y, w, h, lw, p);
    }

    public Node makeQuadPoly(double x, double y, double w, double h,
                             float lw, Paint p)
    {
        Path poly = new Path();
        double hw = w/2.0;
        double hh = h/2.0;
        double cx = x + hw;
        double cy = y + hh;
        int inc = 90+9;
        int angle = inc;
        poly.getElements().add(new MoveTo(cx+hw, cy));
        while (true) {
            double cos = Math.cos(Math.toRadians(angle));
            double sin = Math.sin(Math.toRadians(angle));
            double vx = cx + cos * hw;
            double vy = cy + sin * hh;
            poly.getElements().add(new QuadCurveTo(cx, cy, vx, vy));
            if (angle == 0) {
                break;
            }
            angle += inc;
            if (angle >= 360) {
                angle -= 360;
            }
        }
        poly.getElements().add(new ClosePath());

        return finish(poly, x, y, w, h, lw, p);
    }

    public Node makeCubicPoly(double x, double y, double w, double h,
                              float lw, Paint p)
    {
        Path poly = new Path();
        double hw = w/2.0;
        double hh = h/2.0;
        double cx = x + hw;
        double cy = y + hh;
        int inc = 90+9;
        int angle = inc;
        poly.getElements().add(new MoveTo(cx+hw, cy));
        double odx = hw;
        double ody = 0;
        while (true) {
            double cos = Math.cos(Math.toRadians(angle));
            double sin = Math.sin(Math.toRadians(angle));
            double dx = cos * hw;
            double dy = sin * hh;
            poly.getElements().add(new CubicCurveTo(cx+odx/2.0, cy+ody/2.0,
                                                    cx+ dx/2.0, cy+ dy/2.0,
                                                    cx+ dx,     cy+ dy));
            odx = dx;
            ody = dy;
            if (angle == 0) {
                break;
            }
            angle += inc;
            if (angle >= 360) {
                angle -= 360;
            }
        }
        poly.getElements().add(new ClosePath());

        return finish(poly, x, y, w, h, lw, p);
    }

    public Node finish(Shape s,
                       double x, double y, double w, double h,
                       float lw, Paint p)
    {
        if (stroketype == null) {
            s.setFill(p);
            s.setStroke(null);
        } else {
            s.setFill(null);
            s.setStroke(p);
            s.setStrokeWidth(lw);
            s.setStrokeType(stroketype);
            if (dodashes) {
                s.getStrokeDashArray().add(10.0);
                s.getStrokeDashArray().add(10.0);
                s.setStrokeLineCap(StrokeLineCap.BUTT);
            }
        }
        return markup(s, x, y, w, h, lw);
    }

    public Node markup(Node n,
                       double x, double y, double w, double h,
                       float lw)
    {
        if (!domarkup) {
            return n;
        }
        float off1, off2;
        if (stroketype == StrokeType.INSIDE) {
            off1 = -1.0f;
            off2 = -0.5f;
        } else if (stroketype == StrokeType.CENTERED) {
            off1 = -0.5f;
            off2 = +0.5f;
        } else if (stroketype == StrokeType.OUTSIDE) {
            off1 = +0.5f;
            off2 = +1.0f;
        } else if (stroketype == null) {
            off1 = off2 = 0f;
        } else {
            throw new IllegalArgumentException("Unrecognized StrokeType: "+
                                               stroketype);
        }
        off1 *= lw;
        off2 *= lw;

        Rectangle r0 = new Rectangle();
        r0.setX(x);
        r0.setY(y);
        r0.setWidth(w);
        r0.setHeight(h);
        r0.setFill(null);
        r0.setStroke(Color.GREEN);
        r0.setStrokeWidth(2);

        Group g = new Group();
        g.getChildren().add(n);
        g.getChildren().add(r0);

        if (stroketype != null) {
            Rectangle r1 = new Rectangle();
            r1.setX(x-off1);
            r1.setY(y-off1);
            r1.setWidth(w+off1+off1);
            r1.setHeight(h+off1+off1);
            r1.setFill(null);
            r1.setStroke(Color.RED);
            r1.setStrokeWidth(2);

            Rectangle r2 = new Rectangle();
            r2.setX(x-off2);
            r2.setY(y-off2);
            r2.setWidth(w+off2+off2);
            r2.setHeight(h+off2+off2);
            r2.setFill(null);
            r2.setStroke(Color.RED);
            r2.setStrokeWidth(2);

            g.getChildren().add(r1);
            g.getChildren().add(r2);
        }

        return g;
    }

    public static void main(String argv[]) {
        for (String arg: argv) {
            if (arg.startsWith("out")) {
                stroketype = StrokeType.OUTSIDE;
            } else if (arg.startsWith("in")) {
                stroketype = StrokeType.INSIDE;
            } else if (arg.startsWith("center")) {
                stroketype = StrokeType.CENTERED;
            } else if (arg.startsWith("fill")) {
                stroketype = null;
            } else if (arg.startsWith("dash")) {
                dodashes = true;
            } else if (arg.startsWith("grad")) {
                dograd = true;
            } else if (arg.startsWith("rect")) {
                shapetype = ShapeType.RECT;
            } else if (arg.startsWith("rrect") || arg.startsWith("round")) {
                shapetype = ShapeType.RRECT;
            } else if (arg.startsWith("circle")) {
                shapetype = ShapeType.CIRCLE;
            } else if (arg.startsWith("oval") || arg.startsWith("ellipse")) {
                shapetype = ShapeType.OVAL;
            } else if (arg.startsWith("path")) {
                shapetype = ShapeType.RECT_PATH;
            } else if (arg.startsWith("quadpath")) {
                shapetype = ShapeType.RECT_QUAD;
            } else if (arg.startsWith("cubicpath")) {
                shapetype = ShapeType.RECT_CUBIC;
            } else if (arg.startsWith("tri")) {
                shapetype = ShapeType.TRIANGLE;
            } else if (arg.startsWith("quadtri")) {
                shapetype = ShapeType.TRI_QUAD;
            } else if (arg.startsWith("cubictri")) {
                shapetype = ShapeType.TRI_CUBIC;
            } else if (arg.startsWith("poly")) {
                shapetype = ShapeType.POLYGON;
            } else if (arg.startsWith("quadpoly")) {
                shapetype = ShapeType.POLY_QUAD;
            } else if (arg.startsWith("cubicpoly")) {
                shapetype = ShapeType.POLY_CUBIC;
            } else if (arg.startsWith("superpoly")) {
                shapetype = ShapeType.SUPER_POLY;
            } else if (arg.startsWith("rand")) {
                randomize = true;
            } else if (arg.startsWith("num=")) {
                NSHAPES = Integer.parseInt(arg.substring(4));
            } else if (arg.startsWith("dim=")) {
                DIM = Integer.parseInt(arg.substring(4));
                HDIM = DIM/2.0;
            } else if (arg.startsWith("deg=")) {
                degoff = Double.parseDouble(arg.substring(4));
            } else {
                throw new IllegalArgumentException("Unrecognized argument: "+arg);
            }
        }
        Launcher.launch(BenchShapes.class, argv);
    }
}