/*
    PIP - Platform Independent Primitives
    File
	Copyright (C) 2016  Ivan Pelipenko peri4ko@gmail.com

    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 "pifile.h"
#include "pidir.h"
#ifdef WINDOWS
#  undef S_IFDIR
#  undef S_IFREG
#  undef S_IFLNK
#  undef S_IFBLK
#  undef S_IFCHR
#  undef S_IFSOCK
#  define S_IFDIR  0x01
#  define S_IFREG  0x02
#  define S_IFLNK  0x04
#  define S_IFBLK  0x08
#  define S_IFCHR  0x10
#  define S_IFSOCK 0x20
#else
#  include <sys/stat.h>
#  include <sys/time.h>
#  include <fcntl.h>
#  include <utime.h>
#endif
#define S_IFHDN  0x40
#if defined(QNX) || defined(ANDROID)
#  define _fopen_call_     fopen
#  define _fseek_call_     fseek
#  define _ftell_call_     ftell
#  define _stat_struct_    struct stat
#  define _stat_call_      stat
#  define _stat_link_      lstat
#else
#  if defined(MAC_OS)
#    define _fopen_call_   fopen
#    define _fseek_call_   fseek
#    define _ftell_call_   ftell
#  else
#    define _fopen_call_   fopen64
#    define _fseek_call_   fseeko64
#    define _ftell_call_   ftello64
#  endif
#  define _stat_struct_    struct stat64
#  define _stat_call_      stat64
#  define _stat_link_      lstat64
#endif


/*! \class PIFile
 * \brief Local file
 * 
 * \section  PIFile_sec0 Synopsis
 * This class provide access to local file. You can manipulate
 * binary content or use this class as text stream. To binary
 * access there are function \a read(), \a write(), and many
 * \a writeBinary() functions. For write variables to file in
 * their text representation threr are many "<<" operators.
 * 
 * \section  PIFile_sec1 Position
 * Each opened file has a read/write position - logical position
 * in the file content you read from or you write to. You can
 * find out current position with function \a pos(). Function
 * \a seek(llong position) move position to position "position",
 * \a seekToBegin() move position to the begin of file,
 * \a seekToEnd() move position to the end of file.
 * 
 */

REGISTER_DEVICE(PIFile);


PIString PIFile::FileInfo::name() const {
	if (path.isEmpty()) return PIString();
	return path.mid(path.findLast(PIDir::separator) + 1);
}


PIString PIFile::FileInfo::baseName() const {
	if (path.isEmpty()) return PIString();
	PIString n = name(), e = extension();
	if (e.isEmpty()) return n;
	return n.cutRight(e.size_s() + 1);
}


PIString PIFile::FileInfo::extension() const {
	PIString n = name();
	if (n.isEmpty()) return PIString();
	while (n.startsWith("."))
		n.pop_front();
	if (n.isEmpty()) return PIString();
	int i = n.find(".");
	if (i < 0) return PIString();
	return n.mid(i + 1);
}


PIString PIFile::FileInfo::dir() const {
	if (path.isEmpty()) return PIString();
	PIString ret = path.mid(0, path.findLast(PIDir::separator));
	if (ret.isEmpty()) ret = PIDir::separator;
	return ret;
}




PIFile::PIFile(): PIIODevice() {
	fdi = -1;
	setPrecision(5);
}


PIFile::PIFile(const PIString & path, PIIODevice::DeviceMode mode): PIIODevice(path, mode) {
	fdi = -1;
	setPrecision(5);
	if (!path.isEmpty())
		openDevice();
}


PIFile::PIFile(const PIFile & other) {
	fdi = -1;
	setPrecision(other.prec_);
	setPath(other.path());
	mode_ = other.mode_;
}


bool PIFile::openDevice() {
	close();
	PIString p = path();
	if (p.isEmpty()) return false;
	//piCout << "fopen " << path_.data() << ": " << strType(mode_).data() << fd;
	if ((mode_ & PIIODevice::WriteOnly) == PIIODevice::WriteOnly) {
		if (!isExists(p)) {
			FILE * fd = fopen(p.data(), "w");
			if (fd != 0) fclose(fd);
		}
	}
	fd = _fopen_call_(p.data(), strType(mode_).data());
	opened_ = (fd != 0);
	if (opened_) {
		fdi = fileno(fd);
#ifndef WINDOWS
		fcntl(fdi, F_SETFL, O_NONBLOCK);
#endif
	seekToBegin();
	}
	//piCout << "open file" << fd << opened_;
	return opened_;
}


bool PIFile::closeDevice() {
	//piCout << "close file" << fd << opened_;
	if (!opened_ || fd == 0) return true;
	bool cs = (fclose(fd) == 0);
	if (cs) fd = 0;
	fdi = -1;
	//piCout << "closed file" << fd << opened_;
	return cs;
}


PIString PIFile::readLine() {
	PIByteArray str;
	if (!opened_) return str;
	int cc;
	while (!isEnd()) {
		cc = fgetc(fd);
		if (char(cc) == '\n' || cc == EOF) break;
		str.push_back(char(cc));
	}
	str.push_back('\0');
	if (defaultCharset()) {
		return PIString::fromCodepage((const char *)str.data(), defaultCharset());
	}
	//cout << "readline: " << str << endl;
	return PIString(str);
}


llong PIFile::readAll(void * data) {
	llong cp = pos(), s = size(), i = -1;
	seekToBegin();
	if (s < 0) {
		while (!isEnd())
			read(&(((char*)data)[++i]), 1);
	} else
		read((char * )data, s);
	seek(cp);
	return s;
}


PIByteArray PIFile::readAll(bool forceRead) {
	PIByteArray a;
	llong cp = pos();
	if (forceRead) {
		seekToBegin();
		while (!isEnd())
			a.push_back(readChar());
		seek(cp);
		return a;
	}
	llong s = size();
	if (s < 0) return a;
	a.resize(s);
	s = readAll(a.data());
	seek(cp);
	if (s >= 0) a.resize(s);
	return a;
}


llong PIFile::size() const {
	if (!opened_) return -1;
	llong s, cp = pos();
	_fseek_call_(fd, 0, SEEK_END); clearerr(fd);
	s = pos();
	_fseek_call_(fd, cp, SEEK_SET); clearerr(fd);
	return s;
}


void PIFile::resize(llong new_size, uchar fill_) {
	llong ds = new_size - size();
	if (ds == 0) return;
	if (ds > 0) {
		uchar * buff = new uchar[ds];
		memset(buff, fill_, ds);
		write(buff, ds);
		delete[] buff;
		return;
	}
	if (new_size == 0) {
		clear();
		return;
	}
	piCoutObj << "Downsize is not supported yet :-(";
}



bool PIFile::isExists(const PIString & path) {
	FILE * f = _fopen_call_(PIString(path).data(), "r");
	bool ok = (f != 0);
	if (ok) fclose(f);
	return ok;
}


bool PIFile::remove(const PIString & path) {
#ifdef WINDOWS
	if (PIDir::isExists(path))
		return RemoveDirectory(path.data()) > 0;
	else
#endif
	return ::remove(path.data()) == 0;
}


PIString PIFile::constructFullPath() const {
	return fullPathPrefix() + "://" + path();
}


void PIFile::configureFromFullPath(const PIString & full_path) {
	setPath(full_path);
}


PIString PIFile::strType(const PIIODevice::DeviceMode type) {
	switch (type) {
	case PIIODevice::ReadOnly: return "rb";
	case PIIODevice::ReadWrite:
	case PIIODevice::WriteOnly: return "r+b";
	}
	return "rb";
}


void PIFile::flush() {
	if (opened_) fflush(fd);
}


void PIFile::seek(llong position) {
	if (!opened_) return;
	_fseek_call_(fd, position, SEEK_SET);
	clearerr(fd);
}


void PIFile::seekToBegin() {
	if (!opened_) return;
	_fseek_call_(fd, 0, SEEK_SET);
	clearerr(fd);
}


void PIFile::seekToEnd() {
	if (!opened_) return;
	_fseek_call_(fd, 0, SEEK_END);
	clearerr(fd);
}


void PIFile::seekToLine(llong line) {
	if (!opened_) return;
	seekToBegin();
	piForTimes(line) readLine();
	clearerr(fd);
}


char PIFile::readChar() {
	return (char)fgetc(fd);
}


void PIFile::setPath(const PIString & path) {
	PIIODevice::setPath(path);
	if (opened_) openDevice();
}


llong PIFile::pos() const {
	if (!opened_) return -1;
	return _ftell_call_(fd);
}


bool PIFile::isEnd() const {
	if (!opened_) return true;
	return (feof(fd) || ferror(fd));
}


void PIFile::setPrecision(int prec) {
	prec_ = prec;
	if (prec_ >= 0) prec_str = "." + PIString::fromNumber(prec_);
	else prec_str = "";
}


int PIFile::read(void * read_to, int max_size) {
	if (!canRead() || fd == 0) return -1;
	return fread(read_to, 1, max_size, fd);
}


int PIFile::write(const void * data, int max_size) {
	if (!canWrite() || fd == 0) return -1;
	return fwrite(data, 1, max_size, fd);
}


PIFile & PIFile::writeToBinLog(ushort id, const void * data, int size) { /// DEPRECATED
	if (!isWriteable() || fd == 0) return *this;
	writeBinary(id).writeBinary((ushort)size);
	write(data, size);
	flush();
	return *this;
}


PIFile &PIFile::operator <<(const PIString & v) {
	if (canWrite() && fd != 0)
		*this << v.toCharset(defaultCharset());
	return *this;
}


void PIFile::clear() {
	close();
	fd = fopen(path().data(), "w");
	if (fd != 0) fclose(fd);
	fd = 0;
	opened_ = false;
	open();
}


void PIFile::remove() {
	close();
	std::remove(path().data());
}



const char * PIFile::defaultCharset() {
	return PIInit::instance()->file_charset;
}


void PIFile::setDefaultCharset(const char * c) {
	PIInit::instance()->setFileCharset(c);
}



PIFile::FileInfo PIFile::fileInfo(const PIString & path) {
	FileInfo ret;
	if (path.isEmpty()) return ret;
	ret.path = path.replaceAll("\\", PIDir::separator);
	PIString n = ret.name();
	//piCout << "open" << path;
#ifdef WINDOWS
	DWORD attr = GetFileAttributes((LPCTSTR)(path.data()));
	if (attr == 0xFFFFFFFF) return ret;
	HANDLE hFile = 0;
	if ((attr & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) {
		hFile = CreateFile(path.data(), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
	} else {
		hFile = CreateFile(path.data(), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0);
	}
	if (!hFile) return ret;
	BY_HANDLE_FILE_INFORMATION fi;
	memset(&fi, 0, sizeof(fi));
	if (GetFileInformationByHandle(hFile, &fi) != 0) {
		LARGE_INTEGER filesize;
		filesize.LowPart = filesize.HighPart = 0;
		if (fi.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) ret.flags |= FileInfo::Hidden;
		if (fi.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ret.flags |= FileInfo::Dir;
		else {
			ret.flags |= FileInfo::File;
			filesize.LowPart = fi.nFileSizeLow;
			filesize.HighPart = fi.nFileSizeHigh;
		}
		PIString ext = ret.extension();
		ret.perm_user = FileInfo::Permissions(true, (attr & FILE_ATTRIBUTE_READONLY) != FILE_ATTRIBUTE_READONLY, ext == "bat" || ext == "exe");
		ret.perm_group = ret.perm_other = ret.perm_user;
		ret.size = filesize.QuadPart;
		ret.time_access = PIDateTime(fi.ftLastAccessTime);
		ret.time_modification = PIDateTime(fi.ftLastWriteTime);
		/*PIByteArray sec;
		DWORD sec_n(0);
		//SECURITY_DESCRIPTOR sec;
		GetFileSecurity(path.data(), DACL_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION, (SECURITY_DESCRIPTOR*)sec.data(), 0, &sec_n);
		sec.resize(sec_n);
		GetFileSecurity(path.data(), DACL_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION, (SECURITY_DESCRIPTOR*)sec.data(), sec.size(), &sec_n);
		errorClear();
		SID sid; BOOL def;
		GetSecurityDescriptorGroup((PSECURITY_DESCRIPTOR)sec.data(), &sid, &def);
		char * s(0);
		ConvertSidToStringSid((PSID)&sid, s);
		piCout << s;
		LocalFree(s);
		//ret.id_user = ;*/
	}
	CloseHandle(hFile);
#else
	_stat_struct_ fs;
	memset(&fs, 0, sizeof(fs));
	_stat_call_(path.data(), &fs);
	int mode = fs.st_mode;
	ret.size = fs.st_size;
    ret.id_user = fs.st_uid;
	ret.id_group = fs.st_gid;
#ifdef ANDROID
	ret.time_access = PIDateTime::fromSystemTime(PISystemTime(fs.st_atime, fs.st_atime_nsec));
	ret.time_modification = PIDateTime::fromSystemTime(PISystemTime(fs.st_mtime, fs.st_mtime_nsec));
#else
#  ifdef QNX
	ret.time_access = PIDateTime::fromSecondSinceEpoch(fs.st_atime);
	ret.time_modification = PIDateTime::fromSecondSinceEpoch(fs.st_mtime);
#  else
#    ifdef MAC_OS
#      define ATIME st_atimespec
#      define MTIME st_ctimespec
#    else
#      define ATIME st_atim
#      define MTIME st_mtim
#    endif
	ret.time_access = PIDateTime::fromSystemTime(PISystemTime(fs.ATIME.tv_sec, fs.ATIME.tv_nsec));
	ret.time_modification = PIDateTime::fromSystemTime(PISystemTime(fs.MTIME.tv_sec, fs.MTIME.tv_nsec));
#  endif
#endif
	ret.perm_user  = FileInfo::Permissions((mode & S_IRUSR) == S_IRUSR, (mode & S_IWUSR) == S_IWUSR, (mode & S_IXUSR) == S_IXUSR);
	ret.perm_group = FileInfo::Permissions((mode & S_IRGRP) == S_IRGRP, (mode & S_IWGRP) == S_IWGRP, (mode & S_IXGRP) == S_IXGRP);
	ret.perm_other = FileInfo::Permissions((mode & S_IROTH) == S_IROTH, (mode & S_IWOTH) == S_IWOTH, (mode & S_IXOTH) == S_IXOTH);
	memset(&fs, 0, sizeof(fs));
	_stat_link_(path.data(), &fs);
	mode &= ~S_IFLNK;
	mode |= S_IFLNK & fs.st_mode;
	if (n.startsWith(".")) mode |= S_IFHDN;
	if ((mode & S_IFDIR) == S_IFDIR) ret.flags |= FileInfo::Dir;
	if ((mode & S_IFREG) == S_IFREG) ret.flags |= FileInfo::File;
	if ((mode & S_IFLNK) == S_IFLNK) ret.flags |= FileInfo::SymbolicLink;
	if ((mode & S_IFHDN) == S_IFHDN) ret.flags |= FileInfo::Hidden;
#endif
	if (n == ".") ret.flags = FileInfo::Dir | FileInfo::Dot;
	if (n == "..") ret.flags = FileInfo::Dir | FileInfo::DotDot;
	return ret;
}


bool PIFile::applyFileInfo(const PIString & path, const PIFile::FileInfo & info) {
	if (path.isEmpty()) return false;
	PIString fp(path);
	if (fp.endsWith(PIDir::separator)) fp.pop_back();
#ifdef WINDOWS
	DWORD attr = GetFileAttributes((LPCTSTR)(path.data()));
	if (attr == 0xFFFFFFFF) return false;
	attr &= ~(FILE_ATTRIBUTE_HIDDEN | FILE_ATTRIBUTE_READONLY);
	if (info.isHidden()) attr |= FILE_ATTRIBUTE_HIDDEN;
	if (!info.perm_user.write) attr |= FILE_ATTRIBUTE_READONLY;
	if (SetFileAttributes((LPCTSTR)(fp.data()), attr) == 0) {
		piCout << "[PIFile] applyFileInfo: \"SetFileAttributes\" error:" << errorString();
		//return false;
	}
	HANDLE hFile = 0;
	if ((attr & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) {
		hFile = CreateFile(path.data(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
	} else {
		hFile = CreateFile(path.data(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
	}
	if (!hFile) return false;
	FILETIME atime = info.time_access.toFILETIME(), mtime = info.time_modification.toFILETIME();
	if (SetFileTime(hFile, 0, &atime, &mtime) == 0) {
		piCout << "[PIFile] applyFileInfo: \"SetFileTime\" error:" << errorString();
		return false;
	}
	CloseHandle(hFile);
#else
	int mode(0);
	if (info.perm_user.read)   mode |= S_IRUSR;
	if (info.perm_user.write)  mode |= S_IWUSR;
	if (info.perm_user.exec)   mode |= S_IXUSR;
	if (info.perm_group.read)  mode |= S_IRGRP;
	if (info.perm_group.write) mode |= S_IWGRP;
	if (info.perm_group.exec)  mode |= S_IXGRP;
	if (info.perm_other.read)  mode |= S_IROTH;
	if (info.perm_other.write) mode |= S_IWOTH;
	if (info.perm_other.exec)  mode |= S_IXOTH;
	if (chmod(fp.data(), mode) != 0) {
		piCout << "[PIFile] applyFileInfo: \"chmod\" error:" << errorString();
		//return false;
	}
	if (chown(fp.data(), info.id_user, info.id_group) != 0) {
		piCout << "[PIFile] applyFileInfo: \"chown\" error:" << errorString();
		//return false;
	}
	struct timeval tm[2];
	PISystemTime st = info.time_access.toSystemTime();
	tm[0].tv_sec = st.seconds;
	tm[0].tv_usec = st.nanoseconds / 1000;
	st = info.time_modification.toSystemTime();
	tm[1].tv_sec = st.seconds;
	tm[1].tv_usec = st.nanoseconds / 1000;
	if (utimes(fp.data(), tm) != 0) {
		piCout << "[PIFile] applyFileInfo: \"utimes\" error:" << errorString();
		//return false;
	}
#endif
	return true;
}
