import time
import random
import csv
import os
import sys

sys.setrecursionlimit(20000)

BASE = os.path.dirname(os.path.abspath(__file__))
DATA_PATH = BASE

N = 10000
REPEAT = 5


def ll_insert(head, name, phone):
    new_node = {"name": name, "phone": phone, "next": head}
    return new_node


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()
    return result


BUCKET_SIZE = 1000


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):
    result = []
    for bucket in buckets:
        curr = bucket
        while curr:
            result.append((curr["name"], curr["phone"]))
            curr = curr["next"]
    result.sort()
    return result


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"]
    if name < root["name"]:
        return bst_find(root["left"], name)
    return bst_find(root["right"], name)


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"]
        temp = root["right"]
        while temp["left"]:
            temp = temp["left"]
        root["name"] = temp["name"]
        root["phone"] = temp["phone"]
        root["right"] = bst_delete(root["right"], temp["name"])
    return root


def bst_list_all(root):
    result = []

    def walk(node):
        if node is None:
            return
        walk(node["left"])
        result.append((node["name"], node["phone"]))
        walk(node["right"])

    walk(root)
    return result


def make_records(n):
    records = []
    for i in range(n):
        records.append((f"User_{i:05d}", f"8-900-{i % 10000:04d}"))
    return records


records_all = make_records(N)
records_shuffled = records_all[:]
random.shuffle(records_shuffled)
records_sorted = sorted(records_all)

all_names = [name for name, phone in records_all]
find_existing = random.sample(all_names, 100)
find_missing = [f"None_{i}" for i in range(10)]
find_names = find_existing + find_missing
random.shuffle(find_names)
delete_names = random.sample(all_names, 50)

all_results = []
summary = []


def build_structure(struct_type, records):
    if struct_type == "LinkedList":
        head = None
        for name, phone in records:
            head = ll_insert(head, name, phone)
        return head
    if struct_type == "HashTable":
        buckets = [None] * BUCKET_SIZE
        for name, phone in records:
            ht_insert(buckets, name, phone)
        return buckets
    root = None
    for name, phone in records:
        root = bst_insert(root, name, phone)
    return root


def do_find(struct_type, container, names):
    for name in names:
        if struct_type == "LinkedList":
            ll_find(container, name)
        elif struct_type == "HashTable":
            ht_find(container, name)
        else:
            bst_find(container, name)


def do_delete(struct_type, container, names):
    if struct_type == "LinkedList":
        for name in names:
            container = ll_delete(container, name)
        return container
    if struct_type == "HashTable":
        for name in names:
            ht_delete(container, name)
        return container
    for name in names:
        container = bst_delete(container, name)
    return container


def run_one_test(struct_type, mode_name, records):
    ins_times = []
    find_times = []
    del_times = []

    for run in range(REPEAT):
        start = time.perf_counter()
        container = build_structure(struct_type, records)
        ins_times.append(time.perf_counter() - start)

        start = time.perf_counter()
        do_find(struct_type, container, find_names)
        find_times.append(time.perf_counter() - start)

        start = time.perf_counter()
        do_delete(struct_type, container, delete_names)
        del_times.append(time.perf_counter() - start)

        all_results.append([
            struct_type, mode_name, f"Run {run + 1}",
            ins_times[-1], find_times[-1], del_times[-1],
        ])

    avg_ins = sum(ins_times) / REPEAT
    avg_find = sum(find_times) / REPEAT
    avg_del = sum(del_times) / REPEAT

    all_results.append([
        struct_type, mode_name, "AVERAGE", avg_ins, avg_find, avg_del,
    ])
    summary.append({
        "name": struct_type,
        "mode": mode_name,
        "ins": avg_ins,
        "find": avg_find,
        "del": avg_del,
    })


print("Запуск экспериментов...")
for mode_name, data in [("случайный", records_shuffled), ("сортированный", records_sorted)]:
    for struct_type in ["LinkedList", "HashTable", "BST"]:
        print(f"  {struct_type} ({mode_name})")
        run_one_test(struct_type, mode_name, data)

csv_path = os.path.join(DATA_PATH, "results.csv")
with open(csv_path, "w", newline="", encoding="utf-8-sig") as f:
    writer = csv.writer(f, delimiter=";")
    writer.writerow(["Структура", "Режим", "Итерация", "Вставка", "Поиск", "Удаление"])
    writer.writerows(all_results)

print("CSV сохранён:", csv_path)

try:
    import matplotlib.pyplot as plt

    plt.rcParams["font.sans-serif"] = ["Segoe UI", "Arial", "Tahoma", "DejaVu Sans"]
    plt.rcParams["axes.unicode_minus"] = False

    labels = ["insert", "find", "delete"]
    structs = ["LinkedList", "HashTable", "BST"]
    colors = ["#5dade2", "#e67e22", "#58d68d"]

    fig1, axs = plt.subplots(1, 3, figsize=(15, 5))
    fig1.suptitle("Влияние порядка данных")

    for i, s_name in enumerate(structs):
        rand_d = next(r for r in summary if r["name"] == s_name and r["mode"] == "случайный")
        sort_d = next(r for r in summary if r["name"] == s_name and r["mode"] == "сортированный")
        x = [0, 1, 2]
        w = 0.35
        axs[i].bar([p - w / 2 for p in x], [rand_d["ins"], rand_d["find"], rand_d["del"]], w, label="случайный")
        axs[i].bar([p + w / 2 for p in x], [sort_d["ins"], sort_d["find"], sort_d["del"]], w, label="сортированный")
        axs[i].set_title(s_name)
        axs[i].set_xticks(x)
        axs[i].set_xticklabels(labels)
        axs[i].legend()
        axs[i].grid(axis="y", alpha=0.3)

    plt.tight_layout()
    plt.savefig(os.path.join(DATA_PATH, "order_impact.png"))
    plt.close()

    fig2, axs2 = plt.subplots(1, 3, figsize=(15, 5))
    fig2.suptitle(f"Сравнение структур (N={N})")

    for i, key in enumerate(["ins", "find", "del"]):
        vals = []
        names = []
        for r in summary:
            names.append(f"{r['name']}\n({r['mode'][:4]})")
            vals.append(r[key])
        axs2[i].bar(names, vals, color=colors * 2)
        axs2[i].set_title(labels[i])
        axs2[i].tick_params(axis="x", rotation=20)

    plt.tight_layout()
    plt.savefig(os.path.join(DATA_PATH, "struct_comparison.png"))
    plt.close()
    print("Графики сохранены")
except ImportError:
    print("matplotlib не установлен")

report_path = os.path.join(os.path.dirname(BASE), "report.md")
with open(report_path, "w", encoding="utf-8-sig") as f:
    f.write("# Отчёт: сравнение структур данных\n\n")
    f.write(f"N = {N}, повторов = {REPEAT}\n\n")
    f.write("| Структура | Режим | Вставка (с) | Поиск (с) | Удаление (с) |\n")
    f.write("| --- | --- | --- | --- | --- |\n")
    for r in summary:
        f.write(
            f"| {r['name']} | {r['mode']} | {r['ins']:.6f} | {r['find']:.6f} | {r['del']:.6f} |\n"
        )
    f.write("\n## Графики\n\n")
    f.write("![Сравнение](data/struct_comparison.png)\n\n")
    f.write("![Порядок данных](data/order_impact.png)\n\n")
    f.write("## Выводы\n\n")
    f.write("- BST на отсортированных данных сильно тормозит (вырождение дерева).\n")
    f.write("- Хеш-таблица быстра на поиске и слабо зависит от порядка вставки.\n")
    f.write("- Связный список медленный при поиске.\n")
    f.write("- Для частого поиска предпочтительна хеш-таблица.\n")

print("Отчёт:", report_path)
print("Готово.")
