#include <channel-groups.h>
#include <tile-cache.h>
#include <inlines.h>
#include <modules/Maps.h>
#include <df/block_square_event_designation_priorityst.h>

#include <random>

// iterates the DF job list and adds channel jobs to the `jobs` container
void ChannelJobs::load_channel_jobs() {
    locations.clear();
    df::job_list_link* node = df::global::world->jobs.list.next;
    while (node) {
        df::job* job = node->item;
        node = node->next;
        if (is_channel_job(job)) {
            locations.emplace(job->pos);
            jobs.emplace(job->pos, job);
        }
    }
}

bool ChannelJobs::has_cavein_conditions(const df::coord &map_pos) {
    if likely(Maps::isValidTilePos(map_pos)) {
        auto p = map_pos;
        auto ttype = *Maps::getTileType(p);
        if (!DFHack::isOpenTerrain(ttype)) {
            // check shared neighbour for cave-in conditions
            df::coord neighbours[4];
            get_connected_neighbours(map_pos, neighbours);
            int connectedness = 4;
            for (auto n: neighbours) {
                if (!Maps::isValidTilePos(n) || active.count(n) || DFHack::isOpenTerrain(*Maps::getTileType(n))) {
                    connectedness--;
                }
            }
            if (!connectedness) {
                // do what?
                p.z--;
                if (!Maps::isValidTilePos(p)) return false;
                ttype = *Maps::getTileType(p);
                if (DFHack::isOpenTerrain(ttype) || DFHack::isFloorTerrain(ttype)) {
                    return true;
                }
            }
        }
    }
    return false;
}

bool ChannelJobs::possible_cavein(const df::coord &job_pos) {
    for (auto iter : active) {
        if (iter == job_pos) continue;
        if (calc_distance(job_pos, iter) <= 2) {
            // find neighbours
            df::coord n1[8];
            df::coord n2[8];
            get_neighbours(job_pos, n1);
            get_neighbours(iter, n2);
            // find shared neighbours
            for (int i = 0; i < 7; ++i) {
                for (int j = i + 1; j < 8; ++j) {
                    if (n1[i] == n2[j]) {
                        if (has_cavein_conditions(n1[i])) {
                            WARN(jobs).print("Channel-Safely::jobs: Cave-in conditions detected at (" COORD ")\n", COORDARGS(n1[i]));
                            return true;
                        }
                    }
                }
            }
        }
    }
    return false;
}

// adds map_pos to a group if an adjacent one exists, or creates one if none exist... if multiple exist they're merged into the first found
void ChannelGroups::add(const df::coord &map_pos) {
    // if we've already added this, we don't need to do it again
    if (groups_map.count(map_pos)) {
        return;
    }
    /* We need to add map_pos to an existing group if possible...
     * So what we do is we look at neighbours to see if they belong to one or more existing groups
     * If there is more than one group, we'll be merging them
     */
    df::coord neighbors[8];
    get_neighbours(map_pos, neighbors);
    Group* group = nullptr;
    int group_index = -1;

    DEBUG(groups).print("    add(" COORD ")\n", COORDARGS(map_pos));
    // and so we begin iterating the neighbours
    for (auto &neighbour: neighbors) {
        if unlikely(!Maps::isValidTilePos(neighbour)) continue;
        // go to the next neighbour if this one doesn't have a group
        if (!groups_map.count(neighbour)) {
            TRACE(groups).print(" -> neighbour is not designated\n");
            continue;
        }
        // get the group, since at least one exists... then merge any additional into that one
        if (!group){
            TRACE(groups).print(" -> group* has no valid state yet\n");
            group_index = groups_map.find(neighbour)->second;
            group = &groups.at(group_index);
        } else {
            TRACE(groups).print(" -> group* has an existing state\n");

            // we don't do anything if the found group is the same as the existing group
            auto index2 = groups_map.find(neighbour)->second;
            if (group_index != index2) {
                // we already have group "prime" if you will, so we're going to merge the new find into prime
                Group &group2 = groups.at(index2);
                // merge
                TRACE(groups).print(" -> merging two groups. group 1 size: %zu. group 2 size: %zu\n", group->size(),
                                    group2.size());
                for (auto pos2: group2) {
                    group->emplace(pos2);
                    groups_map[pos2] = group_index;
                }
                group2.clear();
                free_spots.emplace(index2);
                TRACE(groups).print("    merged size: %zu\n", group->size());
            }
        }
    }
    // if we haven't found at least one group by now we need to create/get one
    if (!group) {
        TRACE(groups).print(" -> no merging took place\n");
        // first we check if we can re-use a group that's been freed
        if (!free_spots.empty()) {
            TRACE(groups).print(" -> use recycled old group\n");
            // first element in a set is always the lowest value, so we re-use from the front of the vector
            group_index = *free_spots.begin();
            group = &groups[group_index];
            free_spots.erase(free_spots.begin());
        } else {
            TRACE(groups).print(" -> brand new group\n");
            // we create a brand-new group to use
            group_index = groups.size();
            groups.emplace_back();
            group = &groups[group_index];
        }
    }
    // puts the "add" in "ChannelGroups::add"
    group->emplace(map_pos);
    DEBUG(groups).print(" = group[%d] of (" COORD ") is size: %zu\n", group_index, COORDARGS(map_pos), group->size());

    // we may have performed a merge, so we update all the `coord -> group index` mappings
    for (auto &wpos: *group) {
        groups_map[wpos] = group_index;
    }
    DEBUG(groups).print(" <- add() exits, there are %zu mappings\n", groups_map.size());
}

// scans a single tile for channel designations
void ChannelGroups::scan_one(const df::coord &map_pos) {
    df::map_block* block = Maps::getTileBlock(map_pos);
    int16_t lx = map_pos.x % 16;
    int16_t ly = map_pos.y % 16;
    if (is_dig_designation(block->designation[lx][ly])) {
        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
                if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) {
                    TRACE(groups).print("   adding (" COORD ")\n", COORDARGS(map_pos));
                    add(map_pos);
                }
            }
        }
    } else if (TileCache::Get().hasChanged(map_pos, block->tiletype[lx][ly])) {
        TileCache::Get().uncache(map_pos);
        remove(map_pos);
    }
}

// builds groupings of adjacent channel designations
void ChannelGroups::scan(bool full_scan) {
    static std::default_random_engine RNG(0);
    static std::bernoulli_distribution sometimes_scanFULLY(0.15);
    if (!full_scan) {
        full_scan = sometimes_scanFULLY(RNG);
    }

    // save current jobs, then clear and load the current jobs
    std::set<df::coord> last_jobs;
    for (auto &pos : jobs) {
        last_jobs.emplace(pos);
    }
    jobs.load_channel_jobs();
    // transpose channel jobs to
    std::set<df::coord> new_jobs;
    std::set<df::coord> gone_jobs;
    set_difference(last_jobs, jobs, gone_jobs);
    set_difference(jobs, last_jobs, new_jobs);
    INFO(groups).print("gone jobs: %zd\nnew jobs: %zd\n",gone_jobs.size(), new_jobs.size());
    for (auto &pos : new_jobs) {
        add(pos);
    }
    for (auto &pos : gone_jobs){
        remove(pos);
    }

    DEBUG(groups).print("  scan()\n");
    // foreach block
    for (int32_t z = mapz - 1; z >= 0; --z) {
        for (int32_t by = 0; by < mapy; ++by) {
            for (int32_t bx = 0; bx < mapx; ++bx) {
                // the block
                if (df::map_block* block = Maps::getBlock(bx, by, z)) {
                    // skip this block?
                    if (!full_scan && !block->flags.bits.designated) {
                        continue;
                    }
                    df::map_block* block_above = Maps::getBlock(bx, by, z+1);
                    // foreach tile
                    bool empty_group = true;
                    for (int16_t lx = 0; lx < 16; ++lx) {
                        for (int16_t ly = 0; ly < 16; ++ly) {
                            // the tile, check if it has a channel designation
                            df::coord map_pos((bx * 16) + lx, (by * 16) + ly, z);
                            if (TileCache::Get().hasChanged(map_pos, block->tiletype[lx][ly])) {
                                TileCache::Get().uncache(map_pos);
                                remove(map_pos);
                                if (jobs.count(map_pos)) {
                                    jobs.erase(map_pos);
                                }
                                block->designation[lx][ly].bits.dig = df::tile_dig_designation::No;
                            } else if (is_dig_designation(block->designation[lx][ly]) || block->occupancy[lx][ly].bits.dig_marked ) {
                                // We have a dig designated, or marked. Some of these will not need intervention.
                                if (block_above &&
                                    !is_channel_designation(block->designation[lx][ly]) &&
                                    !is_channel_designation(block_above->designation[lx][ly])) {
                                    // if this tile isn't a channel designation, and doesn't have a channel designation above it.. we can skip it
                                    continue;
                                }
                                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
                                        TRACE(groups).print("   tile designation priority: %d\n", evT->priority[lx][ly]);
                                        if (evT->priority[lx][ly] < 1000 * config.ignore_threshold) {
                                            if (empty_group) {
                                                group_blocks.emplace(block);
                                                empty_group = false;
                                            }
                                            TRACE(groups).print("   adding (" COORD ")\n", COORDARGS(map_pos));
                                            add(map_pos);
                                        } else if (groups_map.count(map_pos)) {
                                            remove(map_pos);
                                        }
                                    }
                                }
                            }
                        }
                    }
                    // erase the block if we didn't find anything iterating through it
                    if (empty_group) {
                        group_blocks.erase(block);
                    }
                }
            }
        }
    }
    INFO(groups).print("scan() exits\n");
}

// clears out the containers for unloading maps or disabling the plugin
void ChannelGroups::clear() {
    debug_map();
    WARN(groups).print(" <- clearing groups\n");
    jobs.clear();
    group_blocks.clear();
    free_spots.clear();
    groups_map.clear();
    for(size_t i = 0; i < groups.size(); ++i) {
        groups[i].clear();
        free_spots.emplace(i);
    }
}

// erases map_pos from its group, and deletes mappings IFF the group is empty
void ChannelGroups::remove(const df::coord &map_pos) {
    // we don't need to do anything if the position isn't in a group (granted, that should never be the case)
    INFO(groups).print(" remove()\n");
    if (groups_map.count(map_pos)) {
        INFO(groups).print(" -> found group\n");
        // get the group, and map_pos' block*
        int group_index = groups_map.find(map_pos)->second;
        Group &group = groups[group_index];
        // erase map_pos from the group
        INFO(groups).print(" -> erase(" COORD ")\n", COORDARGS(map_pos));
        group.erase(map_pos);
        groups_map.erase(map_pos);
        // clean up if the group is empty
        if (group.empty()) {
            WARN(groups).print(" -> group is empty\n");
            // erase `coord -> group group_index` mappings
            for (auto iter = groups_map.begin(); iter != groups_map.end();) {
                if (group_index == iter->second) {
                    iter = groups_map.erase(iter);
                    continue;
                }
                ++iter;
            }
            // flag the `groups` group_index as available
            free_spots.insert(group_index);
        }
    }
    INFO(groups).print(" remove() exits\n");
}

// finds a group corresponding to a map position if one exists
Groups::const_iterator ChannelGroups::find(const df::coord &map_pos) const {
    const auto iter = groups_map.find(map_pos);
    if (iter != groups_map.end()) {
        return groups.begin() + iter->second;
    }
    return groups.end();
}

// returns an iterator to the first element stored
Groups::const_iterator ChannelGroups::begin() const {
    return groups.begin();
}

// returns an iterator to after the last element stored
Groups::const_iterator ChannelGroups::end() const {
    return groups.end();
}

// returns a count of 0 or 1 depending on whether map_pos is mapped to a group
size_t ChannelGroups::count(const df::coord &map_pos) const {
    return groups_map.count(map_pos);
}

// prints debug info about the groups stored, and their members
void ChannelGroups::debug_groups() {
    if (DFHack::debug_groups.isEnabled(DebugCategory::LDEBUG)) {
        int idx = 0;
        DEBUG(groups).print(" debugging group data\n");
        for (auto &group: groups) {
            DEBUG(groups).print("  group %d (size: %zu)\n", idx, group.size());
            for (auto &pos: group) {
                DEBUG(groups).print("   (%d,%d,%d)\n", pos.x, pos.y, pos.z);
            }
            idx++;
        }
    }
}

// prints debug info group mappings
void ChannelGroups::debug_map() {
    if (DFHack::debug_groups.isEnabled(DebugCategory::LTRACE)) {
        INFO(groups).print("Group Mappings: %zu\n", groups_map.size());
        for (auto &pair: groups_map) {
            TRACE(groups).print(" map[" COORD "] = %d\n", COORDARGS(pair.first), pair.second);
        }
    }
}