/* GPC (C) 2017-2018 Stephane Charette <stephanecharette@gmail.com>
 * $Id: IJS.cpp 2485 2018-03-17 23:22:13Z stephane $
 */

#include "GPC.hpp"


IJS::IJS(void) :
	number_of_unknown_bytes(0)
{
	return;
}


IJS::~IJS(void)
{
	return;
}


IJS &IJS::load(const File &filename)
{
	jobs	.clear();
	groups	.clear();
	bitmaps	.clear();
	fonts	.clear();
	footers	.clear();
	number_of_unknown_bytes = 0;

	size_t len = filename.getSize();

	// at the very least, the .ijs file should have several records in it, even if it has no bitmaps
	const auto minimum_size = sizeof(RecJob) + sizeof(RecGroup) + sizeof(RecFooter);
	if (len < minimum_size)
	{
		/// @throw Lox::Exception if the file is impossibly small (no content?)
		throw Lox::Exception(LOX_WHERE, "IJS file is not valid: " + filename.getFullPathName().toStdString());
	}

	file = filename;
	FileInputStream input_stream(file);
	if (input_stream.openedOk() == false)
	{
		/// @throw Lox::Exception if the file cannot be read (permission problem?)
		throw Lox::Exception(LOX_WHERE, "IJS file cannot be opened: " + filename.getFullPathName().toStdString());
	}

	LOG_MSG("reading " << len << " bytes from " << file.getFullPathName().toStdString());
	contents = VBytes(len, '\0');
	input_stream.read(contents.data(), len);

	const uint8_t *ptr = contents.data();

	const RecJob *job = reinterpret_cast<const RecJob*>(ptr);
	if (job->short_name[0] != 'J' && job->unknown1 != 0x0e000000)
	{
		/// @throw Lox::Exception if the file header is not a valid .ijs header
		throw Lox::Exception(LOX_WHERE, "IJS file header is not recognized: " + file.getFullPathName().toStdString());
	}

	LOG_MSG("offset #" << (contents.size() - len) << ": identified job: " << job->short_name << " -> " << job->long_filename);
	jobs.push_back(job);
	ptr += sizeof(RecJob);
	len -= sizeof(RecJob);

	while (len > 0 && jobs.empty() == false)
	{
		// attempt to identify one of these 4 things:
		//
		//	1) RecGroup
		//	2) RecBitmap
		//	3) RecFont
		//	4) RecFooter

//		LOG_MSG("offset #" << (contents.size() - len) << ": attempting to parse .ijs file, " << len << " bytes remain");

		if (len >= sizeof(RecBitmap))
		{
			const RecBitmap *bitmap = reinterpret_cast<const RecBitmap*>(ptr);
			if (bitmap->short_filename[0]	== 'J' &&
				bitmap->long_filename[0]	== 'J' )
			{
				LOG_MSG("offset #" << (contents.size() - len) << ": identified bitmap: " << bitmap->short_filename << " -> " << bitmap->long_filename << " (group #" << (int) bitmap->group_number << ", trigger value " << bitmap->trigger_value << ")");
				bitmaps.push_back(bitmap);
				ptr += sizeof(RecBitmap);
				len -= sizeof(RecBitmap);
				continue;
			}
		}

		if (len >= sizeof(RecFont))
		{
			const RecFont *font = reinterpret_cast<const RecFont*>(ptr);
			if (font->short_filename[0] == 'F' &&
				font->short_filename[1] == 'o' &&
				font->short_filename[2] == 'n' &&
				font->short_filename[3] == 't' )
			{
				LOG_MSG("offset #" << (contents.size() - len) << ": identified font: " << font->short_filename << " -> " << font->long_filename);
				fonts.push_back(font);
				ptr += sizeof(RecFont);
				len -= sizeof(RecFont);
				continue;
			}
		}

		if (len >= sizeof(RecGroup))
		{
			const RecGroup *group = reinterpret_cast<const RecGroup*>(ptr);
			if (ByteOrder::swap(group->number_of_images) < 50 &&
				ptr[sizeof(RecGroup)] == 'J') // next byte after a group should be an image, which starts with a filename, as as "J01..."
			{
				// need to try and understand what this structure represents, so dump out what we have
				std::stringstream ss;
				for (size_t idx = 0; idx < sizeof(RecGroup); idx++)
				{
					ss << std::setfill('0') << std::setw(2) << std::hex << (int) (ptr[idx]) << ' ';
				}

				LOG_MSG("offset #" << std::setw(4) << (contents.size() - len) << ": identified group with " << ByteOrder::swap(group->number_of_images) << " images: " << ss.str());

				/* The groups are not always in order from 1 to n!  We have
				 * some example .ijs files that show the groups in this order:
				 *		2, 3, 4, 1
				 * If we blindly push back onto the vector the groups as they
				 * appear in the .ijs, we'll be wrong about offsets, etc., when
				 * an image references a specific group.
				 *
				 * So as ugly as this is, peek ahead at the next image to
				 * determine which group we're actually dealing with, and
				 * store the group in the correct location within the vector.
				 */
				const uint8_t possible_group_number = ptr[sizeof(RecGroup) + 0x4e]; // group number is 0x4e bytes into the bitmap record
				const size_t idx = possible_group_number - 1;

				if (idx == groups.size())
				{
					// this is a normal situation, for example the groups
					// vector already contains 2 records, this is our 3rd
					// record, and it will be stored at index #2 (zero-based)
					groups.push_back(group);
				}
				else if (possible_group_number < 1 || possible_group_number > groups.size() + 10)
				{
					// How did we get such a strange number?  Maybe the offset
					// into the record changed, or the group is followed by
					// something other than a bitmap!?  Either way, something
					// is very wrong, and this will likely result in problems.
					LOG_MSG("- group seems to be #" << (int)possible_group_number << ", but that value seems impossible; the group vector currently contains " << groups.size() << " groups");
					groups.push_back(group);
				}
				else
				{
					// otherwise if we get here, then it means the group number
					// is not the next one we thought would appear in the file,
					// which means the groups are out-of-order (or the file is
					// corrupted?)

					while (groups.size() <= idx)
					{
						// insert a few blank records to fill out the vector
						// so the assignment afterwards operator[] doesn't
						// cause undefined behaviour
						groups.push_back(nullptr);
						LOG_MSG("pushed a null group record onto the vector (groups now contains " << groups.size() << " records)");
					}
					groups[idx] = group;
					LOG_MSG("- group was stored at index # [" << idx << "] of " << groups.size() << " total groups");
				}

				ptr += sizeof(RecGroup);
				len -= sizeof(RecGroup);

				continue;
			}
		}

		if (len >= sizeof(RecFooter))
		{
			const RecFooter *footer = reinterpret_cast<const RecFooter*>(ptr);
			if (footer->unknown[3] == 0x05 ||
				footer->unknown[8] == 0x05 )
			{
				LOG_MSG("offset #" << (contents.size() - len) << ": identified footer");
				footers.push_back(footer);
				ptr += sizeof(RecFooter);
				len -= sizeof(RecFooter);
				continue;
			}
		}

		// if we get here, then we're lost -- we failed to identify the data
		//
		// so increment the pointer by 1, and try again, hopefully on the next
		// iteration we'll manage to identify something and get back on track
		//
		LOG_MSG("offset #" << (contents.size() - len) << ": failed to identify structure");
		number_of_unknown_bytes ++;
		ptr ++;
		len --;
	}

	return *this;
}


VStr IJS::warnings(void) const
{
	VStr v;

	if (file.getFullPathName().isEmpty())
	{
		v.push_back("IJS object is empty (file has not yet been loaded).");
	}
	else if (contents.empty())
	{
		v.push_back("IJS file has no content.");
	}
	else
	{
		if (jobs.size() != 1)
		{
			v.push_back("IJS file header is missing.");
		}

		if (bitmaps.empty())
		{
			v.push_back("IJS file contains no bitmaps.");
		}

		for (size_t idx = 0; idx < groups.size(); idx++)
		{
			if (groups[idx] == nullptr)
			{
				// this means the groups were out-of-order in the ijs file,
				// and we attempted to insert temporary placeholders, but
				// obviously it didn't work since we now have some null
				// group pointers remaining
				v.push_back("Failed to parse some of the group records.  IJS file is unusable.");
			}
		}

		size_t total_number_of_images_in_groups = 0;
		for (size_t idx = 0; idx < groups.size(); idx++)
		{
			if (groups[idx] != nullptr)
			{
				const size_t count = ByteOrder::swap(groups[idx]->number_of_images);
				total_number_of_images_in_groups += count;
			}
		}

		if (bitmaps.size() != total_number_of_images_in_groups)
		{
			v.push_back("Number of images (" + std::to_string(bitmaps.size()) + ") doesn't match the images referenced in groups (" + std::to_string(total_number_of_images_in_groups) + ").");
		}

		if (footers.size() != 1)
		{
			v.push_back("IJS file footer or end-of-file was not correctly identified.");
		}

		if (number_of_unknown_bytes > 0)
		{
			v.push_back("IJS file was not parsed cleanly (failed to identify " + Lox::Numbers::approximateSize(number_of_unknown_bytes) + ").");
		}
	}

	return v;
}


std::string IJS::summary(void) const
{
	std::stringstream ss;
	ss	<< "Parsed the following structures from " << file.getFullPathName().toStdString() << ":" << std::endl
		<< "\t- Headers: "	<< jobs		.size()		<< std::endl
		<< "\t- Groups: "	<< groups	.size()		<< std::endl
		<< "\t- Bitmaps: "	<< bitmaps	.size()		<< std::endl
		<< "\t- Fonts: "	<< fonts	.size();

	if (groups.empty() == false)
	{
		ss << std::endl << "The groups are defined as follows:";

		for (size_t idx=0; idx < groups.size(); idx++)
		{
			const size_t count = ByteOrder::swap(groups[idx]->number_of_images);
			ss << std::endl << "\t- #" << (idx + 1) << " group: " << count << " image" << (count == 1 ? "" : "s");
		}
	}

	const VStr v = warnings();
	if (v.empty() == false)
	{
		ss << std::endl << "Note:";
		for (const auto &msg : v)
		{
			ss << std::endl << "\t- " << msg;
		}
	}

	return ss.str();
}
