// Copyright (c) 2012- 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/.

#pragma once

#include <vector>
#include <map>
#include <mutex>

#include "Common/CommonTypes.h"
#include "Common/File/Path.h"
#include "Common/Swap.h"
#include "Core/Loaders.h"

class DiskCachingFileLoaderCache;

class DiskCachingFileLoader : public ProxiedFileLoader {
public:
	DiskCachingFileLoader(FileLoader *backend);
	~DiskCachingFileLoader();

	bool Exists() override;
	bool ExistsFast() override;
	s64 FileSize() override;

	size_t ReadAt(s64 absolutePos, size_t bytes, size_t count, void *data, Flags flags = Flags::NONE) override {
		return ReadAt(absolutePos, bytes * count, data, flags) / bytes;
	}
	size_t ReadAt(s64 absolutePos, size_t bytes, void *data, Flags flags = Flags::NONE) override;

	static std::vector<Path> GetCachedPathsInUse();

private:
	void Prepare();
	void InitCache();
	void ShutdownCache();

	std::once_flag preparedFlag_;
	s64 filesize_ = 0;
	DiskCachingFileLoaderCache *cache_ = nullptr;

	// We don't support concurrent disk cache access (we use memory cached indexes.)
	// So we have to ensure there's only one of these per.
	static std::map<Path, DiskCachingFileLoaderCache *> caches_;
	static std::mutex cachesMutex_;
};

class DiskCachingFileLoaderCache {
public:
	DiskCachingFileLoaderCache(const Path &path, u64 filesize);
	~DiskCachingFileLoaderCache();

	bool IsValid() {
		return f_ != nullptr;
	}

	void AddRef() {
		++refCount_;
	}

	bool Release() {
		return --refCount_ == 0;
	}

	static void SetCacheDir(const Path &path) {
		cacheDir_ = path;
	}

	size_t ReadFromCache(s64 pos, size_t bytes, void *data);
	// Guaranteed to read at least one block into the cache.
	size_t SaveIntoCache(FileLoader *backend, s64 pos, size_t bytes, void *data, FileLoader::Flags flags);

	bool HasData() const;

private:
	void InitCache(const Path &path);
	void ShutdownCache();
	bool MakeCacheSpaceFor(size_t blocks);
	void RebalanceGenerations();
	u32 AllocateBlock(u32 indexPos);

	struct BlockInfo;
	bool ReadBlockData(u8 *dest, BlockInfo &info, size_t offset, size_t size);
	void WriteBlockData(BlockInfo &info, const u8 *src);
	void WriteIndexData(u32 indexPos, BlockInfo &info);
	s64 GetBlockOffset(u32 block);

	Path MakeCacheFilePath(const Path &filename);
	std::string MakeCacheFilename(const Path &path);
	bool LoadCacheFile(const Path &path);
	void LoadCacheIndex();
	void CreateCacheFile(const Path &path);
	bool LockCacheFile(bool lockStatus);
	bool RemoveCacheFile(const Path &path);
	void CloseFileHandle();

	u64 FreeDiskSpace();
	u32 DetermineMaxBlocks();
	u32 CountCachedFiles();
	void GarbageCollectCacheFiles(u64 goalBytes);

	// File format:
	// 64 magic
	// 32 version
	// 32 blockSize
	// 64 filesize
	// 32 maxBlocks
	// 32 flags
	// index[filesize / blockSize] <-- ~500 KB for 4GB
	//   32 (fileoffset - headersize) / blockSize -> -1=not present
	//   16 generation?
	//   16 hits?
	// blocks[up to maxBlocks]
	//   8 * blockSize

	enum {
		CACHE_VERSION = 3,
		DEFAULT_BLOCK_SIZE = 65536,
		MAX_BLOCKS_PER_READ = 16,
		MAX_BLOCKS_LOWER_BOUND = 256, // 16 MB
		MAX_BLOCKS_UPPER_BOUND = 8192, // 512 MB
		INVALID_BLOCK = 0xFFFFFFFF,
		INVALID_INDEX = 0xFFFFFFFF,
	};

	int refCount_ = 0;
	s64 filesize_;
	u32 blockSize_;
	u16 generation_;
	u16 oldestGeneration_;
	u32 maxBlocks_;
	u32 flags_;
	size_t cacheSize_;
	size_t indexCount_;
	std::mutex lock_;
	Path origPath_;

	struct FileHeader {
		char magic[8];
		u32_le version;
		u32_le blockSize;
		s64_le filesize;
		u32_le maxBlocks;
		u32_le flags;
	};

	enum FileFlags {
		FLAG_LOCKED = 1 << 0,
	};

	struct BlockInfo {
		u32 block;
		u16 generation;
		u16 hits;

		BlockInfo() : block(-1), generation(0), hits(0) {
		}
	};

	std::vector<BlockInfo> index_;
	std::vector<u32> blockIndexLookup_;

	FILE *f_ = nullptr;
	int fd_ = 0;

	static Path cacheDir_;
};