/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * 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 "ultima/nuvie/core/nuvie_defs.h"
#include "ultima/nuvie/misc/u6_misc.h"
#include "ultima/nuvie/files/nuvie_io_file.h"
#include "ultima/nuvie/files/u6_lzw.h"
#include "ultima/nuvie/files/u6_lib_n.h"

namespace Ultima {
namespace Nuvie {

U6Lib_n::U6Lib_n() : num_offsets(0), items(nullptr), data(nullptr),
	del_data(false), filesize(0), game_type(NUVIE_GAME_U6), lib_size(0) {
}


U6Lib_n::~U6Lib_n(void) {
	close();
}

// load u6lib from `filename'
bool U6Lib_n::open(const Common::Path &filename, uint8 size, uint8 type) {
	NuvieIOFileRead *file;

	file = new NuvieIOFileRead();

	if (file->open(filename) == false) {
		delete file;
		return false;
	}

	del_data = true;

	return open((NuvieIO *)file, size, type);
}


// load u6lib from opened stream
bool U6Lib_n::open(NuvieIO *new_data, uint8 size, uint8 type) {
	game_type = type;
	data = new_data;

	lib_size = size;
	this->parse_lib();

	return true;
}

void U6Lib_n::close() {
	if (items) {
		for (uint32 i = 0; i < num_offsets; i++)
			delete items[i].name;
		free(items);
	}
	items = nullptr;

	if (del_data) {
		if (data != nullptr)
			data->close();
		delete data;
	}

	data = nullptr;
	del_data = false;

	num_offsets = 0;

	return;
}

/* Open a ^new^ file for writing, with lib_size and type.
 */
bool U6Lib_n::create(const Common::Path &filename, uint8 size, uint8 type) {
	NuvieIOFileWrite *file = new NuvieIOFileWrite();
	if (!file->open(filename)) {
		DEBUG(0, LEVEL_ERROR, "U6Lib: Error creating %s\n", filename.toString().c_str());
		delete file;
		return false;
	}
	game_type = type;
	lib_size = size;
	data = (NuvieIO *)file;
	return true;
}


uint32 U6Lib_n::get_num_items(void) {
	return num_offsets;
}


/* Returns the location of `item_number' in the library file.
 */
uint32 U6Lib_n::get_item_offset(uint32 item_number) {
	if (item_number >= num_offsets)
		return 0;
	return (items[item_number].offset);
}

uint32 U6Lib_n::get_item_size(uint32 item_number) {
	if (item_number >= num_offsets)
		return 0;

	return (items[item_number].uncomp_size);
}


// read and return item data
unsigned char *U6Lib_n::get_item(uint32 item_number, unsigned char *ret_buf) {
	U6LibItem *item;
	unsigned char *buf, *lzw_buf;

	if (item_number >= num_offsets)
		return nullptr;

	item = &items[item_number];

	if (item->size == 0 || item->offset == 0)
		return nullptr;

	if (ret_buf == nullptr)
		buf = (unsigned char *)malloc(item->uncomp_size);
	else
		buf = ret_buf;

	data->seek(item->offset);

	if (is_compressed(item_number)) {
		U6Lzw lzw;
		lzw_buf = (unsigned char *)malloc(item->size);
		data->readToBuf(lzw_buf, item->size);
		lzw.decompress_buffer(lzw_buf, item->size, buf, item->uncomp_size);
	} else {
		data->readToBuf(buf, item->size);
	}
	return buf;
}

bool U6Lib_n::is_compressed(uint32 item_number) {
	uint32 i;

	switch (items[item_number].flag) {
	case 0x1 :
	case 0x20 :
		return true;
	case 0xff :
		for (i = item_number; i < num_offsets; i++) {
			if (items[i].flag != 0xff)
				break;
		}
		if (i < num_offsets)
			return is_compressed(i);
		break;
	}

	return false;
}

void U6Lib_n::parse_lib() {
	uint32 i;
	bool skip4 = false;

	if (lib_size != 2 && lib_size != 4)
		return;

	data->seekStart();

	if (game_type != NUVIE_GAME_U6) { //U6 doesn't have a 4 byte filesize header.
		skip4 = true;
		filesize = data->read4();
	} else
		filesize = data->get_size();

	num_offsets = calculate_num_offsets(skip4);

	items = (U6LibItem *)malloc(sizeof(U6LibItem) * (num_offsets + 1));
	memset(items, 0, sizeof(U6LibItem) * (num_offsets + 1));

	data->seekStart();
	if (skip4)
		data->seek(0x4);
	for (i = 0; i < num_offsets && !data->is_end(); i++) {
		if (lib_size == 2)
			items[i].offset = data->read2();
		else {
			items[i].offset = data->read4();
			// U6 converse files dont have flag?
			items[i].flag = (items[i].offset & 0xff000000) >> 24; //extract flag byte
			items[i].offset &= 0xffffff;
		}
	}

	items[num_offsets].offset = filesize; //this is used to calculate the size of the last item in the lib.

	calculate_item_sizes();

	return;
}


// for reading, calculate item sizes based on offsets
void U6Lib_n::calculate_item_sizes() {
	uint32 i, next_offset = 0;

	for (i = 0; i < num_offsets; i++) {
		items[i].size = 0;
		// get next non-zero offset, including the filesize at items[num_offsets]
		for (uint32 o = (i + 1); o <= num_offsets; o++)
			if (items[o].offset) {
				next_offset = items[o].offset;
				break;
			}

		if (items[i].offset && (next_offset > items[i].offset))
			items[i].size = next_offset - items[i].offset;

		items[i].uncomp_size = calculate_item_uncomp_size(&items[i]);
	}

	return;
}

// for reading, calculate uncompressed item size based on item flag
uint32 U6Lib_n::calculate_item_uncomp_size(U6LibItem *item) {
	uint32 uncomp_size = 0;

	switch (item->flag) {
	case 0x01 : //compressed
	case 0x20 : //MD fonts.lzc, MDD_MUS.LZC use this tag among others
		data->seek(item->offset);
		uncomp_size = data->read4();
		break;

	//FIX check this. uncompressed 4 byte item size header
	case 0xc1 :
		uncomp_size = item->size; // - 4;
		break;

	// uncompressed
	case 0x0  :
	case 0x2  :
	case 0xe0 :
	default   :
		uncomp_size = item->size;
		break;
	}

	return uncomp_size;
}

// we need to handle nullptr offsets at the start of the offset table in the converse.a file
uint32 U6Lib_n::calculate_num_offsets(bool skip4) { //skip4 bytes of header.
	uint32 i;
	uint32 offset = 0;

	if (skip4)
		data->seek(0x4);


// We assume the first data in the file is directly behind the offset table,
// so we continue scanning until we hit a data block.
	uint32 max_count = 0xffffffff;
	for (i = 0; !data->is_end(); i++) {
		if (i == max_count)
			return i;

		if (lib_size == 2)
			offset = data->read2();
		else {
			offset = data->read4();
			offset &= 0xffffff; // clear flag byte.
		}
		if (offset != 0) {
			if (skip4)
				offset -= 4;

			if (offset / lib_size < max_count)
				max_count = offset / lib_size;
		}
	}

	return 0;
}


/* For writing multiple files to a lib, read in source filenames and offsets
 * from an opened index file. Offsets may be ignored when writing.
 */
void U6Lib_n::load_index(Common::ReadStream *index_f) {
	char input[256] = "", // input line
	                  offset_str[9] = "", // listed offset
	                                  name[256] = ""; // source file name
	int in_len = 0, oc = 0; // length of input line, character in copy string
	int c = 0, entry_count = 0; // character in input line, number of entries

	if (!index_f)
		return;
	while (strgets(input, 256, index_f)) {
		in_len = strlen(input);
		// skip spaces, read offset, break on #
		for (c = 0; c < in_len && Common::isSpace(input[c]) && input[c] != '#'; c++);
		for (oc = 0; c < in_len && !Common::isSpace(input[c]) && input[c] != '#'; c++)
			offset_str[oc++] = input[c];
		offset_str[oc] = '\0';
		// skip spaces, read name, break on # or \n or \r
		for (; c < in_len && Common::isSpace(input[c]) && input[c] != '#'; c++);
		for (oc = 0; c < in_len && input[c] != '\n' && input[c] != '\r' && input[c] != '#'; c++)
			name[oc++] = input[c];
		name[oc] = '\0';
		if (strlen(offset_str)) { // if line is not empty (!= zero entry)
			uint32 offset32 = strtol(offset_str, nullptr, 16);
			add_item(offset32, name);
			++entry_count;
		}
		offset_str[0] = '\0';
		oc = 0;
	}
	(void)entry_count; // Fix "unused varable" warning
}


/* Append an offset and a name to the library. The other fields are initialized.
 */
void U6Lib_n::add_item(uint32 offset32, const char *name) {
	if (!num_offsets)
		items = (U6LibItem *)malloc(sizeof(U6LibItem));
	else
		items = (U6LibItem *)nuvie_realloc(items, sizeof(U6LibItem) * (num_offsets + 1));
	U6LibItem *item = &items[num_offsets];
	item->offset = offset32;
	item->name = new string(name);
	item->size = 0;
	item->uncomp_size = 0;
	item->flag = 0; // uncompressed
	item->data = nullptr;
	++num_offsets;
}


/* Returns the name of (filename associated with) `item_number'.
 */
const char *U6Lib_n::get_item_name(uint32 item_number) {
	if (item_number >= num_offsets)
		return nullptr;
	return (items[item_number].name ? items[item_number].name->c_str() : nullptr);
}


/* Set data for an item, in preparation of writing or to cache the library.
 * Size & uncompressed size is set to source length.
 */
void U6Lib_n::set_item_data(uint32 item_number, unsigned char *src, uint32 src_len) {
	if (item_number >= num_offsets)
		return;
// FIXME: need a way to set an item as compressed or uncompressed so we know
// which size to set
	items[item_number].size = src_len;
	items[item_number].uncomp_size = src_len;
	if (src_len) {
		unsigned char *dcopy = (unsigned char *)malloc(src_len);
		memcpy(dcopy, src, src_len);
		items[item_number].data = dcopy;
	} else
		items[item_number].data = 0;
}


/* For writing, (re)calculate item offsets from item sizes.
 */
void U6Lib_n::calc_item_offsets() {
	if (num_offsets == 0)
		return;
	if (items[0].size) // first offset is past library index
		items[0].offset = (num_offsets * lib_size);
	else
		items[0].offset = 0; // 0 = no data, no affect on other items
//    DEBUG(0,LEVEL_DEBUGGING,"calc_item_offsets: sizes[0] == %d\n", sizes[0]);
//    DEBUG(0,LEVEL_DEBUGGING,"calc_item_offsets: offsets[0] == %d\n", offsets[0]);
	for (uint32 i = 1; i < num_offsets; i++) {
		if (items[i].size) {
			// find previous item with non-zero offset
			uint32 prev_i = 0;
			for (uint32 i_sub = 1; i_sub <= i; i_sub++) {
				prev_i = i - i_sub;
				if (items[prev_i].offset != 0)
					break;
			}
			items[i].offset = (items[prev_i].offset + items[prev_i].size);
			if (items[i].offset == 0) // last item had no data; skip index here
				items[i].offset = (num_offsets * lib_size);
		} else
			items[i].offset = 0; // 0 = no data, no affect on other items
//        DEBUG(0,LEVEL_DEBUGGING,"calc_item_offsets: sizes[%d] == %d\n", i, sizes[i]);
//        DEBUG(0,LEVEL_DEBUGGING,"calc_item_offsets: offsets[%d] == %d\n", i, offsets[i]);
	}
}

void U6Lib_n::write_header() {
	data->seekStart();
	if (game_type == NUVIE_GAME_U6)
		return;

	uint32 totalSize = 4 + num_offsets * lib_size;

	for (uint i = 0; i < num_offsets; i++) {
		totalSize += items[i].size;
	}

	data->write4(totalSize);
}

/* Write the library index. (the 2 or 4 byte offsets before the data)
 */
void U6Lib_n::write_index() {
	data->seekStart();
	if (game_type != NUVIE_GAME_U6) {
		data->seek(4);
	}

	for (uint32 o = 0; o < num_offsets; o++) {
		uint32 offset = items[o].offset;
		if (game_type != NUVIE_GAME_U6 && offset != 0) {
			offset += 4;
		}
		if (lib_size == 2)
			data->write2((uint16)offset);
		else if (lib_size == 4)
			data->write4(offset);
	}
}


/* Write all item data to the library file at their respective offsets.
 */
void U6Lib_n::write_items() {
	for (uint32 i = 0; i < num_offsets; i++)
		write_item(i);
}


/* Write item data to the library file at the indicated offset, unless the
 * offset is 0 (then the data is considered empty).
 */
void U6Lib_n::write_item(uint32 item_number) {
	if (item_number >= num_offsets
	        || items[item_number].offset == 0 || items[item_number].size == 0)
		return;
	if (game_type == NUVIE_GAME_U6)
		data->seek(items[item_number].offset);
	else
		data->seek(items[item_number].offset + 4);
	((NuvieIOFileWrite *)data)->writeBuf(items[item_number].data, items[item_number].size);
}

} // End of namespace Nuvie
} // End of namespace Ultima
