 /* The smooth Class Library
  * Copyright (C) 1998-2022 Robert Kausch <robert.kausch@gmx.net>
  *
  * This library is free software; you can redistribute it and/or
  * modify it under the terms of "The Artistic License, Version 2.0".
  *
  * THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
  * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
  * WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE. */

/* Define to get 64 bit stat call.
 */
#ifndef __WIN32__
#	define _FILE_OFFSET_BITS 64
#endif

#include <smooth/files/file.h>
#include <smooth/files/directory.h>
#include <smooth/misc/math.h>

#ifdef __WIN32__
#	include <windows.h>
#else
#	include <stdio.h>
#	include <time.h>
#	include <utime.h>
#	include <sys/stat.h>
#endif

namespace smooth
{
#ifdef __WIN32__
	typedef FILETIME FileTime;

	DateTime FileTimeToDateTime(const FileTime &fileTime)
	{
		SYSTEMTIME	 utc, time;

		FileTimeToSystemTime(&fileTime, &utc);
		SystemTimeToTzSpecificLocalTime(NIL, &utc, &time);

		DateTime	 dateTime;

		dateTime.SetYMD(time.wYear, time.wMonth, time.wDay);
		dateTime.SetHMS(time.wHour, time.wMinute, time.wSecond);

		return dateTime;
	}

	FileTime DateTimeToFileTime(const DateTime &dateTime)
	{
		SYSTEMTIME	 time, utc;

		memset(&time, 0, sizeof(time));

		time.wYear   = dateTime.GetYear();
		time.wMonth  = dateTime.GetMonth();
		time.wDay    = dateTime.GetDay();

		time.wHour   = dateTime.GetHour();
		time.wMinute = dateTime.GetMinute();
		time.wSecond = dateTime.GetSecond();

		FileTime	 fileTime;

		TzSpecificLocalTimeToSystemTime(NIL, &time, &utc);
		SystemTimeToFileTime(&utc, &fileTime);

		return fileTime;
	}

	Bool GetFileTime(const File &file, FileTime *cTime, FileTime *aTime, FileTime *wTime)
	{
		if (!file.Exists()) return False;

		HANDLE	 handle = CreateFile(Directory::MakeExtendedPath(file), GENERIC_READ, FILE_SHARE_READ, NIL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NIL);

		::GetFileTime(handle, cTime, aTime, wTime);

		CloseHandle(handle);

		return True;
	}

	Bool SetFileTime(const File &file, FileTime *cTime, FileTime *aTime, FileTime *wTime)
	{
		if (!file.Exists()) return False;

		HANDLE	 handle = CreateFile(Directory::MakeExtendedPath(file), GENERIC_WRITE, 0, NIL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NIL);

		::SetFileTime(handle, cTime, aTime, wTime);

		CloseHandle(handle);

		return True;
	}
#else
	typedef time_t FileTime;

	DateTime FileTimeToDateTime(const FileTime &fileTime)
	{
		tm	 time;

		localtime_r(&fileTime, &time);

		DateTime	 dateTime;

		dateTime.SetYMD(time.tm_year + 1900, time.tm_mon + 1, time.tm_mday);
		dateTime.SetHMS(time.tm_hour, time.tm_min, time.tm_sec);

		return dateTime;
	}

	FileTime DateTimeToFileTime(const DateTime &dateTime)
	{
		tm	 time;

		memset(&time, 0, sizeof(time));

		time.tm_year  = dateTime.GetYear() - 1900;
		time.tm_mon   = dateTime.GetMonth() - 1;
		time.tm_mday  = dateTime.GetDay();

		time.tm_hour  = dateTime.GetHour();
		time.tm_min   = dateTime.GetMinute();
		time.tm_sec   = dateTime.GetSecond();

		time.tm_isdst = -1;

		return mktime(&time);
	}

	Bool GetFileTime(const File &file, FileTime *cTime, FileTime *aTime, FileTime *wTime)
	{
		if (!file.Exists()) return False;

		struct stat	 info;

		if (stat(String(file).ConvertTo("UTF-8"), &info) != 0) return False;

		if (cTime != NIL) *cTime = info.st_mtime;
		if (aTime != NIL) *aTime = info.st_atime;
		if (wTime != NIL) *wTime = info.st_mtime;

		return True;
	}

	Bool SetFileTime(const File &file, FileTime *cTime, FileTime *aTime, FileTime *wTime)
	{
		if (!file.Exists()) return False;

		struct stat	 info;
		struct utimbuf	 times;

		if (stat(String(file).ConvertTo("UTF-8"), &info) != 0) return False;

		times.actime  = info.st_atime;
		times.modtime = info.st_mtime;

		if (cTime != NIL) times.modtime = *cTime;
		if (aTime != NIL) times.actime  = *aTime;
		if (wTime != NIL) times.modtime = *wTime;

		if (utime(String(file).ConvertTo("UTF-8"), &times) != 0) return False;

		return True;
	}
#endif
};

static const S::String	&kDelimiter   = S::Directory::GetDirectoryDelimiter();

static const S::String	 kSlash	      = "/";
static const S::String	 kBackslash   = "\\";
static const S::String	 kUncPrefix   = "\\\\";
static const S::String	 kDot	      = ".";
static const S::String	 kDots	      = "..";
static const S::String	 kTilde	      = "~";

static const S::String	 kDotElement  = S::String(kDelimiter).Append(kDot).Append(kDelimiter);
static const S::String	 kDotsElement = S::String(kDelimiter).Append(kDots).Append(kDelimiter);

S::File::File()
{
}

S::File::File(const String &iFileName, const String &iFilePath)
{
	fileName = iFileName;
	filePath = iFilePath;

#ifdef __WIN32__
	if (fileName.Contains(kSlash)) fileName.Replace(kSlash, kDelimiter);
	if (filePath.Contains(kSlash)) filePath.Replace(kSlash, kDelimiter);
#else
	if (fileName.Contains(kBackslash)) fileName.Replace(kBackslash, kDelimiter);
	if (filePath.Contains(kBackslash)) filePath.Replace(kBackslash, kDelimiter);
#endif

	if (fileName != NIL && filePath == NIL)
	{
#ifdef __WIN32__
		if (fileName.StartsWith(kDelimiter) && !fileName.StartsWith(kUncPrefix)) fileName = String(Directory::GetActiveDirectory()).Head(2).Append(fileName);
#endif

#ifdef __WIN32__
		if (fileName[1] == ':' || fileName.StartsWith(kUncPrefix))
#else
		if (fileName.StartsWith(kDelimiter) || fileName.StartsWith(kTilde))
#endif
		{
			filePath = fileName;
			fileName = NIL;
		}
		else
		{
			filePath = String(Directory::GetActiveDirectory()).Append(kDelimiter).Append(fileName);
			fileName = NIL;
		}
	}

	if (fileName == NIL)
	{
		Int	 lastBS = filePath.FindLast(kDelimiter);

		fileName = filePath.Tail(filePath.Length() - lastBS - 1);
		filePath[lastBS >= 0 ? lastBS : 0] = 0;
	}

	/* Replace ./ elements.
	 */
	if (!filePath.EndsWith(kDelimiter)) filePath.Append(kDelimiter);

	if (filePath.Contains(kDotElement)) filePath.Replace(kDotElement, kDelimiter);

	if (filePath.StartsWith(String(kDot).Append(kDelimiter))) filePath = String(Directory::GetActiveDirectory()).Append(filePath.Tail(filePath.Length() - 2));

	/* Replace ../ elements.
	 */
	while (filePath.Contains(kDotsElement))
	{
		Int	 upPos	= filePath.Find(kDotsElement);
		Int	 prePos	= filePath.Head(upPos).FindLast(kDelimiter);

		filePath.Replace(filePath.SubString(prePos, upPos - prePos + 3), String());
	}

	if (filePath.EndsWith(kDelimiter)) filePath[filePath.Length() - 1] = 0;
}

S::File::File(const File &iFile)
{
	fileName = iFile.fileName;
	filePath = iFile.filePath;
}

S::File::~File()
{
}

S::File::operator S::String() const
{
	return String(filePath).Append(kDelimiter).Append(fileName);
}

const S::String &S::File::GetFileName() const
{
	return fileName;
}

const S::String &S::File::GetFilePath() const
{
	return filePath;
}

S::Int64 S::File::GetFileSize() const
{
	if (!Exists()) return -1;

#ifdef __WIN32__
	HANDLE	 handle	  = CreateFile(Directory::MakeExtendedPath(*this), GENERIC_READ, FILE_SHARE_READ, NIL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NIL);

	DWORD	 sizeLow  = 0;
	DWORD	 sizeHigh = 0;

	sizeLow = ::GetFileSize(handle, &sizeHigh);

	CloseHandle(handle);

	return (Int64(sizeHigh) << 32) + sizeLow;
#else
	struct stat	 info;

	if (stat(String(*this).ConvertTo("UTF-8"), &info) != 0) return 0;

	return info.st_size;
#endif
}

S::DateTime S::File::GetCreationTime() const
{
	FileTime	 fileTime;

	if (!GetFileTime(*this, &fileTime, NIL, NIL)) return DateTime();

	return FileTimeToDateTime(fileTime);
}

S::DateTime S::File::GetAccessTime() const
{
	FileTime	 fileTime;

	if (!GetFileTime(*this, NIL, &fileTime, NIL)) return DateTime();

	return FileTimeToDateTime(fileTime);
}

S::DateTime S::File::GetWriteTime() const
{
	FileTime	 fileTime;

	if (!GetFileTime(*this, NIL, NIL, &fileTime)) return DateTime();

	return FileTimeToDateTime(fileTime);
}

S::Bool S::File::SetAccessTime(const DateTime &dateTime)
{
	FileTime	 fileTime = DateTimeToFileTime(dateTime);

	return SetFileTime(*this, NIL, &fileTime, NIL);
}

S::Bool S::File::SetWriteTime(const DateTime &dateTime)
{
	FileTime	 fileTime = DateTimeToFileTime(dateTime);

	return SetFileTime(*this, NIL, NIL, &fileTime);
}

S::Bool S::File::Exists() const
{
#ifdef __WIN32__
	WIN32_FIND_DATA	 findData;
	HANDLE		 handle = FindFirstFile(Directory::MakeExtendedPath(*this), &findData);

	if (handle == INVALID_HANDLE_VALUE) return False;

	FindClose(handle);

	if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) return False;
#else
	struct stat	 info;

	if (stat(String(*this).ConvertTo("UTF-8"), &info) != 0) return False;

	if (!S_ISREG(info.st_mode)) return False;
#endif

	return True;
}

S::Int S::File::Create()
{
	if (Exists()) return Error();

#ifdef __WIN32__
	HANDLE	 handle = CreateFile(Directory::MakeExtendedPath(*this), GENERIC_READ, FILE_SHARE_READ, NIL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NIL);

	if (handle == INVALID_HANDLE_VALUE) return Error();

	CloseHandle(handle);
#endif

	return Success();
}

S::Int S::File::Copy(const File &destination)
{
	if (!Exists()) return Error();

#ifdef __WIN32__
	Bool	 result = CopyFile(Directory::MakeExtendedPath(*this), Directory::MakeExtendedPath(destination), True);
#else
	Bool	 result = False;

	FILE	*source	= fopen(String(*this).ConvertTo("UTF-8"), "rb");
	FILE	*dest	= fopen(String(destination).ConvertTo("UTF-8"), "wb");

	if (source != NIL && dest != NIL)
	{
		Int	 bytesLeft = GetFileSize();
		Int	 chunkSize = 32768;

		UnsignedByte	*buffer = new UnsignedByte [chunkSize];

		while (bytesLeft)
		{
			chunkSize = Math::Min(chunkSize, bytesLeft);

			if (fread(buffer, chunkSize, 1, source) == 1) fwrite(buffer, chunkSize, 1, dest);
			else					      break;

			bytesLeft -= chunkSize;
		}

		delete [] buffer;

		if (!bytesLeft) result = True;
	}

	if (source != NIL) fclose(source);
	if (dest != NIL) fclose(dest);
#endif

	if (result == False) return Error();
	else		     return Success();
}

S::Int S::File::Move(const File &destination)
{
	if (!Exists()) return Error();

#ifdef __WIN32__
	String		 fileName	= Directory::MakeExtendedPath(*this);
	UnsignedInt	 fileAttributes	= GetFileAttributes(fileName);

	SetFileAttributes(fileName, fileAttributes & ~FILE_ATTRIBUTE_READONLY);

	Bool	 result = MoveFile(fileName, Directory::MakeExtendedPath(destination));

	SetFileAttributes(fileName, fileAttributes);
#else
	Bool	 result = (rename(String(*this).ConvertTo("UTF-8"), String(destination).ConvertTo("UTF-8")) == 0);
#endif

	if (result == False) return Error();
	else		     return Success();
}

S::Int S::File::Delete()
{
	if (!Exists()) return Error();

#ifdef __WIN32__
	String	 fileName = Directory::MakeExtendedPath(*this);

	SetFileAttributes(fileName, GetFileAttributes(fileName) & ~FILE_ATTRIBUTE_READONLY);

	Bool	 result = DeleteFile(fileName);
#else
	Bool	 result = (remove(String(*this).ConvertTo("UTF-8")) == 0);
#endif

	if (result == False) return Error();
	else		     return Success();
}

S::Int S::File::Truncate()
{
	if (!Exists()) return Error();

	if (Delete() == Error()) return Error();

	return Create();
}
