package com.tyron.vectorparser.util;

import android.graphics.Path;
import android.graphics.RectF;
import android.util.Log;

public class PathParser {

  private static final String TAG = "PATH_PARSER";

  /*
   * This is where the hard-to-parse paths are handled.
   * Uppercase rules are absolute positions, lowercase are relative.
   * Types of path rules:
   * <p/>
   * <ol>
   * <li>M/m - (x y)+ - Move to (without drawing)
   * <li>Z/z - (no params) - Close path (back to starting point)
   * <li>L/l - (x y)+ - Line to
   * <li>H/h - x+ - Horizontal ine to
   * <li>V/v - y+ - Vertical line to
   * <li>C/c - (x1 y1 x2 y2 x y)+ - Cubic bezier to
   * <li>S/s - (x2 y2 x y)+ - Smooth cubic bezier to (shorthand that assumes the x2, y2 from previous C/S is the x1, y1 of this bezier)
   * <li>Q/q - (x1 y1 x y)+ - Quadratic bezier to
   * <li>T/t - (x y)+ - Smooth quadratic bezier to (assumes previous control point is "reflection" of last one w.r.t. to current point)
   * </ol>
   * <p/>
   * Numbers are separate by whitespace, comma or nothing at all (!) if they are self-delimiting, (ie. begin with a - sign)
   */
  public static Path doPath(String s) {
    int n = s.length();
    ParserHelper ph = new ParserHelper(s);
    ph.skipWhitespace();
    Path p = new Path();
    float lastX = 0;
    float lastY = 0;
    float lastX1 = 0;
    float lastY1 = 0;
    float contourInitialX = 0;
    float contourInitialY = 0;
    RectF r = new RectF();
    char prevCmd = 'm';
    char cmd = 'x';
    while (ph.pos < n) {
      char next = s.charAt(ph.pos);
      if (!Character.isDigit(next) && !(next == '.') && !(next == '-')) {
        cmd = next;
        ph.advance();
      } else if (cmd == 'M') { // implied command
        cmd = 'L';
      } else if (cmd == 'm') { // implied command
        cmd = 'l';
      } else { // implied command
        // ignore
      }
      p.computeBounds(r, true);
      // Log.d(TAG, "  " + cmd + " " + r);
      // Util.debug("* Commands remaining: '" + path + "'.");
      boolean wasCurve = false;
      switch (cmd) {
        case 'M':
        case 'm':
          {
            float x = ph.nextFloat();
            float y = ph.nextFloat();
            if (cmd == 'm') {
              p.rMoveTo(x, y);
              lastX += x;
              lastY += y;
            } else {
              p.moveTo(x, y);
              lastX = x;
              lastY = y;
            }
            contourInitialX = lastX;
            contourInitialY = lastY;
            break;
          }
        case 'Z':
        case 'z':
          {
            /// p.lineTo(contourInitialX, contourInitialY);
            p.close();
            lastX = contourInitialX;
            lastY = contourInitialY;
            break;
          }
        case 'L':
        case 'l':
          {
            float x = ph.nextFloat();
            float y = ph.nextFloat();
            if (cmd == 'l') {
              if ((prevCmd == 'M' || prevCmd == 'm') && x == 0 && y == 0) {
                p.addCircle(x, y, 1f, Path.Direction.CW);
              } else {
                p.rLineTo(x, y);
                lastX += x;
                lastY += y;
              }
            } else {
              if ((prevCmd == 'M' || prevCmd == 'm') && x == lastX && y == lastY) {
                p.addCircle(x, y, 1f, Path.Direction.CW);
              } else {
                p.lineTo(x, y);
                lastX = x;
                lastY = y;
              }
            }
            break;
          }
        case 'H':
        case 'h':
          {
            float x = ph.nextFloat();
            if (cmd == 'h') {
              p.rLineTo(x, 0);
              lastX += x;
            } else {
              p.lineTo(x, lastY);
              lastX = x;
            }
            break;
          }
        case 'V':
        case 'v':
          {
            float y = ph.nextFloat();
            if (cmd == 'v') {
              p.rLineTo(0, y);
              lastY += y;
            } else {
              p.lineTo(lastX, y);
              lastY = y;
            }
            break;
          }
        case 'C':
        case 'c':
          {
            wasCurve = true;
            float x1 = ph.nextFloat();
            float y1 = ph.nextFloat();
            float x2 = ph.nextFloat();
            float y2 = ph.nextFloat();
            float x = ph.nextFloat();
            float y = ph.nextFloat();
            if (cmd == 'c') {
              x1 += lastX;
              x2 += lastX;
              x += lastX;
              y1 += lastY;
              y2 += lastY;
              y += lastY;
            }
            p.cubicTo(x1, y1, x2, y2, x, y);
            lastX1 = x2;
            lastY1 = y2;
            lastX = x;
            lastY = y;
            break;
          }
        case 'S':
        case 's':
          {
            wasCurve = true;
            float x2 = ph.nextFloat();
            float y2 = ph.nextFloat();
            float x = ph.nextFloat();
            float y = ph.nextFloat();
            if (cmd == 's') {
              x2 += lastX;
              x += lastX;
              y2 += lastY;
              y += lastY;
            }
            float x1 = 2 * lastX - lastX1;
            float y1 = 2 * lastY - lastY1;
            p.cubicTo(x1, y1, x2, y2, x, y);
            lastX1 = x2;
            lastY1 = y2;
            lastX = x;
            lastY = y;
            break;
          }
        case 'A':
        case 'a':
          {
            float rx = ph.nextFloat();
            float ry = ph.nextFloat();
            float theta = ph.nextFloat();
            int largeArc = (int) ph.nextFloat();
            int sweepArc = (int) ph.nextFloat();
            float x = ph.nextFloat();
            float y = ph.nextFloat();
            if (cmd == 'a') {
              x += lastX;
              y += lastY;
            }
            drawArc(p, lastX, lastY, x, y, rx, ry, theta, largeArc == 1, sweepArc == 1);
            lastX = x;
            lastY = y;
            break;
          }
        case 'T':
        case 't':
          {
            wasCurve = true;
            float x = ph.nextFloat();
            float y = ph.nextFloat();
            if (cmd == 't') {
              x += lastX;
              y += lastY;
            }
            float x1 = 2 * lastX - lastX1;
            float y1 = 2 * lastY - lastY1;
            p.cubicTo(lastX, lastY, x1, y1, x, y);
            lastX = x;
            lastY = y;
            lastX1 = x1;
            lastY1 = y1;
            break;
          }
        case 'Q':
        case 'q':
          {
            wasCurve = true;
            float x1 = ph.nextFloat();
            float y1 = ph.nextFloat();
            float x = ph.nextFloat();
            float y = ph.nextFloat();
            if (cmd == 'q') {
              x += lastX;
              y += lastY;
              x1 += lastX;
              y1 += lastY;
            }
            p.cubicTo(lastX, lastY, x1, y1, x, y);
            lastX1 = x1;
            lastY1 = y1;
            lastX = x;
            lastY = y;
            break;
          }
        default:
          Log.w(TAG, "Invalid path command: " + cmd);
          ph.advance();
      }
      prevCmd = cmd;
      if (!wasCurve) {
        lastX1 = lastX;
        lastY1 = lastY;
      }
      ph.skipWhitespace();
    }
    return p;
  }

  /*
   * Elliptical arc implementation based on the SVG specification notes
   * Adapted from the Batik library (Apache-2 license) by SAU
   */
  private static void drawArc(
      Path path,
      double x0,
      double y0,
      double x,
      double y,
      double rx,
      double ry,
      double angle,
      boolean largeArcFlag,
      boolean sweepFlag) {
    double dx2 = (x0 - x) / 2.0;
    double dy2 = (y0 - y) / 2.0;
    angle = Math.toRadians(angle % 360.0);
    double cosAngle = Math.cos(angle);
    double sinAngle = Math.sin(angle);

    double x1 = (cosAngle * dx2 + sinAngle * dy2);
    double y1 = (-sinAngle * dx2 + cosAngle * dy2);
    rx = Math.abs(rx);
    ry = Math.abs(ry);

    double Prx = rx * rx;
    double Pry = ry * ry;
    double Px1 = x1 * x1;
    double Py1 = y1 * y1;

    // check that radii are large enough
    double radiiCheck = Px1 / Prx + Py1 / Pry;
    if (radiiCheck > 1) {
      rx = Math.sqrt(radiiCheck) * rx;
      ry = Math.sqrt(radiiCheck) * ry;
      Prx = rx * rx;
      Pry = ry * ry;
    }

    // Step 2 : Compute (cx1, cy1)
    double sign = (largeArcFlag == sweepFlag) ? -1 : 1;
    double sq = ((Prx * Pry) - (Prx * Py1) - (Pry * Px1)) / ((Prx * Py1) + (Pry * Px1));
    sq = (sq < 0) ? 0 : sq;
    double coef = (sign * Math.sqrt(sq));
    double cx1 = coef * ((rx * y1) / ry);
    double cy1 = coef * -((ry * x1) / rx);

    double sx2 = (x0 + x) / 2.0;
    double sy2 = (y0 + y) / 2.0;
    double cx = sx2 + (cosAngle * cx1 - sinAngle * cy1);
    double cy = sy2 + (sinAngle * cx1 + cosAngle * cy1);

    // Step 4 : Compute the angleStart (angle1) and the angleExtent (dangle)
    double ux = (x1 - cx1) / rx;
    double uy = (y1 - cy1) / ry;
    double vx = (-x1 - cx1) / rx;
    double vy = (-y1 - cy1) / ry;
    double p, n;

    // Compute the angle start
    n = Math.sqrt((ux * ux) + (uy * uy));
    p = ux; // (1 * ux) + (0 * uy)
    sign = (uy < 0) ? -1.0 : 1.0;
    double angleStart = Math.toDegrees(sign * Math.acos(p / n));

    // Compute the angle extent
    n = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
    p = ux * vx + uy * vy;
    sign = (ux * vy - uy * vx < 0) ? -1.0 : 1.0;
    double angleExtent = Math.toDegrees(sign * Math.acos(p / n));
    if (!sweepFlag && angleExtent > 0) {
      angleExtent -= 360f;
    } else if (sweepFlag && angleExtent < 0) {
      angleExtent += 360f;
    }
    angleExtent %= 360f;
    angleStart %= 360f;

    RectF oval =
        new RectF((float) (cx - rx), (float) (cy - ry), (float) (cx + rx), (float) (cy + ry));
    path.addArc(oval, (float) angleStart, (float) angleExtent);
  }
}
