import random
import time
import csv
import os
import matplotlib.pyplot as plt
import numpy as np

from sys import setrecursionlimit

setrecursionlimit(20000)


def ll_insert(head, name, phone):
    new_node = {'name': name, 'phone': phone, 'next': None}
    if head is None:
        return new_node
    current = head
    while current:
        if current['name'] == name:
            current['phone'] = phone
            return head
        if current['next'] is None:
            break
        current = current['next']
    current['next'] = new_node
    return head


def ll_find(head, name):
    current = head
    while current:
        if current['name'] == name:
            return current['phone']
        current = current['next']
    return None


def ll_delete(head, name):
    if head is None:
        return None
    if head['name'] == name:
        return head['next']
    prev = head
    current = head['next']
    while current:
        if current['name'] == name:
            prev['next'] = current['next']
            return head
        prev = current
        current = current['next']
    return head


def ll_list_all(head):
    records = []
    current = head
    while current:
        records.append((current['name'], current['phone']))
        current = current['next']
    records.sort(key=lambda x: x[0])
    return records


def hash_function(name, size):
    return sum(ord(ch) for ch in name) % size


def ht_create(size=1000):
    return [None] * size


def ht_insert(buckets, name, phone):
    index = hash_function(name, len(buckets))
    buckets[index] = ll_insert(buckets[index], name, phone)


def ht_find(buckets, name):
    index = hash_function(name, len(buckets))
    return ll_find(buckets[index], name)


def ht_delete(buckets, name):
    index = hash_function(name, len(buckets))
    buckets[index] = ll_delete(buckets[index], name)


def ht_list_all(buckets):
    records = []
    for head in buckets:
        current = head
        while current:
            records.append((current['name'], current['phone']))
            current = current['next']
    records.sort(key=lambda x: x[0])
    return records


def bst_insert(root, name, phone):
    if root is None:
        return {'name': name, 'phone': phone, 'left': None, 'right': None}
    if name < root['name']:
        root['left'] = bst_insert(root['left'], name, phone)
    elif name > root['name']:
        root['right'] = bst_insert(root['right'], name, phone)
    else:
        root['phone'] = phone
    return root


def bst_find(root, name):
    if root is None:
        return None
    if name == root['name']:
        return root['phone']
    elif name < root['name']:
        return bst_find(root['left'], name)
    else:
        return bst_find(root['right'], name)


def bst_min_node(node):
    current = node
    while current and current['left']:
        current = current['left']
    return current


def bst_delete(root, name):
    if root is None:
        return None
    if name < root['name']:
        root['left'] = bst_delete(root['left'], name)
    elif name > root['name']:
        root['right'] = bst_delete(root['right'], name)
    else:
        if root['left'] is None:
            return root['right']
        elif root['right'] is None:
            return root['left']
        temp = bst_min_node(root['right'])
        root['name'] = temp['name']
        root['phone'] = temp['phone']
        root['right'] = bst_delete(root['right'], temp['name'])
    return root


def bst_list_all(root, result=None):
    if result is None:
        result = []
    if root:
        bst_list_all(root['left'], result)
        result.append((root['name'], root['phone']))
        bst_list_all(root['right'], result)
    return result


def generate_records(n, duplicate_prob=0.1):
    records = []
    for i in range(n):
        if random.random() < duplicate_prob and i > 0:
            name = records[random.randint(0, i - 1)][0]
        else:
            name = f"User_{random.randint(0, n * 2)}"
        phone = f"+7-999-{random.randint(1000000, 9999999)}"
        records.append((name, phone))
    return records


def run_experiment(structure_name, init_func, insert_func, find_func, delete_func, list_func, records, query_names,
                   delete_names):
    if structure_name == "HashTable":
        data = init_func()
    else:
        data = None

    start = time.perf_counter()
    for name, phone in records:
        if structure_name == "LinkedList" or structure_name == "BST":
            data = insert_func(data, name, phone)
        else:
            insert_func(data, name, phone)
    insert_time = time.perf_counter() - start

    start = time.perf_counter()
    for name in query_names:
        find_func(data, name)
    find_time = time.perf_counter() - start

    start = time.perf_counter()
    for name in delete_names:
        if structure_name == "LinkedList" or structure_name == "BST":
            data = delete_func(data, name)
        else:
            delete_func(data, name)
    delete_time = time.perf_counter() - start

    all_records = list_func(data)
    return insert_time, find_time, delete_time, len(all_records)


def main():
    N = 3000

    save_dir = r"C:\Users\User\2026-rff_mp\ShulpinIN\datastructure_lab1\docs\data"
    csv_path = os.path.join(save_dir, "results.csv")
    graph_path = os.path.join(save_dir, "performance_comparison.png")



    records_original = generate_records(N, duplicate_prob=0.05)
    records_shuffled = records_original.copy()
    random.shuffle(records_shuffled)
    records_sorted = sorted(records_original, key=lambda x: x[0])

    existing_names = list(set([r[0] for r in records_original]))
    query_names = random.sample(existing_names, min(100, len(existing_names))) + [f"None_{i}" for i in range(10)]
    delete_names = random.sample(existing_names, min(50, len(existing_names)))

    results = [["Structure", "Mode", "Operation", "Time(sec)"]]

    for mode_name, records in [("random", records_shuffled), ("sorted", records_sorted)]:
        for structure_name, init_func, insert_func, find_func, delete_func, list_func in [
            ("LinkedList", None, ll_insert, ll_find, ll_delete, ll_list_all),
            ("BST", None, bst_insert, bst_find, bst_delete, bst_list_all),
            ("HashTable", ht_create, ht_insert, ht_find, ht_delete, ht_list_all)
        ]:
            ins, fin, dlt, _ = run_experiment(structure_name, init_func, insert_func, find_func, delete_func, list_func,
                                              records, query_names, delete_names)
            results.append([structure_name, mode_name, "insert", ins])
            results.append([structure_name, mode_name, "search_110", fin])
            results.append([structure_name, mode_name, "delete_50", dlt])


    with open(csv_path, "w", newline="", encoding='utf-8') as f:
        writer = csv.writer(f)
        writer.writerows(results)

    print(f"Results saved to {csv_path}")


    structures = ["LinkedList", "HashTable", "BST"]

    random_insert = []
    random_search = []
    random_delete = []
    sorted_insert = []
    sorted_search = []
    sorted_delete = []

    for row in results[1:]:
        structure, mode, operation, time_val = row
        if mode == "random" and operation == "insert":
            random_insert.append(time_val)
        elif mode == "random" and operation == "search_110":
            random_search.append(time_val)
        elif mode == "random" and operation == "delete_50":
            random_delete.append(time_val)
        elif mode == "sorted" and operation == "insert":
            sorted_insert.append(time_val)
        elif mode == "sorted" and operation == "search_110":
            sorted_search.append(time_val)
        elif mode == "sorted" and operation == "delete_50":
            sorted_delete.append(time_val)

    # Построение и сохранение графика
    fig, axes = plt.subplots(1, 3, figsize=(18, 6))
    x = np.arange(len(structures))
    width = 0.35

    axes[0].bar(x - width / 2, random_insert, width, label="Random", color="steelblue")
    axes[0].bar(x + width / 2, sorted_insert, width, label="Sorted", color="coral")
    axes[0].set_xticks(x)
    axes[0].set_xticklabels(structures)
    axes[0].set_ylabel("Time (sec)")
    axes[0].set_title("Insert")
    axes[0].legend()
    axes[0].grid(True)

    axes[1].bar(x - width / 2, random_search, width, label="Random", color="steelblue")
    axes[1].bar(x + width / 2, sorted_search, width, label="Sorted", color="coral")
    axes[1].set_xticks(x)
    axes[1].set_xticklabels(structures)
    axes[1].set_ylabel("Time (sec)")
    axes[1].set_title("Search")
    axes[1].legend()
    axes[1].grid(True)

    axes[2].bar(x - width / 2, random_delete, width, label="Random", color="steelblue")
    axes[2].bar(x + width / 2, sorted_delete, width, label="Sorted", color="coral")
    axes[2].set_xticks(x)
    axes[2].set_xticklabels(structures)
    axes[2].set_ylabel("Time (sec)")
    axes[2].set_title("Delete")
    axes[2].legend()
    axes[2].grid(True)

    plt.tight_layout()


    plt.savefig(graph_path, dpi=300)
    print(f"Graph saved to {graph_path}")


    plt.show()


if __name__ == "__main__":
    main()