/**
 * @file
 * @brief Simple checks to make sure netlist data structures are consistent.
 *
 * These include checking for duplicated names, dangling links, etc.
 */
#include <cstdio>
#include <cstring>

#include "logic_types.h"
#include "physical_types_util.h"
#include "vtr_assert.h"
#include "vtr_log.h"

#include "vpr_types.h"
#include "vpr_error.h"
#include "globals.h"
#include "hash.h"
#include "vpr_utils.h"
#include "check_netlist.h"

#define ERROR_THRESHOLD 100

/**************** Subroutines local to this module **************************/

static int check_connections_to_global_clb_pins(ClusterNetId net_id, int verbosity, bool is_flat);

static int check_for_duplicated_names();

static int check_clb_conn(ClusterBlockId iblk, int num_conn);

static int check_clb_internal_nets(ClusterBlockId iblk, const IntraLbPbPinLookup& intra_lb_pb_pini_lookup);

/*********************** Subroutine definitions *****************************/

void check_netlist(int verbosity) {
    int error = 0;
    int num_conn;
    t_hash **net_hash_table, *h_net_ptr;

    /* This routine checks that the netlist makes sense. */
    auto& cluster_ctx = g_vpr_ctx.mutable_clustering();

    // Return internal netlist verification first.
    cluster_ctx.clb_nlist.verify();

    net_hash_table = alloc_hash_table();

    /* Check that nets fanout and have a driver. */
    int global_to_non_global_connection_count = 0;
    for (auto net_id : cluster_ctx.clb_nlist.nets()) {
        h_net_ptr = insert_in_hash_table(net_hash_table, cluster_ctx.clb_nlist.net_name(net_id).c_str(), size_t(net_id));
        if (h_net_ptr->count != 1) {
            VTR_LOG_ERROR("Net %s has multiple drivers.\n",
                          cluster_ctx.clb_nlist.net_name(net_id).c_str());
            error++;
        }
        // This function is called during packing - For the time being, we only use a flat netlist during routing
        global_to_non_global_connection_count += check_connections_to_global_clb_pins(net_id, verbosity, false);
        if (error >= ERROR_THRESHOLD) {
            VTR_LOG_ERROR("Too many errors in netlist, exiting.\n");
        }
    }
    free_hash_table(net_hash_table);
    if (global_to_non_global_connection_count > 0) {
        VTR_LOG("Netlist contains %d global net to non-global architecture pin connections\n", global_to_non_global_connection_count);
    }

    auto& device_ctx = g_vpr_ctx.device();
    IntraLbPbPinLookup intra_lb_pb_pin_lookup(device_ctx.logical_block_types);

    /* Check that each block makes sense. */
    for (auto blk_id : cluster_ctx.clb_nlist.blocks()) {
        num_conn = (int)cluster_ctx.clb_nlist.block_pins(blk_id).size();
        error += check_clb_conn(blk_id, num_conn);
        error += check_clb_internal_nets(blk_id, intra_lb_pb_pin_lookup);
        if (error >= ERROR_THRESHOLD) {
            VPR_ERROR(VPR_ERROR_OTHER,
                      "Too many errors in netlist, exiting.\n");
        }
    }

    error += check_for_duplicated_names();

    if (error != 0) {
        VPR_ERROR(VPR_ERROR_OTHER,
                  "Found %d fatal Errors in the input netlist.\n", error);
    }
}

/**
 * @brief Checks that a global net (net_id) connects only to global CLB input pin
 *        and that non-global nets never connects to a global CLB pin.
 *
 * Either global or non-global nets are allowed to connect to pads.
 */
static int check_connections_to_global_clb_pins(ClusterNetId net_id, int verbosity, bool is_flat) {
    auto& cluster_ctx = g_vpr_ctx.clustering();

    bool net_is_ignored = cluster_ctx.clb_nlist.net_is_ignored(net_id);

    /* For now global signals can be driven by an I/O pad or any CLB output       *
     * although a CLB output generates a warning.  I could make a global CLB      *
     * output pin type to allow people to make architectures that didn't have     *
     * this warning.                                                              */
    int global_to_non_global_connection_count = 0;
    for (auto pin_id : cluster_ctx.clb_nlist.net_pins(net_id)) {
        ClusterBlockId blk_id = cluster_ctx.clb_nlist.pin_block(pin_id);
        auto logical_type = cluster_ctx.clb_nlist.block_type(blk_id);
        auto physical_type = pick_physical_type(logical_type);

        int log_index = cluster_ctx.clb_nlist.pin_logical_index(pin_id);
        int pin_index = get_physical_pin(physical_type, logical_type, log_index);

        if (physical_type->is_ignored_pin[pin_index] != net_is_ignored && !physical_type->is_io()) {
            VTR_LOGV_WARN(verbosity > 2,
                          "Global net '%s' connects to non-global architecture pin '%s' (netlist pin '%s')\n",
                          cluster_ctx.clb_nlist.net_name(net_id).c_str(),
                          block_type_pin_index_to_name(physical_type, pin_index, is_flat).c_str(),
                          cluster_ctx.clb_nlist.pin_name(pin_id).c_str());

            ++global_to_non_global_connection_count;
        }
    }

    return global_to_non_global_connection_count;
}

///@brief Checks that the connections into and out of the clb make sense.
static int check_clb_conn(ClusterBlockId iblk, int num_conn) {
    auto& cluster_ctx = g_vpr_ctx.clustering();
    auto& clb_nlist = cluster_ctx.clb_nlist;

    int error = 0;
    t_logical_block_type_ptr type = clb_nlist.block_type(iblk);

    if (num_conn == 1) {
        for (auto pin_id : clb_nlist.block_pins(iblk)) {
            auto pin_type = clb_nlist.pin_type(pin_id);

            if (pin_type == PinType::SINK && !clb_nlist.block_contains_primary_output(iblk)) {
                //Input only and not a Primary-Output block
                VTR_LOG_WARN(
                    "Logic block #%d (%s) has only 1 input pin '%s'"
                    " -- the whole block is hanging logic that should be swept.\n",
                    iblk, clb_nlist.block_name(iblk).c_str(),
                    clb_nlist.pin_name(pin_id).c_str());
            }
            if (pin_type == PinType::DRIVER && !clb_nlist.block_contains_primary_input(iblk)) {
                //Output only and not a Primary-Input block
                VTR_LOG_WARN(
                    "Logic block #%d (%s) has only 1 output pin '%s'."
                    " It may be a constant generator.\n",
                    iblk, clb_nlist.block_name(iblk).c_str(),
                    clb_nlist.pin_name(pin_id).c_str());
            }

            break;
        }
    }

    /* This case should already have been flagged as an error -- this is *
     * just a redundant double check.                                    */

    if (num_conn > type->pb_type->num_pins) {
        VTR_LOG_ERROR("logic block #%d with output %s has %d pins.\n",
                      iblk, cluster_ctx.clb_nlist.block_name(iblk).c_str(), num_conn);
        error++;
    }

    return (error);
}

///@brief Check that internal-to-logic-block connectivity is continuous and logically consistent
static int check_clb_internal_nets(ClusterBlockId iblk, const IntraLbPbPinLookup& pb_graph_pin_lookup) {
    auto& cluster_ctx = g_vpr_ctx.clustering();

    int error = 0;
    const auto& pb_route = cluster_ctx.clb_nlist.block_pb(iblk)->pb_route;
    int num_pins_in_block = cluster_ctx.clb_nlist.block_pb(iblk)->pb_graph_node->total_pb_pins;

    t_logical_block_type_ptr type = cluster_ctx.clb_nlist.block_type(iblk);

    for (int i = 0; i < num_pins_in_block; i++) {
        if (!pb_route.count(i)) continue;

        VTR_ASSERT(pb_route.count(i));

        if (pb_route[i].atom_net_id || pb_route[i].driver_pb_pin_id != UNDEFINED) {
            const t_pb_graph_pin* pb_gpin = pb_graph_pin_lookup.pb_gpin(type->index, i);
            if ((pb_gpin->port->type == IN_PORT && pb_gpin->is_root_block_pin())
                || (pb_gpin->port->type == OUT_PORT && pb_gpin->parent_node->is_primitive())) {
                if (pb_route[i].driver_pb_pin_id != UNDEFINED) {
                    VTR_LOG_ERROR(
                        "Internal connectivity error in logic block #%d with output %s."
                        " Internal node %d driven when it shouldn't be driven \n",
                        iblk, cluster_ctx.clb_nlist.block_name(iblk).c_str(), i);
                    error++;
                }
            } else {
                if (!pb_route[i].atom_net_id || pb_route[i].driver_pb_pin_id == UNDEFINED) {
                    VTR_LOG_ERROR(
                        "Internal connectivity error in logic block #%d with output %s."
                        " Internal node %d dangling\n",
                        iblk, cluster_ctx.clb_nlist.block_name(iblk).c_str(), i);
                    error++;
                } else {
                    int prev_pin = pb_route[i].driver_pb_pin_id;
                    if (pb_route[prev_pin].atom_net_id != pb_route[i].atom_net_id) {
                        VTR_LOG_ERROR(
                            "Internal connectivity error in logic block #%d with output %s."
                            " Internal node %d driven by different net than internal node %d\n",
                            iblk, cluster_ctx.clb_nlist.block_name(iblk).c_str(), i, prev_pin);
                        error++;
                    }
                }
            }
        }
    }
    return error;
}

static int check_for_duplicated_names() {
    int error, clb_count;
    t_hash **clb_hash_table, *clb_h_ptr;

    auto& cluster_ctx = g_vpr_ctx.clustering();

    clb_hash_table = alloc_hash_table();

    error = clb_count = 0;

    for (auto blk_id : cluster_ctx.clb_nlist.blocks()) {
        clb_h_ptr = insert_in_hash_table(clb_hash_table, cluster_ctx.clb_nlist.block_name(blk_id).c_str(), clb_count);
        if (clb_h_ptr->count > 1) {
            VTR_LOG_ERROR("Block %s has duplicated name.\n",
                          cluster_ctx.clb_nlist.block_name(blk_id).c_str());
            error++;
        } else {
            clb_count++;
        }
    }

    free_hash_table(clb_hash_table);

    return error;
}
