// Copyright (c) 2015- PPSSPP Project.

// 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, version 2.0 or later versions.

// 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 2.0 for more details.

// A copy of the GPL 2.0 should have been included with the program.
// If not, see http://www.gnu.org/licenses/

// Official git repository and contact information can be found at
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.

#include <algorithm>
#include <thread>
#include <cstring>

#include "Common/Thread/ThreadUtil.h"
#include "Common/TimeUtil.h"
#include "Common/Log.h"
#include "Core/FileLoaders/RamCachingFileLoader.h"

// Takes ownership of backend.
RamCachingFileLoader::RamCachingFileLoader(FileLoader *backend)
	: ProxiedFileLoader(backend) {
	filesize_ = backend->FileSize();
	if (filesize_ > 0) {
		InitCache();
	}
}

RamCachingFileLoader::~RamCachingFileLoader() {
	if (filesize_ > 0) {
		ShutdownCache();
	}
}

bool RamCachingFileLoader::Exists() {
	if (exists_ == -1) {
		exists_ = ProxiedFileLoader::Exists() ? 1 : 0;
	}
	return exists_ == 1;
}

bool RamCachingFileLoader::ExistsFast() {
	if (exists_ == -1) {
		return ProxiedFileLoader::ExistsFast();
	}
	return exists_ == 1;
}

bool RamCachingFileLoader::IsDirectory() {
	if (isDirectory_ == -1) {
		isDirectory_ = ProxiedFileLoader::IsDirectory() ? 1 : 0;
	}
	return isDirectory_ == 1;
}

s64 RamCachingFileLoader::FileSize() {
	return filesize_;
}

size_t RamCachingFileLoader::ReadAt(s64 absolutePos, size_t bytes, void *data, Flags flags) {
	size_t readSize = 0;
	if (cache_ == nullptr || (flags & Flags::HINT_UNCACHED) != 0) {
		readSize = backend_->ReadAt(absolutePos, bytes, data, flags);
	} else {
		readSize = ReadFromCache(absolutePos, bytes, data);
		// While in case the cache size is too small for the entire read.
		while (readSize < bytes) {
			SaveIntoCache(absolutePos + readSize, bytes - readSize, flags);
			size_t bytesFromCache = ReadFromCache(absolutePos + readSize, bytes - readSize, (u8 *)data + readSize);
			readSize += bytesFromCache;
			if (bytesFromCache == 0) {
				// We can't read any more.
				break;
			}
		}

		StartReadAhead(absolutePos + readSize);
	}
	return readSize;
}

void RamCachingFileLoader::InitCache() {
	std::lock_guard<std::mutex> guard(blocksMutex_);
	u32 blockCount = (u32)((filesize_ + BLOCK_SIZE - 1) >> BLOCK_SHIFT);
	// Overallocate for the last block.
	cache_ = (u8 *)malloc((size_t)blockCount << BLOCK_SHIFT);
	if (cache_ == nullptr) {
		ERROR_LOG(Log::IO, "Failed to allocate cache for Cache full ISO in RAM! Will fall back to regular reads.");
		return;
	}
	aheadRemaining_ = blockCount;
	blocks_.resize(blockCount);
}

void RamCachingFileLoader::ShutdownCache() {
	Cancel();

	// We can't delete while the thread is running, so have to wait.
	// This should only happen from the menu.
	if (aheadThread_.joinable())
		aheadThread_.join();

	_dbg_assert_(!aheadThreadRunning_);

	std::lock_guard<std::mutex> guard(blocksMutex_);
	blocks_.clear();
	if (cache_ != nullptr) {
		free(cache_);
		cache_ = nullptr;
	}
}

void RamCachingFileLoader::Cancel() {
	if (aheadThreadRunning_) {
		std::lock_guard<std::mutex> guard(blocksMutex_);
		aheadCancel_ = true;
	}

	ProxiedFileLoader::Cancel();
}

size_t RamCachingFileLoader::ReadFromCache(s64 pos, size_t bytes, void *data) {
	s64 cacheStartPos = pos >> BLOCK_SHIFT;
	s64 cacheEndPos = (pos + bytes - 1) >> BLOCK_SHIFT;
	if ((size_t)cacheEndPos >= blocks_.size()) {
		cacheEndPos = blocks_.size() - 1;
	}

	size_t readSize = 0;
	size_t offset = (size_t)(pos - (cacheStartPos << BLOCK_SHIFT));
	u8 *p = (u8 *)data;

	// Clamp bytes to what's actually available.
	if (pos + (s64)bytes > filesize_) {
		// Should've been caught above, but just in case.
		if (pos >= filesize_) {
			return 0;
		}
		bytes = (size_t)(filesize_ - pos);
	}

	std::lock_guard<std::mutex> guard(blocksMutex_);
	for (s64 i = cacheStartPos; i <= cacheEndPos; ++i) {
		if (blocks_[(size_t)i] == 0) {
			return readSize;
		}

		size_t toRead = std::min(bytes - readSize, (size_t)BLOCK_SIZE - offset);
		s64 cachePos = (i << BLOCK_SHIFT) + offset;
		memcpy(p + readSize, &cache_[cachePos], toRead);
		readSize += toRead;

		// Don't need an offset after the first read.
		offset = 0;
	}
	return readSize;
}

void RamCachingFileLoader::SaveIntoCache(s64 pos, size_t bytes, Flags flags) {
	s64 cacheStartPos = pos >> BLOCK_SHIFT;
	s64 cacheEndPos = (pos + bytes - 1) >> BLOCK_SHIFT;
	if ((size_t)cacheEndPos >= blocks_.size()) {
		cacheEndPos = blocks_.size() - 1;
	}

	size_t blocksToRead = 0;
	{
		std::lock_guard<std::mutex> guard(blocksMutex_);
		for (s64 i = cacheStartPos; i <= cacheEndPos; ++i) {
			if (blocks_[(size_t)i] == 0) {
				++blocksToRead;
				if (blocksToRead >= MAX_BLOCKS_PER_READ) {
					break;
				}

				// TODO: Shouldn't we break as soon as we see a 1?
			}
		}
	}

	s64 cacheFilePos = cacheStartPos << BLOCK_SHIFT;
	size_t bytesRead = backend_->ReadAt(cacheFilePos, blocksToRead << BLOCK_SHIFT, &cache_[cacheFilePos], flags);

	// In case there was an error, let's not mark blocks that failed to read as read.
	u32 blocksActuallyRead = (u32)((bytesRead + BLOCK_SIZE - 1) >> BLOCK_SHIFT);
	{
		std::lock_guard<std::mutex> guard(blocksMutex_);

		// In case they were simultaneously read.
		u32 blocksRead = 0;
		for (size_t i = 0; i < blocksActuallyRead; ++i) {
			if (blocks_[(size_t)cacheStartPos + i] == 0) {
				blocks_[(size_t)cacheStartPos + i] = 1;
				++blocksRead;
			}
		}

		if (aheadRemaining_ != 0) {
			aheadRemaining_ -= blocksRead;
		}
	}
}

void RamCachingFileLoader::StartReadAhead(s64 pos) {
	if (cache_ == nullptr) {
		return;
	}

	std::lock_guard<std::mutex> guard(blocksMutex_);
	aheadPos_ = pos;
	if (aheadThreadRunning_) {
		// Already going.
		return;
	}

	aheadThreadRunning_ = true;
	aheadCancel_ = false;
	if (aheadThread_.joinable())
		aheadThread_.join();
	aheadThread_ = std::thread([this] {
		SetCurrentThreadName("FileLoaderReadAhead");

		AndroidJNIThreadContext jniContext;

		while (aheadRemaining_ != 0 && !aheadCancel_) {
			// Where should we look?
			const u32 cacheStartPos = NextAheadBlock();
			if (cacheStartPos == 0xFFFFFFFF) {
				// Must be full.
				break;
			}
			u32 cacheEndPos = cacheStartPos + BLOCK_READAHEAD - 1;
			if (cacheEndPos >= blocks_.size()) {
				cacheEndPos = (u32)blocks_.size() - 1;
			}

			for (u32 i = cacheStartPos; i <= cacheEndPos; ++i) {
				if (blocks_[i] == 0) {
					SaveIntoCache((u64)i << BLOCK_SHIFT, BLOCK_SIZE * BLOCK_READAHEAD, Flags::NONE);
					break;
				}
			}
		}

		aheadThreadRunning_ = false;
	});
}

u32 RamCachingFileLoader::NextAheadBlock() {
	std::lock_guard<std::mutex> guard(blocksMutex_);

	// If we had an aheadPos_ set, start reading from there and go forward.
	u32 startFrom = (u32)(aheadPos_ >> BLOCK_SHIFT);
	// But next time, start from the beginning again.
	aheadPos_ = 0;

	for (u32 i = startFrom; i < blocks_.size(); ++i) {
		if (blocks_[i] == 0) {
			return i;
		}
	}

	return 0xFFFFFFFF;
}
