
//              Copyright Catch2 Authors
// Distributed under the Boost Software License, Version 1.0.
//   (See accompanying file LICENSE.txt or copy at
//        https://www.boost.org/LICENSE_1_0.txt)

// SPDX-License-Identifier: BSL-1.0

#include <catch2/internal/catch_istream.hpp>
#include <catch2/internal/catch_enforce.hpp>
#include <catch2/internal/catch_debug_console.hpp>
#include <catch2/internal/catch_unique_ptr.hpp>
#include <catch2/internal/catch_stdstreams.hpp>

#include <cstdio>
#include <fstream>
#include <sstream>
#include <vector>

namespace Catch {

    Catch::IStream::~IStream() = default;

namespace Detail {
    namespace {
        template<typename WriterF, std::size_t bufferSize=256>
        class StreamBufImpl final : public std::streambuf {
            char data[bufferSize];
            WriterF m_writer;

        public:
            StreamBufImpl() {
                setp( data, data + sizeof(data) );
            }

            ~StreamBufImpl() noexcept override {
                StreamBufImpl::sync();
            }

        private:
            int overflow( int c ) override {
                sync();

                if( c != EOF ) {
                    if( pbase() == epptr() )
                        m_writer( std::string( 1, static_cast<char>( c ) ) );
                    else
                        sputc( static_cast<char>( c ) );
                }
                return 0;
            }

            int sync() override {
                if( pbase() != pptr() ) {
                    m_writer( std::string( pbase(), static_cast<std::string::size_type>( pptr() - pbase() ) ) );
                    setp( pbase(), epptr() );
                }
                return 0;
            }
        };

        ///////////////////////////////////////////////////////////////////////////

        struct OutputDebugWriter {

            void operator()( std::string const& str ) {
                if ( !str.empty() ) {
                    writeToDebugConsole( str );
                }
            }
        };

        ///////////////////////////////////////////////////////////////////////////

        class FileStream final : public IStream {
            std::ofstream m_ofs;
        public:
            FileStream( std::string const& filename ) {
                m_ofs.open( filename.c_str() );
                CATCH_ENFORCE( !m_ofs.fail(), "Unable to open file: '" << filename << '\'' );
                m_ofs << std::unitbuf;
            }
            ~FileStream() override = default;
        public: // IStream
            std::ostream& stream() override {
                return m_ofs;
            }
        };

        ///////////////////////////////////////////////////////////////////////////

        class CoutStream final : public IStream {
            std::ostream m_os;
        public:
            // Store the streambuf from cout up-front because
            // cout may get redirected when running tests
            CoutStream() : m_os( Catch::cout().rdbuf() ) {}
            ~CoutStream() override = default;

        public: // IStream
            std::ostream& stream() override { return m_os; }
            bool isConsole() const override { return true; }
        };

        class CerrStream : public IStream {
            std::ostream m_os;

        public:
            // Store the streambuf from cerr up-front because
            // cout may get redirected when running tests
            CerrStream(): m_os( Catch::cerr().rdbuf() ) {}
            ~CerrStream() override = default;

        public: // IStream
            std::ostream& stream() override { return m_os; }
            bool isConsole() const override { return true; }
        };

        ///////////////////////////////////////////////////////////////////////////

        class DebugOutStream final : public IStream {
            Detail::unique_ptr<StreamBufImpl<OutputDebugWriter>> m_streamBuf;
            std::ostream m_os;
        public:
            DebugOutStream()
            :   m_streamBuf( Detail::make_unique<StreamBufImpl<OutputDebugWriter>>() ),
                m_os( m_streamBuf.get() )
            {}

            ~DebugOutStream() override = default;

        public: // IStream
            std::ostream& stream() override { return m_os; }
        };

    } // unnamed namespace
} // namespace Detail

    ///////////////////////////////////////////////////////////////////////////

    auto makeStream( std::string const& filename ) -> Detail::unique_ptr<IStream> {
        if ( filename.empty() || filename == "-" ) {
            return Detail::make_unique<Detail::CoutStream>();
        }
        if( filename[0] == '%' ) {
            if ( filename == "%debug" ) {
                return Detail::make_unique<Detail::DebugOutStream>();
            } else if ( filename == "%stderr" ) {
                return Detail::make_unique<Detail::CerrStream>();
            } else if ( filename == "%stdout" ) {
                return Detail::make_unique<Detail::CoutStream>();
            } else {
                CATCH_ERROR( "Unrecognised stream: '" << filename << '\'' );
            }
        }
        return Detail::make_unique<Detail::FileStream>( filename );
    }

}
