/*
 * Copyright (c) 2013 by Gerrit Grunwald
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package eu.hansolo.enzo.common;

import javafx.geometry.Bounds;
import javafx.scene.shape.Arc;
import javafx.scene.shape.ArcTo;
import javafx.scene.shape.ArcType;
import javafx.scene.shape.Circle;
import javafx.scene.shape.ClosePath;
import javafx.scene.shape.CubicCurve;
import javafx.scene.shape.CubicCurveTo;
import javafx.scene.shape.Ellipse;
import javafx.scene.shape.FillRule;
import javafx.scene.shape.HLineTo;
import javafx.scene.shape.Line;
import javafx.scene.shape.LineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.scene.shape.Polygon;
import javafx.scene.shape.Polyline;
import javafx.scene.shape.QuadCurve;
import javafx.scene.shape.QuadCurveTo;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.SVGPath;
import javafx.scene.shape.Shape;
import javafx.scene.shape.VLineTo;
import javafx.scene.text.Text;

import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;


/**
 * Created by
 * User: hansolo
 * Date: 02.04.13
 * Time: 12:49
 */
public class ShapeConverter {
    private static final double KAPPA = 0.5522847498307935;

    public static String shapeToSvgString(final Shape SHAPE) {
        final StringBuilder fxPath = new StringBuilder();
        if (Line.class.equals(SHAPE.getClass())) {
            fxPath.append(convertLine((Line) SHAPE));
        } else if (Arc.class.equals(SHAPE.getClass())) {
            fxPath.append(convertArc((Arc) SHAPE));
        } else if (QuadCurve.class.equals(SHAPE.getClass())) {
            fxPath.append(convertQuadCurve((QuadCurve) SHAPE));
        } else if (CubicCurve.class.equals(SHAPE.getClass())) {
            fxPath.append(convertCubicCurve((CubicCurve) SHAPE));
        } else if (Rectangle.class.equals(SHAPE.getClass())) {
            fxPath.append(convertRectangle((Rectangle) SHAPE));
        } else if (Circle.class.equals(SHAPE.getClass())) {
            fxPath.append(convertCircle((Circle) SHAPE));
        } else if (Ellipse.class.equals(SHAPE.getClass())) {
            fxPath.append(convertEllipse((Ellipse) SHAPE));
        } else if (Text.class.equals(SHAPE.getClass())) {
            Path path = (Path)(Shape.subtract(SHAPE, new Rectangle(0, 0)));
            fxPath.append(convertPath(path));
        } else if (Path.class.equals(SHAPE.getClass())) {
            fxPath.append(convertPath((Path) SHAPE));
        } else if (Polygon.class.equals(SHAPE.getClass())) {
            fxPath.append(convertPolygon((Polygon) SHAPE));
        } else if (Polyline.class.equals(SHAPE.getClass())) {
            fxPath.append(convertPolyline((Polyline) SHAPE));
        } else if (SVGPath.class.equals(SHAPE.getClass())) {
            fxPath.append(((SVGPath) SHAPE).getContent());
        }
        return fxPath.toString();
    }

    public static SVGPath shapeToSvgPath(final Shape SHAPE) {
        SVGPath svgPath = new SVGPath();
        svgPath.setContent(shapeToSvgString(SHAPE));
        return svgPath;
    }

    public static Path svgPathToPath(final SVGPath SVG_PATH) {
        String data = SVG_PATH.getContent();
        data = data.replaceAll("/(,)/", "/ /");
        data = data.replaceAll("/([A-Za-z])/", "/ $1 /"); // wrap single characters in blanks
        StringTokenizer tokenizer = new StringTokenizer(data, " ");
        List<String> pathList = new ArrayList<>();
        while(tokenizer.hasMoreElements()) {
            pathList.add(tokenizer.nextElement().toString());
        }
        PathReader pathReader = new PathReader(pathList);
        return processPath(pathList, pathReader);
    }

    public static String convertLine(final Line LINE) {
        final StringBuilder fxPath = new StringBuilder();
        fxPath.append("M ").append(LINE.getStartX()).append(" ").append(LINE.getStartY()).append(" ")
              .append("L ").append(LINE.getEndX()).append(" ").append(LINE.getEndY());
        return fxPath.toString();
    }

    public static String convertArc(final Arc ARC) {
        StringBuilder fxPath = new StringBuilder();
        double centerX    = ARC.getCenterX();
        double centerY    = ARC.getCenterY();
        double radiusX    = ARC.getRadiusX();
        double radiusY    = ARC.getRadiusY();
        double startAngle = Math.toRadians(-ARC.getStartAngle());
        double endAngle   = Math.toRadians(-ARC.getStartAngle() - ARC.getLength());
        double length     = ARC.getLength();

        double startX = radiusX * Math.cos(startAngle);
        double startY = radiusY * Math.sin(startAngle);

        double endX   = centerX + radiusX * Math.cos(endAngle);
        double endY   = centerY + radiusY * Math.sin(endAngle);

        int xAxisRot  = 0;
        int largeArc  = (length > 180) ? 1 : 0;
        int sweep     = (length < 0) ? 1 : 0;

        fxPath.append("M ").append(centerX).append(" ").append(centerY).append(" ");

        if (ArcType.ROUND == ARC.getType()) {
            fxPath.append("l ").append(startX).append(" ").append(startY).append(" ");
        }

        fxPath.append("A ").append(radiusX).append(" ").append(radiusY).append(" ")
              .append(xAxisRot).append(" ").append(largeArc).append(" ").append(sweep).append(" ")
              .append(endX).append(" ").append(endY).append(" ");
        if (ArcType.CHORD == ARC.getType() || ArcType.ROUND == ARC.getType()) {
            fxPath.append("Z");
        }
        return fxPath.toString();
    }

    public static String convertQuadCurve(final QuadCurve QUAD_CURVE) {
        final StringBuilder fxPath = new StringBuilder();
        fxPath.append("M ").append(QUAD_CURVE.getStartX()).append(" ").append(QUAD_CURVE.getStartY()).append(" ")
              .append("Q ").append(QUAD_CURVE.getControlX()).append(" ").append(QUAD_CURVE.getControlY())
              .append(QUAD_CURVE.getEndX()).append(" ").append(QUAD_CURVE.getEndY());
        return fxPath.toString();
    }

    public static String convertCubicCurve(final CubicCurve CUBIC_CURVE) {
        final StringBuilder fxPath = new StringBuilder();
        fxPath.append("M ").append(CUBIC_CURVE.getStartX()).append(" ").append(CUBIC_CURVE.getStartY()).append(" ")
              .append("C ").append(CUBIC_CURVE.getControlX1()).append(" ").append(CUBIC_CURVE.getControlY1()).append(" ")
              .append(CUBIC_CURVE.getControlX2()).append(" ").append(CUBIC_CURVE.getControlY2()).append(" ")
              .append(CUBIC_CURVE.getEndX()).append(" ").append(CUBIC_CURVE.getEndY());
        return fxPath.toString();
    }

    public static String convertRectangle(final Rectangle RECTANGLE) {
        final StringBuilder fxPath = new StringBuilder();
        final Bounds bounds = RECTANGLE.getBoundsInLocal();
        if (Double.compare(RECTANGLE.getArcWidth(), 0.0) == 0 && Double.compare(RECTANGLE.getArcHeight(), 0.0) == 0) {
            fxPath.append("M ").append(bounds.getMinX()).append(" ").append(bounds.getMinY()).append(" ")
                  .append("H ").append(bounds.getMaxX()).append(" ")
                  .append("V ").append(bounds.getMaxY()).append(" ")
                  .append("H ").append(bounds.getMinX()).append(" ")
                  .append("V ").append(bounds.getMinY()).append(" ")
                  .append("Z");
        } else {
            double x         = bounds.getMinX();
            double y         = bounds.getMinY();
            double width     = bounds.getWidth();
            double height    = bounds.getHeight();
            double arcWidth  = RECTANGLE.getArcWidth();
            double arcHeight = RECTANGLE.getArcHeight();
            double r         = x + width;
            double b         = y + height;
            fxPath.append("M ").append(x + arcWidth).append(" ").append(y).append(" ")
                  .append("L ").append(r - arcWidth).append(" ").append(y).append(" ")
                  .append("Q ").append(r).append(" ").append(y).append(" ").append(r).append(" ").append(y + arcHeight).append(" ")
                  .append("L ").append(r).append(" ").append(y + height - arcHeight).append(" ")
                  .append("Q ").append(r).append(" ").append(b).append(" ").append(r - arcWidth).append(" ").append(b).append(" ")
                  .append("L ").append(x + arcWidth).append(" ").append(b).append(" ")
                  .append("Q ").append(x).append(" ").append(b).append(" ").append(x).append(" ").append(b - arcHeight).append(" ")
                  .append("L ").append(x).append(" ").append(y + arcHeight).append(" ")
                  .append("Q ").append(x).append(" ").append(y).append(" ").append(x + arcWidth).append(" ").append(y).append(" ")
                  .append("Z");
        }
        return fxPath.toString();
    }

    public static String convertCircle(final Circle CIRCLE) {
        final StringBuilder fxPath = new StringBuilder();
        final double CENTER_X         = CIRCLE.getCenterX() == 0 ? CIRCLE.getRadius() : CIRCLE.getCenterX();
        final double CENTER_Y         = CIRCLE.getCenterY() == 0 ? CIRCLE.getRadius() : CIRCLE.getCenterY();
        final double RADIUS           = CIRCLE.getRadius();
        final double CONTROL_DISTANCE = RADIUS * KAPPA;
        // Move to first point
        fxPath.append("M ").append(CENTER_X).append(" ").append(CENTER_Y - RADIUS).append(" ");
        // 1. quadrant
        fxPath.append("C ").append(CENTER_X + CONTROL_DISTANCE).append(" ").append(CENTER_Y - RADIUS).append(" ")
              .append(CENTER_X + RADIUS).append(" ").append(CENTER_Y - CONTROL_DISTANCE).append(" ")
              .append(CENTER_X + RADIUS).append(" ").append(CENTER_Y).append(" ");
        // 2. quadrant
        fxPath.append("C ").append(CENTER_X + RADIUS).append(" ").append(CENTER_Y + CONTROL_DISTANCE).append(" ")
              .append(CENTER_X + CONTROL_DISTANCE).append(" ").append(CENTER_Y + RADIUS).append(" ")
              .append(CENTER_X).append(" ").append(CENTER_Y + RADIUS).append(" ");
        // 3. quadrant
        fxPath.append("C ").append(CENTER_X - CONTROL_DISTANCE).append(" ").append(CENTER_Y + RADIUS).append(" ")
              .append(CENTER_X - RADIUS).append(" ").append(CENTER_Y + CONTROL_DISTANCE).append(" ")
              .append(CENTER_X - RADIUS).append(" ").append(CENTER_Y).append(" ");
        // 4. quadrant
        fxPath.append("C ").append(CENTER_X - RADIUS).append(" ").append(CENTER_Y - CONTROL_DISTANCE).append(" ")
              .append(CENTER_X - CONTROL_DISTANCE).append(" ").append(CENTER_Y - RADIUS).append(" ")
              .append(CENTER_X).append(" ").append(CENTER_Y - RADIUS).append(" ");
        // Close path
        fxPath.append("Z");
        return fxPath.toString();
    }

    public static String convertEllipse(final Ellipse ELLIPSE) {
        final StringBuilder fxPath = new StringBuilder();
        final double CENTER_X           = ELLIPSE.getCenterX() == 0 ? ELLIPSE.getRadiusX() : ELLIPSE.getCenterX();
        final double CENTER_Y           = ELLIPSE.getCenterY() == 0 ? ELLIPSE.getRadiusY() : ELLIPSE.getCenterY();
        final double RADIUS_X           = ELLIPSE.getRadiusX();
        final double RADIUS_Y           = ELLIPSE.getRadiusY();
        final double CONTROL_DISTANCE_X = RADIUS_X * KAPPA;
        final double CONTROL_DISTANCE_Y = RADIUS_Y * KAPPA;
        // Move to first point
        fxPath.append("M ").append(CENTER_X).append(" ").append(CENTER_Y - RADIUS_Y).append(" ");
        // 1. quadrant
        fxPath.append("C ").append(CENTER_X + CONTROL_DISTANCE_X).append(" ").append(CENTER_Y - RADIUS_Y).append(" ")
              .append(CENTER_X + RADIUS_X).append(" ").append(CENTER_Y - CONTROL_DISTANCE_Y).append(" ")
              .append(CENTER_X + RADIUS_X).append(" ").append(CENTER_Y).append(" ");
        // 2. quadrant
        fxPath.append("C ").append(CENTER_X + RADIUS_X).append(" ").append(CENTER_Y + CONTROL_DISTANCE_Y).append(" ")
              .append(CENTER_X + CONTROL_DISTANCE_X).append(" ").append(CENTER_Y + RADIUS_Y).append(" ")
              .append(CENTER_X).append(" ").append(CENTER_Y + RADIUS_Y).append(" ");
        // 3. quadrant
        fxPath.append("C ").append(CENTER_X - CONTROL_DISTANCE_X).append(" ").append(CENTER_Y + RADIUS_Y).append(" ")
              .append(CENTER_X - RADIUS_X).append(" ").append(CENTER_Y + CONTROL_DISTANCE_Y).append(" ")
              .append(CENTER_X - RADIUS_X).append(" ").append(CENTER_Y).append(" ");
        // 4. quadrant
        fxPath.append("C ").append(CENTER_X - RADIUS_X).append(" ").append(CENTER_Y - CONTROL_DISTANCE_Y).append(" ")
              .append(CENTER_X - CONTROL_DISTANCE_X).append(" ").append(CENTER_Y - RADIUS_Y).append(" ")
              .append(CENTER_X).append(" ").append(CENTER_Y - RADIUS_Y).append(" ");
        // Close path
        fxPath.append("Z");
        return fxPath.toString();
    }

    public static String convertPath(final Path PATH) {
        final StringBuilder fxPath = new StringBuilder();
        for (PathElement element : PATH.getElements()) {
            if (MoveTo.class.equals(element.getClass())) {
                fxPath.append("M ")
                      .append(((MoveTo) element).getX()).append(" ")
                      .append(((MoveTo) element).getY()).append(" ");
            } else if (LineTo.class.equals(element.getClass())) {
                fxPath.append("L ")
                      .append(((LineTo) element).getX()).append(" ")
                      .append(((LineTo) element).getY()).append(" ");
            } else if (CubicCurveTo.class.equals(element.getClass())) {
                fxPath.append("C ")
                      .append(((CubicCurveTo) element).getControlX1()).append(" ")
                      .append(((CubicCurveTo) element).getControlY1()).append(" ")
                      .append(((CubicCurveTo) element).getControlX2()).append(" ")
                      .append(((CubicCurveTo) element).getControlY2()).append(" ")
                      .append(((CubicCurveTo) element).getX()).append(" ")
                      .append(((CubicCurveTo) element).getY()).append(" ");
            } else if (QuadCurveTo.class.equals(element.getClass())) {
                fxPath.append("Q ")
                      .append(((QuadCurveTo) element).getControlX()).append(" ")
                      .append(((QuadCurveTo) element).getControlY()).append(" ")
                      .append(((QuadCurveTo) element).getX()).append(" ")
                      .append(((QuadCurveTo) element).getY()).append(" ");
            } else if (ArcTo.class.equals(element.getClass())) {
                fxPath.append("A ")
                      .append(((ArcTo) element).getX()).append(" ")
                      .append(((ArcTo) element).getY()).append(" ")
                      .append(((ArcTo) element).getRadiusX()).append(" ")
                      .append(((ArcTo) element).getRadiusY()).append(" ");
            } else if (HLineTo.class.equals(element.getClass())) {
                fxPath.append("H ")
                      .append(((HLineTo) element).getX()).append(" ");
            } else if (VLineTo.class.equals(element.getClass())) {
                fxPath.append("V ")
                      .append(((VLineTo) element).getY()).append(" ");
            } else if (ClosePath.class.equals(element.getClass())) {
                fxPath.append("Z");
            }
        }
        return fxPath.toString();
    }

    public static String convertPolygon(final Polygon POLYGON) {
        final StringBuilder fxPath = new StringBuilder();
        final int           size   = POLYGON.getPoints().size();
        if (size % 2 == 0) {
            List<Double> coordinates     = POLYGON.getPoints();
            for (int i = 0 ; i < size ; i += 2) {
                fxPath.append(i == 0 ? "M " : "L ")
                      .append(coordinates.get(i)).append(" ").append(coordinates.get(i + 1)).append(" ");
            }
            fxPath.append("Z");
        }
        return fxPath.toString();
    }

    public static String convertPolyline(final Polyline POLYLINE) {
        final StringBuilder fxPath = new StringBuilder();
        final int           size   = POLYLINE.getPoints().size();
        if (size % 2 == 0) {
            List<Double> coordinates     = POLYLINE.getPoints();
            for (int i = 0 ; i < size ; i += 2) {
                fxPath.append(i == 0 ? "M " : "L ")
                      .append(coordinates.get(i)).append(" ").append(coordinates.get(i + 1)).append(" ");
            }
        }
        return fxPath.toString();
    }

    private static Path processPath(final List<String> PATH_LIST, final PathReader READER) {
        final Path PATH = new Path();
        PATH.setFillRule(FillRule.EVEN_ODD);
        while (!PATH_LIST.isEmpty()) {
            if ("M".equals(READER.read())) {
                PATH.getElements().add(new MoveTo(READER.nextX(), READER.nextY()));
            } else if ("L".equals(READER.read())) {
                PATH.getElements().add(new LineTo(READER.nextX(), READER.nextY()));
            } else if ("C".equals(READER.read())) {
                PATH.getElements().add(new CubicCurveTo(READER.nextX(), READER.nextY(), READER.nextX(), READER.nextY(), READER.nextX(), READER.nextY()));
            } else if ("Q".equals(READER.read())) {
                PATH.getElements().add(new QuadCurveTo(READER.nextX(), READER.nextY(), READER.nextX(), READER.nextY()));
            } else if ("H".equals(READER.read())) {
                PATH.getElements().add(new HLineTo(READER.nextX()));
            } else if ("L".equals(READER.read())) {
                PATH.getElements().add(new VLineTo(READER.nextY()));
            } else if ("A".equals(READER.read())) {
                PATH.getElements().add(new ArcTo(READER.nextX(), READER.nextY(), 0, READER.nextX(), READER.nextY(), false, false));
            } else if ("Z".equals(READER.read())) {
                PATH.getElements().add(new ClosePath());
            }
        }
        return PATH;
    }

    private static class PathReader {
        protected List<String> path;

        PathReader(final List<String> PATH) {
            path = PATH;
        }
        String read() {
            return path.remove(0);
        }
        double nextX() {
            return Double.parseDouble(read());
        }
        double nextY() {
            return Double.parseDouble(read());
        }
    }
}
