"""CS 106 21FA - Lab 4

Describe the module here. Fix the lab number above and the name below.
Delete the second Author line if working solo.

Simulation of population of individuals and how they reproduce, etc.
Original author: Prof. Victor Norman, August 7, 2019
Revisions by Ken Arnold (ka37), Fall 2021

Author: YOUR-NAME (yn123)
Author: PARTNER-NAME (pn31)
"""


import random


# Use this DEBUG constant to make debugging statements that can be "turned" off
# before submission.  A debugging print statement looks like this:
# if DEBUG:
#     print('This message printed only when DEBUG is True')
# TODO: change DEBUG to False before submitting, so that debugging print statements
# are not printed.
DEBUG = True


NUM_INITIAL_INDIVIDUALS = 10
HEALTH_VARIATION = 2.0  # plus or minus
DIE_AGE = 8
REPRODUCE_AGE = 1
CHANCE_OF_REPRODUCING = 0.35  # 35% chance of reproducing

NUM_ITERATIONS = 50 # FIXME for next year: call this NUM_TIME_STEPS instead

# List of individuals.
# Each individual is a dictionary with 4 items. Example:
# {
#    "gender": "M", # or "F"
#    "age": 2,
#    "health": 1.0,
#    "is_alive": True # or False
# }
individuals = []


def display_population_stats(individuals, counters):
    print(
        f'Died of poor health: {counters["num_died_of_poor_health"]}, Died of old age: '
        + f'{counters["num_died_of_old_age"]}, Born: {counters["num_born"]}, Total population: {len(individuals)}'
    )


def reset_daily_counters(counters):
    counters["num_died_of_old_age"] = 0
    counters["num_died_of_poor_health"] = 0
    counters["num_born"] = 0


def increment_age(individuals):
    """increment the age of each individual"""
    for i in range(len(individuals)):
        individual = individuals[i]
        individual["age"] += 1


def update_health(individuals):
    """update the health of each individual:
    Pick a random amount its health value should change."""
    for i in range(len(individuals)):
        individual = individuals[i]
        # TODO: adjust this individual's health by a random amount
        # between -HEALTH_VARIATION and HEALTH_VARIATION.


def update_alive(individuals, counters):
    """Update whether each individual is alive.

    An individual dies if it reaches the max age or has negative health.
    
    Keep track of the causes of death.
    If an individual dies of old age, make sure it's not also counted as dying of poor health.
    """
    for i in range(len(individuals)):
        individual = individuals[i]
        # TODO: check if this individual should die due to 
        # old age.  If so, 
        # - print a debugging message that the individual i died of old age
        # - change is_alive to False for that individual
        # - increment the correct counter
        # otherwise, check if their health is 0 or less. If so:
        # - print a debugging message that the individual i died of poor health
        # - change is_alive to False for that individual
        # - increment the correct counter


def find_male(individuals, males_already_paired):
    """Find the first available male individual.

    Parameters:
    - individuals: the population (a list of dictionaries)
    - males_already_paired: the indices of males that shouldn't be
        considered because they have already been paired
        
    Returns: the index of an eligible male, or None if none are found.
    """
    for male_index in range(len(individuals)):
        individual = individuals[male_index]
        # TODO: Check if this individual is an eligible male.
        # They must be: (1) male, (2) at least REPRODUCE_AGE age, and
        # (3) not already paired (i.e.,
        # `male_index in males_already_paired` should not be true)
        #
        # Replace the following condition with the correct condition:
        if False:
            return male_index

    # Return a special object to indicate that no candidate male was found.
    return None


def get_reproducing_pairs(individuals):
    """Go through the population and pair up females and males. Do
    not assign either a female or male when it is already assigned.
    Return a pair of the two lists -- females in the first list and 
    males in the second list. These are indices of the individuals
    in the main list.
    Return two empty lists if no pairs are found.
    """
    females = []
    males = []

    # Look for females of sufficient age.
    # Since we only look for a single match for each female,
    # no females will get doubly assigned. But we'll have to be careful about males.
    for female_index in range(len(individuals)):
        individual = individuals[female_index]
        # TODO: Check if this individual is an eligible female.
        # They must be: female and at least REPRODUCE_AGE age.
        # Replace the following condition with the correct condition:
        if False:
            # Look for a candidate male.
            male_index = find_male(individuals, males)
            if male_index != None:
                females.append(female_index)
                males.append(male_index)
            else:
                # No males are left, abort.
                break

    # Return the two lists.
    return females, males



def reproduce(individuals, counters):
    """
    Get reproducing pairs of females and males and create a new offspring based
    on CHANCE_OF_REPRODUCING.
    """
    females, males = get_reproducing_pairs(individuals)
    # TODO: add a debugging print statement here to print the results from 
    # the previous line.
    for i in range(len(females)):
        # Someday, perhaps: consider health of the female and male in the pair?

        # TODO: 
        # if a random number between 0 and 1 is less than the CHANCE_OF_REPRODUCING, then
        #   make a new offspring with age 0 (see the "initialize first population" code below for an example)
        #   add that offspring to the list of individuals
        #   then, increment the num_born counter
        #   and print a debugging message saying a new baby was born.
        ...


def remove_dead_individuals(individuals):
    """Remove any individuals from the population if they are not alive."""
    individuals[:] = [x for x in individuals if x["is_alive"]]


# ---------------- main -----------------

# Initialize first population of individuals
for i in range(NUM_INITIAL_INDIVIDUALS):
    individual = {
        "gender": random.choice(["M", "F"]),
        "age": random.randint(0, 6),
        "health": random.randint(1, 8),
        "is_alive": True,
    }
    individuals.append(individual)

# Initialize counters
counters = {}
reset_daily_counters(counters)

display_population_stats(individuals, counters)


# Main Loop
for i in range(NUM_ITERATIONS):
    print("-" * 25, "Day", i, "-" * 25)
    increment_age(individuals)
    update_health(individuals)
    update_alive(individuals, counters)
    remove_dead_individuals(individuals)
    # Stop if there are no individuals left.
    if len(individuals) == 0:
        break
    reproduce(individuals, counters)
    display_population_stats(individuals, counters)
    reset_daily_counters(counters)
