import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_circular_chart/src/circular_chart.dart';
import 'package:flutter_circular_chart/src/entry.dart';
import 'package:flutter_circular_chart/src/painter.dart';

// The default chart tween animation duration.
const Duration _kDuration = const Duration(milliseconds: 300);
// The default angle the chart is oriented at.
const double _kStartAngle = -90.0;

enum CircularChartType {
  Pie,
  Radial,
}

class AnimatedCircularChart extends StatefulWidget {
  AnimatedCircularChart({
    Key key,
    @required this.size,
    @required this.initialChartData,
    this.chartType = CircularChartType.Radial,
    this.duration = _kDuration,
    this.percentageValues = false,
    this.holeRadius,
    this.startAngle = _kStartAngle,
  })
      : assert(size != null),
        super(key: key);

  /// The size of the bounding box this chart will be constrained to.
  final Size size;

  /// The data used to build the chart displayed when the widget is first placed.
  /// Each [CircularStackEntry] in the list defines an individual stack of data:
  /// For a Pie chart that corresponds to individual slices in the chart.
  /// For a Radial chart it corresponds to individual segments on the same arc.
  ///
  /// If length > 1 and [chartType] is [CircularChartType.Radial] then the stacks
  /// will be grouped together as concentric circles.
  ///
  /// If [chartType] is [CircularChartType.Pie] then length cannot be > 1.
  final List<CircularStackEntry> initialChartData;

  /// The type of chart to be rendered.
  /// Use [CircularChartType.Pie] for a circle divided into slices for each entry.
  /// Use [CircularChartType.Radial] for one or more arcs with a hole in the center.
  final CircularChartType chartType;

  /// The duration of the chart animation when [AnimatedCircularChartState.updateData]
  /// is called.
  final Duration duration;

  /// If true then the data values provided will determine what percentage of the circle
  /// this segment occupies [i.e: a value of 100 is the full circle].
  ///
  /// Otherwise the data is normalized such that the sum of all values in each stack
  /// is considered to encompass 100% of the circle.
  ///
  /// defaults to false.
  final bool percentageValues;

  /// For [CircularChartType.Radial] charts this defines the circle in the center
  /// of the canvas, around which the chart is drawn. If not provided then it will
  /// be automatically calculated to accommodate all the data.
  ///
  /// Has no effect in [CircularChartType.Pie] charts.
  final double holeRadius;

  /// The chart gets drawn and animates clockwise from [startAngle], defaulting to the
  /// top/center point or -90.0. In terms of a clock face these would be:
  /// - -90.0:  12 o'clock
  /// - 0.0:    3 o'clock
  /// - 90.0:   6 o'clock
  /// - 180.0:  9 o'clock
  final double startAngle;

  /// The state from the closest instance of this class that encloses the given context.
  ///
  /// This method is typically used by [AnimatedCircularChart] item widgets that insert or
  /// remove items in response to user input.
  ///
  /// ```dart
  /// AnimatedCircularChartState animatedCircularChart = AnimatedCircularChart.of(context);
  /// ```
  static AnimatedCircularChartState of(BuildContext context,
      {bool nullOk: false}) {
    assert(context != null);
    assert(nullOk != null);

    final AnimatedCircularChartState result = context
        .ancestorStateOfType(const TypeMatcher<AnimatedCircularChartState>());

    if (nullOk || result != null) return result;

    throw new FlutterError(
        'AnimatedCircularChart.of() called with a context that does not contain a AnimatedCircularChart.\n'
        'No AnimatedCircularChart ancestor could be found starting from the context that was passed to AnimatedCircularChart.of(). '
        'This can happen when the context provided is from the same StatefulWidget that '
        'built the AnimatedCircularChart.\n'
        'The context used was:\n'
        '  $context');
  }

  @override
  AnimatedCircularChartState createState() => new AnimatedCircularChartState();
}

/// The state for a circular chart that animates when its data is updated.
///
/// When the chart data changes with [updateData] an animation begins running.
///
/// An app that needs to update its data in response to an event
/// can refer to the [AnimatedCircularChart]'s state with a global key:
///
/// ```dart
/// GlobalKey<AnimatedCircularChartState> chartKey = new GlobalKey<AnimatedCircularChartState>();
/// ...
/// new AnimatedCircularChart(key: chartKey, ...);
/// ...
/// chartKey.currentState.updateData(newData);
/// ```
class AnimatedCircularChartState extends State<AnimatedCircularChart>
    with TickerProviderStateMixin {
  CircularChartTween _tween;
  AnimationController _animation;
  final Map<String, int> _stackRanks = <String, int>{};
  final Map<String, int> _entryRanks = <String, int>{};

  @override
  void initState() {
    super.initState();
    _animation = new AnimationController(
      duration: widget.duration,
      vsync: this,
    );

    _assignRanks(widget.initialChartData);

    _tween = new CircularChartTween(
      new CircularChart.empty(chartType: widget.chartType),
      new CircularChart.fromData(
        size: widget.size,
        data: widget.initialChartData,
        chartType: widget.chartType,
        stackRanks: _stackRanks,
        entryRanks: _entryRanks,
        percentageValues: widget.percentageValues,
        holeRadius: widget.holeRadius,
        startAngle: widget.startAngle,
      ),
    );
    _animation.forward();
  }

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

  void _assignRanks(List<CircularStackEntry> data) {
    for (CircularStackEntry stackEntry in data) {
      _stackRanks.putIfAbsent(stackEntry.rankKey, () => _stackRanks.length);
      for (CircularSegmentEntry entry in stackEntry.entries) {
        _entryRanks.putIfAbsent(entry.rankKey, () => _entryRanks.length);
      }
    }
  }

  /// Update the data this chart represents and start an animation that will tween
  /// between the old data and this one.
  void updateData(List<CircularStackEntry> data) {
    _assignRanks(data);

    setState(() {
      _tween = new CircularChartTween(
        _tween.evaluate(_animation),
        new CircularChart.fromData(
          size: widget.size,
          data: data,
          chartType: widget.chartType,
          stackRanks: _stackRanks,
          entryRanks: _entryRanks,
          percentageValues: widget.percentageValues,
          holeRadius: widget.holeRadius,
          startAngle: widget.startAngle,
        ),
      );
      _animation.forward(from: 0.0);
    });
  }

  @override
  Widget build(BuildContext context) {
    return new CustomPaint(
      size: widget.size,
      painter: new AnimatedCircularChartPainter(_tween.animate(_animation)),
    );
  }
}
