#include "NcchProcess.h"
#include "lzss.h"
#include <tc/io.h>
#include <tc/cli.h>
#include <tc/crypto.h>
#include <tc/ArgumentNullException.h>

#include <ntd/n3ds/CtrKeyGenerator.h>
#include <ntd/n3ds/exheader.h>
#include "ExHeaderProcess.h"
#include "ExeFsProcess.h"
#include "IvfcProcess.h"

ctrtool::NcchProcess::NcchProcess() :
	mModuleLabel("ctrtool::NcchProcess"),
	mInputStream(),
	mVerbose(false),
	mVerify(false),
	mRaw(false),
	mPlain(false),
	mShowSyscallNames(false),
	mContentSize(0),
	mBlockSize(0),
	mDecompressExeFsCode(false)
{
	memset((byte_t*)&mHeader, 0, sizeof(ntd::n3ds::NcchHeader));
	for (size_t i = 0; i < NcchRegionNum; i++)
	{
		mRegionOpt[i].show_info = true;
		mRegionOpt[i].show_fs = false;
		mRegionOpt[i].bin_extract_path.makeNull();
		mRegionOpt[i].fs_extract_path.makeNull();

		mRegionInfo[i].valid = ValidState::Unchecked;
		mRegionInfo[i].offset = 0;
		mRegionInfo[i].size = 0;
		mRegionInfo[i].hashed_offset = 0;
		mRegionInfo[i].hashed_size = 0;
		mRegionInfo[i].raw_stream = nullptr;
		mRegionInfo[i].ready_stream = nullptr;
	}
}

void ctrtool::NcchProcess::setInputStream(const std::shared_ptr<tc::io::IStream>& input_stream)
{
	mInputStream = input_stream;
}

void ctrtool::NcchProcess::setKeyBag(const ctrtool::KeyBag& key_bag)
{
	mKeyBag = key_bag;
}

void ctrtool::NcchProcess::setVerboseMode(bool verbose)
{
	mVerbose = verbose;
}

void ctrtool::NcchProcess::setVerifyMode(bool verify)
{
	mVerify = verify;
}

void ctrtool::NcchProcess::setRawMode(bool raw)
{
	mRaw = raw;
}

void ctrtool::NcchProcess::setPlainMode(bool plain)
{
	mPlain = plain;
}

void ctrtool::NcchProcess::setShowSyscallName(bool show_name)
{
	mShowSyscallNames = show_name;
}


void ctrtool::NcchProcess::setRegionProcessOutputMode(NcchRegion region, bool show_info, bool show_fs, const tc::Optional<tc::io::Path>& bin_extract_path, const tc::Optional<tc::io::Path>& fs_extract_path)
{
	mRegionOpt[region].show_info = show_info;
	mRegionOpt[region].show_fs = show_fs;
	mRegionOpt[region].bin_extract_path = bin_extract_path;
	mRegionOpt[region].fs_extract_path = fs_extract_path;
}

void ctrtool::NcchProcess::process()
{
	importHeader();
	determineRegionLayout();
	determineRegionEncryption();
	if (mVerify)
		verifyRegions();
	if (mRegionOpt[NcchRegion_Header].show_info)
		printHeader();
	extractRegionBinaries();
	processRegions();
}

void ctrtool::NcchProcess::importHeader()
{
	// validate input stream
	if (mInputStream == nullptr)
	{
		throw tc::ArgumentNullException(mModuleLabel, "Input stream was null.");
	}
	if (mInputStream->canRead() == false || mInputStream->canSeek() == false)
	{
		throw tc::InvalidOperationException(mModuleLabel, "Input stream requires read/seek permissions.");
	}

	// import header
	if (mInputStream->length() < sizeof(ntd::n3ds::NcchHeader))
	{
		throw tc::InvalidOperationException(mModuleLabel, "Input stream too small.");
	}
	mInputStream->seek(0, tc::io::SeekOrigin::Begin);
	mInputStream->read((byte_t*)&mHeader, sizeof(ntd::n3ds::NcchHeader));

	// check the struct magic
	if (mHeader.header.struct_magic.unwrap() != ntd::n3ds::NcchCommonHeader::kStructMagic)
	{
		throw tc::InvalidOperationException(mModuleLabel, "NcchHeader is corrupted (Bad struct magic).");
	}


	// determine block size
	switch (mHeader.header.format_version.unwrap())
	{
		// CFA
		case ntd::n3ds::NcchCommonHeader::FormatVersion_CFA:
		// CXI
		case ntd::n3ds::NcchCommonHeader::FormatVersion_CXI:
			mBlockSize = static_cast<int64_t>(1) << (mHeader.header.flags.block_size_log + 9);
			break;
		// Prototype
		case ntd::n3ds::NcchCommonHeader::FormatVersion_CXI_PROTOTYPE:
			mBlockSize = static_cast<int64_t>(1);
			break;
		default:
			throw tc::InvalidOperationException(mModuleLabel, fmt::format("NcchHeader has unsupported format version. (0x{:02x})", mHeader.header.format_version.unwrap()));

	}


	if (mHeader.header.exhdr_size.unwrap() != 0 && mHeader.header.exhdr_size.unwrap() != sizeof(ntd::n3ds::ExtendedHeader))
	{
		throw tc::InvalidOperationException(mModuleLabel, fmt::format("NcchHeader has invalid ExHeader size. (0x{:02x})", mHeader.header.exhdr_size.unwrap()));
	}

	// get content size
	mContentSize = int64_t(mHeader.header.content_blk_size.unwrap()) * mBlockSize;

	if (mInputStream->length() < mContentSize)
	{
		throw tc::InvalidOperationException(mModuleLabel, "Input stream too small.");
	}
}

void ctrtool::NcchProcess::determineRegionLayout()
{
	// get region layout
	mRegionInfo[NcchRegion_Header].offset = 0;
	mRegionInfo[NcchRegion_Header].size = sizeof(ntd::n3ds::NcchHeader);
	mRegionInfo[NcchRegion_Header].hashed_offset = sizeof(ntd::n3ds::NcchHeader::signature);
	mRegionInfo[NcchRegion_Header].hashed_size = sizeof(ntd::n3ds::NcchCommonHeader);

	if (mHeader.header.exhdr_size.unwrap() > 0)
	{
		mRegionInfo[NcchRegion_ExHeader].offset = sizeof(ntd::n3ds::NcchHeader);
		mRegionInfo[NcchRegion_ExHeader].size = mHeader.header.exhdr_size.unwrap() + sizeof(ntd::n3ds::AccessDescriptor);
		mRegionInfo[NcchRegion_ExHeader].hashed_offset = 0;
		mRegionInfo[NcchRegion_ExHeader].hashed_size = mHeader.header.exhdr_size.unwrap();
	}
	if (mHeader.header.format_version.unwrap() == ntd::n3ds::NcchCommonHeader::FormatVersion_CXI_PROTOTYPE && mHeader.header.exhdr_hash[0] != 0)
	{
		mRegionInfo[NcchRegion_ExHeader].offset = sizeof(ntd::n3ds::NcchHeader);
		mRegionInfo[NcchRegion_ExHeader].size = sizeof(ntd::n3ds::ExtendedHeader) + sizeof(ntd::n3ds::AccessDescriptor);
		mRegionInfo[NcchRegion_ExHeader].hashed_offset = 0;
		mRegionInfo[NcchRegion_ExHeader].hashed_size = mHeader.header.exhdr_size.unwrap();
	}
	if (mHeader.header.plain_region_blk_size.unwrap() > 0)
	{
		mRegionInfo[NcchRegion_PlainRegion].offset = mHeader.header.plain_region_blk_offset.unwrap() * mBlockSize;
		mRegionInfo[NcchRegion_PlainRegion].size = mHeader.header.plain_region_blk_size.unwrap() * mBlockSize;
		mRegionInfo[NcchRegion_PlainRegion].hashed_offset = 0;
		mRegionInfo[NcchRegion_PlainRegion].hashed_size = 0;
	}
	if (mHeader.header.logo_blk_size.unwrap() > 0)
	{
		mRegionInfo[NcchRegion_Logo].offset = mHeader.header.logo_blk_offset.unwrap() * mBlockSize;
		mRegionInfo[NcchRegion_Logo].size = mHeader.header.logo_blk_size.unwrap() * mBlockSize;
		mRegionInfo[NcchRegion_Logo].hashed_offset = 0;
		mRegionInfo[NcchRegion_Logo].hashed_size = mRegionInfo[NcchRegion_Logo].size;
	}
	if (mHeader.header.exefs_blk_size.unwrap() > 0)
	{
		mRegionInfo[NcchRegion_ExeFs].offset = mHeader.header.exefs_blk_offset.unwrap() * mBlockSize;
		mRegionInfo[NcchRegion_ExeFs].size = mHeader.header.exefs_blk_size.unwrap() * mBlockSize;
		mRegionInfo[NcchRegion_ExeFs].hashed_offset = 0;
		mRegionInfo[NcchRegion_ExeFs].hashed_size = mHeader.header.exefs_prot_blk_size.unwrap() * mBlockSize;
	}
	if (mHeader.header.romfs_blk_size.unwrap() > 0)
	{
		mRegionInfo[NcchRegion_RomFs].offset = mHeader.header.romfs_blk_offset.unwrap() * mBlockSize;
		mRegionInfo[NcchRegion_RomFs].size = mHeader.header.romfs_blk_size.unwrap() * mBlockSize;
		mRegionInfo[NcchRegion_RomFs].hashed_offset = 0;
		mRegionInfo[NcchRegion_RomFs].hashed_size = mHeader.header.romfs_prot_blk_size.unwrap() * mBlockSize;
	}
	
	// create raw streams
	for (size_t i = 0; i < NcchRegionNum; i++)
	{
		if (mRegionInfo[i].size)
		{
			mRegionInfo[i].raw_stream = std::make_shared<tc::io::SubStream>(tc::io::SubStream(mInputStream, mRegionInfo[i].offset, mRegionInfo[i].size));
			// plain region & header are never encrypted
			if (i == NcchRegion_PlainRegion || i == NcchRegion_Header || i == NcchRegion_Logo)
			{
				mRegionInfo[i].ready_stream = mRegionInfo[i].raw_stream;
			}
		}
	}
}


void ctrtool::NcchProcess::determineRegionEncryption()
{
	// quick test to determine if the crypto layer has been stripped by tools like GodMode9, not strictly required, but this matches classic ctrtool behaviour
	bool crypto_is_stripped = false;
	if (mRegionInfo[NcchRegion_ExHeader].size >= sizeof(ntd::n3ds::ExtendedHeader) && mHeader.header.flags.other_flag.test(ntd::n3ds::NcchCommonHeader::OtherFlag_NoEncryption) == false)
	{
		std::array<byte_t, tc::crypto::Sha256Generator::kHashSize> exheader_hash;
		tc::ByteData exheader_data = tc::ByteData(sizeof(ntd::n3ds::ExtendedHeader));
		mRegionInfo[NcchRegion_ExHeader].raw_stream->seek(0, tc::io::SeekOrigin::Begin);
		mRegionInfo[NcchRegion_ExHeader].raw_stream->read(exheader_data.data(), exheader_data.size());
	
		tc::crypto::GenerateSha256Hash(exheader_hash.data(), exheader_data.data(), exheader_data.size());
		crypto_is_stripped = memcmp(exheader_hash.data(), mHeader.header.exhdr_hash.data(), exheader_hash.size()) == 0;
	}
	if (crypto_is_stripped)
	{
		fmt::print("[LOG/NCCH] NCCH appears to be decrypted, contrary to header flags.\n");
	}

	// determine encryption mode
	if (mHeader.header.flags.other_flag.test(ntd::n3ds::NcchCommonHeader::OtherFlag_NoEncryption) || mPlain || crypto_is_stripped)
	{
		// set ready streams to be not encrypted
		for (size_t i = 0; i < NcchRegionNum; i++)
		{
			if (mRegionInfo[i].size && mRegionInfo[i].ready_stream == nullptr && mRegionInfo[i].raw_stream != nullptr)
			{
				mRegionInfo[i].ready_stream = mRegionInfo[i].raw_stream;
			}
		}
	}
	else
	{
		struct KeySlot
		{
			byte_t valid_x;
			KeyBag::Aes128Key x;
			byte_t valid_y;
			KeyBag::Aes128Key y;
			byte_t valid_key;
			KeyBag::Aes128Key key;
		} keyslot[2];

		keyslot[0].valid_x = ValidState::Unchecked;
		keyslot[0].valid_y = ValidState::Unchecked;
		keyslot[0].valid_key = ValidState::Unchecked;
		keyslot[1].valid_x = ValidState::Unchecked;
		keyslot[1].valid_y = ValidState::Unchecked;
		keyslot[1].valid_key = ValidState::Unchecked;

		// if fixed AES key
		if (mHeader.header.flags.other_flag.test(ntd::n3ds::NcchCommonHeader::OtherFlag_FixedAesKey))
		{
			// set AES keys to fixed key

			// load aes keys
			auto key_itr = mKeyBag.ncch_fixed_key.find(isSystemTitle() ? mKeyBag.NCCH_SYSTEM_FIXED_KEY : mKeyBag.NCCH_APPLICATION_FIXED_KEY);
			if (key_itr == mKeyBag.ncch_fixed_key.end())
			{
				keyslot[0].valid_key = ValidState::Fail;
				keyslot[1].valid_key = ValidState::Fail;

				fmt::print(stderr, "Could not read {} fixed key.\n", (isSystemTitle()? "system" : "application"));
			}

			// save keys
			keyslot[0].key = key_itr->second;
			keyslot[1].key = key_itr->second;

			keyslot[0].valid_key = ValidState::Good;
			keyslot[1].valid_key = ValidState::Good;

		}
		else
		{
			// keyslot[0]
			// load key_x
			auto keyx_itr = mKeyBag.ncch_secure_key_x.find(0);
			if (keyx_itr == mKeyBag.ncch_secure_key_x.end())
			{
				keyslot[0].valid_x = ValidState::Fail;

				fmt::print(stderr, "Could not read secure key_x[0x{:02x}].\n", 0);
			}
			else
			{
				keyslot[0].x = keyx_itr->second;

				keyslot[0].valid_x = ValidState::Good;
			}
			
			// load key_y
			memcpy(keyslot[0].y.data(), mHeader.signature.data(), keyslot[0].y.size());
			keyslot[0].valid_y = ValidState::Good;
			
			// keyslot[1]
			// load key_x
			byte_t security_version = mHeader.header.flags.security_version;
			keyx_itr = mKeyBag.ncch_secure_key_x.find(security_version);
			if (keyx_itr == mKeyBag.ncch_secure_key_x.end())
			{
				keyslot[1].valid_x = ValidState::Fail;

				fmt::print(stderr, "Could not read secure key_x[0x{:02x}].\n", security_version);
			}
			else
			{
				keyslot[1].x = keyx_itr->second;

				keyslot[1].valid_x = ValidState::Good;
			}
			
			memcpy(keyslot[1].x.data(), keyx_itr->second.data(), keyslot[1].x.size());

			// load key_y
			if (mHeader.header.flags.other_flag.test(ntd::n3ds::NcchCommonHeader::OtherFlag_SeededAesKeyY))
			{
				tc::crypto::Sha256Generator hashgen;
				std::array<byte_t, tc::crypto::Sha256Generator::kHashSize> hash;

				// import seed
				KeyBag::Aes128Key seed;
				auto seed_itr = mKeyBag.seed_db.find(mHeader.header.program_id.unwrap());
				if (seed_itr != mKeyBag.seed_db.end())
				{
					memcpy(seed.data(), seed_itr->second.data(), seed.size());
				}
				else if (mKeyBag.fallback_seed.isSet())
				{
					memcpy(seed.data(), mKeyBag.fallback_seed.get().data(), seed.size());
				}
				else
				{
					keyslot[1].valid_y = ValidState::Fail;

					fmt::print(stderr, "This title uses seed crypto, but no seed is set, unable to decrypt.\n");
					fmt::print(stderr, "Use -p to avoid decryption or use --seeddb=dbfile or --seed=SEEDHERE.\n");
				}

				if (keyslot[1].valid_y != ValidState::Fail)
				{
					// validate seed
					hashgen.initialize();
					hashgen.update(seed.data(), seed.size());
					hashgen.update((byte_t*)&mHeader.header.program_id, sizeof(mHeader.header.program_id));
					hashgen.getHash(hash.data());
					if (memcmp(hash.data(), mHeader.header.seed_checksum.data(), mHeader.header.seed_checksum.size()) != 0)
					{
						keyslot[1].valid_y = ValidState::Fail;

						fmt::print(stderr, "Seed check mismatch. (Got {:08x}, expected: {:08x})\n",
							((tc::bn::be32<uint32_t>*)hash.data())->unwrap(),
							((tc::bn::be32<uint32_t>*)mHeader.header.seed_checksum.data())->unwrap());
					}
				}

				if (keyslot[1].valid_y != ValidState::Fail)
				{
					// generate seeded key_y
					hashgen.initialize();
					hashgen.update(keyslot[0].y.data(), keyslot[0].y.size());
					hashgen.update(seed.data(), seed.size());
					hashgen.getHash(hash.data());

					memcpy(keyslot[1].y.data(), hash.data(), keyslot[1].y.size());
					keyslot[1].valid_y = ValidState::Good;
				}
			}
			else
			{
				memcpy(keyslot[1].y.data(), mHeader.signature.data(), keyslot[1].y.size());
				keyslot[1].valid_y = ValidState::Good;
			}
			
			// generate secure keys
			if (keyslot[0].valid_x == ValidState::Good && keyslot[0].valid_y == ValidState::Good)
			{
				ntd::n3ds::CtrKeyGenerator::GenerateKey(keyslot[0].x.data(), keyslot[0].y.data(), keyslot[0].key.data());
				keyslot[0].valid_key = ValidState::Good;
			}
			else
			{
				keyslot[0].valid_key = ValidState::Fail;
			}

			if (keyslot[1].valid_x == ValidState::Good && keyslot[1].valid_y == ValidState::Good)
			{
				ntd::n3ds::CtrKeyGenerator::GenerateKey(keyslot[1].x.data(), keyslot[1].y.data(), keyslot[1].key.data());
				keyslot[1].valid_key = ValidState::Good;
			}
			else
			{
				keyslot[1].valid_key = ValidState::Fail;
			}	
		}

		// output keys if required
		if (mVerbose)
		{
			fmt::print("[LOG/NCCH] NCCH AES Key0 {}\n", (keyslot[0].valid_key ? tc::cli::FormatUtil::formatBytesAsString(keyslot[0].key.data(), keyslot[0].key.size(), true, "") : "could not be determined"));
			fmt::print("[LOG/NCCH] NCCH AES Key1 {}\n", (keyslot[1].valid_key ? tc::cli::FormatUtil::formatBytesAsString(keyslot[1].key.data(), keyslot[1].key.size(), true, "") : "could not be determined"));
		}

		// generate aes counter
		KeyBag::Aes128Key exheader_aesctr, exefs_aesctr, romfs_aesctr;
		if (mRegionInfo[NcchRegion_ExHeader].size)
		{
			getAesCounter(exheader_aesctr.data(), NcchRegion_ExHeader);

			if (mVerbose)
			{
				fmt::print("[LOG/NCCH] NCCH ExHeader AES Counter {}\n", tc::cli::FormatUtil::formatBytesAsString(exheader_aesctr.data(), exheader_aesctr.size(), true, ""));
			}
		}
		if (mRegionInfo[NcchRegion_ExeFs].size)
		{
			getAesCounter(exefs_aesctr.data(), NcchRegion_ExeFs);

			if (mVerbose)
			{
				fmt::print("[LOG/NCCH] NCCH ExeFS AES Counter {}\n", tc::cli::FormatUtil::formatBytesAsString(exefs_aesctr.data(), exefs_aesctr.size(), true, ""));
			}
		}
		if (mRegionInfo[NcchRegion_RomFs].size)
		{
			getAesCounter(romfs_aesctr.data(), NcchRegion_RomFs);

			if (mVerbose)
			{
				fmt::print("[LOG/NCCH] NCCH RomFS AES Counter {}\n", tc::cli::FormatUtil::formatBytesAsString(romfs_aesctr.data(), romfs_aesctr.size(), true, ""));
			}
		}

		// prepare ready_stream using encryption streams if keys are available
		if (mRegionInfo[NcchRegion_ExHeader].size && keyslot[0].valid_key == ValidState::Good)
		{
			mRegionInfo[NcchRegion_ExHeader].ready_stream = std::shared_ptr<tc::crypto::Aes128CtrEncryptedStream>(new tc::crypto::Aes128CtrEncryptedStream(mRegionInfo[NcchRegion_ExHeader].raw_stream, keyslot[0].key, exheader_aesctr));
		}
		if (mRegionInfo[NcchRegion_ExeFs].size && keyslot[0].valid_key == ValidState::Good)
		{
			// if key[1] is valid create the correct mixed key stream
			if (keyslot[1].valid_key == ValidState::Good)
			{
				// if the keys are the same, don't over complicate the encrypted stream
				if (false)//(memcmp(keyslot[0].key.data(), keyslot[1].key.data(), keyslot[0].key.size()) == 0)
				{
					mRegionInfo[NcchRegion_ExeFs].ready_stream = std::shared_ptr<tc::crypto::Aes128CtrEncryptedStream>(new tc::crypto::Aes128CtrEncryptedStream(mRegionInfo[NcchRegion_ExeFs].raw_stream, keyslot[0].key, exefs_aesctr));
				}
				else
				{
					// import ExeFs header
					if (mRegionInfo[NcchRegion_ExeFs].raw_stream->length() < sizeof(ntd::n3ds::ExeFsHeader))
					{
						throw tc::InvalidOperationException(mModuleLabel, "Stream is too small (cannot import ExeFsHeader).");
					}
					ntd::n3ds::ExeFsHeader exefs_hdr;
					mRegionInfo[NcchRegion_ExeFs].raw_stream->seek(0, tc::io::SeekOrigin::Begin);
					mRegionInfo[NcchRegion_ExeFs].raw_stream->read((byte_t*)&exefs_hdr, sizeof(exefs_hdr));

					tc::crypto::DecryptAes128Ctr((byte_t*)&exefs_hdr, (byte_t*)&exefs_hdr, sizeof(exefs_hdr), 0, keyslot[0].key.data(), keyslot[0].key.size(), exefs_aesctr.data(), exefs_aesctr.size());

					// quick header validation
					if (exefs_hdr.file_table[0].name[0] == 0 ||
					    exefs_hdr.file_table[0].offset.unwrap() != 0 ||
					    exefs_hdr.getFileHash(0)->operator[](0) == 0)
					{
						throw tc::ArgumentOutOfRangeException(mModuleLabel, "ExeFsHeader is corrupted (Bad first entry).");
					}

					// create key maps
					std::vector<std::shared_ptr<tc::io::IStream>> exefs_streams;
					exefs_streams.push_back(std::make_shared<tc::crypto::Aes128CtrEncryptedStream>(tc::crypto::Aes128CtrEncryptedStream(std::make_shared<tc::io::SubStream>(tc::io::SubStream(mRegionInfo[NcchRegion_ExeFs].raw_stream, 0x0, sizeof(ntd::n3ds::ExeFsHeader))), keyslot[0].key, exefs_aesctr)));
					byte_t keyslot_index;
					for (size_t i = 0; i < exefs_hdr.kFileNum; i++)
					{
						std::string file_name = exefs_hdr.file_table[i].name.decode();
						int64_t file_offset = sizeof(ntd::n3ds::ExeFsHeader) + int64_t(exefs_hdr.file_table[i].offset.unwrap());
						int64_t file_length = align<int64_t>(exefs_hdr.file_table[i].size.unwrap(), exefs_hdr.kExeFsSectionAlignSize);

						// skip empty sections
						if (exefs_hdr.file_table[i].size.unwrap() == 0)
							continue;

						// determine range based on name
						if (file_name == "icon" || file_name == "banner")
						{
							// old key
							keyslot_index = 0;
						}
						else
						{
							// new key
							keyslot_index = 1;
						}

						tc::crypto::Aes128CtrEncryptedStream::counter_t file_counter;
						memcpy(file_counter.data(), exefs_aesctr.data(), exefs_aesctr.size());
						tc::crypto::IncrementCounterAes128Ctr(file_counter.data(), file_offset >> 4);
						exefs_streams.push_back(std::make_shared<tc::crypto::Aes128CtrEncryptedStream>(tc::crypto::Aes128CtrEncryptedStream(std::make_shared<tc::io::SubStream>(tc::io::SubStream(mRegionInfo[NcchRegion_ExeFs].raw_stream, file_offset, file_length)), keyslot[keyslot_index].key, file_counter)));
					}
					mRegionInfo[NcchRegion_ExeFs].ready_stream = std::make_shared<tc::io::ConcatenatedStream>(tc::io::ConcatenatedStream(exefs_streams));
				}
			}
			// otherwise use the "best-effort" single key stream (only icon and banner will be decrypt properly)
			else
			{
				fmt::print(stderr, "Only NCCH key0 was determined, so ExeFS will be partially decrypted\n");
				mRegionInfo[NcchRegion_ExeFs].ready_stream = std::shared_ptr<tc::crypto::Aes128CtrEncryptedStream>(new tc::crypto::Aes128CtrEncryptedStream(mRegionInfo[NcchRegion_ExeFs].raw_stream, keyslot[0].key, exefs_aesctr));
			}
		}
		if (mRegionInfo[NcchRegion_RomFs].size && keyslot[1].valid_key == ValidState::Good)
		{
			mRegionInfo[NcchRegion_RomFs].ready_stream = std::shared_ptr<tc::crypto::Aes128CtrEncryptedStream>(new tc::crypto::Aes128CtrEncryptedStream(mRegionInfo[NcchRegion_RomFs].raw_stream, keyslot[1].key, romfs_aesctr));
		}
	}
}


void ctrtool::NcchProcess::verifyRegions()
{
	tc::crypto::Sha256Generator hash_calc;
	using Sha256Hash = std::array<byte_t, hash_calc.kHashSize>;
	std::array<Sha256Hash, NcchRegionNum> region_hashes;
	tc::ByteData cache = tc::ByteData(0x10000);
	size_t cache_read_len = 0;

	// generate region sha2-256 hashes
	for (size_t i = 0; i < mRegionInfo.size(); i++)
	{
		if (mRegionInfo[i].hashed_size > 0 && mRegionInfo[i].ready_stream != nullptr)
		{
			// seek ready_stream to hashed_offset
			mRegionInfo[i].ready_stream->seek(mRegionInfo[i].hashed_offset, tc::io::SeekOrigin::Begin);

			// init hash calc
			hash_calc.initialize();

			// update hash with cache reads
			for (int64_t remaining_data = mRegionInfo[i].hashed_size; remaining_data > 0; remaining_data -= int64_t(cache_read_len))
			{
				cache_read_len = size_t(std::min<int64_t>(cache.size(), remaining_data));
				cache_read_len = mRegionInfo[i].ready_stream->read(cache.data(), cache_read_len);
				if (cache_read_len == 0)
				{
					throw tc::io::IOException(mModuleLabel, "Failed to read from NCCH region file.");
				}

				hash_calc.update(cache.data(), cache_read_len);
			}
			
			// save hash
			hash_calc.getHash(region_hashes[i].data());
		}
	}
	
	// header signature
	if (mRegionInfo[NcchRegion_Header].hashed_size > 0)
	{
		// verify hash using CFA key
		if (mHeader.header.flags.content_flag.form_type == mHeader.header.FormType_SimpleContent)
		{
			auto rsakey_itr = mKeyBag.rsa_key.find(mKeyBag.RSAKEY_CFA_CCI);
			if (rsakey_itr != mKeyBag.rsa_key.end())
			{
				tc::crypto::RsaKey pubkey = rsakey_itr->second;

				mRegionInfo[NcchRegion_Header].valid = tc::crypto::VerifyRsa2048Pkcs1Sha256(mHeader.signature.data(), region_hashes[NcchRegion_Header].data(), pubkey) ? ValidState::Good : ValidState::Fail;
			}
			else
			{
				// cannot locate rsa key to verify
				fmt::print(stderr, "Could not read CFA public key.\n");
				mRegionInfo[NcchRegion_Header].valid = ValidState::Fail;
			}
			
		}
		// verify hash using CXI key from exheader
		else
		{
			if (mRegionInfo[NcchRegion_ExHeader].size > 0 && mRegionInfo[NcchRegion_ExHeader].ready_stream != nullptr)
			{
				// import exheader
				tc::ByteData exheader_data = tc::ByteData(mRegionInfo[NcchRegion_ExHeader].size);
				mRegionInfo[NcchRegion_ExHeader].ready_stream->seek(0, tc::io::SeekOrigin::Begin);
				mRegionInfo[NcchRegion_ExHeader].ready_stream->read(exheader_data.data(), exheader_data.size());
				ntd::n3ds::AccessDescriptor* accessdesc = (ntd::n3ds::AccessDescriptor*)(exheader_data.data() + sizeof(ntd::n3ds::ExtendedHeader));

				// create public key from access desc
				tc::crypto::RsaKey pubkey = tc::crypto::RsaPublicKey(accessdesc->ncch_rsa_modulus.data(), accessdesc->ncch_rsa_modulus.size());

				// verify header signature
				mRegionInfo[NcchRegion_Header].valid = tc::crypto::VerifyRsa2048Pkcs1Sha256(mHeader.signature.data(), region_hashes[NcchRegion_Header].data(), pubkey) ? ValidState::Good : ValidState::Fail;
			}
			else
			{
				// cannot locate rsa key to verify
				fmt::print(stderr, "Could not read CXI public key from AccessDescriptor.\n");
				mRegionInfo[NcchRegion_Header].valid = ValidState::Fail;
			}
		}
	}

	// exheader hash
	if (mRegionInfo[NcchRegion_ExHeader].hashed_size > 0)
	{
		mRegionInfo[NcchRegion_ExHeader].valid = memcmp(region_hashes[NcchRegion_ExHeader].data(), mHeader.header.exhdr_hash.data(), region_hashes[NcchRegion_ExHeader].size()) == 0 ? ValidState::Good : ValidState::Fail;
	}

	// logo hash
	if (mRegionInfo[NcchRegion_Logo].hashed_size > 0)
	{
		mRegionInfo[NcchRegion_Logo].valid = memcmp(region_hashes[NcchRegion_Logo].data(), mHeader.header.logo_hash.data(), region_hashes[NcchRegion_Logo].size()) == 0 ? ValidState::Good : ValidState::Fail;
	}

	// exefs hash
	if (mRegionInfo[NcchRegion_ExeFs].hashed_size > 0)
	{
		mRegionInfo[NcchRegion_ExeFs].valid = memcmp(region_hashes[NcchRegion_ExeFs].data(), mHeader.header.exefs_prot_hash.data(), region_hashes[NcchRegion_ExeFs].size()) == 0 ? ValidState::Good : ValidState::Fail;
	}

	// romfs hash
	if (mRegionInfo[NcchRegion_RomFs].hashed_size > 0)
	{
		mRegionInfo[NcchRegion_RomFs].valid = memcmp(region_hashes[NcchRegion_RomFs].data(), mHeader.header.romfs_prot_hash.data(), region_hashes[NcchRegion_RomFs].size()) == 0 ? ValidState::Good : ValidState::Fail;
	}

}

void ctrtool::NcchProcess::printHeader()
{
	fmt::print("\n");
	fmt::print("NCCH:\n");
	fmt::print("Header:                 {}\n", "NCCH");
	fmt::print("Signature: {:6}       {}", getValidString(mRegionInfo[NcchRegion_Header].valid), tc::cli::FormatUtil::formatBytesAsStringWithLineLimit(mHeader.signature.data(), mHeader.signature.size(), true, "", 0x20, 24, false));
	fmt::print("Content size:           0x{:08x}\n", mHeader.header.content_blk_size.unwrap() * mBlockSize);
	fmt::print("Title id:               {:016x}\n", mHeader.header.content_id.unwrap());
	fmt::print("Maker code:             {}\n", mHeader.header.maker_code.decode());
	fmt::print("FormatVersion:          {:d}\n", mHeader.header.format_version.unwrap());
	fmt::print("Title seed check:       {}\n", tc::cli::FormatUtil::formatBytesAsString(mHeader.header.seed_checksum.data(), mHeader.header.seed_checksum.size(), true, ""));
	fmt::print("Program id:             {:016x}\n", mHeader.header.program_id.unwrap());
	fmt::print("Logo hash: {:6}       {}\n", getValidString(mRegionInfo[NcchRegion_Logo].valid), tc::cli::FormatUtil::formatBytesAsString(mHeader.header.logo_hash.data(), mHeader.header.logo_hash.size(), true, ""));
	fmt::print("Product code:           {}\n",  mHeader.header.product_code.decode());
	fmt::print("Exheader size:          0x{:x}\n", mHeader.header.exhdr_size.unwrap());
	fmt::print("Exheader hash: {:6}   {}\n", getValidString(mRegionInfo[NcchRegion_ExHeader].valid), tc::cli::FormatUtil::formatBytesAsString(mHeader.header.exhdr_hash.data(), mHeader.header.exhdr_hash.size(), true, ""));
	fmt::print("Flags:                  {}\n", tc::cli::FormatUtil::formatBytesAsString(mHeader.header.flags.data(), mHeader.header.flags.size(), true, ""));

	// crypto key
	fmt::print(" > Crypto Key           ");
	if (mHeader.header.flags.other_flag.test(ntd::n3ds::NcchCommonHeader::OtherFlag_NoEncryption))
	{
		fmt::print("None");
	}
	else if (mHeader.header.flags.other_flag.test(ntd::n3ds::NcchCommonHeader::OtherFlag_FixedAesKey))
	{
		fmt::print("Fixed ({})", (isSystemTitle() ? "System" : "Application"));
	}
	else
	{
		fmt::print("Secure ({:d})", (uint32_t)mHeader.header.flags.security_version);
		if (mHeader.header.flags.other_flag.test(ntd::n3ds::NcchCommonHeader::OtherFlag_SeededAesKeyY))
		{
			fmt::print(" (KeyY seeded)");
		}
	}
	fmt::print("\n");

	std::vector<std::string> content_platforms;
	for (size_t bit = 0; bit < mHeader.header.flags.content_platform.bit_size(); bit++)
	{
		if (mHeader.header.flags.content_platform.test(bit))
		{
			content_platforms.push_back(getContentPlatformString(bit));
		}
	}
	fmt::print(" > ContentPlatorm:      {}", tc::cli::FormatUtil::formatListWithLineLimit(content_platforms, 4, 24, false));

	fmt::print(" > FormType:            {}\n", getFormTypeString(mHeader.header.flags.content_flag.form_type));
	fmt::print(" > ContentType:         {}\n", getContentTypeString(mHeader.header.flags.content_flag.content_type));
	fmt::print(" > BlockSize:           0x{:x}\n", mBlockSize);
	if (mHeader.header.flags.other_flag.test(ntd::n3ds::NcchCommonHeader::OtherFlag_NoMountRomFS))
		fmt::print(" > No RomFS mount\n");
	if (mHeader.header.flags.other_flag.test(ntd::n3ds::NcchCommonHeader::OtherFlag_ManualDisclosure))
		fmt::print(" > Disclose eManual\n");

	fmt::print("Plain region offset:    0x{:08x}\n", mHeader.header.plain_region_blk_offset.unwrap() * mBlockSize);
	fmt::print("Plain region size:      0x{:08x}\n", mHeader.header.plain_region_blk_size.unwrap() * mBlockSize);
	fmt::print("Logo offset:            0x{:08x}\n", mHeader.header.logo_blk_offset.unwrap() * mBlockSize);
	fmt::print("Logo size:              0x{:08x}\n", mHeader.header.logo_blk_size.unwrap() * mBlockSize);
	fmt::print("ExeFS offset:           0x{:08x}\n", mHeader.header.exefs_blk_offset.unwrap() * mBlockSize);
	fmt::print("ExeFS size:             0x{:08x}\n", mHeader.header.exefs_blk_size.unwrap() * mBlockSize);
	fmt::print("ExeFS hash region size: 0x{:08x}\n", mHeader.header.exefs_prot_blk_size.unwrap() * mBlockSize);
	fmt::print("RomFS offset:           0x{:08x}\n", mHeader.header.romfs_blk_offset.unwrap() * mBlockSize);
	fmt::print("RomFS size:             0x{:08x}\n", mHeader.header.romfs_blk_size.unwrap() * mBlockSize);
	fmt::print("RomFS hash region size: 0x{:08x}\n", mHeader.header.romfs_prot_blk_size.unwrap() * mBlockSize);
	fmt::print("ExeFS hash: {:6}      {}\n", getValidString(mRegionInfo[NcchRegion_ExeFs].valid), tc::cli::FormatUtil::formatBytesAsString(mHeader.header.exefs_prot_hash.data(), mHeader.header.exefs_prot_hash.size(), true, ""));
	fmt::print("RomFS hash: {:6}      {}\n", getValidString(mRegionInfo[NcchRegion_RomFs].valid), tc::cli::FormatUtil::formatBytesAsString(mHeader.header.romfs_prot_hash.data(), mHeader.header.romfs_prot_hash.size(), true, ""));
}

void ctrtool::NcchProcess::extractRegionBinaries()
{
	/*
	original order
		ncch_save(ctx, NCCHTYPE_EXEFS, actions);
		ncch_save(ctx, NCCHTYPE_ROMFS, actions);
		ncch_save(ctx, NCCHTYPE_EXHEADER, actions);
		ncch_save(ctx, NCCHTYPE_LOGO, actions);
		ncch_save(ctx, NCCHTYPE_PLAINRGN, actions);
	*/

	tc::io::LocalFileSystem local_fs;
	tc::ByteData cache = tc::ByteData(0x10000);
	size_t cache_read_len;
	std::shared_ptr<tc::io::IStream> in_stream;
	std::shared_ptr<tc::io::IStream> out_stream;
	for (size_t i = 0; i < NcchRegionNum; i++)
	{
		if (mRegionOpt[i].bin_extract_path.isSet() && mRegionInfo[i].ready_stream != nullptr)
		{
			switch(i)
			{
				case NcchRegion_Header: fmt::print("Saving Header...\n"); break;
				case NcchRegion_ExHeader: fmt::print("Saving Extended Header...\n"); break;
				case NcchRegion_PlainRegion: fmt::print("Saving Plain Region...\n"); break;
				case NcchRegion_Logo: fmt::print("Saving Logo...\n"); break;
				case NcchRegion_ExeFs: fmt::print("Saving ExeFS...\n"); break;
				case NcchRegion_RomFs: fmt::print("Saving RomFS...\n"); break;
			}

			in_stream = mRegionInfo[i].ready_stream;
			local_fs.openFile(mRegionOpt[i].bin_extract_path.get(), tc::io::FileMode::OpenOrCreate, tc::io::FileAccess::Write, out_stream);

			in_stream->seek(0, tc::io::SeekOrigin::Begin);
			out_stream->seek(0, tc::io::SeekOrigin::Begin);
			for (int64_t remaining_data = in_stream->length(); remaining_data > 0;)
			{
				cache_read_len = in_stream->read(cache.data(), cache.size());
				if (cache_read_len == 0)
				{
					throw tc::io::IOException(mModuleLabel, "Failed to read from NCCH Region.");
				}

				out_stream->write(cache.data(), cache_read_len);

				remaining_data -= int64_t(cache_read_len);
			}
		}
	}
}

void ctrtool::NcchProcess::processRegions()
{
	if (mRegionInfo[NcchRegion_ExHeader].size != 0 && mRegionInfo[NcchRegion_ExHeader].ready_stream != nullptr)
	{
		ctrtool::ExHeaderProcess proc;
		proc.setInputStream(mRegionInfo[NcchRegion_ExHeader].ready_stream);
		proc.setKeyBag(mKeyBag);
		proc.setCliOutputMode(mRegionOpt[NcchRegion_ExHeader].show_info);
		proc.setVerboseMode(mVerbose);
		proc.setVerifyMode(mVerify);
		proc.setShowSyscallName(mShowSyscallNames);
		
		proc.process();
	}
	if (mRegionInfo[NcchRegion_ExeFs].size != 0 && mRegionInfo[NcchRegion_ExeFs].ready_stream != nullptr)
	{
		// prior to reading exefs, check compressed flag in exheader
		if (mRegionInfo[NcchRegion_ExHeader].size > 0 && mRegionInfo[NcchRegion_ExHeader].ready_stream != nullptr)
		{
			// import exheader			
			ntd::n3ds::ExtendedHeader exheader;
			mRegionInfo[NcchRegion_ExHeader].ready_stream->seek(0, tc::io::SeekOrigin::Begin);
			mRegionInfo[NcchRegion_ExHeader].ready_stream->read((byte_t*)&exheader, sizeof(ntd::n3ds::ExtendedHeader));

			mDecompressExeFsCode = exheader.system_control_info.flags.bitarray.test(ntd::n3ds::SystemControlInfo::Flags_CompressExefsPartition0);
		}

		ctrtool::ExeFsProcess proc;
		proc.setInputStream(mRegionInfo[NcchRegion_ExeFs].ready_stream);
		proc.setCliOutputMode(mRegionOpt[NcchRegion_ExeFs].show_info, mRegionOpt[NcchRegion_ExeFs].show_fs);
		proc.setVerboseMode(mVerbose);
		proc.setVerifyMode(mVerify);
		proc.setRawMode(mRaw);
		proc.setDecompressCode(mDecompressExeFsCode);
		if (mRegionOpt[NcchRegion_ExeFs].fs_extract_path.isSet())
		{
			proc.setExtractPath(mRegionOpt[NcchRegion_ExeFs].fs_extract_path.get());
		}
		proc.process();
	}
	if (mRegionInfo[NcchRegion_RomFs].size != 0 && mRegionInfo[NcchRegion_RomFs].ready_stream != nullptr)
	{
		ctrtool::IvfcProcess proc;
		proc.setInputStream(mRegionInfo[NcchRegion_RomFs].ready_stream);
		proc.setKeyBag(mKeyBag);
		proc.setCliOutputMode(mRegionOpt[NcchRegion_RomFs].show_info, mRegionOpt[NcchRegion_RomFs].show_fs);
		proc.setVerboseMode(mVerbose);
		proc.setVerifyMode(mVerify);
		if (mRegionOpt[NcchRegion_RomFs].fs_extract_path.isSet())
		{
			proc.setExtractPath(mRegionOpt[NcchRegion_RomFs].fs_extract_path.get());
		}
		proc.process();
	}
}

std::string ctrtool::NcchProcess::getValidString(byte_t validstate)
{
	std::string ret_str;

	switch (validstate)
	{
		case Unchecked:
			ret_str =  "";
			break;
		case Good:
			ret_str =  "(GOOD)";
			break;
		case Fail:
		default:
			ret_str =  "(FAIL)";
			break;
	}

	return ret_str;
}

std::string ctrtool::NcchProcess::getContentPlatformString(size_t bit)
{
	std::string ret_str;

	switch(bit)
	{
		case ntd::n3ds::NcchCommonHeader::ContentPlatform_CTR :
			ret_str = "CTR";
			break;
		case ntd::n3ds::NcchCommonHeader::ContentPlatform_SNAKE :
			ret_str = "SNAKE";
			break;
		default:
			ret_str = fmt::format("Unknown (bit {:d}", bit);
	}

	return ret_str;
}

std::string ctrtool::NcchProcess::getFormTypeString(byte_t var)
{
	std::string ret_str;

	switch(var)
	{
		case ntd::n3ds::NcchCommonHeader::FormType_Unassigned :
			ret_str = "Not Assigned";
			break;
		case ntd::n3ds::NcchCommonHeader::FormType_SimpleContent :
			ret_str = "Simple Content";
			break;
		case ntd::n3ds::NcchCommonHeader::FormType_ExecutableWithoutRomFS :
			ret_str = "Executable (without RomFS)";
			break;
		case ntd::n3ds::NcchCommonHeader::FormType_Executable :
			ret_str = "Executable";
			break;
		default:
			ret_str = "Unknown";
			break;
	}

	return ret_str;
}

std::string ctrtool::NcchProcess::getContentTypeString(byte_t var)
{
	std::string ret_str;

	switch(var)
	{
		case ntd::n3ds::NcchCommonHeader::ContentType_Application :
			ret_str = "Application";
			break;
		case ntd::n3ds::NcchCommonHeader::ContentType_SystemUpdate :
			ret_str = "System Update (CTR)";
			break;
		case ntd::n3ds::NcchCommonHeader::ContentType_Manual :
			ret_str = "Manual";
			break;
		case ntd::n3ds::NcchCommonHeader::ContentType_Child :
			ret_str = "Child";
			break;
		case ntd::n3ds::NcchCommonHeader::ContentType_Trial :
			ret_str = "Trial";
			break;
		case ntd::n3ds::NcchCommonHeader::ContentType_ExtendedSystemUpdate :
			ret_str = "System Update (SNAKE)";
			break;
		default:
			ret_str = "Unknown";
			break;
	}

	return ret_str;
}


bool ctrtool::NcchProcess::isSystemTitle()
{
	return (mHeader.header.content_id.unwrap() & 0x0000001000000000) != 0;
}

void ctrtool::NcchProcess::getAesCounter(byte_t* counter, byte_t ncch_region)
{
	uint16_t version = mHeader.header.format_version.unwrap();

	struct AesCounter_v0_v2
	{
		enum NcchTypeForCtr
		{
			NcchTypeForCtr_ExHeader = 1,
			NcchTypeForCtr_ExeFs = 2,
			NcchTypeForCtr_RomFs = 3
		};

		tc::bn::be64<uint64_t> title_id;
		byte_t type;
		std::array<byte_t, 7> block_bytes;
	};

	struct AesCounter_v1
	{
		tc::bn::le64<uint64_t> title_id;
		tc::bn::be64<uint64_t> begin_offset;
	};

	if (version == ntd::n3ds::NcchCommonHeader::FormatVersion_CFA || version == ntd::n3ds::NcchCommonHeader::FormatVersion_CXI)
	{
		AesCounter_v0_v2* tmp = (AesCounter_v0_v2*)counter;
		tmp->title_id.wrap(mHeader.header.content_id.unwrap());
		if (ncch_region == NcchRegion_ExHeader)
			tmp->type = tmp->NcchTypeForCtr_ExHeader;
		else if (ncch_region == NcchRegion_ExeFs)
			tmp->type = tmp->NcchTypeForCtr_ExeFs;
		else if (ncch_region == NcchRegion_RomFs)
			tmp->type = tmp->NcchTypeForCtr_RomFs;
		memset(tmp->block_bytes.data(), 0, tmp->block_bytes.size());
	}
	else if (version == ntd::n3ds::NcchCommonHeader::FormatVersion_CXI_PROTOTYPE)
	{
		AesCounter_v1* tmp = (AesCounter_v1*)counter;
		tmp->title_id.wrap(mHeader.header.content_id.unwrap());
		tmp->begin_offset.wrap(mRegionInfo[ncch_region].offset);
	}
}
