#!/usr/bin/env python
# coding: utf-8

# In[33]:


# Data
amounts = [12.35, 12.35*2, 12.35*3, 12.35*4, 12.35*5, 12.35*6, 12.35*7, 12.35*8, 12.35*9, 12.35*10]

coin_list = [
    [0.2, 0.1, 0.05],
    [1, 0.2, 0.1, 0.05],
    [2, 1, 0.2, 0.1, 0.05],
    [5, 2, 1, 0.2, 0.1, 0.05],
    [5, 2, 1, 0.5, 0.2, 0.1, 0.05],
    [5, 2, 1, 0.5, 0.2, 0.1, 0.05, 0.02],
    [5, 2, 1, 0.5, 0.2, 0.1, 0.05, 0.02, 0.01]
]

import time
import matplotlib.pyplot as plt


# In[34]:


# Greedy
def greedy_make_change(amount, coins):
    i = 0
    change = []

    while amount > 0 and len(coins) > i:
        # print(str(round(amount//coins[i])) + " Coins of " + str(coins[i]) + "€")
        for j in range(round(amount//coins[i])):
            change.append(coins[i])
        amount = round(amount%coins[i], 2)
        i = i+1
    if amount > 0:
        print(f"Cannot make exact change for {amount:.2f}€")
    return change


# In[35]:


# Iterative

# Function to count occurrences of items in a list
def count_occurrences(array):
    counts = {}
    for item in array:
        if item in counts:
            counts[item] += 1  # If the item is already in the dictionary, increment its count
        else:
            counts[item] = 1  # If the item is not in the dictionary, add it with a count of 1
    return counts

# Function to calculate all combinations of coins to make a specific amount
def calculate_change_combinations(amount, coins):
    # Convert euro amounts to cents for calculations
    amount_cents = int(amount * 100)
    coin_values_cents = [int(coin * 100) for coin in coins]

    # Initialize a list to store combinations and their counts
    combinations = []
    stack = [(0, [], 0)]  # (current amount in cents, current combination, current coin index)

    while stack:
        current_amount, current_combination, current_coin_index = stack.pop()

        # If the current combination sums up to the target amount, add it to the list
        if current_amount == amount_cents:
            combinations.append(current_combination)
        # If the current amount is less than the target amount and there are more coins to consider
        elif current_amount < amount_cents and current_coin_index < len(coin_values_cents):
            coin = coin_values_cents[current_coin_index]
            max_count = (amount_cents - current_amount) // coin  # Maximum count of the current coin

            # Try adding different counts of the current coin to explore possibilities
            for count in range(max_count + 1):
                new_amount = current_amount + count * coin
                new_combination = current_combination + [coins[current_coin_index]] * count
                # Push the new state onto the stack for further exploration
                stack.append((new_amount, new_combination, current_coin_index + 1))

    # Print the total number of combinations
    # print(f"Total number of combinations: {len(combinations)}")
    return combinations


# In[36]:


# Recursive
def make_change_recursive(amount, coins, start, current_change, result):
    if amount == 0:
        result.append(current_change[:])
        return

    for i in range(start, len(coins)):
        coin_cents = round(coins[i] * 100)
        if amount >= coin_cents:
            current_change.append(coins[i])
            make_change_recursive(amount - coin_cents, coins, i, current_change, result)
            current_change.pop()

    return result


# In[50]:


greedy_times_coin_list = []
greedy_labels_coin_list = []

coins_list_copy = coin_list.copy()

for coins in coins_list_copy:
    start = time.perf_counter()
    greedy_make_change(amounts[0], coins)
    end = time.perf_counter()
    greedy_times_coin_list.append(end - start)
    greedy_labels_coin_list.append(str(len(coins)))

greedy_times_amount_list = []
greedy_labels_amount_list = []

# Plot a graph
plt.figure(figsize=(10, 6))

# NO BAR
plt.plot(greedy_labels_coin_list[1:], greedy_times_coin_list[1:], marker='o', linestyle='solid')
plt.xlabel('Coins')
plt.ylabel('Time (s)')
plt.title('Greedy Algorithm using different coins to make change for 12.35€')

plt.show()

for amount in amounts:
    start = time.perf_counter()
    greedy_make_change(amount, coin_list[-1])
    end = time.perf_counter()
    greedy_times_amount_list.append(end - start)
    greedy_labels_amount_list.append(str(amount))

# NO BAR
plt.plot(greedy_labels_amount_list[1:], greedy_times_amount_list[1:], marker='o', linestyle='solid')
plt.xlabel('Amount')
plt.ylabel('Time (s)')
plt.title('Greedy Algorithm using 9 coins to make change for different amounts')

plt.show()


# In[47]:


iterative_times_coin_list = []
iterative_labels_coin_list = []

coins_list_copy = coin_list.copy()

for coins in coins_list_copy[:5]:
    start = time.perf_counter()
    calculate_change_combinations(amounts[0], coins)
    end = time.perf_counter()
    iterative_times_coin_list.append(end - start)
    iterative_labels_coin_list.append(str(len(coins)))

# Plot a graph
plt.figure(figsize=(10, 6))

# NO BAR
plt.plot(iterative_labels_coin_list[1:], iterative_times_coin_list[1:], marker='o', linestyle='solid')
plt.xlabel('Coins')
plt.ylabel('Time (s)')
plt.title('Iterative Algorithm generating all the possibilities using different coins to make change for 12.35€')

plt.show()


# The complexity increase more when we add coins that are smaller. We can see that adding 0.02 and 0.01 have more impact than adding 2, and 5.

# In[46]:


recursive_times_coin_list = []
recursive_labels_coin_list = []

coins_list_copy = coin_list.copy()

for coins in coins_list_copy[:5]:
    # print(coins)
    start = time.perf_counter()
    make_change_recursive(amounts[0]*100, coins, 0, [], [])
    end = time.perf_counter()
    recursive_times_coin_list.append(end - start)
    recursive_labels_coin_list.append(str(len(coins)))

# Plot a graph
plt.figure(figsize=(10, 6))

# NO BAR
plt.plot(recursive_labels_coin_list[1:], recursive_times_coin_list[1:], marker='o', linestyle='solid')
plt.xlabel('Coins')
plt.ylabel('Time (s)')
plt.title('Recursive Algorithm generating all the possibilities using different coins to make change for 12.35€')

plt.show()


# Complexity seems polynomial or exponential, but it is not possible to test using this problem as it takes to much time to run on all the list of coins.

# In[45]:


# Export all results to json

import json

# Greedy
greedy_results = {
    "greedy_times_coin_list": greedy_times_coin_list,
    "greedy_labels_coin_list": greedy_labels_coin_list,
    "greedy_times_amount_list": greedy_times_amount_list,
    "greedy_labels_amount_list": greedy_labels_amount_list
}

with open('greedy_results.json', 'w') as fp:
    json.dump(greedy_results, fp)

# Iterative
iterative_results = {
    "iterative_times_coin_list": iterative_times_coin_list,
    "iterative_labels_coin_list": iterative_labels_coin_list
}

with open('iterative_results.json', 'w') as fp:
    json.dump(iterative_results, fp)

# Recursive
recursive_results = {
    "recursive_times_coin_list": recursive_times_coin_list,
    "recursive_labels_coin_list": recursive_labels_coin_list
}

with open('recursive_results.json', 'w') as fp:
    json.dump(recursive_results, fp)
    

