import 'dart:math';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class AnimatedButton extends StatefulWidget {
  AnimatedButton({
    Key? key,
    required this.text,
    required this.onPressed,
    required this.controller,
    this.textColor,
    required this.loadingColor,
    this.color,
  }) : super(key: key);

  final String text;
  final Color? color;
  final Color? textColor;
  final Color loadingColor;
  final VoidCallback? onPressed;
  final AnimationController controller;

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

class _AnimatedButtonState extends State<AnimatedButton>
    with SingleTickerProviderStateMixin {
  late Animation<double> _sizeAnimation;
  late Animation<double> _textOpacityAnimation;
  late Animation<double> _buttonOpacityAnimation;
  late Animation<double> _ringThicknessAnimation;
  late Animation<double> _ringOpacityAnimation;
  late Animation<Color?> _colorAnimation;
  var _isLoading = false;
  var _hover = false;
  var _width = 120.0;

  late Color _color;
  late Color _loadingColor;

  static const _height = 40.0;
  static const _loadingCircleRadius = _height / 2;
  static const _loadingCircleThickness = 4.0;

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

    _textOpacityAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
      CurvedAnimation(
        parent: widget.controller,
        curve: Interval(0.0, .25),
      ),
    );

    // _colorAnimation
    // _width, _sizeAnimation

    _buttonOpacityAnimation =
        Tween<double>(begin: 1.0, end: 0.0).animate(CurvedAnimation(
      parent: widget.controller,
      curve: Threshold(.65),
    ));

    _ringThicknessAnimation =
        Tween<double>(begin: _loadingCircleRadius, end: _loadingCircleThickness)
            .animate(CurvedAnimation(
      parent: widget.controller,
      curve: Interval(.65, .85),
    ));
    _ringOpacityAnimation =
        Tween<double>(begin: 1.0, end: 0.0).animate(CurvedAnimation(
      parent: widget.controller,
      curve: Interval(.85, 1.0),
    ));

    widget.controller.addStatusListener(handleStatusChanged);
  }

  @override
  void didChangeDependencies() {
    _updateColorAnimation();
    _updateWidth();
    super.didChangeDependencies();
  }

  void _updateColorAnimation() {
    final theme = Theme.of(context);
    final buttonTheme = theme.floatingActionButtonTheme;

    _color = (widget.color ?? buttonTheme.backgroundColor)!;
    _loadingColor = widget.loadingColor ?? theme.colorScheme.secondary;

    _colorAnimation = ColorTween(

      begin: _color,
      end: _loadingColor,
    ).animate(
      CurvedAnimation(
        parent: widget.controller,
        curve: const Interval(0.0, .65, curve: Curves.fastOutSlowIn),
      ),
    );
  }

  @override
  void didUpdateWidget(AnimatedButton oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (oldWidget.color != widget.color ||
        oldWidget.loadingColor != widget.loadingColor) {
      _updateColorAnimation();
    }

    if (oldWidget.text != widget.text) {
      _updateWidth();
    }
  }

  @override
  void dispose() {
    super.dispose();
    widget.controller.removeStatusListener(handleStatusChanged);
  }

  void handleStatusChanged(status) {
    if (status == AnimationStatus.forward) {
      setState(() => _isLoading = true);
    }
    if (status == AnimationStatus.dismissed) {
      setState(() => _isLoading = false);
    }
  }

  /// sets width and size animation
  void _updateWidth() {
    final theme = Theme.of(context);
    // final fontSize = theme.textTheme.button!.fontSize;
    final fontSize = 12.0;
    final renderParagraph = RenderParagraph(
      TextSpan(
        text: widget.text,
        style: TextStyle(
          fontSize: fontSize,
          fontWeight: FontWeight.normal,
          letterSpacing: 0.6,
        ),
      ),
      textDirection: TextDirection.ltr,
      maxLines: 1,
    );

    renderParagraph.layout(BoxConstraints(minWidth: 120.0));

    // text width based on fontSize, plus 45.0 for padding
    var textWidth =
        renderParagraph.getMinIntrinsicWidth(fontSize!).ceilToDouble() + 45.0;

    // button width is min 120.0 and max 240.0
    _width = textWidth > 120.0 && textWidth < 240.0
        ? textWidth
        : textWidth >= 240.0
            ? 240.0
            : 120.0;

    _sizeAnimation = Tween<double>(begin: 1.0, end: _height / _width)
        .animate(CurvedAnimation(
      parent: widget.controller,
      curve: Interval(0.0, .65, curve: Curves.fastOutSlowIn),
    ));
  }

  Widget _buildButtonText(ThemeData theme) {
    return FadeTransition(
      opacity: _textOpacityAnimation,
      child: AnimatedText(
        text: widget.text,
        style: TextStyle(color: widget.textColor ?? Colors.white),
      ),
    );
  }

  Widget _buildButton(ThemeData theme) {
    final buttonTheme = theme.floatingActionButtonTheme;

    return FadeTransition(
      opacity: _buttonOpacityAnimation,
      child: AnimatedContainer(
        duration: Duration(milliseconds: 300),
        child: AnimatedBuilder(
          animation: _colorAnimation,
          builder: (context, child) => Material(
            shape: RoundedRectangleBorder(
                borderRadius: BorderRadius.circular(_height / 2)),
            color: _colorAnimation.value,
            child: child,
            shadowColor: _color,
            elevation: (_isLoading == false)
                ? (_hover == true
                    ? buttonTheme.highlightElevation ?? 0.0
                    : buttonTheme.elevation ?? 0.0)
                : 0.0,
          ),
          child: InkWell(
            onTap: !_isLoading ? widget!.onPressed : null,
            splashColor: buttonTheme.splashColor,
            customBorder: buttonTheme.shape,
            onHighlightChanged: (value) => setState(() => _hover = value),
            child: SizeTransition(
              sizeFactor: _sizeAnimation,
              axis: Axis.horizontal,
              child: Container(
                width: _width,
                height: _height,
                alignment: Alignment.center,
                child: _buildButtonText(theme),
              ),
            ),
          ),
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Stack(
      alignment: Alignment.center,
      children: <Widget>[
        FadeTransition(
          opacity: _ringOpacityAnimation,
          child: AnimatedBuilder(
            animation: _ringThicknessAnimation,
            builder: (context, child) => Ring(
              color: widget!.loadingColor,
              size: _height,
              thickness: _ringThicknessAnimation.value,
            ),
          ),
        ),
        if (_isLoading)
          SizedBox(
            width: _height - _loadingCircleThickness,
            height: _height - _loadingCircleThickness,
            child: CircularProgressIndicator(
              valueColor: AlwaysStoppedAnimation<Color>(widget.loadingColor),
              // backgroundColor: Colors.red,
              strokeWidth: _loadingCircleThickness,
            ),
          ),
        _buildButton(theme),
      ],
    );
  }
}

class Ring extends StatelessWidget {
  Ring({
    Key? key,
    required this.color,
    this.size = 40.0,
    this.thickness = 2.0,
    this.value = 1.0,
  })  : assert(size - thickness > 0),
        assert(thickness >= 0),
        super(key: key);

  final Color color;
  final double size;
  final double thickness;
  final double value;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: size - thickness,
      height: size - thickness,
      child: thickness == 0
          ? null
          : CircularProgressIndicator(
              valueColor: AlwaysStoppedAnimation<Color>(color),
              strokeWidth: thickness,
              value: value,
            ),
    );
  }
}

enum AnimatedTextRotation { up, down }

/// https://medium.com/flutter-community/flutter-challenge-3d-bottom-navigation-bar-48952a5fd996
class AnimatedText extends StatefulWidget {
  AnimatedText({
    Key? key,
    required this.text,
    required this.style,
    this.textRotation = AnimatedTextRotation.up,
  }) : super(key: key);

  final String text;
  final TextStyle style;
  final AnimatedTextRotation textRotation;

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

class _AnimatedTextState extends State<AnimatedText>
    with SingleTickerProviderStateMixin {
  var _newText = '';
  var _oldText = '';
  var _layoutHeight = 0.0;
  final _textKey = GlobalKey();

  late Animation<double> _animation;
  late AnimationController _controller;

  double get radius => _layoutHeight / 2;

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

    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 500),
    );

    _animation = Tween<double>(begin: 0.0, end: pi / 2).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOutBack,
    ));

    _oldText = widget.text;

    WidgetsBinding.instance.addPostFrameCallback((_) {
      setState(() => _layoutHeight = getWidgetSize(_textKey)!.height);
    });
  }

  @override
  void didUpdateWidget(AnimatedText oldWidget) {
    super.didUpdateWidget(oldWidget);

    if (widget.text != oldWidget.text) {
      _oldText = oldWidget.text;
      _newText = widget.text;
      _controller.forward().then((_) {
        setState(() {
          final t = _oldText;
          _oldText = _newText;
          _newText = t;
        });
        _controller.reset();
      });
    }
  }

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

  Matrix4 get _matrix {
    // Fix: The text is not centered after applying perspective effect in the web build. Idk why
    if (kIsWeb) {
      return Matrix4.identity();
    }
    return Matrix4.identity()..setEntry(3, 2, .006);
  }

  Matrix4 _getFrontSideUp(double value) {
    return _matrix
      ..translate(
        0.0,
        -radius * sin(_animation.value),
        -radius * cos(_animation.value),
      )
      ..rotateX(-_animation.value); // 0 -> -pi/2
  }

  Matrix4 _getBackSideUp(double value) {
    return _matrix
      ..translate(
        0.0,
        radius * cos(_animation.value),
        -radius * sin(_animation.value),
      )
      ..rotateX((pi / 2) - _animation.value); // pi/2 -> 0
  }

  Matrix4 _getFrontSideDown(double value) {
    return _matrix
      ..translate(
        0.0,
        radius * sin(_animation.value),
        -radius * cos(_animation.value),
      )
      ..rotateX(_animation.value); // 0 -> pi/2
  }

  Matrix4 _getBackSideDown(double value) {
    return _matrix
      ..translate(
        0.0,
        -radius * cos(_animation.value),
        -radius * sin(_animation.value),
      )
      ..rotateX(_animation.value - pi / 2); // -pi/2 -> 0
  }

  @override
  Widget build(BuildContext context) {
    final rollUp = widget.textRotation == AnimatedTextRotation.up;
    final oldText = Text(
      _oldText,
      key: _textKey,
      style: widget.style,
    );
    final newText = Text(
      _newText,
      style: widget.style,
    );

    return AnimatedBuilder(
      animation: _animation,
      builder: (context, child) => Stack(
        alignment: Alignment.center,
        children: <Widget>[
          if (_animation.value <= toRadian(85))
            Transform(
              alignment: Alignment.center,
              transform: rollUp
                  ? _getFrontSideUp(_animation.value)
                  : _getFrontSideDown(_animation.value),
              child: oldText,
            ),
          if (_animation.value >= toRadian(5))
            Transform(
              alignment: Alignment.center,
              transform: rollUp
                  ? _getBackSideUp(_animation.value)
                  : _getBackSideDown(_animation.value),
              child: newText,
            ),
        ],
      ),
    );
  }

// Helpers
  double toRadian(double degree) => degree * pi / 180;

  double lerp(double start, double end, double percent) =>
      (start + percent * (end - start));

  Size? getWidgetSize(GlobalKey key) {
    return key.currentContext!.size;
    //return renderBox?.size;
  }
}
