"""plugins.py

I referenced the code from https://github.com/dongfangduoshou123/YoloV3-TensorRT/blob/master/seralizeEngineFromPythonAPI.py
"""


import ctypes

import numpy as np
import tensorrt as trt
import os

from yolo_to_onnx import (is_pan_arch, DarkNetParser, get_category_num,
                          get_h_and_w, get_output_convs, get_anchors)

lib_path = os.path.abspath(__file__ + "/../../models/yolov4-tiny/libyolo_layer.so")

try:
    ctypes.cdll.LoadLibrary(lib_path)
except OSError as e:
    raise SystemExit(f'ERROR: failed to load {lib_path}.  '
                     'Did you forget to do a "make" in the "../plugins/" '
                     'subdirectory?') from e


def get_scales(cfg_file_path):
    """Get scale_x_y's of all yolo layers from the cfg file."""
    with open(cfg_file_path, 'r') as f:
        cfg_lines = f.readlines()
    yolo_lines = [l.strip() for l in cfg_lines if l.startswith('[yolo]')]
    scale_lines = [l.strip() for l in cfg_lines if l.startswith('scale_x_y')]
    if len(scale_lines) == 0:
        return [1.0] * len(yolo_lines)
    else:
        assert len(scale_lines) == len(yolo_lines)
        return [float(l.split('=')[-1]) for l in scale_lines]


def get_new_coords(cfg_file_path):
    """Get new_coords flag of yolo layers from the cfg file."""
    with open(cfg_file_path, 'r') as f:
        cfg_lines = f.readlines()
    yolo_lines = [l.strip() for l in cfg_lines if l.startswith('[yolo]')]
    newc_lines = [l.strip() for l in cfg_lines if l.startswith('new_coords')]
    if len(newc_lines) == 0:
        return 0
    else:
        assert len(newc_lines) == len(yolo_lines)
        return int(newc_lines[-1].split('=')[-1])


def get_plugin_creator(plugin_name, logger):
    """Get the TensorRT plugin creator."""
    trt.init_libnvinfer_plugins(logger, '')
    plugin_creator_list = trt.get_plugin_registry().plugin_creator_list
    for c in plugin_creator_list:
        if c.name == plugin_name:
            return c
    return None


def add_yolo_plugins(network, model_name, logger):
    """Add yolo plugins into a TensorRT network."""
    cfg_file_path = model_name + '.cfg'
    parser = DarkNetParser()
    layer_configs = parser.parse_cfg_file(cfg_file_path)
    num_classes = get_category_num(cfg_file_path)
    output_tensor_names = get_output_convs(layer_configs)
    h, w = get_h_and_w(layer_configs)
    if len(output_tensor_names) == 2:
        yolo_whs = [
            [w // 32, h // 32], [w // 16, h // 16]]
    elif len(output_tensor_names) == 3:
        yolo_whs = [
            [w // 32, h // 32], [w // 16, h // 16],
            [w //  8, h //  8]]
    elif len(output_tensor_names) == 4:
        yolo_whs = [
            [w // 64, h // 64], [w // 32, h // 32],
            [w // 16, h // 16], [w //  8, h //  8]]
    else:
        raise TypeError('bad number of outputs: %d' % len(output_tensor_names))
    if is_pan_arch(cfg_file_path):
        yolo_whs.reverse()
    anchors = get_anchors(cfg_file_path)
    if len(anchors) != len(yolo_whs):
        raise ValueError('bad number of yolo layers: %d vs. %d' %
                         (len(anchors), len(yolo_whs)))
    if network.num_outputs != len(anchors):
        raise ValueError('bad number of network outputs: %d vs. %d' %
                         (network.num_outputs, len(anchors)))
    scales = get_scales(cfg_file_path)
    if any([s < 1.0 for s in scales]):
        raise ValueError('bad scale_x_y: %s' % str(scales))
    if len(scales) != len(anchors):
        raise ValueError('bad number of scales: %d vs. %d' %
                         (len(scales), len(anchors)))
    new_coords = get_new_coords(cfg_file_path)

    plugin_creator = get_plugin_creator('YoloLayer_TRT', logger)
    if not plugin_creator:
        raise RuntimeError('cannot get YoloLayer_TRT plugin creator')
    old_tensors = [network.get_output(i) for i in range(network.num_outputs)]
    new_tensors = [None] * network.num_outputs
    for i, old_tensor in enumerate(old_tensors):
        input_multiplier = w // yolo_whs[i][0]
        new_tensors[i] = network.add_plugin_v2(
            [old_tensor],
            plugin_creator.create_plugin('YoloLayer_TRT', trt.PluginFieldCollection([
                trt.PluginField("yoloWidth", np.array(yolo_whs[i][0], dtype=np.int32), trt.PluginFieldType.INT32),
                trt.PluginField("yoloHeight", np.array(yolo_whs[i][1], dtype=np.int32), trt.PluginFieldType.INT32),
                trt.PluginField("inputMultiplier", np.array(input_multiplier, dtype=np.int32), trt.PluginFieldType.INT32),
                trt.PluginField("newCoords", np.array(new_coords, dtype=np.int32), trt.PluginFieldType.INT32),
                trt.PluginField("numClasses", np.array(num_classes, dtype=np.int32), trt.PluginFieldType.INT32),
                trt.PluginField("numAnchors", np.array(len(anchors[i]) // 2, dtype=np.int32), trt.PluginFieldType.INT32),
                trt.PluginField("anchors", np.ascontiguousarray(anchors[i], dtype=np.float32), trt.PluginFieldType.FLOAT32),
                trt.PluginField("scaleXY", np.array(scales[i], dtype=np.float32), trt.PluginFieldType.FLOAT32),
            ]))
        ).get_output(0)

    for new_tensor in new_tensors:
        network.mark_output(new_tensor)
    for old_tensor in old_tensors:
        network.unmark_output(old_tensor)

    return network


def add_concat(network, model_name, logger):
    """Add a final concatenation output into a TensorRT network."""
    if network.num_outputs < 2 or network.num_outputs > 4:
        raise TypeError('bad number of yolo layers: %d' % network.num_outputs)
    yolo_tensors = [network.get_output(i) for i in range(network.num_outputs)]
    concat_tensor = network.add_concatenation(yolo_tensors).get_output(0)
    for yolo_tensor in yolo_tensors:
        network.unmark_output(yolo_tensor)
    concat_tensor.name = 'detections'
    network.mark_output(concat_tensor)
    return network
