/****************************************************************************
 * Copyright (C) 2011 Dimok
 *
 * 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/>.
 ****************************************************************************/
#include <stdio.h>
#include <malloc.h>
#include <string.h>
#include "prompts/ProgressWindow.h"
#include "FileOperations/fileops.h"
#include "language/gettext.h"
#include "utils/ShowError.h"
#include "utils/tools.h"
#include "wad.h"

extern "C"
{
	void aes_set_key(u8 *key);
	void aes_decrypt(u8 *iv, u8 *inbuf, u8 *outbuf, unsigned long long len);
	void _decrypt_title_key(u8 *tik, u8 *title_key);
}

typedef struct map_entry
{
	char name[8];
	u8 hash[20];
} __attribute__((packed)) map_entry_t;

typedef struct uid_entry {
	u64 title_id;
	u32 uid;
} __attribute__((packed)) uid_entry_t;

Wad::Wad(const char *wadpath)
	: pFile(0), header(0),
	  p_tik(0), p_tmd(0),
	  content_map(0), content_map_size(0),
	  content_start(0)
{
	Open(wadpath);
}

Wad::~Wad()
{
	Close();
}

void Wad::Close(void)
{
	if(pFile)
		fclose(pFile);
	if(header)
		free(header);
	if(p_tik)
		free(p_tik);
	if(p_tmd)
		free(p_tmd);
	if(content_map)
		free(content_map);

	pFile = 0;
	header = 0;
	p_tik = 0;
	p_tmd = 0;
	content_map = 0;
	content_map_size = 0;
}

bool Wad::Open(const char *wadpath)
{
	if(!wadpath)
		return false;

	// Close if another file is opened already
	Close();

	// Open file
	pFile = fopen(wadpath, "rb");
	if(!pFile)
	{
		ShowError(tr("Can't open file: %s"), wadpath);
		return false;
	}

	// Read wad header
	header = (wadHeader *) malloc(sizeof(wadHeader));
	if(!header)
	{
		ShowError(tr("Not enough memory."));
		return false;
	}

	if(fread(header, 1, sizeof(wadHeader), pFile) != sizeof(wadHeader))
	{
		ShowError(tr("Failed to read wad header."));
		return false;
	}

	// Check for sanity
	if(header->header_len != sizeof(wadHeader))
	{
		ShowError(tr("Invalid wad file."));
		return false;
	}

	u32 offset = round_up( header->header_len, 64 );
	offset += round_up( header->certs_len, 64 );
	if (header->crl_len)
		offset += round_up( header->crl_len, 64 );

	// Read title ticket
	p_tik = (u8 *) malloc(header->tik_len);
	if(!p_tik)
	{
		ShowError(tr("Not enough memory."));
		return false;
	}

	fseek(pFile, offset, SEEK_SET);

	if(fread(p_tik, 1, header->tik_len, pFile) != header->tik_len)
	{
		ShowError(tr("Failed to read ticket."));
		return false;
	}

	offset += round_up( header->tik_len, 64 );

	// Read title tmd
	p_tmd = (u8 *) malloc(header->tmd_len);
	if(!p_tik)
	{
		ShowError(tr("Not enough memory."));
		return false;
	}

	fseek(pFile, offset, SEEK_SET);

	if(fread(p_tmd, 1, header->tmd_len, pFile) != header->tmd_len)
	{
		ShowError(tr("Failed to read tmd file."));
		return false;
	}

	offset += round_up( header->tmd_len, 64 );

	// Prepare offset for install
	content_start = offset;

	return true;
}

bool Wad::UnInstall(const char *installpath)
{
	if(!installpath || !pFile || !header || !p_tmd || !p_tik)
		return false;

	char filepath[1024];
	tmd *tmd_data = (tmd *) SIGNATURE_PAYLOAD((signed_blob *) p_tmd);

	int result = true;

	// Remove ticket
	snprintf(filepath, sizeof(filepath), "%s/ticket/%08x/%08x.tik", installpath, (u32) (tmd_data->title_id >> 32), (u32) tmd_data->title_id);
	if(!RemoveFile(filepath))
		result = false;

	// Remove contents / data
	snprintf(filepath, sizeof(filepath), "%s/title/%08x/%08x/", installpath, (u32) (tmd_data->title_id >> 32), (u32) tmd_data->title_id);
	if(!RemoveDirectory(filepath))
		result = false;

	return result;
}

bool Wad::Install(const char *installpath)
{
	if(!installpath || !pFile || !header || !p_tmd || !p_tik)
		return false;

	char filepath[1024];
	u8 title_key[16];
	tmd *tmd_data = (tmd *) SIGNATURE_PAYLOAD((signed_blob *) p_tmd);

	// Create necessary folders if not existing
	snprintf(filepath, sizeof(filepath), "%s/ticket/%08x/", installpath, (u32) (tmd_data->title_id >> 32));
	CreateSubfolder(filepath);

	snprintf(filepath, sizeof(filepath), "%s/title/%08x/%08x/content/", installpath, (u32) (tmd_data->title_id >> 32), (u32) tmd_data->title_id);
	CreateSubfolder(filepath);

	snprintf(filepath, sizeof(filepath), "%s/title/%08x/%08x/data/", installpath, (u32) (tmd_data->title_id >> 32), (u32) tmd_data->title_id);
	CreateSubfolder(filepath);

	// Write ticket file
	snprintf(filepath, sizeof(filepath), "%s/ticket/%08x/%08x.tik", installpath, (u32) (tmd_data->title_id >> 32), (u32) tmd_data->title_id);
	if(!WriteFile(filepath, p_tik, header->tik_len))
		return false;

	// Write tmd file
	snprintf(filepath, sizeof(filepath), "%s/title/%08x/%08x/content/title.tmd", installpath, (u32) (tmd_data->title_id >> 32), (u32) tmd_data->title_id);
	if(!WriteFile(filepath, p_tmd, header->tmd_len))
		return false;

	// Get title key and prepare decryption
	_decrypt_title_key(p_tik, title_key);
	aes_set_key(title_key);

	// Start progress
	ProgressCancelEnable(true);
	StartProgress(0, 0, 0, true, true);

	// Install contens
	bool result = InstallContents(installpath);

	// Stop progress
	ProgressStop();
	ProgressCancelEnable(false);

	if(!result)
		return false;

	// Update /sys/uid.sys
	if(!SetTitleUID(installpath, tmd_data->title_id))
		return false;

	return true;
}

bool Wad::WriteFile(const char *filepath, u8 *buffer, u32 len)
{
	FILE *f = fopen(filepath, "wb");
	if(!f)
	{
		ShowError(tr("Can't create file: %s"), filepath);
		return false;
	}

	u32 write = fwrite(buffer, 1, len, f);
	fclose(f);

	if(write != len)
		ShowError(tr("Write error on file: %s"), filepath);

	return (write == len);
}

bool Wad::InstallContents(const char *installpath)
{
	const u32 blocksize = 50 * 1024;
	u16 cnt;
	u32 totalDone = 0;
	u32 totalSize = 0;
	u32 offset = content_start;
	char filepath[1024];
	char progressTxt[80];
	u8 iv[16];

	// tmd
	tmd *tmd_data = (tmd *) SIGNATURE_PAYLOAD((signed_blob *) p_tmd);

	// Get total size for progress bar
	for (cnt = 0; cnt < tmd_data->num_contents; cnt++)
	{
		if(tmd_data->contents[cnt].type == 0x8001) {
			// shared content
			int result = CheckContentMap(installpath, &tmd_data->contents[cnt], filepath);
			if(result == 1) // exists already, skip file
				continue;
		}
		totalSize += round_up( tmd_data->contents[cnt].size, 64 );
	}

	for (cnt = 0; cnt < tmd_data->num_contents; cnt++)
	{
		if(ProgressCanceled())
			break;

		if(cnt > 0)
			offset += round_up( tmd_data->contents[cnt-1].size, 64);

		u32 done = 0, len;
		tmd_content *content = &tmd_data->contents[cnt];

		// Encrypted content size
		len = round_up( content->size, 64 );

		// Prepare iv for decryption
		memset(iv, 0, sizeof(iv));
		memcpy(iv, &cnt, 2);

		// Install content
		if(content->type == 0x8001) {
			// shared content
			int result = CheckContentMap(installpath, content, filepath);
			if(result == 1) // exists already, skip file
				continue;

			else if(result < 0) // failure
				return false;
			// else it does not exist...install it
		}
		else {
			// private content
			snprintf(filepath, sizeof(filepath), "%s/title/%08x/%08x/content/%08x.app", installpath, (u32) (tmd_data->title_id >> 32), (u32) tmd_data->title_id, content->cid);
		}

		// Create file
		FILE *fp = fopen(filepath, "wb");
		if(!fp)
		{
			ShowError(tr("Can't create file: %s"), filepath);
			return false;
		}

		u8 * inbuf = (u8 *) malloc(blocksize);
		u8 * outbuf = (u8 *) malloc(blocksize);
		if(!inbuf || !outbuf)
		{
			ShowError(tr("Not enough memory."));
			if(inbuf) free(inbuf);
			if(outbuf) free(outbuf);
			fclose(fp);
			return false;
		}

		snprintf(progressTxt, sizeof(progressTxt), "%s %08x.app", tr("Installing content"), content->cid);

		// Go to position
		fseek(pFile, offset, SEEK_SET);

		// Install content data
		while (done < len)
		{
			if(ProgressCanceled())
			{
				done = len;
				break;
			}

			ShowProgress(tr("Installing title..."), progressTxt, 0, totalDone + done, totalSize, true, true);

			// Data length
			u32 size = (len - done);
			if (size > blocksize)
				size = blocksize;

			// Read data
			if(fread(inbuf, 1, size, pFile) != size)
				break;

			// Decrypt data
			aes_decrypt(iv, inbuf, outbuf, size);

			// Write data
			if(fwrite(outbuf, 1, size, fp) != size)
				break;

			// Set new iv for next read chunk
			memcpy(iv, inbuf + blocksize - 16, 16);

			// Increase variables
			done += size;
		}

		// done
		free(inbuf);
		free(outbuf);
		fclose(fp);

		// update progress variable
		totalDone += len;

		// Check if the read/write process stopped before finishing
		if(done < len)
		{
			ShowError(tr("File read/write error."));
			return false;
		}
	}

	return true;
}

int Wad::CheckContentMap(const char *installpath, tmd_content *content, char *filepath)
{
	if(!content_map)
	{
		// Get and keep content map in memory
		snprintf(filepath, 1024, "%s/shared1/content.map", installpath);
		if(LoadFileToMem(filepath, &content_map, &content_map_size) < 0 || content_map_size < sizeof(map_entry_t))
		{
			ShowError(tr("Can't read file: %s"), filepath);
			return -1;
		}

		content_map_size /=  sizeof(map_entry_t);
	}

	map_entry_t *map = (map_entry_t *) content_map;

	for(u32 n = 0; n < content_map_size; n++)
	{
		if(memcmp(map[n].hash, content->hash, 20) == 0)
			return 1; // content exists already
	}

	// Content does not exists, append it.
	u32 next_entry = content_map_size;
	u8 *tmp = (u8 *) realloc(content_map, (next_entry + 1) * sizeof(map_entry_t));
	if(!tmp)
	{
		ShowError(tr("Not enough memory."));
		return -1;
	}

	content_map = tmp;
	content_map_size++;

	map = (map_entry_t *) content_map;
	sprintf(map[next_entry].name, "%08x", next_entry);
	memcpy(map[next_entry].hash, content->hash, 20);

	// write new content.map
	snprintf(filepath, 1024, "%s/shared1/content.map", installpath);
	if(!WriteFile(filepath, content_map, content_map_size * sizeof(map_entry_t)))
		return -1;

	snprintf(filepath, 1024, "%s/shared1/%08x.app", installpath, next_entry);

	return 0;
}

bool Wad::SetTitleUID(const char *installpath, const u64 &tid)
{
	char filepath[1024];
	u8 *uid_sys = NULL;
	u32 uid_sys_size = 0;

	// Read in uid.sys file
	snprintf(filepath, sizeof(filepath), "%s/sys/uid.sys", installpath);

	if(LoadFileToMem(filepath, &uid_sys, &uid_sys_size) < 0 || uid_sys_size < sizeof(uid_entry_t))
	{
		ShowError(tr("Can't read file: %s"), filepath);
		if(uid_sys) free(uid_sys);
		return false;
	}

	uid_sys_size /=  sizeof(uid_entry_t);
	uid_entry_t *map = (uid_entry_t *) uid_sys;

	for(u32 i = 0; i < uid_sys_size; ++i)
	{
		if(map[i].title_id == tid)
		{
			// title entry exists already
			free(uid_sys);
			return true;
		}
	}

	// title entry does not exists, append it
	u32 next_entry = uid_sys_size;
	u8 *tmp = (u8 *) realloc(uid_sys, (next_entry + 1) * sizeof(uid_entry_t));
	if(!tmp)
	{
		ShowError(tr("Not enough memory."));
		free(uid_sys);
		return -1;
	}

	uid_sys = tmp;
	uid_sys_size++;

	map = (uid_entry_t *) uid_sys;
	map[next_entry].title_id = tid;
	map[next_entry].uid = map[next_entry-1].uid + 1;

	// write new uid.sys
	bool result = WriteFile(filepath, uid_sys, uid_sys_size * sizeof(uid_entry_t));

	free(uid_sys);

	return result;
}
