/*******************************************************************************
 *
 * MIT License
 *
 * Copyright (c) 2019 Advanced Micro Devices, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 *******************************************************************************/

#include <miopen/find_db.hpp>

#include <miopen/handle.hpp>
#include <miopen/logger.hpp>
#include <miopen/perf_field.hpp>
#if MIOPEN_EMBED_DB
#include <miopen_data.hpp>
#endif
#include <miopen/filesystem.hpp>

#include <optional>
#include <string>
#include <vector>

namespace miopen {

namespace debug {

// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
MIOPEN_EXPORT bool testing_find_db_enabled = true;

/// \todo Remove when #1723 is resolved.
std::optional<fs::path>& testing_find_db_path_override()
{
    // NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
    static std::optional<fs::path> data{std::nullopt};
    return data;
}

} // namespace debug

#if MIOPEN_EMBED_DB
template <class TDb>
fs::path FindDbRecord_t<TDb>::GetInstalledPathEmbed(const Handle& handle,
                                                    const std::string& path_suffix)
{
    static const auto embed_path = [&] {
        const std::string ext = ".fdb.txt";
        const auto root_path  = GetSystemDbPath();
        const auto base_name  = handle.GetDbBasename();
        const auto suffix     = GetSystemFindDbSuffix() + path_suffix;
        const auto filename   = base_name + "." + suffix + ext;
        const auto file_path  = root_path / filename;
        if(miopen_data().find(make_object_file_name(filename)) != miopen_data().end())
        {
            MIOPEN_LOG_I2("Found exact embedded find database file:" << filename);
            return file_path;
        }
        else
        {
            MIOPEN_LOG_I2("inexact find database search");
            std::vector<fs::path> all_files;
            for(const auto& kinder : miopen_data())
            {
                const auto& fname    = kinder.first.stem();
                const auto& filepath = root_path / fname;
                if(EndsWith(fname.string(), path_suffix + ".fdb.txt"))
                    all_files.push_back(filepath);
            }

            const auto db_id        = handle.GetTargetProperties().DbId();
            const int real_cu_count = handle.GetMaxComputeUnits();
            int closest_cu          = std::numeric_limits<int>::max();
            fs::path best_path;
            for(const auto& entry : all_files)
            {
                const auto fname = entry.stem().string();
                MIOPEN_LOG_I2("Checking embedded find db file: " << fname);
                // Check for alternate back end same ASIC
                if(fname.rfind(base_name, 0) == 0)
                {
                    return entry;
                }
                if(db_id.empty() || !miopen::StartsWith(db_id, "gfx") || real_cu_count == 0)
                    return fs::path{};
                // Check for alternate ASIC any back end
                if(fname.rfind(db_id, 0) == 0)
                {
                    const auto pos = fname.find('_');
                    int cur_count  = -1;
                    if(pos != std::string::npos)
                        cur_count = std::stoi(fname.substr(pos + 1));
                    else
                        cur_count = std::stoi(fname.substr(db_id.length()), nullptr, 16);
                    if(abs(cur_count - real_cu_count) < (closest_cu))
                    {
                        best_path  = entry;
                        closest_cu = abs(cur_count - real_cu_count);
                    }
                }
            }
            return best_path;
        }
    }();
    return embed_path;
}

#else

template <class TDb>
fs::path FindDbRecord_t<TDb>::GetInstalledPathFile(const Handle& handle,
                                                   const std::string& path_suffix)
{
    static const fs::path installed_path = [&] {
        const std::string ext = ".fdb.txt";
        const auto root_path  = GetSystemDbPath();
        const auto base_name  = handle.GetDbBasename();
        const auto suffix =
            GetSystemFindDbSuffix() + (path_suffix.empty() ? "" : ('.' + path_suffix));
        auto file_path = root_path / (base_name + "." + suffix + ext);
        if(fs::exists(file_path))
        {
            MIOPEN_LOG_I2("Found exact find database file: " << file_path);
            return std::move(file_path);
        }

        MIOPEN_LOG_I2("inexact find database search");
        if(!fs::is_directory(root_path))
        {
            MIOPEN_LOG_I("Database directory does not exist");
            file_path = fs::path{};
            return std::move(file_path);
        }

        MIOPEN_LOG_I2("Iterating over find db directory " << root_path);
        std::vector<fs::path> all_files;
        std::vector<fs::path> contents;
        std::copy(fs::directory_iterator(root_path),
                  fs::directory_iterator(),
                  std::back_inserter(contents));
        for(auto const& filepath : contents)
        {
            if(fs::is_regular_file(filepath) && filepath.extension() == path_suffix + ".fdb.txt")
                all_files.push_back(filepath);
        }

        const auto db_id        = handle.GetTargetProperties().DbId();
        const int real_cu_count = handle.GetMaxComputeUnits();
        int closest_cu          = std::numeric_limits<int>::max();

        for(const auto& entry : all_files)
        {
            const auto fname = entry.stem().string();
            MIOPEN_LOG_I("Checking find db file: " << fname);
            // Check for alternate back end same ASIC
            if(fname.starts_with(base_name))
            {
                file_path = entry;
                return std::move(file_path);
            }
            if(db_id.empty() || !miopen::StartsWith(db_id, "gfx") || real_cu_count == 0)
            {
                file_path = fs::path{};
                return std::move(file_path); // empty
            }
            // Check for alternate ASIC any back end
            if(fname.starts_with(db_id))
            {
                const auto pos = fname.find('_');
                int cur_count  = -1;
                if(pos != std::string::npos)
                    cur_count = std::stoi(fname.substr(pos + 1));
                else
                    cur_count = std::stoi(fname.substr(db_id.length()), nullptr, 16);
                if(abs(cur_count - real_cu_count) < (closest_cu))
                {
                    file_path  = entry;
                    closest_cu = abs(cur_count - real_cu_count);
                }
            }
        }
        return std::move(file_path);
    }();

    return installed_path;
}
#endif
template <class TDb>
fs::path FindDbRecord_t<TDb>::GetInstalledPath(const Handle& handle, const std::string& path_suffix)
{
#if !MIOPEN_DISABLE_SYSDB
#if MIOPEN_EMBED_DB
    return GetInstalledPathEmbed(handle, path_suffix);
#else
    return GetInstalledPathFile(handle, path_suffix);
#endif
#else
    std::ignore = handle;
    std::ignore = path_suffix;
    return "";
#endif
}

template <class TDb>
fs::path FindDbRecord_t<TDb>::GetUserPath(const Handle& handle, const std::string& path_suffix)
{
#if !MIOPEN_DISABLE_USERDB
    return GetUserDbPath() / (handle.GetDbBasename() + '.' + GetUserDbSuffix() +
                              (path_suffix.empty() ? "" : '.' + path_suffix) + ".ufdb.txt");
#else
    std::ignore = handle;
    std::ignore = path_suffix;
    return "";
#endif
}

template <class TDb>
bool FindDbRecord_t<TDb>::Validate(const Handle& handle, const NetworkConfig& config) const
{
    auto unbuilt = false;
    auto any     = false;

    if(content.has_value())
    {
        for(const auto& pair : content->As<FindDbData>())
        {
            if(in_sync)
            {
                if(!handle.GetInvoker(config, {{pair.first}}))
                {
                    unbuilt = true;
                    // This is not an logged as error because no error was detected.
                    // Find wasn't executed yet and invokers were not prepared.
                    LogFindDbItem(pair);
                    break;
                }

                any = true;
                continue;
            }
        }
    }
    else
    {
        MIOPEN_LOG_E("Error: no content.");
    }

    return !any || unbuilt;
}

template <class TDb>
void FindDbRecord_t<TDb>::CopyTo(std::vector<Solution>& to) const
{
    if(content.has_value())
    {
        const auto range = content->As<FindDbData>();
        std::transform(range.begin(), range.end(), std::back_inserter(to), [](const auto& pair) {
            return Solution{solver::Id{pair.first}, pair.second.time, pair.second.workspace};
        });
    }
    else
    {
        MIOPEN_LOG_E("Error: no content.");
    }
}

template <class TDb>
void FindDbRecord_t<TDb>::LogFindDbItem(const std::pair<std::string, FindDbData>& item) const
{
    if(content.has_value())
    {
        MIOPEN_LOG_I2("Kernel cache entry not found for solver: "
                      << item.first << " at network config: " << content->GetKey());

        for(const auto& pair2 : content->As<FindDbData>())
            MIOPEN_LOG_I2("Find-db record content: " << pair2.first << ':' << pair2.second);
    }
    else
    {
        MIOPEN_LOG_E("Error: no content.");
    }
}

template class FindDbRecord_t<FindDb>;
template class FindDbRecord_t<UserFindDb>;

} // namespace miopen
