// ===========================================================================
// FILE: lib/ui/game/board_painter.dart
// ===========================================================================

import 'dart:math';
import 'package:flutter/material.dart';

import '../../models/game_board.dart';
import '../../core/app_colors.dart';

class BoardPainter extends CustomPainter {
  final GameBoard board;
  final ThemeColors theme;
  final AppThemeType themeType;
  final double blinkValue;

  final bool isOnline;
  final bool isVsCPU;
  final bool isSetupPhase;
  final Player myPlayer;
  final Player jokerTurn;

  BoardPainter({
    required this.board,
    required this.theme,
    required this.themeType,
    required this.isOnline,
    required this.isVsCPU,
    required this.isSetupPhase,
    required this.myPlayer,
    required this.jokerTurn,
    this.blinkValue = 0.0
  });

  @override
  void paint(Canvas canvas, Size size) {
    if (themeType == AppThemeType.doodle) {
      final Paint paperGridPaint = Paint()
        ..color = Colors.grey.withOpacity(0.3)
        ..strokeWidth = 1.0
        ..style = PaintingStyle.stroke;

      double paperStep = 20.0;
      for (double i = 0; i <= size.width; i += paperStep) {
        canvas.drawLine(Offset(i, 0), Offset(i, size.height), paperGridPaint);
      }
      for (double i = 0; i <= size.height; i += paperStep) {
        canvas.drawLine(Offset(0, i), Offset(size.width, i), paperGridPaint);
      }
    }

    int gridPoints = board.columns + 1;
    double spacing = size.width / gridPoints;
    double offset = spacing / 2;
    Offset getScreenPos(int x, int y) => Offset(x * spacing + offset, y * spacing + offset);

    for (var box in board.boxes) {
      Offset p1 = getScreenPos(box.x, box.y);
      Offset p2 = getScreenPos(box.x + 1, box.y + 1);
      Rect rect = Rect.fromPoints(p1, p2);

      if (box.type == BoxType.invisible) {
        if (box.isRevealed) {
          _drawIconInBox(canvas, rect, ThemeIcons.block(themeType), Colors.grey.shade500);
        }
        continue;
      }

      // Sfondo azzurrino se è di ghiaccio (anche prima di chiuderla)
      if (box.type == BoxType.ice && box.owner == Player.none) {
        canvas.drawRect(rect.deflate(2.0), Paint()..color = Colors.cyanAccent.withOpacity(0.05)..style=PaintingStyle.fill);
      }

      if (box.owner != Player.none) {
        final boxPaint = Paint()
          ..style = PaintingStyle.fill
          ..color = box.owner == Player.red ? theme.playerRed.withOpacity(0.6) : theme.playerBlue.withOpacity(0.6);

        if (themeType == AppThemeType.wood) {
          _drawFlameBox(canvas, rect, box.owner == Player.red);
        } else if (themeType == AppThemeType.doodle) {
          Color penColor = box.owner == Player.red ? Colors.redAccent.shade700 : Colors.blueAccent.shade700;
          _drawScribbleBox(canvas, rect, penColor);
        } else if (themeType == AppThemeType.arcade) {
          _drawArcadeBox(canvas, rect, box.owner == Player.red ? theme.playerRed : theme.playerBlue);
        } else if (themeType == AppThemeType.grimorio) {
          _drawGrimorioBox(canvas, rect, box.owner == Player.red ? theme.playerRed : theme.playerBlue);
        } else {
          canvas.drawRect(rect, boxPaint);
        }
      }

      if (box.hiddenJokerOwner != null) {
        Color jokerColor = box.hiddenJokerOwner == Player.red ? theme.playerRed : theme.playerBlue;

        if (box.isJokerRevealed) {
          _drawIconInBox(canvas, rect, ThemeIcons.joker(themeType), jokerColor);
        } else {
          bool canSee = false;
          if (isOnline || isVsCPU) {
            canSee = box.hiddenJokerOwner == myPlayer;
          } else {
            canSee = false;
          }
          if (canSee) {
            _drawIconInBox(canvas, rect, ThemeIcons.joker(themeType), jokerColor.withOpacity(0.3));
          }
        }
      }

      if (box.type == BoxType.gold) {
        _drawIconInBox(canvas, rect, ThemeIcons.gold(themeType), Colors.amber);
      } else if (box.type == BoxType.bomb) {
        _drawIconInBox(canvas, rect, ThemeIcons.bomb(themeType), themeType == AppThemeType.cyberpunk || themeType == AppThemeType.arcade ? Colors.greenAccent : Colors.deepPurple);
      } else if (box.type == BoxType.swap) {
        _drawIconInBox(canvas, rect, ThemeIcons.swap(themeType), Colors.purpleAccent);
      } else if (box.type == BoxType.ice) {
        _drawIconInBox(canvas, rect, ThemeIcons.ice(themeType), Colors.cyanAccent);
      } else if (box.type == BoxType.multiplier) {
        _drawIconInBox(canvas, rect, ThemeIcons.multiplier(themeType), Colors.yellowAccent);
      }
    }

    for (var line in board.lines) {
      if (!line.isPlayable) continue;

      Offset p1 = getScreenPos(line.p1.x, line.p1.y);
      Offset p2 = getScreenPos(line.p2.x, line.p2.y);

      // --- DISEGNO DELLA LINEA "INCRINATA" DAL GHIACCIO ---
      if (line.isIceCracked) {
        _drawCrackedIceLine(canvas, p1, p2, blinkValue);
        continue; // Non ha ancora un proprietario, passiamo alla prossima!
      }

      bool isLastMove = (line == board.lastMove);
      Color lineColor = line.owner == Player.none
          ? theme.gridLine.withOpacity(0.4)
          : (line.owner == Player.red ? theme.playerRed : theme.playerBlue);

      if (isLastMove && line.owner != Player.none && themeType != AppThemeType.wood && themeType != AppThemeType.cyberpunk && themeType != AppThemeType.arcade && themeType != AppThemeType.grimorio) {
        canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(blinkValue * 0.5)..strokeWidth = 16.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0));
      }

      if (themeType == AppThemeType.wood) {
        if (line.owner == Player.none) {
          canvas.drawLine(p1, p2, Paint()..color = const Color(0xFF3E2723).withOpacity(0.3)..strokeWidth = 4.5..strokeCap = StrokeCap.round);
        } else {
          Color headColor = lineColor;
          if (isLastMove) headColor = Color.lerp(headColor, Colors.yellow, blinkValue * 0.8) ?? headColor;
          _drawRealisticMatch(canvas, p1, p2, headColor, isLastMove: isLastMove, blinkValue: blinkValue);
        }
      } else if (themeType == AppThemeType.cyberpunk) {
        _drawNeonLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
      } else if (themeType == AppThemeType.doodle) {
        Color doodleColor = line.owner == Player.none ? Colors.black.withOpacity(0.05) : lineColor;
        if (isLastMove && line.owner != Player.none) doodleColor = Color.lerp(doodleColor, Colors.black, blinkValue * 0.4) ?? doodleColor;
        _drawWobblyLine(canvas, p1, p2, doodleColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
      } else if (themeType == AppThemeType.arcade) {
        _drawArcadeLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
      } else if (themeType == AppThemeType.grimorio) {
        _drawGrimorioLine(canvas, p1, p2, lineColor, line.owner != Player.none, isLastMove: isLastMove, blinkValue: blinkValue);
      } else {
        if (isLastMove && line.owner != Player.none) lineColor = Color.lerp(lineColor, Colors.white, blinkValue * 0.5) ?? lineColor;
        canvas.drawLine(p1, p2, Paint()..color = lineColor..strokeWidth = isLastMove ? 6.0 + (2.0 * blinkValue) : 6.0..strokeCap = StrokeCap.round);
      }
    }

    final dotPaint = Paint()..style = PaintingStyle.fill;
    Set<Dot> activeDots = {};
    for (var line in board.lines) {
      if (line.isPlayable) {
        activeDots.add(line.p1); activeDots.add(line.p2);
      }
    }

    for (var dot in activeDots) {
      Offset pos = getScreenPos(dot.x, dot.y);
      if (themeType == AppThemeType.wood) {
        canvas.drawCircle(pos, 3.5, dotPaint..color = const Color(0xFF3E2723).withOpacity(0.2));
      } else if (themeType == AppThemeType.cyberpunk) {
        canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3));
        canvas.drawCircle(pos, 3.0, Paint()..color = Colors.white.withOpacity(0.5));
      } else if (themeType == AppThemeType.doodle) {
        canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = Colors.black.withOpacity(0.25));
      } else if (themeType == AppThemeType.arcade) {
        canvas.drawRect(Rect.fromCenter(center: pos, width: 8, height: 8), dotPaint..color = theme.gridLine.withOpacity(0.9));
        canvas.drawRect(Rect.fromCenter(center: pos, width: 4, height: 4), dotPaint..color = theme.background);
      } else if (themeType == AppThemeType.grimorio) {
        canvas.drawCircle(pos, 6.0, Paint()..color = theme.gridLine.withOpacity(0.3)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3.0));
        Path crystal = Path()..moveTo(pos.dx, pos.dy - 5)..lineTo(pos.dx + 3, pos.dy)..lineTo(pos.dx, pos.dy + 5)..lineTo(pos.dx - 3, pos.dy)..close();
        canvas.drawPath(crystal, dotPaint..color = theme.gridLine.withOpacity(0.8));
      } else {
        canvas.drawCircle(pos, 5.0, dotPaint..color = theme.text.withOpacity(0.6));
      }
    }
  }

  void _drawIconInBox(Canvas canvas, Rect rect, IconData icon, Color color) {
    TextPainter textPainter = TextPainter(textDirection: TextDirection.ltr);
    textPainter.text = TextSpan(
      text: String.fromCharCode(icon.codePoint),
      style: TextStyle(
          color: themeType == AppThemeType.arcade ? color : color.withOpacity(0.7),
          fontSize: rect.width * 0.45,
          fontFamily: icon.fontFamily,
          package: icon.fontPackage,
          shadows: themeType == AppThemeType.arcade ? [] : [Shadow(color: color.withOpacity(0.6), blurRadius: 10, offset: const Offset(0, 0))]
      ),
    );
    textPainter.layout();
    textPainter.paint(canvas, Offset(rect.center.dx - textPainter.width / 2, rect.center.dy - textPainter.height / 2));
  }

  void _drawCrackedIceLine(Canvas canvas, Offset p1, Offset p2, double blink) {
    Paint crackPaint = Paint()
      ..color = Colors.cyanAccent.withOpacity(0.6 + (0.4 * blink))
      ..strokeWidth = 3.0
      ..style = PaintingStyle.stroke
      ..strokeCap = StrokeCap.round
      ..maskFilter = const MaskFilter.blur(BlurStyle.solid, 2.0);

    // Effetto linea frammentata
    canvas.drawLine(p1, p2, Paint()..color = Colors.cyan.withOpacity(0.2)..strokeWidth=6.0);

    Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy);
    double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x);

    Path crack = Path()..moveTo(p1.dx, p1.dy);
    int zigzags = 6;
    for (int i=1; i<zigzags; i++) {
      double d = len * (i / zigzags);
      Offset basePt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
      double offset = (i % 2 == 0 ? 3.0 : -3.0);
      crack.lineTo(basePt.dx + perp.x * offset, basePt.dy + perp.y * offset);
    }
    crack.lineTo(p2.dx, p2.dy);
    canvas.drawPath(crack, crackPaint);
  }

  void _drawArcadeBox(Canvas canvas, Rect rect, Color color) {
    double pixelSize = 4.0; Paint paint = Paint()..color = color.withOpacity(0.9)..style = PaintingStyle.fill;
    for (double y = rect.top; y < rect.bottom; y += pixelSize) {
      for (double x = rect.left; x < rect.right; x += pixelSize) {
        int xi = ((x - rect.left) / pixelSize).floor(); int yi = ((y - rect.top) / pixelSize).floor();
        if ((xi + yi) % 2 == 0) canvas.drawRect(Rect.fromLTWH(x, y, pixelSize, pixelSize), paint);
      }
    }
    canvas.drawRect(rect.deflate(2.0), Paint()..color = Colors.white.withOpacity(0.4)..style = PaintingStyle.stroke..strokeWidth = 2.0);
  }

  void _drawGrimorioBox(Canvas canvas, Rect rect, Color color) {
    canvas.drawRect(rect, Paint()..color = color.withOpacity(0.15)..style=PaintingStyle.fill);
    Offset c = rect.center; double r = rect.width * 0.35;
    Paint linePaint = Paint()..color = color.withOpacity(0.8)..style = PaintingStyle.stroke..strokeWidth = 1.5..maskFilter = const MaskFilter.blur(BlurStyle.solid, 1.0);
    canvas.drawCircle(c, r, linePaint); canvas.drawCircle(c, r * 0.8, linePaint..strokeWidth = 0.5);
    Path p = Path();
    for(int i=0; i<3; i++) {
      double a = -pi/2 + i * 2*pi/3; Offset pt = Offset(c.dx + r*cos(a), c.dy + r*sin(a));
      if(i==0) p.moveTo(pt.dx, pt.dy); else p.lineTo(pt.dx, pt.dy);
    }
    p.close(); canvas.drawPath(p, linePaint..strokeWidth = 1.0);
  }

  void _drawArcadeLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
    double pixelSize = 6.0; Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); double len = dir.length; Vector2 ndir = dir.normalized();
    Paint paint = Paint()..color = isConquered ? color : color.withOpacity(0.15)..style = PaintingStyle.fill;
    Paint highlight = Paint()..color = Colors.white.withOpacity(0.6)..style = PaintingStyle.fill;
    for(double d = 0; d <= len; d += pixelSize + 1.0) {
      Offset pt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
      canvas.drawRect(Rect.fromCenter(center: pt, width: pixelSize, height: pixelSize), paint);
      if (isConquered && (d / (pixelSize+1.0)).floor() % 3 == 0) canvas.drawRect(Rect.fromCenter(center: pt - const Offset(1,1), width: pixelSize*0.4, height: pixelSize*0.4), highlight);
    }
    if (isLastMove && isConquered) canvas.drawRect(Rect.fromPoints(p1, p2).inflate(4.0), Paint()..color = Colors.white.withOpacity(blinkValue*0.4)..style=PaintingStyle.stroke..strokeWidth=2.0);
  }

  void _drawGrimorioLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
    if (!isConquered) { canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(0.15)..strokeWidth = 2.0..strokeCap = StrokeCap.round); return; }
    canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(0.6)..strokeWidth = 5.0..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
    canvas.drawLine(p1, p2, Paint()..color = Colors.white.withOpacity(0.7)..strokeWidth = 1.5..strokeCap = StrokeCap.round);
    int seed = (p1.dx * 1000 + p1.dy).toInt(); Random rand = Random(seed);
    Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy); double len = dir.length; Vector2 ndir = dir.normalized(); Vector2 perp = Vector2(-ndir.y, ndir.x);
    Path thread1 = Path(); Path thread2 = Path(); int segments = 15; double step = len / segments;
    double phaseOffset = (isLastMove ? blinkValue * pi * 4 : 0) + rand.nextDouble()*pi;
    for(int i = 0; i <= segments; i++) {
      double d = i * step; Offset basePt = Offset(p1.dx + ndir.x * d, p1.dy + ndir.y * d);
      double amplitude = 3.5; double wave1 = sin(d * 0.15 + phaseOffset) * amplitude; double wave2 = cos(d * 0.15 + phaseOffset) * amplitude;
      Offset pt1 = basePt + Offset(perp.x * wave1, perp.y * wave1); Offset pt2 = basePt + Offset(perp.x * wave2, perp.y * wave2);
      if (i == 0) { thread1.moveTo(pt1.dx, pt1.dy); thread2.moveTo(pt2.dx, pt2.dy); } else { thread1.lineTo(pt1.dx, pt1.dy); thread2.lineTo(pt2.dx, pt2.dy); }
    }
    Paint threadPaint = Paint()..color = color.withOpacity(0.9)..style = PaintingStyle.stroke..strokeWidth = 1.5..maskFilter = const MaskFilter.blur(BlurStyle.solid, 1.0);
    canvas.drawPath(thread1, threadPaint); canvas.drawPath(thread2, threadPaint..color = Colors.white.withOpacity(0.5));
  }

  void _drawFlameBox(Canvas canvas, Rect baseRect, bool isRed) {
    final rand = Random((baseRect.left + baseRect.top).toInt());
    Offset center = baseRect.center; double w = baseRect.width * 0.35; double h = baseRect.height * 0.55; Offset bottomCenter = Offset(center.dx, center.dy + h * 0.5);
    Color outerColor = isRed ? Colors.red.shade600.withOpacity(0.85) : Colors.blue.shade700.withOpacity(0.85); Color midColor = isRed ? Colors.orangeAccent : Colors.lightBlueAccent; Color coreColor = isRed ? Colors.yellowAccent : Colors.white;
    canvas.drawOval(Rect.fromCenter(center: bottomCenter, width: w * 1.5, height: w * 0.5), Paint()..color = Colors.black.withOpacity(0.4)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 4.0));
    void drawFlameLayer(double scale, Color color, double tipOffsetX) {
      Path path = Path(); double fw = w * scale; double fh = h * scale;
      path.moveTo(bottomCenter.dx, bottomCenter.dy); path.cubicTo(bottomCenter.dx + fw, bottomCenter.dy, bottomCenter.dx + fw * 0.8, bottomCenter.dy - fh * 0.6, bottomCenter.dx + tipOffsetX, bottomCenter.dy - fh); path.cubicTo(bottomCenter.dx - fw * 0.8, bottomCenter.dy - fh * 0.6, bottomCenter.dx - fw, bottomCenter.dy, bottomCenter.dx, bottomCenter.dy);
      canvas.drawPath(path, Paint()..color = color..style = PaintingStyle.fill..maskFilter = const MaskFilter.blur(BlurStyle.normal, 1.5));
    }
    double randomTipX = (rand.nextDouble() - 0.5) * w * 0.8; drawFlameLayer(1.0, outerColor, randomTipX); drawFlameLayer(0.65, midColor.withOpacity(0.9), randomTipX * 0.6); drawFlameLayer(0.35, coreColor.withOpacity(0.9), randomTipX * 0.2);
  }

  void _drawScribbleBox(Canvas canvas, Rect baseRect, Color color) {
    final rand = Random((baseRect.left + baseRect.top).toInt());
    final paint = Paint()..color = color.withOpacity(0.85)..style = PaintingStyle.stroke..strokeWidth = 3.5..strokeCap = StrokeCap.round..strokeJoin = StrokeJoin.round;
    final path = Path(); Rect rect = baseRect.deflate(4.0); int numZigs = 15 + rand.nextInt(6); double stepY = rect.height / numZigs;
    path.moveTo(rect.left + rand.nextDouble() * 5, rect.top + rand.nextDouble() * 5);
    for (int i = 1; i <= numZigs; i++) { double targetX = (i % 2 != 0) ? rect.right + (rand.nextDouble() * 4 - 2) : rect.left + (rand.nextDouble() * 4 - 2); double targetY = rect.top + stepY * i + (rand.nextDouble() - 0.5) * 3; double ctrlX = rect.center.dx + (rand.nextDouble() - 0.5) * 20; double ctrlY = targetY - stepY / 2; path.quadraticBezierTo(ctrlX, ctrlY, targetX, targetY); }
    canvas.drawPath(path, paint);
  }

  void _drawRealisticMatch(Canvas canvas, Offset p1, Offset p2, Color headColor, {bool isLastMove = false, double blinkValue = 0.0}) {
    int seed = (p1.dx * 1000 + p1.dy).toInt(); Random rand = Random(seed); Vector2 dir = Vector2(p2.dx - p1.dx, p2.dy - p1.dy).normalized(); double shrink = 8.0; Offset start = Offset(p1.dx + dir.x * shrink, p1.dy + dir.y * shrink); Offset end = Offset(p2.dx - dir.x * shrink, p2.dy - dir.y * shrink); start += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2); end += Offset(rand.nextDouble() * 4 - 2, rand.nextDouble() * 4 - 2); bool headAtEnd = rand.nextBool(); Offset headPos = headAtEnd ? end : start; Offset tailPos = headAtEnd ? start : end; Vector2 matchDir = Vector2(headPos.dx - tailPos.dx, headPos.dy - tailPos.dy).normalized();
    canvas.drawLine(tailPos + const Offset(4, 4), headPos + const Offset(4, 4), Paint()..color = Colors.black.withOpacity(0.6)..strokeWidth = 7.0..strokeCap = StrokeCap.round);
    if (isLastMove) { canvas.drawCircle(headPos, 8.0 + (blinkValue * 6.0), Paint()..color = Colors.orangeAccent.withOpacity(0.6 * blinkValue)..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)); }
    canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFF6D4C41)..strokeWidth = 7.0..strokeCap = StrokeCap.round); canvas.drawLine(tailPos, headPos, Paint()..color = const Color(0xFFEDC498)..strokeWidth = 4.0..strokeCap = StrokeCap.round); Offset burnPos = Offset(headPos.dx - matchDir.x * 8, headPos.dy - matchDir.y * 8); canvas.drawLine(burnPos, headPos, Paint()..color = const Color(0xFF2E1A14)..strokeWidth = 6.0..strokeCap = StrokeCap.round);
    canvas.save(); canvas.translate(headPos.dx, headPos.dy); double angle = atan2(matchDir.y, matchDir.x); canvas.rotate(angle); Rect headOval = Rect.fromCenter(center: Offset.zero, width: 18.0, height: 13.0); canvas.drawOval(headOval.shift(const Offset(1, 2)), Paint()..color = Colors.black.withOpacity(0.6)); canvas.drawOval(headOval, Paint()..color = headColor); canvas.restore();
  }

  void _drawNeonLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
    double mainWidth = isConquered ? (isLastMove ? 6.0 + (blinkValue * 3.0) : 6.0) : 3.0; Color coreColor = isConquered ? (isLastMove ? Color.lerp(Colors.white, color, 1.0 - blinkValue)! : Colors.white.withOpacity(0.9)) : color.withOpacity(0.6);
    canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(isConquered ? (isLastMove ? 0.4 + (0.4 * blinkValue) : 0.4) : 0.2)..strokeWidth = mainWidth * 4..strokeCap = StrokeCap.round..maskFilter = MaskFilter.blur(BlurStyle.normal, isConquered ? 12.0 : 6.0));
    if (isConquered) { canvas.drawLine(p1, p2, Paint()..color = color.withOpacity(isLastMove ? 0.7 + (0.3 * blinkValue) : 0.7)..strokeWidth = mainWidth * 2..strokeCap = StrokeCap.round..maskFilter = const MaskFilter.blur(BlurStyle.normal, 6.0)); }
    canvas.drawLine(p1, p2, Paint()..color = coreColor..strokeWidth = mainWidth..strokeCap = StrokeCap.round);
  }

  void _drawWobblyLine(Canvas canvas, Offset p1, Offset p2, Color color, bool isConquered, {bool isLastMove = false, double blinkValue = 0.0}) {
    final random = Random((p1.dx + p1.dy + p2.dx + p2.dy).toInt()); final dx = p2.dx - p1.dx; final dy = p2.dy - p1.dy;
    double strokeW = isConquered ? (isLastMove ? 4.5 + (2.0 * blinkValue) : 4.5) : 2.0;
    final basePaint = Paint()..color = color..strokeWidth = strokeW..style = PaintingStyle.stroke..strokeCap = StrokeCap.round;
    final mid1 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 8, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 8); canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid1.dx, mid1.dy, p2.dx, p2.dy), basePaint);
    final mid2 = Offset(p1.dx + dx / 2 + (random.nextDouble() - 0.5) * 6, p1.dy + dy / 2 + (random.nextDouble() - 0.5) * 6); canvas.drawPath(Path()..moveTo(p1.dx, p1.dy)..quadraticBezierTo(mid2.dx, mid2.dy, p2.dx, p2.dy), basePaint..strokeWidth = strokeW * 0.5..color = color.withOpacity(0.8));
  }

  @override bool shouldRepaint(covariant BoardPainter oldDelegate) => true;
}

class Vector2 {
  final double x, y; Vector2(this.x, this.y); double get length => sqrt(x * x + y * y);
  Vector2 normalized() { double l = length; return l == 0 ? Vector2(0, 0) : Vector2(x / l, y / l); }
}