// MIT License

// Copyright (c) 2018 Norbert Kozsir

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

// ignore_for_file: invalid_use_of_protected_member

import 'dart:math';
import 'package:flutter/material.dart';

typedef ParticleBuilder = Particle Function(int index);

abstract class Particle {
  void paint(Canvas canvas, Size size, double progress, int seed);
}

class FourRandomSlotParticle extends Particle {
  final List<Particle> children;

  final double relativeDistanceToMiddle;

  FourRandomSlotParticle({required this.children, this.relativeDistanceToMiddle = 2.0});

  @override
  void paint(Canvas canvas, Size size, double progress, int seed) {
    Random random = Random(seed);
    int side = 0;
    for (Particle particle in children) {
      PositionedParticle(
        position: sideToOffset(side, size, random) * relativeDistanceToMiddle,
        child: particle,
      ).paint(canvas, size, progress, seed);
      side++;
    }
  }

  Offset sideToOffset(int side, Size size, Random random) {
    if (side == 0) {
      return Offset(-random.nextDouble() * (size.width / 2), -random.nextDouble() * (size.height / 2));
    } else if (side == 1) {
      return Offset(random.nextDouble() * (size.width / 2), -random.nextDouble() * (size.height / 2));
    } else if (side == 2) {
      return Offset(random.nextDouble() * (size.width / 2), random.nextDouble() * (size.height / 2));
    } else if (side == 3) {
      return Offset(-random.nextDouble() * (size.width / 2), random.nextDouble() * (size.height / 2));
    } else {
      throw Exception();
    }
  }

  double randomOffset(Random random, int range) {
    return range / 2 - random.nextInt(range);
  }
}

class PoppingCircle extends Particle {
  final Color color;

  PoppingCircle({required this.color});

  final double radius = 3.0;

  @override
  void paint(Canvas canvas, Size size, double progress, seed) {
    if (progress < 0.5) {
      canvas.drawCircle(
          Offset.zero,
          radius + (progress * 8),
          Paint()
            ..color = color
            ..style = PaintingStyle.stroke
            ..strokeWidth = 5.0 - progress * 2);
    } else {
      CircleMirror(
        numberOfParticles: 4,
        child: AnimatedPositionedParticle(
            begin: const Offset(0.0, 5.0),
            end: const Offset(0.0, 15.0),
            child: FadingRect(
              color: color,
              height: 7.0,
              width: 2.0,
            )),
        initialRotation: pi / 4,
      ).paint(canvas, size, progress, seed);
    }
  }
}

class Firework extends Particle {
  @override
  void paint(Canvas canvas, Size size, double progress, int seed) {
    FourRandomSlotParticle(children: [
      IntervalParticle(
        interval: const Interval(0.0, 0.5, curve: Curves.easeIn),
        child: PoppingCircle(
          color: Colors.deepOrangeAccent,
        ),
      ),
      IntervalParticle(
        interval: const Interval(0.2, 0.5, curve: Curves.easeIn),
        child: PoppingCircle(
          color: Colors.green,
        ),
      ),
      IntervalParticle(
        interval: const Interval(0.4, 0.8, curve: Curves.easeIn),
        child: PoppingCircle(
          color: Colors.indigo,
        ),
      ),
      IntervalParticle(
        interval: const Interval(0.5, 1.0, curve: Curves.easeIn),
        child: PoppingCircle(
          color: Colors.teal,
        ),
      ),
    ]).paint(canvas, size, progress, seed);
  }
}

/// Mirrors a given particle around a circle.
///
/// When using the default constructor you specify one [Particle], this particle
/// is going to be used on its own, this implies that
/// all mirrored particles are identical (expect for the rotation around the circle)
class CircleMirror extends Particle {
  final ParticleBuilder particleBuilder;

  final double initialRotation;

  final int numberOfParticles;

  CircleMirror.builder({required this.particleBuilder, required this.initialRotation, required this.numberOfParticles});

  CircleMirror({required Particle child, required this.initialRotation, required this.numberOfParticles}) : particleBuilder = ((index) => child);

  @override
  void paint(Canvas canvas, Size size, double progress, seed) {
    canvas.save();
    canvas.rotate(initialRotation);
    for (int i = 0; i < numberOfParticles; i++) {
      particleBuilder(i).paint(canvas, size, progress, seed);
      canvas.rotate(pi / (numberOfParticles / 2));
    }
    canvas.restore();
  }
}

/// Mirrors a given particle around a circle.
///
/// When using the default constructor you specify one [Particle], this particle
/// is going to be used on its own, this implies that
/// all mirrored particles are identical (expect for the rotation around the circle)
class RectangleMirror extends Particle {
  final ParticleBuilder particleBuilder;

  /// Position of the first particle on the rect
  final double initialDistance;

  final int numberOfParticles;

  RectangleMirror.builder({required this.particleBuilder, required this.initialDistance, required this.numberOfParticles});

  RectangleMirror({required Particle child, required this.initialDistance, required this.numberOfParticles})
      : particleBuilder = ((index) => child);

  @override
  void paint(Canvas canvas, Size size, double progress, seed) {
    canvas.save();
    double totalLength = size.width * 2 + size.height * 2;
    double distanceBetweenParticles = totalLength / numberOfParticles;

    bool onHorizontalAxis = true;
    int side = 0;

    assert((distanceBetweenParticles * numberOfParticles).round() == totalLength.round());

    canvas.translate(-size.width / 2, -size.height / 2);

    double currentDistance = initialDistance;
    for (int i = 0; i < numberOfParticles; i++) {
      while (true) {
        if (onHorizontalAxis ? currentDistance > size.width : currentDistance > size.height) {
          currentDistance -= onHorizontalAxis ? size.width : size.height;
          onHorizontalAxis = !onHorizontalAxis;
          side = (++side) % 4;
        } else {
          if (side == 0) {
            assert(onHorizontalAxis);
            moveTo(canvas, size, 0, currentDistance, 0.0, () {
              particleBuilder(i).paint(canvas, size, progress, seed);
            });
          } else if (side == 1) {
            assert(!onHorizontalAxis);
            moveTo(canvas, size, 1, size.width, currentDistance, () {
              particleBuilder(i).paint(canvas, size, progress, seed);
            });
          } else if (side == 2) {
            assert(onHorizontalAxis);
            moveTo(canvas, size, 2, size.width - currentDistance, size.height, () {
              particleBuilder(i).paint(canvas, size, progress, seed);
            });
          } else if (side == 3) {
            assert(!onHorizontalAxis);
            moveTo(canvas, size, 3, 0.0, size.height - currentDistance, () {
              particleBuilder(i).paint(canvas, size, progress, seed);
            });
          }
          break;
        }
      }
      currentDistance += distanceBetweenParticles;
    }

    canvas.restore();
  }

  void moveTo(Canvas canvas, Size size, int side, double x, double y, VoidCallback painter) {
    canvas.save();
    canvas.translate(x, y);
    canvas.rotate(-atan2(size.width / 2 - x, size.height / 2 - y));
    painter();
    canvas.restore();
  }
}

/// Offsets a child by a given [Offset]
class PositionedParticle extends Particle {
  PositionedParticle({required this.position, required this.child});

  final Particle child;

  final Offset position;

  @override
  void paint(Canvas canvas, Size size, double progress, seed) {
    canvas.save();
    canvas.translate(position.dx, position.dy);
    child.paint(canvas, size, progress, seed);
    canvas.restore();
  }
}

/// Animates a childs position based on a Tween<Offset>
class AnimatedPositionedParticle extends Particle {
  AnimatedPositionedParticle({required Offset begin, required Offset end, required this.child}) : offsetTween = Tween<Offset>(begin: begin, end: end);

  final Particle child;

  final Tween<Offset> offsetTween;

  @override
  void paint(Canvas canvas, Size size, double progress, seed) {
    canvas.save();
    canvas.translate(offsetTween.lerp(progress).dx, offsetTween.lerp(progress).dy);
    child.paint(canvas, size, progress, seed);
    canvas.restore();
  }
}

/// Specifies an [Interval] for its child.
///
/// Instead of applying a curve the the input parameters of the paint method,
/// apply it with this Particle.
///
/// If you want you child to only animate from 0.0 - 0.5 (relative), specify an [Interval] with those values.
class IntervalParticle extends Particle {
  final Interval interval;

  final Particle child;

  IntervalParticle({required this.child, required this.interval});

  @override
  void paint(Canvas canvas, Size size, double progress, seed) {
    if (progress < interval.begin || progress > interval.end) return;
    child.paint(canvas, size, interval.transform(progress), seed);
  }
}

/// Does nothing else than holding a list of particles and painting them in that order
class CompositeParticle extends Particle {
  final List<Particle> children;

  CompositeParticle({required this.children});

  @override
  void paint(Canvas canvas, Size size, double progress, seed) {
    for (Particle particle in children) {
      particle.paint(canvas, size, progress, seed);
    }
  }
}

/// A particle which rotates the child.
///
/// Does not animate.
class RotationParticle extends Particle {
  final Particle child;

  final double rotation;

  RotationParticle({required this.child, required this.rotation});

  @override
  void paint(Canvas canvas, Size size, double progress, int seed) {
    canvas.save();
    canvas.rotate(rotation);
    child.paint(canvas, size, progress, seed);
    canvas.restore();
  }
}

/// A particle which rotates a child along a given [Tween]
class AnimatedRotationParticle extends Particle {
  final Particle child;

  final Tween<double> rotation;

  AnimatedRotationParticle({required this.child, required double begin, required double end}) : rotation = Tween<double>(begin: begin, end: end);

  @override
  void paint(Canvas canvas, Size size, double progress, int seed) {
    canvas.save();
    canvas.rotate(rotation.lerp(progress));
    child.paint(canvas, size, progress, seed);
    canvas.restore();
  }
}

/// Geometry
///
/// These are some basic geometric classes which also fade out as time goes on.
/// Each primitive should draw itself at the origin. If the orientation matters it should be directed to the top
/// (negative y)
///
/// A rectangle which also fades out over time.
class FadingRect extends Particle {
  final Color color;
  final double width;
  final double height;

  FadingRect({required this.color, required this.width, required this.height});

  @override
  void paint(Canvas canvas, Size size, double progress, seed) {
    canvas.drawRect(Rect.fromLTWH(0.0, 0.0, width, height), Paint()..color = color.withOpacity(1 - progress));
  }
}

/// A circle which fades out over time
class FadingCircle extends Particle {
  final Color color;
  final double radius;

  FadingCircle({required this.color, required this.radius});

  @override
  void paint(Canvas canvas, Size size, double progress, seed) {
    canvas.drawCircle(Offset.zero, radius, Paint()..color = color.withOpacity(1 - progress));
  }
}

/// A triangle which also fades out over time
class FadingTriangle extends Particle {
  /// This controls the shape of the triangle.
  ///
  /// Value between 0 and 1
  final double variation;

  final Color color;

  /// The size of the base side of the triangle.
  final double baseSize;

  /// This is the factor of how much bigger then length than the width is
  final double heightToBaseFactor;

  FadingTriangle({required this.variation, required this.color, required this.baseSize, required this.heightToBaseFactor});

  @override
  void paint(Canvas canvas, Size size, double progress, int seed) {
    Path path = Path();
    path.moveTo(0.0, 0.0);
    path.lineTo(baseSize * variation, baseSize * heightToBaseFactor);
    path.lineTo(baseSize, 0.0);
    path.close();
    canvas.drawPath(path, Paint()..color = color.withOpacity(1 - progress));
  }
}

/// An ugly looking "snake"
///
/// See for yourself
class FadingSnake extends Particle {
  final double width;
  final double segmentLength;
  final int segments;
  final double curvyness;

  final Color color;

  FadingSnake({required this.width, required this.segmentLength, required this.segments, required this.curvyness, required this.color});

  @override
  void paint(Canvas canvas, Size size, double progress, int seed) {
    canvas.save();
    canvas.rotate(pi / 6);
    Path path = Path();
    for (int i = 0; i < segments; i++) {
      path.quadraticBezierTo(curvyness * i, segmentLength * (i + 1), curvyness * (i + 1), segmentLength * (i + 1));
    }
    for (int i = segments - 1; i >= 0; i--) {
      path.quadraticBezierTo(curvyness * (i + 1), segmentLength * i - curvyness, curvyness * i, segmentLength * i - curvyness);
    }
    path.close();
    canvas.drawPath(path, Paint()..color = color);
    canvas.restore();
  }
}