import 'dart:math' as math;

import 'package:flutter/material.dart';

class CircularStepProgressBar extends StatelessWidget {
  /// Defines if steps grow from
  /// clockwise [CircularDirection.clockwise] or
  /// counterclockwise [CircularDirection.counterclockwise]
  final CircularDirection circularDirection;

  /// Number of steps to underline, all the steps with
  /// index <= [currentStep] will have [Color] equal to
  /// [selectedColor]
  ///
  /// Only used when [customColor] is [null]
  ///
  /// Default value: 0
  final int currentStep;

  /// Total number of step of the complete indicator
  final int totalSteps;

  /// Radial spacing between each step. Remember to
  /// define the value in radiant units
  ///
  /// Default value: math.pi / 20
  final double padding;

  /// Height of the indicator's box container
  final double? height;

  /// Width of the indicator's box container
  final double? width;

  /// Assign a custom [Color] for each step
  ///
  /// Takes a [int], index of the current step starting from 0, and
  /// must return a [Color]
  ///
  /// **NOTE**: If provided, it overrides
  /// [selectedColor] and [unselectedColor]
  final Color Function(int)? customColor;

  /// [Color] of the selected steps
  ///
  /// All the steps with index <= [currentStep]
  ///
  /// Default value: [Colors.blue]
  final Color? selectedColor;

  /// [Color] of the unselected steps
  ///
  /// All the steps with index between
  /// [currentStep] and [totalSteps]
  ///
  /// Default value: [Colors.grey]
  final Color? unselectedColor;

  /// The size of a single step in the indicator
  ///
  /// Default value: 6.0
  final double stepSize;

  /// Specify a custom size for selected steps
  ///
  /// Only applicable when not custom setting (customColor, customStepSize) is defined
  ///
  /// This value will replace the [stepSize] only for selected steps
  final double? selectedStepSize;

  /// Specify a custom size for unselected steps
  ///
  /// Only applicable when not custom setting (customColor, customStepSize) is defined
  ///
  /// This value will replace the [stepSize] only for unselected steps
  final double? unselectedStepSize;

  /// Assign a custom size [double] for each step
  ///
  /// Function takes a [int], index of the current step starting from 0, and
  /// a [bool], which tells if the step is selected based on [currentStep],
  /// and must return a [double] size of the step
  ///
  /// **NOTE**: If provided, it overrides [stepSize]
  final double Function(int, bool)? customStepSize;

  /// [Widget] contained inside the circular indicator
  final Widget? child;

  /// Height of the indicator container in case no [height] parameter
  /// given and parent height is [double.infinity]
  ///
  /// Default value: 100.0
  final double fallbackHeight;

  /// Height of the indicator container in case no [width] parameter
  /// given and parent height is [double.infinity]
  ///
  /// Default value: 100.0
  final double fallbackWidth;

  /// Angle in radiants in which the first step of the indicator is placed.
  /// The initial value is on the top of the indicator (- math.pi / 2)
  /// - 0 => TOP
  /// - math.pi / 2 => LEFT
  /// - math.pi => BOTTOM
  /// - math.pi / 2 * 3 => RIGHT
  /// - math.pi / 2 => TOP (again)
  final double startingAngle;

  /// Angle in radiants which represents the size of the arc used to display the indicator.
  /// It allows you to draw a semi-circle instead of a full 360° (math.pi * 2) circle.
  final double arcSize;

  /// Adds rounded edges at the beginning and at the end of the circular indicator
  /// given a [int], index of each step, and a [bool],
  /// which tells if the step is selected based on [currentStep], and must return a
  /// [bool] that tells if the edges are rounded or not
  ///
  /// **NOTE**: For continuous circular indicators (`padding: 0`), to check if to apply
  /// the rounded edges the packages uses index 0 (for the first arc painted) and
  /// 1 (for the second arc painted)
  ///
  /// ```dart
  /// // Example: Add rounded edges for all the steps
  /// roundedCap: (index, _) => true
  /// ```
  ///
  /// ```dart
  /// // Example: Add rounded edges for the selected arc of the indicator
  /// roundedCap: (index, _) => index == 0,
  /// padding: 0
  /// ```
  final bool Function(int, bool)? roundedCap;

  /// Adds a gradient color to the circular indicator
  ///
  /// **NOTE**: If provided, it overrides [selectedColor], [unselectedColor], and [customColor]
  final Gradient? gradientColor;

  /// Removes the extra angle caused by [StrokeCap.round] when [roundedCap] is applied
  final bool removeRoundedCapExtraAngle;

  const CircularStepProgressBar({
    required this.totalSteps,
    this.child,
    this.height,
    this.width,
    this.customColor,
    this.customStepSize,
    this.selectedStepSize,
    this.unselectedStepSize,
    this.roundedCap,
    this.gradientColor,
    this.circularDirection = CircularDirection.clockwise,
    this.fallbackHeight = 100.0,
    this.fallbackWidth = 100.0,
    this.currentStep = 0,
    this.selectedColor = Colors.blue,
    this.unselectedColor = Colors.grey,
    this.padding = math.pi / 20,
    this.stepSize = 6.0,
    this.startingAngle = 0,
    this.arcSize = math.pi * 2,
    this.removeRoundedCapExtraAngle = false,
    Key? key,
  })  : assert(totalSteps > 0, "Number of total steps (totalSteps) of the CircularStepProgressBar must be greater than 0"),
        assert(currentStep >= 0, "Current step (currentStep) of the CircularStepProgressBar must be greater than or equal to 0"),
        assert(padding >= 0.0, "Padding (padding) of the CircularStepProgressBar must be greater or equal to 0"),
        super(key: key);

  @override
  Widget build(BuildContext context) {
    // Print warning when arcSize greater than math.pi * 2 which causes steps to overlap
    if (arcSize > math.pi * 2) print("WARNING (step_progress_indicator): arcSize of CircularStepProgressBar is greater than 360° (math.pi * 2), this will cause some steps to overlap!");
    TextDirection textDirection = Directionality.of(context);

    return LayoutBuilder(
      builder: (context, constraints) => SizedBox(
        // Apply fallback for both height and width
        // if their value is null and no parent size limit
        height: height != null
            ? height
            : constraints.maxHeight != double.infinity
                ? constraints.maxHeight
                : fallbackHeight,
        width: width != null
            ? width
            : constraints.maxWidth != double.infinity
                ? constraints.maxWidth
                : fallbackWidth,
        child: CustomPaint(
          painter: _CircularIndicatorPainter(
            totalSteps: totalSteps,
            currentStep: currentStep,
            customColor: customColor,
            padding: padding,
            circularDirection: circularDirection,
            selectedColor: selectedColor,
            unselectedColor: unselectedColor,
            arcSize: arcSize,
            stepSize: stepSize,
            customStepSize: customStepSize,
            maxDefinedSize: maxDefinedSize,
            selectedStepSize: selectedStepSize,
            unselectedStepSize: unselectedStepSize,
            startingAngle: startingAngleTopOfIndicator,
            roundedCap: roundedCap,
            gradientColor: gradientColor,
            textDirection: textDirection,
            removeRoundedCapExtraAngle: removeRoundedCapExtraAngle,
          ),
          // Padding needed to show the indicator when child is placed on top of it
          child: Padding(
            padding: EdgeInsets.all(maxDefinedSize),
            child: child,
          ),
        ),
      ),
    );
  }

  /// Compute the maximum possible size of the indicator between
  /// [stepSize] and [customStepSize]
  double get maxDefinedSize {
    if (customStepSize == null) {
      return math.max(stepSize, math.max(selectedStepSize ?? 0, unselectedStepSize ?? 0));
    }

    // When customSize defined, compute and return max possible size
    double currentMaxSize = 0;

    for (int step = 0; step < totalSteps; ++step) {
      // Consider max between selected and unselected case
      var customSizeValue = math.max(customStepSize!(step, false), customStepSize!(step, true));
      if (customSizeValue > currentMaxSize) {
        currentMaxSize = customSizeValue;
      }
    }

    return currentMaxSize;
  }

  /// Make [startingAngle] to top-center of indicator (0°) by default
  double get startingAngleTopOfIndicator => startingAngle - math.pi / 2;
}

class _CircularIndicatorPainter implements CustomPainter {
  final int totalSteps;
  final int currentStep;
  final double padding;
  final Color? selectedColor;
  final Color? unselectedColor;
  final double stepSize;
  final double? selectedStepSize;
  final double? unselectedStepSize;
  final double Function(int, bool)? customStepSize;
  final double maxDefinedSize;
  final Color Function(int)? customColor;
  final CircularDirection circularDirection;
  final double startingAngle;
  final double arcSize;
  final bool Function(int, bool)? roundedCap;
  final Gradient? gradientColor;
  final TextDirection textDirection;
  final bool removeRoundedCapExtraAngle;

  _CircularIndicatorPainter({
    required this.totalSteps,
    required this.circularDirection,
    required this.customColor,
    required this.currentStep,
    required this.selectedColor,
    required this.unselectedColor,
    required this.padding,
    required this.stepSize,
    required this.selectedStepSize,
    required this.unselectedStepSize,
    required this.customStepSize,
    required this.startingAngle,
    required this.arcSize,
    required this.maxDefinedSize,
    required this.roundedCap,
    required this.gradientColor,
    required this.textDirection,
    required this.removeRoundedCapExtraAngle,
  });

  @override
  void paint(Canvas canvas, Size size) {
    var w = size.width;
    var h = size.height;

    // Step length is user-defined arcSize
    // divided by the total number of steps (each step same size)
    var stepLength = arcSize / totalSteps;

    // Define general arc paint
    Paint paint = Paint()
      ..style = PaintingStyle.stroke
      ..strokeWidth = maxDefinedSize;

    var rect = Rect.fromCenter(
      // Rect created from the center of the widget
      center: Offset(w / 2, h / 2),
      // For both height and width, subtract maxDefinedSize to fit indicator inside the parent container
      height: h - maxDefinedSize,
      width: w - maxDefinedSize,
    );

    if (gradientColor != null) {
      paint.shader = gradientColor!.createShader(rect, textDirection: textDirection);
    }

    // Change color selected or unselected based on the circularDirection
    var isClockwise = circularDirection == CircularDirection.clockwise;

    // Make a continuous arc without rendering all the steps when possible
    if (padding == 0 && customColor == null && customStepSize == null && roundedCap == null) {
      _drawContinuousArc(canvas, paint, rect, isClockwise);
    } else {
      _drawStepArc(canvas, paint, rect, isClockwise, stepLength);
    }
  }

  /// Draw a series of arcs, each composing the full steps of the indicator
  void _drawStepArc(Canvas canvas, Paint paint, Rect rect, bool isClockwise, double stepLength) {
    // Draw a series of circular arcs to compose the indicator
    // Starting based on startingAngle attribute
    //
    // When clockwise:
    // - Start drawing counterclockwise so to have the selected steps on top of the unselected
    int step = isClockwise ? totalSteps - 1 : 0;
    double stepAngle = isClockwise ? startingAngle - stepLength : startingAngle;
    for (; isClockwise ? step >= 0 : step < totalSteps; isClockwise ? stepAngle -= stepLength : stepAngle += stepLength, isClockwise ? --step : ++step) {
      // Check if the current step is selected or unselected
      var isSelectedColor = _isSelectedColor(step, isClockwise);

      // Size of the step
      var indexStepSize = customStepSize != null
          // Consider step index inverted when counterclockwise
          ? customStepSize!(_indexOfStep(step, isClockwise), isSelectedColor)
          : isSelectedColor
              ? selectedStepSize ?? stepSize
              : unselectedStepSize ?? stepSize;

      // Use customColor if defined
      var stepColor = customColor != null
          // Consider step index inverted when counterclockwise
          ? customColor!(_indexOfStep(step, isClockwise))
          : isSelectedColor
              ? selectedColor!
              : unselectedColor!;

      // Apply stroke cap to each step
      var hasStrokeCap = roundedCap != null ? roundedCap!(_indexOfStep(step, isClockwise), isSelectedColor) : false;
      var strokeCap = hasStrokeCap ? StrokeCap.round : StrokeCap.butt;

      // Remove extra size caused by rounded stroke cap
      // https://github.com/SandroMaglione/step-progress-indicator/issues/20#issue-786114745
      var extraCapSize = indexStepSize / 2;
      var extraCapAngle = extraCapSize / (rect.width / 2);
      var extraCapRemove = hasStrokeCap && removeRoundedCapExtraAngle;

      // Draw arc steps of the indicator
      _drawArcOnCanvas(
        canvas: canvas,
        rect: rect,
        startingAngle: stepAngle + (extraCapRemove ? extraCapAngle : 0),
        sweepAngle: stepLength - padding - (extraCapRemove ? extraCapAngle * 2 : 0),
        paint: paint,
        color: stepColor,
        strokeWidth: indexStepSize,
        strokeCap: strokeCap,
      );
    }
  }

  /// Draw optimized continuous indicator instead of multiple steps
  void _drawContinuousArc(Canvas canvas, Paint paint, Rect rect, bool isClockwise) {
    // Compute color of the selected and unselected bars
    var firstStepColor = isClockwise ? selectedColor : unselectedColor;
    var secondStepColor = !isClockwise ? selectedColor : unselectedColor;

    // Selected and unselected step sizes if defined, otherwise use stepSize
    var firstStepSize = isClockwise ? selectedStepSize ?? stepSize : unselectedStepSize ?? stepSize;
    var secondStepSize = !isClockwise ? selectedStepSize ?? stepSize : unselectedStepSize ?? stepSize;

    // Compute length and starting angle of the selected and unselected bars
    var firstArcLength = arcSize * (currentStep / totalSteps);
    var secondArcLength = arcSize - firstArcLength;

    // firstArcStartingAngle = startingAngle
    var secondArcStartingAngle = startingAngle + firstArcLength;

    // Apply stroke cap to both arcs
    // NOTE: For continuous circular indicator, it uses 0 and 1 as index to
    // apply the rounded cap
    var firstArcStrokeCap = roundedCap != null
        ? isClockwise
            ? roundedCap!(0, true)
            : roundedCap!(1, false)
        : false;
    var secondArcStrokeCap = roundedCap != null
        ? isClockwise
            ? roundedCap!(1, false)
            : roundedCap!(0, true)
        : false;
    var firstCap = firstArcStrokeCap ? StrokeCap.round : StrokeCap.butt;
    var secondCap = secondArcStrokeCap ? StrokeCap.round : StrokeCap.butt;

    // When clockwise, draw the second arc first and the first on top of it
    // Required when stroke cap is rounded to make the selected step on top of the unselected
    if (circularDirection == CircularDirection.clockwise) {
      // Second arc, selected when counterclockwise, unselected otherwise
      _drawArcOnCanvas(
        canvas: canvas,
        rect: rect,
        paint: paint,
        startingAngle: secondArcStartingAngle,
        sweepAngle: secondArcLength,
        strokeWidth: secondStepSize,
        color: secondStepColor!,
        strokeCap: secondCap,
      );

      // First arc, selected when clockwise, unselected otherwise
      _drawArcOnCanvas(
        canvas: canvas,
        rect: rect,
        paint: paint,
        startingAngle: startingAngle,
        sweepAngle: firstArcLength,
        strokeWidth: firstStepSize,
        color: firstStepColor!,
        strokeCap: firstCap,
      );
    } else {
      // First arc, selected when clockwise, unselected otherwise
      _drawArcOnCanvas(
        canvas: canvas,
        rect: rect,
        paint: paint,
        startingAngle: startingAngle,
        sweepAngle: firstArcLength,
        strokeWidth: firstStepSize,
        color: firstStepColor!,
        strokeCap: firstCap,
      );

      // Second arc, selected when counterclockwise, unselected otherwise
      _drawArcOnCanvas(
        canvas: canvas,
        rect: rect,
        paint: paint,
        startingAngle: secondArcStartingAngle,
        sweepAngle: secondArcLength,
        strokeWidth: secondStepSize,
        color: secondStepColor!,
        strokeCap: secondCap,
      );
    }
  }

  /// Draw the actual arc for a continuous indicator
  void _drawArcOnCanvas({
    required Canvas canvas,
    required Rect rect,
    required double startingAngle,
    required double sweepAngle,
    required Paint paint,
    required Color color,
    required double strokeWidth,
    required StrokeCap strokeCap,
  }) =>
      canvas.drawArc(
        rect,
        startingAngle,
        sweepAngle,
        false /*isRadial*/,
        paint
          ..color = color
          ..strokeWidth = strokeWidth
          ..strokeCap = strokeCap,
      );

  bool _isSelectedColor(int step, bool isClockwise) => isClockwise ? step < currentStep : (step + 1) > (totalSteps - currentStep);

  /// Start counting indexes from the right if clockwise and on the left if counterclockwise
  int _indexOfStep(int step, bool isClockwise) => isClockwise ? step : totalSteps - step - 1;

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => oldDelegate != this;

  @override
  bool hitTest(Offset position) => false;

  @override
  void addListener(listener) {}

  @override
  void removeListener(listener) {}

  @override
  Null get semanticsBuilder => null;

  @override
  bool shouldRebuildSemantics(CustomPainter oldDelegate) => false;
}

/// Used to define the [circularDirection] attribute of the [CircularStepProgressBar]
enum CircularDirection {
  clockwise,
  counterclockwise,
}
