// 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.

import 'dart:math' as math;

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

import 'colors.dart';

const double _kDefaultIndicatorRadius = 10.0;

// Extracted from iOS 13.2 Beta.
const Color _kActiveTickColor = CupertinoDynamicColor.withBrightness(
  color: Color(0xFF3C3C44),
  darkColor: Color(0xFFEBEBF5),
);

/// An iOS-style activity indicator that spins clockwise.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=AENVH-ZqKDQ}
///
/// {@tool dartpad}
/// This example shows how [CupertinoActivityIndicator] can be customized.
///
/// ** See code in examples/api/lib/cupertino/activity_indicator/cupertino_activity_indicator.0.dart **
/// {@end-tool}
///
/// See also:
///
///  * [CupertinoLinearActivityIndicator], which displays progress along a line.
///  * <https://developer.apple.com/design/human-interface-guidelines/progress-indicators/>
class CupertinoActivityIndicator extends StatefulWidget {
  /// Creates an iOS-style activity indicator that spins clockwise.
  const CupertinoActivityIndicator({
    super.key,
    this.color,
    this.animating = true,
    this.radius = _kDefaultIndicatorRadius,
  }) : assert(radius > 0.0),
       progress = 1.0;

  /// Creates a non-animated iOS-style activity indicator that displays
  /// a partial count of ticks based on the value of [progress].
  ///
  /// When provided, the value of [progress] must be between 0.0 (zero ticks
  /// will be shown) and 1.0 (all ticks will be shown) inclusive. Defaults
  /// to 1.0.
  const CupertinoActivityIndicator.partiallyRevealed({
    super.key,
    this.color,
    this.radius = _kDefaultIndicatorRadius,
    this.progress = 1.0,
  }) : assert(radius > 0.0),
       assert(progress >= 0.0),
       assert(progress <= 1.0),
       animating = false;

  /// Color of the activity indicator.
  ///
  /// Defaults to color extracted from native iOS.
  final Color? color;

  /// Whether the activity indicator is running its animation.
  ///
  /// Defaults to true.
  final bool animating;

  /// Radius of the spinner widget.
  ///
  /// Defaults to 10 pixels. Must be positive.
  final double radius;

  /// Determines the percentage of spinner ticks that will be shown. Typical usage would
  /// display all ticks, however, this allows for more fine-grained control such as
  /// during pull-to-refresh when the drag-down action shows one tick at a time as
  /// the user continues to drag down.
  ///
  /// Defaults to one. Must be between zero and one, inclusive.
  final double progress;

  @override
  State<CupertinoActivityIndicator> createState() => _CupertinoActivityIndicatorState();
}

class _CupertinoActivityIndicatorState extends State<CupertinoActivityIndicator>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(duration: const Duration(seconds: 1), vsync: this);

    if (widget.animating) {
      _controller.repeat();
    }
  }

  @override
  void didUpdateWidget(CupertinoActivityIndicator oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.animating != oldWidget.animating) {
      if (widget.animating) {
        _controller.repeat();
      } else {
        _controller.stop();
      }
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: widget.radius * 2,
      width: widget.radius * 2,
      child: CustomPaint(
        painter: _CupertinoActivityIndicatorPainter(
          position: _controller,
          activeColor: widget.color ?? CupertinoDynamicColor.resolve(_kActiveTickColor, context),
          radius: widget.radius,
          progress: widget.progress,
        ),
      ),
    );
  }
}

const double _kTwoPI = math.pi * 2.0;

/// Alpha values extracted from the native component (for both dark and light mode) to
/// draw the spinning ticks.
const List<int> _kAlphaValues = <int>[47, 47, 47, 47, 72, 97, 122, 147];

/// The alpha value that is used to draw the partially revealed ticks.
const int _partiallyRevealedAlpha = 147;

class _CupertinoActivityIndicatorPainter extends CustomPainter {
  _CupertinoActivityIndicatorPainter({
    required this.position,
    required this.activeColor,
    required this.radius,
    required this.progress,
  }) : tickFundamentalShape = RRect.fromLTRBXY(
         -radius / _kDefaultIndicatorRadius,
         -radius / 3.0,
         radius / _kDefaultIndicatorRadius,
         -radius,
         radius / _kDefaultIndicatorRadius,
         radius / _kDefaultIndicatorRadius,
       ),
       super(repaint: position);

  final Animation<double> position;
  final Color activeColor;
  final double radius;
  final double progress;

  // Use a RRect instead of RSuperellipse since this shape is really small
  // and should make little visual difference.
  final RRect tickFundamentalShape;

  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint();
    final int tickCount = _kAlphaValues.length;

    canvas.save();
    canvas.translate(size.width / 2.0, size.height / 2.0);

    final int activeTick = (tickCount * position.value).floor();

    for (var i = 0; i < tickCount * progress; ++i) {
      final int t = (i - activeTick) % tickCount;
      paint.color = activeColor.withAlpha(
        progress < 1 ? _partiallyRevealedAlpha : _kAlphaValues[t],
      );
      canvas.drawRRect(tickFundamentalShape, paint);
      canvas.rotate(_kTwoPI / tickCount);
    }

    canvas.restore();
  }

  @override
  bool shouldRepaint(_CupertinoActivityIndicatorPainter oldPainter) {
    return oldPainter.position != position ||
        oldPainter.activeColor != activeColor ||
        oldPainter.progress != progress;
  }
}

/// An iOS-style linear activity indicator.
///
/// The [CupertinoLinearActivityIndicator] is a linear progress bar that
/// displays a colored bar to indicate the progress of an ongoing task.
///
/// {@tool dartpad}
/// This example shows how [CupertinoLinearActivityIndicator] can be customized.
///
/// ** See code in examples/api/lib/cupertino/activity_indicator/cupertino_linear_activity_indicator.0.dart **
/// {@end-tool}
///
/// See also:
///
///  * [CupertinoActivityIndicator], which is an iOS-style activity indicator that spins clockwise.
///  * <https://developer.apple.com/design/human-interface-guidelines/progress-indicators/>
class CupertinoLinearActivityIndicator extends StatelessWidget {
  /// Creates a linear iOS-style activity indicator.
  const CupertinoLinearActivityIndicator({
    super.key,
    required this.progress,
    this.height = 4.5,
    this.color,
  }) : assert(height > 0),
       assert(progress >= 0.0 && progress <= 1.0);

  /// The current progress of the linear activity indicator.
  ///
  /// This value must be between 0.0 and 1.0. A value of 0.0 means no progress
  /// and 1.0 means that progress is complete.
  final double progress;

  /// The height of the line used to draw the linear activity indicator.
  ///
  /// Defaults to 4.5 units. Must be positive.
  final double height;

  /// The color of the progress bar.
  ///
  /// This color represents the portion of the bar that indicates progress.
  ///
  /// Defaults to [CupertinoColors.activeBlue] if no color is specified.
  final Color? color;

  @override
  Widget build(BuildContext context) {
    return ConstrainedBox(
      constraints: BoxConstraints(minHeight: height, minWidth: double.infinity),
      child: CustomPaint(
        painter: _CupertinoLinearActivityIndicator(progress: progress, color: color),
      ),
    );
  }
}

class _CupertinoLinearActivityIndicator extends CustomPainter {
  _CupertinoLinearActivityIndicator({required this.progress, this.color})
    : _backgroundPaint = Paint()
        ..color = CupertinoColors.systemFill
        ..style = PaintingStyle.fill,
      _progressPaint = Paint()
        ..color = color ?? CupertinoColors.activeBlue
        ..style = PaintingStyle.fill;

  final double progress;

  final Color? color;

  /// The background paint used to draw the full width of the progress bar.
  ///
  /// This paint object is created once and reused to fill the background
  /// with a system fill color.
  final Paint _backgroundPaint;

  /// The paint used to draw the progress portion of the progress bar.
  ///
  /// This paint object is created once and reused to fill the progress area.
  final Paint _progressPaint;

  @override
  void paint(Canvas canvas, Size size) {
    // Draw the background of the progress bar.
    canvas.drawRRect(
      BorderRadius.all(Radius.circular(size.height / 2)).toRRect(Offset.zero & size),
      _backgroundPaint,
    );

    // Draw the progress portion of the bar.
    if (progress > 0) {
      canvas.drawRRect(
        BorderRadius.all(
          Radius.circular(size.height / 2),
        ).toRRect(Offset.zero & Size(clampDouble(progress, 0.0, 1.0) * size.width, size.height)),
        _progressPaint,
      );
    }
  }

  @override
  bool shouldRepaint(_CupertinoLinearActivityIndicator old) =>
      old.progress != progress || old.color != color;
}
