mirrored_repositories/dungeon-paper-app/lib/app/widgets
import 'dart:math';

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

class NumberTextField extends StatelessWidget {
  /// Controls the text being edited.
  ///
  /// If null, this widget will create its own [TextEditingController] and
  /// initialize its [TextEditingController.text] with [initialValue].
  final TextEditingController? controller;

  final num? minValue;
  final num? maxValue;
  final NumberType numberType;

  const NumberTextField({
    super.key,
    this.controller,
    this.initialValue,
    this.focusNode,
    this.decoration = const InputDecoration(),
    TextInputType? keyboardType,
    this.textCapitalization = TextCapitalization.none,
    this.textInputAction,
    this.style,
    this.strutStyle,
    this.textDirection,
    this.textAlign = TextAlign.start,
    this.textAlignVertical,
    this.autofocus = false,
    this.readOnly = false,
    this.toolbarOptions,
    this.showCursor,
    this.obscuringCharacter = '•',
    this.obscureText = false,
    this.autocorrect = true,
    this.smartDashesType,
    this.smartQuotesType,
    this.enableSuggestions = true,
    this.maxLengthEnforcement,
    this.maxLines = 1,
    this.minLines,
    this.expands = false,
    this.maxLength,
    this.onChanged,
    this.onTap,
    this.onEditingComplete,
    this.onFieldSubmitted,
    this.onSaved,
    this.validator,
    this.inputFormatters,
    this.enabled,
    this.cursorWidth = 2.0,
    this.cursorHeight,
    this.cursorRadius,
    this.cursorColor,
    this.keyboardAppearance,
    this.scrollPadding = const EdgeInsets.all(20.0),
    this.enableInteractiveSelection,
    this.selectionControls,
    this.buildCounter,
    this.scrollPhysics,
    this.autofillHints,
    this.autovalidateMode,
    this.scrollController,
    this.restorationId,
    this.enableIMEPersonalizedLearning = true,
    this.mouseCursor,
    //
    this.minValue,
    this.maxValue,
    required this.numberType,
  })  : keyboardType = keyboardType ??
            (numberType == NumberType.double
                ? const TextInputType.numberWithOptions(decimal: true)
                : const TextInputType.numberWithOptions(decimal: false));

  final String? initialValue;
  final FocusNode? focusNode;
  final InputDecoration? decoration;
  final TextInputType? keyboardType;
  final TextCapitalization textCapitalization;
  final TextInputAction? textInputAction;
  final TextStyle? style;
  final StrutStyle? strutStyle;
  final TextDirection? textDirection;
  final TextAlign textAlign;
  final TextAlignVertical? textAlignVertical;
  final bool autofocus;
  final bool readOnly;
  final ToolbarOptions? toolbarOptions;
  final bool? showCursor;
  final String obscuringCharacter;
  final bool obscureText;
  final bool autocorrect;
  final SmartDashesType? smartDashesType;
  final SmartQuotesType? smartQuotesType;
  final bool enableSuggestions;
  final MaxLengthEnforcement? maxLengthEnforcement;
  final int? maxLines;
  final int? minLines;
  final bool expands;
  final int? maxLength;
  final ValueChanged<String>? onChanged;
  final GestureTapCallback? onTap;
  final VoidCallback? onEditingComplete;
  final ValueChanged<String>? onFieldSubmitted;
  final FormFieldSetter<String>? onSaved;
  final FormFieldValidator<String>? validator;
  final List<TextInputFormatter>? inputFormatters;
  final bool? enabled;
  final double cursorWidth;
  final double? cursorHeight;
  final Radius? cursorRadius;
  final Color? cursorColor;
  final Brightness? keyboardAppearance;
  final EdgeInsets scrollPadding;
  final bool? enableInteractiveSelection;
  final TextSelectionControls? selectionControls;
  final InputCounterWidgetBuilder? buildCounter;
  final ScrollPhysics? scrollPhysics;
  final Iterable<String>? autofillHints;
  final AutovalidateMode? autovalidateMode;
  final ScrollController? scrollController;
  final String? restorationId;
  final bool enableIMEPersonalizedLearning;
  final MouseCursor? mouseCursor;

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      key: key,
      controller: controller,
      initialValue: initialValue,
      focusNode: focusNode,
      decoration: (decoration ?? const InputDecoration()).copyWith(
        prefix: decoration?.prefixIcon ??
            IconButton(
              icon: const Icon(Icons.remove),
              iconSize: 16,
              splashColor: Colors.red.withOpacity(0.2),
              visualDensity: VisualDensity.compact,
              onPressed: _decr,
            ),
        suffix: decoration?.suffixIcon ??
            IconButton(
              icon: const Icon(Icons.add),
              iconSize: 16,
              splashColor: Colors.green.withOpacity(0.2),
              visualDensity: VisualDensity.compact,
              onPressed: _incr,
            ),
      ),
      keyboardType: keyboardType ??
          TextInputType.numberWithOptions(
              decimal: numberType == NumberType.double),
      textCapitalization: textCapitalization,
      textInputAction: textInputAction,
      style: style,
      strutStyle: strutStyle,
      textDirection: textDirection,
      textAlign: textAlign,
      textAlignVertical: textAlignVertical,
      autofocus: autofocus,
      readOnly: readOnly,
      toolbarOptions: toolbarOptions,
      showCursor: showCursor,
      obscuringCharacter: obscuringCharacter,
      obscureText: obscureText,
      autocorrect: autocorrect,
      smartDashesType: smartDashesType,
      smartQuotesType: smartQuotesType,
      enableSuggestions: enableSuggestions,
      maxLengthEnforcement: maxLengthEnforcement,
      maxLines: maxLines,
      minLines: minLines,
      expands: expands,
      maxLength: maxLength,
      onChanged: onChanged,
      onTap: onTap,
      onEditingComplete: onEditingComplete,
      onFieldSubmitted: onFieldSubmitted,
      onSaved: onSaved,
      validator: validator,
      inputFormatters: [
        NumberInputFormatter(
            min: minValue, max: maxValue, numberType: numberType),
        ...?inputFormatters,
      ],
      enabled: enabled,
      cursorWidth: cursorWidth,
      cursorHeight: cursorHeight,
      cursorRadius: cursorRadius,
      cursorColor: cursorColor,
      keyboardAppearance: keyboardAppearance,
      scrollPadding: scrollPadding,
      enableInteractiveSelection: enableInteractiveSelection,
      selectionControls: selectionControls,
      buildCounter: buildCounter,
      scrollPhysics: scrollPhysics,
      autofillHints: autofillHints,
      autovalidateMode: autovalidateMode,
      scrollController: scrollController,
      restorationId: restorationId,
      enableIMEPersonalizedLearning: enableIMEPersonalizedLearning,
      mouseCursor: mouseCursor,
    );
  }

  num? _parse(String value) {
    switch (numberType) {
      case NumberType.double:
        return double.tryParse(value);
      case NumberType.int:
        return int.tryParse(value);
    }
  }

  void _decr() {
    controller?.text = max((_parse(controller?.text ?? '') ?? 1) - 1,
            minValue ?? -double.infinity)
        .toString();
    onChanged?.call(controller?.text ?? '');
  }

  void _incr() {
    controller?.text = min((_parse(controller?.text ?? '') ?? 0) + 1,
            maxValue ?? double.infinity)
        .toString();
    onChanged?.call(controller?.text ?? '');
  }
}

enum NumberType {
  double,
  int,
}

class NumberInputFormatter extends TextInputFormatter {
  NumberInputFormatter({
    this.min,
    this.max,
    required this.numberType,
  });

  final num? min;
  final num? max;
  final NumberType numberType;

  @override
  TextEditingValue formatEditUpdate(
      TextEditingValue oldValue, TextEditingValue newValue) {
    if (newValue.text.isEmpty) {
      return newValue;
    }

    final value = double.tryParse(newValue.text);

    if (value != null &&
        ((max != null && value > max!) || (min != null && value < min!))) {
      return oldValue;
    }

    switch (numberType) {
      case NumberType.int:
        return _intFormat(oldValue, newValue);
      case NumberType.double:
        return _doubleFormat(oldValue, newValue);
    }
  }

  TextEditingValue _doubleFormat(
      TextEditingValue oldValue, TextEditingValue newValue) {
    if (newValue.text.endsWith('.')) {
      return newValue;
    }

    if (double.tryParse(newValue.text) != null) {
      return newValue;
    }

    return oldValue;
  }

  TextEditingValue _intFormat(
      TextEditingValue oldValue, TextEditingValue newValue) {
    if (int.tryParse(newValue.text) != null) {
      return newValue;
    }

    return oldValue;
  }
}
