import 'dart:async';

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

import 'package:intl/intl.dart' as intl;

import 'basic_day_based_widget.dart';
import 'date_picker_keys.dart';
import 'day_based_changeable_picker_presenter.dart';
import 'day_picker_selection.dart';
import 'i_selectable_picker.dart';
import 'month_navigation_row.dart';
import 'semantic_sorting.dart';
import 'styles/date_picker_styles.dart';
import 'styles/event_decoration.dart';
import 'styles/layout_settings.dart';
import 'typedefs.dart';
import 'utils.dart';

const Locale _defaultLocale = Locale('en', 'US');

/// Date picker based on [DayBasedPicker] picker (for days, weeks, ranges).
/// Allows select previous/next month.
class DayBasedChangeablePicker<T> extends StatefulWidget {
  /// The currently selected date.
  ///
  /// This date is highlighted in the picker.
  final DayPickerSelection? selection;

  /// Called when the user picks a new T.
  final ValueChanged<T> onChanged;

  /// Called when the error was thrown after user selection.
  final OnSelectionError? onSelectionError;

  /// The earliest date the user is permitted to pick.
  final DateTime? firstDate;

  /// The latest date the user is permitted to pick.
  final DateTime? lastDate;

  /// Date for defining what month should be shown initially.
  ///
  /// Default value is [selection.earliest] for non empty selection
  /// and [DateTime.now()] for empty selection.
  final DateTime? initiallyShowDate;

  /// Layout settings what can be customized by user
  final DatePickerLayoutSettings? datePickerLayoutSettings;

  /// Styles what can be customized by user
  final DatePickerRangeStyles? datePickerStyles;

  /// Some keys useful for integration tests
  final DatePickerKeys? datePickerKeys;

  /// Logic for date selections.
  final ISelectablePicker<T>? selectablePicker;

  /// Builder to get event decoration for each date.
  ///
  /// For selected days all event styles are overridden by selected styles.
  final EventDecorationBuilder? eventDecorationBuilder;

  /// Called when the user changes the month
  final ValueChanged<DateTime>? onMonthChanged;

  /// Create picker with option to change month.
  DayBasedChangeablePicker({
    Key? key,
    required this.selection,
    required this.onChanged,
    required this.firstDate,
    required this.lastDate,
    required this.datePickerLayoutSettings,
    required this.datePickerStyles,
    required this.selectablePicker,
    DateTime? initiallyShownDate,
    this.datePickerKeys,
    this.onSelectionError,
    this.eventDecorationBuilder,
    this.onMonthChanged,
  })  : initiallyShowDate =
            _getInitiallyShownDate(initiallyShownDate!, selection!),
        super(key: key);

  @override
  State<DayBasedChangeablePicker<T>> createState() =>
      _DayBasedChangeablePickerState<T>();
}

// todo: Check initial selection and call onSelectionError in case it has error
// todo: (ISelectablePicker.curSelectionIsCorrupted);
class _DayBasedChangeablePickerState<T>
    extends State<DayBasedChangeablePicker<T>> {
  DateTime _todayDate = DateTime.now();

  Locale curLocale = _defaultLocale;

  MaterialLocalizations localizations = _defaultLocalizations;

  PageController _dayPickerController = PageController();

  // Styles from widget fulfilled with current Theme.
  DatePickerRangeStyles _resultStyles = DatePickerRangeStyles();

  DayBasedChangeablePickerPresenter _presenter = _defaultPresenter;

  late Timer _timer;
  late StreamSubscription<T> _changesSubscription;

  @override
  void initState() {
    super.initState();

    // Initially display the pre-selected date.
    final int monthPage = _getInitPage();
    _dayPickerController = PageController(initialPage: monthPage);

    _changesSubscription = widget.selectablePicker!.onUpdate!
        .listen((newSelectedDate) => widget.onChanged(newSelectedDate))
          ..onError((e) => widget.onSelectionError != null
              ? widget.onSelectionError!.call(e)
              : print(e.toString()));

    _updateCurrentDate();
    _initPresenter();
  }

  @override
  void didUpdateWidget(DayBasedChangeablePicker<T> oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (widget.datePickerStyles != oldWidget.datePickerStyles) {
      final ThemeData theme = Theme.of(context);
      _resultStyles = widget.datePickerStyles!.fulfillWithTheme(theme);
    }

    if (widget.selectablePicker != oldWidget.selectablePicker) {
      _changesSubscription = widget.selectablePicker!.onUpdate!
          .listen((newSelectedDate) => widget.onChanged(newSelectedDate))
            ..onError((e) => widget.onSelectionError != null
                ? widget.onSelectionError!.call(e)
                : print(e.toString()));
    }
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    curLocale = Localizations.localeOf(context);

    MaterialLocalizations curLocalizations =
        Localizations.of<MaterialLocalizations>(
            context, MaterialLocalizations)!;
    if (localizations != curLocalizations) {
      localizations = curLocalizations;
      _initPresenter();
    }

    final ThemeData theme = Theme.of(context);
    _resultStyles = widget.datePickerStyles!.fulfillWithTheme(theme);
  }

  @override
  // ignore: prefer_expression_function_bodies
  Widget build(BuildContext context) {
    return SizedBox(
        width: widget.datePickerLayoutSettings!.monthPickerPortraitWidth,
        height: widget.datePickerLayoutSettings!.maxDayPickerHeight,
        child: Column(
          children: <Widget>[
            widget.datePickerLayoutSettings!.hideMonthNavigationRow
                ? const SizedBox()
                : SizedBox(
                    height: widget.datePickerLayoutSettings!.dayPickerRowHeight,
                    child: Padding(
                        //match _DayPicker main layout padding
                        padding:
                            widget.datePickerLayoutSettings!.contentPadding,
                        child: _buildMonthNavigationRow()),
                  ),
            Expanded(
              child: Semantics(
                sortKey: MonthPickerSortKey.calendar,
                child: _buildDayPickerPageView(),
              ),
            ),
          ],
        ));
  }

  @override
  void dispose() {
    _timer.cancel();
    _dayPickerController.dispose();
    _changesSubscription.cancel();
    widget.selectablePicker!.dispose();
    _presenter.dispose();
    super.dispose();
  }

  void _updateCurrentDate() {
    _todayDate = DateTime.now();
    final DateTime tomorrow =
        DateTime(_todayDate.year, _todayDate.month, _todayDate.day + 1);
    Duration timeUntilTomorrow = tomorrow.difference(_todayDate);
    timeUntilTomorrow +=
        const Duration(seconds: 1); // so we don't miss it by rounding
    _timer.cancel();
    _timer = Timer(timeUntilTomorrow, () {
      setState(_updateCurrentDate);
    });
  }

  // ignore: prefer_expression_function_bodies
  Widget _buildMonthNavigationRow() {
    return StreamBuilder<DayBasedChangeablePickerState>(
        stream: _presenter.data,
        initialData: _presenter.lastVal,
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return const SizedBox();
          }

          DayBasedChangeablePickerState state = snapshot.data!;

          return MonthNavigationRow(
            previousPageIconKey: widget.datePickerKeys!.previousPageIconKey,
            nextPageIconKey: widget.datePickerKeys!.nextPageIconKey,
            previousMonthTooltip: state.prevTooltip!,
            nextMonthTooltip: state.nextTooltip!,
            onPreviousMonthTapped:
                (state.isFirstMonth == true ? null : _presenter.gotoPrevMonth)!,
            onNextMonthTapped:
                (state.isLastMonth == true ? null : _presenter.gotoNextMonth)!,
            title: Text(
              state.curMonthDis!,
              key: widget.datePickerKeys?.selectedPeriodKeys,
              style: _resultStyles.displayedPeriodTitle,
            ),
            nextIcon: widget.datePickerStyles!.nextIcon,
            prevIcon: widget.datePickerStyles!.prevIcon,
          );
        });
  }

  Widget _buildDayPickerPageView() => PageView.builder(
        controller: _dayPickerController,
        scrollDirection: Axis.horizontal,
        itemCount:
            DatePickerUtils.monthDelta(widget.firstDate!, widget.lastDate!) + 1,
        itemBuilder: _buildCalendar,
        onPageChanged: _handleMonthPageChanged,
      );

  Widget _buildCalendar(BuildContext context, int index) {
    final DateTime targetDate =
        DatePickerUtils.addMonthsToMonthDate(widget.firstDate!, index);

    return DayBasedPicker(
      key: ValueKey<DateTime>(targetDate),
      selectablePicker: widget.selectablePicker,
      currentDate: _todayDate,
      firstDate: widget.firstDate,
      lastDate: widget.lastDate,
      displayedMonth: targetDate,
      datePickerLayoutSettings: widget.datePickerLayoutSettings!,
      selectedPeriodKey: widget.datePickerKeys?.selectedPeriodKeys,
      datePickerStyles: _resultStyles,
      eventDecorationBuilder: widget.eventDecorationBuilder,
      localizations: localizations,
    );
  }

  // Returns appropriate date to be shown for init.
  // If [widget.initiallyShowDate] is out of bounds [widget.firstDate]
  // - [widget.lastDate], nearest bound will be used.
  DateTime _getCheckedInitialDate() {
    DateTime initiallyShowDateChecked = widget.initiallyShowDate!;
    if (initiallyShowDateChecked.isBefore(widget.firstDate!)) {
      initiallyShowDateChecked = widget.firstDate!;
    }

    if (initiallyShowDateChecked.isAfter(widget.lastDate!)) {
      initiallyShowDateChecked = widget.lastDate!;
    }

    return initiallyShowDateChecked;
  }

  int _getInitPage() {
    final initialDate = _getCheckedInitialDate();
    int initPage = DatePickerUtils.monthDelta(widget.firstDate!, initialDate);

    return initPage;
  }

  void _initPresenter() {
    _presenter.dispose();

    _presenter = DayBasedChangeablePickerPresenter(
        firstDate: widget.firstDate!,
        lastDate: widget.lastDate!,
        localizations: localizations,
        showPrevMonthDates: widget.datePickerLayoutSettings!.showPrevMonthEnd,
        showNextMonthDates: widget.datePickerLayoutSettings!.showNextMonthStart,
        firstDayOfWeekIndex: widget.datePickerStyles!.firstDayOfeWeekIndex);
    _presenter.data!.listen(_onStateChanged);

    // date used to define what month should be shown
    DateTime initSelection = _getCheckedInitialDate();

    // Give information about initial selection to presenter.
    // It should be done after first frame when PageView is already created.
    // Otherwise event from presenter will cause a error.
    _ambiguate(WidgetsBinding.instance).addPostFrameCallback((_) {
      _presenter.setSelectedDate(initSelection);
    });
  }

  void _onStateChanged(DayBasedChangeablePickerState newState) {
    DateTime newMonth = newState.currentMonth!;
    final int monthPage =
        DatePickerUtils.monthDelta(widget.firstDate!, newMonth);
    _dayPickerController.animateToPage(monthPage,
        duration: const Duration(milliseconds: 200), curve: Curves.easeInOut);
  }

  void _handleMonthPageChanged(int monthPage) {
    DateTime firstMonth = widget.firstDate!;
    DateTime newMonth = DateTime(firstMonth.year, firstMonth.month + monthPage);
    _presenter.changeMonth(newMonth);

    widget.onMonthChanged?.call(newMonth);
  }

  static MaterialLocalizations get _defaultLocalizations =>
      MaterialLocalizationEn(
        twoDigitZeroPaddedFormat:
            intl.NumberFormat('00', _defaultLocale.toString()),
        fullYearFormat: intl.DateFormat.y(_defaultLocale.toString()),
        longDateFormat: intl.DateFormat.yMMMMEEEEd(_defaultLocale.toString()),
        shortMonthDayFormat: intl.DateFormat.MMMd(_defaultLocale.toString()),
        decimalFormat:
            intl.NumberFormat.decimalPattern(_defaultLocale.toString()),
        shortDateFormat: intl.DateFormat.yMMMd(_defaultLocale.toString()),
        mediumDateFormat: intl.DateFormat.MMMEd(_defaultLocale.toString()),
        compactDateFormat: intl.DateFormat.yMd(_defaultLocale.toString()),
        yearMonthFormat: intl.DateFormat.yMMMM(_defaultLocale.toString()),
      );

  static DayBasedChangeablePickerPresenter get _defaultPresenter =>
      DayBasedChangeablePickerPresenter(
          firstDate: DateTime.now(),
          lastDate: DateTime.now(),
          localizations: _defaultLocalizations,
          showPrevMonthDates: false,
          showNextMonthDates: false,
          firstDayOfWeekIndex: 1);
}

DateTime _getInitiallyShownDate(
  DateTime initiallyShowDate,
  DayPickerSelection selection,
) {
  if (selection.isNotEmpty) return selection.earliest;
  return DateTime.now();
}

/// It needs to support both Flutter 3 and earlier versions .
///
/// This allows a value of type T or T?
/// to be treated as a value of type T?.
///
/// We use this so that APIs that have become
/// non-nullable can still be used with `!` and `?`
/// to support older versions of the API as well.
T _ambiguate<T>(T value) => value;
