import random
import time
import csv
import sys
sys.setrecursionlimit(30000)


# =========================
# LINKED LIST
# =========================

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 head is None:
        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):
    result = []

    current = head

    while current:
        result.append((current["name"], current["phone"]))
        current = current["next"]

    return sorted(result)


# =========================
# HASH TABLE
# =========================

TABLE_SIZE = 1000


def hash_func(name):
    return sum(ord(c) for c in name) % TABLE_SIZE


def ht_insert(buckets, name, phone):
    index = hash_func(name)
    buckets[index] = ll_insert(buckets[index], name, phone)


def ht_find(buckets, name):
    index = hash_func(name)
    return ll_find(buckets[index], name)


def ht_delete(buckets, name):
    index = hash_func(name)
    buckets[index] = ll_delete(buckets[index], name)


def ht_list_all(buckets):
    result = []

    for bucket in buckets:
        result.extend(ll_list_all(bucket))

    return sorted(result)


# =========================
# BST
# =========================

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

        temp = bst_min(root["right"])

        root["name"] = temp["name"]
        root["phone"] = temp["phone"]

        root["right"] = bst_delete(root["right"], temp["name"])

    return root


def bst_list_all(root):
    if root is None:
        return []

    return (
        bst_list_all(root["left"])
        + [(root["name"], root["phone"])]
        + bst_list_all(root["right"])
    )


# =========================
# TEST DATA
# =========================

N = 10000

records = [
    (f"User_{i:05d}", str(random.randint(100000, 999999)))
    for i in range(N)
]

records_shuffled = records[:]
random.shuffle(records_shuffled)

records_sorted = sorted(records)


# =========================
# BENCHMARK
# =========================

results = [
    ["Structure", "Mode", "Operation", "Time"]
]


def benchmark_linked_list(records_input, mode):
    global results

    head = None

    start = time.perf_counter()

    for name, phone in records_input:
        head = ll_insert(head, name, phone)

    end = time.perf_counter()

    results.append(["LinkedList", mode, "insert", end - start])

    sample = random.sample(records_input, 100)

    start = time.perf_counter()

    for name, _ in sample:
        ll_find(head, name)

    end = time.perf_counter()

    results.append(["LinkedList", mode, "find", end - start])

    start = time.perf_counter()

    for name, _ in sample[:50]:
        head = ll_delete(head, name)

    end = time.perf_counter()

    results.append(["LinkedList", mode, "delete", end - start])


def benchmark_hash_table(records_input, mode):
    global results

    buckets = [None] * TABLE_SIZE

    start = time.perf_counter()

    for name, phone in records_input:
        ht_insert(buckets, name, phone)

    end = time.perf_counter()

    results.append(["HashTable", mode, "insert", end - start])

    sample = random.sample(records_input, 100)

    start = time.perf_counter()

    for name, _ in sample:
        ht_find(buckets, name)

    end = time.perf_counter()

    results.append(["HashTable", mode, "find", end - start])

    start = time.perf_counter()

    for name, _ in sample[:50]:
        ht_delete(buckets, name)

    end = time.perf_counter()

    results.append(["HashTable", mode, "delete", end - start])


def benchmark_bst(records_input, mode):
    global results

    root = None

    start = time.perf_counter()

    for name, phone in records_input:
        root = bst_insert(root, name, phone)

    end = time.perf_counter()

    results.append(["BST", mode, "insert", end - start])

    sample = random.sample(records_input, 100)

    start = time.perf_counter()

    for name, _ in sample:
        bst_find(root, name)

    end = time.perf_counter()

    results.append(["BST", mode, "find", end - start])

    start = time.perf_counter()

    for name, _ in sample[:50]:
        root = bst_delete(root, name)

    end = time.perf_counter()

    results.append(["BST", mode, "delete", end - start])


# =========================
# RUN TESTS
# =========================

benchmark_linked_list(records_shuffled, "random")
benchmark_linked_list(records_sorted, "sorted")

benchmark_hash_table(records_shuffled, "random")
benchmark_hash_table(records_sorted, "sorted")

benchmark_bst(records_shuffled, "random")
benchmark_bst(records_sorted, "sorted")


# =========================
# SAVE CSV
# =========================

with open("results.csv", "w", newline="") as f:
    writer = csv.writer(f)
    writer.writerows(results)

print("Done! Results saved to results.csv")