/* 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/>.
 *
 * Copyright 2020 Google
 *
 */
#include "hadesch/hadesch.h"
#include "hadesch/video.h"
#include "hadesch/ambient.h"
#include "common/translation.h"

namespace Hadesch {
static const char *kHighlightImage = "r6010ol0";
static const char *kMaterialsImage = "r6010ok0";
static const char *kMaterialsMoveImage = "r6020ba0";
static const char *kMinotaurImage = "r6040ba0";
static const char *kDigits = "0123456789";
static const int numSquares = 25;
static const char *minotaurStates[] = {
	"",
	"",
	"",
	"inactive",
	"encourage",
	"trapped",
	"escaped",
	"critical",
	"level",
	"idle"
};

enum DaedalusDialogState {
	kMinotaur0 = 0,
	kMinotaur1 = 1,
	kMinotaur2 = 2,
	kMinotaurInactive = 3,
	kMinotaurEncourage = 4,
	kMinotaurTrapped = 5,
	kMinotaurEscaped = 6,
	kMinotaurCritical = 7,
	kMinotaurLevel = 8,
	kMinotaurIdle = 9
};

enum Strength {
	kStrengthStraw = 1,
	kStrengthWood = 2,
	kStrengthBricks = 3,
	kStrengthStone = 4
};

enum Position {
	kDown = 0,
	kLeft = 1,
	kUp = 2,
	kRight = 3
};

static const char *dirnames[] = {
	"down",
	"left",
	"up",
	"right"
};

enum {
	kRerenderLabyrinth = 1017001
};

static const char *daedalusSoundSMK[] = {
	"R6100nA0",
	"R6100wA0",
	"R6100nB0",
	"R6100wC0",
	"R6100nD0",
};

static const TranscribedSound daedalusSoundsAIF[] = {
	{"R6100nH0", _hs("Help us to move the walls so that they are strong enough to stop the minotaur")},
	{"R6100nL0", _hs("Click on a square to rotate the walls")},
	{"R6100nG0", _hs("Some walls are already locked in place and won't rotate")},
	{"R6100nK0", _hs("If you need help, refer to workman's equations")},
	{"R6170nA0", _hs("Careful, my friend. Some of the walls are not strong enough")},
	{"R6150nA0", _hs("You're a brave bullfighter, my friend")},
	{"R6150nB0", _hs("Keep it up. It looks like he's tiring")},
	{"R6150nC0", _hs("That's taking the bull by the horns")},
	{"R6150nD0", _hs("Don't give up. You can't beat him")},
	{"R6180nA0", _hs("You have beaten the Minotaur. You have the makings of a hero")},
	{"R6180nC0", _hs("You have beaten the beast at last")},
	{"R6180nD0", _hs("You have done it. The people of Crete are once again safe")},
	{"R6170nC0", _hs("Let's try again")},
	{"R6170nD0", _hs("Warn the people of Crete: the Minotaur has escaped. Workers, keep the Minotaur back in the labyrinth")},
	{"R6170nE0", _hs("I believe you and the Minotaur have not seen the last of one another")},
	{"R6170nF0", _hs("Ah that was a nobble effort, my friend")},
	{"R6160nA0", _hs("The Minotaur has broken though a critical wall. Workers, calm on the beast")},
	{"R6090eA0", _hs("Eh. Hm")},
	{"R6190nA0", _hs("Ok. Onto level two")},
	{"R6190nB0", _hs("Onto level three")},
};

struct Wall {
	int _id;
	bool _isCritical;
	int _inTransit;
	Strength _strength;
	Position _position;

	Wall() {
		_id = -1;
		_isCritical = false;
		_strength = kStrengthStraw;
		_position = kDown;
		_inTransit = 0;
	}
};

struct Cell {
	Common::Array<Wall> _movableWalls;
	Common::Array<Wall> _immovableWalls;
	bool _isRotatable;

	Cell() {
		_isRotatable = false;
	}
};

struct Labyrinth {
	Cell _cells[25];
};

class MinotaurHandler : public Handler {
public:
	MinotaurHandler() {
		_highlight = -1;

		_dialogState = kMinotaur0;
		_minotaurX = 1;
		_minotaurY = 2;
		_minotaurTileId = 7;
		_soundCounter = 0;
		_soundMax = 5;

		_lastChargeSound = -1;
		_lastTrappedSound = -1;
		_lastEscapedSound = -1;
		_levelId = 0;

		// consts
		xVector = Common::Point(-55, -33);
		yVector = Common::Point(+55, -33);
	}

	void handleClick(const Common::String &name) override {
		if (name.firstChar() >= '0' && name.firstChar() <= '9') {
			rotate(name.asUint64());
			renderLabyrinth();
			return;
		}
		/* TODO: MNSH: Daedalus */
	}

	void handleEvent(int eventId) override {
		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
		switch (eventId) {
		case kRerenderLabyrinth:
			renderLabyrinth();
			break;
		case 17953:
			g_vm->addTimer(17954, 300);
			break;
		case 17954:
			treatDialogState();
			break;
		}
	}

	void handleMouseOver(const Common::String &name) override {
		if (name.firstChar() >= '0' && name.firstChar() <= '9')
			_highlight = name.asUint64();
		else
			_highlight = -1;
		renderLabyrinth();
	}
	void handleMouseOut(const Common::String &name) override {
		_highlight = -1;
		renderLabyrinth();
	}

	void prepareRoom() override {
		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
		room->loadHotZones("minotaur.hot", true);
		// TODO: load other puzzles
		loadPuzzle("3x3j");
		room->addStaticLayer("r6010pA0", 10000);
		room->addStaticLayer("r6010tA0", 6400);
		room->addStaticLayer("r6010oA0", 5500);
		room->addStaticLayer("r6010oB0", 4000);
		renderLabyrinth();
		g_vm->getHeroBelt()->setColour(HeroBelt::kCool);
		setDialogState(kMinotaur1);
	}

	bool handleCheat(const Common::String &cheat) override {
		for (unsigned i = 0; i < ARRAYSIZE(minotaurStates); i++)
			if (minotaurStates[i][0] && minotaurStates[i] == cheat) {
				setDialogState(DaedalusDialogState(i));
				return true;
			}
		return false;
	}

private:
	void setDialogState(DaedalusDialogState state) {
		if (_dialogState) {
			// TODO
		} else {
			_dialogState = state;
		}
	}

	void playDaedalusSound(int index) {
		// TODO: balance
		_currentSound = index;
		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
		if (index < ARRAYSIZE(daedalusSoundSMK))
			room->playVideo(daedalusSoundSMK[index], 17953);
		else
			room->playSpeech(daedalusSoundsAIF[index-ARRAYSIZE(daedalusSoundSMK)], 17953);
	}

	void playDaedalusSoundWrap() {
		Persistent *persistent = g_vm->getPersistent();
		int index = 0;
		switch (_dialogState) {
		case kMinotaur0:
			treatDialogState();
			return;
		case kMinotaur1:
			index = _soundCounter;
			break;
		case kMinotaur2:
			index = _soundCounter + 5;
			break;
		case kMinotaurInactive:
			index = 9;
			break;
		case kMinotaurEncourage:
			index = randomExcept(10, 13, _lastChargeSound);
			_lastChargeSound = index;
			break;
		case kMinotaurTrapped:
			if (persistent->_quest == kCreteQuest)
				index = 14;
			else
				index = randomExcept(14, 16, _lastTrappedSound);
			_lastTrappedSound = index;
			break;
		case kMinotaurEscaped:
			index = randomExcept(17, 20, _lastEscapedSound);
			_lastEscapedSound = index;
			break;
		case kMinotaurCritical:
			index = 21;
			break;
		case kMinotaurLevel:
			index = 23 + _levelId / 15;
			break;
		case kMinotaurIdle:
			index = 22;
			break;
		}

		playDaedalusSound(index);
	}

	int randomExcept(int from, int to, int except) {
		Common::RandomSource &r = g_vm->getRnd();
		if (except < from || except > to)
			return r.getRandomNumberRng(from, to);
		int x = r.getRandomNumberRng(from, to - 1);
		if (x >= except)
			x++;
		return x;
	}

	void treatDialogState() {
		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
		switch(_dialogState) {
		case kMinotaur1:
			if (_soundCounter < _soundMax) {
				playDaedalusSoundWrap();
				_soundCounter++;
				return;
			}
			setDialogState(kMinotaur2);
			return;
		case kMinotaur2:
			if (_soundCounter < _soundMax) {
				playDaedalusSoundWrap();
				_soundCounter++;
				return;
			}
			room->enableMouse();
			setDialogState(kMinotaur0);
			return;
		default:
			// TODO: implement this;
			return;
		}
	}

	void renderLabyrinth() {
		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
		if (_highlight >= 0)
			room->selectFrame(kHighlightImage, 9990, 0,
					  getTileBase(_highlight)
					  - Common::Point(34, 52));
		else
			room->stopAnim(kHighlightImage);

		for (int cell = 0; cell < numSquares; cell++) {
			for (int j = 0; j < ARRAYSIZE(dirnames); j++) {
				room->stopAnim(
					LayerId(kMaterialsImage, cell, Common::String(dirnames[j]) + "outer"));
				room->stopAnim(
					LayerId(kMaterialsImage, cell, Common::String(dirnames[j]) + "inner"));
				room->stopAnim(
					LayerId(kMaterialsMoveImage, cell, "to-" + Common::String(dirnames[j])));
			}

			for (int j = 0; j < (int) _current._cells[cell]._movableWalls.size(); j++) {
				renderWall(cell, _current._cells[cell]._movableWalls[j], false);
			}

			// Both original engine and us we're not able to handle 3
			// walls in the same place. The only way to avoid this from every hapenning
			// is to never put an immovable wall between 2 cells with movable walls.
			// Hence if we have any movable walls then we can make immovable walls outer
			// and they will not conflict with existing labyrinths.
			// If we have no movable walls we can easily put all immovable walls as inner

			bool immovableAreOuter = !_current._cells[cell]._movableWalls.empty();

			for (int j = 0; j < (int) _current._cells[cell]._immovableWalls.size(); j++) {
				renderWall(cell, _current._cells[cell]._immovableWalls[j], immovableAreOuter);
			}
		}

		// TODO: which frame?
		room->selectFrame(kMinotaurImage, getMinotaurZ(), 30,
				  getTileBase(_minotaurX, _minotaurY) - Common::Point(/*114, 117*/30+82, 33*2+52));
	}

	int getMinotaurZ() {
		if (_minotaurX >= 5) {
			return 6500;
		}

		if (_minotaurX < 0) {
			return 4500;
		}

		if (_minotaurY >= 5) {
			return 5960;
		}

		if (_minotaurY < 0) {
			return 4500;
		}

		return 5000 + 150 * (_minotaurX + _minotaurY) + 60;
	}

	void renderWall(int cell, Wall &wall, bool outer) {
		Common::Point delta;
		if (wall._inTransit) {
			wall._inTransit--;
 			g_vm->getVideoRoom()->selectFrame(
				LayerId(kMaterialsMoveImage, cell, Common::String("to-") + dirnames[wall._position]),
				getWallZ(cell, wall._position, outer),
				(wall._strength - kStrengthStraw) * 4 + (wall._position + 1) % 4,
				getTileBase(cell) + Common::Point(-40, -88));
			g_vm->addTimer(kRerenderLabyrinth, kDefaultSpeed);
			return;
		}

		switch (wall._position) {
		case kUp:
			delta = xVector + xVector + yVector + Common::Point(-8, -3) +
				(outer ? Common::Point(0, 0) : Common::Point(+7, +5));
			break;
		case kDown:
			delta = xVector + yVector + Common::Point(-8, -3) +
				(outer ? Common::Point(+7, +5) : Common::Point(0, 0));
			break;
		case kLeft:
			delta = xVector + Common::Point(0, -33)
				+ (outer ? Common::Point(-7, +5) : Common::Point(0, 0));
			break;
		case kRight:
			delta = xVector + yVector + Common::Point(0, -33)
				+ (outer ? Common::Point(0, 0) : Common::Point(-7, +5));
			break;
		}

		Common::Point pos = getTileBase(cell) + delta;

		LayerId layer(kMaterialsImage, cell,
			      Common::String(dirnames[wall._position]) +
			      (outer ? "outer" : "inner"));
		int frame = (wall._strength - kStrengthStraw) * 2 + (wall._position % 2);
		g_vm->getVideoRoom()->selectFrame(layer, getWallZ(cell, wall._position, outer), frame, pos);
	}

	int getWallZ(int cell, Position pos, bool outer) {
		int zValue = 150 * (cell / 5 + cell % 5) + 5000;
		switch (pos) {
		case kUp:
			zValue += outer? 110 : 100;
			break;
		case kDown:
			zValue += outer ? -10 : 0;
			break;
		case kLeft:
			zValue += outer ? 40 : 50;
			break;
		case kRight:
			zValue += outer ? 80 : 70;
			break;
		}
		return zValue;
	}

	Common::Point getTileBase(int x, int y) {
		return Common::Point(320, 456) + xVector * x + yVector * y;
	}

	Common::Point getTileBase(int id) {
		return getTileBase(id / 5, id % 5);
	}

	void rotate(int cell) {
		for (int j = 0;
		     j < (int) _current._cells[cell]._movableWalls.size();
		     j++) {
			_current._cells[cell]._movableWalls[j]._position
				= (Position) ((_current._cells[cell]._movableWalls[j]._position + 1) % 4);
			_current._cells[cell]._movableWalls[j]._inTransit = 1;
		}
	}

	void loadPuzzle(const Common::String &name) {
		Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
		Common::SharedPtr<Common::SeekableReadStream> gameStream(room->openFile(name + ".mcf"));
		Common::SharedPtr<Common::SeekableReadStream> solStream(room->openFile(name + ".sol"));
		Common::SharedPtr<Common::SeekableReadStream> cwStream(room->openFile(name + ".cw"));
		readLabStream(_current, gameStream);
		readLabStream(_solution, solStream);
		for (int cell = 0; cell < numSquares; cell++) {
			room->setHotzoneEnabled(Common::String::format("%d", cell),
						!_current._cells[cell]._movableWalls.empty());
		}
	}

	static void readLabStream(Labyrinth &lab, Common::SharedPtr<Common::SeekableReadStream> stream) {
		stream->readLine(); // Level number
		int gridSize = stream->readLine().asUint64();
		stream->readLine(); // ?
		stream->readLine(); // ?
		int numLines = stream->readLine().asUint64();
		// Extra walls
		if (gridSize == 3 || gridSize == 4) {
			Wall w;
			w._isCritical = false;
			w._id = -1;
			w._strength = kStrengthStone;
			w._position = kRight;
			if (gridSize == 3)
				lab._cells[4]._immovableWalls.push_back(w);
			lab._cells[24]._immovableWalls.push_back(w);
		}
		for (int i = 0; i < numLines; i++) {
			Common::String line = stream->readLine();
			size_t cur = 0;
			int rawcellid = line.asUint64();
			int transformedcellid =
				5 * (rawcellid / gridSize) +
				(rawcellid % gridSize) + (5 - gridSize)
				+ 5 * ((5 - gridSize) / 2);
			cur = line.findFirstNotOf(kDigits, cur);
			cur = line.findFirstOf(kDigits, cur);
			int numWalls = line.substr(cur).asUint64();
			cur = line.findFirstNotOf(kDigits, cur);
			cur = line.findFirstOf(kDigits, cur);
			/*int rotatable =*/ line.substr(cur).asUint64();
			cur = line.findFirstNotOf(kDigits, cur);
			cur = line.findFirstOf(kDigits, cur);
			for (int j = 0; j < numWalls; j++) {
				Wall w;
				w._isCritical = false;
				w._id = line.substr(cur).asUint64();
				cur = line.findFirstNotOf(kDigits, cur);
				cur = line.findFirstOf(kDigits, cur);
				int pos = line.substr(cur).asUint64();
				cur = line.findFirstNotOf(kDigits, cur);
				cur = line.findFirstOf(kDigits, cur);
				w._strength = (Strength) line.substr(cur).asUint64();
				cur = line.findFirstNotOf(kDigits, cur);
				cur = line.findFirstOf(kDigits, cur);
				switch (pos % 4) {
				case 0:
					w._position = kLeft;
					break;
				case 1:
					w._position = kUp;
					break;
				case 2:
					w._position = kRight;
					break;
				case 3:
					w._position = kDown;
					break;
				}
				if (pos >= 4) {
					lab._cells[transformedcellid]._immovableWalls.push_back(w);
				} else {
					lab._cells[transformedcellid]._movableWalls.push_back(w);
				}
			}
		}
	}

	Common::Point xVector;
	Common::Point yVector;
	int _highlight;

	DaedalusDialogState _dialogState;
	int _minotaurX;
	int _minotaurY;
	int _minotaurTileId;
	int _lastChargeSound;
	int _lastTrappedSound;
	int _lastEscapedSound;
	int _levelId;
	int _currentSound;
	int _soundCounter;
	int _soundMax;

	Labyrinth _current;
	Labyrinth _solution;
};

Common::SharedPtr<Hadesch::Handler> makeMinotaurHandler() {
	return Common::SharedPtr<Hadesch::Handler>(new MinotaurHandler());
}

}
