import time
import random
import csv
import os
import sys

sys.setrecursionlimit(30000)

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


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


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


def ll_list_all(head):
    entries = []
    curr = head
    while curr is not None:
        entries.append((curr['name'], curr['phone']))
        curr = curr['next']
    entries.sort(key=lambda x: x[0])
    return entries

def _hash(name, bucket_count):
    h = 0
    for ch in name:
        h = (h * 31 + ord(ch)) % bucket_count
    return h


def ht_create(bucket_count=2000):
    return [None] * bucket_count


def ht_insert(buckets, name, phone):
    idx = _hash(name, len(buckets))
    buckets[idx] = ll_insert(buckets[idx], name, phone)


def ht_find(buckets, name):
    idx = _hash(name, len(buckets))
    return ll_find(buckets[idx], name)


def ht_delete(buckets, name):
    idx = _hash(name, len(buckets))
    buckets[idx] = ll_delete(buckets[idx], name)


def ht_list_all(buckets):
    entries = []
    for head in buckets:
        curr = head
        while curr is not None:
            entries.append((curr['name'], curr['phone']))
            curr = curr['next']
    entries.sort(key=lambda x: x[0])
    return entries

def bst_insert(root, name, phone):
    new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}
    if root is None:
        return new_node

    parent = None
    curr = root
    while curr is not None:
        parent = curr
        if name < curr['name']:
            curr = curr['left']
        elif name > curr['name']:
            curr = curr['right']
        else:
            curr['phone'] = phone
            return root

    if name < parent['name']:
        parent['left'] = new_node
    else:
        parent['right'] = new_node
    return root


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


def _bst_min_node(node):
    while node and node['left'] is not None:
        node = node['left']
    return node


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']
        if root['right'] is None:
            return root['left']
        min_node = _bst_min_node(root['right'])
        root['name'] = min_node['name']
        root['phone'] = min_node['phone']
        root['right'] = bst_delete(root['right'], min_node['name'])
    return root


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

def generate_test_data(n=10000):
    records = [(f"User_{i:05d}", f"+7-999-{i:05d}") for i in range(n)]
    records_sorted = records[:]
    records_shuffled = records[:]
    random.shuffle(records_shuffled)
    return records_sorted, records_shuffled

def measure_insert(struct_name, records):
    start = time.perf_counter()
    if struct_name == "LinkedList":
        head = None
        for name, phone in records:
            head = ll_insert(head, name, phone)
        obj = head
    elif struct_name == "HashTable":
        buckets = ht_create(bucket_count=2000)
        for name, phone in records:
            ht_insert(buckets, name, phone)
        obj = buckets
    elif struct_name == "BST":
        root = None
        for name, phone in records:
            root = bst_insert(root, name, phone)
        obj = root
    else:
        raise ValueError(f"Unknown structure: {struct_name}")
    elapsed = time.perf_counter() - start
    return elapsed, obj

def measure_find(obj, struct_name, existing_names, nonexisting_names):
    start = time.perf_counter()
    for name in existing_names:
        if struct_name == "LinkedList":
            ll_find(obj, name)
        elif struct_name == "HashTable":
            ht_find(obj, name)
        else: 
            bst_find(obj, name)
    for name in nonexisting_names:
        if struct_name == "LinkedList":
            ll_find(obj, name)
        elif struct_name == "HashTable":
            ht_find(obj, name)
        else:
            bst_find(obj, name)
    return time.perf_counter() - start

def measure_delete(obj, struct_name, names_to_delete):
    start = time.perf_counter()
    if struct_name == "LinkedList":
        for name in names_to_delete:
            obj = ll_delete(obj, name)
    elif struct_name == "HashTable":
        for name in names_to_delete:
            ht_delete(obj, name)
    else: 
        for name in names_to_delete:
            obj = bst_delete(obj, name)
    elapsed = time.perf_counter() - start
    return elapsed, obj

def run_experiment(n=10000, repeats=5):
    records_sorted, records_shuffled = generate_test_data(n)
    existing_names = [name for name, _ in records_sorted[:100]]
    nonexisting_names = [f"None_{i}" for i in range(10)]
    all_names = [name for name, _ in records_sorted]

    structures = ["LinkedList", "HashTable", "BST"]
    modes = [("shuffled", records_shuffled), ("sorted", records_sorted)]

    results = []  
    for struct_name in structures:
        for mode_name, records in modes:
            for rep in range(repeats):
                insert_time, obj = measure_insert(struct_name, records)
                results.append([struct_name, mode_name, "insert", rep+1, insert_time])

                find_time = measure_find(obj, struct_name, existing_names, nonexisting_names)
                results.append([struct_name, mode_name, "find", rep+1, find_time])

                random.seed(rep)
                to_delete = random.sample(all_names, 50)
                delete_time, obj = measure_delete(obj, struct_name, to_delete)
                results.append([struct_name, mode_name, "delete", rep+1, delete_time])

    return results

def save_results_to_csv(results, filename="docs/data/results.csv"):
    os.makedirs(os.path.dirname(filename), exist_ok=True)
    with open(filename, 'w', newline='', encoding='utf-8') as f:
        writer = csv.writer(f)
        writer.writerow(["Structure", "Mode", "Operation", "Repeat", "Time_sec"])
        writer.writerows(results)
    print(f"Результаты сохранены в {filename}")


def aggregate_results(results):
    from collections import defaultdict
    agg = defaultdict(list)
    for row in results:
        struct, mode, op, rep, t = row
        agg[(struct, mode, op)].append(t)
    means = {k: sum(v)/len(v) for k, v in agg.items()}
    return means


def plot_results(means, output_dir="docs"):
    try:
        import matplotlib.pyplot as plt
        import numpy as np
    except ImportError:
        print("Matplotlib не установлен. Графики не построены.")
        return

    operations = ["insert", "find", "delete"]
    structures = ["LinkedList", "HashTable", "BST"]
    modes = ["shuffled", "sorted"]

    fig, axes = plt.subplots(1, 3, figsize=(15, 5))
    for idx, op in enumerate(operations):
        ax = axes[idx]
        x = np.arange(len(structures))
        width = 0.35
        shuffled_means = [means.get((struct, "shuffled", op), 0) for struct in structures]
        sorted_means = [means.get((struct, "sorted", op), 0) for struct in structures]
        ax.bar(x - width/2, shuffled_means, width, label='случайный порядок', color='skyblue')
        ax.bar(x + width/2, sorted_means, width, label='отсортированный порядок', color='salmon')
        ax.set_xticks(x)
        ax.set_xticklabels(structures, rotation=15)
        ax.set_ylabel('Время (сек)')
        ax.set_title(f'{op.upper()}')
        ax.legend()
        ax.grid(axis='y', linestyle='--', alpha=0.7)

    plt.tight_layout()
    plt.savefig(os.path.join(output_dir, "comparison.png"), dpi=150)
    plt.show()

if __name__ == "__main__":
    results = run_experiment(n=10000, repeats=5)
    save_results_to_csv(results)
    means = aggregate_results(results)
    print("\nСреднее время по операциям (сек):")
    for (struct, mode, op), t in sorted(means.items()):
        print(f"{struct:12} {mode:8} {op:6} : {t:.6f}")
    plot_results(means)