import 'package:flutter/material.dart';

/// Displays a chat title that reveals characters with a streaming animation
/// whenever the title changes.
class StreamingTitleText extends StatefulWidget {
  const StreamingTitleText({
    super.key,
    required this.title,
    required this.style,
    this.cursorColor,
    this.cursorWidth = 2,
    this.cursorHeight,
    this.onAnimationComplete,
  });

  /// The title that should be rendered. When this value changes the widget
  /// replays the streaming animation.
  final String title;

  /// Text style used for the title.
  final TextStyle style;

  /// Optional cursor color while streaming. Defaults to the text color.
  final Color? cursorColor;

  /// Width of the animated cursor while the title is streaming.
  final double cursorWidth;

  /// Optional cursor height override. When null we use the font size's height.
  final double? cursorHeight;

  /// Optional callback fired after the streaming animation finishes.
  final VoidCallback? onAnimationComplete;

  @override
  State<StreamingTitleText> createState() => _StreamingTitleTextState();
}

class _StreamingTitleTextState extends State<StreamingTitleText>
    with TickerProviderStateMixin {
  late final AnimationController _revealController;
  late final AnimationController _cursorController;
  late Animation<double> _cursorOpacity;
  String _activeTitle = '';

  @override
  void initState() {
    super.initState();
    _activeTitle = widget.title;

    _revealController =
        AnimationController(vsync: this, duration: _durationFor(widget.title))
          ..addListener(() {
            setState(() {});
          })
          ..addStatusListener((status) {
            if (status == AnimationStatus.completed) {
              _cursorController.stop();
              widget.onAnimationComplete?.call();
            }
          });

    _cursorController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 700),
    );
    _cursorOpacity = TweenSequence<double>([
      TweenSequenceItem(
        tween: Tween<double>(
          begin: 0.0,
          end: 1.0,
        ).chain(CurveTween(curve: Curves.easeOutQuart)),
        weight: 50,
      ),
      TweenSequenceItem(
        tween: Tween<double>(
          begin: 1.0,
          end: 0.0,
        ).chain(CurveTween(curve: Curves.easeInQuart)),
        weight: 50,
      ),
    ]).animate(_cursorController);

    if (_activeTitle.isNotEmpty) {
      // Skip the animation when mounting with an existing title.
      _revealController.value = 1.0;
    }
  }

  @override
  void didUpdateWidget(covariant StreamingTitleText oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.title != widget.title) {
      _activeTitle = widget.title;
      _revealController.duration = _durationFor(widget.title);
      if (_activeTitle.isEmpty) {
        _revealController.value = 0.0;
        _cursorController.stop();
      } else {
        _cursorController
          ..reset()
          ..repeat(reverse: true);
        _revealController.forward(from: 0.0);
      }
    }
  }

  @override
  void dispose() {
    _cursorController.dispose();
    _revealController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_activeTitle.isEmpty) {
      return const SizedBox.shrink();
    }

    final characters = _activeTitle.characters;
    final int totalGlyphs = characters.length;
    final double clampedProgress = _revealController.value.clamp(0.0, 1.0);
    int revealedGlyphs = (clampedProgress * totalGlyphs).floor();
    if (revealedGlyphs > totalGlyphs) {
      revealedGlyphs = totalGlyphs;
    }
    final String visibleText = characters.take(revealedGlyphs).toString();

    final bool isAnimating =
        _revealController.isAnimating && revealedGlyphs < totalGlyphs;

    final Color cursorColor =
        widget.cursorColor ??
        widget.style.color ??
        Theme.of(context).colorScheme.primary;

    final double cursorHeight =
        widget.cursorHeight ??
        (widget.style.fontSize != null
            ? widget.style.fontSize! * (widget.style.height ?? 1.1)
            : 18.0);

    return Row(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
        Flexible(
          child: Text(
            // When the animation completes we fall back to the full string.
            revealedGlyphs >= totalGlyphs ? _activeTitle : visibleText,
            maxLines: 1,
            overflow: TextOverflow.fade,
            softWrap: false,
            textAlign: TextAlign.center,
            style: widget.style,
          ),
        ),
        if (isAnimating)
          FadeTransition(
            opacity: _cursorOpacity,
            child: Container(
              width: widget.cursorWidth,
              height: cursorHeight,
              margin: const EdgeInsets.only(left: 2),
              decoration: BoxDecoration(
                color: cursorColor,
                borderRadius: BorderRadius.circular(widget.cursorWidth),
              ),
            ),
          ),
      ],
    );
  }

  Duration _durationFor(String value) {
    if (value.isEmpty) {
      return const Duration(milliseconds: 200);
    }
    final int glyphs = value.characters.length;
    final int millis = (glyphs * 28).clamp(360, 1400).toInt();
    return Duration(milliseconds: millis);
  }
}
