/* GPC (C) 2017-2018 Stephane Charette <stephanecharette@gmail.com>
 * $Id: IJS.cpp 2458 2018-02-27 03:41:19Z stephane $
 */

#include "GPC.hpp"


IJB::IJB(void) :
	rec(nullptr)
{
	return;
}


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


IJB::IJB(const IJB &rhs)
{
	this->operator=(rhs);

	return;
}


IJB &IJB::operator=(const IJB &rhs)
{
	if (this != &rhs)
	{
		contents	= rhs.contents;
		file		= rhs.file;
		rec			= reinterpret_cast<const RecIJB*>(contents.data());
		img			= extract_image();
	}

	return *this;
}


IJB &IJB::load(const File &filename)
{
	img = Image();

	const std::string name = filename.getFullPathName().toStdString();

	if (not filename.existsAsFile())
	{
		/// @throw Lox::Exception if the file does not exist
		throw Lox::Exception(LOX_WHERE, "Referenced .ijb file does not exist: " + name);
	}

	// at the very least, the .ijb file should have a file header
	const size_t len = filename.getSize();
	const auto minimum_size = sizeof(RecIJB);
	if (len < minimum_size)
	{
		/// @throw Lox::Exception if the file is impossibly small (no content?)
		throw Lox::Exception(LOX_WHERE, "Referenced .ijb file is not valid: " + name);
	}

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

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

	rec = reinterpret_cast<const RecIJB*>(contents.data());

	if (ByteOrder::swap(rec->len) != contents.size() - sizeof(RecIJB))
	{
		/// @throw Lox::Exception if the file is not a valid .ijb bitmap
		throw Lox::Exception(LOX_WHERE, "IJB file is invalid: " + name);
	}

	if (rec->width	== 0 ||
		rec->height	== 0 ||
		rec->len	== 0)
	{
		/// @throw Lox::Exception if the file has an invalid size (width, height, or length)
		throw Lox::Exception(LOX_WHERE, "IJB file has an invalid size: " + name);
	}

	img = extract_image();

	return *this;
}


std::string IJB::summary(void) const
{
	std::stringstream ss;
	ss	<< "Parsed the following information from " << file.getFullPathName().toStdString() << ":" << std::endl
		<< "\t- Short name: "	<< rec->short_name					<< std::endl
		<< "\t- Long name: "	<< rec->long_filename				<< std::endl
		<< "\t- File size: "	<< contents.size();

	if (rec != nullptr)
	{
		ss	<< ""														<< std::endl
			<< "\t- Data size: "	<< ByteOrder::swap(rec->len)		<< std::endl
			<< "\t- Height: "		<< ByteOrder::swap(rec->height)		<< std::endl
			<< "\t- Width: "		<< ByteOrder::swap(rec->width) * 8;
	}

	return ss.str();
}


Image IJB::extract_image() const
{
	const int px_height		= ByteOrder::swap(rec->height);
	const int byte_width	= ByteOrder::swap(rec->width);
	const int px_width		= byte_width * 8;

	Image image(Image::PixelFormat::RGB, px_width, px_height, false);

	const uint8_t *ptr = rec->data;
	for (int y = 0; y < px_height; y++)
	{
		for (int x = 0; x < byte_width; x++)
		{
			// get the next 8 pixels since each pixel is a single ON|OFF bit
			uint8_t packed_pixels = *ptr;
			ptr ++;

			// pixels are packed backwards, meaning we'll see X offset 7, then 6, then 5, etc...
			for (int offset = 7; offset >= 0; offset --)
			{
				const bool is_pixel_enabled = (packed_pixels & 0x01);
				packed_pixels >>= 1;

				image.setPixelAt(x * 8 + offset, y, is_pixel_enabled ? Colours::black : Colours::white);
			}
		}
	}

	// don't forget the .ijb are flipped horizontally (as if looking in a mirror)
	// so flip the image back so the user is not surprised
	flip_horizontal(image);

	// and lastly, rotate the image back to a horizontal position
	rotate_ccw90(image);

	return image;
}


IJB &IJB::set_new_image(const Image &new_image)
{
	Image dst = new_image;
	dst.duplicateIfShared();
	flip_horizontal(dst);
	rotate_ccw90(dst);

	const int px_height		= ByteOrder::swap(rec->height);
	const int byte_width	= ByteOrder::swap(rec->width);
	const int px_width		= byte_width * 8;
	uint8_t *ptr			= contents.data() + sizeof(RecIJB);

	for (int y = 0; y < px_height; y++)
	{
		int x = 0;
		while (x < px_width)
		{
			uint8_t data_byte = '\0';

			// we need to pack 8 pixels together to create a single image byte
			for (int index = 0; index <= 7; index ++)
			{
				/* Remember the .ijb files have a single bit per pixel.
				 * Pixel is either on, or off.  White is stored as zero,
				 * and black is stored as 1.  So if the dst image colour
				 * is very pale, we consider that white.  Otherwise, let
				 * it be black.  If *ALL* the individual RGB values are
				 * greater than 0xEE, then consider it white.
				 */
				const Colour c = dst.getPixelAt(x + index, y);
				const bool pixel_is_black = (
						c.getRed()		< 0xee ||
						c.getGreen()	< 0xee ||
						c.getBlue()		< 0xee );

				data_byte <<= 1;
				if (pixel_is_black)
				{
					// "0" is white and "1" is black
					data_byte |= 0x01;
				}
			}

			x += 8;

			// we have the next 8 pixels -- store this byte
			*ptr = data_byte;
			ptr ++;
		}
	}

	// Before we replace the image, make a backup of the old one.  Insert a timestamp into the backup name.
	const File bmp_file = file.getSiblingFile(rec->short_name);
	if (bmp_file.existsAsFile())
	{
		const std::time_t now = std::time(nullptr);
		std::tm * tm = std::localtime(&now);
		char buffer[50];
		strftime(buffer, sizeof(buffer), "_%Y-%m-%d_%H-%M-%S", tm);
		std::string backup_name = rec->short_name;
		const size_t pos = backup_name.rfind(".bmp");
		if (pos != std::string::npos)
		{
			backup_name.erase(pos);
		}
		backup_name += buffer + std::string(".bmp");
		const File backup_file = bmp_file.getSiblingFile(backup_name.c_str());
		bmp_file.copyFileTo(backup_file);
	}

	const bool success = file.replaceWithData(contents.data(), contents.size());
	if (not success)
	{
		/// @throw Lox::Exception if the file cannot be replaced
		throw Lox::Exception(LOX_WHERE, "IJB file cannot be replaced: " + file.getFullPathName().toStdString());
	}

	img = new_image;

	create_browseable_image(true);

	return *this;
}


IJB &IJB::rotate_image_180(void)
{
	rotate_180(img);
	set_new_image(img);

	return *this;
}


const IJB &IJB::create_browseable_image(const bool force) const
{
	// come up with a name such as "foobar_300x200.png"
	File png_file = file.getSiblingFile(rec->short_name);
	String name = png_file.getFileNameWithoutExtension();
	name += "_" + String(img.getWidth()) + "x" + String(img.getHeight());
	png_file = file.getSiblingFile(name).withFileExtension(".png");

	if (force || png_file.existsAsFile() == false)
	{
		MemoryOutputStream os;
		PNGImageFormat().writeImageToStream(img, os);
		png_file.replaceWithData(os.getData(), os.getDataSize());
	}

	// finally, we may as well create the .bmp file as well now that we have the code to read/write .bmp files
	File bmp_file = file.getSiblingFile(rec->short_name);
	if (force || bmp_file.existsAsFile() == false)
	{
		save_image_as_bmp(img, bmp_file);
	}

	return *this;
}
