#include <stdlib.h>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

namespace {

constexpr int max_input_file_size = 16 * 1024 * 1024;

using Bytes = std::vector<unsigned char>;
using Strings = std::vector<std::string>;

Strings array_attributes{};
std::string input_file_path{};
Bytes input_bytes{};
std::string output_file_path{};
std::string content{};

[[noreturn]] void fail(const std::string& message)
{
	throw std::runtime_error{message};
}

void load_file(const std::string& path)
{
	std::filebuf file{};
	file.open(path.c_str(), std::ios_base::in | std::ios_base::binary | std::ios_base::ate);

	if (!file.is_open())
	{
		fail("Failed to open a file.");
	}

	const std::streampos end_position = file.pubseekoff(0, std::ios_base::cur);

	if (end_position < 0)
	{
		fail("Failed to seek a file to the end.");
	}

	if (end_position > max_input_file_size)
	{
		fail("File too big (max size: " + std::to_string(max_input_file_size) + ").");
	}

	const std::streampos begin_position = file.pubseekpos(0);

	if (begin_position < 0)
	{
		fail("Failed to seek a file to the beginning.");
	}

	input_bytes.resize(static_cast<size_t>(end_position));

	if (file.sgetn(reinterpret_cast<char*>(input_bytes.data()), end_position) != end_position)
	{
		fail("Failed to read whole file.");
	}
}

void save_output_file()
{
	std::filebuf output_file{};
	output_file.open(output_file_path, std::ios_base::out | std::ios_base::trunc);

	if (!output_file.is_open())
	{
		fail("Failed to open a file.");
	}

	std::streamsize stream_size = static_cast<std::streamsize>(content.size());

	if (output_file.sputn(reinterpret_cast<const char*>(content.data()), stream_size) != stream_size)
	{
		fail("Failed to write whole file.");
	}
}

void make_content()
{
	content.reserve(input_bytes.size() * 4);

	content += "// WARNING This file was generated by bstone_bin2c\n";
	content += '\n';

	const std::string byte_count_string = std::to_string(input_bytes.size());
	content += "const int byte_count = " + byte_count_string + ";\n";
	content += '\n';

	for (const std::string& array_attribute : array_attributes)
	{
		content += array_attribute;
		content += " ";
	}

	content += "const unsigned char bytes[byte_count] =\n";
	content += "{\n";

	constexpr int line_max_length = 80;
	size_t line_length = line_max_length;
	bool is_first_byte = true;

	for (unsigned char input_byte : input_bytes)
	{
		const std::string byte_string = std::to_string(static_cast<int>(input_byte)) + ',';

		if (line_length + byte_string.size() > line_max_length)
		{
			if (!is_first_byte)
			{
				content += '\n';
			}

			content += '\t';
			line_length = 0;
		}

		content += byte_string;
		is_first_byte = false;
		line_length += byte_string.size();
	}

	if (!content.empty() && content.back() != '\n')
	{
		content += '\n';
	}

	content += "};\n";
}

void convert()
{
	std::cout << "Load input file \"" << input_file_path << "\"." << std::endl;
	load_file(input_file_path);

	std::cout << "Convert." << std::endl;
	make_content();

	std::cout << "Save output file \"" << output_file_path << "\"." << std::endl;
	save_output_file();
}

} // namespace

int main(int argc, char* argv[])
{
	try
	{
		if (argc == 1)
		{
			std::cerr << "Usage: app [-aa arr_attr] -i input -o output" << std::endl;
			std::cerr << "Options:" << std::endl;
			std::cerr << "  -aa arr_attr: Adds specified attribute to array." << std::endl;
			std::cerr << "                May be specified multiple times." << std::endl;
			std::cerr << "  -i file_path: Input file path." << std::endl;
			std::cerr << "  -o file_path: Output file path." << std::endl;
			return EXIT_FAILURE;
		}

		for (int i_arg = 1; i_arg < argc; )
		{
			const std::string arg = argv[i_arg];

			if (false)
			{}
			else if (arg == "-aa")
			{
				++i_arg;

				if (i_arg < argc)
				{
					array_attributes.emplace_back(argv[i_arg]);
					++i_arg;
				}
			}
			else if (arg == "-i")
			{
				++i_arg;

				if (i_arg < argc)
				{
					input_file_path = argv[i_arg];
					++i_arg;
				}
			}
			else if (arg == "-o")
			{
				++i_arg;

				if (i_arg < argc)
				{
					output_file_path = argv[i_arg];
					++i_arg;
				}
			}
			else
			{
				fail("Unknown option (" + arg + ").");
			}
		}

		if (input_file_path.empty())
		{
			fail("No input file path.");
		}

		if (output_file_path.empty())
		{
			fail("No output file path.");
		}

		convert();
		return EXIT_SUCCESS;
	}
	catch (const std::exception& exception)
	{
		std::cerr << "[ERROR] " << exception.what() << std::endl;
		return EXIT_FAILURE;
	}
	catch (...)
	{
		std::cerr << "[ERROR] Generic failure." << std::endl;
		return EXIT_FAILURE;
	}
}
