#include "StdAfx.h"
#include "WinFile.h"
#include <math.h>
#include <locale>


CWinFile::CWinFile(const CString &strName, FILE_CHARSET nManualCharSet)
	: CMyFile(strName)
	, m_nManualCharset(nManualCharSet)
{
	m_nTabWidth = 4;
}


CWinFile::~CWinFile(void)
{
	Close();
}

//выход: TRUE - Файл открылся, FALSE - не открылся.
BOOL CWinFile::Open(BOOL bWrite)
{
	BOOL bRet = FALSE;
	CString strMode;

	if (!bWrite)
	{
		FILE_CHARSET nDetectedCharset = AnalyseCharset();

		//если кодировку вручную не задавали
		if (m_nManualCharset == CWinFile::CHARSET_UNDEFINE)
		{
			//то будем использовать автоопределённую.
			m_nManualCharset = nDetectedCharset;
		}
		else if (m_nManualCharset == CWinFile::CHARSET_UTF8 && nDetectedCharset == CWinFile::CHARSET_UNICODE)
		{
			m_nManualCharset = nDetectedCharset; //уточняем юникод
		}
		else if (m_nManualCharset == CWinFile::CHARSET_UNICODE && nDetectedCharset == CWinFile::CHARSET_UTF8)
		{
			m_nManualCharset = nDetectedCharset; //уточняем юникод
		}

		_tprintf(_T("Автоопределение кодировки: %s, преобразование будет из: %s\n"), GetFormatName(nDetectedCharset).GetBuffer(), GetFormatName(m_nManualCharset).GetBuffer());
		//а иначе считаем, что пользователю лучше знать, какую он использует кодировку.
		strMode = _T("r");

		switch (m_nManualCharset)
		{
			case CHARSET_KOI8:
				strMode += _T("b"); //при открытии в текстовом режиме, если конец строки не \r\n, начинаются феерические глюки с определением конца файла и позиционированием.
				break;

			case CHARSET_CP866:
				strMode += _T("b");
				break;

			case CHARSET_CP1251:
				strMode += _T("b");
				break;

			case CHARSET_UTF8:
				strMode += _T("b");
				break;

			case CHARSET_UNICODE:
				strMode += _T("b, ccs=UNICODE");
				break;
		}
	}
	else
	{
		if (m_nManualCharset == CWinFile::CHARSET_UNDEFINE)
		{
			//то будем использовать по-умолчанию юникод
			m_nManualCharset = CWinFile::CHARSET_UNICODE;
		}

		strMode = _T("w");

		switch (m_nManualCharset)
		{
			case CHARSET_KOI8:
				strMode += _T("b");
				break;

			case CHARSET_CP866:
				strMode += _T("t");
				break;

			case CHARSET_CP1251:
				strMode += _T("t");
				break;

			case CHARSET_UTF8:
				strMode += _T("t, ccs=UTF-8");
				break;

			case CHARSET_UNICODE:
				strMode += _T("t, ccs=UNICODE");
				break;
		}
	}

	m_f = _tfopen(m_strFileName, strMode);

	if (m_f)  //если файл открылся, то всё нормально
	{
		if (!bWrite)
		{
			fseek(m_f, 0, SEEK_END);
			m_nFileLength = ftell(m_f);
			fseek(m_f, 0, SEEK_SET);
			BYTE BOM[3];

			if (m_nManualCharset == CHARSET_UNICODE)
			{
				//сперва поищем BOM
				fread(BOM, 1, 2, m_f);

				if (BOM[0] == 0xFF && BOM[1] == 0xFE)
				{
				}
				else
				{
					fseek(m_f, 0, SEEK_SET); //файл оказался без БОМ
				}
			}
			else if (m_nManualCharset == CHARSET_UTF8)
			{
				fread(BOM, 1, 3, m_f);

				if (BOM[0] == 0xEF && BOM[1] == 0xBB && BOM[2] == 0xBF)
				{
				}
				else
				{
					fseek(m_f, 0, SEEK_SET); //файл оказался без БОМ
				}
			}
		}

		bRet = TRUE;
	}

	return bRet;
}


WCHAR CWinFile::ReadChar()
{
	WCHAR tch = 0;
	BYTE ch[2] = {0};
	WCHAR wch[2] = {0};

	if (m_f)
	{
		switch (m_nManualCharset)
		{
			case CHARSET_KOI8:
				fread(&ch[0], sizeof(BYTE), 1, m_f);

				if (ch[0] < 128)
				{
					tch = (WCHAR)ch[0];
				}
				else
				{
					tch = m_koi8tbl[ch[0] - 128];
				}

				break;

			case CHARSET_CP866:
				fread(&ch[0], sizeof(BYTE), 1, m_f);
				MultiByteToWideChar(CP_OEMCP, 0, reinterpret_cast<LPCSTR>(ch), 1, reinterpret_cast<LPWSTR>(wch), 1);
				tch = wch[0];
				break;

			case CHARSET_CP1251:
				fread(&ch[0], sizeof(BYTE), 1, m_f);
				MultiByteToWideChar(CP_ACP, 0, reinterpret_cast<LPCSTR>(ch), 1, reinterpret_cast<LPWSTR>(wch), 1);
				tch = wch[0];
				break;

			case CHARSET_UTF8:
				tch = ReadUTF8Char();
				break;

			case CHARSET_UNICODE:
				tch = (WCHAR)fgetwc(m_f); //эта функция очень плохо относится к символу конца строки, особенно если это \n вместо \r\n
				break;

			default:
				ASSERT(FALSE);
		}
	}

	return tch;
}

BOOL CWinFile::WriteChar(WCHAR tch)
{
	BYTE ch[2] = {0};
	WCHAR wch[2] = {0};
	size_t n = 0;

	if (m_f)
	{
		wch[0] = tch;

		switch (m_nManualCharset)
		{
			case CHARSET_KOI8:
				ch[0] = UNICODEtoBK_Byte(tch);
				n = fwrite(&ch[0], sizeof(BYTE), 1, m_f);
				break;

			case CHARSET_CP866:
				WideCharToMultiByte(CP_OEMCP, 0, reinterpret_cast<LPCWSTR>(wch), 1, reinterpret_cast<LPSTR>(ch), 1, NULL, NULL);
				n = fputc(ch[0], m_f);
				break;

			case CHARSET_CP1251:
				WideCharToMultiByte(CP_ACP, 0, reinterpret_cast<LPCWSTR>(wch), 1, reinterpret_cast<LPSTR>(ch), 1, NULL, NULL);
				n = fputc(ch[0], m_f);
				break;

			case CHARSET_UTF8:
			case CHARSET_UNICODE:
				n = fputwc(tch, m_f);
				break;

			default:
				ASSERT(FALSE);
		}
	}

	return (n != 0);
}


CWinFile::FILE_CHARSET CWinFile::AnalyseCharset()
{
	FILE_CHARSET nRet = CHARSET_KOI8; //если все наши попытки распознать кодировку будут неудачны, по умолчанию считаем, что должно быть КОИ8
	BYTE BOM[3];

	if (m_f)
	{
		//если файл был открыт, то его надо закрыть.
		fclose(m_f);
	}

	//открываем файл в бинарном режиме
	m_f = _tfopen(m_strFileName, _T("rb"));
	//сперва поищем BOM
	fread(BOM, 1, 3, m_f);

	if (BOM[0] == 0xFF && BOM[1] == 0xFE)
	{
		nRet = CHARSET_UNICODE;
	}
	else if (BOM[0] == 0xEF && BOM[1] == 0xBB && BOM[2] == 0xBF)
	{
		//утф8 без бом будет определяться некорректно возможно
		nRet = CHARSET_UTF8;
	}
	else
	{
		//дальше надо анализировать кодировку.
		fseek(m_f, 0, SEEK_SET);
		double nCodes80_9F = 0.0; //тут рус загл в CP866 или тут русские буквы с префиксом D1 в UTF8
		double nCodesA0_AF = 0.0; //тут рус стр в CP866
		double nCodesB0_CF = 0.0; //тут русские буквы с префиксом D0 в UTF8
		double nCodesA0_BF = 0.0; //тут почти ничего не должно быть для KOI8 и CP1251
		double nCodesC0_DF = 0.0; //тут рус загл в CP1251 или рус стр в KOI8
		double nCodesE0_EF = 0.0; //тут рус стр в CP866
		double nCodesE0_FF = 0.0; //тут рус стр в CP1251 или рус загл в KOI8
		double nD0 = 0.0;
		double nD1 = 0.0;
		size_t nLen = 0;
		BYTE ch = 0;

		while (!feof(m_f))
		{
			nLen += fread(&ch, 1, 1, m_f);

			//сперва непересекающиеся интервалы
			if (0x80 <= ch && ch <= 0x9f)
			{
				nCodes80_9F++;
			}
			else if (0xa0 <= ch && ch <= 0xbf)
			{
				nCodesA0_BF++;
			}
			else if (0xc0 <= ch && ch <= 0xdf)
			{
				nCodesC0_DF++;
			}
			else if (0xe0 <= ch && ch <= 0xff)
			{
				nCodesE0_FF++;
			}

			//затем остальные
			if (ch == 0xd0)
			{
				nD0++;
			}
			else if (ch == 0xd1)
			{
				nD1++;
			}
			else if (0xa0 <= ch && ch <= 0xaf)
			{
				nCodesA0_AF++;
			}
			else if (0xb0 <= ch && ch <= 0xcf)
			{
				nCodesB0_CF++;
			}
			else if (0xe0 <= ch && ch <= 0xef)
			{
				nCodesE0_EF++;
			}
		}
		//теперь по полученным числам проверяем
		double nOnePercent = (double)nLen / 100.0;

		/*
		 * Если количество символов D1 и количество кодов из интервала 80..9f и
		 * количество символов D0 и количество кодов из интервала b0..cf
		 * примерно равны друг другу (разница в пределах 1% от размера файла),
		 * то это скорее всего UTF-8 (распознаётся только русская кодировка, остальные нам не надо.)
		*/
		if ((nD0 >= nOnePercent * 30.0) &&
		        (nD1 >= nOnePercent * 30.0) &&
		        (abs(nD1 - nCodes80_9F) < nOnePercent) &&
		        (abs(nD0 - nCodesB0_CF) < nOnePercent))
		{
			nRet = CHARSET_UTF8;
		}
		/*
		 * Если кодов D1 нет, но есть коды из интервала 80..9f
		 * а так же кодов из интервалов a0..af и e0..ef примерно в 8 раз больше
		*/
		else if ((nD1 <= nOnePercent) &&
		         (nCodes80_9F > 1.0) &&
		         (nCodesE0_EF + nCodesA0_AF > nOnePercent) &&
		         ((nCodesE0_EF + nCodesA0_AF) / nCodes80_9F > 8.0)
		        )
		{
			nRet = CHARSET_CP866;
		}
		else
		{
			if (nCodes80_9F + nCodesA0_BF < nOnePercent * 3.0)
			{
				if ((nCodesE0_FF > 1.0) && (nCodesC0_DF / nCodesE0_FF > 7.0))
				{
					nRet = CHARSET_KOI8;
				}
				else if ((nCodesC0_DF > 1.0) && (nCodesE0_FF / nCodesC0_DF > 7.0))
				{
					nRet = CHARSET_CP1251;
				}
			}
		}
	}

	fclose(m_f);
	return nRet;
}

CString CWinFile::GetFormatName(FILE_CHARSET fchr)
{
	switch (fchr)
	{
		case CHARSET_UNICODE:
			return _T("Unicode UTF-16LE");

		case CHARSET_CP1251:
			return _T("Windows 1251");

		case CHARSET_CP866:
			return _T("OEM CP-866");

		case CHARSET_KOI8:
			return _T("KOI8R");

		case CHARSET_UTF8:
			return _T("Unicode UTF-8");
	}

	return _T("Неизвестно");
}
//из-за того, что fgetpos/fsetpos хреново работают с UTF-8, будем самостоятельно читать UTF8 из бинарного файла.
WCHAR CWinFile::ReadUTF8Char()
{
	/*
	The encoding used to represent Unicode into bytes is based on rules that define how to break-up the bit-string representing an UCS into bytes.

	If an UCS fits 7 bits, its coded as 0xxxxxxx. This makes ASCII character represented by themselves
	If an UCS fits 11 bits, it is coded as 110xxxxx 10xxxxxx
	If an UCS fits 16 bits, it is coded as 1110xxxx 10xxxxxx 10xxxxxx
	If an UCS fits 21 bits, it is coded as 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
	If an UCS fits 26 bits, it is coded as 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
	If an UCS fits 31 bits, it is coded as 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
	*/
	BOOL bFail = FALSE;
	BYTE ch[8] = {0};
	WCHAR wch[2] = {0};
	int pos = 0;
	BYTE mask = 0x80;
	fread(&ch[pos++], sizeof(BYTE), 1, m_f); //читаем первый символ

	if ((ch[0] & mask) != 0)
	{
		mask >>= 1;

		while ((ch[0] & mask))
		{
			fread(&ch[pos++], sizeof(BYTE), 1, m_f);
			mask >>= 1;

			if (pos >= 7)
			{
				//значит ошибка. неверная кодировка
				bFail = TRUE;
				break;
			}
		}
	}

	if (!bFail)
	{
		MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast<LPCSTR>(ch), -1, reinterpret_cast<LPWSTR>(wch), 1);
		return wch[0];
	}
	else
	{
		return 0;
	}
}

