// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math' as math;
import 'dart:typed_data';

import 'package:ui/ui.dart' as ui;

import 'path_utils.dart';

/// Converts conic curve to a list of quadratic curves for rendering on
/// canvas or conversion to svg.
///
/// See "High order approximation of conic sections by quadratic splines"
/// by Michael Floater, 1993.
/// Skia implementation reference:
/// https://github.com/google/skia/blob/main/src/core/SkGeometry.cpp
class Conic {
  Conic(this.p0x, this.p0y, this.p1x, this.p1y, this.p2x, this.p2y, this.fW);

  double p0x, p0y, p1x, p1y, p2x, p2y;
  final double fW;
  static const int _maxSubdivisionCount = 5;

  /// Returns array of points for the approximation of the conic as quad(s).
  ///
  /// First offset is start Point. Each pair of offsets after are quadratic
  /// control and end points.
  List<ui.Offset> toQuads() {
    final List<ui.Offset> pointList = <ui.Offset>[];
    // This value specifies error bound.
    const double conicTolerance = 1.0 / 4.0;

    // Based on error bound, compute how many times we should subdivide
    final int subdivideCount = _computeSubdivisionCount(conicTolerance);

    // Split conic into quads, writes quad coordinates into [_pointList] and
    // returns number of quads.
    assert(subdivideCount >= 0 && subdivideCount <= _maxSubdivisionCount);
    int quadCount = 1 << subdivideCount;
    bool skipSubdivide = false;
    pointList.add(ui.Offset(p0x, p0y));
    if (subdivideCount == _maxSubdivisionCount) {
      // We have an extreme number of quads, chop this conic and check if
      // it generates a pair of lines, in which case we should not subdivide.
      final _ConicPair dst = _ConicPair();
      _chop(dst);
      final Conic conic0 = dst.first!;
      final Conic conic1 = dst.second!;
      // If this chop generates pair of lines no need to subdivide.
      if (conic0.p1x == conic0.p2x &&
          conic0.p1y == conic0.p2y &&
          conic1.p0x == conic1.p1x &&
          conic1.p0y == conic1.p1y) {
        final ui.Offset controlPointOffset = ui.Offset(conic0.p1x, conic0.p1y);
        pointList.add(controlPointOffset);
        pointList.add(controlPointOffset);
        pointList.add(controlPointOffset);
        pointList.add(ui.Offset(conic1.p2x, conic1.p2y));
        quadCount = 2;
        skipSubdivide = true;
      }
    }
    if (!skipSubdivide) {
      _subdivide(this, subdivideCount, pointList);
    }

    // If there are any non-finite generated points, pin to middle of hull.
    final int pointCount = 2 * quadCount + 1;
    bool hasNonFinitePoints = false;
    for (int p = 0; p < pointCount; ++p) {
      if (pointList[p].dx.isNaN || pointList[p].dy.isNaN) {
        hasNonFinitePoints = true;
        break;
      }
    }
    if (hasNonFinitePoints) {
      for (int p = 1; p < pointCount - 1; ++p) {
        pointList[p] = ui.Offset(p1x, p1y);
      }
    }
    return pointList;
  }

  // Subdivides a conic and writes to points list.
  static void _subdivide(Conic src, int level, List<ui.Offset> pointList) {
    assert(level >= 0);
    if (0 == level) {
      // At lowest subdivision point, copy control point and end point to
      // target.
      pointList.add(ui.Offset(src.p1x, src.p1y));
      pointList.add(ui.Offset(src.p2x, src.p2y));
      return;
    }
    final _ConicPair dst = _ConicPair();
    src._chop(dst);
    final Conic conic0 = dst.first!;
    final Conic conic1 = dst.second!;
    final double startY = src.p0y;
    final double endY = src.p2y;
    final double cpY = src.p1y;
    if (SPath.between(startY, cpY, endY)) {
      // Ensure that chopped conics maintain their y-order.
      final double midY = conic0.p2y;
      if (!SPath.between(startY, midY, endY)) {
        // The computed midpoint is outside end points, move it to
        // closer one.
        final double closerY =
            (midY - startY).abs() < (midY - endY).abs() ? startY : endY;
        conic0.p2y = conic1.p0y = closerY;
      }
      if (!SPath.between(startY, conic0.p1y, conic0.p2y)) {
        // First control point not between start and end points, move it
        // to start.
        conic0.p1y = startY;
      }
      if (!SPath.between(conic1.p0y, conic1.p1y, endY)) {
        // Second control point not between start and end points, move it
        // to end.
        conic1.p1y = endY;
      }
      // Verify that conics points are ordered.
      assert(SPath.between(startY, conic0.p1y, conic0.p2y));
      assert(SPath.between(conic0.p1y, conic0.p2y, conic1.p1y));
      assert(SPath.between(conic0.p2y, conic1.p1y, endY));
    }
    --level;
    _subdivide(conic0, level, pointList);
    _subdivide(conic1, level, pointList);
  }

  static double _subdivideWeightValue(double w) {
    return math.sqrt(0.5 + w * 0.5);
  }

  // Splits conic into 2 parts based on weight.
  void _chop(_ConicPair pair) {
    final double scale = 1.0 / (1.0 + fW);
    final double newW = _subdivideWeightValue(fW);
    final ui.Offset wp1 = ui.Offset(fW * p1x, fW * p1y);
    ui.Offset m = ui.Offset((p0x + (2 * wp1.dx) + p2x) * scale * 0.5,
        (p0y + 2 * wp1.dy + p2y) * scale * 0.5);
    if (m.dx.isNaN || m.dy.isNaN) {
      final double w2 = fW * 2;
      final double scaleHalf = 1.0 / (1 + fW) * 0.5;
      m = ui.Offset((p0x + (w2 * p1x) + p2x) * scaleHalf,
          (p0y + (w2 * p1y) + p2y) * scaleHalf);
    }
    pair.first = Conic(p0x, p0y, (p0x + wp1.dx) * scale, (p0y + wp1.dy) * scale,
        m.dx, m.dy, newW);
    pair.second = Conic(m.dx, m.dy, (p2x + wp1.dx) * scale,
        (p2y + wp1.dy) * scale, p2x, p2y, newW);
  }

  void chopAtYExtrema(List<Conic> dst) {
    final double? t = _findYExtrema();
    if (t == null) {
      dst.add(this);
      return;
    }
    if (!_chopAt(t, dst, cleanupMiddle: true)) {
      // If chop can't return finite values, don't chop.
      dst.add(this);
      return;
    }
  }

  ///////////////////////////////////////////////////////////////////////////////
  //
  // NURB representation for conics.  Helpful explanations at:
  //
  // http://citeseerx.ist.psu.edu/viewdoc/
  //   download?doi=10.1.1.44.5740&rep=rep1&type=ps
  // and
  // http://www.cs.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/NURBS/RB-conics.html
  //
  // F = (A (1 - t)^2 + C t^2 + 2 B (1 - t) t w)
  //     ------------------------------------------
  //         ((1 - t)^2 + t^2 + 2 (1 - t) t w)
  //
  //   = {t^2 (P0 + P2 - 2 P1 w), t (-2 P0 + 2 P1 w), P0}
  //     ------------------------------------------------
  //             {t^2 (2 - 2 w), t (-2 + 2 w), 1}
  //
  // F' = 2 (C t (1 + t (-1 + w)) - A (-1 + t) (t (-1 + w) - w) + B (1 - 2 t) w)
  //
  //  t^2 : (2 P0 - 2 P2 - 2 P0 w + 2 P2 w)
  //  t^1 : (-2 P0 + 2 P2 + 4 P0 w - 4 P1 w)
  //  t^0 : -2 P0 w + 2 P1 w
  //
  //  We disregard magnitude, so we can freely ignore the denominator of F', and
  //  divide the numerator by 2
  //
  //    coeff[0] for t^2
  //    coeff[1] for t^1
  //    coeff[2] for t^0
  //
  double? _findYExtrema() {
    final double p20 = p2y - p0y;
    final double p10 = p1y - p0y;
    final double wP10 = fW * p10;
    final double coeff0 = fW * p20 - p20;
    final double coeff1 = p20 - 2 * wP10;
    final double coeff2 = wP10;
    final QuadRoots quadRoots = QuadRoots();
    final int rootCount = quadRoots.findRoots(coeff0, coeff1, coeff2);
    assert(rootCount == 0 || rootCount == 1);
    if (rootCount == 1) {
      return quadRoots.root0;
    }
    return null;
  }

  bool _chopAt(double t, List<Conic> dst, {bool cleanupMiddle = false}) {
    // Map conic to 3D.
    final double tx0 = p0x;
    final double ty0 = p0y;
    const double tz0 = 1;
    final double tx1 = p1x * fW;
    final double ty1 = p1y * fW;
    final double tz1 = fW;
    final double tx2 = p2x;
    final double ty2 = p2y;
    const double tz2 = 1;
    // Now interpolate each dimension.
    final double dx0 = tx0 + (tx1 - tx0) * t;
    final double dx2 = tx1 + (tx2 - tx1) * t;
    final double dx1 = dx0 + (dx2 - dx0) * t;
    final double dy0 = ty0 + (ty1 - ty0) * t;
    final double dy2 = ty1 + (ty2 - ty1) * t;
    final double dy1 = dy0 + (dy2 - dy0) * t;
    final double dz0 = tz0 + (tz1 - tz0) * t;
    final double dz2 = tz1 + (tz2 - tz1) * t;
    final double dz1 = dz0 + (dz2 - dz0) * t;
    // Compute new weights.
    final double root = math.sqrt(dz1);
    if (SPath.nearlyEqual(root, 0)) {
      return false;
    }
    final double w0 = dz0 / root;
    final double w2 = dz2 / root;
    if (SPath.nearlyEqual(dz0, 0) || SPath.nearlyEqual(dz1, 0) || SPath.nearlyEqual(dz2, 0)) {
      return false;
    }
    // Now we can construct the 2 conics by projecting 3D down to 2D.
    final double chopPointX = dx1 / dz1;
    final double chopPointY = dy1 / dz1;

    double cp0y = dy0 / dz0;
    double cp1y = dy2 / dz2;
    if (cleanupMiddle) {
      // Clean-up the middle, since we know t was meant to be at
      // an Y-extrema.
      cp0y = chopPointY;
      cp1y = chopPointY;
    }

    final Conic conic0 =
        Conic(p0x, p0y, dx0 / dz0, cp0y, chopPointX, chopPointY, w0);
    final Conic conic1 =
        Conic(chopPointX, chopPointY, dx2 / dz2, cp1y, p2x, p2y, w2);
    dst.add(conic0);
    dst.add(conic1);
    return true;
  }

  /// Computes number of binary subdivisions of the curve given
  /// the tolerance.
  ///
  /// The number of subdivisions never exceed [_maxSubdivisionCount].
  int _computeSubdivisionCount(double tolerance) {
    assert(tolerance.isFinite);
    // Expecting finite coordinates.
    assert(p0x.isFinite &&
        p1x.isFinite &&
        p2x.isFinite &&
        p0y.isFinite &&
        p1y.isFinite &&
        p2y.isFinite);
    if (tolerance < 0) {
      return 0;
    }
    // See "High order approximation of conic sections by quadratic splines"
    // by Michael Floater, 1993.
    // Error bound e0 = |a| |p0 - 2p1 + p2| / 4(2 + a).
    final double a = fW - 1;
    final double k = a / (4.0 * (2.0 + a));
    final double x = k * (p0x - 2 * p1x + p2x);
    final double y = k * (p0y - 2 * p1y + p2y);

    double error = math.sqrt(x * x + y * y);
    int pow2 = 0;
    for (; pow2 < _maxSubdivisionCount; ++pow2) {
      if (error <= tolerance) {
        break;
      }
      error *= 0.25;
    }
    return pow2;
  }

  ui.Offset evalTangentAt(double t) {
    // The derivative equation returns a zero tangent vector when t is 0 or 1,
    // and the control point is equal to the end point.
    // In this case, use the conic endpoints to compute the tangent.
    if ((t == 0 && p0x == p1x && p0y == p1y) ||
        (t == 1 && p1x == p2x && p1y == p2y)) {
      return ui.Offset(p2x - p0x, p2y - p0y);
    }
    final double p20x = p2x - p0x;
    final double p20y = p2y - p0y;
    final double p10x = p1x - p0x;
    final double p10y = p1y - p0y;

    final double cx = fW * p10x;
    final double cy = fW * p10y;
    final double ax = fW * p20x - p20x;
    final double ay = fW * p20y - p20y;
    final double bx = p20x - cx - cx;
    final double by = p20y - cy - cy;
    final SkQuadCoefficients quadC = SkQuadCoefficients(ax, ay, bx, by, cx, cy);
    return ui.Offset(quadC.evalX(t), quadC.evalY(t));
  }

  static double evalNumerator(
      double p0, double p1, double p2, double w, double t) {
    assert(t >= 0 && t <= 1);
    final double src2w = p1 * w;
    final double C = p0;
    final double A = p2 - 2 * src2w + C;
    final double B = 2 * (src2w - C);
    return polyEval(A, B, C, t);
  }

  static double evalDenominator(double w, double t) {
    final double B = 2 * (w - 1);
    const double C = 1;
    final double A = -B;
    return polyEval(A, B, C, t);
  }
}

class QuadBounds {
  double minX = 0;
  double minY = 0;
  double maxX = 0;
  double maxY = 0;

  void calculateBounds(Float32List points, int pointIndex) {
    final double x1 = points[pointIndex++];
    final double y1 = points[pointIndex++];
    final double cpX = points[pointIndex++];
    final double cpY = points[pointIndex++];
    final double x2 = points[pointIndex++];
    final double y2 = points[pointIndex++];

    minX = math.min(x1, x2);
    minY = math.min(y1, y2);
    maxX = math.max(x1, x2);
    maxY = math.max(y1, y2);

    // At extrema's derivative = 0.
    // Solve for
    // -2x1+2tx1 + 2cpX + 4tcpX + 2tx2 = 0
    // -2x1 + 2cpX +2t(x1 + 2cpX + x2) = 0
    // t = (x1 - cpX) / (x1 - 2cpX + x2)
    double denom = x1 - (2 * cpX) + x2;
    if (denom.abs() > SPath.scalarNearlyZero) {
      final double t1 = (x1 - cpX) / denom;
      if ((t1 >= 0) && (t1 <= 1.0)) {
        // Solve (x,y) for curve at t = tx to find extrema
        final double tprime = 1.0 - t1;
        final double extremaX =
            (tprime * tprime * x1) + (2 * t1 * tprime * cpX) + (t1 * t1 * x2);
        final double extremaY =
            (tprime * tprime * y1) + (2 * t1 * tprime * cpY) + (t1 * t1 * y2);
        // Expand bounds.
        minX = math.min(minX, extremaX);
        maxX = math.max(maxX, extremaX);
        minY = math.min(minY, extremaY);
        maxY = math.max(maxY, extremaY);
      }
    }
    // Now calculate dy/dt = 0
    denom = y1 - (2 * cpY) + y2;
    if (denom.abs() > SPath.scalarNearlyZero) {
      final double t2 = (y1 - cpY) / denom;
      if ((t2 >= 0) && (t2 <= 1.0)) {
        final double tprime2 = 1.0 - t2;
        final double extrema2X = (tprime2 * tprime2 * x1) +
            (2 * t2 * tprime2 * cpX) +
            (t2 * t2 * x2);
        final double extrema2Y = (tprime2 * tprime2 * y1) +
            (2 * t2 * tprime2 * cpY) +
            (t2 * t2 * y2);
        // Expand bounds.
        minX = math.min(minX, extrema2X);
        maxX = math.max(maxX, extrema2X);
        minY = math.min(minY, extrema2Y);
        maxY = math.max(maxY, extrema2Y);
      }
    }
  }
}

class ConicBounds {
  double minX = 0;
  double minY = 0;
  double maxX = 0;
  double maxY = 0;
  void calculateBounds(Float32List points, double w, int pointIndex) {
    final double x1 = points[pointIndex++];
    final double y1 = points[pointIndex++];
    final double cpX = points[pointIndex++];
    final double cpY = points[pointIndex++];
    final double x2 = points[pointIndex++];
    final double y2 = points[pointIndex++];

    minX = math.min(x1, x2);
    minY = math.min(y1, y2);
    maxX = math.max(x1, x2);
    maxY = math.max(y1, y2);

    // {t^2 (P0 + P2 - 2 P1 w), t (-2 P0 + 2 P1 w), P0}
    // ------------------------------------------------
    //       {t^2 (2 - 2 w), t (-2 + 2 w), 1}
    // Calculate coefficients and solve root.
    final QuadRoots roots = QuadRoots();
    final double p20x = x2 - x1;
    final double p10x = cpX - x1;
    final double wP10x = w * p10x;
    final double ax = w * p20x - p20x;
    final double bx = p20x - 2 * wP10x;
    final double cx = wP10x;
    int n = roots.findRoots(ax, bx, cx);
    if (n != 0) {
      final double t1 = roots.root0!;
      if ((t1 >= 0) && (t1 <= 1.0)) {
        final double denom = Conic.evalDenominator(w, t1);
        double numerator = Conic.evalNumerator(x1, cpX, x2, w, t1);
        final double extremaX = numerator / denom;
        numerator = Conic.evalNumerator(y1, cpY, y2, w, t1);
        final double extremaY = numerator / denom;
        // Expand bounds.
        minX = math.min(minX, extremaX);
        maxX = math.max(maxX, extremaX);
        minY = math.min(minY, extremaY);
        maxY = math.max(maxY, extremaY);
      }
    }
    final double p20y = y2 - y1;
    final double p10y = cpY - y1;
    final double wP10y = w * p10y;
    final double a = w * p20y - p20y;
    final double b = p20y - 2 * wP10y;
    final double c = wP10y;
    n = roots.findRoots(a, b, c);

    if (n != 0) {
      final double t2 = roots.root0!;
      if ((t2 >= 0) && (t2 <= 1.0)) {
        final double denom = Conic.evalDenominator(w, t2);
        double numerator = Conic.evalNumerator(x1, cpX, x2, w, t2);
        final double extrema2X = numerator / denom;
        numerator = Conic.evalNumerator(y1, cpY, y2, w, t2);
        final double extrema2Y = numerator / denom;
        // Expand bounds.
        minX = math.min(minX, extrema2X);
        maxX = math.max(maxX, extrema2X);
        minY = math.min(minY, extrema2Y);
        maxY = math.max(maxY, extrema2Y);
      }
    }
  }
}

class _ConicPair {
  Conic? first;
  Conic? second;
}
