/**
 * SVG Import — converts SVG files to Face DSL elements.
 *
 * Supports: circle, ellipse, rect, line, polygon, polyline, path, text, g.
 * Handles: fill, stroke, stroke-width, opacity, stroke-linecap, transforms.
 * Paths are linearized (bezier curves approximated as line segments → shape).
 */

const SVGImport = {

  /**
   * Parse an SVG string and return an array of DSL element objects.
   * @param {string} svgText  Raw SVG markup
   * @returns {{ elements: Array, name: string }}
   */
  parse(svgText) {
    const parser = new DOMParser();
    const doc = parser.parseFromString(svgText, 'image/svg+xml');
    const svg = doc.querySelector('svg');
    if (!svg) return { elements: [], name: 'SVG' };

    // Determine coordinate system from viewBox or width/height
    let vbX = 0, vbY = 0, vbW, vbH;
    const vb = svg.getAttribute('viewBox');
    if (vb) {
      [vbX, vbY, vbW, vbH] = vb.split(/[\s,]+/).map(Number);
    } else {
      vbW = parseFloat(svg.getAttribute('width')) || 100;
      vbH = parseFloat(svg.getAttribute('height')) || 100;
    }

    // Uniform scaling to fit in 0-100% square
    const scale = 100 / Math.max(vbW, vbH);
    const offX = (100 - vbW * scale) / 2;
    const offY = (100 - vbH * scale) / 2;

    const conv = {
      x: (v) => ((v - vbX) * scale + offX),
      y: (v) => ((v - vbY) * scale + offY),
      s: (v) => (v * scale),
      fmt: (v) => {
        const r = Math.round(v * 10) / 10;
        return r === Math.floor(r) ? String(Math.floor(r)) : r.toFixed(1);
      }
    };

    // Pre-scan gradient definitions (linearGradient, radialGradient)
    this._gradientMap = {};
    for (const grad of svg.querySelectorAll('linearGradient, radialGradient')) {
      const id = grad.getAttribute('id');
      if (!id) continue;
      const stops = grad.querySelectorAll('stop');
      if (stops.length === 0) continue;
      // Pick dominant color: stop closest to 50%, or midpoint of first/last
      let bestStop = stops[0];
      let bestDist = 1;
      for (const stop of stops) {
        let offsetStr = stop.getAttribute('offset') || '0';
        const offset = offsetStr.includes('%') ? parseFloat(offsetStr) / 100 : parseFloat(offsetStr);
        const dist = Math.abs((offset || 0) - 0.5);
        if (dist < bestDist) { bestDist = dist; bestStop = stop; }
      }
      // stop-color can be an attribute or in the style attribute
      let color = bestStop.getAttribute('stop-color');
      if (!color) {
        const st = bestStop.getAttribute('style');
        if (st) {
          const m = st.match(/stop-color\s*:\s*([^;]+)/);
          if (m) color = m[1].trim();
        }
      }
      if (color) this._gradientMap[id] = this._normalizeColor(color) || color;
    }

    const elements = [];
    this._processChildren(svg, elements, conv, [1, 0, 0, 1, 0, 0]);

    const title = svg.querySelector('title');
    const name = title ? title.textContent.trim() : 'SVG';

    return { elements, name };
  },

  // ── Process SVG children ──

  _processChildren(parent, elements, conv, parentMatrix) {
    for (const child of parent.children) {
      const tag = child.tagName;
      if (tag === 'defs' || tag === 'style' || tag === 'title' || tag === 'desc') continue;

      const localMatrix = this._parseTransform(child.getAttribute('transform'));
      const matrix = this._multiplyMatrix(parentMatrix, localMatrix);

      switch (tag) {
        case 'circle': this._convertCircle(child, elements, conv, matrix); break;
        case 'ellipse': this._convertEllipse(child, elements, conv, matrix); break;
        case 'rect': this._convertRect(child, elements, conv, matrix); break;
        case 'line': this._convertLine(child, elements, conv, matrix); break;
        case 'polygon': case 'polyline': this._convertPolyShape(child, elements, conv, matrix); break;
        case 'path': this._convertPath(child, elements, conv, matrix); break;
        case 'text': this._convertText(child, elements, conv, matrix); break;
        case 'g': this._convertGroup(child, elements, conv, matrix); break;
      }
    }
  },

  // ── SVG element converters ──

  _convertCircle(el, elements, conv, matrix) {
    const cx = parseFloat(el.getAttribute('cx')) || 0;
    const cy = parseFloat(el.getAttribute('cy')) || 0;
    const r = parseFloat(el.getAttribute('r')) || 0;
    if (r <= 0) return;

    const center = this._applyMatrix(matrix, cx, cy);
    const edge = this._applyMatrix(matrix, cx + r, cy);
    const rScaled = Math.sqrt((edge.x - center.x) ** 2 + (edge.y - center.y) ** 2);

    const props = this._extractProps(el, conv);
    elements.push({
      type: 'circle',
      args: [conv.fmt(conv.x(center.x)), conv.fmt(conv.y(center.y)), conv.fmt(conv.s(rScaled))],
      props
    });
  },

  _convertEllipse(el, elements, conv, matrix) {
    const cx = parseFloat(el.getAttribute('cx')) || 0;
    const cy = parseFloat(el.getAttribute('cy')) || 0;
    const rx = parseFloat(el.getAttribute('rx')) || 0;
    const ry = parseFloat(el.getAttribute('ry')) || 0;
    if (rx <= 0 && ry <= 0) return;

    // Check if transform is just a translation (no rotation/skew)
    if (this._isSimpleTranslation(matrix)) {
      const center = this._applyMatrix(matrix, cx, cy);
      const props = this._extractProps(el, conv);
      elements.push({
        type: 'ellipse',
        args: [conv.fmt(conv.x(center.x)), conv.fmt(conv.y(center.y)),
               conv.fmt(conv.s(rx)), conv.fmt(conv.s(ry))],
        props
      });
    } else {
      // Complex transform: approximate as shape
      const points = [];
      const n = 24;
      for (let i = 0; i < n; i++) {
        const a = (i / n) * Math.PI * 2;
        const px = cx + rx * Math.cos(a);
        const py = cy + ry * Math.sin(a);
        const tp = this._applyMatrix(matrix, px, py);
        points.push(tp);
      }
      this._addShapeFromPoints(points, el, elements, conv);
    }
  },

  _convertRect(el, elements, conv, matrix) {
    const x = parseFloat(el.getAttribute('x')) || 0;
    const y = parseFloat(el.getAttribute('y')) || 0;
    const w = parseFloat(el.getAttribute('width')) || 0;
    const h = parseFloat(el.getAttribute('height')) || 0;
    if (w <= 0 || h <= 0) return;

    if (this._isSimpleTranslation(matrix)) {
      const tl = this._applyMatrix(matrix, x, y);
      const rx = parseFloat(el.getAttribute('rx')) || 0;
      const props = this._extractProps(el, conv);
      if (rx > 0) props.corner = conv.fmt(conv.s(rx));
      elements.push({
        type: 'rect',
        args: [conv.fmt(conv.x(tl.x)), conv.fmt(conv.y(tl.y)),
               conv.fmt(conv.s(w)), conv.fmt(conv.s(h))],
        props
      });
    } else {
      // Rotated/skewed rect → shape
      const corners = [
        this._applyMatrix(matrix, x, y),
        this._applyMatrix(matrix, x + w, y),
        this._applyMatrix(matrix, x + w, y + h),
        this._applyMatrix(matrix, x, y + h)
      ];
      this._addShapeFromPoints(corners, el, elements, conv);
    }
  },

  _convertLine(el, elements, conv, matrix) {
    const x1 = parseFloat(el.getAttribute('x1')) || 0;
    const y1 = parseFloat(el.getAttribute('y1')) || 0;
    const x2 = parseFloat(el.getAttribute('x2')) || 0;
    const y2 = parseFloat(el.getAttribute('y2')) || 0;

    const p1 = this._applyMatrix(matrix, x1, y1);
    const p2 = this._applyMatrix(matrix, x2, y2);
    const props = this._extractProps(el, conv);

    elements.push({
      type: 'line',
      args: [conv.fmt(conv.x(p1.x)), conv.fmt(conv.y(p1.y)),
             conv.fmt(conv.x(p2.x)), conv.fmt(conv.y(p2.y))],
      props
    });
  },

  _convertPolyShape(el, elements, conv, matrix) {
    const pointsAttr = el.getAttribute('points');
    if (!pointsAttr) return;
    const nums = pointsAttr.trim().split(/[\s,]+/).map(Number);
    const points = [];
    for (let i = 0; i < nums.length - 1; i += 2) {
      const tp = this._applyMatrix(matrix, nums[i], nums[i + 1]);
      points.push(tp);
    }
    if (points.length < 3) return;
    this._addShapeFromPoints(points, el, elements, conv);
  },

  _convertPath(el, elements, conv, matrix) {
    const d = el.getAttribute('d');
    if (!d) return;

    const subpaths = this._parsePath(d);
    const props = this._extractProps(el, conv);

    // Clean and transform each subpath
    const validSubs = [];
    for (const subpath of subpaths) {
      if (subpath.length < 2) continue;
      const transformed = subpath.map(p => this._applyMatrix(matrix, p.x, p.y));

      // Deduplicate very close consecutive points
      const cleaned = [transformed[0]];
      for (let i = 1; i < transformed.length; i++) {
        const prev = cleaned[cleaned.length - 1];
        const dx = transformed[i].x - prev.x;
        const dy = transformed[i].y - prev.y;
        if (dx * dx + dy * dy > 0.01) cleaned.push(transformed[i]);
      }

      // Remove closing point if it matches the start
      if (cleaned.length >= 3) {
        const first = cleaned[0], last = cleaned[cleaned.length - 1];
        if (Math.abs(first.x - last.x) < 0.5 && Math.abs(first.y - last.y) < 0.5) {
          cleaned.pop();
        }
      }
      validSubs.push(cleaned);
    }

    // Filter out degenerate subpaths, emit lines for 2-point subs
    const shapeSubs = [];
    for (const cleaned of validSubs) {
      if (cleaned.length < 3) {
        if (cleaned.length === 2) {
          elements.push({
            type: 'line',
            args: [conv.fmt(conv.x(cleaned[0].x)), conv.fmt(conv.y(cleaned[0].y)),
                   conv.fmt(conv.x(cleaned[1].x)), conv.fmt(conv.y(cleaned[1].y))],
            props: { ...props }
          });
        }
      } else {
        shapeSubs.push(cleaned);
      }
    }

    if (shapeSubs.length === 0) return;

    // Single subpath → simple shape
    if (shapeSubs.length === 1) {
      const args = [];
      for (const p of shapeSubs[0]) args.push(conv.fmt(conv.x(p.x)), conv.fmt(conv.y(p.y)));
      elements.push({ type: 'shape', args, props: { ...props } });
      return;
    }

    // Multiple subpaths → compound shape with M separator (evenodd fill)
    const args = [];
    for (let si = 0; si < shapeSubs.length; si++) {
      if (si > 0) args.push('M');
      for (const p of shapeSubs[si]) {
        args.push(conv.fmt(conv.x(p.x)), conv.fmt(conv.y(p.y)));
      }
    }
    elements.push({ type: 'shape', args, props: { ...props } });
  },

  _convertText(el, elements, conv, matrix) {
    const text = el.textContent.trim();
    if (!text) return;
    const x = parseFloat(el.getAttribute('x')) || 0;
    const y = parseFloat(el.getAttribute('y')) || 0;
    const tp = this._applyMatrix(matrix, x, y);

    const props = {};
    const fill = this._getAttrOrStyle(el, 'fill');
    if (fill && fill !== 'none') props.color = this._normalizeColor(fill);
    const fontSize = this._getAttrOrStyle(el, 'font-size');
    if (fontSize) {
      const sz = parseFloat(fontSize);
      if (!isNaN(sz)) props.size = conv.fmt(conv.s(sz));
    }
    const fontWeight = this._getAttrOrStyle(el, 'font-weight');
    if (fontWeight === 'bold' || (parseInt(fontWeight) >= 700)) props.bold = 'true';
    const opacity = this._getAttrOrStyle(el, 'opacity');
    if (opacity && parseFloat(opacity) < 1) props.alpha = opacity;
    const anchor = this._getAttrOrStyle(el, 'text-anchor');
    if (anchor === 'start') props.align = 'left';
    else if (anchor === 'end') props.align = 'right';

    elements.push({
      type: 'text',
      args: ['"' + text.replace(/"/g, '') + '"',
             conv.fmt(conv.x(tp.x)), conv.fmt(conv.y(tp.y))],
      props
    });
  },

  _convertGroup(el, elements, conv, matrix) {
    const children = [];
    this._processChildren(el, children, conv, matrix);
    if (children.length === 0) return;

    const name = el.getAttribute('id') || el.getAttribute('data-name') || 'Groupe';
    elements.push({
      isGroup: true,
      name: name.replace(/[_-]/g, ' '),
      children
    });
  },

  // ── Helper: create shape from points ──

  _addShapeFromPoints(points, svgEl, elements, conv) {
    if (points.length < 3) return;
    const args = [];
    for (const p of points) {
      args.push(conv.fmt(conv.x(p.x)), conv.fmt(conv.y(p.y)));
    }
    const props = this._extractProps(svgEl, conv);
    elements.push({ type: 'shape', args, props });
  },

  // ── SVG style extraction ──

  _getAttrOrStyle(el, prop) {
    const style = el.getAttribute('style');
    if (style) {
      const match = style.match(new RegExp(prop + '\\s*:\\s*([^;]+)'));
      if (match) return match[1].trim();
    }
    return el.getAttribute(prop);
  },

  _extractProps(el, conv) {
    const props = {};
    const fill = this._getAttrOrStyle(el, 'fill');
    if (fill && fill !== 'none') props.fill = this._normalizeColor(fill);
    else if (fill === 'none') { /* no fill */ }
    else if (!this._getAttrOrStyle(el, 'stroke')) props.fill = '#000000'; // SVG default

    const stroke = this._getAttrOrStyle(el, 'stroke');
    if (stroke && stroke !== 'none') props.stroke = this._normalizeColor(stroke);

    const sw = this._getAttrOrStyle(el, 'stroke-width');
    if (sw && stroke && stroke !== 'none') {
      const val = parseFloat(sw);
      if (!isNaN(val)) props.w = conv.fmt(conv.s(val));
    }

    const opacity = this._getAttrOrStyle(el, 'opacity');
    if (opacity && parseFloat(opacity) < 1) props.alpha = String(parseFloat(opacity).toFixed(2));

    const fillOpacity = this._getAttrOrStyle(el, 'fill-opacity');
    if (fillOpacity && parseFloat(fillOpacity) < 1 && !opacity) {
      props.alpha = String(parseFloat(fillOpacity).toFixed(2));
    }

    const cap = this._getAttrOrStyle(el, 'stroke-linecap');
    if (cap && cap !== 'butt') props.cap = cap;

    return props;
  },

  _normalizeColor(color) {
    if (!color || color === 'none') return null;
    // Resolve gradient references to representative solid color
    const urlMatch = color.match(/url\(['"]?#([^)'"]+)['"]?\)/);
    if (urlMatch) {
      return this._gradientMap[urlMatch[1]] || '#888888';
    }
    if (color.startsWith('#')) return color.toUpperCase();
    if (color.startsWith('rgb')) {
      const match = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
      if (match) {
        return '#' + [match[1], match[2], match[3]]
          .map(n => parseInt(n).toString(16).padStart(2, '0')).join('').toUpperCase();
      }
    }
    // Use canvas to resolve named colors
    try {
      const ctx = document.createElement('canvas').getContext('2d');
      ctx.fillStyle = color;
      return ctx.fillStyle.toUpperCase();
    } catch (e) {
      return color;
    }
  },

  // ── Transform matrix handling ──

  _parseTransform(attr) {
    if (!attr) return [1, 0, 0, 1, 0, 0];
    let result = [1, 0, 0, 1, 0, 0];

    const regex = /(translate|rotate|scale|matrix|skewX|skewY)\(([^)]+)\)/g;
    let m;
    while ((m = regex.exec(attr)) !== null) {
      const type = m[1];
      const args = m[2].split(/[\s,]+/).map(Number);
      let t;
      switch (type) {
        case 'translate':
          t = [1, 0, 0, 1, args[0] || 0, args[1] || 0];
          break;
        case 'rotate': {
          const a = (args[0] || 0) * Math.PI / 180;
          const cos = Math.cos(a), sin = Math.sin(a);
          if (args.length === 3) {
            const cx = args[1], cy = args[2];
            t = [cos, sin, -sin, cos, cx - cos * cx + sin * cy, cy - sin * cx - cos * cy];
          } else {
            t = [cos, sin, -sin, cos, 0, 0];
          }
          break;
        }
        case 'scale':
          t = [args[0] || 1, 0, 0, args[1] !== undefined ? args[1] : (args[0] || 1), 0, 0];
          break;
        case 'matrix':
          t = args.length >= 6 ? args.slice(0, 6) : [1, 0, 0, 1, 0, 0];
          break;
        default:
          continue;
      }
      result = this._multiplyMatrix(result, t);
    }
    return result;
  },

  _multiplyMatrix(a, b) {
    return [
      a[0] * b[0] + a[2] * b[1],
      a[1] * b[0] + a[3] * b[1],
      a[0] * b[2] + a[2] * b[3],
      a[1] * b[2] + a[3] * b[3],
      a[0] * b[4] + a[2] * b[5] + a[4],
      a[1] * b[4] + a[3] * b[5] + a[5]
    ];
  },

  _applyMatrix(m, x, y) {
    return {
      x: m[0] * x + m[2] * y + m[4],
      y: m[1] * x + m[3] * y + m[5]
    };
  },

  _isSimpleTranslation(m) {
    return Math.abs(m[0] - 1) < 0.001 && Math.abs(m[1]) < 0.001 &&
           Math.abs(m[2]) < 0.001 && Math.abs(m[3] - 1) < 0.001;
  },

  // ── SVG path parser ──

  _parsePath(d) {
    const subpaths = [];
    let current = [];
    let cx = 0, cy = 0, sx = 0, sy = 0;
    let lastCp = null;

    const regex = /([MmLlHhVvCcSsQqTtAaZz])([^MmLlHhVvCcSsQqTtAaZz]*)/g;
    let match;
    while ((match = regex.exec(d)) !== null) {
      const cmd = match[1];
      const argsStr = match[2].trim();
      let args = [];
      if (argsStr.length > 0) {
        const numRe = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
        let nm;
        while ((nm = numRe.exec(argsStr)) !== null) args.push(parseFloat(nm[0]));
      }
      const isRel = cmd === cmd.toLowerCase();
      const C = cmd.toUpperCase();

      switch (C) {
        case 'M': {
          if (current.length > 0) subpaths.push(current);
          const x = isRel ? cx + args[0] : args[0];
          const y = isRel ? cy + args[1] : args[1];
          current = [{ x, y }];
          cx = x; cy = y; sx = x; sy = y;
          for (let i = 2; i < args.length; i += 2) {
            const lx = isRel ? cx + args[i] : args[i];
            const ly = isRel ? cy + args[i + 1] : args[i + 1];
            current.push({ x: lx, y: ly });
            cx = lx; cy = ly;
          }
          break;
        }
        case 'L': {
          for (let i = 0; i < args.length; i += 2) {
            const x = isRel ? cx + args[i] : args[i];
            const y = isRel ? cy + args[i + 1] : args[i + 1];
            current.push({ x, y });
            cx = x; cy = y;
          }
          break;
        }
        case 'H': {
          for (let i = 0; i < args.length; i++) {
            const x = isRel ? cx + args[i] : args[i];
            current.push({ x, y: cy });
            cx = x;
          }
          break;
        }
        case 'V': {
          for (let i = 0; i < args.length; i++) {
            const y = isRel ? cy + args[i] : args[i];
            current.push({ x: cx, y });
            cy = y;
          }
          break;
        }
        case 'C': {
          for (let i = 0; i < args.length; i += 6) {
            const x1 = isRel ? cx + args[i] : args[i];
            const y1 = isRel ? cy + args[i + 1] : args[i + 1];
            const x2 = isRel ? cx + args[i + 2] : args[i + 2];
            const y2 = isRel ? cy + args[i + 3] : args[i + 3];
            const x = isRel ? cx + args[i + 4] : args[i + 4];
            const y = isRel ? cy + args[i + 5] : args[i + 5];
            this._cubicTo(current, cx, cy, x1, y1, x2, y2, x, y);
            lastCp = { x: x2, y: y2 };
            cx = x; cy = y;
          }
          break;
        }
        case 'S': {
          for (let i = 0; i < args.length; i += 4) {
            const x1 = lastCp ? 2 * cx - lastCp.x : cx;
            const y1 = lastCp ? 2 * cy - lastCp.y : cy;
            const x2 = isRel ? cx + args[i] : args[i];
            const y2 = isRel ? cy + args[i + 1] : args[i + 1];
            const x = isRel ? cx + args[i + 2] : args[i + 2];
            const y = isRel ? cy + args[i + 3] : args[i + 3];
            this._cubicTo(current, cx, cy, x1, y1, x2, y2, x, y);
            lastCp = { x: x2, y: y2 };
            cx = x; cy = y;
          }
          break;
        }
        case 'Q': {
          for (let i = 0; i < args.length; i += 4) {
            const x1 = isRel ? cx + args[i] : args[i];
            const y1 = isRel ? cy + args[i + 1] : args[i + 1];
            const x = isRel ? cx + args[i + 2] : args[i + 2];
            const y = isRel ? cy + args[i + 3] : args[i + 3];
            this._quadTo(current, cx, cy, x1, y1, x, y);
            lastCp = { x: x1, y: y1 };
            cx = x; cy = y;
          }
          break;
        }
        case 'T': {
          for (let i = 0; i < args.length; i += 2) {
            const x1 = lastCp ? 2 * cx - lastCp.x : cx;
            const y1 = lastCp ? 2 * cy - lastCp.y : cy;
            const x = isRel ? cx + args[i] : args[i];
            const y = isRel ? cy + args[i + 1] : args[i + 1];
            this._quadTo(current, cx, cy, x1, y1, x, y);
            lastCp = { x: x1, y: y1 };
            cx = x; cy = y;
          }
          break;
        }
        case 'A': {
          for (let i = 0; i < args.length; i += 7) {
            const rx = args[i], ry = args[i + 1];
            const xRot = args[i + 2];
            const largeArc = args[i + 3];
            const sweepFlag = args[i + 4];
            const x = isRel ? cx + args[i + 5] : args[i + 5];
            const y = isRel ? cy + args[i + 6] : args[i + 6];
            this._arcTo(current, cx, cy, rx, ry, xRot, largeArc, sweepFlag, x, y);
            cx = x; cy = y;
          }
          lastCp = null;
          break;
        }
        case 'Z': {
          cx = sx; cy = sy;
          break;
        }
      }

      if (C !== 'C' && C !== 'S' && C !== 'Q' && C !== 'T') lastCp = null;
    }

    if (current.length > 0) subpaths.push(current);
    return subpaths;
  },

  // ── Bezier/arc linearization ──

  _cubicTo(pts, x0, y0, x1, y1, x2, y2, x3, y3) {
    const n = 10;
    for (let i = 1; i <= n; i++) {
      const t = i / n, mt = 1 - t;
      pts.push({
        x: mt * mt * mt * x0 + 3 * mt * mt * t * x1 + 3 * mt * t * t * x2 + t * t * t * x3,
        y: mt * mt * mt * y0 + 3 * mt * mt * t * y1 + 3 * mt * t * t * y2 + t * t * t * y3
      });
    }
  },

  _quadTo(pts, x0, y0, x1, y1, x2, y2) {
    const n = 8;
    for (let i = 1; i <= n; i++) {
      const t = i / n, mt = 1 - t;
      pts.push({
        x: mt * mt * x0 + 2 * mt * t * x1 + t * t * x2,
        y: mt * mt * y0 + 2 * mt * t * y1 + t * t * y2
      });
    }
  },

  _arcTo(pts, x1, y1, rxIn, ryIn, xRot, largeArc, sweepFlag, x2, y2) {
    let rx = Math.abs(rxIn), ry = Math.abs(ryIn);
    if (rx === 0 || ry === 0) { pts.push({ x: x2, y: y2 }); return; }

    const phi = xRot * Math.PI / 180;
    const cosPhi = Math.cos(phi), sinPhi = Math.sin(phi);

    const dx2 = (x1 - x2) / 2, dy2 = (y1 - y2) / 2;
    const x1p = cosPhi * dx2 + sinPhi * dy2;
    const y1p = -sinPhi * dx2 + cosPhi * dy2;

    let lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
    if (lambda > 1) { rx *= Math.sqrt(lambda); ry *= Math.sqrt(lambda); }

    const num = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
    const den = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
    let sq = Math.sqrt(Math.max(0, num / den));
    if (largeArc === sweepFlag) sq = -sq;

    const cxp = sq * rx * y1p / ry;
    const cyp = -sq * ry * x1p / rx;

    const cxFinal = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
    const cyFinal = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;

    const vecAngle = (ux, uy, vx, vy) => {
      const dot = ux * vx + uy * vy;
      const len = Math.sqrt(ux * ux + uy * uy) * Math.sqrt(vx * vx + vy * vy);
      let a = Math.acos(Math.max(-1, Math.min(1, dot / len)));
      if (ux * vy - uy * vx < 0) a = -a;
      return a;
    };

    const theta1 = vecAngle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
    let dtheta = vecAngle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);

    if (!sweepFlag && dtheta > 0) dtheta -= 2 * Math.PI;
    if (sweepFlag && dtheta < 0) dtheta += 2 * Math.PI;

    const n = Math.max(8, Math.ceil(Math.abs(dtheta) / (Math.PI / 8)));
    for (let i = 1; i <= n; i++) {
      const t = theta1 + dtheta * i / n;
      const xp = rx * Math.cos(t), yp = ry * Math.sin(t);
      pts.push({
        x: cosPhi * xp - sinPhi * yp + cxFinal,
        y: sinPhi * xp + cosPhi * yp + cyFinal
      });
    }
  }
};
