/* 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/config-manager.h"
#include "twp/twp.h"
#include "twp/detection.h"
#include "twp/dialog.h"
#include "twp/motor.h"
#include "twp/resmanager.h"
#include "twp/squtil.h"
#include "twp/tsv.h"

namespace Twp {

class SerialMotors : public Motor {
public:
	SerialMotors(const std::initializer_list<Common::SharedPtr<Motor> > &motors) : _motors(motors) {}
	explicit SerialMotors(const Common::Array<Common::SharedPtr<Motor> > &motors) : _motors(motors) {}

	void onUpdate(float elapsed) override {
		if (!_motors.empty()) {
			_motors[0]->update(elapsed);
			if (!_motors[0]->isEnabled()) {
				debugC(kDebugDialog, "SerialMotors next");
				_motors.remove_at(0);
			}
		} else {
			debugC(kDebugDialog, "SerialMotors is over");
			disable();
		}
	}

private:
	Common::Array<Common::SharedPtr<Motor> > _motors;
};

class SelectLabelMotor : public Motor {
public:
	SelectLabelMotor(Dialog *dlg, int line, const Common::String &name)
		: _dlg(dlg), _line(line), _name(name) {
	}

	void onUpdate(float elapsed) override {
		_dlg->selectLabel(_line, _name);
		disable();
	}

private:
	Dialog *_dlg;
	int _line;
	Common::String _name;
};

CondStateVisitor::CondStateVisitor(Dialog *dlg, DialogSelMode mode) : _dlg(dlg), _mode(mode) {
}

DialogConditionState CondStateVisitor::createState(int line, DialogConditionMode mode) {
	return DialogConditionState(mode, _dlg->_context.actor, _dlg->_context.dialogName, line);
}

DialogConditionState::DialogConditionState() = default;

DialogConditionState::DialogConditionState(DialogConditionMode m, const Common::String &k, const Common::String &dlg, int ln)
	: mode(m), actorKey(k), dialog(dlg), line(ln) {
}

void CondStateVisitor::visit(const YOnce &node) {
	if (_mode == DialogSelMode::Choose)
		_dlg->_states.push_back(createState(node._line, DialogConditionMode::Once));
}

void CondStateVisitor::visit(const YShowOnce &node) {
	if (_mode == DialogSelMode::Show)
		_dlg->_states.push_back(createState(node._line, DialogConditionMode::ShowOnce));
}

void CondStateVisitor::visit(const YOnceEver &node) {
	if (_mode == DialogSelMode::Choose)
		_dlg->_states.push_back(createState(node._line, DialogConditionMode::OnceEver));
}

void CondStateVisitor::visit(const YTempOnce &node) {
	if (_mode == DialogSelMode::Show)
		_dlg->_states.push_back(createState(node._line, DialogConditionMode::TempOnce));
}

ExpVisitor::ExpVisitor(Dialog *dialog) : _dialog(dialog) {}
ExpVisitor::~ExpVisitor() = default;

void ExpVisitor::visit(const YCodeExp &node) {
	debugC(kDebugDialog, "execute code %s", node._code.c_str());
	sqexec(g_twp->getVm(), node._code.c_str(), "dialog");
}

void ExpVisitor::visit(const YGoto &node) {
	debugC(kDebugDialog, "execute goto %s", node._name.c_str());
	_dialog->selectLabel(node._line, node._name);
}

void ExpVisitor::visit(const YShutup &node) {
	debugC(kDebugDialog, "shutup");
	_dialog->_tgt->shutup();
}

void ExpVisitor::visit(const YPause &node) {
	debugC(kDebugDialog, "pause %d", node._time);
	_dialog->_action = _dialog->_tgt->pause(node._time);
}

void ExpVisitor::visit(const YWaitFor &node) {
	debugC(kDebugDialog, "TODO: waitFor {%s}", node._actor.c_str());
}

void ExpVisitor::visit(const YParrot &node) {
	_dialog->_context.parrot = node._active;
}

void ExpVisitor::visit(const YDialog &node) {
	_dialog->_context.actor = node._actor;
}

void ExpVisitor::visit(const YOverride &node) {
	warning("TODO: override %s", node._node.c_str());
}

void ExpVisitor::visit(const YAllowObjects &node) {
	warning("TODO: allowObjects");
}

void ExpVisitor::visit(const YWaitWhile &node) {
	debugC(kDebugDialog, "wait while");
	_dialog->_action = _dialog->_tgt->waitWhile(node._cond);
}

void ExpVisitor::visit(const YLimit &node) {
	debugC(kDebugDialog, "limit");
	_dialog->_context.limit = node._max;
}

void ExpVisitor::visit(const YSay &node) {
	_dialog->_action = _dialog->_tgt->say(node._actor, node._text);
}

CondVisitor::CondVisitor(Dialog *dialog) : _dialog(dialog) {}
CondVisitor::~CondVisitor() = default;

void CondVisitor::visit(const YCodeCond &node) {
	_accepted = _dialog->isCond(node._code);
}

void CondVisitor::visit(const YOnce &node) {
	_accepted = _dialog->isOnce(node._line);
}

void CondVisitor::visit(const YShowOnce &node) {
	_accepted = _dialog->isShowOnce(node._line);
}

void CondVisitor::visit(const YOnceEver &node) {
	_accepted = _dialog->isOnceEver(node._line);
}

void CondVisitor::visit(const YTempOnce &node) {
	_accepted = _dialog->isTempOnce(node._line);
}

DialogSlot::DialogSlot() : Node("DialogSlot") {}

Dialog::Dialog() : Node("Dialog") {}
Dialog::~Dialog() = default;

static YChoice *getChoice(DialogSlot *slot) {
	return (YChoice *)(slot->_stmt->_exp.get());
}

void Dialog::choose(int choice) {
	if (_state == WaitingForChoice) {
		choose(&_slots[choice]);
	}
}

void Dialog::choose(DialogSlot *slot) {
	if (slot && slot->_isValid) {
		sqcall("onChoiceClick");
		for (size_t i = 0; i < slot->_stmt->_conds.size(); i++) {
			Common::SharedPtr<YCond> cond = slot->_stmt->_conds[i];
			CondStateVisitor v(slot->_dlg, DialogSelMode::Choose);
			cond->accept(v);
		}
		YChoice *choice = getChoice(slot);
		if (slot->_dlg->_context.parrot) {
			slot->_dlg->_state = DialogState::Active;
			slot->_dlg->_tgt->say(slot->_dlg->_context.actor, choice->_text);
			slot->_dlg->_action = Common::SharedPtr<SelectLabelMotor>(new SelectLabelMotor(slot->_dlg, choice->_goto->_line, choice->_goto->_name));
		} else {
			slot->_dlg->selectLabel(choice->_goto->_line, choice->_goto->_name);
		}
	}
}

void Dialog::start(const Common::String &actor, const Common::String &name, const Common::String &node) {
	_context.actor = actor;
	_context.dialogName = name;
	_context.parrot = true;
	_context.limit = MAXCHOICES;
	// keepIf(self.states, proc(x: DialogConditionState): bool = x.mode != TempOnce);
	Common::String path = name + ".byack";
	debugC(kDebugDialog, "start dialog %s", path.c_str());
	GGPackEntryReader reader;
	reader.open(*g_twp->_pack, path);
	YackParser parser;
	_cu.reset(parser.parse(&reader));
	selectLabel(0, node);
	update(0);
}

void Dialog::update(float dt) {
	_fadeTime += dt;
	switch (_state) {
	case DialogState::None:
		break;
	case DialogState::Active:
		running(dt);
		break;
	case DialogState::WaitingForChoice: {
		Color color = _tgt->actorColor(_context.actor);
		Color colorHover = _tgt->actorColorHover(_context.actor);
		for (size_t i = 0; i < MAXDIALOGSLOTS; i++) {
			DialogSlot *slot = &_slots[i];
			if (slot->_isValid) {
				Rectf rect = Rectf::fromPosAndSize(slot->getPos() - Math::Vector2d(0.f, -slot->getSize().getY() / 2.f), Math::Vector2d(SCREEN_WIDTH - SLOTMARGIN, slot->getSize().getY()));
				bool over = rect.contains(_mousePos);
				// shake choice when cursor is over
				if ((slot->_shakeTime > 0.0f) && slot->_shake) {
					slot->_shake->update(dt);
					slot->_shakeTime -= dt;
					if (slot->_shakeTime < 0.f) {
						slot->_shakeTime = 0.f;
					}
				}
				if (over && !slot->_over && slot->_shakeTime < 0.1f) {
					slot->_shakeTime = 0.25f;
					slot->_shake = Common::ScopedPtr<Motor>(new Shake(slot, 0.6f));
					slot->_over = over;
				}
				if (!over) {
					slot->_over = false;
				}
				// slide choice text wen text is too large
				const float width = slot->getSize().getX();
				if (width > (SCREEN_WIDTH - SLOTMARGIN)) {
					if (over) {
						if ((width + slot->getPos().getX()) > (SCREEN_WIDTH - SLOTMARGIN)) {
							slot->setPos(Math::Vector2d(slot->getPos().getX() - SLIDINGSPEED * dt, slot->getPos().getY()));
							if ((width + slot->getPos().getX()) < (SCREEN_WIDTH - SLOTMARGIN)) {
								slot->setPos(Math::Vector2d((SCREEN_WIDTH - SLOTMARGIN) - width, slot->getPos().getY()));
							}
						}
					} else if (slot->getPos().getX() < SLOTMARGIN) {
						slot->setPos(Math::Vector2d(slot->getPos().getX() + SLIDINGSPEED * dt, slot->getPos().getY()));
						if (slot->getPos().getX() > SLOTMARGIN) {
							slot->setPos(Math::Vector2d(SLOTMARGIN, slot->getPos().getY()));
						}
					}
				}
				slot->_text.setColor(over ? colorHover : color);
				if (over && g_twp->_cursor.isLeftDown())
					choose(i);
			}
		}
	} break;
	}
}

bool Dialog::isOnce(int line) const {
	for (const auto &state : _states) {
		if (state.mode == Once && state.actorKey == _context.actor && state.dialog == _context.dialogName && state.line == line) {
			debugC(kDebugDialog, "isOnce %d: false", line);
			return false;
		}
	}
	debugC(kDebugDialog, "isOnce %d: true", line);
	return true;
}

bool Dialog::isShowOnce(int line) const {
	for (const auto &state : _states) {
		if (state.mode == ShowOnce && state.actorKey == _context.actor && state.dialog == _context.dialogName && state.line == line) {
			debugC(kDebugDialog, "isShowOnce %d: false", line);
			return false;
		}
	}
	debugC(kDebugDialog, "isShowOnce %d: true", line);
	return true;
}

bool Dialog::isOnceEver(int line) const {
	for (const auto &state : _states) {
		if (state.mode == OnceEver && state.dialog == _context.dialogName && state.line == line) {
			debugC(kDebugDialog, "isOnceEver %d: false", line);
			return false;
		}
	}
	debugC(kDebugDialog, "isOnceEver %d: true", line);
	return true;
}

bool Dialog::isTempOnce(int line) const {
	for (const auto &state : _states) {
		if (state.mode == TempOnce && state.actorKey == _context.actor && state.dialog == _context.dialogName && state.line == line) {
			debugC(kDebugDialog, "isTempOnce %d: false", line);
			return false;
		}
	}
	debugC(kDebugDialog, "isTempOnce %d: true", line);
	return true;
}

bool Dialog::isCond(const Common::String &cond) const {
	bool result = _tgt->execCond(cond);
	debugC(kDebugDialog, "isCond '%s': %s", cond.c_str(), result ? "TRUE" : "FALSE");
	return result;
}

Common::SharedPtr<YLabel> Dialog::label(int line, const Common::String &name) const {
	for (auto label : _cu->_labels) {
		if ((label->_name == name) && (label->_line >= line)) {
			return label;
		}
	}
	line = 0;
	for (auto label : _cu->_labels) {
		if ((label->_name == name) && (label->_line >= line)) {
			return label;
		}
	}
	return nullptr;
}

void Dialog::selectLabel(int line, const Common::String &name) {
	debugC(kDebugDialog, "select label %s", name.c_str());
	_lbl = label(line, name);
	_currentStatement = 0;
	clearSlots();
	_state = _lbl ? Active : None;
}

void Dialog::gotoNextLabel() {
	if (_lbl) {
		size_t i = Twp::find(_cu->_labels, _lbl);
		if ((i != (size_t)-1) && (i != _cu->_labels.size() - 1)) {
			Common::SharedPtr<YLabel> label = _cu->_labels[i + 1];
			selectLabel(label->_line, label->_name);
		} else {
			_state = None;
		}
	}
}

void Dialog::updateChoiceStates() {
	_state = WaitingForChoice;
	_fadeTime = 0.f;
	for (auto &_slot : _slots) {
		DialogSlot *slot = &_slot;
		if (slot->_isValid) {
			for (auto cond : slot->_stmt->_conds) {
				CondStateVisitor v(this, DialogSelMode::Show);
				cond->accept(v);
			}
		}
	}
}

void Dialog::run(Common::SharedPtr<YStatement> stmt) {
	if (acceptConditions(stmt)) {
		ExpVisitor visitor(this);
		stmt->_exp->accept(visitor);
		IsGoto isGoto;
		stmt->_exp->accept(isGoto);
		if (isGoto._isGoto)
			return;
	}
	_currentStatement++;
}

bool Dialog::acceptConditions(Common::SharedPtr<YStatement> stmt) {
	CondVisitor vis(this);
	for (auto cond : stmt->_conds) {
		cond->accept(vis);
		if (!vis._accepted) {
			return false;
		}
	}
	return true;
}
void Dialog::running(float dt) {
	if (_action && _action->isEnabled())
		_action->update(dt);
	else if (!_lbl)
		_state = None;
	else if (_currentStatement == _lbl->_stmts.size())
		gotoNextLabel();
	else {
		_state = Active;
		while (_lbl && (_currentStatement < _lbl->_stmts.size()) && (_state == Active)) {
			Common::SharedPtr<YStatement> statmt = _lbl->_stmts[_currentStatement];
			IsChoice isChoice;
			statmt->_exp->accept(isChoice);
			if (!acceptConditions(statmt))
				_currentStatement++;
			else if (isChoice._isChoice) {
				addSlot(statmt);
				_currentStatement++;
			} else if (choicesReady())
				updateChoiceStates();
			else if (_action && _action->isEnabled()) {
				_action->update(dt);
				return;
			} else {
				IsShutup isShutup;
				statmt->_exp->accept(isShutup);
				if (g_twp->isSomeoneTalking() && !isShutup._isShutup) {
					return;
				}
				run(statmt);
				if (_lbl && (_currentStatement == _lbl->_stmts.size()))
					gotoNextLabel();
			}
		}
		if (choicesReady())
			updateChoiceStates();
		else if (!_action || !_action->isEnabled())
			_state = None;
	}
}

static Common::String text(const Common::String &txt) {
	Common::String result(g_twp->getTextDb().getText(txt));
	result = remove(result, '(', ')');
	result = remove(result, '{', '}');
	return result;
}

void Dialog::addSlot(Common::SharedPtr<YStatement> stmt) {
	YChoice *choice = (YChoice *)stmt->_exp.get();
	if ((!_slots[choice->_number - 1]._isValid) && (numSlots() < _context.limit)) {
		DialogSlot *slot = &_slots[choice->_number - 1];
		slot->_text.setFont("sayline");
		slot->_text.setText(Common::String::format("● %s", text(choice->_text).c_str()));
		slot->_stmt = stmt;
		slot->_dlg = this;
		Math::Vector2d slotSize(slot->_text.getBounds());
		float slotHeight = slotSize.getY() - 3.f;
		slot->setSize({slotSize.getX(), slotHeight});
		const float y = slotHeight * (MAXCHOICES - numSlots() - 2);
		slot->setPos(Math::Vector2d(SLOTMARGIN, y));
		slot->_isValid = true;
	}
}

int Dialog::numSlots() const {
	int num = 0;
	for (const auto &_slot : _slots) {
		if (_slot._isValid)
			num++;
	}
	return num;
}

void Dialog::clearSlots() {
	for (auto &_slot : _slots) {
		_slot._isValid = false;
	}
}

void Dialog::drawCore(const Math::Matrix4 &trsf) {
	if (_state == WaitingForChoice) {
		// draw HUD background
		SpriteSheet *gameSheet = g_twp->_resManager->spriteSheet("GameSheet");
		const SpriteSheetFrame &backingFrame = gameSheet->getFrame("ui_backing_tall");
		Texture *gameTexture = g_twp->_resManager->texture(gameSheet->meta.image);
		float alpha = 0.33f; // prefs(UiBackingAlpha);
		g_twp->getGfx().drawSprite(backingFrame.frame, *gameTexture, Color(0, 0, 0, alpha * getAlpha()), trsf);
	}

	const float slotDelay = 0.1f;
	float fadeTime = MIN(_fadeTime, 1.0f + slotDelay * MAXCHOICES);
	int slotNum = 0;
	for (auto &_slot : _slots) {
		DialogSlot *slot = &_slot;
		if (slot->_isValid) {
			Color c = slot->_text.getColor();
			Math::Matrix4 t(slot->getTrsf(trsf));
			float alpha = CLIP(6.f * (fadeTime - slotNum * slotDelay), 0.f, 1.f);
			t.translate(Math::Vector3d(0.f, (6.f * alpha), 0.f));
			slot->_text.setColor(Color::withAlpha(c, alpha));
			slot->_text.draw(g_twp->getGfx(), t);
			slotNum++;
		}
	}
}

int Dialog::getActiveSlot(const Math::Vector2d &pos) const {
	int index = -1;
	int num = 0;
	for (int i = 0; i < MAXDIALOGSLOTS; i++) {
		const DialogSlot *slot = &_slots[i];
		if (!slot->_isValid)
			continue;
		const Math::Vector2d p = slot->getPos();
		const Math::Vector2d s = slot->getSize();
		const Rectf r(p.getX(), p.getY() + s.getY() / 2.f, s.getX(), s.getY());
		if (r.contains(pos)) {
			index = num;
		}
		num++;
	}
	return index;
}

Math::Vector2d Dialog::getChoicePos(int index) const {
	int n = 0;
	for (int i = 0; i < MAXDIALOGSLOTS; i++) {
		const DialogSlot *slot = &_slots[i];
		if (!slot->_isValid)
			continue;
		if (n == index) {
			Math::Vector2d p(slot->getPos());
			Math::Vector2d s(slot->getSize());
			return Math::Vector2d(p.getX() + s.getX() / 2.f, p.getY() + s.getY() + 8.f);
		}
		n++;
	}
	return Math::Vector2d();
}

Math::Vector2d Dialog::getNextChoicePos(const Math::Vector2d &pos) {
	int index = getActiveSlot(pos);
	index = MIN(index + 1, numSlots() - 1);
	return getChoicePos(index);
}

Math::Vector2d Dialog::getPreviousChoicePos(const Math::Vector2d &pos) {
	int index = getActiveSlot(pos);
	index = MAX(index - 1, 0);
	return getChoicePos(index);
}

} // namespace Twp
