import time
import random
import csv
import os
import sys

import matplotlib.pyplot as plt
import numpy as np

sys.setrecursionlimit(20000)


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

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=2000):
    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):

    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"]

        if name < current["name"]:
            current = current["left"]

        else:
            current = current["right"]

    return None


def bst_find_min(node):
    current = node
    while 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"]
        if 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):

    records = []

    for i in range(N):
        name = f"User_{i:05d}"
        phone = f"+7-999-{random.randint(1000000, 9999999)}"
        records.append((name, phone))
    records_shuffled = records.copy()
    random.shuffle(records_shuffled)
    records_sorted = sorted(records)
    return records_shuffled, records_sorted


def get_test_queries(records):

    existing = random.sample(records, 100)
    existing_names = [name for name, _ in existing]
    missing_names = [
        f"None_{i:05d}"
        for i in range(10)
    ]
    queries = existing_names + missing_names
    random.shuffle(queries)
    return queries


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


def measure_insertion(structure_type, records, repeats=5):

    times = []
    for _ in range(repeats):
        if structure_type == "LinkedList":
            structure = None
        elif structure_type == "HashTable":
            structure = ht_create()

        else:
            structure = None
        start = time.perf_counter()
        for name, phone in records:
            if structure_type == "LinkedList":
                structure = ll_insert(
                    structure,
                    name,
                    phone
                )

            elif structure_type == "HashTable":
                ht_insert(
                    structure,
                    name,
                    phone
                )

            else:
                structure = bst_insert(
                    structure,
                    name,
                    phone
                )
        end = time.perf_counter()
        times.append(end - start)
    return times



def measure_search(
    structure_type,
    structure,
    queries,
    repeats=5
):

    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)

            else:

                bst_find(structure, name)
        end = time.perf_counter()
        times.append(end - start)
    return times


def measure_deletion(
    structure_type,
    structure,
    delete_names,
    repeats=5
):

    times = []

    for _ in range(repeats):
        if structure_type == "LinkedList":
            temp = copy_linked_list(structure)
        elif structure_type == "HashTable":
            temp = structure.copy()
            for i in range(len(temp)):
                if temp[i]:
                    temp[i] = copy_linked_list(temp[i])

        else:
            temp = copy_bst(structure)
        start = time.perf_counter()
        for name in delete_names:
            if structure_type == "LinkedList":
                temp = ll_delete(temp, name)
            elif structure_type == "HashTable":
                ht_delete(temp, name)
            else:
                temp = bst_delete(temp, name)
        end = time.perf_counter()
        times.append(end - start)
    return times

def build_structure(structure_type, records):
    if structure_type == "LinkedList":
        structure = None
        for name, phone in records:
            structure = ll_insert(
                structure,
                name,
                phone
            )

    elif structure_type == "HashTable":
        structure = ht_create()
        for name, phone in records:
            ht_insert(
                structure,
                name,
                phone
            )

    else:
        structure = None
        for name, phone in records:
            structure = bst_insert(
                structure,
                name,
                phone
            )
    return structure


def run_experiment(N=10000):
    records_shuffled, records_sorted = generate_test_data(N)
    queries = get_test_queries(records_shuffled)
    delete_names = get_delete_names(records_shuffled)
    structures = [
        "LinkedList",
        "HashTable",
        "BST"
    ]
    modes = [
        ("random", records_shuffled),
        ("sorted", records_sorted)
    ]
    results = []
    for structure in structures:
        for mode_name, records in modes:
            insert_times = measure_insertion(
                structure,
                records
            )
            final_structure = build_structure(
                structure,
                records
            )
            search_times = measure_search(
                structure,
                final_structure,
                queries
            )
            delete_times = measure_deletion(
                structure,
                final_structure,
                delete_names
            )
            results.append({
                "Structure": structure,
                "Mode": mode_name,
                "Insert": insert_times,
                "Search": search_times,
                "Delete": delete_times,
                "AvgInsert":
                    sum(insert_times) / len(insert_times),
                "AvgSearch":
                    sum(search_times) / len(search_times),
                "AvgDelete":
                    sum(delete_times) / len(delete_times)
            })

    return results

def save_to_csv(results):
    os.makedirs("docs", exist_ok=True)
    with open(
        "docs/results.csv",
        "w",
        newline="",
        encoding="utf-8"
    ) as file:
        writer = csv.writer(file)
        writer.writerow([
            "Structure",
            "Mode",
            "Operation",
            "Run1",
            "Run2",
            "Run3",
            "Run4",
            "Run5",
            "Average"
        ])

        for result in results:
            writer.writerow([
                result["Structure"],
                result["Mode"],
                "Insert",
                *[f"{x:.6f}" for x in result["Insert"]],
                f"{result['AvgInsert']:.6f}"
            ])
            writer.writerow([
                result["Structure"],
                result["Mode"],
                "Search",
                *[f"{x:.6f}" for x in result["Search"]],
                f"{result['AvgSearch']:.6f}"
            ])
            writer.writerow([
                result["Structure"],
                result["Mode"],
                "Delete",
                *[f"{x:.6f}" for x in result["Delete"]],
                f"{result['AvgDelete']:.6f}"
            ])


def plot_results(results):
    structures = [
        "LinkedList",
        "HashTable",
        "BST"
    ]
    operations = [
        "AvgInsert",
        "AvgSearch",
        "AvgDelete"
    ]
    titles = [
        "Insert",
        "Search",
        "Delete"
    ]
    fig, axes = plt.subplots(
        1,
        3,
        figsize=(18, 6)
    )
    for ax, operation, title in zip(
        axes,
        operations,
        titles
    ):
        x = np.arange(len(structures))
        width = 0.35
        random_vals = []
        sorted_vals = []
        for structure in structures:
            for result in results:
                if (
                    result["Structure"] == structure
                    and result["Mode"] == "random"
                ):
                    random_vals.append(
                        result[operation]
                    )
                if (
                    result["Structure"] == structure
                    and result["Mode"] == "sorted"
                ):
                    sorted_vals.append(
                        result[operation]
                    )

        ax.bar(
            x - width / 2,
            random_vals,
            width,
            label="Random"
        )
        ax.bar(
            x + width / 2,
            sorted_vals,
            width,
            label="Sorted"
        )
        ax.set_xticks(x)
        ax.set_xticklabels(structures)
        ax.set_ylabel("Time (sec)")
        ax.set_title(title)
        ax.legend()
        ax.grid(True)
    plt.tight_layout()
    plt.savefig(
        "docs/performance_comparison.png",
        dpi=300
    )
    plt.show()


if __name__ == "__main__":
    print("\nTesting data structures...\n")
    results = run_experiment(N=10000)
    save_to_csv(results)
    plot_results(results)
    print("\nResults saved:")
    print("docs/results.csv")
    print("docs/performance_comparison.png")
    print("\nConclusions:\n")
    print(
        "1. LinkedList is the slowest structure "
        "for searching."
    )

    print(
        "2. HashTable shows the best "
        "search performance."
    )

    print(
        "3. BST works well on random data."
    )

    print(
        "4. Sorted data causes BST degradation."
    )

    print(
        "5. HashTable is best for frequent search."
    )

    print(
        "6. BST is useful for ordered data."
    )