import tensorrt as trt
import torch
import mmap


def trt_dtype_to_torch(dtype):
    if dtype == trt.DataType.FLOAT:
        return torch.float32
    elif dtype == trt.DataType.INT32:
        return torch.int32
    elif dtype == trt.DataType.BOOL:
        return torch.bool
    else:
        raise RuntimeError(f"Unsupported dtype {dtype}")


class TRTOutputAllocator(trt.IOutputAllocator):
    def __init__(self, execution_context, dtype, device):
        trt.IOutputAllocator.__init__(self)
        self.execution_context = execution_context
        self.outputs = self.execution_context.outputs
        self.dtype = dtype
        self.device = device

    def reallocate_output(self, tensor_name, memory, size, alignment):
        if tensor_name in self.outputs:
            x = self.outputs[tensor_name]
            if x.numel() * x.element_size() == size:
                return x.data_ptr()
            del self.outputs[tensor_name]
        x = torch.zeros(size // 4, dtype=self.dtype, device=self.device)
        x.requires_grad = False
        self.outputs[tensor_name] = x
        return x.data_ptr()

    def notify_shape(self, tensor_name, shape):
        x = self.outputs[tensor_name]
        if x.shape != shape:
            x_ = x.view(list(shape))
            assert x_.data_ptr() == x.data_ptr()
            self.outputs[tensor_name] = x_


class TRTExecutionContext:
    def __init__(self, runtime, engine, optimization_profile_index, stream):
        self.runtime = runtime
        self.engine = engine
        self.stream = stream
        self.optimization_profile_index = None
        self.context = engine.create_execution_context()
        self.set_optimization_profile_index(optimization_profile_index)
        self.tensor_descs = []
        self.outputs = {}
        self.torch_device = torch.device("cuda", self.stream.device.index)
        for i in range(engine.num_io_tensors):
            name = engine.get_tensor_name(i)
            assert engine.get_tensor_location(name) == trt.TensorLocation.DEVICE
            dtype = engine.get_tensor_dtype(name)
            assert dtype in (
                trt.DataType.FLOAT,
                trt.DataType.INT32,
                trt.DataType.BOOL,
            )
            assert (
                engine.get_tensor_format(name, optimization_profile_index)
                == trt.TensorFormat.LINEAR
            )
            mode = engine.get_tensor_mode(name)
            self.tensor_descs.append(
                (
                    name,
                    mode,
                    engine.get_tensor_shape(name),
                    dtype,
                )
            )
            if mode == trt.TensorIOMode.OUTPUT:
                self.context.set_output_allocator(
                    name,
                    TRTOutputAllocator(
                        self, trt_dtype_to_torch(dtype), self.torch_device
                    ),
                )

    def set_optimization_profile_index(self, optimization_profile_index):
        if self.optimization_profile_index == optimization_profile_index:
            return
        self.stream.synchronize()
        self.optimization_profile_index = optimization_profile_index
        self.context.set_optimization_profile_async(
            optimization_profile_index, self.stream.cuda_stream
        )
        self.stream.synchronize()

    def eval_async(self, **kwargs):
        for i, (name, mode, shape, dtype) in enumerate(self.tensor_descs):
            if mode == trt.TensorIOMode.INPUT:
                if name not in kwargs:
                    raise RuntimeError(f"Missing input {name}")
                arg = kwargs[name]
                assert arg.device.type == "cuda"
                assert arg.device.index == self.stream.device.index
                assert arg.dtype == trt_dtype_to_torch(dtype)
                assert len(arg.shape) == len(shape)
                assert all(b == -1 or a == b for a, b in zip(arg.shape, shape))
                assert arg.is_contiguous()
                self.context.set_input_shape(name, arg.shape)
                self.context.set_tensor_address(name, arg.data_ptr())
        self.context.execute_async_v3(self.stream.cuda_stream)

    def eval_await(self):
        self.stream.synchronize()
        return self.outputs

    def eval(self, **kwargs):
        self.eval_async(**kwargs)
        return self.eval_await()


class TRTModel:
    def __init__(self, runtime, path):
        self.runtime = runtime
        with open(path, "rb") as f, mmap.mmap(
            f.fileno(), 0, access=mmap.ACCESS_READ
        ) as region:
            self.engine = runtime.deserialize_cuda_engine(region)
        assert self.engine

    def create_execution_context(self, optimization_profile_index=0, stream=None):
        if stream is None:
            stream = torch.cuda.Stream()
        return TRTExecutionContext(
            self.runtime, self.engine, optimization_profile_index, stream
        )

    def get_profile_index_for_dim_constraint(self, input_name, dim_index, max_size):
        candidates = []  # (profile_index, size)
        for i in range(self.engine.num_optimization_profiles):
            _, opt_shape, max_shape = self.engine.get_tensor_profile_shape(
                input_name, i
            )
            if max_shape[dim_index] >= max_size:
                candidates.append((i, opt_shape[dim_index]))
        if not candidates:
            raise RuntimeError(
                f"No profile found for {input_name} with dim {dim_index} at least {max_size}"
            )
        candidates.sort(key=lambda x: x[1])
        return candidates[0][0]

    def get_profile_max_size(self, profile_index, input_name, dim_index):
        _, _, max_shape = self.engine.get_tensor_profile_shape(
            input_name, profile_index
        )
        return max_shape[dim_index]
