import bisect
import comfy_aimdo.host_buffer
import comfy_aimdo.torch
import torch

from . import memory_management
from . import model_management
from . import utils
from .cli_args import args

def _add_to_bucket(module, module_pin, buckets, size, priority):
    bucket = buckets.setdefault(size, [])
    entry = [-priority, 0, module]
    entry[1] = id(entry)
    bisect.insort(bucket, entry)
    module_pin["balancer_entry"] = entry

def _steal_pin(module, stack, buckets, size, priority, subset):
    bucket = buckets.get(size)
    if bucket is None:
        return False

    while bucket and bucket[-1][-1] is None:
        bucket.pop()
    if not bucket:
        del buckets[size]
        return False

    if priority <= -bucket[-1][0]:
        return False

    *_, victim = bucket.pop()
    module_pin = module._pins[subset]
    victim_pin = victim._pins[subset]
    module_pin["pin"] = victim_pin["pin"]
    module_pin["registered"] = victim_pin["registered"]
    module_pin["stack_index"] = victim_pin["stack_index"]
    stack_index = module_pin["stack_index"]
    stack[stack_index] = (module, stack[stack_index][1])

    victim_pin["registered"] = False
    del victim_pin["pin"]
    del victim_pin["stack_index"]
    del victim_pin["balancer_entry"]

    _add_to_bucket(module, module_pin, buckets, size, priority)
    return True

def get_pin(module, subset="weights"):
    pins = module.__dict__.get("_pins")
    module_pin = None if pins is None else pins.get(subset)
    pin = None if module_pin is None else module_pin.get("pin")
    if pin is None or module_pin["registered"] or args.disable_pinned_memory:
        return pin

    _, _, stack_split, pinned_size, *_ = module._pin_state[subset]
    size = pin.nbytes
    model_management.ensure_pin_registerable(size)

    if torch.cuda.cudart().cudaHostRegister(pin.data_ptr(), size, 1) != 0:
        model_management.discard_cuda_async_error()
        return pin

    module_pin["registered"] = True
    stack_split[0] = max(stack_split[0], module_pin["stack_index"])
    model_management.TOTAL_PINNED_MEMORY += size
    pinned_size[0] += size
    return pin

def pin_memory(module, subset="weights", size=None):
    pin_state = module._pin_state
    if args.disable_pinned_memory:
        return

    pin = get_pin(module, subset)
    if pin is not None:
        return

    pins = module.__dict__.setdefault("_pins", {})
    module_pin = pins.setdefault(subset, {})
    hostbuf, stack, stack_split, pinned_size, counter, buckets = pin_state[subset]
    if size is None:
        size = memory_management.vram_aligned_size([module.weight, module.bias])
    registerable_size = size
    loaded = subset.endswith("-loaded")
    priority = module_pin.get("balancer_priority")

    if priority is None:
        priority = utils.bit_reverse_range(counter[0], 16)
        counter[0] += 1
        module_pin["balancer_priority"] = priority

    memory_management.extra_ram_release(memory_management.RAM_CACHE_HEADROOM)
    if (not model_management.ensure_pin_budget(size, loaded=loaded) or
        not model_management.ensure_pin_registerable(registerable_size)):
        return _steal_pin(module, stack, buckets, size, priority, subset)

    offset = hostbuf.size
    extended = False
    try:
        hostbuf.extend(size=size, register=False)
        extended = True
        pin = comfy_aimdo.torch.hostbuf_to_tensor(hostbuf)[offset:offset + size]
        pin.untyped_storage()._comfy_hostbuf = hostbuf
        if torch.cuda.cudart().cudaHostRegister(pin.data_ptr(), size, 1) != 0:
            model_management.discard_cuda_async_error()
            model_management.free_registrations(size)
            if torch.cuda.cudart().cudaHostRegister(pin.data_ptr(), size, 1) != 0:
                model_management.discard_cuda_async_error()
                del pin
                hostbuf.truncate(offset, do_unregister=False)
                return _steal_pin(module, stack, buckets, size, priority, subset)
    except RuntimeError:
        if extended:
            hostbuf.truncate(offset, do_unregister=False)
        return _steal_pin(module, stack, buckets, size, priority, subset)

    module_pin["pin"] = pin
    stack.append((module, offset))
    module_pin["registered"] = True
    module_pin["stack_index"] = len(stack) - 1
    stack_split[0] = max(stack_split[0], module_pin["stack_index"])
    model_management.TOTAL_PINNED_MEMORY += size
    pinned_size[0] += size
    _add_to_bucket(module, module_pin, buckets, size, priority)
    return True
