//	UTF8File.cpp - A class which can be used to load from/save to a UTF-8 text
//				   file.
//	----------------------------------------------------------------------------
//	This file is part of 'NiallsAVLib', base code for any kind of audiovisual
//	apps.
//	Copyright (C) 2012  Niall Moody
//
//	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 "UTF8File.h"
#include "DebugHeaders.h"

#ifdef WIN32
#include <windows.h>
#include <strsafe.h>
#else
#include <clocale>
#include <cstdlib>
#include <locale>
#endif

#include <cassert>
#include <sstream>
#include <iostream>

using std::wstringstream;
using std::use_facet;
using std::wstring;
using std::string;
using std::locale;
using std::ios;

//------------------------------------------------------------------------------
UTF8File::UTF8File(const FilePath& path):
filePath(path),
linesRead(0),
failStream(false)
{

}

//------------------------------------------------------------------------------
UTF8File::UTF8File()
{

}

//------------------------------------------------------------------------------
UTF8File::~UTF8File()
{
	if(stream.is_open())
		stream.close();
}

//------------------------------------------------------------------------------
void UTF8File::setFilePath(const FilePath& path)
{
	if(filePath.getPath() != L"")
		DebugStatement(L"UTF8File::setFilePath(): filePath is not empty. Are you sure you haven't already started writing to a file?");

	filePath = path;
}

//------------------------------------------------------------------------------
void UTF8File::readFile(wstring& text, bool includeLineEndings)
{
	wstringstream tempstr;

	while(!stream.eof())
	{
		wstring tempstr2;

		readLine(tempstr2);
		tempstr << tempstr2.c_str();
		if(includeLineEndings)
			tempstr << L'\n';
	}

	text = tempstr.str();
}

//------------------------------------------------------------------------------
bool UTF8File::readLine(wstring& text)
{
	char buffer[16384];
	const int bufferSize = 16384;

	if(!stream.is_open())
		initStream(true);

	stream.getline(buffer, bufferSize);

	charToWstring(buffer, text);

	return stream.eof();
}

//------------------------------------------------------------------------------
void UTF8File::writeText(const wstring& text, bool flushAfter, bool noDebug)
{
	string tempstr;

	if(!stream.is_open())
		initStream(false);

	wstringToChar(text, tempstr);

	stream << tempstr;
	if(flushAfter)
		stream.flush();
}

//------------------------------------------------------------------------------
UTF8File& operator<<(UTF8File& out, const std::wstring& text)
{
	out.writeText(text);

	return out;
}

//------------------------------------------------------------------------------
void UTF8File::charToWstring(const string& in, wstring& out)
{
#ifdef WIN32
	int numChars;

	numChars = MultiByteToWideChar(CP_UTF8, 0, in.c_str(), -1, NULL, 0);

	if(numChars > 0)
	{
		out.resize(numChars);

		MultiByteToWideChar(CP_UTF8,
							0,
							in.c_str(),
							-1,
							const_cast<wchar_t *>(out.c_str()),
							numChars);

		//This function adds a null char to the end of the string, so we need
		//to get rid of it.
		out = out.substr(0, numChars-1);
	}
	else
		DebugStatement(L"UTF8File::charToWstring(): string is empty.");
#else
	locale loc("");
	wstring tempstr;
	const char* narrowPointer;
	W2CConvert::result result;
	wchar_t* widePointer;
	mbstate_t state = mbstate_t();
	
	LocaleSingleton::setLocale();

	const W2CConvert& fac = use_facet<W2CConvert>(loc);

	size_t length = in.length();

	tempstr.reserve(length + 1);

	wchar_t *tempW = const_cast<wchar_t *>(tempstr.c_str());

	result = fac.in(state,
 					in.c_str(), 
 					in.c_str() + length + 1, 
 					narrowPointer,
 					tempW, 
 					tempW + length + 1,
 					widePointer);
 	switch(result)
 	{
 		case W2CConvert::ok:
 			out.assign(tempW, widePointer);
 			out = out.substr(0, out.length()-1);
 			break;
 		case W2CConvert::partial:
 			out.assign(tempW, widePointer);
 			out = out.substr(0, out.length()-1);
 			DebugStatement(L"UTF8File::wstringToChar() warning: partial conversion; output string = " << out);
 			break;
 		case W2CConvert::error:
 			DebugStatement(L"UTF8File::wstringToChar() error: could not convert string; output string = " << out);
 			break;
 		case W2CConvert::noconv:
 			DebugStatement(L"UTF8File::wstringToChar() warning: string types were identical (how on earth did you manage that?); output string = " << out);
 			break;
	}
#endif
}

//------------------------------------------------------------------------------
void UTF8File::wstringToChar(const wstring& in, string& out, bool noDebug)
{
#ifdef WIN32
	int numChars;
	string outBuffer;
	const wchar_t *inBuffer = in.c_str();

	//Get the number of characters in the text.
	numChars = WideCharToMultiByte(CP_UTF8,
								   0,
								   inBuffer,
								   (int)in.size(),
								   NULL,
								   0,
								   NULL,
								   NULL);

	if(numChars > 0)
	{
		//Make sure we've got space for the converted text.
		out.resize(numChars);

		WideCharToMultiByte(CP_UTF8,
							0,
							inBuffer,
							(int)in.size(),
							const_cast<char *>(out.c_str()),
							numChars,
							NULL,
							NULL);
	}
	else if(!noDebug)
		DebugStatement(L"UTF8File: wstringToChar(); string is empty");
#else
	locale loc("");
	string tempstr;
	char* narrowPointer;
	W2CConvert::result result;
	const wchar_t* widePointer;
	mbstate_t state = mbstate_t();
	
	LocaleSingleton::setLocale();

	const W2CConvert& fac = use_facet<W2CConvert>(loc);

	size_t length = in.length() * fac.max_length();

	tempstr.reserve(length + 1);

	char *tempC = const_cast<char *>(tempstr.c_str());

	result = fac.out(state,
 					 in.c_str(), 
 					 in.c_str() + in.length() + 1, 
 					 widePointer,
 					 tempC, 
 					 tempC + length + 1,
 					 narrowPointer);
 	switch(result)
 	{
 		case W2CConvert::ok:
 			out.assign(tempC, narrowPointer);
 			out = out.substr(0, out.length()-1);
 			break;
 		case W2CConvert::partial:
 			out.assign(tempC, narrowPointer);
 			out = out.substr(0, out.length()-1);
			if(!noDebug)
 				DebugStatement(L"UTF8File::wstringToChar() warning: partial conversion; input string = " << in);
 			break;
 		case W2CConvert::error:
 			if(!noDebug)
 				DebugStatement(L"UTF8File::wstringToChar() error: could not convert string; input string = " << in);
 			break;
 		case W2CConvert::noconv:
 			if(!noDebug)
 				DebugStatement(L"UTF8File::wstringToChar() warning: string types were identical (how on earth did you manage that?); input string = " << in);
 			break;
	}
#endif
}

//------------------------------------------------------------------------------
void UTF8File::initStream(bool read)
{
	if(failStream)
		return;

	checkParentDir();

#ifdef WIN32
	int numChars;
	string outBuffer;
	const wchar_t *inBuffer = filePath.getPath().c_str();

	//Get the number of characters in the filepath.
	numChars = WideCharToMultiByte(CP_UTF8,
								   0,
								   inBuffer,
								   (int)filePath.getPath().size(),
								   NULL,
								   0,
								   NULL,
								   NULL);

	if(numChars > 0)
	{
		//Make sure we've got space for the converted text.
		outBuffer.resize(numChars);

		WideCharToMultiByte(CP_UTF8,
							0,
							inBuffer,
							(int)filePath.getPath().size(),
							const_cast<char *>(outBuffer.c_str()),
							numChars,
							NULL,
							NULL);

		if(read)
		{
			char bom[3];

			stream.open(outBuffer.c_str(), ios::in);

			//Skip the BOM.
			stream.read(bom, 3);
			if(!((bom[0] == char(0xEF)) && (bom[1] == char(0xBB)) && (bom[2] == char(0xBF))))
				stream.seekg(0);
		}
		else
		{
			stream.open(outBuffer.c_str(), ios::out|ios::trunc);

			//Write the BOM.
			stream << char(0xEF) << char(0xBB) << char(0xBF);
		}
		linesRead = 0;
	}
	else
	{
		failStream = true;
		DebugStatement(L"UTF8File: initStream(), filePath is empty");
	}
	
#else
	string tempPath;

	LocaleSingleton::setLocale();

	//Necessary?
	wstringToChar(filePath.getPath(), tempPath);

	if(read)
	{
		char bom[3];

		stream.open(tempPath.c_str(), ios::in);

		//Skip the BOM.
		stream.read(bom, 3);
		if(!((bom[0] == char(0xEF)) && (bom[1] == char(0xBB)) && (bom[2] == char(0xBF))))
			stream.seekg(0);
	}
	else
	{
		stream.open(tempPath.c_str(), ios::out|ios::trunc);

		//Write the BOM.
		stream << char(0xEF) << char(0xBB) << char(0xBF);
	}
#endif
	if(stream.fail())
	{
		failStream = true;
		DebugStatement(L"UTF8File: Could not open file: " << filePath.getPath().c_str());
	}
}

//------------------------------------------------------------------------------
void UTF8File::checkParentDir()
{
	if(!filePath.getParent().exists())
	{
		DebugStatement(L"UTF8File: !filePath.getParent().exists()");

		if(!filePath.getParent().createAsDirectory())
		{
			DebugStatement(L"UTF8File: checkParentDir() could not create parent dir: " << filePath.getParent().getPath().c_str());
		}
		else
			DebugStatement(L"UTF8File: Created parent dir: " << filePath.getPath());
	}
}

//------------------------------------------------------------------------------
void UTF8File::LocaleSingleton::setLocale()
{
	static LocaleSingleton instance;
}

//------------------------------------------------------------------------------
UTF8File::LocaleSingleton::LocaleSingleton()
{
#ifdef OSX
	setlocale(LC_ALL, "en_us.UTF-8");
#endif
}

