/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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/>.
 *
 */

#if defined(__ANDROID__)

// Allow use of stuff in <time.h> and abort()
#define FORBIDDEN_SYMBOL_EXCEPTION_time_h
#define FORBIDDEN_SYMBOL_EXCEPTION_abort

// Disable printf override in common/forbidden.h to avoid
// clashes with log.h from the Android SDK.
// That header file uses
//   __attribute__ ((format(printf, 3, 4)))
// which gets messed up by our override mechanism; this could
// be avoided by either changing the Android SDK to use the equally
// legal and valid
//   __attribute__ ((format(__printf__, 3, 4)))
// or by refining our printf override to use a varadic macro
// (which then wouldn't be portable, though).
// Anyway, for now we just disable the printf override globally
// for the Android port
#define FORBIDDEN_SYMBOL_EXCEPTION_printf

// Allow calling of fdopen
#define FORBIDDEN_SYMBOL_EXCEPTION_FILE

// Allow calling of close system call
#include <unistd.h>

#include "backends/platform/android/android.h"
#include "backends/platform/android/jni-android.h"

#include "backends/fs/android/android-fs-factory.h"
#include "backends/fs/android/android-saf-fs.h"

#include "backends/fs/posix/posix-iostream.h"

#include "common/debug.h"
#include "common/util.h"

jmethodID AndroidSAFFilesystemNode::_MID_getTreeId = 0;
jmethodID AndroidSAFFilesystemNode::_MID_pathToNode = 0;
jmethodID AndroidSAFFilesystemNode::_MID_getChildren = 0;
jmethodID AndroidSAFFilesystemNode::_MID_getChild = 0;
jmethodID AndroidSAFFilesystemNode::_MID_createDirectory = 0;
jmethodID AndroidSAFFilesystemNode::_MID_createFile = 0;
jmethodID AndroidSAFFilesystemNode::_MID_createReadStream = 0;
jmethodID AndroidSAFFilesystemNode::_MID_createWriteStream = 0;
jmethodID AndroidSAFFilesystemNode::_MID_removeTree = 0;

jfieldID AndroidSAFFilesystemNode::_FID__treeName = 0;
jfieldID AndroidSAFFilesystemNode::_FID__root = 0;

jfieldID AndroidSAFFilesystemNode::_FID__parent = 0;
jfieldID AndroidSAFFilesystemNode::_FID__path = 0;
jfieldID AndroidSAFFilesystemNode::_FID__documentId = 0;
jfieldID AndroidSAFFilesystemNode::_FID__flags = 0;

bool AndroidSAFFilesystemNode::_JNIinit = false;

const char AndroidSAFFilesystemNode::SAF_MOUNT_POINT[] = "/saf/";

void AndroidSAFFilesystemNode::initJNI() {
	if (_JNIinit) {
		return;
	}

	JNIEnv *env = JNI::getEnv();

#define FIND_METHOD(prefix, name, signature) do {                           \
    _MID_ ## prefix ## name = env->GetMethodID(cls, #name, signature);      \
        if (_MID_ ## prefix ## name == 0)                                   \
            error("Can't find method ID " #name);                           \
    } while (0)
#define FIND_FIELD(prefix, name, signature) do {                            \
    _FID_ ## prefix ## name = env->GetFieldID(cls, #name, signature);       \
        if (_FID_ ## prefix ## name == 0)                                   \
            error("Can't find field ID " #name);                            \
    } while (0)
#define SAFFSNodeSig "Lorg/scummvm/scummvm/SAFFSTree$SAFFSNode;"

	jclass cls = env->FindClass("org/scummvm/scummvm/SAFFSTree");

	FIND_METHOD(, getTreeId, "()Ljava/lang/String;");
	FIND_METHOD(, pathToNode, "(Ljava/lang/String;)" SAFFSNodeSig);
	FIND_METHOD(, getChildren, "(" SAFFSNodeSig ")[" SAFFSNodeSig);
	FIND_METHOD(, getChild, "(" SAFFSNodeSig "Ljava/lang/String;)" SAFFSNodeSig);
	FIND_METHOD(, createDirectory, "(" SAFFSNodeSig "Ljava/lang/String;)" SAFFSNodeSig);
	FIND_METHOD(, createFile, "(" SAFFSNodeSig "Ljava/lang/String;)" SAFFSNodeSig);
	FIND_METHOD(, createReadStream, "(" SAFFSNodeSig ")I");
	FIND_METHOD(, createWriteStream, "(" SAFFSNodeSig ")I");
	FIND_METHOD(, removeTree, "()V");

	FIND_FIELD(, _treeName, "Ljava/lang/String;");
	FIND_FIELD(, _root, SAFFSNodeSig);

	cls = env->FindClass("org/scummvm/scummvm/SAFFSTree$SAFFSNode");

	FIND_FIELD(, _parent, SAFFSNodeSig);
	FIND_FIELD(, _path, "Ljava/lang/String;");
	FIND_FIELD(, _documentId, "Ljava/lang/String;");
	FIND_FIELD(, _flags, "I");

#undef SAFFSNodeSig
#undef FIND_FIELD
#undef FIND_METHOD

	_JNIinit = true;
}

AndroidSAFFilesystemNode *AndroidSAFFilesystemNode::makeFromPath(const Common::String &path) {
	if (!path.hasPrefix(SAF_MOUNT_POINT)) {
		// Not a SAF mount point
		return nullptr;
	}

	// Path is in the form /saf/<treeid>/<path>
	size_t pos = path.findFirstOf('/', sizeof(SAF_MOUNT_POINT) - 1);
	Common::String treeId;
	Common::String realPath;
	if (pos == Common::String::npos) {
		treeId = path.substr(sizeof(SAF_MOUNT_POINT) - 1);
	} else {
		treeId = path.substr(sizeof(SAF_MOUNT_POINT) - 1, pos - sizeof(SAF_MOUNT_POINT) + 1);
		realPath = path.substr(pos);
	}

	jobject safTree = JNI::findSAFTree(treeId);
	if (!safTree) {
		LOGW("AndroidSAFFilesystemNode::makeFromPath: tree id %s not found", treeId.c_str());
		return nullptr;
	}

	JNIEnv *env = JNI::getEnv();

	jstring pathObj = env->NewStringUTF(realPath.c_str());

	jobject node = env->CallObjectMethod(safTree, _MID_pathToNode, pathObj);

	env->DeleteLocalRef(pathObj);

	if (env->ExceptionCheck()) {
		LOGE("SAFFSTree::pathToNode failed");

		env->ExceptionDescribe();
		env->ExceptionClear();

		env->DeleteLocalRef(safTree);
		return nullptr;
	}

	if (!node) {
		env->DeleteLocalRef(safTree);
		return nullptr;
	}

	AndroidSAFFilesystemNode *ret = new AndroidSAFFilesystemNode(safTree, node);

	env->DeleteLocalRef(node);
	env->DeleteLocalRef(safTree);

	return ret;
}

AndroidSAFFilesystemNode *AndroidSAFFilesystemNode::makeFromTree(jobject safTree) {
	assert(safTree);

	JNIEnv *env = JNI::getEnv();

	jobject node = env->GetObjectField(safTree, _FID__root);
	if (!node) {
		env->DeleteLocalRef(safTree);
		return nullptr;
	}

	AndroidSAFFilesystemNode *ret = new AndroidSAFFilesystemNode(safTree, node);

	env->DeleteLocalRef(node);
	env->DeleteLocalRef(safTree);

	return ret;
}

AndroidSAFFilesystemNode::AndroidSAFFilesystemNode(jobject safTree, jobject safNode) :
	_cached(false), _flags(0), _safParent(nullptr) {

	JNIEnv *env = JNI::getEnv();

	_safTree = env->NewGlobalRef(safTree);
	assert(_safTree);
	_safNode = env->NewGlobalRef(safNode);
	assert(_safNode);

	cacheData();
}

AndroidSAFFilesystemNode::AndroidSAFFilesystemNode(jobject safTree, jobject safParent,
        const Common::String &path, const Common::String &name) :
	_safNode(nullptr), _cached(false), _flags(0), _safParent(nullptr) {

	JNIEnv *env = JNI::getEnv();

	_safTree = env->NewGlobalRef(safTree);
	assert(_safTree);
	_safParent = env->NewGlobalRef(safParent);
	assert(_safParent);

	// In this case _path is the parent
	_path = path;
	_newName = name;
}

// We need the custom copy constructor because of the reference
AndroidSAFFilesystemNode::AndroidSAFFilesystemNode(const AndroidSAFFilesystemNode &node)
	: AbstractFSNode(), _safNode(nullptr) {

	JNIEnv *env = JNI::getEnv();

	_safTree = env->NewGlobalRef(node._safTree);
	assert(_safTree);

	if (node._safNode) {
		_safNode = env->NewGlobalRef(node._safNode);
		assert(_safNode);
	}

	if (node._safParent) {
		_safParent = env->NewGlobalRef(node._safParent);
		assert(_safParent);
	}

	_cached = node._cached;
	_path = node._path;
	_flags = node._flags;
	_newName = node._newName;
}

AndroidSAFFilesystemNode::~AndroidSAFFilesystemNode() {
	JNIEnv *env = JNI::getEnv();

	env->DeleteGlobalRef(_safTree);
	env->DeleteGlobalRef(_safNode);
	env->DeleteGlobalRef(_safParent);
}

Common::String AndroidSAFFilesystemNode::getName() const {
	if (!_safNode || !_safParent) {
		// _newName is for non-existent paths or root node pretty name
		return _newName;
	}

	return lastPathComponent(_path, '/');
}

Common::String AndroidSAFFilesystemNode::getPath() const {
	assert(_safTree != nullptr);

	if (_safNode != nullptr) {
		return _path;
	}

	// When no node, it means _path is the parent node
	return _path + "/" + _newName;
}

AbstractFSNode *AndroidSAFFilesystemNode::getChild(const Common::String &n) const {
	assert(_safTree != nullptr);
	assert(_safNode != nullptr);

	// Make sure the string contains no slashes
	assert(!n.contains('/'));

	JNIEnv *env = JNI::getEnv();

	jstring name = env->NewStringUTF(n.c_str());

	jobject child = env->CallObjectMethod(_safTree, _MID_getChild, _safNode, name);

	env->DeleteLocalRef(name);

	if (env->ExceptionCheck()) {
		LOGE("SAFFSTree::getChild failed");

		env->ExceptionDescribe();
		env->ExceptionClear();

		return nullptr;
	}

	if (child) {
		AndroidSAFFilesystemNode *ret = new AndroidSAFFilesystemNode(_safTree, child);
		env->DeleteLocalRef(child);
		return ret;
	}

	return new AndroidSAFFilesystemNode(_safTree, _safNode, _path, n);
}

bool AndroidSAFFilesystemNode::getChildren(AbstractFSList &myList, ListMode mode,
        bool hidden) const {
	assert(_flags & DIRECTORY);

	assert(_safTree != nullptr);
	if (!_safNode) {
		return false;
	}

	JNIEnv *env = JNI::getEnv();

	jobjectArray array =
	    (jobjectArray)env->CallObjectMethod(_safTree, _MID_getChildren, _safNode);

	if (env->ExceptionCheck()) {
		LOGE("SAFFSTree::getChildren failed");

		env->ExceptionDescribe();
		env->ExceptionClear();

		return false;
	}

	myList.clear();

	jsize size = env->GetArrayLength(array);
	myList.reserve(size);

	for (jsize i = 0; i < size; ++i) {
		jobject node = env->GetObjectArrayElement(array, i);

		myList.push_back(new AndroidSAFFilesystemNode(_safTree, node));

		env->DeleteLocalRef(node);
	}
	env->DeleteLocalRef(array);

	return true;
}

AbstractFSNode *AndroidSAFFilesystemNode::getParent() const {
	assert(_safTree != nullptr);
	assert(_safNode != nullptr);

	if (_safParent) {
		return new AndroidSAFFilesystemNode(_safTree, _safParent);
	}

	return AndroidFilesystemFactory::instance().makeRootFileNode();
}

Common::SeekableReadStream *AndroidSAFFilesystemNode::createReadStream() {
	assert(_safTree != nullptr);

	if (!_safNode) {
		return nullptr;
	}

	JNIEnv *env = JNI::getEnv();

	jint fd = env->CallIntMethod(_safTree, _MID_createReadStream, _safNode);

	if (env->ExceptionCheck()) {
		LOGE("SAFFSTree::createReadStream failed");

		env->ExceptionDescribe();
		env->ExceptionClear();

		return nullptr;
	}

	if (fd == -1) {
		return nullptr;
	}

	FILE *f = fdopen(fd, "r");
	if (!f) {
		close(fd);
		return nullptr;
	}

	return new PosixIoStream(f);
}

Common::SeekableWriteStream *AndroidSAFFilesystemNode::createWriteStream() {
	assert(_safTree != nullptr);

	JNIEnv *env = JNI::getEnv();

	if (!_safNode) {
		assert(_safParent);
		jstring name = env->NewStringUTF(_newName.c_str());

		jobject child = env->CallObjectMethod(_safTree, _MID_createFile, _safParent, name);

		env->DeleteLocalRef(name);

		if (env->ExceptionCheck()) {
			LOGE("SAFFSTree::createFile failed");

			env->ExceptionDescribe();
			env->ExceptionClear();

			return nullptr;
		}

		if (!child) {
			return nullptr;
		}

		_safNode = env->NewGlobalRef(child);
		assert(_safNode);

		env->DeleteLocalRef(child);

		cacheData(true);
	}

	jint fd = env->CallIntMethod(_safTree, _MID_createWriteStream, _safNode);
	if (env->ExceptionCheck()) {
		LOGE("SAFFSTree::createWriteStream failed");

		env->ExceptionDescribe();
		env->ExceptionClear();

		return nullptr;
	}

	if (fd == -1) {
		return nullptr;
	}

	FILE *f = fdopen(fd, "w");
	if (!f) {
		close(fd);
		return nullptr;
	}

	return new PosixIoStream(f);
}

bool AndroidSAFFilesystemNode::createDirectory() {
	assert(_safTree != nullptr);

	if (_safNode) {
		return _flags & DIRECTORY;
	}

	assert(_safParent);

	JNIEnv *env = JNI::getEnv();

	jstring name = env->NewStringUTF(_newName.c_str());

	jobject child = env->CallObjectMethod(_safTree, _MID_createDirectory, _safParent, name);

	env->DeleteLocalRef(name);

	if (env->ExceptionCheck()) {
		LOGE("SAFFSTree::createDirectory failed");

		env->ExceptionDescribe();
		env->ExceptionClear();

		return false;
	}

	if (!child) {
		return false;
	}

	_safNode = env->NewGlobalRef(child);
	assert(_safNode);

	env->DeleteLocalRef(child);

	cacheData(true);

	return true;
}

void AndroidSAFFilesystemNode::removeTree() {
	assert(_safParent == nullptr);

	JNIEnv *env = JNI::getEnv();

	env->CallVoidMethod(_safTree, _MID_removeTree);

	if (env->ExceptionCheck()) {
		LOGE("SAFFSTree::removeTree failed");

		env->ExceptionDescribe();
		env->ExceptionClear();
	}
}

void AndroidSAFFilesystemNode::cacheData(bool force) {
	if (_cached && !force) {
		return;
	}

	JNIEnv *env = JNI::getEnv();

	_flags = env->GetIntField(_safNode, _FID__flags);

	jobject safParent = env->GetObjectField(_safNode, _FID__parent);
	if (safParent) {
		if (_safParent) {
			env->DeleteGlobalRef(_safParent);
		}
		_safParent = env->NewGlobalRef(safParent);
		assert(_safParent);
		env->DeleteLocalRef(safParent);
	}

	if (_safParent == nullptr) {
		jstring nameObj = (jstring)env->GetObjectField(_safTree, _FID__treeName);
		const char *nameP = env->GetStringUTFChars(nameObj, 0);
		if (nameP != 0) {
			_newName = Common::String(nameP);
			env->ReleaseStringUTFChars(nameObj, nameP);
		}
		env->DeleteLocalRef(nameObj);
	}
	_path.clear();

	Common::String workingPath;

	jstring pathObj = (jstring)env->GetObjectField(_safNode, _FID__path);
	const char *path = env->GetStringUTFChars(pathObj, 0);
	if (path != 0) {
		workingPath = Common::String(path);
		env->ReleaseStringUTFChars(pathObj, path);
	} else {
		env->DeleteLocalRef(pathObj);
		return;
	}
	env->DeleteLocalRef(pathObj);

	jstring idObj = (jstring)env->CallObjectMethod(_safTree, _MID_getTreeId);
	if (env->ExceptionCheck()) {
		LOGE("SAFFSTree::getTreeId failed");

		env->ExceptionDescribe();
		env->ExceptionClear();

		env->ReleaseStringUTFChars(pathObj, path);
		env->DeleteLocalRef(pathObj);
		return;
	}

	if (!idObj) {
		return;
	}

	const char *id = env->GetStringUTFChars(idObj, 0);
	if (id != 0) {
		_path = Common::String::format("%s%s%s", SAF_MOUNT_POINT, id, workingPath.c_str());
		env->ReleaseStringUTFChars(idObj, id);
	}
	env->DeleteLocalRef(idObj);

	_cached = true;
}

const char AddSAFFakeNode::SAF_ADD_FAKE_PATH[] = "/saf";

AddSAFFakeNode::~AddSAFFakeNode() {
	delete _proxied;
}

AbstractFSNode *AddSAFFakeNode::getChild(const Common::String &name) const {
	// We can't call getChild as it's protected
	return nullptr;
}

AbstractFSNode *AddSAFFakeNode::getParent() const {
	// We are always just below the root and getParent is protected
	return AndroidFilesystemFactory::instance().makeRootFileNode();
}

bool AddSAFFakeNode::exists() const {
	if (!_proxied) {
		makeProxySAF();
	}

	if (!_proxied) {
		return false;
	}

	return _proxied->exists();
}

bool AddSAFFakeNode::getChildren(AbstractFSList &list, ListMode mode, bool hidden) const {
	if (!_proxied) {
		makeProxySAF();
	}

	if (!_proxied) {
		return false;
	}

	return _proxied->getChildren(list, mode, hidden);
}

Common::String AddSAFFakeNode::getPath() const {
	if (!_proxied) {
		makeProxySAF();
	}

	if (!_proxied) {
		return "";
	}

	return _proxied->getPath();
}

bool AddSAFFakeNode::isReadable() const {
	if (!_proxied) {
		makeProxySAF();
	}

	if (!_proxied) {
		return false;
	}

	return _proxied->isReadable();
}

bool AddSAFFakeNode::isWritable() const {
	if (!_proxied) {
		makeProxySAF();
	}

	if (!_proxied) {
		return false;
	}

	return _proxied->isWritable();
}

void AddSAFFakeNode::makeProxySAF() const {
	if (_proxied) {
		return;
	}

	// I18N: This may be displayed in the Android UI used to add a Sotrage Attach Framework authorization
	jobject saftree = JNI::getNewSAFTree(true, true, "", _("Choose a new folder"));
	if (!saftree) {
		return;
	}

	_proxied = AndroidSAFFilesystemNode::makeFromTree(saftree);
}

#endif
