//
// MIT License
// Copyright (c) 2020 Jonathan R. Madsen
// 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
// 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.
//
// Global utility functions
//

#pragma once

#include "PTL/Types.hh"

#include <chrono>
#include <cstdlib>
#include <functional>
#include <iomanip>
#include <iostream>
#include <map>
#include <mutex>
#include <set>
#include <sstream>
#include <string>
#include <tuple>

namespace PTL
{
//--------------------------------------------------------------------------------------//
// use this function to get rid of "unused parameter" warnings
//
template <typename... Args>
void
ConsumeParameters(Args...)
{}

//--------------------------------------------------------------------------------------//
// a non-string environment option with a string identifier
template <typename Tp>
using EnvChoice = std::tuple<Tp, std::string, std::string>;

//--------------------------------------------------------------------------------------//
// list of environment choices with non-string and string identifiers
template <typename Tp>
using EnvChoiceList = std::set<EnvChoice<Tp>>;

//--------------------------------------------------------------------------------------//

class EnvSettings
{
public:
    typedef std::mutex                        mutex_t;
    typedef std::string                       string_t;
    typedef std::multimap<string_t, string_t> env_map_t;
    typedef std::pair<string_t, string_t>     env_pair_t;

public:
    static EnvSettings* GetInstance()
    {
        static EnvSettings* _instance = new EnvSettings();
        return _instance;
    }

public:
    template <typename Tp>
    void insert(const std::string& env_id, Tp val)
    {
        std::stringstream ss;
        ss << std::boolalpha << val;
        m_mutex.lock();
        if(m_env.find(env_id) != m_env.end())
        {
            for(const auto& itr : m_env)
                if(itr.first == env_id && itr.second == ss.str())
                {
                    m_mutex.unlock();
                    return;
                }
        }
        m_env.insert(env_pair_t(env_id, ss.str()));
        m_mutex.unlock();
    }

    template <typename Tp>
    void insert(const std::string& env_id, EnvChoice<Tp> choice)
    {
        Tp&          val      = std::get<0>(choice);
        std::string& str_val  = std::get<1>(choice);
        std::string& descript = std::get<2>(choice);

        std::stringstream ss, ss_long;
        ss << std::boolalpha << val;
        ss_long << std::boolalpha << std::setw(8) << std::left << val << " # (\""
                << str_val << "\") " << descript;
        m_mutex.lock();
        if(m_env.find(env_id) != m_env.end())
        {
            for(const auto& itr : m_env)
                if(itr.first == env_id && itr.second == ss.str())
                {
                    m_mutex.unlock();
                    return;
                }
        }
        m_env.insert(env_pair_t(env_id, ss_long.str()));
        m_mutex.unlock();
    }

    const env_map_t& get() const { return m_env; }
    mutex_t&         mutex() const { return m_mutex; }

    friend std::ostream& operator<<(std::ostream& os, const EnvSettings& env)
    {
        std::stringstream filler;
        filler.fill('#');
        filler << std::setw(90) << "";
        std::stringstream ss;
        ss << filler.str() << "\n# Environment settings:\n";
        env.mutex().lock();
        for(const auto& itr : env.get())
        {
            ss << "# " << std::setw(35) << std::right << itr.first << "\t = \t"
               << std::left << itr.second << "\n";
        }
        env.mutex().unlock();
        ss << filler.str();
        os << ss.str() << std::endl;
        return os;
    }

private:
    env_map_t       m_env;
    mutable mutex_t m_mutex;
};

//--------------------------------------------------------------------------------------//
//  use this function to get an environment variable setting +
//  a default if not defined, e.g.
//      int num_threads =
//          GetEnv<int>("FORCENUMBEROFTHREADS",
//                          std::thread::hardware_concurrency());
//
template <typename Tp>
Tp
GetEnv(const std::string& env_id, Tp _default = Tp())
{
    char* env_var = std::getenv(env_id.c_str());
    if(env_var)
    {
        std::string        str_var = std::string(env_var);
        std::istringstream iss(str_var);
        Tp                 var = Tp();
        iss >> var;
        // record value defined by environment
        EnvSettings::GetInstance()->insert<Tp>(env_id, var);
        return var;
    }
    // record default value
    EnvSettings::GetInstance()->insert<Tp>(env_id, _default);

    // return default if not specified in environment
    return _default;
}

//--------------------------------------------------------------------------------------//
//  overload for boolean
//
template <>
inline bool
GetEnv(const std::string& env_id, bool _default)
{
    char* env_var = std::getenv(env_id.c_str());
    if(env_var)
    {
        std::string var = std::string(env_var);
        bool        val = true;
        if(var.find_first_not_of("0123456789") == std::string::npos)
            val = (bool) atoi(var.c_str());
        else
        {
            for(auto& itr : var)
                itr = tolower(itr);
            if(var == "off" || var == "false")
                val = false;
        }
        // record value defined by environment
        EnvSettings::GetInstance()->insert<bool>(env_id, val);
        return val;
    }
    // record default value
    EnvSettings::GetInstance()->insert<bool>(env_id, false);

    // return default if not specified in environment
    return _default;
}

//--------------------------------------------------------------------------------------//
//  overload for GetEnv + message when set
//
template <typename Tp>
Tp
GetEnv(const std::string& env_id, Tp _default, const std::string& msg)
{
    char* env_var = std::getenv(env_id.c_str());
    if(env_var)
    {
        std::string        str_var = std::string(env_var);
        std::istringstream iss(str_var);
        Tp                 var = Tp();
        iss >> var;
        std::cout << "Environment variable \"" << env_id << "\" enabled with "
                  << "value == " << var << ". " << msg << std::endl;
        // record value defined by environment
        EnvSettings::GetInstance()->insert<Tp>(env_id, var);
        return var;
    }
    // record default value
    EnvSettings::GetInstance()->insert<Tp>(env_id, _default);

    // return default if not specified in environment
    return _default;
}

//--------------------------------------------------------------------------------------//
//  use this function to get an environment variable setting from set of choices
//
//      EnvChoiceList<int> choices =
//              { EnvChoice<int>(NN,     "NN",     "nearest neighbor interpolation"),
//                EnvChoice<int>(LINEAR, "LINEAR", "bilinear interpolation"),
//                EnvChoice<int>(CUBIC,  "CUBIC",  "bicubic interpolation") };
//
//      int eInterp = GetEnv<int>("INTERPOLATION", choices, CUBIC);
//
template <typename Tp>
Tp
GetEnv(const std::string& env_id, const EnvChoiceList<Tp>& _choices, Tp _default)
{
    auto asupper = [](std::string var) {
        for(auto& itr : var)
            itr = toupper(itr);
        return var;
    };

    char* env_var = std::getenv(env_id.c_str());
    if(env_var)
    {
        std::string str_var = std::string(env_var);
        std::string upp_var = asupper(str_var);
        Tp          var     = Tp();
        // check to see if string matches a choice
        for(const auto& itr : _choices)
        {
            if(asupper(std::get<1>(itr)) == upp_var)
            {
                // record value defined by environment
                EnvSettings::GetInstance()->insert(env_id, itr);
                return std::get<0>(itr);
            }
        }
        std::istringstream iss(str_var);
        iss >> var;
        // check to see if string matches a choice
        for(const auto& itr : _choices)
        {
            if(var == std::get<0>(itr))
            {
                // record value defined by environment
                EnvSettings::GetInstance()->insert(env_id, itr);
                return var;
            }
        }
        // the value set in env did not match any choices
        std::stringstream ss;
        ss << "\n### Environment setting error @ " << __FUNCTION__ << " (line "
           << __LINE__ << ")! Invalid selection for \"" << env_id
           << "\". Valid choices are:\n";
        for(const auto& itr : _choices)
            ss << "\t\"" << std::get<0>(itr) << "\" or \"" << std::get<1>(itr) << "\" ("
               << std::get<2>(itr) << ")\n";
        std::cerr << ss.str() << std::endl;
        abort();
    }

    std::string _name = "???";
    std::string _desc = "description not provided";
    for(const auto& itr : _choices)
        if(std::get<0>(itr) == _default)
        {
            _name = std::get<1>(itr);
            _desc = std::get<2>(itr);
            break;
        }

    // record default value
    EnvSettings::GetInstance()->insert(env_id, EnvChoice<Tp>(_default, _name, _desc));

    // return default if not specified in environment
    return _default;
}

//--------------------------------------------------------------------------------------//

template <typename Tp>
Tp
GetChoice(const EnvChoiceList<Tp>& _choices, const std::string& str_var)
{
    auto asupper = [](std::string var) {
        for(auto& itr : var)
            itr = toupper(itr);
        return var;
    };

    std::string upp_var = asupper(str_var);
    Tp          var     = Tp();
    // check to see if string matches a choice
    for(const auto& itr : _choices)
    {
        if(asupper(std::get<1>(itr)) == upp_var)
        {
            // record value defined by environment
            return std::get<0>(itr);
        }
    }
    std::istringstream iss(str_var);
    iss >> var;
    // check to see if string matches a choice
    for(const auto& itr : _choices)
    {
        if(var == std::get<0>(itr))
        {
            // record value defined by environment
            return var;
        }
    }
    // the value set in env did not match any choices
    std::stringstream ss;
    ss << "\n### Environment setting error @ " << __FUNCTION__ << " (line " << __LINE__
       << ")! Invalid selection \"" << str_var << "\". Valid choices are:\n";
    for(const auto& itr : _choices)
        ss << "\t\"" << std::get<0>(itr) << "\" or \"" << std::get<1>(itr) << "\" ("
           << std::get<2>(itr) << ")\n";
    std::cerr << ss.str() << std::endl;
    abort();
}

//--------------------------------------------------------------------------------------//

inline void
PrintEnv(std::ostream& os = std::cout)
{
    os << (*EnvSettings::GetInstance());
}

//--------------------------------------------------------------------------------------//

struct ScopeDestructor
{
    template <typename FuncT>
    ScopeDestructor(FuncT&& _func)
    : m_functor(std::forward<FuncT>(_func))
    {}

    // delete copy operations
    ScopeDestructor(const ScopeDestructor&) = delete;
    ScopeDestructor& operator=(const ScopeDestructor&) = delete;

    // allow move operations
    ScopeDestructor(ScopeDestructor&& rhs) noexcept
    : m_functor(std::move(rhs.m_functor))
    {
        rhs.m_functor = []() {};
    }

    ScopeDestructor& operator=(ScopeDestructor&& rhs) noexcept
    {
        if(this != &rhs)
        {
            m_functor     = std::move(rhs.m_functor);
            rhs.m_functor = []() {};
        }
        return *this;
    }

    ~ScopeDestructor() { m_functor(); }

private:
    std::function<void()> m_functor = []() {};
};

//--------------------------------------------------------------------------------------//

}  // namespace PTL
