/*
 *  nextpnr -- Next Generation Place and Route
 *
 *  Copyright (C) 2018  Clifford Wolf <clifford@symbioticeda.com>
 *
 *  Permission to use, copy, modify, and/or distribute this software for any
 *  purpose with or without fee is hereby granted, provided that the above
 *  copyright notice and this permission notice appear in all copies.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 *  WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 *  MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 *  ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 *  WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 *  ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 *  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */

#include "log.h"
#include "nextpnr.h"

#if 0
#define dbg(...) log(__VA_ARGS__)
#else
#define dbg(...)
#endif

USING_NEXTPNR_NAMESPACE

namespace {

void archcheck_names(const Context *ctx)
{
    log_info("Checking entity names.\n");

    log_info("Checking bel names..\n");
    for (BelId bel : ctx->getBels()) {
        IdStringList name = ctx->getBelName(bel);
        BelId bel2 = ctx->getBelByName(name);
        if (bel != bel2) {
            log_error("bel != bel2, name = %s\n", ctx->nameOfBel(bel));
        }
    }

    log_info("Checking wire names..\n");
    for (WireId wire : ctx->getWires()) {
        IdStringList name = ctx->getWireName(wire);
        WireId wire2 = ctx->getWireByName(name);
        if (wire != wire2) {
            log_error("wire != wire2, name = %s\n", ctx->nameOfWire(wire));
        }
    }

    log_info("Checking bucket names..\n");
    for (BelBucketId bucket : ctx->getBelBuckets()) {
        IdString name = ctx->getBelBucketName(bucket);
        BelBucketId bucket2 = ctx->getBelBucketByName(name);
        if (bucket != bucket2) {
            log_error("bucket != bucket2, name = %s\n", name.c_str(ctx));
        }
    }

#ifndef ARCH_ECP5
    log_info("Checking pip names..\n");
    for (PipId pip : ctx->getPips()) {
        IdStringList name = ctx->getPipName(pip);
        PipId pip2 = ctx->getPipByName(name);
        if (pip != pip2) {
            log_error("pip != pip2, name = %s\n", ctx->nameOfPip(pip));
        }
    }
#endif
    log_break();
}

void archcheck_locs(const Context *ctx)
{
    log_info("Checking location data.\n");

    log_info("Checking all bels..\n");
    for (BelId bel : ctx->getBels()) {
        log_assert(bel != BelId());
        dbg("> %s\n", ctx->getBelName(bel).c_str(ctx));

        Loc loc = ctx->getBelLocation(bel);
        dbg("   ... %d %d %d\n", loc.x, loc.y, loc.z);

        log_assert(0 <= loc.x);
        log_assert(0 <= loc.y);
        log_assert(0 <= loc.z);
        log_assert(loc.x < ctx->getGridDimX());
        log_assert(loc.y < ctx->getGridDimY());
        log_assert(loc.z < ctx->getTileBelDimZ(loc.x, loc.y));

        BelId bel2 = ctx->getBelByLocation(loc);
        dbg("   ... %s\n", ctx->getBelName(bel2).c_str(ctx));
        log_assert(bel == bel2);
    }

    log_info("Checking all locations..\n");
    for (int x = 0; x < ctx->getGridDimX(); x++)
        for (int y = 0; y < ctx->getGridDimY(); y++) {
            dbg("> %d %d\n", x, y);
            std::unordered_set<int> usedz;

            for (int z = 0; z < ctx->getTileBelDimZ(x, y); z++) {
                BelId bel = ctx->getBelByLocation(Loc(x, y, z));
                if (bel == BelId())
                    continue;
                Loc loc = ctx->getBelLocation(bel);
                dbg("   + %d %s\n", z, ctx->nameOfBel(bel));
                log_assert(x == loc.x);
                log_assert(y == loc.y);
                log_assert(z == loc.z);
                usedz.insert(z);
            }

            for (BelId bel : ctx->getBelsByTile(x, y)) {
                Loc loc = ctx->getBelLocation(bel);
                dbg("   - %d %s\n", loc.z, ctx->nameOfBel(bel));
                log_assert(x == loc.x);
                log_assert(y == loc.y);
                log_assert(usedz.count(loc.z));
                usedz.erase(loc.z);
            }

            log_assert(usedz.empty());
        }

    log_break();
}

// Implements a LRU cache for pip to wire via getPipsDownhill/getPipsUphill.
//
// This allows a fast way to check getPipsDownhill/getPipsUphill from getPips,
// without balloning memory usage.
struct LruWireCacheMap
{
    LruWireCacheMap(const Context *ctx, size_t cache_size) : ctx(ctx), cache_size(cache_size)
    {
        cache_hits = 0;
        cache_misses = 0;
        cache_evictions = 0;
    }

    const Context *ctx;
    size_t cache_size;

    // Cache stats for checking on cache behavior.
    size_t cache_hits;
    size_t cache_misses;
    size_t cache_evictions;

    // Most recent accessed wires are added to the back of the list, front of
    // list is oldest wire in cache.
    std::list<WireId> last_access_list;
    // Quick wire -> list element lookup.
    std::unordered_map<WireId, std::list<WireId>::iterator> last_access_map;

    std::unordered_map<PipId, WireId> pips_downhill;
    std::unordered_map<PipId, WireId> pips_uphill;

    void removeWireFromCache(WireId wire_to_remove)
    {
        for (PipId pip : ctx->getPipsDownhill(wire_to_remove)) {
            log_assert(pips_downhill.erase(pip) == 1);
        }

        for (PipId pip : ctx->getPipsUphill(wire_to_remove)) {
            log_assert(pips_uphill.erase(pip) == 1);
        }
    }

    void addWireToCache(WireId wire)
    {
        for (PipId pip : ctx->getPipsDownhill(wire)) {
            auto result = pips_downhill.emplace(pip, wire);
            log_assert(result.second);
        }

        for (PipId pip : ctx->getPipsUphill(wire)) {
            auto result = pips_uphill.emplace(pip, wire);
            log_assert(result.second);
        }
    }

    void populateCache(WireId wire)
    {
        // Put this wire at the end of last_access_list.
        auto iter = last_access_list.emplace(last_access_list.end(), wire);
        last_access_map.emplace(wire, iter);

        if (last_access_list.size() > cache_size) {
            // Cache is full, remove front of last_access_list.
            cache_evictions += 1;
            WireId wire_to_remove = last_access_list.front();
            last_access_list.pop_front();
            log_assert(last_access_map.erase(wire_to_remove) == 1);

            removeWireFromCache(wire_to_remove);
        }

        addWireToCache(wire);
    }

    // Determine if wire is in the cache.  If wire is not in the cache,
    // adds the wire to the cache, and potentially evicts the oldest wire if
    // cache is now full.
    void checkCache(WireId wire)
    {
        auto iter = last_access_map.find(wire);
        if (iter == last_access_map.end()) {
            cache_misses += 1;
            populateCache(wire);
        } else {
            // Record that this wire has been accessed.
            cache_hits += 1;
            last_access_list.splice(last_access_list.end(), last_access_list, iter->second);
        }
    }

    // Returns true if pip is uphill of wire (e.g. pip in getPipsUphill(wire)).
    bool isPipUphill(PipId pip, WireId wire)
    {
        checkCache(wire);
        return pips_uphill.at(pip) == wire;
    }

    // Returns true if pip is downhill of wire (e.g. pip in getPipsDownhill(wire)).
    bool isPipDownhill(PipId pip, WireId wire)
    {
        checkCache(wire);
        return pips_downhill.at(pip) == wire;
    }

    void cache_info() const
    {
        log_info("Cache hits: %zu\n", cache_hits);
        log_info("Cache misses: %zu\n", cache_misses);
        log_info("Cache evictions: %zu\n", cache_evictions);
    }
};

void archcheck_conn(const Context *ctx)
{
    log_info("Checking connectivity data.\n");

    log_info("Checking all wires...\n");

    for (WireId wire : ctx->getWires()) {
        for (BelPin belpin : ctx->getWireBelPins(wire)) {
            WireId wire2 = ctx->getBelPinWire(belpin.bel, belpin.pin);
            log_assert(wire == wire2);
        }

        for (PipId pip : ctx->getPipsDownhill(wire)) {
            WireId wire2 = ctx->getPipSrcWire(pip);
            log_assert(wire == wire2);
        }

        for (PipId pip : ctx->getPipsUphill(wire)) {
            WireId wire2 = ctx->getPipDstWire(pip);
            log_assert(wire == wire2);
        }
    }

    log_info("Checking all BELs...\n");
    for (BelId bel : ctx->getBels()) {
        for (IdString pin : ctx->getBelPins(bel)) {
            WireId wire = ctx->getBelPinWire(bel, pin);

            if (wire == WireId()) {
                continue;
            }

            bool found_belpin = false;
            for (BelPin belpin : ctx->getWireBelPins(wire)) {
                if (belpin.bel == bel && belpin.pin == pin) {
                    found_belpin = true;
                    break;
                }
            }

            log_assert(found_belpin);
        }
    }

    // This cache is used to meet two goals:
    //  - Avoid linear scan by invoking getPipsDownhill/getPipsUphill directly.
    //  - Avoid having pip -> wire maps for the entire part.
    //
    // The overhead of maintaining the cache is small relatively to the memory
    // gains by avoiding the full pip -> wire map, and still preserves a fast
    // pip -> wire, assuming that pips are returned from getPips with some
    // chip locality.
    LruWireCacheMap pip_cache(ctx, /*cache_size=*/64 * 1024);
    log_info("Checking all PIPs...\n");
    for (PipId pip : ctx->getPips()) {
        WireId src_wire = ctx->getPipSrcWire(pip);
        if (src_wire != WireId()) {
            log_assert(pip_cache.isPipDownhill(pip, src_wire));
        }

        WireId dst_wire = ctx->getPipDstWire(pip);
        if (dst_wire != WireId()) {
            log_assert(pip_cache.isPipUphill(pip, dst_wire));
        }
    }
}

void archcheck_buckets(const Context *ctx)
{
    log_info("Checking bucket data.\n");

    // BEL buckets should be subsets of BELs that form an exact cover.
    // In particular that means cell types in a bucket should only be
    // placable in that bucket.
    for (BelBucketId bucket : ctx->getBelBuckets()) {

        // Find out which cell types are in this bucket.
        std::unordered_set<IdString> cell_types_in_bucket;
        for (IdString cell_type : ctx->getCellTypes()) {
            if (ctx->getBelBucketForCellType(cell_type) == bucket) {
                cell_types_in_bucket.insert(cell_type);
            }
        }

        // Make sure that all cell types in this bucket have at least one
        // BelId they can be placed at.
        std::unordered_set<IdString> cell_types_unused;

        std::unordered_set<BelId> bels_in_bucket;
        for (BelId bel : ctx->getBelsInBucket(bucket)) {
            BelBucketId bucket2 = ctx->getBelBucketForBel(bel);
            log_assert(bucket == bucket2);

            bels_in_bucket.insert(bel);

            // Check to see if a cell type not in this bucket can be
            // placed at a BEL in this bucket.
            for (IdString cell_type : ctx->getCellTypes()) {
                if (ctx->getBelBucketForCellType(cell_type) == bucket) {
                    if (ctx->isValidBelForCellType(cell_type, bel)) {
                        cell_types_unused.erase(cell_type);
                    }
                } else {
                    log_assert(!ctx->isValidBelForCellType(cell_type, bel));
                }
            }
        }

        // Verify that any BEL not in this bucket reports a different
        // bucket.
        for (BelId bel : ctx->getBels()) {
            if (ctx->getBelBucketForBel(bel) != bucket) {
                log_assert(bels_in_bucket.count(bel) == 0);
            }
        }

        log_assert(cell_types_unused.empty());
    }
}

} // namespace

NEXTPNR_NAMESPACE_BEGIN

void Context::archcheck() const
{
    log_info("Running architecture database integrity check.\n");
    log_break();

    archcheck_names(this);
    archcheck_locs(this);
    archcheck_conn(this);
    archcheck_buckets(this);
}

NEXTPNR_NAMESPACE_END