/*
 * Copyright 2011 Nate Koenig
 *
 * 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.
 *
 */

#ifndef SDF_CONSOLE_HH_
#define SDF_CONSOLE_HH_

#include <fstream>
#include <iostream>
#include <memory>
#include <string>

#include <sdf/config.hh>
#include "sdf/system_util.hh"

#ifdef _WIN32
// Disable warning C4251 which is triggered by
// std::unique_ptr
#pragma warning(push)
#pragma warning(disable: 4251)
#endif

namespace sdf
{
  // Inline bracket to help doxygen filtering.
  inline namespace SDF_VERSION_NAMESPACE {
  //

  /// \addtogroup sdf SDF
  /// \{

  /// \brief Output a debug message
  #define sdfdbg (sdf::Console::Instance()->Log("Dbg", __FILE__, __LINE__))

  /// \brief Output a message
  #define sdfmsg (sdf::Console::Instance()->ColorMsg("Msg", \
                                                     __FILE__, __LINE__, 32))

  /// \brief Output a warning message
  #define sdfwarn (sdf::Console::Instance()->ColorMsg("Warning", \
                                                      __FILE__, __LINE__, 33))

  /// \brief Output an error message
  #define sdferr (sdf::Console::Instance()->ColorMsg("Error", \
                                                     __FILE__, __LINE__, 31))

  class ConsolePrivate;
  class Console;

  /// \def ConsolePtr
  /// \brief Shared pointer to a Console Element
  typedef std::shared_ptr<Console> ConsolePtr;

  /// \brief Message, error, warning, and logging functionality
  class SDFORMAT_VISIBLE Console
  {
    /// \brief An ostream-like class that we'll use for logging.
    public: class SDFORMAT_VISIBLE ConsoleStream
    {
      /// \brief Constructor.
      /// \param[in] _stream Pointer to an output stream operator. Can be
      /// NULL/nullptr.
      public: explicit ConsoleStream(std::ostream *_stream) :
              stream(_stream) {}

      /// \brief Redirect whatever is passed in to both our ostream
      ///        (if non-NULL) and the log file (if open).
      /// \param[in] _rhs Content to be logged.
      /// \return Reference to myself.
      public: template <class T>
        ConsoleStream &operator<<(const T &_rhs);

      /// \brief Print a prefix to both terminal and log file.
      /// \param[in] _lbl Text label
      /// \param[in] _file File containing the error
      /// \param[in] _line Line containing the error
      /// \param[in] _color Color to make the label.  Used only on terminal.
      public: void Prefix(const std::string &_lbl,
                          const std::string &_file,
                          unsigned int _line, int _color);

      /// \brief Set the stream object.
      /// \param[in] _stream Pointer to an output stream. This can be
      /// useful for redirecting the output, for example, to a std::stringstream
      /// for testing.
      public: void SetStream(std::ostream *_stream);

      /// \brief Get the current stream object.
      /// \return Pointer to current stream object.
      public: std::ostream *GetStream();

      /// \brief The ostream to log to; can be NULL/nullptr.
      private: std::ostream *stream;
    };

    /// \brief Default constructor
    private: Console();

    /// \brief Destructor
    public: virtual ~Console();

    /// \brief Return an instance to this class.
    public: static ConsolePtr Instance();

    /// \brief Clear out the current console to make room for a new one.
    public: static void Clear();

    /// \brief Set quiet output
    /// \param[in] q True to prevent warning
    public: void SetQuiet(bool _q);

    /// \brief Use this to output a colored message to the terminal
    /// \param[in] _lbl Text label
    /// \param[in] _file File containing the error
    /// \param[in] _line Line containing the error
    /// \param[in] _color Color to make the label
    /// \return Reference to an output stream
    public: ConsoleStream &ColorMsg(const std::string &lbl,
                                    const std::string &file,
                                    unsigned int line, int color);

    /// \brief Use this to output a message to a log file at
    /// `$HOME/.sdformat/sdformat.log`.
    /// To disable this log file, define the following symbol when
    /// compiling: SDFORMAT_DISABLE_CONSOLE_LOGFILE
    /// \return Reference to output stream
    public: ConsoleStream &Log(const std::string &lbl,
                               const std::string &file,
                               unsigned int line);

    /// \brief Get the current message stream object. This can be
    /// useful for redirecting the output, for example, to a std::stringstream
    /// for testing.
    /// \return Mutable reference to current message stream object.
    public: ConsoleStream &GetMsgStream();

    /// \brief Get the current log stream object. This can be
    /// useful for redirecting the output, for example, to a std::stringstream
    /// for testing.
    /// \return Mutable reference to current log stream object.
    public: ConsoleStream &GetLogStream();

    /// \internal
    /// \brief Pointer to private data.
    private: std::unique_ptr<ConsolePrivate> dataPtr;
  };

  /// \internal
  /// \brief Private data for Console
  class ConsolePrivate
  {
    /// \brief Constructor
    public: ConsolePrivate() : msgStream(&std::cerr), logStream(nullptr) {}

    /// \brief message stream
    public: Console::ConsoleStream msgStream;

    /// \brief log stream
    public: Console::ConsoleStream logStream;

    /// \brief logfile stream
    public: std::ofstream logFileStream;
  };

  ///////////////////////////////////////////////
  template <class T>
  Console::ConsoleStream &Console::ConsoleStream::operator<<(const T &_rhs)
  {
    if (this->stream)
    {
      *this->stream << _rhs;
    }

    if (Console::Instance()->dataPtr->logFileStream.is_open())
    {
      Console::Instance()->dataPtr->logFileStream << _rhs;
      Console::Instance()->dataPtr->logFileStream.flush();
    }

    return *this;
  }
  }

  /// \}
}

#ifdef _WIN32
#pragma warning(pop)
#endif

#endif
