import math
import ctypes
import dataclasses
import torch
from typing import NamedTuple

import comfy_aimdo.host_buffer
from .component_model.module_property import create_module_properties
from .quant_ops import QuantizedTensor


class TensorFileSlice(NamedTuple):
    file_ref: object
    lock: object
    offset: int
    size: int


def read_tensor_file_slice_into(tensor, destination, stream=None, destination2=None):
    if isinstance(tensor, QuantizedTensor):
        if not read_tensor_file_slice_into(tensor._qdata,
                                           destination._qdata if destination is not None else None, stream=stream,
                                           destination2=(destination2._qdata if destination2 is not None else None)):
            return False

        if destination is not None:
            dst_orig_dtype = destination._params.orig_dtype
            destination._params.copy_from(tensor._params, non_blocking=False)
            destination._params = dataclasses.replace(destination._params, orig_dtype=dst_orig_dtype)
        if destination2 is not None:
            dst_orig_dtype = destination2._params.orig_dtype
            destination2._params.copy_from(destination._params if destination is not None else tensor._params, non_blocking=True)
            destination2._params = dataclasses.replace(destination2._params, orig_dtype=dst_orig_dtype)
        return True

    info = getattr(tensor.untyped_storage(), "_comfy_tensor_file_slice", None)
    if info is None:
        return False

    if destination is not None and destination.device.type != "cpu" and destination2 is None:
        destination2 = destination
        destination = None

    file_obj = info.file_ref
    if (file_obj is None
            or (destination is None and destination2 is None)
            or (destination is not None and (destination.device.type != "cpu" or destination.numel() * destination.element_size() < info.size))
            or (destination2 is not None and (destination2.device.type == "cpu" or destination2.numel() * destination2.element_size() < info.size))
            or tensor.numel() * tensor.element_size() != info.size
            or tensor.storage_offset() != 0
            or not tensor.is_contiguous()):
        return False

    if info.size == 0:
        return True

    if destination is None:
        stream_ptr = getattr(stream, "cuda_stream", 0) if stream is not None else 0
        comfy_aimdo.host_buffer.read_file_to_device(file_obj, info.offset, info.size,
                                                    stream_ptr, destination2.data_ptr(),
                                                    destination2.device.index,
                                                    mark_cold=False)
        return True

    hostbuf = getattr(destination.untyped_storage(), "_comfy_hostbuf", None)
    if hostbuf is not None:
        stream_ptr = getattr(stream, "cuda_stream", 0) if stream is not None else 0
        device_ptr = destination2.data_ptr() if destination2 is not None else 0
        with info.lock:
            hostbuf.read_file_slice(file_obj, info.offset, info.size,
                                    offset=destination.data_ptr() - hostbuf.get_raw_address(),
                                    stream=stream_ptr,
                                    device_ptr=device_ptr,
                                    device=None if destination2 is None else destination2.device.index)
        return True

    if not hasattr(file_obj, "seek") or not hasattr(file_obj, "readinto"):
        return False

    buf_type = ctypes.c_ubyte * info.size
    view = memoryview(buf_type.from_address(destination.data_ptr()))

    try:
        with info.lock:
            file_obj.seek(info.offset)
            done = 0
            while done < info.size:
                try:
                    n = file_obj.readinto(view[done:])
                except OSError:
                    return False
                if n <= 0:
                    return False
                done += n
        return True
    finally:
        view.release()


class TensorGeometry(NamedTuple):
    shape: any
    dtype: torch.dtype

    def element_size(self):
        info = torch.finfo(self.dtype) if self.dtype.is_floating_point else torch.iinfo(self.dtype)
        return info.bits // 8

    def numel(self):
        return math.prod(self.shape)


def tensors_to_geometries(tensors, dtype=None):
    geometries = []
    for t in tensors:
        if t is None or isinstance(t, QuantizedTensor):
            geometries.append(t)
            continue
        tdtype = t.dtype
        if hasattr(t, "_model_dtype"):
            tdtype = t._model_dtype
        if dtype is not None:
            tdtype = dtype
        geometries.append(TensorGeometry(shape=t.shape, dtype=tdtype))
    return geometries


def vram_aligned_size(tensor):
    if isinstance(tensor, list):
        return sum([vram_aligned_size(t) for t in tensor])

    if isinstance(tensor, QuantizedTensor):
        inner_tensors, _ = tensor.__tensor_flatten__()
        return vram_aligned_size([getattr(tensor, attr) for attr in inner_tensors])

    if tensor is None:
        return 0

    size = tensor.numel() * tensor.element_size()
    aligment_req = 1024
    return (size + aligment_req - 1) // aligment_req * aligment_req


def interpret_gathered_like(tensors, gathered):
    offset = 0
    dest_views = []

    if gathered.dim() != 1 or gathered.element_size() != 1:
        raise ValueError(f"Buffer must be 1D and single-byte (got {gathered.dim()}D {gathered.dtype})")

    for tensor in tensors:

        if tensor is None:
            dest_views.append(None)
            continue

        if isinstance(tensor, QuantizedTensor):
            inner_tensors, qt_ctx = tensor.__tensor_flatten__()
            templates = {attr: getattr(tensor, attr) for attr in inner_tensors}
        else:
            templates = {"data": tensor}

        actuals = {}
        for attr, template in templates.items():
            size = template.numel() * template.element_size()
            if offset + size > gathered.numel():
                raise ValueError(f"Buffer too small: needs {offset + size} bytes, but only has {gathered.numel()}. ")
            actuals[attr] = gathered[offset:offset + size].view(dtype=template.dtype).view(template.shape)
            offset += vram_aligned_size(template)

        if isinstance(tensor, QuantizedTensor):
            dest_views.append(QuantizedTensor.__tensor_unflatten__(actuals, qt_ctx, 0, 0))
        else:
            dest_views.append(actuals["data"])

    return dest_views


aimdo_allocator = None

_module_properties = create_module_properties()


@_module_properties.getter
def _aimdo_enabled() -> bool:
    return aimdo_allocator is not None


def aimdo_enabled() -> bool:
    return _aimdo_enabled()


def dynamic_vram_available_memory(device: torch.device) -> int:
    """Memory DynamicVRAM can make available without inventing a second budget."""
    from . import model_management

    available = int(model_management.get_free_memory(device))
    if aimdo_enabled():
        import comfy_aimdo.model_vbar

        aimdo_device = device.index if getattr(device, "type", None) == "cuda" else None
        available += int(comfy_aimdo.model_vbar.vbars_analyze(aimdo_device))
    return available


def dynamic_vram_projected_available_memory(device: torch.device, keep_models=()) -> int:
    """Compatibility wrapper for model management's generalized load projection."""
    from . import model_management

    return model_management.projected_dynamic_vram_available_memory(
        (device,), keep_models=keep_models
    )[device]

extra_ram_release_callback = None
RAM_CACHE_HEADROOM = 0

def set_ram_cache_release_state(callback, headroom):
    global extra_ram_release_callback
    global RAM_CACHE_HEADROOM
    extra_ram_release_callback = callback
    RAM_CACHE_HEADROOM = max(0, int(headroom))

def extra_ram_release(target, free_active=False):
    if extra_ram_release_callback is None:
        return 0
    return extra_ram_release_callback(target, free_active=free_active)
