//
// Created by Dan Crosby on 10/13/22.
//

#include "src/main/cpp/net/eagle0/shardok/library/action_point_distances/FixedActionPointDistances.hpp"

#include <thread>

#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"

// Dynamic thread count based on hardware capabilities
static const int ASYNC_COUNT = []() {
    const int cores = static_cast<int>(std::thread::hardware_concurrency());
    // Use cores-2 to leave room for OS and other processes, minimum 4 threads
    const int threadCount = std::max(4, cores - 4);
    printf("ActionPointDistances using %d threads (detected %d cores)\n", threadCount, cores);
    return threadCount;
}();

namespace shardok {

void FixedActionPointDistances::SetCacheDirectory(const string& newDir) {
    cacheDirectory = newDir;
    FilesystemUtils::MakeDirectoryIfNecessary(cacheDirectory);
}

static thread_local byte_vector _scratch;

FixedActionPointDistances::FixedActionPointDistances(const HexMap* map, int columnCount)
    : ActionPointDistances(columnCount) {}

auto FixedActionPointDistances::Create(
        const HexMap* map,
        int64_t terrainTypesHash,
        int64_t modifierHash,
        const BattalionTypeSPtr& battalionType,
        bool includeBravingWater,
        int braveWaterActionPointCost) -> CreationResult {
    // Create the object using private constructor
    auto apd = std::shared_ptr<FixedActionPointDistances>(
            new FixedActionPointDistances(map, map->column_count()));

    CreationResult result;
    result.apd = apd;
    result.loadedFromFile = false;

    string path = "";

    if (!cacheDirectory.empty()) {
        std::stringstream stream;
        stream << cacheDirectory;
        stream << std::hex << terrainTypesHash << '/';

        if (auto directoryPath = stream.str(); !FilesystemUtils::FileExistsAtPath(directoryPath)) {
            FilesystemUtils::MakeDirectoryIfNecessary(stream.str());
        }

        stream << std::hex << modifierHash;
        stream << " " << battalionType->typeId;
        if (includeBravingWater) { stream << " " << braveWaterActionPointCost; }
        stream << ".apd";
        path = stream.str();
    }

    const int indexCount = map->row_count() * map->column_count();

    if (!path.empty() && FilesystemUtils::FileExistsAtPath(path)) {
        apd->distances.resize(indexCount);
        // load from file
        const auto& bytes = _scratch.ReplaceWithPath(path);
        const auto* ptr = reinterpret_cast<const DIST_T*>(bytes.data());

        for (int fromIndex = 0; fromIndex < indexCount; fromIndex++) {
            apd->distances[fromIndex].insert(
                    apd->distances[fromIndex].end(),
                    &(ptr[0]),
                    &(ptr[indexCount]));
            ptr += indexCount;
        }
        result.loadedFromFile = true;
    } else {
        _scratch.reserve(indexCount * indexCount * sizeof(DIST_T));

        vector<std::future<vector<vector<DIST_T>>>> futures(indexCount);

        auto braveWaterPossibleCoords =
                includeBravingWater ? apd->BraveWaterPossibleCoords(map) : nullptr;

        int chunkSize = (indexCount + ASYNC_COUNT - 1) / ASYNC_COUNT;
        // Break into chunks for async
        for (int chunkIdx = 0; chunkIdx < ASYNC_COUNT; chunkIdx++) {
            futures[chunkIdx] = std::async(std::launch::async, [=]() -> vector<vector<DIST_T>> {
                vector<vector<DIST_T>> chunkVec;
                chunkVec.reserve(chunkSize);
                const int chunkStartIndex = chunkIdx * chunkSize;

                for (int i = 0; i < chunkSize; i++) {
                    const auto fromIndex = chunkStartIndex + i;
                    if (fromIndex >= indexCount) { continue; }
                    chunkVec.push_back(ActionPointDistances::GenerateDistances(
                            fromIndex,
                            map,
                            includeBravingWater,
                            braveWaterActionPointCost,
                            battalionType,
                            braveWaterPossibleCoords));
                }
                return chunkVec;
            });
        }

        apd->distances.reserve(indexCount);
        _scratch.clear();
        _scratch.reserve(indexCount * indexCount * sizeof(DIST_T));

        for (int chunkIdx = 0; chunkIdx < ASYNC_COUNT; chunkIdx++) {
            auto resultsVec = futures[chunkIdx].get();
            apd->distances.insert(apd->distances.end(), resultsVec.begin(), resultsVec.end());
            for (const auto& r : resultsVec) { _scratch.append(r); }
        }

        if (!path.empty()) { FilesystemUtils::AtomicallySaveToPath(path, _scratch); }
    }

    return result;
}

}  // namespace shardok
