// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.

#include "NavMesh.h"
#include "Engine/Core/Log.h"
#include "NavigationScene.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Threading/Threading.h"
#include <ThirdParty/recastnavigation/DetourNavMesh.h>
#include <ThirdParty/recastnavigation/DetourNavMeshQuery.h>

#define MAX_NODES 2048
#define USE_DATA_LINK 0
#define USE_NAV_MESH_ALLOC 1

NavMesh::NavMesh()
{
    _navMesh = nullptr;
    _navMeshQuery = dtAllocNavMeshQuery();
    _tileSize = 0;
}

NavMesh::~NavMesh()
{
    dtFreeNavMesh(_navMesh);
    dtFreeNavMeshQuery(_navMeshQuery);
}

int32 NavMesh::GetTilesCapacity() const
{
    return _navMesh ? _navMesh->getMaxTiles() : 0;
}

void NavMesh::SetTileSize(float tileSize)
{
    ScopeLock lock(Locker);

    // Skip if the same or invalid
    if (Math::NearEqual(_tileSize, tileSize) || tileSize < 1)
        return;

    // Dispose the existing mesh (its invalid)
    if (_navMesh)
    {
        dtFreeNavMesh(_navMesh);
        _navMesh = nullptr;
        _tiles.Clear();
    }

    _tileSize = tileSize;
}

void NavMesh::EnsureCapacity(int32 tilesToAddCount)
{
    ScopeLock lock(Locker);

    const int32 newTilesCount = _tiles.Count() + tilesToAddCount;
    const int32 capacity = GetTilesCapacity();
    if (newTilesCount <= capacity)
        return;

    PROFILE_CPU_NAMED("NavMesh.EnsureCapacity");

    // Navmesh tiles capacity growing rule
    int32 newCapacity = 0;
    if (capacity)
    {
        while (newCapacity < newTilesCount)
        {
            newCapacity = Math::RoundUpToPowerOf2(newCapacity);
        }
    }
    else
    {
        newCapacity = 32;
    }

    LOG(Info, "Resizing navmesh from {0} to {1} tiles capacity", capacity, newCapacity);

    // Ensure to have size assigned
    ASSERT(_tileSize != 0);

    // Fre previous data (if any)
    if (_navMesh)
    {
        dtFreeNavMesh(_navMesh);
    }

    // Allocate new navmesh
    _navMesh = dtAllocNavMesh();
    if (dtStatusFailed(_navMeshQuery->init(_navMesh, MAX_NODES)))
    {
        LOG(Fatal, "Failed to initialize nav mesh.");
    }

    // Prepare parameters
    dtNavMeshParams params;
    params.orig[0] = 0.0f;
    params.orig[1] = 0.0f;
    params.orig[2] = 0.0f;
    params.tileWidth = _tileSize;
    params.tileHeight = _tileSize;
    params.maxTiles = newCapacity;
    const int32 tilesBits = (int32)Math::Log2((float)Math::RoundUpToPowerOf2(params.maxTiles));
    params.maxPolys = 1 << (22 - tilesBits);

    // Initialize nav mesh
    if (dtStatusFailed(_navMesh->init(&params)))
    {
        LOG(Fatal, "Navmesh init failed.");
        return;
    }

    // Prepare tiles container
    _tiles.EnsureCapacity(newCapacity);

    // Restore previous tiles
    for (auto& tile : _tiles)
    {
        const int32 dataSize = tile.Data.Length();
#if USE_NAV_MESH_ALLOC
        const auto flags = DT_TILE_FREE_DATA;
        const auto data = (byte*)dtAlloc(dataSize, DT_ALLOC_PERM);
        Platform::MemoryCopy(data, tile.Data.Get(), dataSize);
#else
		const auto flags = 0;
		const auto data = tile.Data.Get();
#endif
        if (dtStatusFailed(_navMesh->addTile(data, dataSize, flags, 0, nullptr)))
        {
            LOG(Warning, "Could not add tile to navmesh.");
        }
    }
}

void NavMesh::AddTiles(NavigationScene* scene)
{
    // Skip if no data
    ASSERT(scene);
    if (scene->Data.Tiles.IsEmpty())
        return;
    auto& data = scene->Data;

    PROFILE_CPU_NAMED("NavMesh.AddTiles");

    ScopeLock lock(Locker);

    // Validate data (must match navmesh) or init navmesh to match the tiles options
    if (_navMesh)
    {
        if (Math::NotNearEqual(data.TileSize, _tileSize))
        {
            LOG(Warning, "Cannot add navigation scene tiles to the navmesh. Navmesh tile size: {0}, input tiles size: {1}", _tileSize, data.TileSize);
            return;
        }
    }
    else
    {
        _tileSize = data.TileSize;
    }

    // Ensure to have space for new tiles
    EnsureCapacity(data.Tiles.Count());

    // Add new tiles
    for (auto& tileData : data.Tiles)
    {
        AddTileInternal(scene, tileData);
    }
}

void NavMesh::AddTile(NavigationScene* scene, NavMeshTileData& tileData)
{
    ASSERT(scene);
    auto& data = scene->Data;

    PROFILE_CPU_NAMED("NavMesh.AddTile");

    ScopeLock lock(Locker);

    // Validate data (must match navmesh) or init navmesh to match the tiles options
    if (_navMesh)
    {
        if (Math::NotNearEqual(data.TileSize, _tileSize))
        {
            LOG(Warning, "Cannot add navigation scene tile to the navmesh. Navmesh tile size: {0}, input tile size: {1}", _tileSize, data.TileSize);
            return;
        }
    }
    else
    {
        _tileSize = data.TileSize;
    }

    // Ensure to have space for new tile
    EnsureCapacity(1);

    // Add new tile
    AddTileInternal(scene, tileData);
}

bool IsTileFromScene(const NavMesh* navMesh, const NavMeshTile& tile, void* customData)
{
    return tile.Scene == (NavigationScene*)customData;
}

void NavMesh::RemoveTiles(NavigationScene* scene)
{
    RemoveTiles(IsTileFromScene, scene);
}

void NavMesh::RemoveTile(int32 x, int32 y, int32 layer)
{
    ScopeLock lock(Locker);

    // Skip if no data
    if (!_navMesh)
        return;

    PROFILE_CPU_NAMED("NavMesh.RemoveTile");

    const auto tileRef = _navMesh->getTileRefAt(x, y, layer);
    if (tileRef == 0)
    {
        return;
    }

    if (dtStatusFailed(_navMesh->removeTile(tileRef, nullptr, nullptr)))
    {
        LOG(Warning, "Failed to remove tile from navmesh.");
    }

    for (int32 i = 0; i < _tiles.Count(); i++)
    {
        auto& tile = _tiles[i];
        if (tile.X == x && tile.Y == y && tile.Layer == layer)
        {
            _tiles.RemoveAt(i);
            break;
        }
    }
}

void NavMesh::RemoveTiles(bool (* prediction)(const NavMesh* navMesh, const NavMeshTile& tile, void* customData), void* userData)
{
    ScopeLock lock(Locker);

    // Skip if no data
    ASSERT(prediction);
    if (!_navMesh)
        return;

    PROFILE_CPU_NAMED("NavMesh.RemoveTiles");

    for (int32 i = 0; i < _tiles.Count(); i++)
    {
        auto& tile = _tiles[i];
        if (prediction(this, tile, userData))
        {
            const auto tileRef = _navMesh->getTileRefAt(tile.X, tile.Y, tile.Layer);
            if (tileRef == 0)
            {
                LOG(Warning, "Missing navmesh tile at {0}x{1}, layer: {2}", tile.X, tile.Y, tile.Layer);
            }
            else
            {
                if (dtStatusFailed(_navMesh->removeTile(tileRef, nullptr, nullptr)))
                {
                    LOG(Warning, "Failed to remove tile from navmesh.");
                }
            }

            _tiles.RemoveAt(i--);
        }
    }
}

void NavMesh::Dispose()
{
    if (_navMesh)
    {
        dtFreeNavMesh(_navMesh);
        _navMesh = nullptr;
    }
    _tiles.Resize(0);
}

void NavMesh::AddTileInternal(NavigationScene* scene, NavMeshTileData& tileData)
{
    // Check if that tile has been added to navmesh
    NavMeshTile* tile = nullptr;
    const auto tileRef = _navMesh->getTileRefAt(tileData.PosX, tileData.PosY, tileData.Layer);
    if (tileRef)
    {
        // Remove any existing tile at that location
        _navMesh->removeTile(tileRef, nullptr, nullptr);

        // Reuse tile data container
        for (int32 i = 0; i < _tiles.Count(); i++)
        {
            auto& e = _tiles[i];
            if (e.X == tileData.PosX && e.Y == tileData.PosY && e.Layer == tileData.Layer)
            {
                tile = &e;
                break;
            }
        }
    }
    else
    {
        // Add tile
        tile = &_tiles.AddOne();
    }
    ASSERT(tile);

    // Copy tile properties
    tile->Scene = scene;
    tile->X = tileData.PosX;
    tile->Y = tileData.PosY;
    tile->Layer = tileData.Layer;
#if USE_DATA_LINK
	tile->Data.Link(tileData.Data);
#else
    tile->Data.Copy(tileData.Data);
#endif

    // Add tile to navmesh
    const int32 dataSize = tile->Data.Length();
#if USE_NAV_MESH_ALLOC
    const auto flags = DT_TILE_FREE_DATA;
    const auto data = (byte*)dtAlloc(dataSize, DT_ALLOC_PERM);
    Platform::MemoryCopy(data, tile->Data.Get(), dataSize);
#else
	const auto flags = 0;
	const auto data = tile->Data.Get();
#endif
    if (dtStatusFailed(_navMesh->addTile(data, dataSize, flags, 0, nullptr)))
    {
        LOG(Warning, "Could not add tile to navmesh.");
    }
}
