import random as rnd
import time
import csv
import matplotlib.pyplot as plt
#############################################################################################

def sort_list(name_list):
    l = len(name_list)
    for i in range(l - 1):
        for j in range(l - i - 1):
            if name_list[j][0] > name_list[j + 1][0]:
                name_list[j], name_list[j + 1] = name_list[j + 1], name_list[j]
    return name_list
 
def hash_key(name):
    h_key = sum(ord(ch) for ch in name)
    return h_key

def create_name_phone(i):
    name = f"User_{i:03d}"
    phone = f"{rnd.randint(100, 999)}-{rnd.randint(100, 999)}"
    return (name, phone)

def file_insert(results):
    with open("results.csv", "w", encoding = "utf-8-sig", newline = "") as file:
        writer = csv.writer(file)
        writer.writerows(results)
        
def drow(time, color_fun, j, text, axes):

    operation = [0, 1, 2, 3]
    x = [j + operation[i] for i in range(4)]
    y = []
    for key in time:
        y.append(time[key] * 1000)
    for i in range(4):
        axes[i].bar(x[i], y[i], width = 0.12,  color = color_fun, label = text)
        axes[i].set_ylabel("Время, мс") 
        axes[i].set_xticks([])
    #plt.bar(x, y, width = 0.12, color = color_fun, label = text)
    
        
        
###########################################################################################################################

def ll_insert(head, name, phone):
    next_node = {'name': name, 'phone': phone, 'next': None}
    if head is None: return next_node 

    running = head
    while running is not None:
        if running['name'] == name:
            running['phone'] = phone
            return head
        running = running['next']

    running = head
    while running['next'] is not None: running = running['next']
    running['next'] = next_node
    return head

def ll_find(head, name):
    running = head

    while True:
        if running['name'] == name:
            return running['phone']
        running = running['next']
        if running is None: break

    return None


def ll_delete(head, name):
    running = head
    
    if running['name'] == name: 
        return head['next']

    while running['next']['name'] != name: 
        running = running['next']
        if  running['next']['next'] is None:
            if running['next']['name'] != name:
                return head
    if running['next']['next'] is None: 
        running['next'] = None
    else: running['next'] = running['next']['next']

    return head
    

def ll_list_all(head):
    name_list = []
    running = head
    while running is not None:
        name_list.append((running['name'], running['phone']))     
        running = running['next']
    return name_list

################################################################################################################################


def LinkedList(head, phone_book):

    start_insert = time.perf_counter()
    for i in range(len(phone_book)):

        head = ll_insert(head, phone_book[i][0], phone_book[i][1])
    #print(head)
    end_insert = time.perf_counter()
    time_insert = end_insert - start_insert

    start_find = time.perf_counter()
    for _ in range(100):
        name = create_name_phone(rnd.randint(0, 999))[0]
        phone = ll_find(head, name)
        #print(name, ":", phone)
    end_find = time.perf_counter()
    time_find = end_find - start_find
    
    
    start_delete = time.perf_counter()
    for i in range(110):
        if i <= 99: name = f"User_{rnd.randint(0,999):03d}"
        else: name = f"None_{i:03d}"
        head = ll_delete(head, name)
    end_delete = time.perf_counter()
    time_delete = end_delete - start_delete
        
    
    start_list = time.perf_counter()    
    name_list = sort_list(ll_list_all(head))
    #print(*name_list)
    end_list = time.perf_counter()
    time_list = end_list - start_list
    
    return (time_insert, time_find, time_delete, time_list)

#########################################################################################################

def ht_insert(buckest, name, phone):
    index = hash_key(name) % len(buckest)
    for i, (Name, Phone) in enumerate(buckest[index]):
        if Name == name:
            buckest[index][i] = (name, phone)
            return buckest
    buckest[index].append((name, phone))
    return buckest

def ht_find(buckest, name):
    index = hash_key(name) % len(buckest)
    for (Name, Phone) in buckest[index]:
        if Name == name:
            return Phone
    return None

def ht_list_all(buckest):
    
    name_list = []

    for index in range(len(buckest)):
        for i, (name, phone) in enumerate(buckest[index]):
            name_list.append((name, phone))

    name_list = sort_list(name_list)

    return name_list


def ht_delete(buckest, name):
    index = hash_key(name) % len(buckest)
    for i, (Name, Phone) in enumerate(buckest[index]):
        if Name == name:
            del buckest[index][i]
    return buckest


####################################################################################################

def HashTable(buckest, phone_book):


    start_insert = time.perf_counter()
    for i in range(len(phone_book)):

        buckest = ht_insert(buckest, phone_book[i][0], phone_book[i][1])
    #print(buckest)
    end_insert = time.perf_counter()
    time_insert = end_insert - start_insert


    start_find = time.perf_counter()
    for _ in range(100):
        name = create_name_phone(rnd.randint(0, 999))[0]
        phone = ht_find(buckest, name)
        #print(name, ":", phone)
    end_find = time.perf_counter()
    time_find = end_find - start_find
    
    
    start_delete = time.perf_counter()
    for i in range(110):
        if i <= 99: name = f"User_{rnd.randint(0,999):03d}"
        else: name = f"None_{i:03d}"
        buckest = ht_delete(buckest, name)
    end_delete = time.perf_counter()
    time_delete = end_delete - start_delete
        
    
    start_list = time.perf_counter()    
    name_list = sort_list(ht_list_all(buckest))
    #print(*name_list)
    end_list = time.perf_counter()
    time_list = end_list - start_list
    
    return (time_insert, time_find, time_delete, time_list)

#################################################################################################

def bst_insert(root, name, phone):
    
    running = root

    if running is None:
        root = {'name': name, 'phone': phone, 'left': None, 'right': None}
        return root
    while True:
        node = hash_key(running['name'])
        sheet = hash_key(name)
        if node < sheet:
            if running['right'] is None:
                running['right'] = {'name': name, 'phone': phone, 'left': None, 'right': None}
                return root
            running = running['right']
        elif node > sheet:
            if running['left'] is None:
                running['left'] = {'name': name, 'phone': phone, 'left': None, 'right': None}
                return root
            running = running['left']
        else:
             running['phone'] = phone
             return root

def bst_find(root, name):

    running = root

    while running is not None:
        node = hash_key(running['name'])
        sheet = hash_key(name)
        if name == running['name']:
            return running['phone']
        elif node < sheet:
            running = running['right']
        else:
            running = running['left']

    return None

def bst_list_all(root, name_list = []):
    if root is None:
        return
    name_list.append((root['name'], root['phone']))
    bst_list_all(root['left'], name_list)
    bst_list_all(root['right'], name_list)
    name_list = sort_list(name_list)
    return name_list
    
def bst_delete(root, name):
    
    if root is None:
        return None
    if hash_key(name) < hash_key(root['name']):
        root['left'] = bst_delete(root['left'], name)
    elif hash_key(name) > hash_key(root['name']):
        root['right'] = bst_delete(root['right'], name)
    else:
        
        if root['left'] is None and root['right'] is None:
            return None
    
        if root['left'] is None:
            return root['right']
        if root['right'] is None:
            return root['left']
        
        min_node = root['right']
        while min_node['left'] is not None:
            min_node = min_node['left']
            
        root['name'] = min_node['name']
        root['phone'] = min_node['phone']

        root['right'] = bst_delete(root['right'], min_node['name'])            
    
    return root
    
#################################################################################################

def BinarySearchTree(root, phone_book):

    start_insert = time.perf_counter()
    for i in range(len(phone_book)):

        root = bst_insert(root, phone_book[i][0], phone_book[i][1])
    #print(buckest)
    end_insert = time.perf_counter()
    time_insert = end_insert - start_insert


    start_find = time.perf_counter()
    for _ in range(100):
        name = create_name_phone(rnd.randint(0, 999))[0]
        phone = bst_find(root, name)
        #print(name, ":", phone)
    end_find = time.perf_counter()
    time_find = end_find - start_find
    
    
    start_delete = time.perf_counter()
    for i in range(110):
        if i <= 99: name = f"User_{rnd.randint(0,999):03d}"
        else: name = f"None_{i:03d}"
        root = bst_delete(root, name)
    end_delete = time.perf_counter()
    time_delete = end_delete - start_delete
        
    
    start_list = time.perf_counter()    
    name_list = sort_list(bst_list_all(root))
    #print(*name_list)
    end_list = time.perf_counter()
    time_list = end_list - start_list
    
    return (time_insert, time_find, time_delete, time_list)

################################################################################################
def main():
    
    phone_book = []
    for i in range(1000):
        phone_book.append(create_name_phone(i))
    for _ in range(9000):
        phone_book.append(create_name_phone(rnd.randint(0, 999)))
    
    phone_book_not_sorted = phone_book.copy()
    rnd.shuffle(phone_book_not_sorted)
    
    phone_book_sorted = phone_book.copy()
    phone_book_sorted = sort_list(phone_book_sorted)
    replay = 10
    
    
    Time_ll_not_sorted = []
    Time_ll_sorted =  []
    
    Time_average_ll_not_sorted = {'insert': 0, 'find': 0, 'delete': 0, 'list': 0}
    Time_average_ll_sorted =  {'insert': 0, 'find': 0, 'delete': 0, 'list': 0}
    
    print("============================================ TESTING LINKEDLIST =====================================\n")
    print('Not sorted: ')
    for _ in range(replay):
        time_ll_not_sorted = LinkedList(None, phone_book_not_sorted)
        Time_ll_not_sorted.append({'insert': time_ll_not_sorted[0], 'find': time_ll_not_sorted[1], 'delete': time_ll_not_sorted[2], 'list': time_ll_not_sorted[3]})
        for i, key in enumerate(Time_average_ll_not_sorted):
            Time_average_ll_not_sorted[key] += time_ll_not_sorted[i]/replay
    for i in range(replay):
        print(Time_ll_not_sorted[i])
    print("Average:", Time_average_ll_not_sorted, "\n\n")
        
    print('Sorted:')    
    for _ in range(replay):
        time_ll_sorted = LinkedList(None, phone_book_sorted)
        Time_ll_sorted.append({'insert': time_ll_sorted[0], 'find': time_ll_sorted[1], 'delete': time_ll_sorted[2], 'list': time_ll_sorted[3]})
        for i, key in enumerate(Time_average_ll_sorted):
            Time_average_ll_sorted[key] += time_ll_sorted[i]/replay
    for i in range(replay):
        print(Time_ll_not_sorted[i])    
    print("Average:", Time_average_ll_sorted, "\n\n")
        
        
        
    Time_ht_not_sorted = []
    Time_ht_sorted = []

    Time_average_ht_not_sorted = {'insert': 0, 'find': 0, 'delete': 0, 'list': 0}
    Time_average_ht_sorted =  {'insert': 0, 'find': 0, 'delete': 0, 'list': 0} 
    
    print("============================================ TESTING HASHTABLE =====================================\n")
    print('Not sorted: ')
    for _ in range(replay):
        time_ht_not_sorted = HashTable([[] for _ in range(100)], phone_book_not_sorted)
        Time_ht_not_sorted.append({'insert': time_ht_not_sorted[0], 'find': time_ht_not_sorted[1], 'delete': time_ht_not_sorted[2], 'list': time_ht_not_sorted[3]})
        for i, key in enumerate(Time_average_ht_not_sorted):
            Time_average_ht_not_sorted[key] += time_ht_not_sorted[i]/replay
    for i in range(replay):
        print(Time_ht_not_sorted[i])
    print(f"Average: {Time_average_ht_not_sorted}\n\n")
    
    print('Sorted: ')
    for _ in range(replay):
        time_ht_sorted = HashTable([[] for _ in range(100)], phone_book_sorted)
        Time_ht_sorted.append({'insert': time_ht_sorted[0], 'find': time_ht_sorted[1], 'delete': time_ht_sorted[2], 'list': time_ht_sorted[3]})
        for i, key in enumerate(Time_average_ht_sorted):
            Time_average_ht_sorted[key] += time_ht_sorted[i]/replay
    for i in range(replay):
        print(Time_ht_sorted[i])
    print(f"Average: {Time_average_ht_sorted}\n\n")    
       
    
    
    Time_bst_not_sorted = []
    Time_bst_sorted = []

    Time_average_bst_not_sorted = {'insert': 0, 'find': 0, 'delete': 0, 'list': 0}
    Time_average_bst_sorted =  {'insert': 0, 'find': 0, 'delete': 0, 'list': 0}  
    
    print("============================================ TESTING BINARYSEARCHTREE =====================================\n")
    print('Not sorted: ')
    for _ in range(replay):
        time_bst_not_sorted = BinarySearchTree(None, phone_book_not_sorted)
        Time_bst_not_sorted.append({'insert': time_bst_not_sorted[0], 'find': time_bst_not_sorted[1], 'delete': time_bst_not_sorted[2], 'list': time_bst_not_sorted[3]})
        for i, key in enumerate(Time_average_bst_not_sorted):
            Time_average_bst_not_sorted[key] += time_bst_not_sorted[i]/replay
    for i in range(replay):
        print(Time_bst_not_sorted[i])
    print(f"Average: {Time_average_bst_not_sorted}\n\n")
    
    print('Sorted: ')
    for _ in range(replay):
        time_bst_sorted = BinarySearchTree(None, phone_book_sorted)
        Time_bst_sorted.append({'insert': time_bst_sorted[0], 'find': time_bst_sorted[1], 'delete': time_bst_sorted[2], 'list': time_bst_sorted[3]})
        for i, key in enumerate(Time_average_bst_sorted):
            Time_average_bst_sorted[key] += time_bst_sorted[i]/replay
    for i in range(replay):
        print(Time_bst_sorted[i])
    print(f"Average: {Time_average_bst_sorted}\n\n")    
    print("=============================================== END TESTING ================================================")
    
    results = [["Структура", "Режим", "Операция", "Время(мс)"]]
    for i in range(replay):
        results.append(["LinkedList", "Случайный", "вставка", Time_ll_not_sorted[i]["insert"]])
        results.append(["LinkedList", "Случайный", "поиск", Time_ll_not_sorted[i]["find"]])
        results.append(["LinkedList", "Случайный", "удаление", Time_ll_not_sorted[i]["delete"]])
        results.append(["LinkedList", "Случайный", "формирование списка", Time_ll_not_sorted[i]["list"]])
        
        results.append(["LinkedList", "Упорядоченный", "вставка", Time_ll_sorted[i]["insert"]])
        results.append(["LinkedList", "Упорядоченный", "поиск", Time_ll_sorted[i]["find"]])
        results.append(["LinkedList", "Упорядоченный", "удаление", Time_ll_sorted[i]["delete"]])
        results.append(["LinkedList", "Упорядоченный", "формирование списка", Time_ll_sorted[i]["list"]])
        
    for i in range(replay):
        results.append(["HashTable", "Случайный", "вставка", Time_ht_not_sorted[i]["insert"]])
        results.append(["HashTable", "Случайный", "поиск", Time_ht_not_sorted[i]["find"]])
        results.append(["HashTable", "Случайный", "удаление", Time_ht_not_sorted[i]["delete"]])
        results.append(["HashTable", "Случайный", "формирование списка", Time_ht_not_sorted[i]["list"]])
        
        results.append(["HashTable", "Упорядоченный", "вставка", Time_ht_sorted[i]["insert"]])
        results.append(["HashTable", "Упорядоченный", "поиск", Time_ht_sorted[i]["find"]])
        results.append(["HashTable", "Упорядоченный", "удаление", Time_ht_sorted[i]["delete"]])
        results.append(["HashTable", "Упорядоченный", "формирование списка", Time_ht_sorted[i]["list"]])

    for i in range(replay):
        results.append(["BinarySearchTree", "Случайный", "вставка", Time_bst_not_sorted[i]["insert"]])
        results.append(["BinarySearchTree", "Случайный", "поиск", Time_bst_not_sorted[i]["find"]])
        results.append(["BinarySearchTree", "Случайный", "удаление", Time_bst_not_sorted[i]["delete"]])
        results.append(["BinarySearchTree", "Случайный", "формирование списка", Time_bst_not_sorted[i]["list"]])
        
        results.append(["BinarySearchTree", "Упорядоченный", "вставка", Time_bst_sorted[i]["insert"]])
        results.append(["BinarySearchTree", "Упорядоченный", "поиск", Time_bst_sorted[i]["find"]])
        results.append(["BinarySearchTree", "Упорядоченный", "удаление", Time_bst_sorted[i]["delete"]])
        results.append(["BinarySearchTree", "Упорядоченный", "формирование списка", Time_bst_sorted[i]["list"]])
        
    for i in range(1, len(results) - 1):
        results[i][3] *= 1000    
    file_insert(results)
    
    fig, axes = plt.subplots(1, 4, figsize = (16, 5))
    manager = plt.get_current_fig_manager()
    manager.full_screen_toggle()


    drow(Time_average_ll_not_sorted, "#0800FF", -0.3, "LinkedList (not sorted)", axes) 
    drow(Time_average_ll_sorted, "#00C8FF", -0.18, "LinkedList (sorted)", axes)
    drow(Time_average_ht_not_sorted, "#0E7A13", -0.06, "HashTable (not sorted)", axes)
    drow(Time_average_ht_sorted, "#4DFF00", 0.06, "HashTable (sorted)", axes)
    drow(Time_average_bst_not_sorted, "#968C1A", 0.18, "BST (not sorted)", axes)
    drow(Time_average_bst_sorted, "#FBFF00", 0.30, "BST (sorted)", axes)
    
    operation = ['insert', 'find', 'delete', 'create list']
    
    for i in range(4):
        axes[i].set_title(operation[i])
    plt.legend(bbox_to_anchor = (1.05, 1), loc = "upper left")



    plt.subplots_adjust(bottom = 0.025, top = 0.95, left = 0.025, right = 0.875)
    plt.savefig("time_schedule.png")
    plt.show()

if __name__ == "__main__":
    main()




