/*
 *  IXDNSLookup.cpp
 *  Author: Benjamin Sergeant
 *  Copyright (c) 2018 Machine Zone, Inc. All rights reserved.
 */

//
// On Windows Universal Platform (uwp), gai_strerror defaults behavior is to returns wchar_t
// which is different from all other platforms. We want the non unicode version.
// See https://github.com/microsoft/vcpkg/pull/11030
// We could do this in IXNetSystem.cpp but so far we are only using gai_strerror in here.
//
#ifdef _UNICODE
#undef _UNICODE
#endif
#ifdef UNICODE
#undef UNICODE
#endif

#include "IXDNSLookup.h"

#include "IXNetSystem.h"
#include <chrono>
#include <string.h>
#include <thread>
#include <utility>

// mingw build quirks
#if defined(_WIN32) && defined(__GNUC__)
#ifndef AI_NUMERICSERV
#define AI_NUMERICSERV NI_NUMERICSERV
#endif
#ifndef AI_ADDRCONFIG
#define AI_ADDRCONFIG LUP_ADDRCONFIG
#endif
#endif

#ifdef __APPLE__
#ifndef AI_NUMERICSERV
#define AI_NUMERICSERV 0
#endif
#endif

namespace ix
{
    const int64_t DNSLookup::kDefaultWait = 1; // ms

    DNSLookup::DNSLookup(const std::string& hostname, int port, int64_t wait)
        : _hostname(hostname)
        , _port(port)
        , _wait(wait)
        , _res(nullptr)
        , _done(false)
    {
        ;
    }

    DNSLookup::AddrInfoPtr DNSLookup::getAddrInfo(const std::string& hostname,
                                            int port,
                                            std::string& errMsg)
    {
        struct addrinfo hints;
        memset(&hints, 0, sizeof(hints));
        hints.ai_flags = AI_ADDRCONFIG | AI_NUMERICSERV;
        hints.ai_family = AF_UNSPEC;
        hints.ai_socktype = SOCK_STREAM;

        std::string sport = std::to_string(port);

        struct addrinfo* res;
        int getaddrinfo_result = getaddrinfo(hostname.c_str(), sport.c_str(), &hints, &res);
        if (getaddrinfo_result)
        {
            errMsg = gai_strerror(getaddrinfo_result);
            res = nullptr;
        }
        return AddrInfoPtr{ res, freeaddrinfo };
    }

    DNSLookup::AddrInfoPtr DNSLookup::resolve(std::string& errMsg,
                                        const CancellationRequest& isCancellationRequested,
                                        bool cancellable)
    {
        return cancellable ? resolveCancellable(errMsg, isCancellationRequested)
                           : resolveUnCancellable(errMsg, isCancellationRequested);
    }

    DNSLookup::AddrInfoPtr DNSLookup::resolveUnCancellable(
        std::string& errMsg, const CancellationRequest& isCancellationRequested)
    {
        errMsg = "no error";

        // Maybe a cancellation request got in before the background thread terminated ?
        if (isCancellationRequested())
        {
            errMsg = "cancellation requested";
            return nullptr;
        }

        return getAddrInfo(_hostname, _port, errMsg);
    }

    DNSLookup::AddrInfoPtr DNSLookup::resolveCancellable(
        std::string& errMsg, const CancellationRequest& isCancellationRequested)
    {
        errMsg = "no error";

        // Can only be called once, otherwise we would have to manage a pool
        // of background thread which is overkill for our usage.
        if (_done)
        {
            return nullptr; // programming error, create a second DNSLookup instance
                            // if you need a second lookup.
        }

        //
        // Good resource on thread forced termination
        // https://www.bo-yang.net/2017/11/19/cpp-kill-detached-thread
        //
        auto ptr = shared_from_this();
        std::weak_ptr<DNSLookup> self(ptr);

        int port = _port;
        std::string hostname(_hostname);

        // We make the background thread doing the work a shared pointer
        // instead of a member variable, because it can keep running when
        // this object goes out of scope, in case of cancellation
        auto t = std::make_shared<std::thread>(&DNSLookup::run, this, self, hostname, port);
        t->detach();

        while (!_done)
        {
            // Wait for 1 milliseconds, to see if the bg thread has terminated.
            // We do not use a condition variable to wait, as destroying this one
            // if the bg thread is alive can cause undefined behavior.
            std::this_thread::sleep_for(std::chrono::milliseconds(_wait));

            // Were we cancelled ?
            if (isCancellationRequested())
            {
                errMsg = "cancellation requested";
                return nullptr;
            }
        }

        // Maybe a cancellation request got in before the bg terminated ?
        if (isCancellationRequested())
        {
            errMsg = "cancellation requested";
            return nullptr;
        }

        errMsg = getErrMsg();
        return getRes();
    }

    void DNSLookup::run(std::weak_ptr<DNSLookup> self,
                        std::string hostname,
                        int port) // thread runner
    {
        // We don't want to read or write into members variables of an object that could be
        // gone, so we use temporary variables (res) or we pass in by copy everything that
        // getAddrInfo needs to work.
        std::string errMsg;
        auto res = getAddrInfo(hostname, port, errMsg);

        if (auto lock = self.lock())
        {
            // Copy result into the member variables
            setRes(res);
            setErrMsg(errMsg);

            _done = true;
        }
    }

    void DNSLookup::setErrMsg(const std::string& errMsg)
    {
        std::lock_guard<std::mutex> lock(_errMsgMutex);
        _errMsg = errMsg;
    }

    const std::string& DNSLookup::getErrMsg()
    {
        std::lock_guard<std::mutex> lock(_errMsgMutex);
        return _errMsg;
    }

    void DNSLookup::setRes(DNSLookup::AddrInfoPtr addr)
    {
        std::lock_guard<std::mutex> lock(_resMutex);
        _res = std::move(addr);
    }

    DNSLookup::AddrInfoPtr DNSLookup::getRes()
    {
        std::lock_guard<std::mutex> lock(_resMutex);
        return _res;
    }
} // namespace ix
