import csv
import os
import numpy as np
from matplotlib import pyplot as plt

def ensure_directories():
    os.makedirs('docs/data', exist_ok=True)

def save_to_csv(results, filename="docs/data/results.csv"):
    ensure_directories()
    
    with open(filename, 'w', newline='', encoding='utf-8') as f:
        writer = csv.writer(f)
        writer.writerow(['Структура', 'Режим', 'Операция', 
                         'Повтор1', 'Повтор2', 'Повтор3', 'Повтор4', 'Повтор5',
                         'Среднее', 'Стд_откл'])
        
        for res in results:
            struct_name = res['structure']
            mode = res['mode']
            
            for op, times, mean, std in [
                ('вставка', res['insert_all'], res['insert_mean'], res['insert_std']),
                ('поиск', res['find_all'], res['find_mean'], res['find_std']),
                ('удаление', res['delete_all'], res['delete_mean'], res['delete_std'])
            ]:
                row = [struct_name, mode, op] + times + [mean, std]
                writer.writerow(row)

def plot_results(results, filename="docs/performance_chart.png"):
    ensure_directories()
    struct_names = {
        'linkedlist': 'LinkedList',
        'hashtable': 'HashTable',
        'bst': 'BST'
    }
    
    operations = ['insert', 'find', 'delete']
    op_names = {'insert': 'Вставка', 'find': 'Поиск', 'delete': 'Удаление'}
    random_data = {}
    sorted_data = {}
    
    for res in results:
        struct_name = struct_names.get(res['structure'], res['structure'])
        mode = res['mode']
        
        if mode == 'случайный':
            random_data[struct_name] = {
                'insert': res['insert_mean'],
                'find': res['find_mean'],
                'delete': res['delete_mean']
            }
        else: 
            sorted_data[struct_name] = {
                'insert': res['insert_mean'],
                'find': res['find_mean'],
                'delete': res['delete_mean']
            }
    
    structure_order = ['LinkedList', 'HashTable', 'BST']
    
    fig, axes = plt.subplots(1, 3, figsize=(15, 5))
    
    for idx, op in enumerate(operations):
        ax = axes[idx]
        
        x = np.arange(len(structure_order))
        width = 0.35
        
        random_means = []
        sorted_means = []
        
        for struct in structure_order:
            if struct in random_data:
                random_means.append(random_data[struct][op])
            else:
                random_means.append(0)
            
            if struct in sorted_data:
                sorted_means.append(sorted_data[struct][op])
            else:
                sorted_means.append(0)
        
        if not random_means and not sorted_means:
            print(f" Нет данных для операции {op}")
            continue
        
        bars1 = ax.bar(x - width/2, random_means, width, 
                       label='Случайный порядок', color='skyblue')
        bars2 = ax.bar(x + width/2, sorted_means, width, 
                       label='Отсортированный порядок', color='salmon')
        
        ax.set_xlabel('Структура данных')
        ax.set_ylabel('Время (секунды)')
        ax.set_title(f'{op_names.get(op, op)}')
        ax.set_xticks(x)
        ax.set_xticklabels(structure_order)
        ax.legend()
        
        for bar in bars1 + bars2:
            height = bar.get_height()
            if height > 0:
                ax.annotate(f'{height:.3f}',
                           xy=(bar.get_x() + bar.get_width() / 2, height),
                           xytext=(0, 3), textcoords="offset points",
                           ha='center', va='bottom', fontsize=8)
    
    plt.tight_layout()
    plt.savefig(filename, dpi=150)
    plt.show()
    
def save_report_md(results, filename="docs/report.md"):
    ensure_directories()

    results_dict = {}
    for res in results:
        key = (res['structure'], res['mode'])
        results_dict[key] = res

    def get_val(struct, mode, field):
        key = (struct, mode)
        if key in results_dict:return results_dict[key][field]
        return 0.0
    
    ll_random_insert = get_val('linkedlist', 'случайный', 'insert_mean')
    ll_random_find = get_val('linkedlist', 'случайный', 'find_mean')
    ll_random_delete = get_val('linkedlist', 'случайный', 'delete_mean')
    ll_sorted_insert = get_val('linkedlist', 'отсортированный', 'insert_mean')
    ll_sorted_find = get_val('linkedlist', 'отсортированный', 'find_mean')
    ll_sorted_delete = get_val('linkedlist', 'отсортированный', 'delete_mean')
    
    ht_random_insert = get_val('hashtable', 'случайный', 'insert_mean')
    ht_random_find = get_val('hashtable', 'случайный', 'find_mean')
    ht_random_delete = get_val('hashtable', 'случайный', 'delete_mean')
    ht_sorted_insert = get_val('hashtable', 'отсортированный', 'insert_mean')
    ht_sorted_find = get_val('hashtable', 'отсортированный', 'find_mean')
    ht_sorted_delete = get_val('hashtable', 'отсортированный', 'delete_mean')
    
    bst_random_insert = get_val('bst', 'случайный', 'insert_mean')
    bst_random_find = get_val('bst', 'случайный', 'find_mean')
    bst_random_delete = get_val('bst', 'случайный', 'delete_mean')
    bst_sorted_insert = get_val('bst', 'отсортированный', 'insert_mean')
    bst_sorted_find = get_val('bst', 'отсортированный', 'find_mean')
    bst_sorted_delete = get_val('bst', 'отсортированный', 'delete_mean')
    
    from datetime import datetime
    
    report_content = f"""# Отчёт по лабораторной работе

## Цель работы

Реализовать три структуры данных «с нуля» (связный список, хеш-таблица, двоичное дерево поиска), применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций.

## Параметры эксперимента

- Количество записей: 10000
- Количество повторов каждого теста: 5
- Размер хеш-таблицы: 1000 корзин

## Результаты экспериментов

### 1. Связный список

| Режим | Вставка (сек) | Поиск (сек) | Удаление (сек) |
|-------|---------------|-------------|----------------|
| Случайный | {ll_random_insert:.4f} | {ll_random_find:.4f} | {ll_random_delete:.4f} |
| Отсортированный | {ll_sorted_insert:.4f} | {ll_sorted_find:.4f} | {ll_sorted_delete:.4f} |

### 2. Хеш-таблица

| Режим | Вставка (сек) | Поиск (сек) | Удаление (сек) |
|-------|---------------|-------------|----------------|
| Случайный | {ht_random_insert:.4f} | {ht_random_find:.4f} | {ht_random_delete:.4f} |
| Отсортированный | {ht_sorted_insert:.4f} | {ht_sorted_find:.4f} | {ht_sorted_delete:.4f} |

### 3. Двоичное дерево поиска (BST)

| Режим | Вставка (сек) | Поиск (сек) | Удаление (сек) |
|-------|---------------|-------------|----------------|
| Случайный | {bst_random_insert:.4f} | {bst_random_find:.4f} | {bst_random_delete:.4f} |
| Отсортированный | {bst_sorted_insert:.4f} | {bst_sorted_find:.4f} | {bst_sorted_delete:.4f} |

## Анализ результатов

### 1. Влияние порядка данных на BST
При добавлении уже отсортированных элементов BST вырождается в линейную структуру — сложность падает с O(log n) до O(n).  
Время вставки выросло с {bst_random_insert:.4f} до {bst_sorted_insert:.4f} секунд — замедление в {bst_sorted_insert/bst_random_insert:.1f} раза.

### 2. Почему хеш-таблица не чувствительна к порядку

Хеш-функция равномерно распределяет ключи по корзинам независимо от их исходного порядка. Поэтому последовательность добавления практически не влияет на производительность.
Сравнение случайного и упорядоченного ввода:
- Случайный режим: {ht_random_insert:.4f} с
- Упорядоченный режим: {ht_sorted_insert:.4f} с
- Различие: {ht_sorted_insert/ht_random_insert:.2f}

### 3. Почему связный список медленный при поиске

Поиск в связном списке требует линейного обхода O(n) — структура не поддерживает произвольный доступ. Это делает его непригодным для крупных справочников, где нужен быстрый поиск по ключу.

Сравнение скорости поиска на случайных данных:
- LinkedList: {ll_random_find:.4f} сек
- HashTable: {ht_random_find:.4f} сек (преимущество в {ll_random_find/ht_random_find:.1f})
- BST: {bst_random_find:.4f} сек

### 4. Сравнение удаления

| Структура | Сложность | Время на 50 удалений (случайные данные) |
|-----------|-----------|------------------------------------------|
| Связный список | O(n) | {ll_random_delete:.4f} сек|
| Хеш-таблица | O(1) в среднем | {ht_random_delete:.4f} сек |
| BST | O(log n) в среднем | {bst_random_delete:.4f} сек |

## Вывод:

| Задача | Рекомендация | Почему |
|--------|-------------|--------|
| Частый поиск | Хеш-таблица | O(1) в среднем, не зависит от порядка |
| Частые вставки/удаления | Хеш-таблица | Амортизированное O(1) |
| Нужен отсортированный вывод | Сбалансированное дерево (AVL/Red-Black) | In-order обход даёт сортировку |
| Мало данных (<100 элементов) | Связный список или массив | Простота, накладные расходы не оправданы |
| Последовательный доступ (очередь/стек) | Связный список | Вставка/удаление в начало/конец за O(1) |

## Заключение
Проведённый эксперимент подтверждает теоретические оценки сложности:

1. **Небалансированное BST это плохой выбор** при работе с реальными данными, которые могут оказаться упорядоченными. Деградация до O(n) делает его непригодным для надёжных систем.

2. **Хеш-таблица показывает стабильные результаты** вне зависимости от порядка входных данных — ключевое преимущество для телефонного справочника с произвольными именами абонентов.

3. **Связный список — нишевый инструмент**, эффективный только при работе с малыми объёмами данных.

"""
    
    with open(filename, 'w', encoding='utf-8') as f:
        f.write(report_content)

def print_analysis(results):
    print("\n" + "="*60)
    print("Анализ результатов")
    print("="*60)
    
    best_insert = min(results, key=lambda x: x['insert_mean'])
    best_find = min(results, key=lambda x: x['find_mean'])
    best_delete = min(results, key=lambda x: x['delete_mean'])
    
    print(f"\n Лучшая для вставки: {best_insert['structure']} ({best_insert['mode']}) - {best_insert['insert_mean']:.4f} сек")
    print(f" Лучшая для поиска: {best_find['structure']} ({best_find['mode']}) - {best_find['find_mean']:.4f} сек")
    print(f" Лучшая для удаления: {best_delete['structure']} ({best_delete['mode']}) - {best_delete['delete_mean']:.4f} сек")
    
    bst_random = None
    bst_sorted = None
    for res in results:
        if res['structure'] == 'bst' and res['mode'] == 'случайный':
            bst_random = res
        elif res['structure'] == 'bst' and res['mode'] == 'отсортированный':
            bst_sorted = res
    
    if bst_random and bst_sorted:
        print("\n Влияние порядка данных на BST:")
        print(f"  Вставка: случайный {bst_random['insert_mean']:.4f} сек vs отсортированный {bst_sorted['insert_mean']:.4f} сек")
        print(f"  Деградация в {bst_sorted['insert_mean']/bst_random['insert_mean']:.1f}x")
    
    ht_random = None
    ht_sorted = None
    for res in results:
        if res['structure'] == 'hashtable' and res['mode'] == 'случайный':
            ht_random = res
        elif res['structure'] == 'hashtable' and res['mode'] == 'отсортированный':
            ht_sorted = res
    
    if ht_random and ht_sorted:
        print("\n Чувствительность хеш-таблицы к порядку:")
        print(f"  Вставка: случайный {ht_random['insert_mean']:.4f} сек vs отсортированный {ht_sorted['insert_mean']:.4f} сек")
        print(f"  Отношение: {ht_sorted['insert_mean']/ht_random['insert_mean']:.2f}x (почти не чувствительна)")
    
    ll_random = None
    for res in results:
        if res['structure'] == 'linkedlist' and res['mode'] == 'случайный':
            ll_random = res
        elif res['structure'] == 'hashtable' and res['mode'] == 'случайный':
            ht_random = res
    
    if ll_random and ht_random:
        print("\n Сравнение скорости поиска:")
        print(f"  LinkedList: {ll_random['find_mean']:.4f} сек")
        print(f"  HashTable:  {ht_random['find_mean']:.4f} сек")
        print(f"  HashTable быстрее в {ll_random['find_mean']/ht_random['find_mean']:.1f} раз")