/*
 * Copyright (C) 2021  P. J. McDermott
 *
 * This file is part of Dodge Balls
 *
 * Dodge Balls 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.
 *
 * Dodge Balls 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 Dodge Balls.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <SDL.h>
#include <SDL_image.h>
#include <expat.h>
#include "dirs.h"
#include "output.h"
#include "tileset.h"
#include "xml.h"

struct db_tileset {
	char              *game_id;
	SDL_Surface       *image;
	SDL_Texture       *texture;
	int                tw;
	int                th;
	int                tc;
	int                cols;
	Uint32             b_col;
	Uint32             p_col;
	int                cur_tile;
	int                firstgid;
	struct db_tileset *next;
};

static char *
_db_tileset_path(const char *game_id, const char *file)
{
	const char *games_dir;
	char       *path;

	games_dir = db_get_games_dir();

	path = malloc((strlen(games_dir) + strlen(game_id) + strlen(file) + 3) *
			sizeof(*path));
	if (path == NULL) {
		db_err("Failed to allocate memory");
		return NULL;
	}

	sprintf(path, "%s/%s/%s", games_dir, game_id, file);

	return path;
}

static void XMLCALL
_db_tsx_invalid_end(void *pv, const char *name)
{
	XML_Parser p;

	db_dbg("          </%s> (invalid)", name);

	p = (XML_Parser) pv;

	db_xml_node_pop(p);
}

static void XMLCALL
_db_tsx_invalid_start(void *pv, const char *name,
		const char **attr __attribute__((__unused__)))
{
	XML_Parser p;

	db_dbg("          <%s> (invalid)", name);

	p = (XML_Parser) pv;

	db_xml_node_push(p, NULL, _db_tsx_invalid_start,
			_db_tsx_invalid_end, NULL);
}

static void XMLCALL
_db_tsx_image_end(void *pv, const char *name)
{
	XML_Parser p;

	db_dbg("            </%s> (image)", name);

	p = (XML_Parser) pv;

	if (db_xml_check_tag(name, "image")) {
		db_xml_node_pop(p);
	} else {
		db_xml_unexpected_end_tag(p, name, "image");
	}
}

static void XMLCALL
_db_tsx_tile_end(void *pv, const char *name)
{
	XML_Parser p;

	db_dbg("            </%s> (tile)", name);

	p = (XML_Parser) pv;

	if (db_xml_check_tag(name, "tile")) {
		db_xml_node_pop(p);
	} else {
		db_xml_unexpected_end_tag(p, name, "tile");
	}
}

static void XMLCALL
_db_tsx_properties_end(void *pv, const char *name)
{
	XML_Parser p;

	db_dbg("              </%s> (properties)", name);

	p = (XML_Parser) pv;

	if (db_xml_check_tag(name, "properties")) {
		db_xml_node_pop(p);
	} else {
		db_xml_unexpected_end_tag(p, name, "properties");
	}
}

static void XMLCALL
_db_tsx_property_end(void *pv, const char *name)
{
	XML_Parser p;

	db_dbg("                </%s> (property)", name);

	p = (XML_Parser) pv;

	if (db_xml_check_tag(name, "property")) {
		db_xml_node_pop(p);
	} else {
		db_xml_unexpected_end_tag(p, name, "property");
	}
}

static void XMLCALL
_db_tsx_property_start(void *pv, const char *name, const char **attr)
{
	XML_Parser         p;
	struct db_tileset *tileset;
	char              *p_name;
	char              *p_type;
	SDL_bool           col;

	db_dbg("                <%s> (property)", name);

	p = (XML_Parser) pv;
	tileset = db_xml_node_peek(p);

	if (db_xml_check_tag(name, "property")) {
		db_xml_get_string_attr(p, attr, "name", &p_name, 1);
		db_xml_get_string_attr(p, attr, "type", &p_type, 1);
		if (strcmp(p_name, "ballcollides") == 0) {
			if (strcmp(p_type, "bool") != 0) {
				db_err("Ball collision must be a Boolean "
						"value");
				free(p_name);
				free(p_type);
				XML_StopParser(p, XML_FALSE);
				return;
			}
			col = SDL_FALSE;  /* Shut up, GCC. */
			db_xml_get_bool_attr(p, attr, "value", &col, 1);
			if (col) {
				tileset->b_col |= (1 << tileset->cur_tile);
			}
			db_dbg("                    Ball collision:   %s",
					(col ? "true" : "false"));
		} else if (strcmp(p_name, "playercollides") == 0) {
			if (strcmp(p_type, "bool") != 0) {
				db_err("Player collision must be a Boolean "
						"value");
				free(p_name);
				free(p_type);
				XML_StopParser(p, XML_FALSE);
				return;
			}
			col = SDL_FALSE;  /* Shut up, GCC. */
			db_xml_get_bool_attr(p, attr, "value", &col, 1);
			if (col) {
				tileset->p_col |= (1 << tileset->cur_tile);
			}
			db_dbg("                    Player collision: %s",
					(col ? "true" : "false"));
		} else {
			db_dbg("        Skipping unknown property \"%s\"",
					p_name);
		}
		free(p_name);
		free(p_type);
		db_xml_node_push(p, tileset, _db_tsx_invalid_start,
				_db_tsx_property_end, NULL);
	} else {
		db_xml_unexpected_start_tag(p, name, "property");
	}
}

static void XMLCALL
_db_tsx_properties_start(void *pv, const char *name,
		const char **attr __attribute__((__unused__)))
{
	XML_Parser         p;
	struct db_tileset *tileset;

	db_dbg("              <%s> (properties)", name);

	p = (XML_Parser) pv;
	tileset = db_xml_node_peek(p);

	if (db_xml_check_tag(name, "properties")) {
		db_xml_node_push(p, tileset, _db_tsx_property_start,
				_db_tsx_properties_end, NULL);
	} else {
		db_xml_unexpected_start_tag(p, name, "properties");
	}
}

static void XMLCALL
_db_tsx_tileset_el_start(void *pv, const char *name, const char **attr)
{
	XML_Parser         p;
	struct db_tileset *tileset;
	char              *source;
	char              *path;

	db_dbg("            <%s> (tileset child)", name);

	p = (XML_Parser) pv;
	tileset = db_xml_node_peek(p);

	if (db_xml_check_tag(name, "image")) {
		db_xml_get_string_attr(p, attr, "source", &source, 1);
		db_dbg("                Image: <%s>", source);
		path = _db_tileset_path(tileset->game_id, source);
		free(source);
		if (path == NULL) {
			XML_StopParser(p, XML_FALSE);
			return;
		}
		tileset->image = IMG_Load(path);
		free(path);
		if (tileset->image == NULL) {
			XML_StopParser(p, XML_FALSE);
			return;
		}
		db_xml_node_push(p, tileset, _db_tsx_invalid_start,
				_db_tsx_image_end, NULL);
	} else if (db_xml_check_tag(name, "tile")) {
		db_xml_get_int_attr(p, attr, "id",  &tileset->cur_tile, 1);
		db_dbg("                Tile size: %dx%d px",
				tileset->tw, tileset->th);
		db_xml_node_push(p, tileset, _db_tsx_properties_start,
				_db_tsx_tile_end, NULL);
	} else {
		db_xml_unexpected_start_tag(p, name, "image or tile");
	}
}

static void XMLCALL
_db_tsx_tileset_end(void *pv, const char *name)
{
	XML_Parser p;

	db_dbg("          </%s> (tileset)", name);

	p = (XML_Parser) pv;

	if (db_xml_check_tag(name, "tileset")) {
		db_xml_node_pop(p);
	} else {
		db_xml_unexpected_end_tag(p, name, "tileset");
	}
}

static void XMLCALL
_db_tsx_tileset_start(void *pv, const char *name, const char **attr)
{
	XML_Parser         p;
	struct db_tileset *tileset;
	int                tw;
	int                th;

	db_dbg("          <%s> (tileset)", name);

	p = (XML_Parser) pv;
	tileset = db_xml_node_peek(p);

	if (db_xml_check_tag(name, "tileset")) {
		db_xml_get_int_attr(p, attr, "tilewidth",  &tw,   1);
		db_xml_get_int_attr(p, attr, "tileheight", &th,   1);
		if (tw != tileset->tw || th != tileset->th) {
			db_err("Tileset has %dx%d tiles, not %dx%d like map",
					tw, th, tileset->tw, tileset->th);
			XML_StopParser(p, XML_FALSE);
			return;
		}
		db_xml_get_int_attr(p, attr, "tilecount",  &tileset->tc,   1);
		if (tileset->tc > 32) {
			db_err("Tileset may not have more than 32 tiles");
			XML_StopParser(p, XML_FALSE);
			return;
		}
		db_xml_get_int_attr(p, attr, "columns",    &tileset->cols, 1);
		db_dbg("              Tile size: %dx%d px",
				tileset->tw, tileset->th);
		db_xml_node_push(p, tileset, _db_tsx_tileset_el_start,
				_db_tsx_tileset_end, NULL);
	} else {
		db_xml_unexpected_start_tag(p, name, "tileset");
	}
}

struct db_tileset *
db_tileset_new(const char *game_id, const char *file, int firstgid,
		int tilewidth, int tileheight, struct db_tileset *prev)
{
	struct db_tileset *tileset;
	XML_Parser         p;
	char              *path;
	FILE              *fp;
	void              *buf;
	size_t             len;
	enum XML_Status    status;

	tileset = calloc(1, sizeof(*tileset));
	if (tileset == NULL) {
		db_err("Failed to allocate memory");
		return NULL;
	}

	tileset->firstgid = firstgid;
	tileset->tw = tilewidth;
	tileset->th = tileheight;
	if (prev != NULL) {
		prev->next = tileset;
	}

	tileset->game_id = strdup(game_id);
	if (tileset->game_id == NULL) {
		db_err("Failed to allocate memory");
		free(tileset);
		return NULL;
	}

	p = XML_ParserCreate(NULL);
	if (p == NULL) {
		free(tileset->game_id);
		free(tileset);
		return NULL;
	}
	XML_UseParserAsHandlerArg(p);
	db_xml_node_push(p, tileset, _db_tsx_tileset_start, _db_tsx_invalid_end,
			NULL);

	path = _db_tileset_path(game_id, file);
	if (path == NULL) {
		db_xml_node_pop(p);
		XML_ParserFree(p);
		free(path);
		free(tileset->game_id);
		free(tileset);
		return NULL;
	}

	fp = fopen(path, "rb");
	if (fp == NULL) {
		db_xml_node_pop(p);
		XML_ParserFree(p);
		free(path);
		free(tileset->game_id);
		free(tileset);
		return NULL;
	}

	buf = XML_GetBuffer(p, 8192);
	if (buf == NULL) {
		db_xml_node_pop(p);
		XML_ParserFree(p);
		free(path);
		fclose(fp);
		free(tileset->game_id);
		free(tileset);
		return NULL;
	}

	db_dbg("        Parsing <%s>:", path);
	free(path);

	while (!feof(fp)) {
		len = fread(buf, 1, 8192, fp);
		status = XML_ParseBuffer(p, len, feof(fp));
		if (status == XML_STATUS_OK) {
			continue;
		}
		db_err("Failed to parse tileset information (%s)",
				XML_ErrorString(XML_GetErrorCode(p)));
		db_xml_node_pop(p);
		XML_ParserFree(p);
		fclose(fp);
		free(tileset->game_id);
		free(tileset);
		return NULL;
	}

	db_dbg("        Parsing done");

	db_xml_node_pop(p);
	XML_ParserFree(p);
	fclose(fp);

	return tileset;
}

SDL_bool
db_tile_player_collides(struct db_tileset *tileset, int gid)
{
	for (; tileset != NULL; tileset = tileset->next) {
		if (gid >= tileset->firstgid &&
				gid < tileset->firstgid + tileset->tc) {
			gid -= tileset->firstgid;
			return tileset->p_col & 1 << gid;
		}
	}
	return SDL_FALSE;
}

SDL_bool
db_tile_ball_collides(struct db_tileset *tileset, int gid)
{
	for (; tileset != NULL; tileset = tileset->next) {
		if (gid >= tileset->firstgid &&
				gid < tileset->firstgid + tileset->tc) {
			gid -= tileset->firstgid;
			return tileset->b_col & 1 << gid;
		}
	}
	return SDL_FALSE;
}

static SDL_Texture *
_db_tileset_texture(struct db_tileset *tileset, SDL_Renderer *renderer)
{
	if (tileset->texture == NULL) {
		tileset->texture = SDL_CreateTextureFromSurface(renderer,
				tileset->image);
	}
	return tileset->texture;
}

int
db_tile_render(struct db_tileset *tileset, SDL_Renderer *renderer, int gid,
		SDL_Rect *dstrect)
{
	SDL_Rect srcrect;

	for (; tileset != NULL; tileset = tileset->next) {
		if (gid >= tileset->firstgid &&
				gid < tileset->firstgid + tileset->tc) {
			gid -= tileset->firstgid;
			srcrect.x = tileset->tw * (gid % tileset->cols);
			srcrect.y = tileset->th * (gid / tileset->cols);
			srcrect.w = tileset->tw;
			srcrect.h = tileset->th;
			db_dbg("Blitting %dx%d tile at (%d,%d) onto %dx%d area "
					"at (%d,%d)...",
					srcrect.w,  srcrect.h,
					srcrect.x,  srcrect.y,
					dstrect->w, dstrect->h,
					dstrect->x, dstrect->y);
			if (SDL_RenderCopy(renderer, _db_tileset_texture(
							tileset, renderer),
						&srcrect, dstrect)
					!= 0) {
				db_err("Failed to copy texture (%s)",
						SDL_GetError());
				return -1;
			}
			return 1;
		}
	}
	db_warn("Tile with gid 0x%8.8x not found", (unsigned int) gid);
	return 0;
}