/* 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */

#include "config.h"
#include "common/translation.h"
#include "backends/platform/tizen/system.h"
#include "backends/platform/tizen/fs.h"

#include <FAppApp.h>

#define BUFFER_SIZE 1024

using namespace Tizen::App;

//
// converts a Tizen (wchar) String into a scummVM (char) string
//
Common::String fromString(const Tizen::Base::String &in) {
	ByteBuffer *buf = StringUtil::StringToUtf8N(in);
	Common::String result((const char*)buf->GetPointer());
	delete buf;
	return result;
}

//
// TizenFileStream
//
class TizenFileStream :
	public Common::SeekableReadStream,
	public Common::WriteStream,
	public Common::NonCopyable {
public:
	static TizenFileStream *makeFromPath(const String &path, bool writeMode);

	TizenFileStream(File *file, bool writeMode);
	~TizenFileStream();

	bool err() const;
	void clearErr();
	bool eos() const;

	uint32 write(const void *dataPtr, uint32 dataSize);
	bool flush();

	int32 pos() const;
	int32 size() const;
	bool seek(int32 offs, int whence = SEEK_SET);
	uint32 read(void *dataPtr, uint32 dataSize);

private:
	byte _buffer[BUFFER_SIZE];
	uint32 _bufferIndex;
	uint32 _bufferLength;
	bool _writeMode;
	File *_file;
};

TizenFileStream::TizenFileStream(File *ioFile, bool writeMode) :
	_bufferIndex(0),
	_bufferLength(0),
	_writeMode(writeMode),
	_file(ioFile) {
	AppAssert(ioFile != 0);
}

TizenFileStream::~TizenFileStream() {
	if (_file) {
		if (_writeMode) {
			flush();
		}
		delete _file;
	}
}

bool TizenFileStream::err() const {
	result r = GetLastResult();
	return (r != E_SUCCESS && r != E_END_OF_FILE);
}

void TizenFileStream::clearErr() {
	SetLastResult(E_SUCCESS);
}

bool TizenFileStream::eos() const {
	return (_bufferLength - _bufferIndex == 0) && (GetLastResult() == E_END_OF_FILE);
}

int32 TizenFileStream::pos() const {
	return _file->Tell() - (_bufferLength - _bufferIndex);
}

int32 TizenFileStream::size() const {
	int32 oldPos = _file->Tell();
	_file->Seek(FILESEEKPOSITION_END, 0);

	int32 length = _file->Tell();
	SetLastResult(_file->Seek(FILESEEKPOSITION_BEGIN, oldPos));

	return length;
}

bool TizenFileStream::seek(int32 offs, int whence) {
	bool result = false;
	switch (whence) {
	case SEEK_SET:
		// set from start of file
		SetLastResult(_file->Seek(FILESEEKPOSITION_BEGIN, offs));
		result = (E_SUCCESS == GetLastResult());
		break;

	case SEEK_CUR:
		// set relative to offs
		if (_bufferIndex < _bufferLength && _bufferIndex > (uint32)-offs) {
			// re-position within the buffer
			SetLastResult(E_SUCCESS);
			_bufferIndex += offs;
			return true;
		} else {
			offs -= (_bufferLength - _bufferIndex);
			if (offs < 0 && _file->Tell() + offs < 0) {
				// avoid negative positioning
				offs = 0;
			}
			if (offs != 0) {
				SetLastResult(_file->Seek(FILESEEKPOSITION_CURRENT, offs));
				result = (E_SUCCESS == GetLastResult());
			} else {
				result = true;
			}
		}
		break;

	case SEEK_END:
		// set relative to end - positive will increase the file size
		SetLastResult(_file->Seek(FILESEEKPOSITION_END, offs));
		result = (E_SUCCESS == GetLastResult());
		break;

	default:
		AppLog("Invalid whence %d", whence);
		return false;
	}

	if (!result) {
		AppLog("seek failed");
	}

	_bufferIndex = _bufferLength = 0;
	return result;
}

uint32 TizenFileStream::read(void *ptr, uint32 len) {
	uint32 result = 0;
	if (!eos()) {
		if (_bufferIndex < _bufferLength) {
			// use existing buffer
			uint32 available = _bufferLength - _bufferIndex;
			if (len <= available) {
				// use allocation
				memcpy((byte *)ptr, &_buffer[_bufferIndex], len);
				_bufferIndex += len;
				result = len;
			} else {
				// use remaining allocation
				memcpy((byte *)ptr, &_buffer[_bufferIndex], available);
				uint32 remaining = len - available;
				result = available;

				if (remaining) {
					result += _file->Read(((byte *)ptr) + available, remaining);
				}
				_bufferIndex = _bufferLength = 0;
			}
		} else if (len < BUFFER_SIZE) {
			// allocate and use buffer
			_bufferIndex = 0;
			_bufferLength = _file->Read(_buffer, BUFFER_SIZE);
			if (_bufferLength) {
				if (_bufferLength < len) {
					len = _bufferLength;
				}
				memcpy((byte *)ptr, _buffer, len);
				result = _bufferIndex = len;
			}
		} else {
			result = _file->Read((byte *)ptr, len);
			_bufferIndex = _bufferLength = 0;
		}
	} else {
		AppLog("Attempted to read past EOS");
	}
	return result;
}

uint32 TizenFileStream::write(const void *ptr, uint32 len) {
	result r = _file->Write(ptr, len);
	SetLastResult(r);
	return (r == E_SUCCESS ? len : 0);
}

bool TizenFileStream::flush() {
	logEntered();
	SetLastResult(_file->Flush());
	return (E_SUCCESS == GetLastResult());
}

TizenFileStream *TizenFileStream::makeFromPath(const String &path, bool writeMode) {
	File *ioFile = new File();

	String filePath = path;
	if (writeMode && (path[0] != '.' && path[0] != '/')) {
		filePath.Insert(App::GetInstance()->GetAppDataPath() + L"/", 0);
	}

	AppLog("Open file %S", filePath.GetPointer());
	TizenFileStream *stream;
	result r = ioFile->Construct(filePath, writeMode ? L"w" : L"r", writeMode);
	if (r == E_SUCCESS) {
		stream = new TizenFileStream(ioFile, writeMode);
	} else {
		AppLog("Failed to open file");
		delete ioFile;
		stream = NULL;
	}
	return stream;
}

//
// TizenFilesystemNode
//
TizenFilesystemNode::TizenFilesystemNode(const Common::String &nodePath) {
	AppAssert(nodePath.size() > 0);
	init(nodePath);
}

TizenFilesystemNode::TizenFilesystemNode(SystemPath systemPath) {
	switch (systemPath) {
	case kData:
		_unicodePath = App::GetInstance()->GetAppDataPath();
		_displayName = _s("[ Data ]");
		break;
	case kResource:
		_unicodePath = App::GetInstance()->GetAppResourcePath();
		_displayName = _s("[ Resources ]");
		break;
	case kSdCard:
		_unicodePath = Tizen::System::Environment::GetExternalStoragePath();
		_displayName = _s("[ SDCard ]");
		break;
	case kMedia:
		_unicodePath = Tizen::System::Environment::GetMediaPath();
		_displayName = _s("[ Media ]");
		break;
	case kShared:
		_unicodePath = App::GetInstance()->GetAppSharedPath();
		_displayName = _s("[ Shared ]");
		break;
	}
	_path = ::fromString(_unicodePath);
	_isValid = _isVirtualDir = !IsFailed(File::GetAttributes(_unicodePath, _attr));
}

TizenFilesystemNode::TizenFilesystemNode(const Common::String &root, const Common::String &nodePath) {
	AppLog("TizenFilesystemNode '%s' '%s'", root.c_str(), nodePath.c_str());

	// Make sure the string contains no slashes
	AppAssert(!nodePath.contains('/'));

	// We assume here that path is already normalized (hence don't bother to
	// call Common::normalizePath on the final path).
	Common::String newPath(root);
	if (root.lastChar() != '/') {
		newPath += '/';
	}
	newPath += nodePath;

	init(newPath);
}

void TizenFilesystemNode::init(const Common::String &nodePath) {
	// Normalize the path (that is, remove unneeded slashes etc.)
	_path = Common::normalizePath(nodePath, '/');
	_displayName = Common::lastPathComponent(_path, '/');

	StringUtil::Utf8ToString(_path.c_str(), _unicodePath);
	_isVirtualDir = (_path == "/");
	_isValid = _isVirtualDir || !IsFailed(File::GetAttributes(_unicodePath, _attr));
}

bool TizenFilesystemNode::exists() const {
	return _isValid;
}

bool TizenFilesystemNode::isReadable() const {
	return _isVirtualDir || _isValid;
}

bool TizenFilesystemNode::isDirectory() const {
	return _isVirtualDir || (_isValid && _attr.IsDirectory());
}

bool TizenFilesystemNode::isWritable() const {
	bool result = (_isValid && !_attr.IsReadOnly());
	if (_unicodePath == App::GetInstance()->GetAppResourcePath()) {
		result = false;
	}
	return result;
}

AbstractFSNode *TizenFilesystemNode::getChild(const Common::String &n) const {
	AppAssert(!_path.empty());
	AppAssert(isDirectory());
	return new TizenFilesystemNode(_path, n);
}

bool TizenFilesystemNode::getChildren(AbstractFSList &myList, ListMode mode, bool hidden) const {
	AppAssert(isDirectory());

	bool result = false;

	if (_isVirtualDir && mode != Common::FSNode::kListFilesOnly && _path == "/") {
		// present well known TIZEN file system areas
		myList.push_back(new TizenFilesystemNode(kData));
		myList.push_back(new TizenFilesystemNode(kSdCard));
		myList.push_back(new TizenFilesystemNode(kMedia));
		myList.push_back(new TizenFilesystemNode(kShared));
	}

	if (!result) {
		DirEnumerator *pDirEnum = 0;
		Directory *pDir = new Directory();

		// open directory
		if (IsFailed(pDir->Construct(_unicodePath))) {
			AppLog("Failed to open directory: %S", _unicodePath.GetPointer());
		} else {
			// read all directory entries
			pDirEnum = pDir->ReadN();
			if (pDirEnum) {
				result = true;
			}

			// loop through all directory entries
			while (pDirEnum && pDirEnum->MoveNext() == E_SUCCESS) {
				DirEntry dirEntry = pDirEnum->GetCurrentDirEntry();

				// skip 'invisible' files if necessary
				Tizen::Base::String fileName = dirEntry.GetName();

				if (fileName[0] == '.' && !hidden) {
					continue;
				}

				// skip '.' and '..' to avoid cycles
				if (fileName == L"." || fileName == L"..") {
					continue;
				}

				// Honor the chosen mode
				if ((mode == Common::FSNode::kListFilesOnly && dirEntry.IsDirectory()) ||
						(mode == Common::FSNode::kListDirectoriesOnly && !dirEntry.IsDirectory())) {
					continue;
				}
				myList.push_back(new TizenFilesystemNode(_path, ::fromString(fileName)));
			}
		}

		// cleanup
		if (pDirEnum) {
			delete pDirEnum;
		}

		// close the opened directory
		if (pDir) {
			delete pDir;
		}
	}

	return result;
}

AbstractFSNode *TizenFilesystemNode::getParent() const {
  logEntered();
	if (_path == "/") {
		return 0; // The filesystem root has no parent
	}

	const char *start = _path.c_str();
	const char *end = start + _path.size();

	// Strip of the last component. We make use of the fact that at this
	// point, path is guaranteed to be normalized
	while (end > start && *(end-1) != '/') {
		end--;
	}

	if (end == start) {
		// This only happens if we were called with a relative path, for which
		// there simply is no parent.
		// TODO: We could also resolve this by assuming that the parent is the
		//			 current working directory, and returning a node referring to that.
		return NULL;
	}

	return new TizenFilesystemNode(Common::String(start, end));
}

Common::SeekableReadStream *TizenFilesystemNode::createReadStream() {
	Common::SeekableReadStream *result = TizenFileStream::makeFromPath(_unicodePath, false);
	if (result != NULL) {
		_isValid = !IsFailed(File::GetAttributes(_unicodePath, _attr));
	}
	return result;
}

Common::WriteStream *TizenFilesystemNode::createWriteStream() {
	Common::WriteStream *result = TizenFileStream::makeFromPath(_unicodePath, true);
	if (result != NULL) {
		_isValid = !IsFailed(File::GetAttributes(_unicodePath, _attr));
	}
	return result;
}