/*
    PIP - Platform Independent Primitives
    File
    Ivan Pelipenko peri4ko@yandex.ru

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Lesser 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 Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "pifile.h"

#include "pidir.h"
#include "piincludes_p.h"
#include "piiostream.h"
#include "piliterals_bytes.h"
#include "pitime_win.h"
#include "pitranslator.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 <fcntl.h>
#  include <sys/stat.h>
#  include <sys/time.h>
#  include <utime.h>
#endif
#define S_IFHDN 0x40
#if defined(QNX) || defined(ANDROID) || defined(FREERTOS)
#  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
#	ifdef CC_GCC
#	  define _fopen_call_ fopen64
#	  define _fseek_call_ fseeko64
#	  define _ftell_call_ ftello64
#	else
#	  define _fopen_call_ fopen
#	  define _fseek_call_ fseek
#	  define _ftell_call_ ftell
#	endif
#  endif
#  define _stat_struct_ struct stat64
#  define _stat_call_   stat64
#  define _stat_link_   lstat64
#endif


//! \class PIFile pifile.h
//! \details
//! \~english \section PIFile_sec0 Synopsis
//! \~russian \section PIFile_sec0 Краткий обзор
//! \~english
//!
//! 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 and read variables to file in
//! their text representation there are many "<<" and ">>" operators.
//!
//! \~russian
//! Этот класс предоставляет доступ к локальному файлу. Можно
//! работать на байтовом уровне, либо использовать его как
//! текстовый поток. Для байтового доступа используются методы
//! \a read(), \a write(), и много \a writeBinary() методов.
//! Для записи и чтения переменных в текстовом представлении
//! используются операторы "<<" и ">>".
//!
//! \~english \section PIFile_sec1 Position
//! \~russian \section PIFile_sec1 Позиция
//! \~english
//! 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.
//!
//! \~russian
//! Каждый файл имеет позицию чтения/записи - логическое положение
//! в содержимом файла, откуда производится чтение или запись.
//! Узнать текущую позицию можно с помощью метода \a pos().
//! Метод \a seek(llong position) перемещает позицию на указанную,
//! \a seekToBegin() перемещает её в начало файла, а \a seekToEnd() - в конец.
//!

REGISTER_DEVICE(PIFile)

PRIVATE_DEFINITION_START(PIFile)
	FILE * fd = nullptr;
PRIVATE_DEFINITION_END(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.findLast(".");
	if (i < 0) return PIString();
	return n.mid(i + 1);
}


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


bool PIFile::FileInfo::isAbsolute() const {
	if (path.isEmpty()) return false;
	if (path[0] == '/') return true;
	if (path.size() >= 2) {
		if (path[0].isAlpha() && path[1] == ':') return true;
	}
	return false;
}


PIFile::PIFile(): PIIODevice() {}


PIFile::PIFile(const PIString & path, PIIODevice::DeviceMode mode): PIIODevice(path, mode) {
	if (!path.isEmpty()) open();
}


bool PIFile::openTemporary(PIIODevice::DeviceMode mode) {
	PIString tp;
#ifdef WINDOWS
	tp = PIDir::temporary().path() + PIDir::separator + "file" + PIString::fromNumber(randomi());
	while (isExists(tp)) {
		tp += PIString::fromNumber(randomi() % 10);
	}
#else
	char template_rc[] = "/tmp/pifile_tmp_XXXXXX";
	int fd             = mkstemp(template_rc);
	if (fd == -1) return false;
	::close(fd);
	tp = template_rc;
#endif
	return open(tp, mode);
}


PIFile::~PIFile() {
	stop();
	close();
}


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


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


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() {
	if (!isOpened()) return {};
	llong prev_pos = pos();
	PIByteArray ret, buffer(4_KiB);
	seekToBegin();
	for (;;) {
		size_t readed = fread(buffer.data(), 1, buffer.size(), PRIVATE->fd);
		if (readed == 0) break;
		ret.append(buffer.data(), readed);
	}
	seek(prev_pos);
	return ret;
}


llong PIFile::size() const {
	if (isClosed()) return -1;
	llong s, cp = pos();
	_fseek_call_(PRIVATE->fd, 0, SEEK_END);
	clearerr(PRIVATE->fd);
	s = pos();
	_fseek_call_(PRIVATE->fd, cp, SEEK_SET);
	clearerr(PRIVATE->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 :-("_tr("PIFile");
}


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 RemoveDirectoryA(path.data()) > 0;
	else
#endif
		return ::remove(path.data()) == 0;
}


bool PIFile::rename(const PIString & from, const PIString & to) {
	return ::rename(from.data(), to.data()) == 0;
}


PIString PIFile::constructFullPathDevice() const {
	return path();
}


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


PIPropertyStorage PIFile::constructVariantDevice() const {
	PIPropertyStorage ret;
	PIVariantTypes::File f;
	f.file = path();
	ret.addProperty("path", f);
	return ret;
}


void PIFile::configureFromVariantDevice(const PIPropertyStorage & d) {
	setPath(d.propertyValueByName("path").toString());
}


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 (isOpened()) fflush(PRIVATE->fd);
}


void PIFile::seek(llong position) {
	if (isClosed()) return;
	if (position == pos()) return;
	_fseek_call_(PRIVATE->fd, position, SEEK_SET);
	clearerr(PRIVATE->fd);
}


void PIFile::seekToBegin() {
	if (isClosed()) return;
	_fseek_call_(PRIVATE->fd, 0, SEEK_SET);
	clearerr(PRIVATE->fd);
}


void PIFile::seekToEnd() {
	if (isClosed()) return;
	_fseek_call_(PRIVATE->fd, 0, SEEK_END);
	clearerr(PRIVATE->fd);
}


void PIFile::seekToLine(llong line) {
	if (isClosed()) return;
	seekToBegin();
	PIIOTextStream ts(this);
	piForTimes(line) ts.readLine();
	clearerr(PRIVATE->fd);
}


void PIFile::skip(llong bytes) {
	if (isClosed() || (bytes == 0)) return;
	_fseek_call_(PRIVATE->fd, bytes, SEEK_CUR);
}


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


void PIFile::setPath(const PIString & path) {
	PIIODevice::setPath(path);
	if (isOpened()) open();
}


llong PIFile::pos() const {
	if (isClosed()) return -1;
	return _ftell_call_(PRIVATE->fd);
}


bool PIFile::isEnd() const {
	if (isClosed()) return true;
	bool ret = (feof(PRIVATE->fd) || ferror(PRIVATE->fd));
	if (!ret && (_size >= 0)) {
		if (pos() > _size) ret = true;
	}
	return ret;
}


PIFile & PIFile::put(const PIByteArray & v) {
	int sz = (int)v.size_s();
	write(createMemoryBlock(&sz));
	write(v);
	return *this;
}


PIByteArray PIFile::get() {
	PIByteArray ret;
	int sz(0);
	read(createMemoryBlock(&sz));
	if (sz > 0) {
		ret.resize(sz);
		read(ret.data(), sz);
	}
	return ret;
}


ssize_t PIFile::readDevice(void * read_to, ssize_t max_size) {
	if (!canRead() || PRIVATE->fd == 0) return -1;
	ssize_t ret = fread(read_to, 1, max_size, PRIVATE->fd);
	return ret;
}


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


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


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


PIFile::FileInfo PIFile::fileInfo(const PIString & path) {
	FileInfo ret;
	if (path.isEmpty()) return ret;
	ret.path   = path.replacedAll("\\", PIDir::separator);
	PIString n = ret.name();
	// piCout << "open" << path;
#ifdef WINDOWS
	DWORD attr = GetFileAttributesA((LPCSTR)(path.data()));
	if (attr == 0xFFFFFFFF) return ret;
	HANDLE hFile = 0;
	if ((attr & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) {
		hFile = CreateFileA(path.data(), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0);
	} else {
		hFile = CreateFileA(path.data(), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0);
	}
	if (!hFile) return ret;
	BY_HANDLE_FILE_INFORMATION fi;
	piZeroMemory(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                 = FILETIME2PIDateTime(fi.ftLastAccessTime);
		ret.time_modification           = FILETIME2PIDateTime(fi.ftLastWriteTime);
	}
	CloseHandle(hFile);
#else
	_stat_struct_ fs;
	piZeroMemory(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
#	if defined(QNX) || defined(FREERTOS)
	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
#  ifndef MICRO_PIP
	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);
	piZeroMemory(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
#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 = GetFileAttributesA((LPCSTR)(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 (SetFileAttributesA((LPCSTR)(fp.data()), attr) == 0) {
		piCout << "[PIFile] applyFileInfo: \"SetFileAttributes\" error:" << errorString();
	}
	HANDLE hFile = 0;
	if ((attr & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) {
		hFile = CreateFileA(path.data(),
		                    GENERIC_READ | GENERIC_WRITE,
		                    FILE_SHARE_READ | FILE_SHARE_WRITE,
		                    0,
		                    OPEN_EXISTING,
		                    FILE_FLAG_BACKUP_SEMANTICS,
		                    0);
	} else {
		hFile = CreateFileA(path.data(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, 0, OPEN_EXISTING, 0, 0);
	}
	if (!hFile) return false;
	FILETIME atime = PIDateTime2FILETIME(info.time_access), mtime = PIDateTime2FILETIME(info.time_modification);
	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();
	}
	if (chown(fp.data(), info.id_user, info.id_group) != 0) {
		piCout << "[PIFile] applyFileInfo: \"chown\" error:" << errorString();
	}
	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();
	}
#endif
	return true;
}


PIByteArray PIFile::readAll(const PIString & path) {
	PIFile f(path, PIIODevice::ReadOnly);
	if (!f.isOpened()) return PIByteArray();
	return f.readAll();
}


int PIFile::writeAll(const PIString & path, const PIByteArray & data) {
	PIFile f(path, PIIODevice::ReadWrite);
	if (!f.isOpened()) return 0;
	f.clear();
	return f.write(data.data(), data.size_s());
}
