//
//  Copyright (c) 2012 Artyom Beilis (Tonkikh)
//
//  Distributed under the Boost Software License, Version 1.0. (See
//  accompanying file LICENSE_1_0.txt or copy at
//  http://www.boost.org/LICENSE_1_0.txt)
//
#ifndef NOWIDE_FILEBUF_HPP
#define NOWIDE_FILEBUF_HPP

#include <iosfwd>
#include <nowide/config.hpp>
#include <nowide/stackstring.hpp>
#include <fstream>
#include <streambuf>
#include <stdio.h>

#ifdef NOWIDE_MSVC
#  pragma warning(push)
#  pragma warning(disable : 4996 4244 4800)
#endif



namespace nowide {
#if !defined(NOWIDE_WINDOWS) && !defined(NOWIDE_FSTREAM_TESTS) && !defined(NOWIDE_DOXYGEN)
    using std::basic_filebuf;
    using std::filebuf;
#else // Windows
    
    ///
    /// \brief This forward declaration defined the basic_filebuf type.
    ///
    /// it is implemented and specialized for CharType = char, it behaves
    /// implements std::filebuf over standard C I/O
    ///
    template<typename CharType,typename Traits = std::char_traits<CharType> >
    class basic_filebuf;
    
    ///
    /// \brief This is implementation of std::filebuf
    ///
    /// it is implemented and specialized for CharType = char, it behaves
    /// implements std::filebuf over standard C I/O
    ///
    template<>
    class basic_filebuf<char> : public std::basic_streambuf<char> {
    public:
        ///
        /// Creates new filebuf
        ///
        basic_filebuf() : 
            buffer_size_(4),
            buffer_(0),
            file_(0),
            own_(true),
            mode_(std::ios::in | std::ios::out)
        {
            setg(0,0,0);
            setp(0,0);
        }
        
        virtual ~basic_filebuf()
        {
            if(file_) {
                ::fclose(file_);
                file_ = 0;
            }
            if(own_ && buffer_)
                delete [] buffer_;
        }
        
        ///
        /// Same as std::filebuf::open but s is UTF-8 string
        ///
        basic_filebuf *open(std::string const &s,std::ios_base::openmode mode)
        {
            return open(s.c_str(),mode);
        }
        ///
        /// Same as std::filebuf::open but s is UTF-8 string
        ///
        basic_filebuf *open(char const *s,std::ios_base::openmode mode)
        {
            if(file_) {
                sync();
                ::fclose(file_);
                file_ = 0;
            }
            bool ate = bool(mode & std::ios_base::ate);
            if(ate)
                mode = mode ^ std::ios_base::ate;
            wchar_t const *smode = get_mode(mode);
            if(!smode)
                return 0;
            wstackstring name;
            if(!name.convert(s)) 
                return 0;
            #ifdef NOWIDE_FSTREAM_TESTS
            FILE *f = ::fopen(s,nowide::convert(smode).c_str());
            #else
            FILE *f = ::_wfopen(name.c_str(),smode);
            #endif
            if(!f)
                return 0;
            if(ate && fseek(f,0,SEEK_END)!=0) {
                fclose(f);
                return 0;
            }
            file_ = f;
            return this;
        }
        ///
        /// Same as std::filebuf::close()
        ///
        basic_filebuf *close()
        {
            bool res = sync() == 0;
            if(file_) {
                if(::fclose(file_)!=0)
                    res = false;
                file_ = 0;
            }
            return res ? this : 0;
        }
        ///
        /// Same as std::filebuf::is_open()
        ///
        bool is_open() const
        {
            return file_ != 0;
        }

    private:
        void make_buffer()
        {
            if(buffer_)
                return;
            if(buffer_size_ > 0) {
                buffer_ = new char [buffer_size_];
                own_ = true;
            }
        }
    protected:
        
        virtual std::streambuf *setbuf(char *s,std::streamsize n)
        {
            if(!buffer_ && n>=0) {
                buffer_ = s;
                buffer_size_ = n;
                own_ = false;
            }
            return this;
        }
        
#ifdef NOWIDE_DEBUG_FILEBUF

        void print_buf(char *b,char *p,char *e)
        {
            std::cerr << "-- Is Null: " << (b==0) << std::endl;; 
            if(b==0)
                return;
            if(e != 0)
                std::cerr << "-- Total: " << e - b <<" offset from start " << p - b << std::endl;
            else
                std::cerr << "-- Total: " << p - b << std::endl;
                
            std::cerr << "-- [";
            for(char *ptr = b;ptr<p;ptr++)
                std::cerr << *ptr;
            if(e!=0) {
                std::cerr << "|";
                for(char *ptr = p;ptr<e;ptr++)
                    std::cerr << *ptr;
            }
            std::cerr << "]" << std::endl;
           
        }
        
        void print_state()
        {
            std::cerr << "- Output:" << std::endl;
            print_buf(pbase(),pptr(),0);
            std::cerr << "- Input:" << std::endl;
            print_buf(eback(),gptr(),egptr());
            std::cerr << "- fpos: " << (file_ ? ftell(file_) : -1L) << std::endl;
        }
        
        struct print_guard
        {
            print_guard(basic_filebuf *p,char const *func)
            {
                self = p;
                f=func;
                std::cerr << "In: " << f << std::endl;
                self->print_state();
            }
            ~print_guard()
            {
                std::cerr << "Out: " << f << std::endl;
                self->print_state();
            }
            basic_filebuf *self;
            char const *f;
        };
#else
#endif        
        
        int overflow(int c)
        {
#ifdef NOWIDE_DEBUG_FILEBUF
            print_guard g(this,__FUNCTION__);
#endif            
            if(!file_)
                return EOF;
            
            if(fixg() < 0)
                return EOF;

            size_t n = pptr() - pbase();
            if(n > 0) {
                if(::fwrite(pbase(),1,n,file_) < n)
                    return -1;
                fflush(file_);
            }

            if(buffer_size_ > 0) {
                make_buffer();
                setp(buffer_,buffer_+buffer_size_);
                if(c!=EOF)
                    sputc(c);
            }
            else if(c!=EOF) {
                if(::fputc(c,file_)==EOF)
                    return EOF;
                fflush(file_);
            }
            return 0;
        }
        
        
        int sync()
        {
            return overflow(EOF);
        }

        int underflow()
        {
#ifdef NOWIDE_DEBUG_FILEBUF
            print_guard g(this,__FUNCTION__);
#endif            
            if(!file_)
                return EOF;
            if(fixp() < 0)
                return EOF;
            if(buffer_size_ == 0) {
                int c = ::fgetc(file_);
                if(c==EOF) {
                    return EOF;
                }
                last_char_ = c;
                setg(&last_char_,&last_char_,&last_char_ + 1);
                return c;
            }
            make_buffer();
            size_t n = ::fread(buffer_,1,buffer_size_,file_);
            setg(buffer_,buffer_,buffer_+n);
            if(n == 0)
                return EOF;
            return std::char_traits<char>::to_int_type(*gptr());
        }

        int pbackfail(int)
        {
            return pubseekoff(-1,std::ios::cur);
        }

        std::streampos seekoff(std::streamoff off,
                            std::ios_base::seekdir seekdir,
                            std::ios_base::openmode /*m*/)
        {
#ifdef NOWIDE_DEBUG_FILEBUF
            print_guard g(this,__FUNCTION__);
#endif            
            if(!file_)
                return EOF;
            if(fixp() < 0 || fixg() < 0)
                return EOF;
            if(seekdir == std::ios_base::cur) {
                if( ::fseek(file_,off,SEEK_CUR) < 0)
                    return EOF;
            }
            else if(seekdir == std::ios_base::beg) {
                if( ::fseek(file_,off,SEEK_SET) < 0)
                    return EOF;
            }
            else if(seekdir == std::ios_base::end) {
                if( ::fseek(file_,off,SEEK_END) < 0)
                    return EOF;
            }
            else
                return -1;
            return ftell(file_);
        }
        std::streampos seekpos(std::streampos off,std::ios_base::openmode m)
        {
            return seekoff(std::streamoff(off),std::ios_base::beg,m);
        }
    private:
        int fixg()
        {
            if(gptr()!=egptr()) {
                std::streamsize off = gptr() - egptr();
                setg(0,0,0);
                if(fseek(file_,off,SEEK_CUR) != 0)
                    return -1;
            }
            setg(0,0,0);
            return 0;
        }
        
        int fixp()
        {
            if(pptr()!=0) {
                int r = sync();
                setp(0,0);
                return r;
            }
            return 0;
        }

        void reset(FILE *f = 0)
        {
            sync();
            if(file_) {
                fclose(file_);
                file_ = 0;
            }
            file_ = f;
        }
        
        
        static wchar_t const *get_mode(std::ios_base::openmode mode)
        {
            //
            // done according to n2914 table 106 27.9.1.4
            //

            // note can't use switch case as overload operator can't be used
            // in constant expression
            if(mode == (std::ios_base::out))
                return L"w";
            if(mode == (std::ios_base::out | std::ios_base::app))
                return L"a";
            if(mode == (std::ios_base::app))
                return L"a";
            if(mode == (std::ios_base::out | std::ios_base::trunc))
                return L"w";
            if(mode == (std::ios_base::in))
                return L"r";
            if(mode == (std::ios_base::in | std::ios_base::out))
                return L"r+";
            if(mode == (std::ios_base::in | std::ios_base::out | std::ios_base::trunc))
                return L"w+";
            if(mode == (std::ios_base::in | std::ios_base::out | std::ios_base::app))
                return L"a+";
            if(mode == (std::ios_base::in | std::ios_base::app))
                return L"a+";
            if(mode == (std::ios_base::binary | std::ios_base::out))
                return L"wb";
            if(mode == (std::ios_base::binary | std::ios_base::out | std::ios_base::app))
                return L"ab";
            if(mode == (std::ios_base::binary | std::ios_base::app))
                return L"ab";
            if(mode == (std::ios_base::binary | std::ios_base::out | std::ios_base::trunc))
                return L"wb";
            if(mode == (std::ios_base::binary | std::ios_base::in))
                return L"rb";
            if(mode == (std::ios_base::binary | std::ios_base::in | std::ios_base::out))
                return L"r+b";
            if(mode == (std::ios_base::binary | std::ios_base::in | std::ios_base::out | std::ios_base::trunc))
                return L"w+b";
            if(mode == (std::ios_base::binary | std::ios_base::in | std::ios_base::out | std::ios_base::app))
                return L"a+b";
            if(mode == (std::ios_base::binary | std::ios_base::in | std::ios_base::app))
                return L"a+b";
            return 0;    
        }
        
        size_t buffer_size_;
        char *buffer_;
        FILE *file_;
        bool own_;
        char last_char_;
        std::ios::openmode mode_;
    };
    
    ///
    /// \brief Convinience typedef
    ///
    typedef basic_filebuf<char> filebuf;
    
    #endif // windows
    
} // nowide


#ifdef NOWIDE_MSVC
#  pragma warning(pop)
#endif


#endif

// vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4
