import sys

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

# Связный список
def ll_insert(head, name, phone):
    new_node = {'name': name, 'phone': phone, 'next': None}  # Создаю новый узел
    if head is None:  # Если список пуст
        return new_node  # Возвращаю узел как голову

    curr = head  # Указатель для обхода
    prev = None  # Храню предыдущий узел
    while curr is not None:  # Иду по списку
        if curr['name'] == name:  # Если нашел такое же имя
            curr['phone'] = phone  # Обновляю телефон
            return head
        prev = curr
        curr = curr['next']
    prev['next'] = new_node  # Добавляю в конец
    return head


def ll_find(head, name):
    curr = head  # Начинаю с головы
    while curr:  # Иду по всему списку
        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']  # Возвращаю второй элемент
    curr = head
    while curr['next']:  # Иду пока есть следующий
        if curr['next']['name'] == name:  # Нашел элемент для удаления
            curr['next'] = curr['next']['next']  # Перепрыгиваю через него
            return head
        curr = curr['next']
    return head


def ll_list_all(head):
    result = []
    curr = head
    while curr:  # Собираю все элементы
        result.append((curr['name'], curr['phone']))
        curr = curr['next']
    result.sort(key=lambda x: x[0])  # Сортирую по имени
    return result


# Хэш-таблица
HASH_SIZE = 1009  # Размер таблицы - простое число


def _hash_name(name):
    return hash(name) % HASH_SIZE  # Беру остаток от деления - это индекс корзины


def ht_insert(buckets, name, phone):
    idx = _hash_name(name)  # Вычисляю индекс корзины
    buckets[idx] = ll_insert(buckets[idx], name, phone)  # Метод цепочек - вставляю в список


def ht_find(buckets, name):
    idx = _hash_name(name)  # Нахожу корзину
    return ll_find(buckets[idx], name)  # Ищу в цепочке


def ht_delete(buckets, name):
    idx = _hash_name(name)  # Нахожу корзину
    buckets[idx] = ll_delete(buckets[idx], name)  # Удаляю из цепочки


def ht_list_all(buckets):
    all_entries = []
    for bucket in buckets:  # Прохожу по всем корзинам
        if bucket is not None:
            curr = bucket
            while curr:  # Собираю всю цепочку
                all_entries.append((curr['name'], curr['phone']))
                curr = curr['next']
    all_entries.sort(key=lambda x: x[0])
    return all_entries


# Двоичное дерево поиска
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):
    curr = root
    while curr:  # Итеративный спуск по дереву
        if name == curr['name']:  # Нашел
            return curr['phone']
        elif name < curr['name']:  # Искомое меньше - налево
            curr = curr['left']
        else:  # Искомое больше - направо
            curr = curr['right']
    return None


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 = root['right']
        while min_node['left']:  # Иду до самого левого
            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 bst_list_all(root):
    result = []

    def inorder(node):  # Симметричный обход
        if node:
            inorder(node['left'])  # Сначала левое
            result.append((node['name'], node['phone']))  # Потом корень
            inorder(node['right'])  # Потом правое

    inorder(root)
    return result


# ============================================================
# TECT
# ============================================================

import os
import random
import time
import csv
import pandas as pd
import matplotlib.pyplot as plt

# ============================================================
# ПОДГОТОВКА ПАПОК
# ============================================================

DATA_DIR = os.path.join("docs", "data")
os.makedirs(DATA_DIR, exist_ok=True)

csv_path = os.path.join(DATA_DIR, "lab1_results.csv")
graph_path = os.path.join(DATA_DIR, "lab1_graph.png")

# ============================================================
# ТЕСТОВЫЕ ДАННЫЕ
# ============================================================

random.seed(42)  # Фиксирую seed для повторяемости

N = 3000  # 3000 записей

base_records = [
    (f"User_{i:05d}", f"123-{i:05d}")
    for i in range(N)
]

records_shuffled = base_records.copy()
random.shuffle(records_shuffled)  # Перемешанный порядок

records_sorted = sorted(base_records, key=lambda x: x[0])  # Отсортированный порядок

# Данные для поиска
search_existing = [
    name for name, _ in random.sample(base_records, 100)  # 100 существующих имен
]

search_nonexist = [
    f"None_{i}"
    for i in range(10)  # 10 несуществующих имен
]

# Данные для удаления
delete_names = [
    name for name, _ in random.sample(base_records, 50)  # 50 имен для удаления
]


# ============================================================
# СОЗДАНИЕ СТРУКТУР
# ============================================================

def build_structure(records, struct_type):
    if struct_type == "ll":
        structure = None
        for name, phone in records:
            structure = ll_insert(structure, name, phone)  # Последовательная вставка
        return structure

    elif struct_type == "ht":
        structure = [None] * HASH_SIZE
        for name, phone in records:
            ht_insert(structure, name, phone)  # Вставка с хэшированием
        return structure

    elif struct_type == "bst":
        structure = None
        for name, phone in records:
            structure = bst_insert(structure, name, phone)  # Вставка с ветвлением
        return structure


# ============================================================
# INSERT
# ============================================================

def measure_insert(records, struct_type):
    start = time.perf_counter()
    build_structure(records, struct_type)  # Замеряю время построения структуры
    end = time.perf_counter()
    return end - start


# ============================================================
# SEARCH
# ============================================================

def measure_search(records, struct_type):
    structure = build_structure(records, struct_type)  # Строю структуру
    start = time.perf_counter()

    if struct_type == "ll":
        for name in search_existing + search_nonexist:
            ll_find(structure, name)  # Поиск перебором
    elif struct_type == "ht":
        for name in search_existing + search_nonexist:
            ht_find(structure, name)  # Поиск через хэш
    elif struct_type == "bst":
        for name in search_existing + search_nonexist:
            bst_find(structure, name)  # Поиск спуском по дереву

    end = time.perf_counter()
    return end - start


# ============================================================
# DELETE
# ============================================================

def measure_delete(records, struct_type):
    structure = build_structure(records, struct_type)  # Строю структуру
    start = time.perf_counter()

    if struct_type == "ll":
        for name in delete_names:
            structure = ll_delete(structure, name)  # Удаление со сдвигом
    elif struct_type == "ht":
        for name in delete_names:
            ht_delete(structure, name)  # Удаление из цепочки
    elif struct_type == "bst":
        for name in delete_names:
            structure = bst_delete(structure, name)  # Удаление с ребалансировкой

    end = time.perf_counter()
    return end - start


# ============================================================
# ЗАМЕРЫ
# ============================================================

all_data = []

experiments = [
    ("LinkedList", "ll"),
    ("HashTable", "ht"),
    ("BST", "bst")
]

modes = [
    ("shuffled", records_shuffled),  # Тест на случайных данных
    ("sorted", records_sorted)  # Тест на отсортированных данных
]

for struct_name, struct_type in experiments:
    for mode_name, records in modes:
        for rep in range(1, 4):  # 3 повтора для усреднения
            insert_time = measure_insert(records, struct_type)
            search_time = measure_search(records, struct_type)
            delete_time = measure_delete(records, struct_type)

            all_data.append([struct_name, mode_name, rep, "insert", insert_time])
            all_data.append([struct_name, mode_name, rep, "search", search_time])
            all_data.append([struct_name, mode_name, rep, "delete", delete_time])

# ============================================================
# CSV
# ============================================================

with open(csv_path, "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f)
    writer.writerow(["Структура", "Режим", "Повтор", "Операция", "Время (сек)"])
    writer.writerows(all_data)

print(f"CSV сохранён: {csv_path}")

# ============================================================
# ГРАФИК
# ============================================================

df = pd.read_csv(csv_path)

df_avg = (
    df.groupby(["Структура", "Режим", "Операция"])["Время (сек)"]
    .mean()
    .reset_index()
)

fig, ax = plt.subplots(figsize=(12, 6))

ops = ["insert", "search", "delete"]
x = range(len(ops))
width = 0.12

configs = [
    ("LinkedList", "shuffled"),
    ("LinkedList", "sorted"),
    ("HashTable", "shuffled"),
    ("HashTable", "sorted"),
    ("BST", "shuffled"),
    ("BST", "sorted")
]

for i, (struct, mode) in enumerate(configs):
    subset = df_avg[
        (df_avg["Структура"] == struct) &
        (df_avg["Режим"] == mode)
        ]
    times = [
        subset[subset["Операция"] == op]["Время (сек)"].values[0]
        for op in ops
    ]
    ax.bar(
        [p + i * width for p in x],
        times,
        width,
        label=f"{struct} ({mode})"
    )

ax.set_xticks([p + 2.5 * width for p in x])
ax.set_xticklabels(ops)
ax.set_ylabel("Среднее время (сек)")
ax.set_title("Сравнение структур данных")
ax.legend(bbox_to_anchor=(1.05, 1), loc="upper left")

plt.tight_layout()
plt.savefig(graph_path)
print(f"График сохранён: {graph_path}")
plt.show()