#include <channel-manager.h>
#include <tile-cache.h>
#include <inlines.h>

#include <modules/EventManager.h> //hash function for df::coord
#include <df/block_square_event_designation_priorityst.h>

#define NUMARGS(...) std::tuple_size<decltype(std::make_tuple(__VA_ARGS__))>::value
#define d_assert(condition, ...) \
            static_assert(NUMARGS(__VA_ARGS__) >= 1, "d_assert(condition, format, ...) requires at least up to format as arguments"); \
            if (!condition) {                                                   \
                DFHack::Core::getInstance().getConsole().printerr(__VA_ARGS__); \
                assert(0);                                                      \
            }


df::unit* find_dwarf(const df::coord &map_pos) {

    df::unit* nearest = nullptr;
    uint32_t distance;
    for (auto unit : df::global::world->units.active) {
        if (!nearest) {
            nearest = unit;
            distance = calc_distance(unit->pos, map_pos);
        } else if (unit->status.labors[df::unit_labor::MINE]) {
            uint32_t d = calc_distance(unit->pos, map_pos);
            if (d < distance) {
                nearest = unit;
                distance = d;
            } else if (Maps::canWalkBetween(unit->pos, map_pos)) {
                return unit;
            }
        }
    }
    return nearest;
}

// sets mark flags as necessary, for all designations
void ChannelManager::manage_groups() {
    INFO(manager).print("manage_groups()\n");
    // make sure we've got a fort map to analyze
    if (World::isFortressMode() && Maps::IsValid()) {
        // iterate the groups we built/updated
        for (const auto &group: groups) {
            manage_group(group, true, has_any_groups_above(groups, group));
        }
    }
}

void ChannelManager::manage_group(const df::coord &map_pos, bool set_marker_mode, bool marker_mode) {
    INFO(manager).print("manage_group(" COORD ")\n ", COORDARGS(map_pos));
    if (!groups.count(map_pos)) {
        groups.scan_one(map_pos);
    }
    auto iter = groups.find(map_pos);
    if (iter != groups.end()) {
        manage_group(*iter, set_marker_mode, marker_mode);
    }
    INFO(manager).print("manage_group() is done\n");
}

void ChannelManager::manage_group(const Group &group, bool set_marker_mode, bool marker_mode) const {
    INFO(manager).print("manage_group()\n");
    if (!set_marker_mode) {
        marker_mode = has_any_groups_above(groups, group);
    }
    // cavein prevention
    bool cavein_possible = false;
    uint8_t least_access = 100;

    std::unordered_map<df::coord, uint8_t> cavein_candidates;
    if (!marker_mode) {
        /* To prevent cave-ins we're looking at accessibility of tiles with open space below them
         * If it has space below, it has somewhere to fall
         * Accessibility tells us how close to a cave-in a tile is, low values are at risk of cave-ins
         * To count access, we find a random miner dwarf and count how many tile neighbours they can path to
         * */
        // find a dwarf to path from
        df::coord miner_pos = find_dwarf(*group.begin())->pos;

        // Analyze designations
        for (const auto &pos: group) {
            df::coord below(pos);
            below.z--;
            const auto below_ttype = *Maps::getTileType(below);
            // we can skip designations already queued for insta-digging
            if (CSP::dignow_queue.count(pos)) continue;
            if (DFHack::isOpenTerrain(below_ttype) || DFHack::isFloorTerrain(below_ttype)) {
                // the tile below is not solid earth
                // we're counting accessibility then dealing with 0 access
                DEBUG(manager).print("analysis: cave-in condition found\n");
                INFO(manager).print("(%d) checking accessibility of (" COORD ") from (" COORD ")\n", cavein_possible,COORDARGS(pos),COORDARGS(miner_pos));
                auto access = count_accessibility(miner_pos, pos);
                if (access == 0) {
                    TRACE(groups).print(" has 0 access\n");
                    if (config.insta_dig) {
                        manage_one(pos, true, false);
                        dig_now(DFHack::Core::getInstance().getConsole(), pos);
                    } else {
                        // todo: engage dig management, swap channel designations for dig
                    }
                } else {
                    WARN(manager).print(" has %d access\n", access);
                    cavein_possible = config.riskaverse;
                    cavein_candidates.emplace(pos, access);
                    least_access = std::min(access, least_access);
                }
            } else if (config.insta_dig && isEntombed(miner_pos, pos)) {
                manage_one(pos, true, false);
                dig_now(DFHack::Core::getInstance().getConsole(), pos);
            }
        }
        DEBUG(manager).print("cavein possible(%d)\n"
                             "%zu candidates\n"
                             "least access %d\n", cavein_possible, cavein_candidates.size(), least_access);
    }
    // managing designations
    if (!group.empty()) {
        DEBUG(manager).print("managing group #%d\n", groups.debugGIndex(*group.begin()));
    }
    for (auto &pos: group) {
        // if no cave-in is possible [or we don't check for], we'll just execute normally and move on
        if (!cavein_possible) {
            TRACE(manager).print("cave-in evaluated false\n");
            d_assert(manage_one(pos, true, marker_mode), "manage_one() is failing under !cavein");
            continue;
        }
        // cavein is only possible if marker_mode is false
        // we want to dig the cavein candidates first, the least accessible ones specifically
        const static uint8_t MAX = 84; //arbitrary number that indicates the value has changed
        const static uint8_t OFFSET = 2; //value has been tweaked to avoid cave-ins whilst activating as many designations as possible
        if (CSP::dignow_queue.count(pos) || (cavein_candidates.count(pos) &&
                                             least_access < MAX && cavein_candidates[pos] <= least_access+OFFSET)) {

            TRACE(manager).print("cave-in evaluated true and either of dignow or (%d <= %d)\n", cavein_candidates[pos], least_access+OFFSET);
            df::coord local(pos);
            local.x %= 16;
            local.y %= 16;
            auto block = Maps::ensureTileBlock(pos);
            // if we don't find the priority in block_events, it probably means bad things
            for (df::block_square_event* event: block->block_events) {
                if (auto evT = virtual_cast<df::block_square_event_designation_priorityst>(event)) {
                    // we want to let the user keep some designations free of being managed
                    auto b = std::max(0, cavein_candidates[pos] - least_access);
                    auto v = 1000 + (b * 1700);
                    DEBUG(manager).print("(" COORD ") 1000+1000(%d) -> %d {least-access: %d}\n",COORDARGS(pos), b, v, least_access);
                    evT->priority[Coord(local)] = v;
                }
            }
            d_assert(manage_one(pos, true, false), "manage_one() is failing for cavein ");
            continue;
        }
        // cavein possible, but we failed to meet the criteria for activation
        if (cavein_candidates.count(pos)) {
            DEBUG(manager).print("cave-in evaluated true and the cavein candidate's accessibility check was made as (%d <= %d)\n", cavein_candidates[pos], least_access+OFFSET);
        } else {
            DEBUG(manager).print("cave-in evaluated true and the position was not a candidate, nor was it set for dignow\n");
        }
        d_assert(manage_one(pos, true, true), "manage_one() is failing to set a cave-in causing designation to marker mode");
    }
    INFO(manager).print("manage_group() is done\n");
}

bool ChannelManager::manage_one(const df::coord &map_pos, bool set_marker_mode, bool marker_mode) const {
    if (Maps::isValidTilePos(map_pos)) {
        TRACE(manager).print("manage_one((" COORD "), %d, %d)\n", COORDARGS(map_pos), set_marker_mode, marker_mode);
        df::map_block* block = Maps::getTileBlock(map_pos);
        // we calculate the position inside the block*
        df::coord local(map_pos);
        local.x = local.x % 16;
        local.y = local.y % 16;
        df::tile_occupancy &tile_occupancy = block->occupancy[Coord(local)];
        // ensure that we aren't on the top-most layers
        if (map_pos.z < mapz - 3) {
            // do we already know whether to set marker mode?
            if (!marker_mode) {
                // marker_mode is set to true if it is unsafe to dig
                marker_mode = (!set_marker_mode &&
                               (has_group_above(groups, map_pos) || !is_safe_to_dig_down(map_pos))) ||
                              (set_marker_mode &&
                               is_channel_designation(block->designation[Coord(local)]) && !is_safe_to_dig_down(map_pos));
            }
            if (marker_mode) {
                if (jobs.count(map_pos)) {
                    cancel_job(map_pos);
                }
            } else if (!block->flags.bits.designated) {
                block->flags.bits.designated = true;
            }
            tile_occupancy.bits.dig_marked = marker_mode;
            TRACE(manager).print("marker mode %s\n", marker_mode ? "ENABLED" : "DISABLED");
        } else {
            // if we are though, it should be totally safe to dig
            tile_occupancy.bits.dig_marked = false;
        }
        TRACE(manager).print(" <- manage_one() exits normally\n");
        return true;
    }
    return false;
}

void ChannelManager::mark_done(const df::coord &map_pos) {
    groups.remove(map_pos);
    jobs.erase(map_pos);
    CSP::dignow_queue.erase(map_pos);
    TileCache::Get().uncache(map_pos);
}