// Copyright 2019 Proyectos y Sistemas de Mantenimiento SL (eProsima).
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <chrono>
#include <iomanip>
#include <iostream>
#include <memory>
#include <mutex>

#include <fastrtps/utils/DBQueue.h>

#include <fastdds/dds/log/Log.hpp>
#include <fastdds/dds/log/OStreamConsumer.hpp>
#include <fastdds/dds/log/StdoutConsumer.hpp>
#include <fastdds/dds/log/StdoutErrConsumer.hpp>
#include <fastdds/dds/log/Colors.hpp>
#include <utils/SystemInfo.hpp>
#include <utils/thread.hpp>
#include <utils/threading.hpp>

namespace eprosima {
namespace fastdds {
namespace dds {
namespace detail {

struct LogResources
{
    LogResources()
        : logging_(false)
        , work_(false)
        , current_loop_(0)
        , filenames_(false)
        , functions_(true)
        , verbosity_(Log::Error)
    {
#if STDOUTERR_LOG_CONSUMER
        consumers_.emplace_back(new StdoutErrConsumer);
#else
        consumers_.emplace_back(new StdoutConsumer);
#endif // STDOUTERR_LOG_CONSUMER
    }

    ~LogResources()
    {
        Flush();
        KillThread();
    }

    /**
     * Registers an user defined consumer to route log output.
     * There is a default stdout consumer active as default.
     * @param consumer r-value to a consumer unique_ptr. It will be invalidated after the call.
     */
    void RegisterConsumer(
            std::unique_ptr<LogConsumer>&& consumer)
    {
        std::unique_lock<std::mutex> guard(config_mutex_);
        consumers_.emplace_back(std::move(consumer));
    }

    //! Removes all registered consumers_, including the default stdout.
    void ClearConsumers()
    {
        Flush();

        std::lock_guard<std::mutex> guard(config_mutex_);
        consumers_.clear();
    }

    //! Enables the reporting of filenames_ in log entries. Disabled by default.
    void ReportFilenames(
            bool report)
    {
        std::lock_guard<std::mutex> configGuard(config_mutex_);
        filenames_ = report;
    }

    //! Enables the reporting of function names in log entries. Enabled by default when supported.
    void ReportFunctions(
            bool report)
    {
        std::lock_guard<std::mutex> configGuard(config_mutex_);
        functions_ = report;
    }

    //! Sets the verbosity_ level, allowing for messages equal or under that priority to be logged.
    void SetVerbosity(
            Log::Kind kind)
    {
        verbosity_ = kind;
    }

    //! Returns the current verbosity_ level.
    Log::Kind GetVerbosity()
    {
        return verbosity_;
    }

    //! Sets a filter that will pattern-match against log categories, dropping any unmatched categories.
    void SetCategoryFilter(
            const std::regex& filter)
    {
        std::unique_lock<std::mutex> configGuard(config_mutex_);
        category_filter_.reset(new std::regex(filter));
    }

    //! Sets a filter that will pattern-match against filenames_, dropping any unmatched categories.
    void SetFilenameFilter(
            const std::regex& filter)
    {
        std::unique_lock<std::mutex> configGuard(config_mutex_);
        filename_filter_.reset(new std::regex(filter));
    }

    //! Sets a filter that will pattern-match against the provided error string, dropping any unmatched categories.
    void SetErrorStringFilter(
            const std::regex& filter)
    {
        std::unique_lock<std::mutex> configGuard(config_mutex_);
        error_string_filter_.reset(new std::regex(filter));
    }

    //! Sets thread configuration for the logging thread.
    void SetThreadConfig(
            const rtps::ThreadSettings& config)
    {
        std::lock_guard<std::mutex> guard(cv_mutex_);
        thread_settings_ = config;
    }

    //! Returns the logging_ engine to configuration defaults.
    void Reset()
    {
        rtps::ThreadSettings thr_config{};
        SetThreadConfig(thr_config);

        std::lock_guard<std::mutex> configGuard(config_mutex_);
        category_filter_.reset();
        filename_filter_.reset();
        error_string_filter_.reset();
        filenames_ = false;
        functions_ = true;
        verbosity_ = Log::Error;
        consumers_.clear();

#if STDOUTERR_LOG_CONSUMER
        consumers_.emplace_back(new StdoutErrConsumer);
#else
        consumers_.emplace_back(new StdoutConsumer);
#endif // if STDOUTERR_LOG_CONSUMER
    }

    //! Waits until all info logged up to the call time is consumed
    void Flush()
    {
        std::unique_lock<std::mutex> guard(cv_mutex_);

        if (!logging_ && !logging_thread_.joinable())
        {
            // already killed
            return;
        }

        /*   Flush() two steps strategy:

             I must assure Log::Run swaps the queues because only swapping the queues the background content
             will be consumed (first Run() loop).

             Then, I must assure the new front queue content is consumed (second Run() loop).
         */

        int last_loop = -1;

        for (int i = 0; i < 2; ++i)
        {
            cv_.wait(guard,
                    [&]()
                    {
                        /* I must avoid:
                         + the two calls be processed without an intermediate Run() loop (by using last_loop sequence number)
                         + deadlock by absence of Run() loop activity (by using BothEmpty() call)
                         */
                        return !logging_ ||
                        (logs_.Empty() &&
                        (last_loop != current_loop_ || logs_.BothEmpty()));
                    });

            last_loop = current_loop_;

        }
    }

    /**
     * Not recommended to call this method directly! Use the following macros:
     *  * EPROSIMA_LOG_INFO(cat, msg);
     *  * EPROSIMA_LOG_WARNING(cat, msg);
     *  * EPROSIMA_LOG_ERROR(cat, msg);
     *
     * @todo this method takes 2 mutexes (same mutex) internally.
     * This is a very high sensible point of the code and it should be refactored to be as efficient as possible.
     */
    void QueueLog(
            const std::string& message,
            const Log::Context& context,
            Log::Kind kind)
    {
        StartThread();

        std::string timestamp = SystemInfo::get_timestamp();
        logs_.Push(Log::Entry{ message, context, kind, timestamp });
        {
            std::unique_lock<std::mutex> guard(cv_mutex_);
            work_ = true;
            // pessimization
            cv_.notify_all();
            // wait till the thread is initialized
            cv_.wait(guard, [&]
                    {
                        return current_loop_;
                    });
        }
    }

    //! Stops the logging_ thread. It will re-launch on the next call to QueueLog.
    void KillThread()
    {
        {
            std::unique_lock<std::mutex> guard(cv_mutex_);
            logging_ = false;
            work_ = false;
        }

        if (logging_thread_.joinable())
        {
            cv_.notify_all();
            if (!logging_thread_.is_calling_thread())
            {
                logging_thread_.join();
            }
        }
    }

private:

    void StartThread()
    {
        std::unique_lock<std::mutex> guard(cv_mutex_);
        if (!logging_ && !logging_thread_.joinable())
        {
            logging_ = true;
            auto thread_fn = [this]()
                    {
                        run();
                    };
            logging_thread_ = eprosima::create_thread(thread_fn, thread_settings_, "dds.log");
        }
    }

    void run()
    {
        std::unique_lock<std::mutex> guard(cv_mutex_);

        while (logging_)
        {
            cv_.wait(guard,
                    [&]()
                    {
                        return !logging_ || work_;
                    });

            work_ = false;

            guard.unlock();
            {
                logs_.Swap();
                while (!logs_.Empty())
                {
                    Log::Entry& entry = logs_.Front();
                    {
                        std::unique_lock<std::mutex> configGuard(config_mutex_);

                        if (preprocess(entry))
                        {
                            for (auto& consumer : consumers_)
                            {
                                consumer->Consume(entry);
                            }
                        }
                    }
                    // This Pop() is also a barrier for Log::Flush wait condition
                    logs_.Pop();
                }
            }
            guard.lock();

            // avoid overflow
            if (++current_loop_ > 10000)
            {
                current_loop_ = 0;
            }

            cv_.notify_all();
        }
    }

    bool preprocess(
            Log::Entry& entry)
    {
        if (category_filter_ && !regex_search(entry.context.category, *category_filter_))
        {
            return false;
        }
        if (filename_filter_ && !regex_search(entry.context.filename, *filename_filter_))
        {
            return false;
        }
        if (error_string_filter_ && !regex_search(entry.message, *error_string_filter_))
        {
            return false;
        }
        if (!filenames_)
        {
            entry.context.filename = nullptr;
        }
        if (!functions_)
        {
            entry.context.function = nullptr;
        }

        return true;
    }

    fastrtps::DBQueue<Log::Entry> logs_;
    std::vector<std::unique_ptr<LogConsumer>> consumers_;
    eprosima::thread logging_thread_;

    // Condition variable segment.
    std::condition_variable cv_;
    std::mutex cv_mutex_;
    bool logging_;
    bool work_;
    int current_loop_;

    // Context configuration.
    std::mutex config_mutex_;
    bool filenames_;
    bool functions_;
    std::unique_ptr<std::regex> category_filter_;
    std::unique_ptr<std::regex> filename_filter_;
    std::unique_ptr<std::regex> error_string_filter_;

    std::atomic<Log::Kind> verbosity_;
    rtps::ThreadSettings thread_settings_;
};

const std::shared_ptr<LogResources>& get_log_resources()
{
    static std::shared_ptr<LogResources> instance = std::make_shared<LogResources>();
    return instance;
}

}  // namespace detail

void Log::RegisterConsumer(
        std::unique_ptr<LogConsumer>&& consumer)
{
    detail::get_log_resources()->RegisterConsumer(std::move(consumer));
}

void Log::ClearConsumers()
{
    detail::get_log_resources()->ClearConsumers();
}

void Log::Reset()
{
    detail::get_log_resources()->Reset();
}

void Log::Flush()
{
    detail::get_log_resources()->Flush();
}

void Log::ReportFilenames(
        bool report)
{
    detail::get_log_resources()->ReportFilenames(report);
}

void Log::ReportFunctions(
        bool report)
{
    detail::get_log_resources()->ReportFunctions(report);
}

void Log::KillThread()
{
    detail::get_log_resources()->KillThread();
}

void Log::QueueLog(
        const std::string& message,
        const Log::Context& context,
        Log::Kind kind)
{
    detail::get_log_resources()->QueueLog(message, context, kind);
}

Log::Kind Log::GetVerbosity()
{
    return detail::get_log_resources()->GetVerbosity();
}

void Log::SetVerbosity(
        Log::Kind kind)
{
    detail::get_log_resources()->SetVerbosity(kind);
}

void Log::SetCategoryFilter(
        const std::regex& filter)
{
    detail::get_log_resources()->SetCategoryFilter(filter);
}

void Log::SetFilenameFilter(
        const std::regex& filter)
{
    detail::get_log_resources()->SetFilenameFilter(filter);
}

void Log::SetErrorStringFilter(
        const std::regex& filter)
{
    detail::get_log_resources()->SetErrorStringFilter(filter);
}

void Log::SetThreadConfig(
        const rtps::ThreadSettings& config)
{
    detail::get_log_resources()->SetThreadConfig(config);
}

void LogConsumer::print_timestamp(
        std::ostream& stream,
        const Log::Entry& entry,
        bool color) const
{
    std::string white = (color) ? C_B_WHITE : "";
    stream << white << entry.timestamp << " ";
}

void LogConsumer::print_header(
        std::ostream& stream,
        const Log::Entry& entry,
        bool color) const
{
    std::string c_b_color = (!color) ? "" :
            (entry.kind == Log::Kind::Error) ? C_B_RED :
            (entry.kind == Log::Kind::Warning) ? C_B_YELLOW :
            (entry.kind == Log::Kind::Info) ? C_B_GREEN : "";

    std::string white = (color) ? C_B_WHITE : "";

    stream << c_b_color << "[" << white << entry.context.category << c_b_color << " " << entry.kind << "] ";
}

void LogConsumer::print_context(
        std::ostream& stream,
        const Log::Entry& entry,
        bool color) const
{
    if (color)
    {
        stream << C_B_BLUE;
    }
    if (entry.context.filename)
    {
        stream << " (" << entry.context.filename;
        stream << ":" << entry.context.line << ")";
    }
    if (entry.context.function)
    {
        stream << " -> Function ";
        if (color)
        {
            stream << C_CYAN;
        }
        stream << entry.context.function;
    }
}

void LogConsumer::print_message(
        std::ostream& stream,
        const Log::Entry& entry,
        bool color) const
{
    std::string white = (color) ? C_WHITE : "";
    stream << white << entry.message;
}

void LogConsumer::print_new_line(
        std::ostream& stream,
        bool color) const
{
    std::string def = (color) ? C_DEF : "";
    stream << def << std::endl;
}

} //namespace dds
} //namespace fastdds
} //namespace eprosima
