import 'dart:math';

import 'package:flutter/material.dart';
import 'package:github_dataviz/catmull.dart';
import 'package:github_dataviz/constants.dart';
import 'package:github_dataviz/data/data_series.dart';
import 'package:github_dataviz/data/week_label.dart';
import 'package:github_dataviz/mathutils.dart';

class LayeredChart extends StatefulWidget {
  final List<DataSeries> dataToPlot;
  final List<WeekLabel> milestones;
  final double animationValue;

  const LayeredChart(this.dataToPlot, this.milestones, this.animationValue,
      {Key? key})
      : super(key: key);

  @override
  State<LayeredChart> createState() {
    return _LayeredChartState();
  }
}

class _LayeredChartState extends State<LayeredChart> {
  late List<Path> paths;
  late List<Path> capPaths;
  late List<double> maxValues;
  late double theta;
  late double graphHeight;
  late List<TextPainter> labelPainter;
  late List<TextPainter> milestonePainter;
  Size? lastSize;

  void buildPaths(
      Size size,
      List<DataSeries> dataToPlot,
      List<WeekLabel> milestones,
      int numPoints,
      double graphGap,
      double margin,
      double capTheta,
      double capSize) {
    double screenRatio = size.width / size.height;
    double degrees = MathUtils.clampedMap(screenRatio, 0.5, 2.5, 50, 5);
    theta = pi * degrees / 180;
    graphHeight = MathUtils.clampedMap(screenRatio, 0.5, 2.5, 50, 150);

    int m = dataToPlot.length;
    paths = [];
    capPaths = [];
    maxValues = [];
    for (int i = 0; i < m; i++) {
      int n = dataToPlot[i].series.length;
      maxValues.add(0);
      for (int j = 0; j < n; j++) {
        double v = dataToPlot[i].series[j].toDouble();
        if (v > maxValues[i]) {
          maxValues[i] = v;
        }
      }
    }
    double totalGap = m * graphGap;
    double xIndent = totalGap / tan(capTheta);
    double startX = margin + xIndent;
    double endX = size.width - margin;
    double startY = size.height;
    double endY = startY - (endX - startX) * tan(theta);
    double xWidth = (endX - startX) / numPoints;
    double capRangeX = capSize * cos(capTheta);
    double tanCapTheta = tan(capTheta);
    final curvePoints = <double>[];
    for (int i = 0; i < m; i++) {
      List<int> series = dataToPlot[i].series;
      int n = series.length;
      final controlPoints = <Point2D>[];
      controlPoints.add(Point2D(-1, 0));
      double last = 0;
      for (int j = 0; j < n; j++) {
        double v = series[j].toDouble();
        controlPoints.add(Point2D(j.toDouble(), v));
        last = v;
      }
      controlPoints.add(Point2D(n.toDouble(), last));
      CatmullInterpolator curve = CatmullInterpolator(controlPoints);
      ControlPointAndValue cpv = ControlPointAndValue();
      for (int j = 0; j < numPoints; j++) {
        cpv.value = MathUtils.map(
            j.toDouble(), 0, (numPoints - 1).toDouble(), 0, (n - 1).toDouble());
        curve.progressiveGet(cpv);
        curvePoints.add(MathUtils.map(
            max(0, cpv.value!), 0, maxValues[i].toDouble(), 0, graphHeight));
      }
      paths.add(Path());
      capPaths.add(Path());
      paths[i].moveTo(startX, startY);
      capPaths[i].moveTo(startX, startY);
      for (int j = 0; j < numPoints; j++) {
        double v = curvePoints[j];
        int k = j + 1;
        double xDist = xWidth;
        double capV = v;
        while (k < numPoints && xDist <= capRangeX) {
          double cy = curvePoints[k] + xDist * tanCapTheta;
          capV = max(capV, cy);
          k++;
          xDist += xWidth;
        }
        double x = MathUtils.map(
            j.toDouble(), 0, (numPoints - 1).toDouble(), startX, endX);
        double baseY = MathUtils.map(
            j.toDouble(), 0, (numPoints - 1).toDouble(), startY, endY);
        double y = baseY - v;
        double cY = baseY - capV;
        paths[i].lineTo(x, y);
        if (j == 0) {
          int k = capRangeX ~/ xWidth;
          double mx = MathUtils.map(
              -k.toDouble(), 0, (numPoints - 1).toDouble(), startX, endX);
          double my = MathUtils.map(
                  -k.toDouble(), 0, (numPoints - 1).toDouble(), startY, endY) -
              capV;
          capPaths[i].lineTo(mx, my);
        }
        capPaths[i].lineTo(x, cY);
      }
      paths[i].lineTo(endX, endY);
      paths[i].lineTo(endX, endY + 1);
      paths[i].lineTo(startX, startY + 1);
      paths[i].close();
      capPaths[i].lineTo(endX, endY);
      capPaths[i].lineTo(endX, endY + 1);
      capPaths[i].lineTo(startX, startY + 1);
      capPaths[i].close();
    }
    labelPainter = [];
    for (int i = 0; i < dataToPlot.length; i++) {
      TextSpan span = TextSpan(
          style: const TextStyle(
              color: Color.fromARGB(255, 255, 255, 255), fontSize: 12),
          text: dataToPlot[i].label.toUpperCase());
      TextPainter tp = TextPainter(
          text: span,
          textAlign: TextAlign.left,
          textDirection: TextDirection.ltr);
      tp.layout();
      labelPainter.add(tp);
    }
    milestonePainter = [];
    for (int i = 0; i < milestones.length; i++) {
      TextSpan span = TextSpan(
          style: const TextStyle(
              color: Color.fromARGB(255, 255, 255, 255), fontSize: 10),
          text: milestones[i].label.toUpperCase());
      TextPainter tp = TextPainter(
          text: span,
          textAlign: TextAlign.left,
          textDirection: TextDirection.ltr);
      tp.layout();
      milestonePainter.add(tp);
    }
    lastSize = Size(size.width, size.height);
  }

  @override
  Widget build(BuildContext context) {
    return Container(
        color: Constants.backgroundColor,
        child: CustomPaint(
            foregroundPainter: _ChartPainter(this, widget.dataToPlot,
                widget.milestones, 80, 50, 50, 12, 500, widget.animationValue),
            child: Container()));
  }
}

class _ChartPainter extends CustomPainter {
  static List<Color?> colors = [
    Colors.red[900],
    const Color(0xffc4721a),
    Colors.lime[900],
    Colors.green[900],
    Colors.blue[900],
    Colors.purple[900],
  ];
  static List<Color?> capColors = [
    Colors.red[500],
    Colors.amber[500],
    Colors.lime[500],
    Colors.green[500],
    Colors.blue[500],
    Colors.purple[500],
  ];

  List<DataSeries> dataToPlot;
  List<WeekLabel> milestones;

  double margin;
  double graphGap;
  late double capTheta;
  double capSize;
  int numPoints;
  double amount = 1.0;

  late Paint pathPaint;
  late Paint capPaint;
  late Paint textPaint;
  late Paint milestonePaint;
  late Paint linePaint;
  late Paint fillPaint;

  _LayeredChartState state;

  _ChartPainter(
      this.state,
      this.dataToPlot,
      this.milestones,
      this.margin,
      this.graphGap,
      double capDegrees,
      this.capSize,
      this.numPoints,
      this.amount) {
    capTheta = pi * capDegrees / 180;
    pathPaint = Paint();
    pathPaint.style = PaintingStyle.fill;
    capPaint = Paint();
    capPaint.style = PaintingStyle.fill;
    textPaint = Paint();
    textPaint.color = const Color(0xFFFFFFFF);
    milestonePaint = Paint();
    milestonePaint.color = Constants.milestoneColor;
    milestonePaint.style = PaintingStyle.stroke;
    milestonePaint.strokeWidth = 2;
    linePaint = Paint();
    linePaint.style = PaintingStyle.stroke;
    linePaint.strokeWidth = 0.5;
    fillPaint = Paint();
    fillPaint.style = PaintingStyle.fill;
    fillPaint.color = const Color(0xFF000000);
  }

  @override
  void paint(Canvas canvas, Size size) {
    if (dataToPlot.isEmpty) {
      return;
    }

    if (state.lastSize == null ||
        size.width != state.lastSize!.width ||
        size.height != state.lastSize!.height) {
      print('Building paths, lastsize = ${state.lastSize}');
      state.buildPaths(size, dataToPlot, milestones, numPoints, graphGap,
          margin, capTheta, capSize);
    }
    int m = dataToPlot.length;
    int numWeeks = dataToPlot[0].series.length;
    // How far along to draw
    double totalGap = m * graphGap;
    double xIndent = totalGap / tan(capTheta);
    double dx = xIndent / (m - 1);
    double startX = margin + xIndent;
    double endX = size.width - margin;
    double startY = size.height;
    double endY = startY - (endX - startX) * tan(state.theta);
    // MILESTONES
    {
      for (int i = 0; i < milestones.length; i++) {
        WeekLabel milestone = milestones[i];
        double p = (milestone.weekNum!.toDouble() / numWeeks) + (1 - amount);
        if (p < 1) {
          double x1 = MathUtils.map(p, 0, 1, startX, endX);
          double y1 = MathUtils.map(p, 0, 1, startY, endY);
          double x2 = x1 - xIndent;
          double y2 = y1 - graphGap * (m - 1);
          x1 += dx * 0.5;
          y1 += graphGap * 0.5;
          double textY = y1 + 5;
          double textX = x1 + 5 * tan(capTheta);
          canvas.drawLine(Offset(x1, y1), Offset(x2, y2), milestonePaint);
          canvas.save();
          TextPainter tp = state.milestonePainter[i];
          canvas.translate(textX, textY);
          canvas.skew(tan(capTheta * 1.0), -tan(state.theta));
          canvas.translate(-tp.width / 2, 0);
          tp.paint(canvas, const Offset(0, 0));
          canvas.restore();
        }
      }
    }
    for (int i = m - 1; i >= 0; i--) {
      canvas.save();
      canvas.translate(-dx * i, -graphGap * i);

      {
        // TEXT LABELS
        canvas.save();
        double textPosition = 0.2;
        double textX = MathUtils.map(textPosition, 0, 1, startX, endX);
        double textY = MathUtils.map(textPosition, 0, 1, startY, endY) + 5;
        canvas.translate(textX, textY);
        TextPainter tp = state.labelPainter[i];
        canvas.skew(0, -tan(state.theta));
        canvas.drawRect(
            Rect.fromLTWH(-1, -1, tp.width + 2, tp.height + 2), fillPaint);
        tp.paint(canvas, const Offset(0, 0));
        canvas.restore();
      }

      linePaint.color = capColors[i]!;
      canvas.drawLine(Offset(startX, startY), Offset(endX, endY), linePaint);

      Path clipPath = Path();
      clipPath.moveTo(startX - capSize, startY + 11);
      clipPath.lineTo(endX, endY + 1);
      clipPath.lineTo(endX, endY - state.graphHeight - capSize);
      clipPath.lineTo(startX - capSize, startY - state.graphHeight - capSize);
      clipPath.close();
      canvas.clipPath(clipPath);

      pathPaint.color = colors[i]!;
      capPaint.color = capColors[i]!;
      double offsetX = MathUtils.map(1 - amount, 0, 1, startX, endX);
      double offsetY = MathUtils.map(1 - amount, 0, 1, startY, endY);
      canvas.translate(offsetX - startX, offsetY - startY);
      canvas.drawPath(state.capPaths[i], capPaint);
      canvas.drawPath(state.paths[i], pathPaint);

      canvas.restore();
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}