import 'dart:ui';
import 'dart:math' as math;

import 'package:ecosim/src/common/models/pvector.dart';
import 'package:ecosim/src/ecosim/models/food.dart';
import 'package:ecosim/src/ecosim/models/poison.dart';
import 'package:ecosim/src/ecosim/models/predator.dart';
import 'package:ecosim/src/ecosim/models/world_object.dart';
import 'package:ecosim/src/ecosim/utils.dart';
import 'package:flutter/material.dart';

import 'package:collection/collection.dart';

import 'boid_base.dart';

// Implementation of the Boid
// A Boid represents the prey in the simulation
// Descriptions for the overwritten functions can
// be found in the BoidBaseBehavior class
class Boid extends WorldObject implements BoidBaseBehavior {
  PVector acceleration = PVector(0, 0);
  PVector velocity = getRandUnitVec2();

  // set the boids render size in pixel
  // defaults to 2
  final double r;

  // health of the boid
  double health;
  // max health of the boid
  final double maxHealth;
  // probability for the boid to replicate
  final double replicationProbability;
  // damage the boid takes on each world tick
  // when the simulation is running
  final double tickDamage;

  // timestamp when the boid was spawned
  final int birthTimestamp;
  // last replication timestamp
  // used as check that boids can only
  // replicate after being alive for a certain
  // amount
  int lastReplicationTimestamp = 0;
  // default hardcoded time a boid needs to
  // be alive to replicate
  final int cloneDelay = 10000;
  // threshold for the chance of mutation
  final double mutationThreshold;

  BoidBaseDna dna;
  // the color the boid is drawn in if healthy
  final Color healthyColor;
  // the color the boid is drawn in if unhealthy
  final Color unhealthyColor;

  Boid({
    required super.position,
    required this.velocity,
    required this.tickDamage,
    required this.health,
    required this.maxHealth,
    required this.replicationProbability,
    required this.birthTimestamp,
    this.lastReplicationTimestamp = 0,
    required this.dna,
    required this.healthyColor,
    required this.unhealthyColor,
    this.r = 2,
    required this.mutationThreshold,
  });

  // Clone function to create
  // A copy of a boid (with a chance of mutation,
  // a small change for the DNA values)
  // Based on the programmed chance it can return
  // null if the boid is not replicated
  Boid? clone() {
    if (DateTime.now().millisecondsSinceEpoch - birthTimestamp < cloneDelay ||
        DateTime.now().millisecondsSinceEpoch - lastReplicationTimestamp <
            cloneDelay) {
      return null;
    }
    final rng = math.Random();
    final replicationThreshold = 1.0 - replicationProbability;
    if (rng.getRandomDoubleInRange(0.0, 1.0) <= replicationThreshold &&
        health > maxHealth / 2) {
      final newDna = BoidBaseDna(
        steeringForce: dna.steeringForce,
        speed: dna.speed,
        foodPull: dna.foodPull,
        poisonPull: dna.poisonPull,
        predatorPull: dna.predatorPull,
        foodPerceptionRadius: dna.foodPerceptionRadius,
        poisonPerceptionRadius: dna.poisonPerceptionRadius,
        predatorPerceptionRadius: dna.predatorPerceptionRadius,
        mutationThreshold: mutationThreshold,
      );
      newDna.mutate();
      lastReplicationTimestamp = DateTime.now().millisecondsSinceEpoch;
      return Boid(
        position: position,
        dna: newDna,
        velocity: velocity,
        tickDamage: tickDamage,
        health: maxHealth,
        maxHealth: maxHealth,
        replicationProbability: replicationProbability,
        birthTimestamp: DateTime.now().millisecondsSinceEpoch,
        healthyColor: healthyColor,
        unhealthyColor: unhealthyColor,
        mutationThreshold: mutationThreshold,
      );
    } else {
      return null;
    }
  }

  @override
  void update() {
    if (health > 100) health = 100;
  }

  @override
  void updateBoidTransform() {
    velocity = velocity + acceleration;
    velocity.normalize();
    acceleration = PVector(0, 0);

    final newTranslation =
        position + PVector(velocity.x, velocity.y) * dna.speed;
    position.set(newTranslation.x, newTranslation.y);
  }

  @override
  void updateBoidDirection(
    List<List<Object>> objects,
    Size worldSize,
  ) {
    final closestFood = getClosestItem(
      position,
      dna.foodPerceptionRadius,
      objects.firstWhereOrNull((element) => element.firstOrNull is Food),
      worldSize,
    );
    final closestPoison = getClosestItem(
      position,
      dna.poisonPerceptionRadius,
      objects.firstWhereOrNull((element) => element.firstOrNull is Poison),
      worldSize,
    );
    final closestPredator = getClosestItem(
      position,
      dna.predatorPerceptionRadius,
      objects
          .firstWhereOrNull((element) => element.firstOrNull is PredatorBoid),
      worldSize,
    );
    if (closestFood != null) {
      PVector steer = getSteeringForce(closestFood, position, velocity) *
          dna.steeringForce.abs() *
          dna.foodPull;
      acceleration.add(steer);
    }
    if (closestPoison != null) {
      PVector steer = getSteeringForce(closestPoison, position, velocity) *
          dna.steeringForce.abs() *
          dna.poisonPull;
      acceleration.add(steer);
    }
    if (closestPredator != null) {
      PVector steer = getSteeringForce(closestPredator, position, velocity) *
          dna.steeringForce.abs() *
          dna.predatorPull;
      acceleration.add(steer);
    }
  }

  @override
  void boundaryBoidsDirectionUpdate(Size size) {
    var (x, y) = (position.x, position.y);
    var (minX, minY, maxX, maxY) = (0, 0, size.width, size.height);

    if (x <= minX || x >= maxX || y <= minY || y >= maxY) {
      // completely cancel current acceleration to stop the boid
      // this is needed, a boid sometimes could be moving too fast
      // and would otherwise shortly break through the boundary
      acceleration.mult(0);
      // add new acceleration to try to move away from the boundary
      acceleration += getSteeringForce(
              PVector(size.width / 2, size.height / 2), position, velocity) *
          0.001;
    }
  }

  @override
  PVector? getClosestItem(
    PVector origin,
    double range,
    List<Object>? items,
    Size worldSize,
  ) {
    double closestItemDist = double.infinity;
    WorldObject? closestItem;

    if (items == null) return null;
    for (int i = 0; i <= items.length - 1; i++) {
      final worldObject = items[i] as WorldObject;
      if (worldObject.position.x <= 0 ||
          worldObject.position.x >= worldSize.width ||
          worldObject.position.y <= 0 ||
          worldObject.position.y >= worldSize.height) break;
      final dist = worldObject.position.distanceTo(origin);
      if (dist <= closestItemDist && dist <= range) {
        closestItemDist = dist;
        closestItem = worldObject;
      }
    }

    if (closestItemDist <= 2) {
      handleBoidCollision(items, closestItem);
    }

    return closestItem?.position;
  }

  @override
  void handleBoidCollision(List<Object> items, Object? collisionObject) {
    if (collisionObject is Food) {
      // debugPrint('ATE FOOD with health: $health');
      health += collisionObject.nutrition;
      items.remove(collisionObject);
      // debugPrint('ATE FOOD with food affinity: ${dna.foodPull}');
      // debugPrint('AFTER FOOD health: $health');
    }
    if (collisionObject is Poison) {
      // print("ATE POISON with health: $health");
      health -= collisionObject.damage;
      items.remove(collisionObject);
      // print("AFTER POISON health: $health");
    }
  }

  @override
  void healthTick() {
    health -= tickDamage * ((dna.speed >= 1) ? 1 : dna.speed) * 0.5;
  }

  @override
  bool get dead => health <= 0;

  @override
  void render(Canvas canvas) {
    var angle = velocity.heading + math.pi / 2;
    canvas.save();
    canvas.translate(position.x.toDouble(), position.y.toDouble());

    // Draw a triangle rotated in the direction of velocity
    var white = Paint()..color = const Color(0xFFFFFFFF);
    final Color lifeColor =
        Color.lerp(unhealthyColor, healthyColor, health / 100)!;

    final vertices = Vertices(
      VertexMode.triangles,
      [
        Offset(0, -r),
        Offset(-r, r * 2),
        Offset(r, r * 2),
      ],
      colors: [
        lifeColor,
        lifeColor,
        lifeColor,
      ],
    );
    canvas.rotate(angle);

    canvas.drawVertices(vertices, BlendMode.src, white);

    canvas.restore();
  }

  @override
  void renderDebug(Canvas canvas) {
    var angle = velocity.heading + math.pi / 2;
    canvas.save();
    canvas.translate(position.x.toDouble(), position.y.toDouble());

    const textStyle = TextStyle(
      color: Colors.white,
      fontSize: 12,
    );
    final textSpan = TextSpan(
      // render the health of the boid:
      text: '${health.roundToDouble()}',
      style: textStyle,
    );
    final textPainter = TextPainter(
      text: textSpan,
      textDirection: TextDirection.ltr,
    );
    textPainter.layout(minWidth: 0, maxWidth: 100);
    textPainter.paint(canvas, const Offset(10, 10));

    canvas.rotate(angle);

    var circlePaintFood = Paint()
      ..color = const Color(0xFF00FF00)
      ..style = PaintingStyle.stroke;
    var circlePaintPoison = Paint()
      ..color = const Color(0xFFFF0000)
      ..style = PaintingStyle.stroke;
    var circlePaintPredator = Paint()
      ..color = const Color.fromARGB(255, 212, 0, 255)
      ..style = PaintingStyle.stroke;
    canvas.drawCircle(
      Offset.zero,
      dna.foodPerceptionRadius.toDouble(),
      circlePaintFood,
    );
    canvas.drawCircle(
      Offset.zero,
      dna.poisonPerceptionRadius.toDouble(),
      circlePaintPoison,
    );
    canvas.drawCircle(
      Offset.zero,
      dna.predatorPerceptionRadius.toDouble(),
      circlePaintPredator,
    );

    canvas.drawLine(
      Offset.zero,
      Offset(
        0,
        dna.foodPull * -20,
      ),
      circlePaintFood,
    );
    canvas.drawLine(
      Offset.zero,
      Offset(
        0,
        dna.poisonPull * -20,
      ),
      circlePaintPoison,
    );
    canvas.restore();
  }
}
