// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// @dart = 2.8

import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';

import '../input_border.dart';
import '../input_decorator.dart';
import '../material_localizations.dart';
import '../text_form_field.dart';
import '../theme.dart';

import 'date_picker_common.dart';
import 'date_utils.dart' as utils;

/// A [TextFormField] configured to accept and validate a date entered by the user.
///
/// The text entered into this field will be constrained to only allow digits
/// and separators. When saved or submitted, the text will be parsed into a
/// [DateTime] according to the ambient locale. If the input text doesn't parse
/// into a date, the [errorFormatText] message will be displayed under the field.
///
/// [firstDate], [lastDate], and [selectableDayPredicate] provide constraints on
/// what days are valid. If the input date isn't in the date range or doesn't pass
/// the given predicate, then the [errorInvalidText] message will be displayed
/// under the field.
///
/// See also:
///
///  * [showDatePicker], which shows a dialog that contains a material design
///    date picker which includes support for text entry of dates.
///  * [MaterialLocalizations.parseCompactDate], which is used to parse the text
///    input into a [DateTime].
///
class InputDatePickerFormField extends StatefulWidget {
  /// Creates a [TextFormField] configured to accept and validate a date.
  ///
  /// If the optional [initialDate] is provided, then it will be used to populate
  /// the text field. If the [fieldHintText] is provided, it will be shown.
  ///
  /// If [initialDate] is provided, it must not be before [firstDate] or after
  /// [lastDate]. If [selectableDayPredicate] is provided, it must return `true`
  /// for [initialDate].
  ///
  /// [firstDate] must be on or before [lastDate].
  ///
  /// [firstDate], [lastDate], and [autofocus] must be non-null.
  ///
  InputDatePickerFormField({
    Key key,
    DateTime initialDate,
    @required DateTime firstDate,
    @required DateTime lastDate,
    this.onDateSubmitted,
    this.onDateSaved,
    this.selectableDayPredicate,
    this.errorFormatText,
    this.errorInvalidText,
    this.fieldHintText,
    this.fieldLabelText,
    this.autofocus = false,
  }) : assert(firstDate != null),
       assert(lastDate != null),
       assert(autofocus != null),
       initialDate = initialDate != null ? utils.dateOnly(initialDate) : null,
       firstDate = utils.dateOnly(firstDate),
       lastDate = utils.dateOnly(lastDate),
       super(key: key) {
    assert(
      !this.lastDate.isBefore(this.firstDate),
      'lastDate ${this.lastDate} must be on or after firstDate ${this.firstDate}.'
    );
    assert(
      initialDate == null || !this.initialDate.isBefore(this.firstDate),
      'initialDate ${this.initialDate} must be on or after firstDate ${this.firstDate}.'
    );
    assert(
      initialDate == null || !this.initialDate.isAfter(this.lastDate),
      'initialDate ${this.initialDate} must be on or before lastDate ${this.lastDate}.'
    );
    assert(
      selectableDayPredicate == null || initialDate == null || selectableDayPredicate(this.initialDate),
      'Provided initialDate ${this.initialDate} must satisfy provided selectableDayPredicate.'
    );
  }

  /// If provided, it will be used as the default value of the field.
  final DateTime initialDate;

  /// The earliest allowable [DateTime] that the user can input.
  final DateTime firstDate;

  /// The latest allowable [DateTime] that the user can input.
  final DateTime lastDate;

  /// An optional method to call when the user indicates they are done editing
  /// the text in the field. Will only be called if the input represents a valid
  /// [DateTime].
  final ValueChanged<DateTime> onDateSubmitted;

  /// An optional method to call with the final date when the form is
  /// saved via [FormState.save]. Will only be called if the input represents
  /// a valid [DateTime].
  final ValueChanged<DateTime> onDateSaved;

  /// Function to provide full control over which [DateTime] can be selected.
  final SelectableDayPredicate selectableDayPredicate;

  /// The error text displayed if the entered date is not in the correct format.
  final String errorFormatText;

  /// The error text displayed if the date is not valid.
  ///
  /// A date is not valid if it is earlier than [firstDate], later than
  /// [lastDate], or doesn't pass the [selectableDayPredicate].
  final String errorInvalidText;

  /// The hint text displayed in the [TextField].
  ///
  /// If this is null, it will default to the date format string. For example,
  /// 'mm/dd/yyyy' for en_US.
  final String fieldHintText;

  /// The label text displayed in the [TextField].
  ///
  /// If this is null, it will default to the words representing the date format
  /// string. For example, 'Month, Day, Year' for en_US.
  final String fieldLabelText;

  /// {@macro flutter.widgets.editableText.autofocus}
  final bool autofocus;

  @override
  _InputDatePickerFormFieldState createState() => _InputDatePickerFormFieldState();
}

class _InputDatePickerFormFieldState extends State<InputDatePickerFormField> {
  final TextEditingController _controller = TextEditingController();
  DateTime _selectedDate;
  String _inputText;
  bool _autoSelected = false;

  @override
  void initState() {
    super.initState();
    _selectedDate = widget.initialDate;
  }

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

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    if (_selectedDate != null) {
      final MaterialLocalizations localizations = MaterialLocalizations.of(context);
      _inputText = localizations.formatCompactDate(_selectedDate);
      TextEditingValue textEditingValue = _controller.value.copyWith(text: _inputText);
      // Select the new text if we are auto focused and haven't selected the text before.
      if (widget.autofocus && !_autoSelected) {
        textEditingValue = textEditingValue.copyWith(selection: TextSelection(
          baseOffset: 0,
          extentOffset: _inputText.length,
        ));
        _autoSelected = true;
      }
      _controller.value = textEditingValue;
    }
  }

  DateTime _parseDate(String text) {
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
    return localizations.parseCompactDate(text);
  }

  bool _isValidAcceptableDate(DateTime date) {
    return
      date != null &&
      !date.isBefore(widget.firstDate) &&
      !date.isAfter(widget.lastDate) &&
      (widget.selectableDayPredicate == null || widget.selectableDayPredicate(date));
  }

  String _validateDate(String text) {
    final DateTime date = _parseDate(text);
    if (date == null) {
      return widget.errorFormatText ?? MaterialLocalizations.of(context).invalidDateFormatLabel;
    } else if (!_isValidAcceptableDate(date)) {
      return widget.errorInvalidText ?? MaterialLocalizations.of(context).dateOutOfRangeLabel;
    }
    return null;
  }

  void _handleSaved(String text) {
    if (widget.onDateSaved != null) {
      final DateTime date = _parseDate(text);
      if (_isValidAcceptableDate(date)) {
        _selectedDate = date;
        _inputText = text;
        widget.onDateSaved(date);
      }
    }
  }

  void _handleSubmitted(String text) {
    if (widget.onDateSubmitted != null) {
      final DateTime date = _parseDate(text);
      if (_isValidAcceptableDate(date)) {
        _selectedDate = date;
        _inputText = text;
        widget.onDateSubmitted(date);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    final MaterialLocalizations localizations = MaterialLocalizations.of(context);
    final InputDecorationTheme inputTheme = Theme.of(context).inputDecorationTheme;
    return TextFormField(
      decoration: InputDecoration(
        border: inputTheme.border ?? const UnderlineInputBorder(),
        filled: inputTheme.filled ?? true,
        hintText: widget.fieldHintText ?? localizations.dateHelpText,
        labelText: widget.fieldLabelText ?? localizations.dateInputLabel,
      ),
      validator: _validateDate,
      inputFormatters: <TextInputFormatter>[
        DateTextInputFormatter(localizations.dateSeparator),
      ],
      keyboardType: TextInputType.datetime,
      onSaved: _handleSaved,
      onFieldSubmitted: _handleSubmitted,
      autofocus: widget.autofocus,
      controller: _controller,
    );
  }
}

/// A `TextInputFormatter` set up to format dates.
//
// This is not publicly exported (see pickers.dart), as it is
// just meant for internal use by `InputDatePickerFormField` and
// `InputDateRangePicker`.
class DateTextInputFormatter extends TextInputFormatter {

  /// Creates a date formatter with the given separator.
  DateTextInputFormatter(
    this.separator
  ) : _filterFormatter = FilteringTextInputFormatter.allow(RegExp('[\\d$_commonSeparators\\$separator]+'));

  /// List of common separators that are used in dates. This is used to make
  /// sure that if given platform's [TextInputType.datetime] keyboard doesn't
  /// provide the given locale's separator character, they can still enter the
  /// separator using one of these characters (slash, period, comma, dash, or
  /// space).
  static const String _commonSeparators = r'\/\.,-\s';

  /// The date separator for the current locale.
  final String separator;

  // Formatter that will filter out all characters except digits and date
  // separators.
  final TextInputFormatter _filterFormatter;

  @override
  TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
    final TextEditingValue filteredValue = _filterFormatter.formatEditUpdate(oldValue, newValue);
    return filteredValue.copyWith(
      // Replace any non-digits with the given separator
      text: filteredValue.text.replaceAll(RegExp(r'[\D]'), separator),
    );
  }
}
