import random
import time
import csv
import sys

sys.setrecursionlimit(10**8)#прикалюха 6 вылетаел

def mk(name, phone):
    return {"name": name, "phone": phone}

def ll_create_node(record):
    return [record, None]

def ll_insert(ll_head, record):
    new_node = ll_create_node(record)
    new_node[1] = ll_head[0]
    ll_head[0] = new_node

def ll_find(ll_head, name):
    cur = ll_head[0]
    while cur:
        if cur[0]["name"] == name:
            return cur[0]
        cur = cur[1]
    return None

def ll_delete(ll_head, name):
    cur = ll_head[0]
    prev = None
    while cur:
        if cur[0]["name"] == name:
            if prev:
                prev[1] = cur[1]
            else:
                ll_head[0] = cur[1]
            return True
        prev = cur
        cur = cur[1]
    return False

def ll_list_all(ll_head):
    res = []
    cur = ll_head[0]
    while cur:
        res.append(cur[0])
        cur = cur[1]
    return res

def ht_hash(name, size):
    return hash(name) % size

def ht_insert(table, record):
    idx = ht_hash(record["name"], len(table))
    new_node = ll_create_node(record)
    new_node[1] = table[idx]
    table[idx] = new_node

def ht_find(table, name):
    idx = ht_hash(name, len(table))
    cur = table[idx]
    while cur:
        if cur[0]["name"] == name:
            return cur[0]
        cur = cur[1]
    return None

def ht_delete(table, name):
    idx = ht_hash(name, len(table))
    cur = table[idx]
    prev = None
    while cur:
        if cur[0]["name"] == name:
            if prev:
                prev[1] = cur[1]
            else:
                table[idx] = cur[1]
            return True
        prev = cur
        cur = cur[1]
    return False

def ht_list_all(table):
    res = []
    for head in table:
        cur = head
        while cur:
            res.append(cur[0])
            cur = cur[1]
    return res

def bst_create_node(record):
    return [record, None, None]

def bst_insert(root, record):
    if root is None:
        return bst_create_node(record)
    if record["name"] < root[0]["name"]:
        root[1] = bst_insert(root[1], record)
    elif record["name"] > root[0]["name"]:
        root[2] = bst_insert(root[2], record)
    else:
        root[0] = record
    return root

def bst_find(root, name):
    if root is None:
        return None
    if name == root[0]["name"]:
        return root[0]
    elif name < root[0]["name"]:
        return bst_find(root[1], name)
    else:
        return bst_find(root[2], name)

def bst_find_min(node):
    while node[1] is not None:
        node = node[1]
    return node

def bst_delete(root, name):
    if root is None:
        return None
    if name < root[0]["name"]:
        root[1] = bst_delete(root[1], name)
    elif name > root[0]["name"]:
        root[2] = bst_delete(root[2], name)
    else:
        if root[1] is None:
            return root[2]
        elif root[2] is None:
            return root[1]
        else:
            succ = bst_find_min(root[2])
            root[0] = succ[0]
            root[2] = bst_delete(root[2], succ[0]["name"])
    return root

def bst_list_all(root):
    def inorder(node):
        if node is None:
            return []
        return inorder(node[1]) + [node[0]] + inorder(node[2])
    return inorder(root)

def tmr(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        return result, elapsed
    return wrapper

@tmr
def ins_ll(ll_head, records):
    for rec in records:
        ll_insert(ll_head, rec)

@tmr
def fnd_ll(ll_head, names):
    for name in names:
        ll_find(ll_head, name)

@tmr
def del_ll(ll_head, names):
    for name in names:
        ll_delete(ll_head, name)

@tmr
def ins_ht(table, records):
    for rec in records:
        ht_insert(table, rec)

@tmr
def fnd_ht(table, names):
    for name in names:
        ht_find(table, name)

@tmr
def del_ht(table, names):
    for name in names:
        ht_delete(table, name)

@tmr
def ins_bst(root, records):
    for rec in records:
        root = bst_insert(root, rec)
    return root

@tmr
def fnd_bst(root, names):
    for name in names:
        bst_find(root, name)

@tmr
def del_bst(root, names):
    for name in names:
        root = bst_delete(root, name)
    return root

def gen(n, seed=42):
    random.seed(seed)
    recs = []
    for i in range(n):
        name = f"user_{i:05d}"
        phone = random.randint(1000000, 9999999)
        recs.append(mk(name, phone))
    return recs

def prep(recs, ecnt=100, mcnt=10):
    alln = [r["name"] for r in recs]
    ex = random.sample(alln, ecnt)
    ms = [f"none_{i}" for i in range(mcnt)]
    sn = ex + ms
    dn = random.sample(alln, 50)
    return sn, dn

def prm(n):
    if n < 2: return False
    if n % 2 == 0: return n == 2
    d = 3
    while d * d <= n:
        if n % d == 0: return False
        d += 2
    return True

def nxtprm(n):
    while not prm(n):
        n += 1
    return n

def bench(n=10000, rpts=5):
    recs = gen(n)
    shuf = recs.copy()
    random.shuffle(shuf)
    srt = sorted(recs, key=lambda r: r["name"])

    snms, dnms = prep(recs)

    htsz = nxtprm(2 * n)

    exps = [
        {
            "name": "linkedlist",
            "init_empty": lambda: [None],
            "insert": ins_ll,
            "find": fnd_ll,
            "delete": del_ll,
        },
        {
            "name": "hashtable",
            "init_empty": lambda: [None] * htsz,
            "insert": ins_ht,
            "find": fnd_ht,
            "delete": del_ht,
        },
        {
            "name": "bst",
            "init_empty": lambda: None,
            "insert": ins_bst,
            "find": fnd_bst,
            "delete": del_bst,
        },
    ]

    res = []

    for e in exps:
        sn = e["name"]
        for mn, recs_set in [("shuffled", shuf), ("sorted", srt)]:
            for rp in range(1, rpts + 1):
                st = e["init_empty"]()

                if sn == "bst":
                    st, ti = e["insert"](st, recs_set)
                else:
                    _, ti = e["insert"](st, recs_set)

                _, tf = e["find"](st, snms)

                if sn == "bst":
                    st, td = e["delete"](st, dnms)
                else:
                    _, td = e["delete"](st, dnms)

                res.append([sn, mn, "insert", rp, ti])
                res.append([sn, mn, "find", rp, tf])
                res.append([sn, mn, "delete", rp, td])

    with open("results.csv", "w", newline="", encoding="utf-8") as f:
        w = csv.writer(f)
        w.writerow(["тип", "режим", "операция", "повтор", "время"])
        w.writerows(res)

    from collections import defaultdict
    agg = defaultdict(list)
    for row in res:
        k = (row[0], row[1], row[2])
        agg[k].append(row[4])
    print("\n5 повторов в ср:")
    print(f"{'тип':<15} {'режим':<10} {'операция':<10} {'срдений':<10}")
    for k, times in sorted(agg.items()):
        avg = sum(times) / len(times)
        print(f"{k[0]:<15} {k[1]:<10} {k[2]:<10} {avg:<10.6f}")

    return res, agg

def plot(agg):
    try:
        import matplotlib.pyplot as plt
    except ImportError:
        print("матплотлтб скачать")
        return

    sts = ["linkedlist", "hashtable", "bst"]
    mds = ["shuffled", "sorted"]
    ops = ["insert", "find", "delete"]

    fig, axes = plt.subplots(1, 3, figsize=(16, 5))
    fig.suptitle("сравенние", fontsize=14)

    for oi, op in enumerate(ops):
        ax = axes[oi]
        data = []
        for s in sts:
            for m in mds:
                k = (s, m, op)
                avg = sum(agg[k]) / len(agg[k]) if k in agg else 0
                data.append(avg)
        x = range(len(sts))
        width = 0.35
        svals = [data[i*2] for i in range(len(sts))]
        svals2 = [data[i*2+1] for i in range(len(sts))]

        ax.bar([i - width/2 for i in x], svals, width, label='shuffled')
        ax.bar([i + width/2 for i in x], svals2, width, label='sorted')
        ax.set_xticks(x)
        ax.set_xticklabels(sts)
        ax.set_title(op)
        ax.set_ylabel('время (sec)')
        ax.legend()

    plt.tight_layout()
    plt.savefig("kortinko.png")
    plt.show()
    print("zibka kortinko.png")

if __name__ == "__main__":
    res, agg = bench(n=10000, rpts=5)
    plot(agg)