// SPDX-FileCopyrightText: 2026 FocusFlow contributors
// SPDX-License-Identifier: AGPL-3.0-only

part of '../timeline_view.dart';

/// Actions exposed by the timeline context menu.
enum _TimelineMenuAction {
  /// Adds a generic flexible task to the timeline.
  addTask,
}

/// Private implementation type for `_TimelineViewState` in this library.
/// It keeps supporting state and behavior scoped to the owning file so the public API stays focused on the scheduler concepts that callers use directly.
class _TimelineViewState extends State<TimelineView> {
  /// Shared constant value for `_topInset`.
  /// The constant gives this repeated scheduler or UI value one documented source of truth instead of scattering literals across call sites.
  static const _topInset = 12.0;

  /// Private state stored as `_scrollController` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  final _scrollController = ScrollController();

  /// Private state stored as `_didSetInitialScroll` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  bool _didSetInitialScroll = false;

  /// Private state stored as `_handledScrollRequest` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  var _handledScrollRequest = 0;

  /// Private state stored as `_handledScrollToNowRequest` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  var _handledScrollToNowRequest = 0;

  /// Private state stored as `_geometry` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  late TimelineGeometry _geometry;

  /// Private state stored as `_contentHeight` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  late double _contentHeight;

  /// Private state stored as `_placements` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  late List<_TimelineCardPlacement> _placements;

  /// Private state stored as `_cardsLayoutHash` for this implementation.
  /// The field is intentionally scoped to this library so only the owning type can change the related scheduler or UI behavior.
  late int _cardsLayoutHash;

  /// Initializes state owned by this object before the first build.
  /// Use this lifecycle point for controller setup, initial reads, and listener wiring that must happen once per mounted instance.
  @override
  void initState() {
    super.initState();
    _updateLayoutCache();
  }

  /// Performs the `didUpdateWidget` behavior for this scheduler component.
  /// The method provides a named entry point for this operation so callers do not need to know the lower-level state or persistence steps involved.
  @override
  void didUpdateWidget(covariant TimelineView oldWidget) {
    super.didUpdateWidget(oldWidget);
    final rangeChanged =
        oldWidget.range.startMinutes != widget.range.startMinutes ||
        oldWidget.range.endMinutes != widget.range.endMinutes;
    if (rangeChanged) {
      _didSetInitialScroll = false;
    }
    final layoutChanged = _layoutHashFor(widget.cards) != _cardsLayoutHash;
    if (rangeChanged || layoutChanged) {
      _updateLayoutCache();
    } else if (!identical(oldWidget.cards, widget.cards)) {
      _refreshPlacementCards();
    }
  }

  /// Releases resources owned by this object before it leaves the tree.
  /// Controllers, listeners, and other disposable collaborators should be cleaned up here to avoid stale callbacks.
  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }

  /// Builds the widget subtree for this component.
  /// The method translates the current widget configuration and state into Flutter UI without mutating scheduler domain data.
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (context, constraints) {
        _scheduleInitialScroll(_geometry, constraints.maxHeight);
        _scheduleCurrentTimeScroll(_geometry, constraints.maxHeight);
        _scheduleTargetScroll(_geometry);
        return ScrollConfiguration(
          behavior: const _TimelineScrollBehavior(),
          child: GestureDetector(
            behavior: HitTestBehavior.translucent,
            onSecondaryTapDown: widget.onAddTask == null
                ? null
                : _showTimelineMenu,
            child: SingleChildScrollView(
              controller: _scrollController,
              child: SizedBox(
                height: _contentHeight,
                child: Row(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    RepaintBoundary(
                      child: TimelineAxis(
                        geometry: _geometry,
                        topInset: _topInset,
                      ),
                    ),
                    const SizedBox(width: 18),
                    Expanded(
                      child: LayoutBuilder(
                        builder: (context, cardConstraints) {
                          final trackWidth = cardConstraints.maxWidth;
                          return Stack(
                            clipBehavior: Clip.none,
                            children: [
                              Positioned.fill(
                                child: RepaintBoundary(
                                  child: TimelineGrid(
                                    geometry: _geometry,
                                    topInset: _topInset,
                                  ),
                                ),
                              ),
                              for (final placement in _placements)
                                Positioned(
                                  top: placement.top + _topInset,
                                  left: placement.left(trackWidth),
                                  width: placement.width(trackWidth),
                                  height: placement.height,
                                  child: RepaintBoundary(
                                    child: TaskTimelineCard(
                                      card: placement.card,
                                      onTap: () =>
                                          widget.onCardSelected(placement.card),
                                      onStatusPressed:
                                          widget.onCardCompleted == null
                                          ? null
                                          : () => widget.onCardCompleted!(
                                              placement.card,
                                            ),
                                      onPushToNext: widget.onPushToNext,
                                      onPushToTomorrow: widget.onPushToTomorrow,
                                      onPushToBacklog: widget.onPushToBacklog,
                                    ),
                                  ),
                                ),
                            ],
                          );
                        },
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        );
      },
    );
  }

  /// Shows the timeline context menu and dispatches the selected action.
  Future<void> _showTimelineMenu(TapDownDetails details) async {
    final overlay = Overlay.of(context).context.findRenderObject();
    if (overlay is! RenderBox) {
      return;
    }
    final position = RelativeRect.fromRect(
      details.globalPosition & Size.zero,
      Offset.zero & overlay.size,
    );
    final action = await showMenu<_TimelineMenuAction>(
      context: context,
      position: position,
      items: const [
        PopupMenuItem<_TimelineMenuAction>(
          value: _TimelineMenuAction.addTask,
          child: Text('Add new task'),
        ),
      ],
    );
    if (!mounted || action != _TimelineMenuAction.addTask) {
      return;
    }
    await widget.onAddTask?.call();
  }

  /// Runs the `_updateLayoutCache` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  void _updateLayoutCache() {
    _geometry = TimelineGeometry(
      startMinutes: widget.range.startMinutes,
      endMinutes: widget.range.endMinutes,
      pixelsPerMinute: 2.45,
    );
    _contentHeight = _geometry.totalHeight + _topInset + 24;
    _placements = _TimelineCardPlacement.pack(
      widget.cards,
      geometry: _geometry,
    );
    _cardsLayoutHash = _layoutHashFor(widget.cards);
  }

  /// Runs the `_layoutHashFor` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  int _layoutHashFor(List<TimelineCardModel> cards) {
    return Object.hashAll(
      cards.map(
        (card) => Object.hash(card.id, card.startMinutes, card.endMinutes),
      ),
    );
  }

  /// Runs the `_refreshPlacementCards` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  void _refreshPlacementCards() {
    final cardsById = {for (final card in widget.cards) card.id: card};
    _placements = [
      for (final placement in _placements)
        placement.withCard(cardsById[placement.card.id] ?? placement.card),
    ];
  }

  /// Runs the `_scheduleInitialScroll` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  void _scheduleInitialScroll(
    TimelineGeometry geometry,
    double viewportHeight,
  ) {
    if (_didSetInitialScroll || !viewportHeight.isFinite) {
      return;
    }
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted || !_scrollController.hasClients) {
        return;
      }
      final now = widget.now?.call() ?? DateTime.now();
      _jumpToCurrentTime(geometry, viewportHeight, now);
      _didSetInitialScroll = true;
    });
  }

  /// Runs the `_scheduleCurrentTimeScroll` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  void _scheduleCurrentTimeScroll(
    TimelineGeometry geometry,
    double viewportHeight,
  ) {
    if (widget.scrollToNowRequest == _handledScrollToNowRequest ||
        !viewportHeight.isFinite) {
      return;
    }
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted || !_scrollController.hasClients) {
        return;
      }
      final now = widget.now?.call() ?? DateTime.now();
      _jumpToCurrentTime(geometry, viewportHeight, now);
      _handledScrollToNowRequest = widget.scrollToNowRequest;
    });
  }

  /// Runs the `_scheduleTargetScroll` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  void _scheduleTargetScroll(TimelineGeometry geometry) {
    final targetCardId = widget.scrollTargetCardId;
    if (targetCardId == null || widget.scrollRequest == _handledScrollRequest) {
      return;
    }
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (!mounted || !_scrollController.hasClients) {
        return;
      }
      TimelineCardModel? targetCard;
      for (final card in widget.cards) {
        if (card.id == targetCardId) {
          targetCard = card;
          break;
        }
      }
      if (targetCard == null) {
        _handledScrollRequest = widget.scrollRequest;
        return;
      }
      final targetOffset =
          geometry.yForMinutesSinceMidnight(targetCard.startMinutes) +
          _topInset -
          24;
      final maxOffset = _scrollController.position.maxScrollExtent;
      final offset = targetOffset.clamp(0, maxOffset).toDouble();
      _scrollController.jumpTo(offset);
      _handledScrollRequest = widget.scrollRequest;
    });
  }

  /// Runs the `_jumpToCurrentTime` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  void _jumpToCurrentTime(
    TimelineGeometry geometry,
    double viewportHeight,
    DateTime now,
  ) {
    final currentMinute = now.hour * 60 + now.minute;
    final clampedMinute = currentMinute
        .clamp(geometry.startMinutes, geometry.endMinutes)
        .toInt();
    final maxOffset = _scrollController.position.maxScrollExtent;
    final offset = _scrollOffsetForMinute(
      geometry: geometry,
      minute: clampedMinute,
      viewportHeight: viewportHeight,
      maxOffset: maxOffset,
    );
    _scrollController.jumpTo(offset);
  }

  /// Runs the `_scrollOffsetForMinute` helper used inside this library.
  /// The helper centralizes a small piece of transformation or validation so higher-level scheduler flow remains easier to read.
  double _scrollOffsetForMinute({
    required TimelineGeometry geometry,
    required int minute,
    required double viewportHeight,
    required double maxOffset,
  }) {
    final targetOffset =
        geometry.yForMinutesSinceMidnight(minute) +
        _topInset -
        viewportHeight * 0.25;
    return targetOffset.clamp(0, maxOffset).toDouble();
  }
}
