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

import 'pm_node.dart';
import 'pm_render_scope.dart';

/// Renders a run of inline nodes ([PmText] / [PmHardBreak]) as a single
/// `Text.rich`, composing marks into `TextStyle`s.
///
/// Link marks need a [TapGestureRecognizer], which must be disposed to avoid
/// leaks. The recognizers are therefore built in [didChangeDependencies] /
/// [didUpdateWidget] and released in [dispose] — never allocated inside
/// [build].
class PmRichText extends StatefulWidget {
  final List<PmNode> inlines;
  final TextStyle baseStyle;
  final TextAlign textAlign;

  const PmRichText({
    required this.inlines,
    required this.baseStyle,
    this.textAlign = TextAlign.start,
    super.key,
  });

  @override
  State<PmRichText> createState() => _PmRichTextState();
}

class _PmRichTextState extends State<PmRichText> {
  final List<TapGestureRecognizer> _recognizers = [];
  InlineSpan _span = const TextSpan();

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _rebuild();
  }

  @override
  void didUpdateWidget(PmRichText oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.inlines != widget.inlines ||
        oldWidget.baseStyle != widget.baseStyle ||
        oldWidget.textAlign != widget.textAlign) {
      _rebuild();
    }
  }

  @override
  void dispose() {
    _disposeRecognizers();
    super.dispose();
  }

  void _disposeRecognizers() {
    for (final recognizer in _recognizers) {
      recognizer.dispose();
    }
    _recognizers.clear();
  }

  void _rebuild() {
    _disposeRecognizers();
    final theme = Theme.of(context);
    final onLinkTap = PmRenderScope.maybeOf(context)?.onLinkTap;
    final spans = <InlineSpan>[];
    for (final node in widget.inlines) {
      if (node is PmHardBreak) {
        spans.add(const TextSpan(text: '\n'));
      } else if (node is PmText) {
        spans.add(_textSpan(node, theme, onLinkTap));
      }
    }
    _span = TextSpan(style: widget.baseStyle, children: spans);
  }

  TextSpan _textSpan(
    PmText node,
    ThemeData theme,
    void Function(String href)? onLinkTap,
  ) {
    String? href;
    final style = _styleForMarks(
      node.marks,
      widget.baseStyle,
      theme,
      onLink: (value) => href = value,
    );
    TapGestureRecognizer? recognizer;
    if (href != null && onLinkTap != null) {
      final target = href!;
      recognizer = TapGestureRecognizer()..onTap = () => onLinkTap(target);
      _recognizers.add(recognizer);
    }
    return TextSpan(text: node.text, style: style, recognizer: recognizer);
  }

  TextStyle _styleForMarks(
    List<PmMark> marks,
    TextStyle base,
    ThemeData theme, {
    required void Function(String href) onLink,
  }) {
    var style = base;
    final decorations = <TextDecoration>[];
    for (final mark in marks) {
      switch (mark.type) {
        case 'bold':
          style = style.copyWith(fontWeight: FontWeight.bold);
        case 'italic':
          style = style.copyWith(fontStyle: FontStyle.italic);
        case 'strike':
          decorations.add(TextDecoration.lineThrough);
        case 'underline':
          decorations.add(TextDecoration.underline);
        case 'code':
          style = style.copyWith(
            fontFamily: 'monospace',
            backgroundColor: theme.colorScheme.surfaceContainerHighest,
          );
        case 'highlight':
          style = style.copyWith(
            backgroundColor: _highlightColor(mark.attrs['color'], theme),
          );
        case 'textStyle':
          final size = _fontSize(mark.attrs['fontSize'], base.fontSize ?? 14);
          if (size != null) style = style.copyWith(fontSize: size);
        case 'link':
          final rawHref = mark.attrs['href'];
          if (rawHref is String && rawHref.isNotEmpty) {
            onLink(rawHref);
            style = style.copyWith(color: theme.colorScheme.primary);
            decorations.add(TextDecoration.underline);
          }
      }
    }
    if (decorations.isNotEmpty) {
      style = style.copyWith(decoration: TextDecoration.combine(decorations));
    }
    return style;
  }

  Color _highlightColor(dynamic color, ThemeData theme) {
    final parsed = color is String ? _parseCssColor(color) : null;
    final base = parsed ?? theme.colorScheme.tertiaryContainer;
    // Full-opacity highlights swamp the text in dark mode, so dial the alpha
    // down further there while keeping the accent readable in light mode.
    final dark = theme.brightness == Brightness.dark;
    return base.withValues(alpha: dark ? 0.30 : 0.45);
  }

  double? _fontSize(dynamic raw, double base) {
    if (raw is! String) return null;
    final match = RegExp(r'^(\d+(?:\.\d+)?)(px|rem|em|%)$').firstMatch(raw);
    if (match == null) return null;
    final value = double.parse(match.group(1)!);
    final double size;
    switch (match.group(2)) {
      case 'px':
        size = value;
      case 'rem':
        size = value * 16;
      case 'em':
        size = base * value;
      case '%':
        size = base * value / 100;
      default:
        return null;
    }
    return size.clamp(8, 72);
  }

  @override
  Widget build(BuildContext context) {
    final softWrap = PmCellTextFlow.softWrapOf(context);
    return Text.rich(
      _span,
      textAlign: widget.textAlign,
      softWrap: softWrap,
      overflow: softWrap ? TextOverflow.clip : TextOverflow.visible,
    );
  }
}

/// Parses a CSS `#rgb`/`#rrggbb`/`#rrggbbaa` hex or a small set of named colors.
/// Returns `null` for anything unrecognised so the caller can fall back.
Color? _parseCssColor(String raw) {
  final value = raw.trim().toLowerCase();
  if (value.startsWith('#')) {
    var hex = value.substring(1);
    if (hex.length == 3) {
      hex = hex.split('').map((c) => '$c$c').join();
    }
    if (hex.length == 6) hex = 'ff$hex';
    if (hex.length == 8) {
      final rgba = int.tryParse(hex, radix: 16);
      if (rgba != null) return Color(rgba);
    }
    return null;
  }
  return _namedColors[value];
}

const _namedColors = <String, Color>{
  'red': Color(0xFFEF4444),
  'orange': Color(0xFFF97316),
  'yellow': Color(0xFFFACC15),
  'green': Color(0xFF22C55E),
  'blue': Color(0xFF3B82F6),
  'purple': Color(0xFFA855F7),
  'pink': Color(0xFFEC4899),
  'gray': Color(0xFF9CA3AF),
  'grey': Color(0xFF9CA3AF),
};
