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

import '../services/device_performance.dart';
import '../services/settings_service.dart';
import 'focus_theme.dart';

/// Renders the focus glow for a focused card in the root [Overlay] so it paints
/// ABOVE sibling cards on all four sides.
///
/// The glow is an outward blur ([FocusTheme.focusGlowShadows]). When drawn
/// in-tree behind a packed rail/grid it is occluded by later-painted neighbours
/// on the trailing edges and only escapes on the leading (left) edge — producing
/// a one-sided halo (issue #1231). Lifting it into an [OverlayPortal] that
/// follows the card via [LayerLink] makes it render above every sibling, so the
/// glow is symmetric on all sides. The crisp focus border stays in-card; only
/// the glow moves to the overlay.
///
/// Only mounts the overlay/leader while the card is focused (or fading out), so
/// there is at most one [LeaderLayer] on screen regardless of how many cards a
/// grid builds.
class FocusGlowOverlay extends StatefulWidget {
  const FocusGlowOverlay({
    super.key,
    required this.isFocused,
    required this.borderRadius,
    required this.color,
    required this.child,
    this.glowSize,
  });

  /// Whether the wrapped card currently shows focus. Drives the glow.
  final bool isFocused;

  /// Border radius of the card, used for the glow's rounded rect.
  final double borderRadius;

  /// Glow colour, resolved from the card's theme at the call site (the overlay
  /// builds in the root Overlay's context, which may not carry a nested theme).
  final Color color;

  /// Explicit card size. When null, falls back to [LayerLink.leaderSize] (one
  /// frame late on first show, hidden under the opacity-0 fade-in).
  final Size? glowSize;

  final Widget child;

  @override
  State<FocusGlowOverlay> createState() => _FocusGlowOverlayState();
}

class _FocusGlowOverlayState extends State<FocusGlowOverlay> {
  final OverlayPortalController _controller = OverlayPortalController();
  final LayerLink _link = LayerLink();

  /// Drives the [AnimatedOpacity] target. Set false on focus loss so the glow
  /// fades out before the portal is hidden in [_handleFadeEnd].
  bool _visible = false;

  /// Glow is skipped on the reduced effects tier (blurred shadows + fade
  /// saveLayer are too expensive on weak GPUs) and when the user turned the
  /// Focus Glow setting off (#1278). The crisp focus border remains.
  static bool get _disabled => DevicePerformance.isReduced || !SettingsService.instance.read(SettingsService.focusGlow);

  @override
  void initState() {
    super.initState();
    if (widget.isFocused && !_disabled) {
      _visible = true;
      _controller.show();
    }
  }

  @override
  void didUpdateWidget(FocusGlowOverlay oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (_disabled) return;
    if (widget.isFocused == oldWidget.isFocused) return;
    if (widget.isFocused) {
      _controller.show();
      // Start hidden, then fade in next frame.
      _visible = false;
      WidgetsBinding.instance.addPostFrameCallback((_) {
        if (mounted && widget.isFocused) setState(() => _visible = true);
      });
    } else {
      // Fade out; _handleFadeEnd hides the portal once opacity reaches 0.
      setState(() => _visible = false);
    }
  }

  void _handleFadeEnd() {
    if (!_visible && mounted && _controller.isShowing) {
      _controller.hide();
      setState(() {}); // drop the OverlayPortal/leader from the tree
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_disabled) return widget.child;

    // Gate the LeaderLayer to the focused card only: when not focused and not
    // mid-fade, return the bare child (no OverlayPortal, no leader).
    if (!widget.isFocused && !_controller.isShowing) {
      return widget.child;
    }

    final duration = FocusTheme.getAnimationDuration(context);

    return OverlayPortal(
      controller: _controller,
      overlayChildBuilder: (overlayContext) {
        final size = widget.glowSize ?? _link.leaderSize;
        final extent = FocusTheme.focusGlowExtent;

        return CompositedTransformFollower(
          link: _link,
          targetAnchor: Alignment.topLeft,
          followerAnchor: Alignment.topLeft,
          offset: Offset(-extent, -extent),
          child: IgnorePointer(
            child: AnimatedOpacity(
              opacity: _visible ? 1.0 : 0.0,
              duration: duration,
              curve: Curves.easeOutCubic,
              onEnd: _handleFadeEnd,
              child: size == null
                  ? const SizedBox.shrink()
                  : CustomPaint(
                      size: Size(size.width + extent * 2, size.height + extent * 2),
                      painter: _FocusGlowPainter(
                        rect: Offset(extent, extent) & size,
                        borderRadius: widget.borderRadius,
                        shadows: FocusTheme.focusGlowShadows(widget.color),
                      ),
                    ),
            ),
          ),
        );
      },
      child: CompositedTransformTarget(link: _link, child: widget.child),
    );
  }
}

/// Paints [shadows] around [rect] with the rect interior clipped out, so only
/// the outer glow shows — matching the original look where the opaque card hid
/// the inner part of the shadow.
class _FocusGlowPainter extends CustomPainter {
  const _FocusGlowPainter({required this.rect, required this.borderRadius, required this.shadows});

  final Rect rect;
  final double borderRadius;
  final List<BoxShadow> shadows;

  @override
  void paint(Canvas canvas, Size size) {
    final rrect = RRect.fromRectAndRadius(rect, Radius.circular(borderRadius));
    final clip = Path.combine(PathOperation.difference, Path()..addRect(Offset.zero & size), Path()..addRRect(rrect));
    canvas.save();
    canvas.clipPath(clip);
    for (final shadow in shadows) {
      canvas.drawRRect(rrect.shift(shadow.offset).inflate(shadow.spreadRadius), shadow.toPaint());
    }
    canvas.restore();
  }

  @override
  bool shouldRepaint(_FocusGlowPainter oldDelegate) {
    return oldDelegate.rect != rect ||
        oldDelegate.borderRadius != borderRadius ||
        !listEquals(oldDelegate.shadows, shadows);
  }
}
