import time
import random
import csv
import matplotlib.pyplot as plt
import numpy as np
import sys
from collections import defaultdict

# Увеличиваем лимит рекурсии для BST
sys.setrecursionlimit(10000)


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

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 not head:
        return None
    if head['name'] == name:
        return head['next']
    current = head
    while current['next']:
        if current['next']['name'] == name:
            current['next'] = current['next']['next']
            return head
        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(c) for c 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 bucket in buckets:
        current = bucket
        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):
    """Итеративная вставка для избежания RecursionError"""
    new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}
    
    if root is None:
        return new_node
    
    current = root
    while True:
        if name < current['name']:
            if current['left'] is None:
                current['left'] = new_node
                break
            current = current['left']
        elif name > current['name']:
            if current['right'] is None:
                current['right'] = new_node
                break
            current = current['right']
        else:
            current['phone'] = phone
            break
    
    return root

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

def bst_find_min(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']
        
        min_node = bst_find_min(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):
    records = []
    stack = []
    current = root
    
    while stack or current:
        while current:
            stack.append(current)
            current = current['left']
        current = stack.pop()
        records.append((current['name'], current['phone']))
        current = current['right']
    
    return records

def copy_linked_list(head):
    if not head:
        return None
    new_head = {'name': head['name'], 'phone': head['phone'], 'next': None}
    current_new = new_head
    current_old = head['next']
    while current_old:
        current_new['next'] = {'name': current_old['name'], 'phone': current_old['phone'], 'next': None}
        current_new = current_new['next']
        current_old = current_old['next']
    return new_head

def copy_bst(node):
    if not node:
        return None
    return {
        'name': node['name'],
        'phone': node['phone'],
        'left': copy_bst(node['left']),
        'right': copy_bst(node['right'])
    }


def generate_test_data(N=10000):

    names = [f"User_{i:05d}" for i in range(N)]
    records = [(name, f"+7-999-{random.randint(1000000, 9999999)}") for name in names]
    
    records_shuffled = records.copy()
    random.shuffle(records_shuffled)
    
    records_sorted = sorted(records, key=lambda x: x[0])
    
    return records_shuffled, records_sorted

def get_test_queries(records, num_existing=100, num_nonexisting=10):
    existing_names = [name for name, _ in random.sample(records, min(num_existing, len(records)))]
    nonexisting_names = [f"None_{i:05d}" for i in range(num_nonexisting)]
    
    queries = existing_names + nonexisting_names
    random.shuffle(queries)
    
    return queries

def get_delete_names(records, num_to_delete=50):
    return [name for name, _ in random.sample(records, min(num_to_delete, len(records)))]


def measure_insertion(structure_type, records, repeats=3):
    times = []
    
    for _ in range(repeats):
        if structure_type == "LinkedList":
            structure = None
            insert_func = ll_insert
        elif structure_type == "HashTable":
            structure = ht_create(2000)
            insert_func = ht_insert
        elif structure_type == "BST":
            structure = None
            insert_func = bst_insert
        else:
            raise ValueError(f"Unknown structure: {structure_type}")
        
        start = time.perf_counter()
        
        for name, phone in records:
            if structure_type == "HashTable":
                insert_func(structure, name, phone)
            else:
                structure = insert_func(structure, name, phone)
        
        end = time.perf_counter()
        times.append(end - start)
    
    return times

def measure_search(structure_type, structure, queries, repeats=3):
    times = []
    
    for _ in range(repeats):
        start = time.perf_counter()
        
        for name in queries:
            if structure_type == "LinkedList":
                ll_find(structure, name)
            elif structure_type == "HashTable":
                ht_find(structure, name)
            elif structure_type == "BST":
                bst_find(structure, name)
        
        end = time.perf_counter()
        times.append(end - start)
    
    return times

def measure_deletion(structure_type, structure, names_to_delete, repeats=3):
    times = []
    
    for _ in range(repeats):
        if structure_type == "LinkedList":
            temp_structure = copy_linked_list(structure)
            delete_func = ll_delete
            
        elif structure_type == "HashTable":
            temp_structure = structure.copy()
            for i in range(len(temp_structure)):
                if temp_structure[i]:
                    temp_structure[i] = copy_linked_list(temp_structure[i])
            delete_func = ht_delete
            
        elif structure_type == "BST":
            temp_structure = copy_bst(structure)
            delete_func = bst_delete
        
        start = time.perf_counter()
        
        for name in names_to_delete:
            if structure_type == "HashTable":
                delete_func(temp_structure, name)
            else:
                temp_structure = delete_func(temp_structure, name)
        
        end = time.perf_counter()
        times.append(end - start)
    
    return times

def run_experiment(N=2000):
    
    print(f"Генерация тестовых данных (N={N})...")
    records_shuffled, records_sorted = generate_test_data(N)
    
    queries = get_test_queries(records_shuffled, num_existing=100, num_nonexisting=10)
    delete_names = get_delete_names(records_shuffled, num_to_delete=50)
    
    structures = ["LinkedList", "HashTable", "BST"]
    modes = ["случайный", "отсортированный"]
    
    results = []
    
    print("\nНачало экспериментов:")
    
    for structure in structures:
        print(f"\nТестирование {structure}...")
        
        for mode in modes:
            print(f"  Режим: {mode}")
            records = records_shuffled if mode == "случайный" else records_sorted
            
            print(f"    Измерение вставки...")
            try:
                insert_times = measure_insertion(structure, records, repeats=3)
                avg_insert = sum(insert_times) / len(insert_times)
            except RecursionError:
                print(f"    ОШИБКА: Превышена глубина рекурсии при вставке в {structure} для {mode} режима")
                continue
            
            print(f"    Создание финальной структуры...")
            if structure == "LinkedList":
                final_structure = None
                for name, phone in records:
                    final_structure = ll_insert(final_structure, name, phone)
            elif structure == "HashTable":
                final_structure = ht_create(2000)
                for name, phone in records:
                    ht_insert(final_structure, name, phone)
            elif structure == "BST":
                final_structure = None
                for name, phone in records:
                    final_structure = bst_insert(final_structure, name, phone)
            
            print(f"    Измерение поиска...")
            search_times = measure_search(structure, final_structure, queries, repeats=3)
            avg_search = sum(search_times) / len(search_times)
            
            print(f"    Измерение удаления...")
            deletion_times = measure_deletion(structure, final_structure, delete_names, repeats=3)
            avg_deletion = sum(deletion_times) / len(deletion_times)
            
            results.append({
                "Структура": structure,
                "Режим": mode,
                "Операция": "вставка",
                "Замеры": insert_times,
                "Среднее": avg_insert
            })
            results.append({
                "Структура": structure,
                "Режим": mode,
                "Операция": "поиск",
                "Замеры": search_times,
                "Среднее": avg_search
            })
            results.append({
                "Структура": structure,
                "Режим": mode,
                "Операция": "удаление",
                "Замеры": deletion_times,
                "Среднее": avg_deletion
            })
            
            print(f"      Вставка: {avg_insert:.6f} сек")
            print(f"      Поиск: {avg_search:.6f} сек")
            print(f"      Удаление: {avg_deletion:.6f} сек")
    
    return results


import os
import csv
from datetime import datetime

def save_to_csv(results, filename="results.csv"):
    save_dir = "/Users/mariiaos/2026-rff_mp/osipovamd/docs"
    filepath = os.path.join(save_dir, filename)
    
    with open(filepath, "w", newline="", encoding="utf-8") as f:
        writer = csv.writer(f)
        writer.writerow(["Структура", "Режим", "Операция", "Замер1", "Замер2", "Замер3", "Среднее"])
        
        for res in results:
            row = [
                res["Структура"],
                res["Режим"],
                res["Операция"],
                *[f"{t:.6f}" for t in res["Замеры"]],
                f"{res['Среднее']:.6f}"
            ]
            writer.writerow(row)
    
    print(f"\nРезультаты сохранены в: {filepath}")
    return filepath

def plot_results(results):
    
    if not results:
        print("Нет данных для построения графиков!")
        return
    
    plt.style.use('seaborn-v0_8-darkgrid')
    fig, axes = plt.subplots(1, 3, figsize=(15, 5))
    
    operations = ["вставка", "поиск", "удаление"]
    structures = ["LinkedList", "HashTable", "BST"]
    modes = ["случайный", "отсортированный"]
    
    colors = {'LinkedList': '#FF6B6B', 'HashTable': '#4ECDC4', 'BST': '#45B7D1'}
    
    for idx, operation in enumerate(operations):
        ax = axes[idx]
        
        x = np.arange(len(modes))
        width = 0.25
        multiplier = 0
        
        for structure in structures:
            values = []
            for mode in modes:
                found = False
                for res in results:
                    if (res["Структура"] == structure and 
                        res["Режим"] == mode and 
                        res["Операция"] == operation):
                        values.append(res["Среднее"])
                        found = True
                        break
                if not found:
                    values.append(0)
            
            if max(values) > 0:
                offset = width * multiplier
                bars = ax.bar(x + offset, values, width, label=structure, color=colors[structure])
                multiplier += 1
        
        ax.set_xlabel('Режим данных', fontsize=12)
        ax.set_ylabel('Время (секунды)', fontsize=12)
        ax.set_title(f'{operation.capitalize()}', fontsize=14, fontweight='bold')
        ax.set_xticks(x + width)
        ax.set_xticklabels(modes)
        ax.legend(loc='upper left')
        ax.grid(True, alpha=0.3)
    
    plt.suptitle('Сравнение производительности структур данных', 
                 fontsize=16, fontweight='bold')
    plt.tight_layout()
    plt.savefig('performance_comparison.png', dpi=300, bbox_inches='tight')
    plt.show()
    


if __name__ == "__main__":
    print("тестирование производительности структур данных")
    
    results = run_experiment(N=1000)  
    save_to_csv(results)
    
    if results:
        print("\nПостроение графиков...")
        plot_results(results)
        
        print("Сводная таблица результатов (среднее время в секундах)")
        print(f"{'Структура':<12} {'Режим':<12} {'Вставка':<10} {'Поиск':<10} {'Удаление':<10}")

        
        for structure in ["LinkedList", "HashTable", "BST"]:
            for mode in ["случайный", "отсортированный"]:
                insert_time = search_time = delete_time = 0
                for res in results:
                    if res["Структура"] == structure and res["Режим"] == mode:
                        if res["Операция"] == "вставка":
                            insert_time = res["Среднее"]
                        elif res["Операция"] == "поиск":
                            search_time = res["Среднее"]
                        elif res["Операция"] == "удаление":
                            delete_time = res["Среднее"]
                
                if insert_time > 0 or search_time > 0 or delete_time > 0:
                    print(f"{structure:<12} {mode:<12} {insert_time:<10.6f} {search_time:<10.6f} {delete_time:<10.6f}")
    else:
        print("\nЭксперимент не дал результатов из-за ошибок.")
    
    print("\nЭксперимент завершён!")