/*
    libmaus2
    Copyright (C) 2009-2013 German Tischler
    Copyright (C) 2011-2013 Genome Research Limited

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
#if ! defined(LIBMAUS2_AIO_POSIXFDOUTPUTSTREAMBUFFER_HPP)
#define LIBMAUS2_AIO_POSIXFDOUTPUTSTREAMBUFFER_HPP

#include <ostream>
#include <cmath>
#include <fstream>

#include <libmaus2/LibMausConfig.hpp>
#include <libmaus2/posix/PosixFunctions.hpp>
#include <libmaus2/autoarray/AutoArray.hpp>
#include <libmaus2/aio/PosixFdInput.hpp>
#include <libmaus2/aio/StreamLock.hpp>

#if \
	defined(LIBMAUS2_HAVE_LSEEK) && \
	defined(LIBMAUS2_HAVE_CLOSE) && \
	defined(LIBMAUS2_HAVE_O_WRONLY) && \
	defined(LIBMAUS2_HAVE_O_CREAT) && \
	defined(LIBMAUS2_HAVE_O_TRUNC) && \
	defined(LIBMAUS2_HAVE_WRITE)
	// defined(LIBMAUS2_HAVE_FSYNC) &&
#define LIBMAUS2_AIO_POSIXFDOUTPUTSTREAMBUFFER_SUPPORTED
#endif

namespace libmaus2
{
	namespace aio
	{
		struct PosixFdOutputStreamBuffer : public ::std::streambuf
		{
			public:
			static int64_t doSeekAbsolute(int const fd, std::string const & filename, int64_t const p, int const whence)
			{
				libmaus2::autoarray::AutoArray<unsigned char> Aoffin(libmaus2::posix::PosixFunctions::getOffTSize());
				libmaus2::autoarray::AutoArray<unsigned char> Aoffout(libmaus2::posix::PosixFunctions::getOffTSize());
				libmaus2::autoarray::AutoArray<unsigned char> Aoffin64(sizeof(int64_t));

				// encode to 64 bit array
				libmaus2::posix::PosixFunctions::encodeSigned<int64_t>(p,Aoffin64.begin(),sizeof(int64_t));
				// translate to off_t
				libmaus2::posix::PosixFunctions::translateSigned(Aoffin.begin(),Aoffin.size(),Aoffin64.begin(),Aoffin64.size());

				// translate back
				libmaus2::posix::PosixFunctions::translateSigned(Aoffin64.begin(),Aoffin64.size(),Aoffin.begin(),Aoffin.size());
				// check
				assert ( libmaus2::posix::PosixFunctions::decodeSigned<int64_t>(Aoffin64.begin(),Aoffin64.size()) == p );

				while ( true )
				{
					std::time_t const time_bef = getTime();
					libmaus2::posix::PosixFunctions::posix_lseek(Aoffout.begin(),fd,Aoffin.begin(),whence);
					std::time_t const time_aft = getTime();
					printWarning("lseek",time_aft-time_bef,filename,fd);

					if ( libmaus2::posix::PosixFunctions::offTIsMinusOne(Aoffout.begin()) )
					{
						int const error = errno;

						switch ( error )
						{
							case EINTR:
							case EAGAIN:
							break;
							default:
							{
								libmaus2::exception::LibMausException se;
								se.getStream() << "PosixOutputStreamBuffer::doSeekkAbsolute(): lseek() failed: " << strerror(error) << std::endl;
								se.finish();
								// se.print(std::cerr,libmaus2::aio::StreamLock::cerrlock);
								throw se;
							}
						}
					}
					else
					{
						return libmaus2::posix::PosixFunctions::decodeSigned<int64_t>(Aoffout.begin(),Aoffout.size());
					}
				}
			}

			static void doClose(int const fd, std::string const & filename)
			{
				int r = -1;

				while ( r < 0 )
				{
					std::time_t const time_bef = getTime();
					r = libmaus2::posix::PosixFunctions::close(fd);
					std::time_t const time_aft = getTime();
					printWarning("close",time_aft-time_bef,filename,fd);

					if ( r < 0 )
					{
						int const error = errno;

						switch ( error )
						{
							case EINTR:
							case EAGAIN:
								break;
							default:
							{
								libmaus2::exception::LibMausException se;
								se.getStream() << "PosixOutputStreamBuffer::doClose(): close() failed: " << strerror(error) << std::endl;
								se.finish();
								// se.print(std::cerr,libmaus2::aio::StreamLock::cerrlock);
								throw se;
							}
						}
					}
				}
			}

			static int doOpen(std::string const & filename,
				int const flags =
					libmaus2::posix::PosixFunctions::get_O_WRONLY() |
					libmaus2::posix::PosixFunctions::get_O_CREAT() |
					libmaus2::posix::PosixFunctions::get_O_TRUNC()
					#if defined(LIBMAUS2_HAVE_O_BINARY)
					| libmaus2::posix::PosixFunctions::get_O_BINARY()
					#endif
					,
					int const mode = 0644)
			{
				int fd = -1;

				while ( fd < 0 )
				{
					std::time_t const time_bef = getTime();
					libmaus2::autoarray::AutoArray<unsigned char> Amode(libmaus2::posix::PosixFunctions::getOpenModeSize());
					libmaus2::posix::PosixFunctions::encode(mode,Amode.begin(),Amode.size());
					fd = libmaus2::posix::PosixFunctions::open(filename.c_str(),flags,Amode.begin());
					std::time_t const time_aft = getTime();
					printWarning("open",time_aft-time_bef,filename,fd);

					if ( fd < 0 )
					{
						int const error = errno;

						switch ( error )
						{
							case EINTR:
							case EAGAIN:
								break;
							default:
							{
								libmaus2::exception::LibMausException se;
								se.getStream() << "PosixOutputStreamBuffer::doOpen(): open("<<filename<<") failed: " << strerror(error) << std::endl;
								se.finish();
								// se.print(std::cerr,libmaus2::aio::StreamLock::cerrlock);
								throw se;
							}
						}
					}
				}

				return fd;
			}

			static void doFlush(
				int const
					#if defined(LIBMAUS2_HAVE_FSYNC)
					fd
					#endif
					,
				std::string const &
					#if defined(LIBMAUS2_HAVE_FSYNC)
					filename
					#endif
			)
			{
				#if defined(LIBMAUS2_HAVE_FSYNC)
				int r = -1;

				while ( r < 0 )
				{
					std::time_t const time_bef = getTime();
					r = libmaus2::posix::PosixFunctions::fsync(fd);
					std::time_t const time_aft = getTime();
					printWarning("fsync",time_aft-time_bef,filename,fd);

					if ( r < 0 )
					{
						int const error = errno;

						switch ( error )
						{
							case EINTR:
							case EAGAIN:
								break;
							case EROFS:
							case EINVAL:
								// descriptor does not support flushing
								return;
							default:
							{
								libmaus2::exception::LibMausException se;
								se.getStream() << "PosixOutputStreamBuffer::doFlush(): fsync() failed: " << strerror(error) << std::endl;
								se.finish();
								// se.print(std::cerr,libmaus2::aio::StreamLock::cerrlock);
								throw se;
							}
						}
					}
				}
				#endif
			}

			static uint64_t
				doWrite(
					int const fd,
					std::string const & filename,
					char * p,
					uint64_t n,
					int64_t const optblocksize,
					uint64_t writepos
				)
			{
				while ( n )
				{
					size_t const towrite = std::min(n,static_cast<uint64_t>(optblocksize));

					{
						std::time_t const time_bef = getTime();
						::libmaus2::ssize_t const w = libmaus2::posix::PosixFunctions::write(fd,p,towrite);
						std::time_t const time_aft = getTime();
						printWarning("write",time_aft-time_bef,filename,fd);

						if ( w < 0 )
						{
							int const error = errno;

							switch ( error )
							{
								case EINTR:
								case EAGAIN:
									break;
								default:
								{
									libmaus2::exception::LibMausException se;
									se.getStream() << "PosixOutputStreamBuffer::doSync(): write() failed: " << strerror(error) << std::endl;
									se.finish();
									// se.print(std::cerr,libmaus2::aio::StreamLock::cerrlock);
									throw se;
								}
							}
						}
						else
						{
							{
							libmaus2::parallel::StdSpinLock::scope_lock_type slock(totaloutlock);
							totalout += w;
							}

							assert ( w <= static_cast<int64_t>(n) );
							n -= w;
							writepos += w;
							p += w;
						}
					}
				}

				assert ( ! n );

				return writepos;
			}

			private:
			static std::time_t const warnThreshold;
			static int const check;
			static std::atomic<uint64_t> totalout;
			static libmaus2::parallel::StdSpinLock totaloutlock;

			static std::time_t getTime()
			{
				return ::std::time(nullptr);
			}

			static void printWarning(char const * const functionname, std::time_t const time, std::string const & filename, int const fd)
			{
				if ( warnThreshold > 0 && time >= warnThreshold )
				{
					libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
					std::cerr << "[W] warning PosixFdOutputStreamBuffer: " << functionname << "(" << fd << ")" << " took " << time << "s ";
					if ( filename.size() )
						std::cerr << " on " << filename;
					std::cerr << std::endl;
				}
			}

			static uint64_t getDefaultBlockSize()
			{
				return 64*1024;
			}

			static int64_t getOptimalIOBlockSize(int const fd, std::string const & fn)
			{
				int64_t const fsopt = libmaus2::aio::PosixFdInput::getOptimalIOBlockSize(fd,fn);

				if ( fsopt <= 0 )
					return getDefaultBlockSize();
				else
					return fsopt;
			}

			std::string filename;
			std::string checkfilename;
			int fd;
			int checkfd;
			bool closefd;
			int64_t const optblocksize;
			uint64_t const buffersize;
			::libmaus2::autoarray::AutoArray<char> buffer;
			uint64_t writepos;

			static libmaus2::off_t doGetFileSize(int const fd, std::string const & filename)
			{
				// current position in stream
				libmaus2::off_t const cur = doSeekAbsolute(fd,filename,0,libmaus2::posix::PosixFunctions::get_SEEK_CUR());
				// end position of stream
				libmaus2::off_t const end = doSeekAbsolute(fd,filename,0,libmaus2::posix::PosixFunctions::get_SEEK_END());
				// go back to previous current
				libmaus2::off_t const final = doSeekAbsolute(fd,filename,cur,libmaus2::posix::PosixFunctions::get_SEEK_SET());

				if ( final != cur )
				{
					libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
					std::cerr << "libmaus2::aio::PosixFdOutputStreamBuffer::doGetFileSize(" << fd << "," << filename << "), failed to seek back to original position: " << final << " != " << cur << std::endl;
				}

				// return end position
				return end;
			}


			void doSync()
			{
				uint64_t const n = pptr()-pbase();
				pbump(-n);
				char * const p = pbase();
				uint64_t const origwritepos = writepos;
				writepos = doWrite(fd,filename,p,n,optblocksize,origwritepos);
				if ( checkfd != -1 )
				{
					uint64_t const checkwritepos = doWrite(checkfd,filename,p,n,optblocksize,origwritepos);
					if ( checkwritepos != writepos )
					{
						libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
						std::cerr << "libmaus2::aio::PosixFdOutputStreamBuffer:doSync(): checkwritepos!=writepos, " << checkwritepos << "!=" << writepos << std::endl;
					}
				}
			}

			public:
			PosixFdOutputStreamBuffer(int const rfd, int64_t const rbuffersize)
			:
			  filename(),
			  checkfilename(),
			  fd(rfd),
			  checkfd(-1),
			  closefd(false),
			  optblocksize(getOptimalIOBlockSize(fd,std::string())),
			  buffersize((rbuffersize <= 0) ? optblocksize : rbuffersize),
			  buffer(buffersize,false),
			  writepos(0)
			{
				setp(buffer.begin(),buffer.end()-1);
			}

			PosixFdOutputStreamBuffer(
				std::string const & rfilename,
				int64_t const rbuffersize,
				int const rflags =
					libmaus2::posix::PosixFunctions::get_O_WRONLY() |
					libmaus2::posix::PosixFunctions::get_O_CREAT() |
					libmaus2::posix::PosixFunctions::get_O_TRUNC()
					#if defined(LIBMAUS2_HAVE_O_BINARY)
					| libmaus2::posix::PosixFunctions::get_O_BINARY()
					#endif
					,
				int const rmode = 0644
			)
			:
			  filename(rfilename),
			  checkfilename(filename + ".check"),
			  fd(doOpen(filename,rflags,rmode)),
			  checkfd(check ? doOpen(checkfilename,rflags,rmode) : -1),
			  closefd(true),
			  optblocksize(getOptimalIOBlockSize(fd,filename)),
			  buffersize((rbuffersize <= 0) ? optblocksize : rbuffersize),
			  buffer(buffersize,false),
			  writepos(0)
			{
				setp(buffer.begin(),buffer.end()-1);
			}

			~PosixFdOutputStreamBuffer()
			{
				sync();
				if ( closefd )
				{
					if ( fd != -1 )
					{
						doClose(fd,filename);
						fd = -1;
					}

					bool const havecheckfile = (checkfd != -1);

					if ( havecheckfile )
					{
						doClose(checkfd,filename);
						checkfd = -1;
					}

					if ( havecheckfile )
					{
						// instantiate streams
						::std::ifstream istr0(filename.c_str(),std::ios::binary);
						::std::ifstream istr1(filename.c_str(),std::ios::binary);

						// see if they are both open
						bool ok0 = istr0.is_open();
						bool ok1 = istr1.is_open();

						bool gok = true;

						if ( ok0 != ok1 )
						{
							libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
							std::cerr << "libmaus2::aio::PosixFdOutputStreamBuffer:~PosixFdOutputStreamBuffer(): " << filename << " is_open ok0=" << ok0 << " ok1=" << ok1 << std::endl;
							gok = false;
						}
						else
						{
							gok = ok0;

							uint64_t const n = 64*1024;
							libmaus2::autoarray::AutoArray<char> BU0(n,false);
							libmaus2::autoarray::AutoArray<char> BU1(n,false);

							while ( istr0 && istr1 )
							{
								istr0.read(BU0.begin(),n);
								istr1.read(BU1.begin(),n);

								if ( istr0.gcount() != istr1.gcount() )
								{
									libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
									std::cerr << "libmaus2::aio::PosixFdOutputStreamBuffer:~PosixFdOutputStreamBuffer(): " << filename << " gcount istr0=" << istr0.gcount() << " != istr1=" << istr1.gcount() << std::endl;
									gok = false;
									break;
								}
								else if ( ! std::equal(BU0.begin(),BU0.begin()+istr0.gcount(),BU1.begin()) )
								{
									libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
									std::cerr << "libmaus2::aio::PosixFdOutputStreamBuffer:~PosixFdOutputStreamBuffer(): " << filename << " data equality failure" << std::endl;
									gok = false;
									break;
								}
							}

							ok0 = istr0 ? true : false;
							ok1 = istr1 ? true : false;

							if ( ok0 != ok1 )
							{
								libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
								std::cerr << "libmaus2::aio::PosixFdOutputStreamBuffer:~PosixFdOutputStreamBuffer(): " << filename << " final eq failure" << std::endl;
								gok = false;
							}
						}

						istr0.close();
						istr1.close();

						{
							if ( !gok )
							{
								libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
								std::cerr << "libmaus2::aio::PosixFdOutputStreamBuffer:~PosixFdOutputStreamBuffer(): " << filename << " FAILED" << std::endl;
							}
						}

						if ( checkfilename.size() )
							::remove(checkfilename.c_str());
					}
				}
			}

			int_type overflow(int_type c = traits_type::eof())
			{
				if ( c != traits_type::eof() )
				{
					*pptr() = c;
					pbump(1);
					doSync();
				}

				return c;
			}

			int sync()
			{
				doSync();
				doFlush(fd,filename);
				if ( checkfd != -1 )
					doFlush(checkfd,filename);
				return 0; // no error, -1 for error
			}

			/**
			 * seek to absolute position
			 **/
			::std::streampos seekpos(::std::streampos sp, ::std::ios_base::openmode which = ::std::ios_base::in | ::std::ios_base::out)
			{
				if ( (which & ::std::ios_base::out) )
				{
					doSync();
					writepos = sp = doSeekAbsolute(fd,filename,sp,libmaus2::posix::PosixFunctions::get_SEEK_SET());
					if ( checkfd != -1 )
					{
						::std::streampos const checksp = doSeekAbsolute(checkfd,filename,sp,libmaus2::posix::PosixFunctions::get_SEEK_SET());
						if ( checksp != sp )
						{
							libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
							std::cerr << "libmaus2::aio::PosixFdOutputStreamBuffer:seekpos(): checksp != sp, " << checksp << "!=" << sp << std::endl;
						}
					}
					return sp;
				}
				else
				{
					return -1;
				}
			}

			/**
			 * relative seek
			 **/
			::std::streampos seekoff(::std::streamoff off, ::std::ios_base::seekdir way, ::std::ios_base::openmode which)
			{
				if ( way == ::std::ios_base::cur )
				{
					/* do not flush buffer if off == 0 (as for tellg) */
					if ( off == 0 )
					{
						if ( (which & ::std::ios_base::out) )
							return writepos + (pptr()-pbase());
						else
							return -1;
					}
					/* seek via absolute position */
					else
					{
						return seekpos(static_cast< ::std::streamoff >(writepos + (pptr()-pbase())) + off,which);
					}
				}
				else if ( way == ::std::ios_base::beg )
				{
					return seekpos(off,which);
				}
				else if ( way == ::std::ios_base::end )
				{
					int64_t const filesize = static_cast<int64_t>(doGetFileSize(fd,filename));
					if ( checkfd != -1 )
					{
						int64_t const checkfilesize = static_cast<int64_t>(doGetFileSize(checkfd,filename));
						if ( checkfilesize != filesize )
						{
							libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
							std::cerr << "libmaus2::aio::PosixFdOutputStreamBuffer::seekoff(): checkfilesize != filesize, " << checkfilesize << "!=" << filesize << std::endl;
						}
					}
					int64_t const spos = filesize + off;
					return seekpos(spos, which);
				}
				else
				{
					return -1;
				}
			}

			static uint64_t getTotalOut()
			{
				uint64_t ltotalout;

				{
				libmaus2::aio::StreamLock::lock_type::scope_lock_type slock(libmaus2::aio::StreamLock::cerrlock);
				ltotalout = totalout;
				}

				return ltotalout;
			}
		};
	}
}
#endif
