// shape_recognizer.dart

import 'dart:math';

import 'package:flutter/material.dart';

import 'ramer_douglas_peucker.dart';

/// 定义了可识别的手势类型。
enum GestureType {
  none, // 未识别或无意义手势：当没有笔画或识别器无法匹配任何已知手势时返回。
  check, // 勾 (✓)：单个笔画组成的“对号”。
  cross, // 叉 (X)：由两条笔画交叉形成的“叉号”。
  halfCheck, // 半勾 (一个勾 ✓，其末端带有一个小叉 x)：由一个勾和另一条短线在勾的终点附近交叉构成。
  slash, // 斜线 (\ 或 /)：单个笔画形成的直线斜线。
}

/// 一个用于识别手绘手势的工具类。
/// 该类通过分析用户绘制的笔画集合（一系列Offset点），尝试将其归类为预定义的手势类型。
/// 识别过程包括点简化（RDP算法）、角度计算、线段交叉检测等几何分析。
class ShapeRecognizer {
  // --- 可配置参数 ---
  /// Ramer-Douglas-Peucker 算法的 epsilon 值，用于简化笔画路径。较大的值会导致更激进的简化，减少点的数量。
  final double rdpEpsilon;

  /// 识别“勾”手势时，折角（拐点）的最小允许角度（度）。
  final double checkAngleMin;

  /// 识别“勾”手势时，折角（拐点）的最大允许角度（度）。
  final double checkAngleMax;

  /// 识别“叉”手势时，两条线段交叉的最小允许夹角（度）。
  final double crossAngleMin;

  /// 识别“叉”手势时，两条线段交叉的最大允许夹角（度）。
  final double crossAngleMax;

  /// 判断线段相交时，交点必须在线段“内部”的最小比例距离。
  /// 例如，0.15 表示交点必须距离线段两端至少 15% 的长度。
  final double minIntersectionDistance;

  /// 识别“勾”手势时，勾的第二段（向上）相对于第一段（向下）的最小长度比例。
  /// 通常勾的第二段会比第一段长。
  final double checkLengthRatio;

  /// 识别“斜线”手势时，线段倾斜的最小绝对斜率。用于排除水平或垂直线。
  final double slashSlopeMin;

  /// 识别“斜线”手势时，线段倾斜的最大绝对斜率。用于排除过于平缓的线。
  final double slashSlopeMax;

  /// 识别“斜线”手势时，线段倾斜的最大弧度。用于排除过于平缓的线。
  final double slashRadianMax;

  /// 构造函数，允许自定义识别参数。
  ShapeRecognizer({
    this.rdpEpsilon = 8.0,
    this.checkAngleMin = 60.0,
    this.checkAngleMax = 140.0,
    this.crossAngleMin = 40.0,
    this.crossAngleMax = 160.0,
    this.minIntersectionDistance = 0.15,
    this.checkLengthRatio = 1.1,
    this.slashSlopeMin = 0.5,
    this.slashSlopeMax = 2.0,
    this.slashRadianMax = 140,
  });

  /// --- 公共入口方法 ---

  /// 识别传入的笔画集合属于哪种手势。
  /// 根据笔画的数量（1笔、2笔或多于2笔）调用不同的内部识别逻辑。
  /// 返回识别到的 GestureType，如果未识别则返回 GestureType.none。
  GestureType recognize(List<List<Offset>> strokes) {
    if (strokes.isEmpty) return GestureType.none; // 如果没有笔画，直接返回无手势

    switch (strokes.length) {
      case 1:

        /// 全部都是单笔画手势，可能是勾 (✓) 或斜线 (\ 或 /)
        final points = strokes.first; // 获取第一笔的全部点
        if (points.length < 2) return GestureType.none; // 笔画点少于2个无法构成有效线段

        var result = _recognizeCheck(points); // 尝试识别为“勾”
        if (result != GestureType.none) return result;
        return _recognizeSlash(points); // 如果不是勾，尝试识别为“斜线”

      case 2:
        // [关键修复] 调整识别顺序：优先识别更具体的“半勾”。
        // 因为“半勾”是“叉”的一种特殊形式，如果先判断“叉”，会导致“半勾”被错误识别。
        var halfCheckResult = _recognizeHalfCheck(strokes);
        if (halfCheckResult != GestureType.none) return halfCheckResult;

        // 如果不是“半勾”，再判断是否为通用的“叉”。
        return _recognizeCross(strokes);
      // // 识别两笔画手势，可能是叉 (X) 或半勾 (✓ + 一条短线)
      // var crossResult = _recognizeCross(strokes); // 尝试识别为“叉”
      // if (crossResult != GestureType.none) return crossResult;
      // return _recognizeHalfCheck(strokes); // 如果不是叉，尝试识别为“半勾”

      default:
        return GestureType.none; // 超过两笔画目前不识别，返回无手势
    }
  }

  /// --- 私有顶层识别逻辑 ---

  /// 判断单笔画是否为“勾”手势。
  /// 内部调用 `_getCheckmarkDetails` 来获取勾的细节，如果获取成功则返回 `GestureType.check`。
  GestureType _recognizeCheck(List<Offset> rawPoints) {
    return _getCheckmarkDetails(rawPoints) != null ? GestureType.check : GestureType.none;
  }

  /// 判断单笔画是否为“斜线”手势。
  /// 内部调用 `_getSlashDetails` 来获取斜线的细节，如果获取成功则返回 `GestureType.slash`。
  GestureType _recognizeSlash(List<Offset> rawPoints) {
    return _getSlashDetails(rawPoints) ? GestureType.slash : GestureType.none;
  }

  /// 判断两笔画是否为“叉”手势。
  /// 内部调用 `_getCrossDetails` 来获取叉的细节，如果获取成功则返回 `GestureType.cross`。
  GestureType _recognizeCross(List<List<Offset>> rawStrokes) {
    // 如果能获取到叉的交点，则识别成功
    return _getCrossDetails(rawStrokes) != null ? GestureType.cross : GestureType.none;
  }

  /// 识别两笔画“半勾”。
  /// 会尝试将每一笔识别为“勾”，另一笔识别为“交叉线”。
  GestureType _recognizeHalfCheck(List<List<Offset>> strokes) {
    if (strokes.length != 2) return GestureType.none;

    /// 第一笔作为勾，第二笔作为交叉线
    var firstStroke = strokes[0];
    var lastStroke = strokes[1];

    /// 尝试识别为“勾”,是√就返回以折点为中心的两条线分为两段
    List<Offset>? checkDetails = _getCheckmarkDetails(firstStroke);
    if (checkDetails == null) {
      /// 如果第一笔不是勾，尝试将第二笔作为勾
      checkDetails = _getCheckmarkDetails(lastStroke);
      if (checkDetails == null) {
        return GestureType.none;
      } else {
        // 交换顺序，确保 checkDetails 总是对应第一笔
        final temp = firstStroke;
        firstStroke = lastStroke;
        lastStroke = temp;
      }
    }

    // 方案1：尝试将 stroke1 作为“勾”，stroke2 作为“交叉线”
    if (_isHalfCheckCombination(
        checkCandidate: firstStroke, crosserCandidate: lastStroke, checkDetails: checkDetails)) {
      return GestureType.halfCheck;
    }

    return GestureType.none;

    // if (twoStrokes.length != 2) return GestureType.none;

    // final stroke1 = twoStrokes[0];
    // final stroke2 = twoStrokes[1];

    // // 尝试组合1: stroke1是勾，stroke2是交叉线
    // final checkDetails1 = _getCheckmarkDetails(stroke1);
    // if (checkDetails1 != null) {
    //   if (_checkHalfCheckLogic(checkDetails1, stroke2)) {
    //     return GestureType.halfCheck;
    //   }
    // }

    // // 尝试组合2: stroke2是勾，stroke1是交叉线
    // final checkDetails2 = _getCheckmarkDetails(stroke2);
    // if (checkDetails2 != null) {
    //   if (_checkHalfCheckLogic(checkDetails2, stroke1)) {
    //     return GestureType.halfCheck;
    //   }
    // }

    // return GestureType.none;
  }

  /// 辅助函数：判断一个组合是否构成半勾。
  /// [checkCandidate]  可能是勾的笔画
  /// [crosserCandidate]  可能是交叉线的笔画
  /// [checkDetails]  勾的细节信息
  bool _isHalfCheckCombination({
    required List<Offset> checkCandidate,
    required List<Offset> crosserCandidate,
    required List<Offset> checkDetails,
  }) {
    // 提取勾的拐点和终点
    final checkCorner = checkDetails[1];
    final checkEndpoint = checkDetails[2];

    final simplifiedCrosser = RamerDouglasPeucker.simplify(crosserCandidate, rdpEpsilon);
    if (simplifiedCrosser.length < 2) return false;

    // 步骤2：判断第二笔是否与第一笔的“折线后到末端”形成一个叉。
    // 这里使用专为半勾设计的、允许在端点附近交叉的检测函数。
    final intersection = _findIntersectionNearTip(
      simplifiedCrosser.first, simplifiedCrosser.last, // 第二笔
      checkCorner, checkEndpoint, // 第一笔的后半段
    );

    if (intersection != null) {
      // 如果找到了符合条件的交叉点，我们再验证交叉角度是否合理
      final vCross = simplifiedCrosser.last - simplifiedCrosser.first;
      final vCheckLeg = checkEndpoint - checkCorner;
      final angle = _calculateAngleBetweenVectors(vCross, vCheckLeg);

      if (angle > crossAngleMin && angle < crossAngleMax) {
        return true; // 成功识别为半勾！
      }
    }

    return false; // 交叉点或角度不满足条件，此组合失败。
  }

  /// 专为“半勾”设计的交叉检测函数，以实现的判断方案。
  /// 它允许交叉点靠近 `checkLeg` 的终点，但对 `crossLine` 保持严格的内部交叉要求。
  Offset? _findIntersectionNearTip(
      Offset crossLineStart, Offset crossLineEnd, Offset checkLegStart, Offset checkLegEnd) {
    final s1X = crossLineEnd.dx - crossLineStart.dx;
    final s1Y = crossLineEnd.dy - crossLineStart.dy;
    final s2X = checkLegEnd.dx - checkLegStart.dx;
    final s2Y = checkLegEnd.dy - checkLegStart.dy;
    final denominator = -s2X * s1Y + s1X * s2Y;
    if (denominator.abs() < 1e-6) return null;

    // t 是交叉点在 crossLine 上的比例位置
    final t =
        (s2X * (crossLineStart.dy - checkLegStart.dy) - s2Y * (crossLineStart.dx - checkLegStart.dx)) / denominator;
    // s 是交叉点在 checkLeg 上的比例位置
    final s =
        (-s1Y * (crossLineStart.dx - checkLegStart.dx) + s1X * (crossLineStart.dy - checkLegStart.dy)) / denominator;

    // 条件1: 交叉线必须被“贯穿”，交叉点不能在其端点附近
    final isCrosserIntersectedWell = t > minIntersectionDistance && t < (1 - minIntersectionDistance);
    print("minIntersectionDistance: $minIntersectionDistance, t: $t");

    // 条件2: 对于勾的第二段，交叉点必须在其后半部分 (s > 0.1) 且不能超过终点 (s <= 1.0)
    final isCheckLegIntersectedNearTip = s >= 0.1 && s <= 1.0;
    print(
        "值：isCheckLegIntersectedNearTip: $isCheckLegIntersectedNearTip,isCrosserIntersectedWell: $isCrosserIntersectedWell, s: $s");

    if (isCrosserIntersectedWell && isCheckLegIntersectedNearTip) {
      return Offset(crossLineStart.dx + (t * s1X), crossLineStart.dy + (t * s1Y));
    }
    return null;
  }

  /// --- 形状细节提取辅助方法 ---

  /// 从原始点集中尝试识别一个“勾”手势，并返回勾的关键三个点：起点、拐点、终点。
  /// 如果识别失败，则返回 `null`。
  List<Offset>? _getCheckmarkDetails(List<Offset> rawPoints) {
    // 1. 使用 RDP 算法简化笔画，去除冗余点，保留关键形状。
    final simplifiedPoints = RamerDouglasPeucker.simplify(rawPoints, rdpEpsilon);
    // 勾至少需要3个点：起点、拐点、终点。
    if (simplifiedPoints.length < 3) return null;

    // 2. 寻找“勾”的拐点：勾的形状通常有一个锐角或特定范围内的转折角。
    int cornerIndex = -1; // 拐点的索引
    double minAngle = 180.0; // 记录最小角度
    for (int i = 1; i < simplifiedPoints.length - 1; i++) {
      // 计算相邻三点形成的夹角。
      final angle = _calculateAngle(simplifiedPoints[i - 1], simplifiedPoints[i], simplifiedPoints[i + 1]);
      if (angle < minAngle) {
        minAngle = angle;
        cornerIndex = i;
      }
    }

    // 3. 检查找到的拐点是否满足“勾”的角度条件。
    if (cornerIndex == -1 || minAngle < checkAngleMin || minAngle > checkAngleMax) return null;

    // 4. 提取勾的三个关键点。
    final p1 = simplifiedPoints.first, p2 = simplifiedPoints[cornerIndex], p3 = simplifiedPoints.last;
    // 5. 检查勾的形状方向：通常勾的“向下”部分 (p1-p2) 后跟“向上”部分 (p2-p3)。
    //   即拐点 p2 应低于起点 p1，终点 p3 应高于拐点 p2（此处原始逻辑 `p3.dy < p2.dy` 与“向上”理解有异，更像传统V字，如果确实是标准勾（√）需要调整）。
    //   实际代码：p2.dy > p1.dy 表示p2在p1下方；p3.dy < p2.dy 表示p3在p2上方，形成标准的“对勾”向下再向上。
    if (p2.dy > p1.dy && p3.dy < p2.dy) {
      final length1 = (p2 - p1).distance, length2 = (p3 - p2).distance; // 计算两条线段的长度
      // 6. 检查勾的长度比例：第二段通常比第一段长（例如：`length2` 应该大于 `length1` 的 `checkLengthRatio` 倍）。
      if (length1 > 0 && length2 > length1 * checkLengthRatio) return [p1, p2, p3]; // 满足所有条件，返回这三个关键点
    }
    return null; // 不满足勾的条件
  }

  /// 从原始点集中尝试识别一个“斜线”手势，并返回线段的起点和终点。
  /// 斜线被定义为一条通过 RDP 简化后只剩下两个点的笔画，且其斜率在指定范围内。
  bool _getSlashDetails(List<Offset> points) {
    if (points.isEmpty) return false;

    final simplified = RamerDouglasPeucker.simplify(points, rdpEpsilon);

    // 1. 如果简化后是2个点，那么它是一条标准的直线斜线。
    if (simplified.length == 2) {
      final p1 = simplified.first, p2 = simplified.last;
      final deltaX = (p2.dx - p1.dx).abs();
      final deltaY = (p2.dy - p1.dy).abs();

      if (deltaX < 1e-6) return true;
      if (deltaY < 1e-6) return false;

      final slope = deltaY / deltaX;
      return slope > slashSlopeMin && slope < slashSlopeMax;
    }

    // 2. 如果简化后是3个点，我们需要判断它是一个平滑的弧线还是一个尖锐的拐角。
    if (simplified.length == 3) {
      final p1 = simplified[0];
      final p2 = simplified[1]; // 中间的点
      final p3 = simplified[2];

      // 计算中间点的角度
      final angle = _calculateAngle(p1, p2, p3);

      // 如果角度非常大（例如 > 160度），说明这是一个平滑的钝角，代表的是一条弧线。
      // 我们接受这种弧线作为斜线的一种变体。
      // 这个160度的阈值可以根据需要进行微调。
      if (angle > slashRadianMax) {
        // 额外检查，确保这条弧线整体上仍然符合斜率范围
        final overallP1 = points.first;
        final overallP3 = points.last;
        final deltaX = (overallP3.dx - overallP1.dx).abs();
        final deltaY = (overallP3.dy - overallP1.dy).abs();

        if (deltaX < 1e-6) return true;
        if (deltaY < 1e-6) return false;

        final slope = deltaY / deltaX;
        return slope > slashSlopeMin && slope < slashSlopeMax;
      }
    }

    // 3. 如果简化后是其他数量的点（如1个或4个以上），则不认为是斜线。
    return false;
  }

  /// 尝试将两笔画识别为“叉”，成功则返回其交点，否则返回 null。
  /// “叉”由两条相交的线段组成，且其夹角在指定范围内。
  Offset? _getCrossDetails(List<List<Offset>> twoStrokes) {
    if (twoStrokes.length != 2) return null; // 必须是两笔画

    // 简化两条笔画。
    final simplifiedStroke1 = RamerDouglasPeucker.simplify(twoStrokes[0], rdpEpsilon);
    final simplifiedStroke2 = RamerDouglasPeucker.simplify(twoStrokes[1], rdpEpsilon);

    if (simplifiedStroke1.length < 2 || simplifiedStroke2.length < 2) return null; // 简化后每条线至少需要2个点

    // 寻找两条线段的交点（使用简化后的起点和终点）。
    final intersection = _findIntersection(
        simplifiedStroke1.first, simplifiedStroke1.last, simplifiedStroke2.first, simplifiedStroke2.last);
    if (intersection == null) return null; // 未相交，则不是叉

    // 计算两条线段的夹角。
    final angle = _calculateAngleBetweenVectors(
        simplifiedStroke1.last - simplifiedStroke1.first, // 第一条线段的向量
        simplifiedStroke2.last - simplifiedStroke2.first); // 第二条线段的向量

    if (angle > crossAngleMin && angle < crossAngleMax) return intersection; // 夹角在叉的范围内，认为是叉

    return null;
  }

  /// --- 几何计算辅助方法 ---

  /// 计算由三个点 `p1`、`p2`、`p3` 组成的折线的拐点 `p2` 处的夹角（度）。
  /// 实际上是计算向量 `p1-p2` 和 `p3-p2` 之间的夹角。
  double _calculateAngle(Offset p1, Offset p2, Offset p3) {
    return _calculateAngleBetweenVectors(p1 - p2, p3 - p2);
  }

  /// 计算两个向量 `v1` 和 `v2` 之间的夹角（度）。
  /// 使用点积和向量长度来计算 `cosTheta`，然后反余弦得到角度。
  double _calculateAngleBetweenVectors(Offset v1, Offset v2) {
    final double dotProduct = v1.dx * v2.dx + v1.dy * v2.dy; // 向量点积
    final double magnitude1 = v1.distance; // 向量 v1 的模（长度）
    final double magnitude2 = v2.distance; // 向量 v2 的模（长度）
    if (magnitude1 == 0 || magnitude2 == 0) return 0; // 如果任何一个向量长度为0，夹角无意义，返回0。
    // 限制 cosTheta 在 [-1, 1] 范围内，以避免浮点数精度问题导致 acos 报错。
    final cosTheta = (dotProduct / (magnitude1 * magnitude2)).clamp(-1.0, 1.0);
    return acos(cosTheta) * 180 / pi; // 返回弧度转换为角度
  }

  /// 寻找两条线段 (p1, p2) 和 (p3, p4) 的交点。
  /// 使用线段交点的参数方程法。
  /// 如果线段平行、共线或交点在线段延长线上，则返回 `null`。
  Offset? _findIntersection(Offset p1, Offset p2, Offset p3, Offset p4) {
    final double s1X = p2.dx - p1.dx, s1Y = p2.dy - p1.dy; // 第一条线段的向量
    final double s2X = p4.dx - p3.dx, s2Y = p4.dy - p3.dy; // 第二条线段的向量
    final double denominator = -s2X * s1Y + s1X * s2Y; // 分母，用于判断平行或共线
    if (denominator == 0) return null; // 线段平行或共线，无唯一交点

    // 计算参数 s 和 t。
    // s 是交点在线段(p3, p4)上的比例位置。
    final double s = (-s1Y * (p1.dx - p3.dx) + s1X * (p1.dy - p3.dy)) / denominator;
    // t 是交点在线段(p1, p2)上的比例位置。
    final double t = (s2X * (p1.dy - p3.dy) - s2Y * (p1.dx - p3.dx)) / denominator;

    // 判断交点是否在线段内部，而不是延长线上。
    // `s` 和 `t` 值需要在 `minIntersectionDistance` 到 `1 - minIntersectionDistance` 之间，
    // 确保交点不靠近线段的端点，这样可以更好地排除偶然的端点接触。
    if (s > minIntersectionDistance &&
        s < (1 - minIntersectionDistance) &&
        t > minIntersectionDistance &&
        t < (1 - minIntersectionDistance)) {
      return Offset(p1.dx + (t * s1X), p1.dy + (t * s1Y)); // 计算并返回交点坐标
    }
    return null; // 交点在线段外部或不满足最小交点距离条件
  }
}
