/*
    PIP - Platform Independent Primitives
    Directory
    Ivan Pelipenko peri4ko@yandex.ru, Andrey Bychkov work.a.b@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 "pidir.h"

#include "piincludes_p.h"


const PIChar PIDir::separator = '/';
#ifdef QNX
#  define _stat_struct_ struct stat
#  define _stat_call_   stat
#  define _stat_link_   lstat
#else
#  define _stat_struct_ struct stat64
#  define _stat_call_   stat64
#  define _stat_link_   lstat64
#endif
#ifndef WINDOWS
#  ifdef ANDROID
#	include <dirent.h>
#  else
#	ifdef FREERTOS
extern "C" {
#	  include <sys/dirent.h>
}
#	else
#	  include <sys/dir.h>
#	endif
#  endif
#  include <sys/stat.h>
#endif


//! \class PIDir pidir.h
//! \details
//! \~english \section PIDir_sec0 Synopsis
//! \~russian \section PIDir_sec0 Краткий обзор
//! \~english
//! This class provide access to local directory.
//!
//! \~russian
//!


PIDir::PIDir(const PIString & dir) {
	setDir(dir);
}


PIDir::PIDir(const PIFile & file) {
	setDir(file.path());
	if (isExists()) return;
	int pos = path_.findLast(separator);
	path_.cutRight(path_.size_s() - pos);
}


bool PIDir::operator==(const PIDir & d) const {
	return d.absolutePath() == absolutePath();
}


bool PIDir::isAbsolute() const {
	if (path_.isEmpty()) return false;
	if (path_[0] == separator) return true;
#ifdef WINDOWS
	return (path_.mid(1, 1) == ":");
#endif
	return false;
}


PIString PIDir::name() const {
	PIStringList fp = absolutePath().split(separator);
	fp.removeStrings("");
	if (fp.isEmpty()) return "";
	return fp.back();
}


PIString PIDir::path() const {
#ifdef WINDOWS
	if (path_.startsWith(separator)) {
		if (path_.length() == 1)
			return separator;
		else
			return path_.mid(1);
	} else
#endif
		return path_;
}


PIString PIDir::absolutePath() const {
	if (isAbsolute()) {
#ifdef WINDOWS
		if (path_.startsWith(separator)) {
			if (path_.length() == 1)
				return separator;
			else
				return path_.mid(1);
		} else
#endif
			return path_;
	}
	return PIDir(PIDir::current().path() + separator + path_).path();
}


//! \details
//! \~english
//! This function remove repeatedly separators and
//! resolve ".." in path. E.g. "/home/.//user/src/../.." will
//! become "/home". \n Returns reference to this %PIDir
//! \~russian
//! Этот метод удаляет повторяющиеся разделители и разрешает
//! "..". Например, путь "/home/.//user/src/../.." станет "/home". \n
//! Возвращает ссылку на этот %PIDir
PIDir & PIDir::cleanPath() {
	PIString p(path_);
	if (p.isEmpty()) {
		path_ = ".";
		return *this;
	}
	PIString sep = PIString(separator);
	path_.replaceAll(sep + sep, sep);
	bool is_abs    = isAbsolute();
	PIStringList l = PIString(p).split(separator);
	l.removeAll(".");
	l.removeAll("");
	for (int i = 0; i < l.size_s() - 1; ++i) {
		if (l[i] != ".." && l[i + 1] == "..") {
			l.remove(i, 2);
			i -= 2;
			if (i < -1) i = -1;
		}
	}
	if (is_abs)
		while (!l.isEmpty()) {
			if (l.front() == "..")
				l.pop_front();
			else
				break;
		}
	path_ = l.join(separator);
	if (is_abs) path_.prepend(separator);
	if (path_.isEmpty()) path_ = ".";
	return *this;
}


PIString PIDir::relative(const PIString & path) const {
	PIDir td(path);
	PIStringList dl(absolutePath().split(separator)), pl(td.absolutePath().split(separator)), rl;
	auto checkPath = [](PIStringList & path) {
		if (path.isEmpty()) return;
		if (path[0].size() >= 2) {
			if (path[0][0].isAlpha() && path[0][1] == ':') {
				path[0][0] = path[0][0].toUpper();
			}
		}
	};
	checkPath(dl);
	checkPath(pl);
	// piCout << pl << "rel to" << dl;
	while (!dl.isEmpty() && !pl.isEmpty()) {
		if (dl.front() != pl.front()) break;
		dl.pop_front();
		pl.pop_front();
	}
	for (int i = 0; i < dl.size_s(); ++i)
		rl << "..";
	rl << pl;
	if (rl.isEmpty()) return ".";
	return rl.join(separator);
}


PIString PIDir::absolute(const PIString & path) const {
	PIFile::FileInfo fi(path);
	if (fi.isAbsolute()) return path;
	PIString ret = absolutePath();
	if (!ret.endsWith(separator) && !path.startsWith(separator)) ret += separator;
	PIDir td(ret + path);
	return td.cleanPath().path();
}


PIDir & PIDir::setDir(const PIString & path) {
	path_ = path;
#ifdef WINDOWS
	path_.replaceAll("\\", separator);
	if (path_.length() > 2)
		if (path_.mid(1, 2).contains(":")) path_.prepend(separator);
#endif
	cleanPath();
	return *this;
}


PIDir & PIDir::cd(const PIString & path) {
	if (path_.isEmpty()) return *this;
	if (path_.back() != separator) path_ += separator;
	path_ += path;
	return cleanPath();
}


bool PIDir::make(bool withParents) {
	PIDir d = cleanedPath();
	// PIString tp;
#ifndef WINDOWS
	bool is_abs = isAbsolute();
#endif
	if (withParents) {
		PIStringList l = d.path().split(separator);
		// piCout << l;
		l.removeAll("");
		// piCout << l;
		PIString cdp;
		for (const auto & i: l) {
			if (!cdp.isEmpty()
#ifndef WINDOWS
			    || is_abs
#endif
			)
				cdp += separator;
			cdp += i;
			// piCout << "dir" << cdp;
			if (!isExists(cdp))
				if (!makeDir(cdp)) return false;
		}
		/*for (int i = l.size_s() - 1; i >= 0; --i) {
		    if (i > 1) tp = PIStringList(l).remove(i, l.size_s() - i).join(separator);
		    else {
		        tp = separator;
		        if (!is_abs) tp.push_front('.');
		    }
		    piCout << "check" << tp;
		    if (isExists(tp)) {
		        for (int j = i + 1; j <= l.size_s(); ++j) {
		            tp = PIStringList(l).remove(j, l.size_s() - j).join(separator);
		            piCout << "make" << tp;
		            if (makeDir(tp)) continue;
		            else return false;
		        }
		        break;
		    };
		}*/
		return true;
	} else if (makeDir(d.path()))
		return true;
	return false;
}


bool PIDir::rename(const PIString & new_name) {
	if (!PIDir::rename(path(), new_name)) return false;
	setDir(new_name);
	return true;
}


#ifdef WINDOWS
bool sort_compare(const PIFile::FileInfo & v0, const PIFile::FileInfo & v1) {
	return strcoll(v0.path.data(), v1.path.data()) < 0;
}
#endif


//! \~\details
//! \~english
//! Scan this directory and returns all directories
//! and files in one list, sorted alphabetically. This list
//! contains also "." and ".." members. There are absolute
//! pathes in returned list.
//! \attention This function doesn`t scan content of inner
//! directories!
//! \~russian
//! Читает директорию и возвращает все директории и файлы
//! одним списком, сортированным по алфавиту. Список содержит
//! также "." и "..". Возвращаются абсолютные пути.
//! \attention Этот метод не читает содержимое директорий
//! рекурсивно!
PIVector<PIFile::FileInfo> PIDir::entries(const PIRegularExpression & regexp) {
	if (!isExists()) return {};
	PIVector<PIFile::FileInfo> ret;
	PIString dp = absolutePath();
	PIString p(dp);
	if (dp == ".")
		dp.clear();
	else if (!dp.endsWith(separator))
		dp += separator;
//	piCout << "start entries from" << p;
#ifdef WINDOWS
	PIFile::FileInfo fi;
	if (dp == separator) {
		char letters[1024];
		DWORD ll = GetLogicalDriveStringsA(1023, letters);
		PIString clet;
		for (DWORD i = 0; i < ll; ++i) {
			if (letters[i] == '\0') {
				clet.resize(2);
				fi.path  = clet;
				fi.flags = PIFile::FileInfo::Dir;
				if (regexp.isValid()) {
					if (regexp.match(fi.name())) {
						ret << fi;
					}
				} else
					ret << fi;
				clet.clear();
			} else
				clet += PIChar(letters[i]);
		}
	} else {
		WIN32_FIND_DATAA fd;
		piZeroMemory(fd);
		p += "\\*";
		void * hf = FindFirstFileA((LPCSTR)(p.data()), &fd);
		if (!hf) return ret;
		bool hdd = false;
		do {
			PIString fn(fd.cFileName);
			if (fn == "..") hdd = true;
			fi = PIFile::fileInfo(dp + fn);
			if (regexp.isValid()) {
				if (regexp.match(fi.name())) {
					ret << fi;
				}
			} else
				ret << fi;
			piZeroMemory(fd);
		} while (FindNextFileA(hf, &fd) != 0);
		FindClose(hf);
		ret.sort(sort_compare);
		if (!hdd) {
			PIFile::FileInfo fi;
			fi.path  = "..";
			fi.flags = PIFile::FileInfo::Dir | PIFile::FileInfo::DotDot;
			ret.push_front(fi);
		}
	}

#else
#  if defined(QNX) || defined(FREERTOS)
	struct dirent * de = 0;
	DIR * dir          = 0;
	dir                = opendir(p.data());
	if (dir) {
		for (;;) {
			de = readdir(dir);
			if (!de) break;
			ret << PIFile::fileInfo(dp + PIString(de->d_name));
		}
		closedir(dir);
	}
#  else
	dirent ** list = nullptr;
	int cnt        = scandir(p.data(),
                      &list,
                      0,
#	if defined(MAC_OS) || defined(ANDROID) || defined(BLACKBERRY)
                      alphasort);
#	else
	                  versionsort);
#	endif
	if (cnt < 0) return ret;
	for (int i = 0; i < cnt; ++i) {
		ret << PIFile::fileInfo(dp + PIString(list[i]->d_name));
		free(list[i]);
	}
	free(list);
#  endif
#endif
	//	piCout << "end entries from" << p;
	return ret;
}


//! \~\details
//! \~english
//! Scan this directory recursively and returns all
//! directories and files in one list, sorted alphabetically.
//! This list doesn`t contains "." and ".." members. There
//! are absolute pathes in returned list, and
//! files placed after directories in this list.
//! \~russian
//! Читает директорию рекурсивно и возвращает все директории и файлы
//! одним списком, сортированным по алфавиту. Список не содержит
//! "." и "..". Возвращаются абсолютные пути, причём файлы
//! располагаются после директорий.
PIVector<PIFile::FileInfo> PIDir::allEntries(const PIRegularExpression & regexp) {
	PIVector<PIFile::FileInfo> ret;
	PIVector<PIFile::FileInfo> dirs;
	PIStringList cdirs, ndirs;
	cdirs << path();
	while (!cdirs.isEmpty()) {
		for (const auto & d: cdirs) {
			scan_                         = d;
			PIVector<PIFile::FileInfo> el = PIDir(d).entries();
			for (const auto & de: el) {
				if (de.name() == "." || de.name() == "..") continue;
				if (de.isSymbolicLink()) continue; /// TODO: resolve symlinks
				if (de.isDir()) {
					dirs << de;
					ndirs << de.path;
				} else {
					if (regexp.isValid()) {
						if (!regexp.match(de.name())) {
							continue;
						}
					}
					ret << de;
				}
			}
		}
		cdirs = ndirs;
		ndirs.clear();
	}
	ret.insert(0, dirs);
	scan_.clear();
	return ret;
}


bool PIDir::isExists(const PIString & path) {
#ifdef WINDOWS
	DWORD ret = GetFileAttributes((LPCTSTR)(path.data()));
	return (ret != 0xFFFFFFFF) && (ret & FILE_ATTRIBUTE_DIRECTORY);
#else
	DIR * dir_ = opendir(path.data());
	if (dir_ == 0) return false;
	closedir(dir_);
#endif
	return true;
}


PIDir PIDir::current() {
#ifndef ESP_PLATFORM
	char rc[1024];
#endif
#ifdef WINDOWS
	piZeroMemory(rc, 1024);
	if (GetCurrentDirectory(1024, (LPTSTR)rc) == 0) return PIString();
	PIString ret(rc);
	ret.replaceAll("\\", PIDir::separator);
	ret.prepend(separator);
	return PIDir(ret);
#else
#  ifndef ESP_PLATFORM
	if (getcwd(rc, 1024) == 0) return PIString();
	return PIDir(rc);
#  else
	return PIDir("/spiffs");
#  endif
#endif
}


PIDir PIDir::home() {
#ifndef ESP_PLATFORM
	char * rc = nullptr;
#endif
#ifdef WINDOWS
	rc = new char[1024];
	piZeroMemory(rc, 1024);
	if (ExpandEnvironmentStrings((LPCTSTR) "%HOMEPATH%", (LPTSTR)rc, 1024) == 0) {
		delete[] rc;
		return PIDir();
	}
	PIString hp(rc);
	piZeroMemory(rc, 1024);
	if (ExpandEnvironmentStrings((LPCTSTR) "%HOMEDRIVE%", (LPTSTR)rc, 1024) == 0) {
		delete[] rc;
		return PIDir();
	}
	PIString hd(rc);
	hp.replaceAll("\\", PIDir::separator);
	delete[] rc;
	// s.prepend(separator);
	return PIDir(hd + hp);
#else
#  ifndef ESP_PLATFORM
	rc = getenv("HOME");
	if (!rc) return PIDir();
	return PIDir(rc);
#  else
	return PIDir();
#  endif
#endif
}


PIDir PIDir::temporary() {
	char * rc = nullptr;
#ifdef WINDOWS
	rc = new char[1024];
	piZeroMemory(rc, 1024);
	int ret = GetTempPath(1024, (LPTSTR)rc);
	if (ret == 0) {
		delete[] rc;
		return PIDir();
	}
	PIString s(rc);
	s.replaceAll("\\", PIDir::separator);
	delete[] rc;
	s.prepend(separator);
	return PIDir(s);
#else
	char template_rc[] = "/tmp/pidir_tmp_XXXXXX";
	rc                 = mkdtemp(template_rc);
	if (!rc) return PIDir();
	PIString s(rc);
	return PIDir(s.left(s.findLast(PIDir::separator)));
#endif
}


PIVector<PIFile::FileInfo> PIDir::allEntries(const PIString & path, const PIRegularExpression & regexp) {
	return PIDir(path).allEntries(regexp);
}


bool PIDir::make(const PIString & path, bool withParents) {
	PIDir d(path);
	if (d.isExists()) return true;
	return d.make(withParents);
}


bool PIDir::setCurrent(const PIString & path) {
#ifdef WINDOWS
	if (SetCurrentDirectory((LPCTSTR)(path.data())) != 0) return true;
#else
	if (chdir(path.data()) == 0) return true;
#endif
	printf("[PIDir] setCurrent(\"%s\") error: %s\n", path.data(), errorString().data());
	return false;
}


bool PIDir::makeDir(const PIString & path) {
#ifdef WINDOWS
	if (CreateDirectory((LPCTSTR)(path.data()), NULL) != 0) return true;
#else
	if (mkdir(path.data(), 16877) == 0) return true;
#endif
	printf("[PIDir] makeDir(\"%s\") error: %s\n", path.data(), errorString().data());
	return false;
}


bool PIDir::removeDir(const PIString & path) {
#ifdef WINDOWS
	if (RemoveDirectory((LPCTSTR)(path.data())) != 0) return true;
#else
	if (rmdir(path.data()) == 0) return true;
#endif
	printf("[PIDir] removeDir(\"%s\") error: %s\n", path.data(), errorString().data());
	return false;
}


bool PIDir::renameDir(const PIString & path, const PIString & new_name) {
	if (::rename(path.data(), new_name.data()) == 0) return true;
	printf("[PIDir] renameDir(\"%s\", \"%s\") error: %s\n", path.data(), new_name.data(), errorString().data());
	return false;
}


PIDir::CurrentDirOverrider::CurrentDirOverrider(const PIString & path) {
	save(PIFile::fileInfo(path));
}


PIDir::CurrentDirOverrider::CurrentDirOverrider(const PIFile::FileInfo & info) {
	save(info);
}


void PIDir::CurrentDirOverrider::restore() {
	if (!active) return;
	active = false;
	PIDir::setCurrent(prev_cd);
}


void PIDir::CurrentDirOverrider::save(const PIFile::FileInfo & info) {
	prev_cd    = PIDir::current().path();
	PIString p = info.isDir() ? info.path : info.dir();
	if (info.isAbsolute())
		PIDir::setCurrent(p);
	else
		PIDir::setCurrent(PIDir::current().path() + PIDir::separator + p);
}
