import 'package:ecosim/src/ecosim/models/boid.dart';
import 'package:ecosim/src/ecosim/models/boid_base.dart';
import 'package:ecosim/src/ecosim/models/predator.dart';
import 'package:ecosim/src/common/models/pvector.dart';
import 'package:ecosim/src/ecosim/models/predator_dna.dart';
import 'package:ecosim/src/ecosim/models/world_object.dart';
import 'package:ecosim/src/ecosim/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'dart:math' as math;

import 'models/food.dart';
import '../seeking/models/pointer_indicator.dart';
import 'models/poison.dart';

// Runs all the logic of the ecosystem simulation
class EcosystemSimulation extends ChangeNotifier {
  final TickerProvider vsync;
  late Ticker _ticker;
  BuildContext context;
  // final double _time = 0.0;
  final List<Boid> boids = [];
  final List<PredatorBoid> predators = [];
  PointerIndicator pointerIndicator = PointerIndicator(
    position: PVector(
      0.0,
      0.0,
    ),
  );
  List<Food> food = [];
  List<Poison> poison = [];

  // boid configuration
  final int boidStartCount;
  final List<num>? boidPresetDNA;
  final Color boidHealthyColor;
  final Color boidUnhealthyColor;
  final double boidTickDamage;
  final double boidReplicateInterval;
  final double boidReplicateProbability;
  final double boidMutationThreshold;
  final double boidCollisionRadius;
  final double boidMaxHealth;

  // predator configuration
  final int predatorStartCount;
  final double predatorBoidNutrition;
  final double predatorCollisionRadius;
  final Color predatorHealthyColor;
  final Color predatorUnhealthyColor;

  // world configuration
  final double worldFoodNutrition;
  final double worldPoisonDamage;
  final int worldFoodStartCount;
  final int worldPoisonStartCount;
  final double worldReplicationRadiusFood;
  final double worldReplicationRadiusPoison;
  final double worldReplicationRadiusCooldown;
  final double worldFoodDecayRate;
  final double worldPoisonDecayRate;

  // decay timers

  final Size screenSize;

  EcosystemSimulation({
    required this.vsync,
    required this.context,
    required this.worldPoisonDamage,
    required this.boidStartCount,
    required this.predatorStartCount,
    required this.worldFoodStartCount,
    required this.worldPoisonStartCount,
    this.boidPresetDNA,
    required this.screenSize,
    required this.predatorBoidNutrition,
    required this.predatorCollisionRadius,
    required this.predatorHealthyColor,
    required this.predatorUnhealthyColor,
    required this.worldReplicationRadiusFood,
    required this.worldReplicationRadiusPoison,
    required this.worldReplicationRadiusCooldown,
    required this.worldFoodDecayRate,
    required this.worldPoisonDecayRate,
    required this.boidHealthyColor,
    required this.boidUnhealthyColor,
    required this.boidTickDamage,
    required this.boidReplicateInterval,
    required this.boidReplicateProbability,
    required this.boidMutationThreshold,
    required this.boidCollisionRadius,
    required this.worldFoodNutrition,
    required this.boidMaxHealth,
  }) {
    _ticker = vsync.createTicker(_onEachTick)..start();
    final rng = math.Random();
    for (var i = 0; i < boidStartCount; i++) {
      final dna = BoidBaseDna.rand(mutationThreshold: boidMutationThreshold);
      double x = rng.nextDouble() * screenSize.width;
      double y = rng.nextDouble() * screenSize.height;
      boids.add(
        Boid(
          position: PVector(x, y),
          dna: dna,
          velocity: PVector(0, -2),
          tickDamage: boidTickDamage,
          health: boidMaxHealth,
          maxHealth: boidMaxHealth,
          replicationProbability: boidReplicateProbability,
          birthTimestamp: DateTime.now().millisecondsSinceEpoch,
          healthyColor: boidHealthyColor,
          unhealthyColor: boidUnhealthyColor,
          mutationThreshold: boidMutationThreshold,
        ),
      );
    }
    for (var i = 0; i < predatorStartCount; i++) {
      final predatorDna = PredatorDna.rand();
      final dna = BoidBaseDna.rand(mutationThreshold: boidMutationThreshold);
      double x = rng.nextDouble() * screenSize.width;
      double y = rng.nextDouble() * screenSize.height;
      predators.add(
        PredatorBoid(
          position: PVector(x, y),
          predatorDna: predatorDna,
          predatorBoidNutrition: predatorBoidNutrition,
          velocity: PVector(0, -2),
          dna: dna,
          health: boidMaxHealth,
          tickDamage: boidTickDamage,
          maxHealth: boidMaxHealth,
          replicationProbability: boidReplicateProbability,
          birthTimestamp: DateTime.now().millisecondsSinceEpoch,
          healthyColor: predatorHealthyColor,
          unhealthyColor: predatorUnhealthyColor,
          mutationThreshold: boidMutationThreshold,
        ),
      );
    }
    for (var i = 0; i < worldFoodStartCount; i++) {
      double x = rng.nextDouble() * screenSize.width;
      double y = rng.nextDouble() * screenSize.height;
      // print('ADD FOOD: $x, $y');
      food.add(Food(
        position: PVector(x, y),
        nutrition: worldFoodNutrition,
        foodDecayRate: worldFoodDecayRate,
        startNutrition: worldFoodNutrition,
      ));
    }
    for (var i = 0; i < worldPoisonStartCount; i++) {
      double x = rng.nextDouble() * screenSize.width;
      double y = rng.nextDouble() * screenSize.height;
      // print('ADD FOOD: $x, $y');
      poison.add(
        Poison(
          position: PVector(x, y),
          damage: worldPoisonDamage,
          poisonDecayRate: worldPoisonDecayRate,
          startDamage: worldPoisonDamage,
        ),
      );
    }
  }

  void worldTick() {
    for (var i = 0; i <= food.length - 1; i++) {
      food[i].update();
      if (food[i].dead) {
        food.removeAt(i);
      }
    }
    for (var i = 0; i <= poison.length - 1; i++) {
      poison[i].update();
      if (poison[i].dead()) {
        poison.removeAt(i);
      }
    }
    for (var i = 0; i <= boids.length - 1; i++) {
      boids[i].healthTick();
    }
    for (var i = 0; i <= predators.length - 1; i++) {
      predators[i].healthTick();
    }
  }

  void populateBoidsTick() {
    if (boids.isEmpty) {
      final rng = math.Random();
      for (int i = 0; i < boidStartCount; i++) {
        final dna = BoidBaseDna.rand(mutationThreshold: boidMutationThreshold);
        double x = rng.nextDouble() * screenSize.width;
        double y = rng.nextDouble() * screenSize.height;
        boids.add(
          Boid(
            position: PVector(x, y),
            dna: dna,
            velocity: PVector(0, -2),
            tickDamage: boidTickDamage,
            health: boidMaxHealth,
            maxHealth: boidMaxHealth,
            replicationProbability: boidReplicateProbability,
            birthTimestamp: DateTime.now().millisecondsSinceEpoch,
            healthyColor: boidHealthyColor,
            unhealthyColor: boidUnhealthyColor,
            mutationThreshold: boidMutationThreshold,
          ),
        );
      }
    }
    if (predators.isEmpty) {
      final rng = math.Random();
      for (int i = 0; i < predatorStartCount; i++) {
        final dna = BoidBaseDna.rand(mutationThreshold: boidMutationThreshold);
        final predatorDna = PredatorDna.rand();
        double x = rng.nextDouble() * screenSize.width;
        double y = rng.nextDouble() * screenSize.height;
        predators.add(
          PredatorBoid(
              position: PVector(x, y),
              dna: dna,
              predatorBoidNutrition: predatorBoidNutrition,
              velocity: PVector(0, -2),
              tickDamage: boidTickDamage,
              health: boidMaxHealth,
              maxHealth: boidMaxHealth,
              replicationProbability: boidReplicateProbability,
              birthTimestamp: DateTime.now().millisecondsSinceEpoch,
              healthyColor: boidHealthyColor,
              unhealthyColor: boidUnhealthyColor,
              predatorDna: predatorDna,
              mutationThreshold: boidMutationThreshold),
        );
      }
    }
  }

  void replicateBoidsTick() {
    if (boids.length < boidStartCount) {
      int i = boids.length;
      while (i < boidStartCount) {
        // check for cloning
        if (boids.length < boidStartCount) {
          final rng = math.Random();
          for (int x = 0; x < boids.length - 1; x++) {
            Boid? newBoid = boids[x].clone();

            if (newBoid != null && boids[x].health > 0.8) {
              // random polar
              final theta = rng.nextDouble() * 2 * math.pi;
              final r = rng.getRandomDoubleInRange(-5, 5);
              final polarX = boids[x].position.x + r * math.cos(theta);
              final polarY = boids[x].position.y + r * math.sin(theta);

              // Check if the new objects position is inside of the simulation area
              if (polarY <= 0 ||
                  polarY >= screenSize.height ||
                  polarX <= 0 ||
                  polarX >= screenSize.width) continue;

              boids.add(
                Boid(
                    position: PVector(polarX, polarY),
                    dna: newBoid.dna,
                    velocity: PVector(0, -2),
                    tickDamage: boidTickDamage,
                    health: boidMaxHealth,
                    maxHealth: boidMaxHealth,
                    replicationProbability: boidReplicateProbability,
                    birthTimestamp: DateTime.now().millisecondsSinceEpoch,
                    healthyColor: boidHealthyColor,
                    unhealthyColor: boidUnhealthyColor,
                    mutationThreshold: boidMutationThreshold),
              );
              i++;
            } else {
              i++;
            }
          }
        }
        i++;
      }
    }
    if (predators.length < predatorStartCount) {
      int i = predators.length;
      while (i < predatorStartCount) {
        // check for cloning
        if (predators.length < predatorStartCount) {
          final rng = math.Random();
          int randomIndex =
              rng.getRandomDoubleInRange(0, predators.length - 1).toInt();
          PredatorBoid? newBoid = predators[randomIndex].clone();

          if (newBoid != null && predators[randomIndex].health > 0.8) {
            // random polar
            final theta = rng.nextDouble() * 2 * math.pi;
            final r = rng.getRandomDoubleInRange(-5, 5);
            final polarX = boids[randomIndex].position.x + r * math.cos(theta);
            final polarY = boids[randomIndex].position.y + r * math.sin(theta);

            // Check if the new objects position is inside of the simulation area
            if (polarY <= 0 ||
                polarY >= screenSize.height ||
                polarX <= 0 ||
                polarX >= screenSize.width) continue;

            predators.add(
              PredatorBoid(
                  position: PVector(polarX, polarY),
                  dna: newBoid.dna,
                  predatorDna: newBoid.predatorDna,
                  predatorBoidNutrition: predatorBoidNutrition,
                  velocity: PVector(0, -2),
                  tickDamage: boidTickDamage,
                  health: boidMaxHealth,
                  maxHealth: boidMaxHealth,
                  replicationProbability: boidReplicateProbability,
                  birthTimestamp: DateTime.now().millisecondsSinceEpoch,
                  healthyColor: boidHealthyColor,
                  unhealthyColor: boidUnhealthyColor,
                  mutationThreshold: boidMutationThreshold),
            );
          }
        }
        i++;
      }
    }
  }

  void replicateWorldObject(List<Object> items, int startCount) {
    final rng = math.Random();
    if (rng.getRandomDoubleInRange(0.0, 1.0) < 0.2) return;
    int itemCountToAdd = startCount - items.length;
    while (itemCountToAdd > 0) {
      final randomItem = items[rng.nextInt(items.length)] as WorldObject;
      // random polar
      final theta = rng.nextDouble() * 2 * math.pi;
      final r = (items.first is Food)
          ? rng.getRandomDoubleInRange(
              -worldReplicationRadiusFood,
              worldReplicationRadiusFood,
            )
          : rng.getRandomDoubleInRange(
              -worldReplicationRadiusPoison,
              worldReplicationRadiusPoison,
            );
      final polarX = randomItem.position.x + r * math.cos(theta);
      final polarY = randomItem.position.y + r * math.sin(theta);

      // Check if the new objects position is inside of the simulation area
      if (polarY <= 0 ||
          polarY >= screenSize.height ||
          polarX <= 0 ||
          polarX >= screenSize.width) continue;

      // Probability to replicate at the center > edges
      final double distToCenter = PVector(polarX, polarY).distanceTo(
        PVector(screenSize.width / 2, screenSize.height / 2),
      );
      if (rng.getRandomDoubleInRange(0.0, 1.0) < distToCenter / 1000000.0) {
        continue;
      }

      items.add(
        (items.first is Food)
            ? Food(
                position: PVector(polarX, polarY),
                nutrition: worldFoodNutrition,
                foodDecayRate: worldFoodDecayRate,
                startNutrition: worldFoodNutrition,
              )
            : Poison(
                position: PVector(polarX, polarY),
                damage: worldPoisonDamage,
                poisonDecayRate: worldPoisonDecayRate,
                startDamage: worldPoisonDamage,
              ),
      );
      itemCountToAdd--;
    }
  }

  void replicateConsumablesTick() {
    if (food.length < worldFoodStartCount && food.isNotEmpty) {
      replicateWorldObject(food, worldFoodStartCount);
    }
    if (poison.length < worldPoisonStartCount && food.isNotEmpty) {
      replicateWorldObject(poison, worldPoisonStartCount);
    }
  }

  void _onEachTick(Duration deltaTime) {
    for (var i = boids.length - 1; i >= 0; i--) {
      boids[i].update();
      boids[i].updateBoidDirection(
        [
          food,
          poison,
          predators,
        ],
        screenSize,
      );
      boids[i].boundaryBoidsDirectionUpdate(screenSize);
      boids[i].updateBoidTransform();
      boids[i].boundaryBoidsDirectionUpdate(screenSize);

      if (boids[i].dead) {
        food.add(
          Food(
            position: PVector(
              boids[i].position.x.toDouble(),
              boids[i].position.y.toDouble(),
            ),
            nutrition: worldFoodNutrition,
            foodDecayRate: worldFoodDecayRate,
            startNutrition: worldFoodNutrition,
          ),
        );
        boids.removeAt(i);
      }
    }

    for (var i = predators.length - 1; i >= 0; i--) {
      predators[i].update();
      predators[i].updateBoidDirection([boids], screenSize);
      predators[i].boundaryBoidsDirectionUpdate(screenSize);
      predators[i].updateBoidTransform();
      predators[i].boundaryBoidsDirectionUpdate(screenSize);

      if (predators[i].dead) {
        food.add(
          Food(
            position: PVector(
              predators[i].position.x.toDouble(),
              predators[i].position.y.toDouble(),
            ),
            nutrition: worldFoodNutrition,
            foodDecayRate: worldFoodDecayRate,
            startNutrition: worldFoodNutrition,
          ),
        );
        predators.removeAt(i);
      }
    }
    notifyListeners();
  }

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