content: package com.annimon.ownlang.modules.canvasfx;
  
  import com.annimon.ownlang.exceptions.TypeException;
  import com.annimon.ownlang.lib.*;
  import com.annimon.ownlang.modules.Module;
  import static com.annimon.ownlang.lib.Converters.*;
  import java.awt.Dimension;
  import java.lang.reflect.Modifier;
  import java.nio.IntBuffer;
  import java.util.Arrays;
  import java.util.HashMap;
  import java.util.LinkedHashMap;
  import java.util.Map;
  import java.util.stream.Collectors;
  import javafx.application.Platform;
  import javafx.embed.swing.JFXPanel;
  import javafx.event.Event;
  import javafx.event.EventType;
  import javafx.geometry.VPos;
  import javafx.scene.Group;
  import javafx.scene.Scene;
  import javafx.scene.canvas.Canvas;
  import javafx.scene.canvas.GraphicsContext;
  import javafx.scene.effect.*;
  import javafx.scene.image.Image;
  import javafx.scene.image.PixelReader;
  import javafx.scene.image.PixelWriter;
  import javafx.scene.image.WritableImage;
  import javafx.scene.image.WritablePixelFormat;
  import javafx.scene.input.DragEvent;
  import javafx.scene.input.KeyCode;
  import javafx.scene.input.KeyEvent;
  import javafx.scene.input.MouseButton;
  import javafx.scene.input.MouseEvent;
  import javafx.scene.input.SwipeEvent;
  import javafx.scene.paint.Color;
  import javafx.scene.shape.ArcType;
  import javafx.scene.shape.FillRule;
  import javafx.scene.shape.StrokeLineCap;
  import javafx.scene.shape.StrokeLineJoin;
  import javafx.scene.text.TextAlignment;
  import javax.swing.JFrame;
  
  /**
   *
   * @author aNNiMON
   */
  public final class canvasfx implements Module {
      
      private static final int FX_EFFECT_TYPE = 5301;
      private static final int FX_COLOR_TYPE = 5302;
  
      private static JFXPanel panel;
      private static Canvas canvas;
      
      private enum Events {
          DRAG_DETECTED(MouseEvent.DRAG_DETECTED),
          MOUSE_CLICKED(MouseEvent.MOUSE_CLICKED),
          MOUSE_DRAGGED(MouseEvent.MOUSE_DRAGGED),
          MOUSE_ENTERED(MouseEvent.MOUSE_ENTERED),
          MOUSE_ENTERED_TARGET(MouseEvent.MOUSE_ENTERED_TARGET),
          MOUSE_EXITED(MouseEvent.MOUSE_EXITED),
          MOUSE_EXITED_TARGET(MouseEvent.MOUSE_EXITED_TARGET),
          MOUSE_MOVED(MouseEvent.MOUSE_MOVED),
          MOUSE_PRESSED(MouseEvent.MOUSE_PRESSED),
          MOUSE_RELEASED(MouseEvent.MOUSE_RELEASED),
          
          KEY_PRESSED(KeyEvent.KEY_PRESSED),
          KEY_RELEASED(KeyEvent.KEY_RELEASED),
          KEY_TYPED(KeyEvent.KEY_TYPED),
          
          SWIPE_DOWN(SwipeEvent.SWIPE_DOWN),
          SWIPE_LEFT(SwipeEvent.SWIPE_LEFT),
          SWIPE_RIGHT(SwipeEvent.SWIPE_RIGHT),
          SWIPE_UP(SwipeEvent.SWIPE_UP);
  
          private final EventType<? extends Event> handler;
  
          Events(EventType<? extends Event> handler) {
              this.handler = handler;
          }
  
          public EventType<? extends Event> getHandler() {
              return handler;
          }
      }
  
      @Override
      public Map<String, Value> constants() {
          final var result = new HashMap<String, Value>(11);
          // Color class
          final Map<Value, Value> colors = Arrays.stream(Color.class.getDeclaredFields())
                  .filter(f -> Modifier.isStatic(f.getModifiers()))
                  .filter(f -> f.getType().equals(Color.class))
                  .collect(Collectors.toMap(
                          f -> new StringValue(f.getName()),
                          f -> {
                              try { return new ColorValue((Color) f.get(Color.class)); }
                              catch (IllegalAccessException ex) { return null; }
                          }));
          colors.put(new StringValue("new"), new FunctionValue(new newColor()));
          colors.put(new StringValue("rgb"), new FunctionValue(new rgbColor()));
          colors.put(new StringValue("hsb"), new FunctionValue(new hsbColor()));
          colors.put(new StringValue("web"), new FunctionValue(new webColor()));
          result.put("Color", new MapValue(colors));
  
          final MapValue arcType = new MapValue(ArcType.values().length);
          for (ArcType value : ArcType.values()) {
              arcType.set(value.name(), NumberValue.of(value.ordinal()));
          }
          result.put("ArcType", arcType);
  
          final MapValue fillRule = new MapValue(FillRule.values().length);
          for (FillRule value : FillRule.values()) {
              fillRule.set(value.name(), NumberValue.of(value.ordinal()));
          }
          result.put("FillRule", fillRule);
  
          final MapValue blendMode = new MapValue(BlendMode.values().length);
          for (BlendMode value : BlendMode.values()) {
              blendMode.set(value.name(), NumberValue.of(value.ordinal()));
          }
          result.put("BlendMode", blendMode);
  
          final MapValue lineCap = new MapValue(StrokeLineCap.values().length);
          for (StrokeLineCap value : StrokeLineCap.values()) {
              lineCap.set(value.name(), NumberValue.of(value.ordinal()));
          }
          result.put("StrokeLineCap", lineCap);
  
          final MapValue lineJoin = new MapValue(StrokeLineJoin.values().length);
          for (StrokeLineJoin value : StrokeLineJoin.values()) {
              lineJoin.set(value.name(), NumberValue.of(value.ordinal()));
          }
          result.put("StrokeLineJoin", lineJoin);
  
          final MapValue textAlignment = new MapValue(TextAlignment.values().length);
          for (TextAlignment value : TextAlignment.values()) {
              textAlignment.set(value.name(), NumberValue.of(value.ordinal()));
          }
          result.put("TextAlignment", textAlignment);
  
          final MapValue vPos = new MapValue(VPos.values().length);
          for (VPos value : VPos.values()) {
              vPos.set(value.name(), NumberValue.of(value.ordinal()));
          }
          result.put("VPos", vPos);
  
          final MapValue events = new MapValue(Events.values().length);
          for (Events value : Events.values()) {
              events.set(value.name(), NumberValue.of(value.ordinal()));
          }
          result.put("Events", events);
  
          final MapValue mouseButton = new MapValue(MouseButton.values().length);
          for (MouseButton value : MouseButton.values()) {
              mouseButton.set(value.name(), NumberValue.of(value.ordinal()));
          }
          result.put("MouseButton", mouseButton);
  
          final MapValue keyCodes = new MapValue(KeyCode.values().length);
          for (KeyCode value : KeyCode.values()) {
              keyCodes.set(value.name(), NumberValue.of(value.ordinal()));
          }
          result.put("KeyCode", keyCodes);
          return result;
      }
  
      @Override
      public Map<String, Function> functions() {
          final var result = new LinkedHashMap<String, Function>(20);
          result.put("window", new CreateWindow());
          result.put("repaint", new Repaint());
  
          result.put("BlendEffect", new BlendEffect());
          result.put("BloomEffect", new BloomEffect());
          result.put("BoxBlurEffect", new BoxBlurEffect());
          result.put("ColorAdjustEffect", new ColorAdjustEffect());
          result.put("ColorInputEffect", new ColorInputEffect());
          result.put("DropShadowEffect", new DropShadowEffect());
          result.put("GaussianBlurEffect", new GaussianBlurEffect());
          result.put("GlowEffect", new GlowEffect());
          result.put("InnerShadowEffect", new InnerShadowEffect());
          result.put("LightingEffect", new LightingEffect());
          result.put("MotionBlurEffect", new MotionBlurEffect());
          result.put("PerspectiveTransformEffect", new PerspectiveTransformEffect());
          result.put("ReflectionEffect", new ReflectionEffect());
          result.put("SepiaToneEffect", new SepiaToneEffect());
          result.put("ShadowEffect", new ShadowEffect());
  
          result.put("addEventFilter", new addEventFilter());
          result.put("addEventHandler", new addEventHandler());
          result.put("createImage", new createImage());
          return result;
      }
      
      private static class ColorValue implements Value {
          
          private final Color color;
          
          public ColorValue(Color effect) {
              this.color = effect;
          }
          
          @Override
          public Object raw() {
              return color;
          }
          
          @Override
          public int asInt() {
              final int a = (int) (color.getOpacity() * 255) & 0xFF;
              final int r = (int) (color.getRed() * 255) & 0xFF;
              final int g = (int) (color.getGreen() * 255) & 0xFF;
              final int b = (int) (color.getBlue() * 255) & 0xFF;
              return ((a << 24) | (r << 16) | (g << 8) | b);
          }
          
          @Override
          public double asNumber() {
              return asInt();
          }
          
          @Override
          public String asString() {
              return color.toString();
          }
          
          @Override
          public int type() {
              return FX_COLOR_TYPE;
          }
          
          @Override
          public int compareTo(Value o) {
              // we don't need this
              return 0;
          }
          
          @Override
          public String toString() {
              return "JavaFX Color " + color;
          }
      }
      
      private static class newColor implements Function {
  
          @Override
          public Value execute(Value[] args) {
              double r, g, b, opacity;
              if (args.length == 1) {
                  final int color = args[0].asInt();
                  r = ((color >> 16) & 0xFF) / 255.0;
                  g = ((color >> 8) & 0xFF) / 255.0;
                  b = (color & 0xFF) / 255.0;
                  opacity = ((color >> 24) & 0xFF) / 255.0;
              } else {
                  r = args[0].asNumber();
                  g = args[1].asNumber();
                  b = args[2].asNumber();
                  opacity = (args.length >= 4) ? args[3].asNumber() : 1d;
              }
              return new ColorValue(new Color(r, g, b, opacity));
          }
      }
      
      private static class rgbColor implements Function {
  
          @Override
          public Value execute(Value[] args) {
              int r = args[0].asInt();
              int g = args[1].asInt();
              int b = args[2].asInt();
              double opacity = (args.length >= 4) ? args[3].asNumber() : 1d;
              return new ColorValue(Color.rgb(r, g, b, opacity));
          }
      }
      
      private static class hsbColor implements Function {
  
          @Override
          public Value execute(Value[] args) {
              double h = args[0].asNumber();
              double s = args[1].asNumber();
              double b = args[2].asNumber();
              double opacity = (args.length >= 4) ? args[3].asNumber() : 1d;
              return new ColorValue(Color.hsb(h, s, b, opacity));
          }
      }
      
      private static class webColor implements Function {
  
          @Override
          public Value execute(Value[] args) {
              return new ColorValue(Color.web(args[0].asString(),
                      (args.length >= 2) ? args[1].asNumber() : 1d ));
          }
      }
      
      //<editor-fold defaultstate="collapsed" desc="Effects">
      private static class EffectValue implements Value {
          
          private final Effect effect;
          
          public EffectValue(Effect effect) {
              this.effect = effect;
          }
          
          @Override
          public Object raw() {
              return effect;
          }
          
          @Override
          public int asInt() {
              throw new TypeException("Cannot cast JavaFX Effect to integer");
          }
          
          @Override
          public double asNumber() {
              throw new TypeException("Cannot cast JavaFX Effect to number");
          }
          
          @Override
          public String asString() {
              return effect.toString();
          }
          
          @Override
          public int type() {
              return FX_EFFECT_TYPE;
          }
          
          @Override
          public int compareTo(Value o) {
              // we don't need this
              return 0;
          }
          
          @Override
          public String toString() {
              return "JavaFX Effect " + effect;
          }
      }
      
      private static class BlendEffect implements Function {
          @Override
          public Value execute(Value[] args) {
              Blend effect = new Blend();
              if (args.length >= 1) {
                  effect.setMode(BlendMode.values()[args[0].asInt()]);
              }
              if (args.length >= 3) {
                  effect.setBottomInput((Effect)args[1].raw());
                  effect.setTopInput((Effect)args[2].raw());
              }
              if (args.length >= 4) {
                  effect.setOpacity(args[3].asNumber());
              }
              return new EffectValue(effect);
          }
      }
      
      private static class BloomEffect implements Function {
          @Override
          public Value execute(Value[] args) {
              Bloom effect = new Bloom();
              if (args.length >= 1) {
                  effect.setThreshold(args[0].asNumber());
              }
              if (args.length >= 2) {
                  effect.setInput((Effect)args[1].raw());
              }
              return new EffectValue(effect);
          }
      }
      
      private static class BoxBlurEffect implements Function {
          @Override
          public Value execute(Value[] args) {
              BoxBlur effect = new BoxBlur();
              if (args.length >= 3) {
                  effect.setWidth(args[0].asNumber());
                  effect.setHeight(args[1].asNumber());
                  effect.setIterations(args[2].asInt());
              }
              if (args.length >= 4) {
                  effect.setInput((Effect)args[3].raw());
              }
              return new EffectValue(effect);
          }
      }
      
      private static class ColorAdjustEffect implements Function {
          @Override
          public Value execute(Value[] args) {
              return new EffectValue(new ColorAdjust(
                      args[0].asNumber(), args[1].asNumber(), args[2].asNumber(), args[3].asNumber()));
          }
      }
      
      private static class ColorInputEffect implements Function {
          @Override
          public Value execute(Value[] args) {
              return new EffectValue(new ColorInput(
                      args[0].asNumber(), args[1].asNumber(), args[2].asNumber(), args[3].asNumber(), (Color) args[4].raw()));
          }
      }
      
      private static class DropShadowEffect implements Function {
          @Override
          public Value execute(Value[] args) {
              DropShadow effect = switch (args.length) {
                  case 2 -> new DropShadow(args[0].asNumber(), (Color) args[1].raw());
                  case 4 -> new DropShadow(args[0].asNumber(), args[1].asInt(), args[2].asInt(),
                          (Color) args[3].raw());
                  case 6 -> new DropShadow(BlurType.values()[args[0].asInt()], (Color) args[1].raw(),
                          args[2].asNumber(), args[3].asNumber(), args[4].asNumber(), args[5].asNumber());
                  default -> new DropShadow();
              };
              return new EffectValue(effect);
          }
      }
      
      private static class GaussianBlurEffect implements Function {
          @Override
          public Value execute(Value[] args) {
              GaussianBlur effect = new GaussianBlur();
              if (args.length >= 1) {
                  effect.setRadius(args[0].asNumber());
              }
              if (args.length >= 2) {
                  effect.setInput((Effect)args[1].raw());
              }
              return new EffectValue(effect);
          }
      }
      
      private static class GlowEffect implements Function {
          @Override
          public Value execute(Value[] args) {
              Glow effect = new Glow();
              if (args.length >= 1) {
                  effect.setLevel(args[0].asNumber());
              }
              if (args.length >= 2) {
                  effect.setInput((Effect)args[1].raw());
              }
              return new EffectValue(effect);
          }
      }
      
      private static class InnerShadowEffect implements Function {
          @Override
          public Value execute(Value[] args) {
              InnerShadow effect = switch (args.length) {
                  case 2 -> new InnerShadow(args[0].asNumber(), (Color) args[1].raw());
                  case 4 -> new InnerShadow(args[0].asNumber(), args[1].asInt(), args[2].asInt(),
                          (Color) args[3].raw());
                  case 6 -> new InnerShadow(BlurType.values()[args[0].asInt()], (Color) args[1].raw(),
                          args[2].asNumber(), args[3].asNumber(), args[4].asNumber(), args[5].asNumber());
                  default -> new InnerShadow();
              };
              return new EffectValue(effect);
          }
      }
      
      private static class LightingEffect implements Function {
          @Override
          public Value execute(Value[] args) {
              Light light;
              final ArrayValue l = (ArrayValue) args[0];
              light = switch (l.size()) {
                  case 3 -> new Light.Distant(l.get(0).asNumber(), l.get(1).asNumber(),
                          (Color) l.get(2).raw());
                  case 4 -> new Light.Point(l.get(0).asNumber(), l.get(1).asNumber(), l.get(2).asNumber(),
                          (Color) l.get(3).raw());
                  case 5 -> new Light.Spot(l.get(0).asNumber(), l.get(1).asNumber(), l.get(2).asNumber(),
                          l.get(3).asNumber(), (Color) l.get(4).raw());
                  default -> null;
              };
              Lighting effect = new Lighting(light);
              if (args.length >= 2) {
                  effect.setSurfaceScale(args[1].asNumber());
              }
              if (args.length >= 3) {
                  effect.setDiffuseConstant(args[2].asNumber());
              }
              if (args.length >= 5) {
                  effect.setSpecularConstant(args[3].asNumber());
                  effect.setSpecularExponent(args[4].asNumber());
              }
              if (args.length >= 7) {
                  effect.setBumpInput((Effect) args[5].raw());
                  effect.setContentInput((Effect) args[6].raw());
              }
              return new EffectValue(effect);
          }
      }
      
      private static class MotionBlurEffect implements Function {
          @Override
          public Value execute(Value[] args) {
              MotionBlur effect = new MotionBlur();
              if (args.length >= 2) {
                  effect.setAngle(args[0].asNumber());
                  effect.setRadius(args[1].asNumber());
              }
              if (args.length >= 3) {
                  effect.setInput((Effect)args[2].raw());
              }
              return new EffectValue(effect);
          }
      }
      
      private static class PerspectiveTransformEffect implements Function {
          @Override
          public Value execute(Value[] args) {
              return new EffectValue(new PerspectiveTransform(
                      args[0].asNumber(), args[1].asNumber(), args[2].asNumber(), args[3].asNumber(),
                      args[4].asNumber(), args[5].asNumber(), args[6].asNumber(), args[7].asNumber() ));
          }
      }
      
      private static class ReflectionEffect implements Function {
          @Override
          public Value execute(Value[] args) {
              return new EffectValue(new Reflection(
                      args[0].asNumber(), args[1].asNumber(), args[2].asNumber(), args[3].asNumber()));
          }
      }
      
      private static class SepiaToneEffect implements Function {
          @Override
          public Value execute(Value[] args) {
              SepiaTone effect = new SepiaTone();
              if (args.length >= 1) {
                  effect.setLevel(args[0].asNumber());
              }
              if (args.length >= 2) {
                  effect.setInput((Effect)args[1].raw());
              }
              return new EffectValue(effect);
          }
      }
      
      private static class ShadowEffect implements Function {
          @Override
          public Value execute(Value[] args) {
              Shadow effect = switch (args.length) {
                  case 2 -> new Shadow(args[0].asNumber(), (Color) args[1].raw());
                  case 3 -> new Shadow(BlurType.values()[args[0].asInt()], (Color) args[1].raw(),
                          args[2].asNumber());
                  default -> new Shadow();
              };
              return new EffectValue(effect);
          }
      }
  //</editor-fold>
      
      private static class ImageFXValue extends MapValue {
  
          private final Image image;
  
          public ImageFXValue(Image image) {
              super(8);
              this.image = image;
              init();
          }
  
          private void init() {
              set("width", NumberValue.of(image.getWidth()));
              set("height", NumberValue.of(image.getHeight()));
              set("preserveRatio", NumberValue.fromBoolean(image.isPreserveRatio()));
              set("smooth", NumberValue.fromBoolean(image.isSmooth()));
              set("getPixels", this::getPixels);
          }
  
          private Value getPixels(Value[] args) {
              final int w = (int) image.getWidth();
              final int h = (int) image.getHeight();
              final int size = w * h;
              final PixelReader pr = image.getPixelReader();
              final WritablePixelFormat<IntBuffer> format = WritablePixelFormat.getIntArgbInstance();
              final int[] buffer = new int[size];
              pr.getPixels(0, 0, w, h, format, buffer, 0, w);
  
              final ArrayValue result = new ArrayValue(size);
              for (int i = 0; i < size; i++) {
                  result.set(i, NumberValue.of(buffer[i]));
              }
              return result;
          }
  
          @Override
          public String toString() {
              return "JavaFX Image " + image;
          }
      }
  
      private static class createImage implements Function {
  
          @Override
          public Value execute(Value[] args) {
              Arguments.checkAtLeast(1, args.length);
              final Image result = switch (args.length) {
                  // createImage(url)
                  case 1 -> new Image(args[0].asString());
                  // createImage(width, height)
                  default -> new WritableImage(args[0].asInt(), args[1].asInt());
                  // createImage(w, h, pixels)
                  case 3 -> {
                      final int w = args[0].asInt();
                      final int h = args[1].asInt();
                      final int size = w * h;
                      final WritableImage writableImage = new WritableImage(w, h);
                      final PixelWriter pw = writableImage.getPixelWriter();
                      final WritablePixelFormat<IntBuffer> format = WritablePixelFormat.getIntArgbInstance();
                      final int[] buffer = new int[size];
                      final ArrayValue array = (ArrayValue) args[2];
                      for (int i = 0; i < size; i++) {
                          buffer[i] = array.get(i).asInt();
                      }
                      pw.setPixels(0, 0, w, h, format, buffer, 0, w);
                      yield writableImage;
                  }
              };
              return new ImageFXValue(result);
          }
      }
  
      public static class GraphicsFXValue extends MapValue {
  
          private final GraphicsContext graphics;
  
          public GraphicsFXValue(GraphicsContext graphics) {
              super(64);
              this.graphics = graphics;
              init();
          }
  
          private void init() {
              set("applyEffect", this::applyEffect);
              set("appendSVGPath", this::appendSVGPath);
              set("arc", this::arc);
              set("arcTo", this::arcTo);
              set("beginPath", voidToVoid(graphics::beginPath));
              set("bezierCurveTo", this::bezierCurveTo);
              set("clearRect", double4ToVoid(graphics::clearRect));
              set("clip", voidToVoid(graphics::clip));
              set("closePath", voidToVoid(graphics::closePath));
              set("drawImage", this::drawImage);
              set("fill", voidToVoid(graphics::fill));
              set("fillArc", this::fillArc);
              set("fillOval", double4ToVoid(graphics::fillOval));
              set("fillPolygon", this::fillPolygon);
              set("fillRect", double4ToVoid(graphics::fillRect));
              set("fillRoundRect", this::fillRoundRect);
              set("fillText", this::fillText);
              set("getFill", this::getFill);
              set("getFillRule", enumOrdinal(graphics::getFillRule));
              set("getGlobalAlpha", voidToDouble(graphics::getGlobalAlpha));
              set("getGlobalBlendMode", enumOrdinal(graphics::getGlobalBlendMode));
              set("getLineCap", enumOrdinal(graphics::getLineCap));
              set("getLineDashOffset", voidToDouble(graphics::getLineDashOffset));
              set("getLineJoin", enumOrdinal(graphics::getLineJoin));
              set("getLineWidth", voidToDouble(graphics::getLineWidth));
              set("getMiterLimit", voidToDouble(graphics::getMiterLimit));
              set("getStroke", this::getStroke);
              set("getTextAlign", enumOrdinal(graphics::getTextAlign));
              set("getTextBaseline", enumOrdinal(graphics::getTextBaseline));
              set("isPointInPath", this::isPointInPath);
              set("lineTo", double2ToVoid(graphics::lineTo));
              set("moveTo", double2ToVoid(graphics::moveTo));
              set("quadraticCurveTo", double4ToVoid(graphics::quadraticCurveTo));
              set("rect", double4ToVoid(graphics::rect));
              set("restore", voidToVoid(graphics::restore));
              set("rotate", doubleToVoid(graphics::rotate));
              set("save", voidToVoid(graphics::save));
              set("scale", double2ToVoid(graphics::scale));
              set("setEffect", this::setEffect);
              set("setFill", this::setFill);
              set("setFillRule", this::setFillRule);
              set("setGlobalAlpha", doubleToVoid(graphics::setGlobalAlpha));
              set("setGlobalBlendMode", this::setGlobalBlendMode);
              set("setLineCap", this::setLineCap);
              set("setLineDashOffset", doubleToVoid(graphics::setLineDashOffset));
              set("setLineJoin", this::setLineJoin);
              set("setLineWidth", doubleToVoid(graphics::setLineWidth));
              set("setMiterLimit", doubleToVoid(graphics::setMiterLimit));
              set("setStroke", this::setStroke);
              set("setTextAlign", this::setTextAlign);
              set("setTextBaseline", this::setTextBaseline);
              set("stroke", voidToVoid(graphics::stroke));
              set("strokeArc", this::strokeArc);
              set("strokeLine", double4ToVoid(graphics::strokeLine));
              set("strokeOval", double4ToVoid(graphics::strokeOval));
              set("strokePolygon", this::strokePolygon);
              set("strokePolyline", this::strokePolyline);
              set("strokeRect", double4ToVoid(graphics::strokeRect));
              set("strokeRoundRect", this::strokeRoundRect);
              set("strokeText", this::strokeText);
              set("transform", this::transform);
              set("translate", double2ToVoid(graphics::translate));
          }
  
          private Value applyEffect(Value[] args) {
              if (args[0].type() != FX_EFFECT_TYPE) {
                  throw new TypeException("Effect expected, found " + Types.typeToString(args[0].type()));
              }
              graphics.applyEffect((Effect) args[0].raw());
              return NumberValue.ZERO;
          }
  
          private Value arc(Value[] args) {
              graphics.arc(args[0].asNumber(), args[1].asNumber(),
                      args[2].asNumber(), args[3].asNumber(),
                      args[4].asNumber(), args[5].asNumber());
              return NumberValue.ZERO;
          }
  
          private Value appendSVGPath(Value[] args) {
              graphics.appendSVGPath(args[0].asString());
              return NumberValue.ZERO;
          }
  
          private Value arcTo(Value[] args) {
              graphics.arcTo(args[0].asNumber(), args[1].asNumber(),
                      args[2].asNumber(), args[3].asNumber(),
                      args[4].asNumber());
              return NumberValue.ZERO;
          }
  
          private Value bezierCurveTo(Value[] args) {
              graphics.bezierCurveTo(args[0].asNumber(), args[1].asNumber(),
                      args[2].asNumber(), args[3].asNumber(),
                      args[4].asNumber(), args[5].asNumber());
              return NumberValue.ZERO;
          }
  
          private Value drawImage(Value[] args) {
              Arguments.checkAtLeast(3, args.length);
              if (!(args[0] instanceof ImageFXValue)) {
                  throw new TypeException("ImageFX expected");
              }
              final Image image = ((ImageFXValue) args[0]).image;
  
              if (args.length >= 9) {
                  graphics.drawImage(image,
                          args[1].asNumber(), args[2].asNumber(),
                          args[3].asNumber(), args[4].asNumber(),
                          args[5].asNumber(), args[6].asNumber(),
                          args[7].asNumber(), args[8].asNumber()
                          );
                  return NumberValue.ZERO;
              }
  
              if (args.length >= 5) {
                  // x y w h
                  graphics.drawImage(image,
                          args[1].asNumber(), args[2].asNumber(),
                          args[3].asNumber(), args[4].asNumber()
                          );
                  return NumberValue.ZERO;
              }
              
              graphics.drawImage(image, args[1].asNumber(), args[2].asNumber());
              return NumberValue.ZERO;
          }
  
          private Value fillArc(Value[] args) {
              graphics.fillArc(args[0].asNumber(), args[1].asNumber(),
                      args[2].asNumber(), args[3].asNumber(),
                      args[4].asNumber(), args[5].asNumber(),
                      ArcType.values()[args[6].asInt()]);
              return NumberValue.ZERO;
          }
  
          private Value fillPolygon(Value[] args) {
              final ArrayValue xarr = (ArrayValue) args[0];
              final ArrayValue yarr = (ArrayValue) args[1];
  
              final int size = xarr.size();
              final double[] xPoints = new double[size];
              final double[] yPoints = new double[size];
              for (int i = 0; i < size; i++) {
                  xPoints[i] = xarr.get(i).asNumber();
                  yPoints[i] = yarr.get(i).asNumber();
              }
  
              graphics.fillPolygon(xPoints, yPoints, args[2].asInt());
              return NumberValue.ZERO;
          }
  
          private Value fillRoundRect(Value[] args) {
              graphics.fillRoundRect(args[0].asNumber(), args[1].asNumber(),
                      args[2].asNumber(), args[3].asNumber(),
                      args[4].asNumber(), args[5].asNumber() );
              return NumberValue.ZERO;
          }
  
          private Value fillText(Value[] args) {
              if (args.length < 4) {
                  // str x y
                  graphics.fillText(args[0].asString(), args[1].asNumber(),
                          args[2].asNumber());
              } else {
                  graphics.fillText(args[0].asString(), args[1].asNumber(),
                          args[2].asNumber(), args[3].asNumber());
              }
              return NumberValue.ZERO;
          }
  
          private Value getFill(Value[] args) {
              return new ColorValue((Color)graphics.getFill());
          }
  
          private Value getStroke(Value[] args) {
              return new ColorValue((Color)graphics.getStroke());
          }
  
          private Value isPointInPath(Value[] args) {
              return NumberValue.fromBoolean(graphics.isPointInPath(args[0].asNumber(), args[1].asNumber()));
          }
  
          private Value setEffect(Value[] args) {
              if (args[0].type() != FX_EFFECT_TYPE) {
                  throw new TypeException("Effect expected, found " + Types.typeToString(args[0].type()));
              }
              graphics.setEffect((Effect) args[0].raw());
              return NumberValue.ZERO;
          }
  
          private Value setFill(Value[] args) {
              graphics.setFill((Color) args[0].raw());
              return NumberValue.ZERO;
          }
  
          private Value setFillRule(Value[] args) {
              graphics.setFillRule(FillRule.values()[args[0].asInt()]);
              return NumberValue.ZERO;
          }
  
          private Value setGlobalBlendMode(Value[] args) {
              graphics.setGlobalBlendMode(BlendMode.values()[args[0].asInt()]);
              return NumberValue.ZERO;
          }
  
          private Value setLineCap(Value[] args) {
              graphics.setLineCap(StrokeLineCap.values()[args[0].asInt()]);
              return NumberValue.ZERO;
          }
  
          private Value setLineJoin(Value[] args) {
              graphics.setLineJoin(StrokeLineJoin.values()[args[0].asInt()]);
              return NumberValue.ZERO;
          }
  
          private Value setStroke(Value[] args) {
              graphics.setStroke((Color) args[0].raw());
              return NumberValue.ZERO;
          }
  
          private Value setTextAlign(Value[] args) {
              graphics.setTextAlign(TextAlignment.values()[args[0].asInt()]);
              return NumberValue.ZERO;
          }
  
          private Value setTextBaseline(Value[] args) {
              graphics.setTextBaseline(VPos.values()[args[0].asInt()]);
              return NumberValue.ZERO;
          }
  
          private Value strokeArc(Value[] args) {
              graphics.strokeArc(args[0].asNumber(), args[1].asNumber(),
                      args[2].asNumber(), args[3].asNumber(),
                      args[4].asNumber(), args[5].asNumber(),
                      ArcType.values()[args[6].asInt()]);
              return NumberValue.ZERO;
          }
  
          private Value strokePolygon(Value[] args) {
              final ArrayValue xarr = (ArrayValue) args[0];
              final ArrayValue yarr = (ArrayValue) args[1];
  
              final int size = xarr.size();
              final double[] xPoints = new double[size];
              final double[] yPoints = new double[size];
              for (int i = 0; i < size; i++) {
                  xPoints[i] = xarr.get(i).asNumber();
                  yPoints[i] = yarr.get(i).asNumber();
              }
  
              graphics.strokePolygon(xPoints, yPoints, args[2].asInt());
              return NumberValue.ZERO;
          }
  
          private Value strokePolyline(Value[] args) {
              final ArrayValue xarr = (ArrayValue) args[0];
              final ArrayValue yarr = (ArrayValue) args[1];
  
              final int size = xarr.size();
              final double[] xPoints = new double[size];
              final double[] yPoints = new double[size];
              for (int i = 0; i < size; i++) {
                  xPoints[i] = xarr.get(i).asNumber();
                  yPoints[i] = yarr.get(i).asNumber();
              }
  
              graphics.strokePolyline(xPoints, yPoints, args[2].asInt());
              return NumberValue.ZERO;
          }
  
          private Value strokeRoundRect(Value[] args) {
              graphics.strokeRoundRect(args[0].asNumber(), args[1].asNumber(),
                      args[2].asNumber(), args[3].asNumber(),
                      args[4].asNumber(), args[5].asNumber() );
              return NumberValue.ZERO;
          }
  
          private Value strokeText(Value[] args) {
              if (args.length < 4) {
                  // str x y
                  graphics.strokeText(args[0].asString(), args[1].asNumber(),
                          args[2].asNumber());
              } else {
                  graphics.strokeText(args[0].asString(), args[1].asNumber(),
                          args[2].asNumber(), args[3].asNumber());
              }
              return NumberValue.ZERO;
          }
  
          private Value transform(Value[] args) {
              graphics.transform(args[0].asNumber(), args[1].asNumber(),
                      args[2].asNumber(), args[3].asNumber(),
                      args[4].asNumber(), args[5].asNumber());
              return NumberValue.ZERO;
          }
  
          @Override
          public String toString() {
              return "GraphicsFXValue " + asString();
          }
      }
      
      private static class CreateWindow implements Function {
  
          @Override
          public Value execute(Value[] args) {
              String title = "";
              int width = 640;
              int height = 480;
              switch (args.length) {
                  case 1:
                      title = args[0].asString();
                      break;
                  case 2:
                      width = args[0].asInt();
                      height = args[1].asInt();
                      break;
                  case 3:
                      title = args[0].asString();
                      width = args[1].asInt();
                      height = args[2].asInt();
                      break;
              }
              panel = new JFXPanel();
              panel.setPreferredSize(new Dimension(width, height));
              panel.setFocusable(true);
              canvas = new Canvas(width, height);
              canvas.setFocusTraversable(true);
              canvas.requestFocus();
              GraphicsContext graphics = canvas.getGraphicsContext2D();
  
              JFrame frame = new JFrame(title);
              frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
              frame.add(panel);
              frame.pack();
              frame.setVisible(true);
              
              Platform.runLater(() -> {
                  Group root = new Group();
                  Scene scene = new Scene(root, Color.WHITE);
                  root.getChildren().add(canvas);
                  panel.setScene(scene);
              });
              return new GraphicsFXValue(graphics);
          }
      }
      
      private static class Repaint implements Function {
  
          @Override
          public Value execute(Value[] args) {
              panel.invalidate();
              panel.repaint();
              return NumberValue.ZERO;
          }
      }
      
      private static class addEventFilter implements Function {
          @Override
          public Value execute(Value[] args) {
              final Function handler = ((FunctionValue) args[1]).getValue();
              final Events event = Events.values()[args[0].asInt()];
              canvas.addEventFilter(event.getHandler(), e -> handleEvent(e, handler));
              return NumberValue.ZERO;
          }
      }
      
      private static class addEventHandler implements Function {
          @Override
          public Value execute(Value[] args) {
              final Function handler = ((FunctionValue) args[1]).getValue();
              final Events event = Events.values()[args[0].asInt()];
              canvas.addEventHandler(event.getHandler(), e -> handleEvent(e, handler));
              return NumberValue.ZERO;
          }
      }
      
      private static void handleEvent(Event event, final Function handler) {
          if (event instanceof MouseEvent me) {
              handleMouseEvent(me, handler);
          } else if (event instanceof KeyEvent ke) {
              handleKeyEvent(ke, handler);
          } else if (event instanceof DragEvent de) {
              handleDragEvent(de, handler);
          }
      }
      
      private static void handleMouseEvent(MouseEvent e, final Function handler) {
          final MapValue map = new MapValue(25);
          map.set("button", NumberValue.of(e.getButton().ordinal()));
          map.set("clickCount", NumberValue.of(e.getClickCount()));
          map.set("sceneX", NumberValue.of(e.getSceneX()));
          map.set("sceneY", NumberValue.of(e.getSceneY()));
          map.set("screenX", NumberValue.of(e.getScreenX()));
          map.set("screenY", NumberValue.of(e.getScreenY()));
          map.set("x", NumberValue.of(e.getX()));
          map.set("y", NumberValue.of(e.getY()));
          map.set("z", NumberValue.of(e.getZ()));
          map.set("isAltDown", NumberValue.fromBoolean(e.isAltDown()));
          map.set("isConsumed", NumberValue.fromBoolean(e.isConsumed()));
          map.set("isControlDown", NumberValue.fromBoolean(e.isControlDown()));
          map.set("isDragDetect", NumberValue.fromBoolean(e.isDragDetect()));
          map.set("isMetaDown", NumberValue.fromBoolean(e.isMetaDown()));
          map.set("isMiddleButtonDown", NumberValue.fromBoolean(e.isMiddleButtonDown()));
          map.set("isPopupTrigger", NumberValue.fromBoolean(e.isPopupTrigger()));
          map.set("isPrimaryButtonDown", NumberValue.fromBoolean(e.isPrimaryButtonDown()));
          map.set("isSecondaryButtonDown", NumberValue.fromBoolean(e.isSecondaryButtonDown()));
          map.set("isShiftDown", NumberValue.fromBoolean(e.isShiftDown()));
          map.set("isShortcutDown", NumberValue.fromBoolean(e.isShortcutDown()));
          map.set("isStillSincePress", NumberValue.fromBoolean(e.isStillSincePress()));
          map.set("isSynthesized", NumberValue.fromBoolean(e.isSynthesized()));
          handler.execute(map);
      }
      
      private static void handleKeyEvent(final KeyEvent e, final Function handler) {
          final MapValue map = new MapValue(10);
          map.set("code", NumberValue.of(e.getCode().ordinal()));
          map.set("character", new StringValue(e.getCharacter()));
          map.set("text", new StringValue(e.getText()));
          map.set("isAltDown", NumberValue.fromBoolean(e.isAltDown()));
          map.set("isConsumed", NumberValue.fromBoolean(e.isConsumed()));
          map.set("isControlDown", NumberValue.fromBoolean(e.isControlDown()));
          map.set("isMetaDown", NumberValue.fromBoolean(e.isMetaDown()));
          map.set("isShiftDown", NumberValue.fromBoolean(e.isShiftDown()));
          map.set("isShortcutDown", NumberValue.fromBoolean(e.isShortcutDown()));
          handler.execute(map);
      }
      
      private static void handleDragEvent(final DragEvent e, final Function handler) {
          final MapValue map = new MapValue(10);
          map.set("sceneX", NumberValue.of(e.getSceneX()));
          map.set("sceneY", NumberValue.of(e.getSceneY()));
          map.set("screenX", NumberValue.of(e.getScreenX()));
          map.set("screenY", NumberValue.of(e.getScreenY()));
          map.set("x", NumberValue.of(e.getX()));
          map.set("y", NumberValue.of(e.getY()));
          map.set("z", NumberValue.of(e.getZ()));
          map.set("isAccepted", NumberValue.fromBoolean(e.isAccepted()));
          map.set("isConsumed", NumberValue.fromBoolean(e.isConsumed()));
          map.set("isDropCompleted", NumberValue.fromBoolean(e.isDropCompleted()));
          handler.execute(map);
      }
      
  }
  