/*
    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/>.
*/

#include <libmaus2/LibMausConfig.hpp>

#include <libmaus2/aio/FileRemoval.hpp>

#include <libmaus2/aio/InputStreamInstance.hpp>

#include <libmaus2/aio/LineSplittingPosixFdOutputStream.hpp>

#include <libmaus2/aio/MemoryFileContainer.hpp>
#include <libmaus2/aio/MemoryInputOutputStream.hpp>
#include <libmaus2/aio/MemoryInputStream.hpp>
#include <libmaus2/aio/MemoryInputStreamBuffer.hpp>
#include <libmaus2/aio/MemoryOutputStream.hpp>
#include <libmaus2/aio/MemoryOutputStreamBuffer.hpp>

#include <libmaus2/aio/PosixFdInputStream.hpp>
#include <libmaus2/aio/PosixFdOutputStream.hpp>
#include <libmaus2/aio/PosixFdInputOutputStream.hpp>

#include <libmaus2/timing/RealTimeClock.hpp>

#include <libmaus2/util/ArgParser.hpp>
#include <libmaus2/util/ArgInfo.hpp>
#include <libmaus2/util/TempFileRemovalContainer.hpp>
#include <libmaus2/util/GetFileSize.hpp>

#include <libmaus2/aio/MemoryBackedOutputStream.hpp>
#include <libmaus2/aio/CharArrayInputStream.hpp>
#include <libmaus2/aio/CharArrayOutputStream.hpp>
#include <libmaus2/aio/CountOutputStream.hpp>

#include <vector>
#include <map>
#include <cmath>

void testPosixFdInput()
{
	::libmaus2::autoarray::AutoArray<unsigned char> A = libmaus2::util::GetFileSize::readFile("configure");
	libmaus2::aio::PosixFdInputStream PFIS("configure",64*1024);

	PFIS.clear();
	PFIS.seekg(0,std::ios::end);
	assert ( static_cast<int64_t>(PFIS.tellg()) == static_cast<int64_t>(A.size()) );
	PFIS.clear();
	PFIS.seekg(0,std::ios::beg);
	uint64_t const inc = 127;

	for ( uint64_t i = 0; i <= A.size(); i += std::min(inc,A.size()-i) )
	{
		PFIS.clear();
		PFIS.seekg(i,std::ios::beg);

		int c = -1;
		#if ! defined(NDEBUG)
		uint64_t p = i;
		#endif
		while ( ( c = PFIS.get() ) != std::istream::traits_type::eof() )
		{
			assert ( c == A[p++] );
		}

		assert ( p == A.size() );

		// if ( (i & (1024-1)) == 0 )
		//	std::cerr << "i=" << i << std::endl;

		if ( i == A.size() )
			break;
	}
}


template<typename stream_type, typename output_stream_type, typename input_stream_type>
void testInputOutput()
{
	std::string const tmpfn = "testAIO.tmp";
	libmaus2::util::TempFileRemovalContainer::addTempFile(tmpfn);

	{
		stream_type PFIOS(tmpfn,std::ios::in|std::ios::out|std::ios::binary|std::ios::trunc);
		PFIOS.put('a');
		std::cerr << PFIOS.tellp() << std::endl;
		PFIOS.seekg(0,std::ios::beg);
		std::cerr << (char)PFIOS.get() << std::endl;
		PFIOS.seekg(0);
		std::cerr << (char)PFIOS.get() << std::endl;

		PFIOS.seekp(0,std::ios::beg);
		PFIOS.put('b');
		std::cerr << PFIOS.tellp() << std::endl;
		PFIOS.seekg(0,std::ios::beg);
		std::cerr << (char)PFIOS.get() << std::endl;
		PFIOS.seekg(0);
		std::cerr << (char)PFIOS.get() << std::endl;

		PFIOS.seekp(0,std::ios::beg);
		PFIOS.put('c');
		PFIOS.put('d');
		std::cerr << PFIOS.tellp() << std::endl;
		PFIOS.seekg(0);
		int c;
		while ( (c=PFIOS.get()) != std::iostream::traits_type::eof() )
		{
			std::cerr << (char) c << std::endl;
		}

		PFIOS.clear();

		std::cerr << "---" << std::endl;

		PFIOS.seekp(2,std::ios::beg);
		PFIOS.put('e');
		PFIOS.put('f');
		PFIOS.put('g');
		std::cerr << PFIOS.tellp() << std::endl;
		PFIOS.seekg(0);
		// int c;
		while ( (c=PFIOS.get()) != std::iostream::traits_type::eof() )
		{
			std::cerr << (char) c << std::endl;
		}

		PFIOS.clear();
	}

	std::cerr << "-----" << std::endl;

	{
		stream_type PFIOS(tmpfn,std::ios::in|std::ios::out|std::ios::binary);

		int c;
		while ( (c=PFIOS.get()) != std::iostream::traits_type::eof() )
		{
			std::cerr << (char) c << std::endl;
		}
	}

	std::cerr << std::string("---") << std::endl;

	{
		stream_type PFIOS(tmpfn,std::ios::in|std::ios::out|std::ios::binary);

		PFIOS.seekg(0,std::ios::end);

		int64_t const l = PFIOS.tellg();

		for ( int64_t i = 0; i < l; ++i )
		{
			PFIOS.seekp(-(i+1),std::ios::end);
			PFIOS.put('a'+i);
		}

		PFIOS.seekg(0);
		int c;
		while ( (c=PFIOS.get()) != std::iostream::traits_type::eof() )
		{
			std::cerr << (char) c << std::endl;
		}

		PFIOS.clear();
	}

	std::cerr << std::string("---") << std::endl;

	{
		output_stream_type PFOS(tmpfn);
		PFOS.put('1');
		PFOS.put('2');
		PFOS.put('3');
		PFOS.put('4');
		PFOS.put('5');
	}

	{
		stream_type PFIOS(tmpfn,std::ios::in|std::ios::out|std::ios::binary);

		int c;
		while ( (c=PFIOS.get()) != std::iostream::traits_type::eof() )
		{
			std::cerr << (char) c << std::endl;
		}
	}

	std::cerr << std::string("***") << std::endl;

	{
		input_stream_type PFIOS(tmpfn);

		int c;
		while ( (c=PFIOS.get()) != std::iostream::traits_type::eof() )
		{
			std::cerr << (char) c << std::endl;
		}
	}
}

void testLineSplittingOutput()
{
	libmaus2::aio::LineSplittingPosixFdOutputStream LSOUT("split",4,32);
	for ( uint64_t i = 0; i < 17; ++i )
	{
		LSOUT << "line_" << i << "\n";
	}
	LSOUT.flush();

	std::vector<std::string> Vfn = LSOUT.getFileNames();
	uint64_t j = 0;
	for ( uint64_t i = 0; i < Vfn.size(); ++i )
	{
		libmaus2::aio::InputStreamInstance ISI(Vfn[i]);

		for ( uint64_t k = 0; k < 4 && j < 17; ++k, ++j )
		{
			std::string line;
			std::getline(ISI,line);
			std::ostringstream ostr;
			ostr << "line_" << j;

			// std::cerr << ostr.str() << " __ " << line << std::endl;

			assert ( ostr.str() == line );
		}

		libmaus2::aio::FileRemoval::removeFile(Vfn[i]);
	}
}

void testLineSplittingNoOutput()
{
	{
		libmaus2::aio::LineSplittingPosixFdOutputStream LSOUT("nosplit",4,32);
	}

	assert ( !libmaus2::util::GetFileSize::fileExists("nosplit") );
}

void testPosixFdOverwrite()
{
	std::string const fn = "nonexistent.test.file";
	std::string const text1 = "Hello world.";
	std::string const text2 = "new text";

	{
		libmaus2::aio::PosixFdOutputStream PFOS(fn);
		PFOS << text1;
		PFOS.flush();
	}

	{
		libmaus2::aio::InputStreamInstance CIS(fn);
		libmaus2::autoarray::AutoArray<char> C(text1.size());
		CIS.read(C.begin(),C.size());
		assert ( CIS.get() < 0 );
		assert ( strncmp ( text1.c_str(), C.begin(), C.size() ) == 0 );
	}

	{
		libmaus2::aio::PosixFdOutputStream PFOS(fn);
		PFOS << text2;
		PFOS.flush();
	}

	{
		libmaus2::aio::InputStreamInstance CIS(fn);
		libmaus2::autoarray::AutoArray<char> C(text2.size());
		CIS.read(C.begin(),C.size());
		assert ( CIS.get() < 0 );
		assert ( strncmp ( text2.c_str(), C.begin(), C.size() ) == 0 );
	}

	libmaus2::aio::FileRemoval::removeFile(fn);
}

void testPutBack()
{
	std::string const fn = "configure";
	uint64_t const fs = libmaus2::util::GetFileSize::getFileSize(fn);
	libmaus2::autoarray::AutoArray<char> A(fs,false);

	{
		libmaus2::aio::InputStreamInstance CIS(fn);
		CIS.read(A.begin(),fs);
	}

	uint64_t const putbacksize = 2048;

	if ( fs >= putbacksize )
	{
		libmaus2::aio::PosixFdInputStream PFIS(fn,64*1024,putbacksize);

		for ( uint64_t z = 0; z < (fs-putbacksize+1); ++z )
		{
			for ( uint64_t i = 0 ; i < putbacksize; ++i )
			{
				#if ! defined(NDEBUG)
				int const c =
				#endif
					PFIS.get();
				assert ( c >= 0 );
				assert ( c == A[z+i] );
			}

			PFIS.clear();

			for ( int64_t i = putbacksize-1; i >= 1; --i )
				PFIS.putback(A[z+i]);
		}
	}

}

void testMemoryBacked(libmaus2::util::ArgParser const & arg)
{
	std::string const tmpfn = libmaus2::util::ArgInfo::getDefaultTmpFileName(arg.progname) + "_mb";
	libmaus2::util::TempFileRemovalContainer::addTempFile(tmpfn);

	{
		uint64_t const n = 1024;
		uint64_t const w = 129;
		libmaus2::aio::MemoryBackedOutputStream MB(tmpfn,n);

		for ( uint64_t i = 0; i < w; ++i )
			MB.put(i%256);

		std::string const url = MB.getURL();
		// std::cerr << "url=" << url << std::endl;
		assert ( url != tmpfn );
		assert ( url.size() >= strlen("mem:") );
		assert ( url.substr(0,4) == std::string("mem:") );

		libmaus2::aio::InputStreamInstance ISI(url);
		for ( uint64_t i = 0; i < w; ++i )
			assert ( ISI.get() == static_cast<int>(i % 256) );
	}

	{
		uint64_t const n = 1024;
		uint64_t const w = 1024;
		libmaus2::aio::MemoryBackedOutputStream MB(tmpfn,n);

		for ( uint64_t i = 0; i < w; ++i )
			MB.put(i%256);

		std::string const url = MB.getURL();
		assert ( url == tmpfn );
		// std::cerr << "[V] url=" << url << std::endl;

		libmaus2::aio::InputStreamInstance ISI(url);
		for ( uint64_t i = 0; i < w; ++i )
		{
			int const c = ISI.get();
			assert ( c != std::istream::traits_type::eof() );
			bool const ok = ( c == static_cast<int>(i % 256) );
			if ( ! ok )
			{
				std::cerr << "[E] expected " << (i%256) << " got " << c << std::endl;
				assert ( ok );
			}
		}
	}

	{
		uint64_t const n = 1024;
		uint64_t const w = 2194;
		libmaus2::aio::MemoryBackedOutputStream MB(tmpfn,n);

		for ( uint64_t i = 0; i < w; ++i )
			MB.put(i%256);

		std::string const url = MB.getURL();
		// std::cerr << "url=" << url << std::endl;
		assert ( url == tmpfn );

		libmaus2::aio::InputStreamInstance ISI(url);
		for ( uint64_t i = 0; i < w; ++i )
			assert ( ISI.get() == static_cast<int>(i % 256) );
	}

	{
		uint64_t const n = 1024;
		uint64_t const w = 1024*1024 + 25;
		libmaus2::aio::MemoryBackedOutputStream MB(tmpfn,n);

		for ( uint64_t i = 0; i < w; ++i )
			MB.put(i%256);

		std::string const url = MB.getURL();
		// std::cerr << "url=" << url << std::endl;
		assert ( url == tmpfn );

		libmaus2::aio::InputStreamInstance ISI(url);
		for ( uint64_t i = 0; i < w; ++i )
			assert ( ISI.get() == static_cast<int>(i % 256) );
	}
}


void testCharArrayInputStream()
{
	char text[] = "hello world";
	uint64_t const l = strlen(text);

	libmaus2::aio::CharArrayInputStream CAIS(text,text+l);

	for ( uint64_t i = 0; i < l; ++i )
	{
		int const c = CAIS.get();

		assert ( c != std::istream::traits_type::eof() );
		assert ( static_cast<char>(c) == text[i] );
	}

	assert ( CAIS.peek() == std::istream::traits_type::eof() );

	std::cerr << "[V] successfully tested libmaus2::aio::CharArrayInputStream" << std::endl;
}

void testCharArrayOutputStream()
{
	char text[] = "hello world";
	uint64_t const l = strlen(text);
	libmaus2::autoarray::AutoArray<char> Aout(l);

	libmaus2::aio::CharArrayOutputStream CAOS(Aout.begin(),Aout.end());

	CAOS.write(text,l);

	for ( uint64_t i = 0; i < l; ++i )
	{
		int const c = Aout[i];
		assert ( static_cast<char>(c) == text[i] );
	}

	assert ( CAOS );

	std::cerr << "[V] successfully tested libmaus2::aio::CharArrayOutputStream" << std::endl;
}

void testCountOutputStream()
{
	char text[] = "hello world";
	uint64_t const l = strlen(text);

	libmaus2::aio::CountOutputStream CAOS;

	CAOS.write(text,l);
	CAOS.flush();

	assert ( CAOS );
	assert ( CAOS.c == l );

	std::cerr << "[V] successfully tested libmaus2::aio::CountOutputStream" << std::endl;
}

#include <libmaus2/sorting/SortingBufferedOutputFile.hpp>

struct SerObject
{
	uint64_t key;
	uint64_t value;

	SerObject() {}
	SerObject(uint64_t const rkey, uint64_t const rvalue) : key(rkey), value(rvalue) {}

	std::ostream & serialise(std::ostream & out) const
	{
		libmaus2::util::NumberSerialisation::serialiseNumber(out,key);
		libmaus2::util::NumberSerialisation::serialiseNumber(out,value);
		return out;
	}

	std::istream & deserialise(std::istream & in)
	{
		key = libmaus2::util::NumberSerialisation::deserialiseNumber(in);
		value = libmaus2::util::NumberSerialisation::deserialiseNumber(in);
		return in;
	}

	std::string toString() const
	{
		std::ostringstream ostr;
		ostr << "SerObject(" << key << "," << value << ")";
		return ostr.str();
	}

	bool operator<(SerObject const S) const
	{
		return key < S.key;
	}
};

#include <libmaus2/aio/SerialisedPeeker.hpp>

void testSerialisingSorting()
{
	std::string const tmpfn = "mem:tmpfile";
	std::vector<SerObject> V;

	{
		libmaus2::aio::OutputStreamInstance OSI(tmpfn);

		uint64_t n = 1858185;

		for ( uint64_t i = 0; i < n; ++i )
		{
			SerObject const SO(n-i-1,i%31);
			SO.serialise(OSI);
			V.push_back(SO);
		}
	}

	std::sort(V.begin(),V.end());

	typedef libmaus2::sorting::SerialisingSortingBufferedOutputFile<SerObject> sorter_type;
	uint64_t const blocksize = 1024*1024;
        uint64_t const backblocksize = sorter_type::getDefaultBackBlockSize();
        uint64_t const maxfan = sorter_type::getDefaultMaxFan();
        uint64_t const sortthreads = 24;
        int const verbose = 1; // sorter_type::getDefaultVerbose();
        std::ostream * verbstr = &std::cerr; // sorter_type::getDefaultVerbStr();

	libmaus2::sorting::SerialisingSortingBufferedOutputFile<SerObject>::sort(tmpfn,blocksize,backblocksize,maxfan,sortthreads,verbose,verbstr);

	libmaus2::aio::SerialisedPeeker<SerObject> SP(tmpfn);
	SerObject SO;
	uint64_t q = 0;
	for ( uint64_t i = 0; SP.getNext(SO); ++i )
	{
		assert ( SO.key == V[i].key );
		assert ( SO.value == V[i].value );
		q += 1;
	}

	assert ( q == V.size() );

	std::cerr << "[V] successfully tested libmaus2::sorting::SerialisingSortingBufferedOutputFile<>::sort" << std::endl;
}

int main(int argc, char * argv[])
{
	try
	{
		libmaus2::util::ArgParser arg(argc,argv);

		testCharArrayInputStream();
		testCharArrayOutputStream();
		testCountOutputStream();
		testSerialisingSorting();

		testMemoryBacked(arg);
		testPutBack();
		testPosixFdOverwrite();
		testLineSplittingOutput();
		testLineSplittingNoOutput();
		testPosixFdInput();
		testInputOutput<libmaus2::aio::PosixFdInputOutputStream,libmaus2::aio::PosixFdOutputStream,libmaus2::aio::PosixFdInputStream>();
		testInputOutput<libmaus2::aio::MemoryInputOutputStream,libmaus2::aio::MemoryOutputStream,libmaus2::aio::MemoryInputStream>();

	}
	catch(std::exception const & ex)
	{
		std::cerr << ex.what() << std::endl;
		return EXIT_FAILURE;
	}
}
