#pragma once

#include <functional>
#include <source_location>

#include "Array.h"
#include "Console.h"
#include "Text.h"

namespace Framework
{
    class File;

    namespace Logging
    {
        /**
         * logs a message with level LogLevel::trace
         *
         * \param location the location of the call
         * \return a stream to write the message to
         */
        DLLEXPORT FlushingOStream trace(
            std::source_location location = std::source_location::current());

        /**
         * logs a message with level LogLevel::debug
         *
         * \param location the location of the call
         * \return a stream to write the message to
         */
        DLLEXPORT FlushingOStream debug(
            std::source_location location = std::source_location::current());

        /**
         * logs a message with level LogLevel::info
         *
         * \param location the location of the call
         * \return a stream to write the message to
         */
        DLLEXPORT FlushingOStream info(
            std::source_location location = std::source_location::current());

        /**
         * logs a message with level LogLevel::warning
         *
         * \param location the location of the call
         * \return a stream to write the message to
         */
        DLLEXPORT FlushingOStream warning(
            std::source_location location = std::source_location::current());

        /**
         * logs a message with level LogLevel::error
         *
         * \param location the location of the call
         * \return a stream to write the message to
         */
        DLLEXPORT FlushingOStream error(
            std::source_location location = std::source_location::current());

        /**
         * provides access to the logging handler
         *
         * \return a reference to the logging handler without increasing the
         * reference count
         */
        class LoggingHandler;
        DLLEXPORT LoggingHandler* zLoggingHandler();

        enum class LogLevel
        {
            Trace,
            Debug,
            Info,
            Warning,
            Error
        };

        /**
         * Formats a log message before it is written to a LoggingChannel.
         */
        class LoggingFormat : public Framework::ReferenceCounter
        {
        public:
            DLLEXPORT LoggingFormat();
            DLLEXPORT virtual ~LoggingFormat();

            /**
             * formats a log message.
             *
             * \param msg the message to format
             * \param level the used log level
             * \param location the location of the call
             * \return the formatted message
             */
            virtual Framework::Text formatMessage(const Framework::Text& msg,
                LogLevel level,
                const std::source_location& location) const
                = 0;
        };

        /**
         * Builds a custom LoggingFormat.
         */
        class LoggingFormatBuilder
        {
        private:
            std::function<LoggingFormat*(LoggingFormat*)> formatTransformer;

            DLLEXPORT LoggingFormatBuilder(
                std::function<LoggingFormat*(LoggingFormat*)>
                    formatTransformer);

        public:
            /**
             * creates a new format builder without any formatting.
             *
             */
            DLLEXPORT LoggingFormatBuilder();

            /**
             * adds a date to the format.
             *
             * \param format the format of the date y for year, m for month, d
             * for day, h for hour, i for minute, s for second example: "y-m-d
             * h:i:s"
             * \param postBlank true if a blank should be inserted after this
             * value
             *
             * \return the next builder
             */
            DLLEXPORT LoggingFormatBuilder datetime(
                Framework::Text format, bool postBlank = true) const;

            /**
             * adds the name of the file where the log message comes from.
             *
             * \param postBlank true if a blank should be inserted after this
             * value
             *
             * \return the next builder
             */
            DLLEXPORT LoggingFormatBuilder fileName(
                bool postBlank = true) const;

            /**
             * adds the line of the file where the log message comes from.
             *
             * \param postBlank true if a blank should be inserted after this
             * value
             *
             * \return the next builder
             */
            DLLEXPORT LoggingFormatBuilder fileLine(
                bool postBlank = true) const;

            /**
             * adds the name of the function where the log message comes from.
             *
             * \param postBlank true if a blank should be inserted after this
             * value
             *
             * \return the next builder
             */
            DLLEXPORT LoggingFormatBuilder functionName(
                bool postBlank = true) const;

            /**
             * adds the log level of the message.
             *
             * \param postBlank true if a blank should be inserted after this
             * value
             *
             * \return the next builder
             */
            DLLEXPORT LoggingFormatBuilder level(bool postBlank = true) const;

            /**
             * sets the color of the mesage after this call
             *
             * \param color the color
             * \return the next builder
             */
            DLLEXPORT LoggingFormatBuilder color(Color color) const;

            /**
             * sets the color of the mesage after this call if a specific log
             * level is used
             *
             * \param level the log level
             * \param color the color
             * \return the next builder
             */
            DLLEXPORT LoggingFormatBuilder color(
                LogLevel level, Color color) const;

            /**
             * sets the background color of the mesage after this call
             *
             * \param color the color
             * \return the next builder
             */
            DLLEXPORT LoggingFormatBuilder backgroundColor(Color color) const;

            /**
             * sets the background color of the mesage after this call if a
             * specific log level is used
             *
             * \param level the log level
             * \param color the color
             * \return the next builder
             */
            DLLEXPORT LoggingFormatBuilder backgroundColor(
                LogLevel level, Color color) const;

            /**
             * adds a custom text to the log message.
             *
             * \param text the text to add
             * \return the next builder
             */
            DLLEXPORT LoggingFormatBuilder text(Framework::Text text) const;

            /**
             * creates the logging format.
             *
             * \return the logging format
             */
            DLLEXPORT LoggingFormat* build() const;
        };

        /**
         * writes log messages to a specific output.
         */
        class LoggingChannel : public ReferenceCounter
        {
        private:
            LoggingFormat* format;

        public:
            DLLEXPORT LoggingChannel();
            DLLEXPORT virtual ~LoggingChannel();

            /**
             * sets the format of the log messages
             *
             * \param format the format
             */
            DLLEXPORT void setFormat(LoggingFormat* format);

            /**
             * writes a log message to this channel.
             *
             * \param msg the message
             * \param level the log level
             * \param location the location of the call
             */
            DLLEXPORT void writeMessage(const Text& msg,
                LogLevel level,
                const std::source_location& location) const;

        protected:
            /**
             * will be called to write the message to the channel.
             *
             * \param msg the message
             */
            virtual void writeMessage(const Text& message) const = 0;
        };

        /**
         * logs messages to the std::cout.
         */
        class CoutLoggingChannel : public LoggingChannel
        {
        public:
            /**
             * creates a new cout logging channel.
             *
             */
            DLLEXPORT CoutLoggingChannel();

        protected:
            /**
             * writes a message to the std::cout.
             *
             * \param msg the message
             */
            DLLEXPORT virtual void writeMessage(const Text& msg) const override;
        };

        /**
         * logs messages to OutputDebugString.
         */
        class OutputDebugStringLoggingChannel : public LoggingChannel
        {
        public:
            /**
             * creates a new cout logging channel.
             *
             */
            DLLEXPORT OutputDebugStringLoggingChannel();

        protected:
            /**
             * writes a message to OutputDebugString.
             *
             * \param msg the message
             */
            DLLEXPORT virtual void writeMessage(const Text& msg) const override;
        };

        /**
         * logs messages to the std::cout.
         */
        class ConsoleHandlerLoggingChannel : public LoggingChannel
        {
        private:
            ConsoleHandler* consolehandler;

        public:
            /**
             * creates a new console logging channel.
             *
             */
            DLLEXPORT ConsoleHandlerLoggingChannel(
                ConsoleHandler* consolehandler);
            DLLEXPORT ~ConsoleHandlerLoggingChannel();

        protected:
            /**
             * writes a message to the given ConsoleHandler.
             *
             * \param msg the message
             */
            DLLEXPORT virtual void writeMessage(const Text& msg) const override;
        };

        /**
         * logs messages to a file.
         */
        class FileLoggingChannel : public LoggingChannel
        {
        private:
            File* file;

        public:
            /**
             * creates a new file logging channel.
             *
             * \param filePath the path to the file. If the file already exists
             * the messages will be appended to the file.
             */
            DLLEXPORT FileLoggingChannel(Text filePath);
            DLLEXPORT FileLoggingChannel(File* file);
            DLLEXPORT ~FileLoggingChannel();

        protected:
            /**
             * writes a message to the file.
             *
             * \param msg the message
             */
            DLLEXPORT virtual void writeMessage(const Text& msg) const override;
        };

        /**
         * used to configure channels where logged messages should be written
         * to.
         */
        class LoggingHandler : public ReferenceCounter
        {
        private:
            RCArray<LoggingChannel>** channels;

        public:
            DLLEXPORT LoggingHandler();
            DLLEXPORT ~LoggingHandler();

            /**
             * adds a channel for a specific log level.
             *
             * \param level the log level
             * \param channel the channel to add
             */
            DLLEXPORT void addChannel(LogLevel level, LoggingChannel* channel);

            /**
             * removes a channel for a specific log level.
             *
             * \param level the log level
             * \param channel the channel to remove without increasing the
             * reference count
             */
            DLLEXPORT void removeLoggingChannel(
                LogLevel level, LoggingChannel* zChannel);

            /**
             * adds a channel to all log levels.
             *
             * \param channel the channel to add
             */
            DLLEXPORT void addChannel(LoggingChannel* channel);

            /**
             * removes a channel from all log levels.
             *
             * \param channel the channel to remove without increasing the
             * reference count
             */
            DLLEXPORT void removeLoggingChannel(LoggingChannel* zChannel);

            /**
             * logs a message to all configured channels of a specific log
             * level.
             *
             * \param level the log level
             * \param message the message
             */
            DLLEXPORT void log(LogLevel level,
                std::source_location& location,
                const Text message) const;
        };
    } // namespace Logging
} // namespace Framework