/* 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 "common/endian.h"
#include "common/stream.h"
#include "common/system.h"
#include "mm/mm1/gfx/dta.h"
#include "mm/mm1/gfx/screen_decoder.h"
#include "mm/mm1/maps/maps.h"
#include "mm/mm1/maps/map00.h"
#include "mm/mm1/maps/map01.h"
#include "mm/mm1/maps/map02.h"
#include "mm/mm1/maps/map03.h"
#include "mm/mm1/maps/map04.h"
#include "mm/mm1/maps/map05.h"
#include "mm/mm1/maps/map06.h"
#include "mm/mm1/maps/map07.h"
#include "mm/mm1/maps/map08.h"
#include "mm/mm1/maps/map09.h"
#include "mm/mm1/maps/map10.h"
#include "mm/mm1/maps/map11.h"
#include "mm/mm1/maps/map12.h"
#include "mm/mm1/maps/map13.h"
#include "mm/mm1/maps/map14.h"
#include "mm/mm1/maps/map15.h"
#include "mm/mm1/maps/map16.h"
#include "mm/mm1/maps/map17.h"
#include "mm/mm1/maps/map18.h"
#include "mm/mm1/maps/map19.h"
#include "mm/mm1/maps/map20.h"
#include "mm/mm1/maps/map21.h"
#include "mm/mm1/maps/map22.h"
#include "mm/mm1/maps/map23.h"
#include "mm/mm1/maps/map24.h"
#include "mm/mm1/maps/map25.h"
#include "mm/mm1/maps/map26.h"
#include "mm/mm1/maps/map27.h"
#include "mm/mm1/maps/map28.h"
#include "mm/mm1/maps/map29.h"
#include "mm/mm1/maps/map30.h"
#include "mm/mm1/maps/map31.h"
#include "mm/mm1/maps/map32.h"
#include "mm/mm1/maps/map33.h"
#include "mm/mm1/maps/map34.h"
#include "mm/mm1/maps/map35.h"
#include "mm/mm1/maps/map36.h"
#include "mm/mm1/maps/map37.h"
#include "mm/mm1/maps/map38.h"
#include "mm/mm1/maps/map39.h"
#include "mm/mm1/maps/map40.h"
#include "mm/mm1/maps/map41.h"
#include "mm/mm1/maps/map42.h"
#include "mm/mm1/maps/map43.h"
#include "mm/mm1/maps/map44.h"
#include "mm/mm1/maps/map45.h"
#include "mm/mm1/maps/map46.h"
#include "mm/mm1/maps/map47.h"
#include "mm/mm1/maps/map48.h"
#include "mm/mm1/maps/map49.h"
#include "mm/mm1/maps/map50.h"
#include "mm/mm1/maps/map51.h"
#include "mm/mm1/maps/map52.h"
#include "mm/mm1/maps/map53.h"
#include "mm/mm1/maps/map54.h"
#include "mm/mm1/maps/map55.h"
#include "mm/mm1/mm1.h"

namespace MM {
namespace MM1 {

Maps::Maps *g_maps;

namespace Maps {

static const byte LOOKUPS_START[4] = { 0, 0, 14, 34 };
static const byte COLOR_OFFSET[55] = {
	1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0,
	0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
	1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
	1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1,
	1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1
};

static const uint16 TILE_AREA1[] = {
	0x10D, 0x0B0B, 0x50A, 0x11A, 0x0B18, 0x517
};
static const uint16 TILE_AREA2[] = {
	0xB0B, 0x50A, 0x10D, 0x0F08, 0x907, 0x11A, 0x0B18, 0x517
};
static const uint16 TILE_AREA3[] = {
	0xB0B, 0x10D, 0x517, 0x0B18, 0x11A, 0x50A
};

static const uint16 *TILE_AREAS[3] = { TILE_AREA1, TILE_AREA2, TILE_AREA3 };
static const byte TILE_OFFSET[3] = { 1,  7, 15 };

#define RESOURCE_TILES_COUNT 12
static const uint16 TILE_WIDTHS[RESOURCE_TILES_COUNT] = {
	32, 40, 24, 16, 32, 40, 24, 16, 176, 96, 48, 16
};
static const uint16 TILE_HEIGHTS[RESOURCE_TILES_COUNT] = {
	128, 96, 64, 32, 128, 96, 64, 32, 96, 64, 32, 16
};

static byte TILE_COLORS[18] = {
	0xe6, 0xe6, 0xe6, 0x72, 0x72, 0x72, 0x62, 0x62, 0x62,
	0x62, 0x62, 0xe1, 0x53, 0x53, 0xff, 0x43, 0x43, 0x63
};

Maps::Maps() {
	g_maps = this;

	_maps.push_back(new Map00());
	_maps.push_back(new Map01());
	_maps.push_back(new Map02());
	_maps.push_back(new Map03());
	_maps.push_back(new Map04());
	_maps.push_back(new Map05());
	_maps.push_back(new Map06());
	_maps.push_back(new Map07());
	_maps.push_back(new Map08());
	_maps.push_back(new Map09());
	_maps.push_back(new Map10());
	_maps.push_back(new Map11());
	_maps.push_back(new Map12());
	_maps.push_back(new Map13());
	_maps.push_back(new Map14());
	_maps.push_back(new Map15());
	_maps.push_back(new Map16());
	_maps.push_back(new Map17());
	_maps.push_back(new Map18());
	_maps.push_back(new Map19());
	_maps.push_back(new Map20());
	_maps.push_back(new Map21());
	_maps.push_back(new Map22());
	_maps.push_back(new Map23());
	_maps.push_back(new Map24());
	_maps.push_back(new Map25());
	_maps.push_back(new Map26());
	_maps.push_back(new Map27());
	_maps.push_back(new Map28());
	_maps.push_back(new Map29());
	_maps.push_back(new Map30());
	_maps.push_back(new Map31());
	_maps.push_back(new Map32());
	_maps.push_back(new Map33());
	_maps.push_back(new Map34());
	_maps.push_back(new Map35());
	_maps.push_back(new Map36());
	_maps.push_back(new Map37());
	_maps.push_back(new Map38());
	_maps.push_back(new Map39());
	_maps.push_back(new Map40());
	_maps.push_back(new Map41());
	_maps.push_back(new Map42());
	_maps.push_back(new Map43());
	_maps.push_back(new Map44());
	_maps.push_back(new Map45());
	_maps.push_back(new Map46());
	_maps.push_back(new Map47());
	_maps.push_back(new Map48());
	_maps.push_back(new Map49());
	_maps.push_back(new Map50());
	_maps.push_back(new Map51());
	_maps.push_back(new Map52());
	_maps.push_back(new Map53());
	_maps.push_back(new Map54());
	_maps.push_back(new Map55());
}

Maps::~Maps() {
	for (uint i = 0; i < _maps.size(); ++i)
		delete _maps[i];
	g_maps = nullptr;
}

void Maps::load(uint mapId) {
	_mapId = mapId;
	_currentMap = _maps[mapId];
	_currentMap->load();
}

void Maps::synchronize(Common::Serializer &s) {
	// Store a count of the number of maps, just in case new ones
	// are added in later as easter eggs just for ScummVM
	int mapCount = _maps.size();
	s.syncAsByte(mapCount);

	for (int i = 0; i < mapCount; ++i) {
		s.syncBytes(_maps[i]->_visited, MAP_SIZE);
	}
}

void Maps::synchronizeCurrent(Common::Serializer &s) {
	// Save current map
	s.syncAsUint16LE(_id);
	s.syncAsByte(_section);

	s.syncAsByte(_mapPos.x);
	s.syncAsByte(_mapPos.y);
	s.syncAsByte(_forwardMask);

	if (s.isLoading()) {
		updateMasksOffsets();
		changeMap(_id, _section);
	}
}

void Maps::select(uint16 id, byte section) {
	_id = id;
	_section = section;

	uint mapId = getIndex(id, section);
	load(mapId);
}

void Maps::display(uint16 id, byte section) {
	select(id, section);
	loadTiles();
	g_events->send("Game", GameMessage("UPDATE"));
}

void Maps::loadTown(TownId townId) {
	switch (townId) {
	case SORPIGAL:
		town15setup();
		_mapPos = Common::Point(8, 3);
		display(0x604);
		break;

	case PORTSMITH:
		town23setup();
		_mapPos = Common::Point(3, 12);
		display(0xc03);
		break;

	case ALGARY:
		town23setup();
		_mapPos = Common::Point(14, 8);
		display(0x302);
		break;

	case DUSK:
		town4setup();
		_mapPos = Common::Point(11, 8);
		display(0x802);
		break;

	case ERLIQUIN:
		town15setup();
		_mapPos = Common::Point(4, 4);
		display(0xB1A);
		break;
	default:
		break;
	}

	byte &visited = _currentMap->_visited[_mapPos.y * MAP_W + _mapPos.x];
	if (!visited)
		visited = VISITED_NORMAL;
}

void Maps::town15setup() {
	_forwardMask = DIRMASK_N;
	updateMasksOffsets();
}

void Maps::town23setup() {
	_forwardMask = DIRMASK_W;
	updateMasksOffsets();
}

void Maps::town4setup() {
	_forwardMask = DIRMASK_E;
	updateMasksOffsets();
}

uint Maps::getIndex(uint16 id, byte section) {
	uint idx = LOOKUPS_START[section];

	// Find map by Id
	for (; idx < _maps.size() && id != _maps[idx]->getId(); ++idx) {}
	assert(idx < _maps.size());

	_colorOffset = COLOR_OFFSET[idx];
	return idx;
}

void Maps::loadTiles() {
	_loadArea = _currentMap->dataByte(MAP_1);
	_loadId = _currentMap->dataWord(MAP_2);
	_loadSection = 1;
	loadTile();

	_loadArea = _currentMap->dataByte(MAP_1);
	_loadId = _currentMap->dataWord(MAP_4);
	_loadSection = 2;
	loadTile();

	_loadArea = _currentMap->dataByte(MAP_1);
	_loadId = _currentMap->dataWord(MAP_6);
	_loadSection = 3;
	loadTile();
}

void Maps::loadTile() {
	assert(_loadArea >= 1 && _loadArea <= 3);
	const uint16 *arr = TILE_AREAS[_loadArea - 1];
	int ctr = TILE_OFFSET[_loadArea - 1];
	int entryIndex;

	for (; *arr != _loadId; ++arr, ++ctr) {
	}

	_loadFlag = 0xff;
	if (ctr >= 19) {
		if (ctr != 19)
			_loadFlag = 0xaa;
		ctr = 1;
	}

	// Get the entry from the wallpix.dta file
	entryIndex = ctr - 1;
	Gfx::DTA dta(WALLPIX_DTA);
	Common::SeekableReadStream *entry = dta.load(entryIndex);
	entry->skip(2);

	// Decode the tiles
	Common::Array<Graphics::ManagedSurface> &tiles =
		_tiles[_loadSection - 1];
	tiles.clear();
	tiles.resize(RESOURCE_TILES_COUNT);

	Gfx::ScreenDecoder decoder;
	byte colors = TILE_COLORS[entryIndex];
	assert(colors);
	decoder._indexes[0] = 0;
	decoder._indexes[1] = colors & 0xf;
	decoder._indexes[2] = (colors >> 4) & 0xf;
	decoder._indexes[3] = 15;

	for (int i = 0; i < RESOURCE_TILES_COUNT; ++i) {
		if (!decoder.loadStream(*entry,
			TILE_WIDTHS[i], TILE_HEIGHTS[i]))
			error("Failed decoding tile");

		tiles[i].copyFrom(*decoder.getSurface());
	}
}

void Maps::turnLeft() {
	_forwardMask = _leftMask;
	updateMasksOffsets();
}

void Maps::turnRight() {
	_forwardMask = _rightMask;
	updateMasksOffsets();
}

void Maps::turnAround() {
	_forwardMask = _backwardsMask;
	updateMasksOffsets();
}

void Maps::step(const Common::Point &delta) {
	_mapPos += delta;
	byte &visited = _currentMap->_visited[_mapPos.y * MAP_W + _mapPos.x];
	if (!visited) {
		visited = VISITED_NORMAL;
	}

	int section = 0, id = 0;
	if (_mapPos.x < 0) {
		_mapPos.x = MAP_W - 1;
		id = _currentMap->dataWord(MAP_WEST_EXIT_ID);
		section = _currentMap->dataByte(MAP_WEST_EXIT_SECTION);
	} else if (_mapPos.x >= MAP_W) {
		_mapPos.x = 0;
		id = _currentMap->dataWord(MAP_EAST_EXIT_ID);
		section = _currentMap->dataByte(MAP_EAST_EXIT_SECTION);
	} else if (_mapPos.y < 0) {
		_mapPos.y = MAP_H - 1;
		id = _currentMap->dataWord(MAP_SOUTH_EXIT_ID);
		section = _currentMap->dataByte(MAP_SOUTH_EXIT_SECTION);
	} else if (_mapPos.y >= MAP_H) {
		_mapPos.y = 0;
		id = _currentMap->dataWord(MAP_NORTH_EXIT_ID);
		section = _currentMap->dataByte(MAP_NORTH_EXIT_SECTION);
	} else {
		return;
	}

	changeMap(id, section);
}

void Maps::changeMap(uint16 id, byte section) {
	// At this point, a new map is being entered
	select(id, section);
	loadTiles();

	visitedTile();

	g_events->send("Game", GameMessage("UPDATE"));
}

void Maps::visitedTile() {
	byte &visited = _currentMap->_visited[_mapPos.y * MAP_W + _mapPos.x];
	if (!visited)
		visited = VISITED_NORMAL;
}

void Maps::clearSpecial() {
	_currentState &= ~CELL_SPECIAL;
	_currentMap->_states[_mapOffset] &= ~CELL_SPECIAL;
}

Common::Point Maps::getMoveDelta(byte mask) {
	switch (mask) {
	case DIRMASK_E:
		return Common::Point(1, 0);
	case DIRMASK_W:
		return Common::Point(-1, 0);
	case DIRMASK_S:
		return Common::Point(0, -1);
	default:
		return Common::Point(0, 1);
	}
}

void Maps::updateMasksOffsets() {
	switch (_forwardMask) {
	case DIRMASK_N:
		_leftMask = DIRMASK_W;
		_rightMask = DIRMASK_E;
		_backwardsMask = DIRMASK_S;

		_forwardOffset = MAP_W;
		_leftOffset = -1;
		_rightOffset = 1;
		_backwardsOffset = -MAP_W;
		break;

	case DIRMASK_E:
		_leftMask = DIRMASK_N;
		_rightMask = DIRMASK_S;
		_backwardsMask = DIRMASK_W;

		_forwardOffset = 1;
		_leftOffset = MAP_W;
		_rightOffset = -MAP_W;
		_backwardsOffset = -1;
		break;

	case DIRMASK_S:
		_leftMask = DIRMASK_E;
		_rightMask = DIRMASK_W;
		_backwardsMask = DIRMASK_N;

		_forwardOffset = -MAP_W;
		_leftOffset = 1;
		_rightOffset = -1;
		_backwardsOffset = MAP_W;
		break;

	case DIRMASK_W:
		_leftMask = DIRMASK_S;
		_rightMask = DIRMASK_N;
		_backwardsMask = DIRMASK_E;

		_forwardOffset = -1;
		_leftOffset = -MAP_W;
		_rightOffset = MAP_W;
		_backwardsOffset = 1;
		break;

	default:
		break;
	}
}

} // namespace Maps
} // namespace MM1
} // namespace MM
