/* 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/ultima4/game/person.h"
#include "ultima/ultima4/game/names.h"
#include "ultima/ultima4/game/player.h"
#include "ultima/ultima4/views/stats.h"
#include "ultima/ultima4/game/context.h"
#include "ultima/ultima4/game/script.h"
#include "ultima/ultima4/controllers/read_choice_controller.h"
#include "ultima/ultima4/controllers/read_int_controller.h"
#include "ultima/ultima4/controllers/read_player_controller.h"
#include "ultima/ultima4/conversation/conversation.h"
#include "ultima/ultima4/core/config.h"
#include "ultima/ultima4/core/settings.h"
#include "ultima/ultima4/core/types.h"
#include "ultima/ultima4/core/utils.h"
#include "ultima/ultima4/events/event_handler.h"
#include "ultima/ultima4/filesys/savegame.h"
#include "ultima/ultima4/map/city.h"
#include "ultima/ultima4/map/location.h"
#include "ultima/ultima4/sound/music.h"
#include "ultima/ultima4/ultima4.h"

namespace Ultima {
namespace Ultima4 {

int chars_needed(const char *s, int columnmax, int linesdesired, int *real_lines);

/**
 * Returns true of the object that 'punknown' points
 * to is a person object
 */
bool isPerson(Object *punknown) {
	Person *p;
	if ((p = dynamic_cast<Person *>(punknown)) != nullptr)
		return true;
	else
		return false;
}

/**
 * Splits a piece of response text into screen-sized chunks.
 */
Common::List<Common::String> replySplit(const Common::String &text) {
	Common::String str = text;
	int pos, real_lines;
	Common::List<Common::String> reply;

	/* skip over any initial newlines */
	if ((pos = str.find("\n\n")) == 0)
		str = str.substr(pos + 1);

	uint num_chars = chars_needed(str.c_str(), TEXT_AREA_W, TEXT_AREA_H, &real_lines);

	/* we only have one chunk, no need to split it up */
	uint len = str.size();
	if (num_chars == len)
		reply.push_back(str);
	else {
		Common::String pre = str.substr(0, num_chars);

		/* add the first chunk to the list */
		reply.push_back(pre);
		/* skip over any initial newlines */
		if ((pos = str.find("\n\n")) == 0)
			str = str.substr(pos + 1);

		while (num_chars != str.size()) {
			/* go to the rest of the text */
			str = str.substr(num_chars);
			/* skip over any initial newlines */
			if ((pos = str.find("\n\n")) == 0)
				str = str.substr(pos + 1);

			/* find the next chunk and add it */
			num_chars = chars_needed(str.c_str(), TEXT_AREA_W, TEXT_AREA_H, &real_lines);
			pre = str.substr(0, num_chars);

			reply.push_back(pre);
		}
	}

	return reply;
}

Person::Person(MapTile tile) : Creature(tile), _start(0, 0) {
	setType(Object::PERSON);
	_dialogue = nullptr;
	_npcType = NPC_EMPTY;
}

Person::Person(const Person *p) : Creature(p->_tile) {
	*this = *p;
}

bool Person::canConverse() const {
	return isVendor() || _dialogue != nullptr;
}

bool Person::isVendor() const {
	return
	    _npcType >= NPC_VENDOR_WEAPONS &&
	    _npcType <= NPC_VENDOR_STABLE;
}

Common::String Person::getName() const {
	if (_dialogue)
		return _dialogue->getName();
	else if (_npcType == NPC_EMPTY)
		return Creature::getName();
	else
		return "(unnamed person)";
}

void Person::goToStartLocation() {
	setCoords(_start);
}

void Person::setDialogue(Dialogue *d) {
	_dialogue = d;
	if (_tile.getTileType()->getName() == "beggar")
		_npcType = NPC_TALKER_BEGGAR;
	else if (_tile.getTileType()->getName() == "guard")
		_npcType = NPC_TALKER_GUARD;
	else
		_npcType = NPC_TALKER;
}

void Person::setNpcType(PersonNpcType t) {
	_npcType = t;
	assertMsg(!isVendor() || _dialogue == nullptr, "vendor has dialogue");
}

Common::List<Common::String> Person::getConversationText(Conversation *cnv, const char *inquiry) {
	Common::String text;

	/*
	 * a convsation with a vendor
	 */
	if (isVendor()) {
		static const Common::String ids[] = {
			"Weapons", "Armor", "Food", "Tavern", "Reagents", "Healer", "Inn", "Guild", "Stable"
		};
		Script *script = cnv->_script;

		/**
		 * We aren't currently running a script, load the appropriate one!
		 */
		if (cnv->_state == Conversation::INTRO) {
			// unload the previous script if it wasn't already unloaded
			if (script->getState() != Script::STATE_UNLOADED)
				script->unload();
			script->load("vendorScript.xml", ids[_npcType - NPC_VENDOR_WEAPONS], "vendor", g_context->_location->_map->getName());
			script->run("intro");
#ifdef IOS_ULTIMA4
			U4IOS::IOSConversationChoiceHelper choiceDialog;
#endif
			while (script->getState() != Script::STATE_DONE) {
				// Gather input for the script
				if (script->getState() == Script::STATE_INPUT) {
					switch (script->getInputType()) {
					case Script::INPUT_CHOICE: {
						const Common::String &choices = script->getChoices();
						// Get choice
#ifdef IOS_ULTIMA4
						choiceDialog.updateChoices(choices, script->getTarget(), npcType);
#endif
						char val = ReadChoiceController::get(choices);
						if (Common::isSpace(val) || val == '\033')
							script->unsetVar(script->getInputName());
						else {
							Common::String s_val;
							s_val = val;
							script->setVar(script->getInputName(), s_val);
						}
					}
					break;

					case Script::INPUT_KEYPRESS:
						ReadChoiceController::get(" \015\033");
						break;

					case Script::INPUT_NUMBER: {
#ifdef IOS_ULTIMA4
						U4IOS::IOSConversationHelper ipadNumberInput;
						ipadNumberInput.beginConversation(U4IOS::UIKeyboardTypeNumberPad, "Amount?");
#endif
						int val = ReadIntController::get(script->getInputMaxLen(), TEXT_AREA_X + g_context->_col, TEXT_AREA_Y + g_context->_line);
						script->setVar(script->getInputName(), val);
					}
					break;

					case Script::INPUT_STRING: {
#ifdef IOS_ULTIMA4
						U4IOS::IOSConversationHelper ipadNumberInput;
						ipadNumberInput.beginConversation(U4IOS::UIKeyboardTypeDefault);
#endif
						Common::String str = ReadStringController::get(script->getInputMaxLen(), TEXT_AREA_X + g_context->_col, TEXT_AREA_Y + g_context->_line);
						if (str.size()) {
							lowercase(str);
							script->setVar(script->getInputName(), str);
						} else script->unsetVar(script->getInputName());
					}
					break;

					case Script::INPUT_PLAYER: {
						ReadPlayerController getPlayerCtrl;
						eventHandler->pushController(&getPlayerCtrl);
						int player = getPlayerCtrl.waitFor();
						if (player != -1) {
							Common::String player_str = xu4_to_string(player + 1);
							script->setVar(script->getInputName(), player_str);
						} else script->unsetVar(script->getInputName());
					}
					break;

					default:
						break;
					} // } switch

					// Continue running the script!
					g_context->_line++;
					script->_continue();
				} // } if
			} // } while
		}

		// Unload the script
		script->unload();
		cnv->_state = Conversation::DONE;
	}

	/*
	 * a conversation with a non-vendor
	 */
	else {
		text = "\n\n\n";

		switch (cnv->_state) {
		case Conversation::INTRO:
			text = getIntro(cnv);
			break;

		case Conversation::TALK:
			text += getResponse(cnv, inquiry) + "\n";
			break;

		case Conversation::CONFIRMATION:
			assertMsg(_npcType == NPC_LORD_BRITISH, "invalid state: %d", cnv->_state);
			text += lordBritishGetQuestionResponse(cnv, inquiry);
			break;

		case Conversation::ASK:
		case Conversation::ASKYESNO:
			assertMsg(_npcType != NPC_HAWKWIND, "invalid state for hawkwind conversation");
			text += talkerGetQuestionResponse(cnv, inquiry) + "\n";
			break;

		case Conversation::GIVEBEGGAR:
			assertMsg(_npcType == NPC_TALKER_BEGGAR, "invalid npc type: %d", _npcType);
			text = beggarGetQuantityResponse(cnv, inquiry);
			break;

		case Conversation::FULLHEAL:
		case Conversation::ADVANCELEVELS:
			/* handled elsewhere */
			break;

		default:
			error("invalid state: %d", cnv->_state);
		}
	}

	return replySplit(text);
}

Common::String Person::getPrompt(Conversation *cnv) {
	if (isVendor())
		return "";

	Common::String prompt;
	if (cnv->_state == Conversation::ASK)
		prompt = getQuestion(cnv);
	else if (cnv->_state == Conversation::GIVEBEGGAR)
		prompt = "How much? ";
	else if (cnv->_state == Conversation::CONFIRMATION)
		prompt = "\n\nHe asks: Art thou well?";
	else if (cnv->_state != Conversation::ASKYESNO)
		prompt = _dialogue->getPrompt();

	return prompt;
}

const char *Person::getChoices(Conversation *cnv) {
	if (isVendor())
		return cnv->_script->getChoices().c_str();

	switch (cnv->_state) {
	case Conversation::CONFIRMATION:
	case Conversation::CONTINUEQUESTION:
		return "ny\015 \033";

	case Conversation::PLAYER:
		return "012345678\015 \033";

	default:
		error("invalid state: %d", cnv->_state);
	}

	return nullptr;
}

Common::String Person::getIntro(Conversation *cnv) {
	if (_npcType == NPC_EMPTY) {
		cnv->_state = Conversation::DONE;
		return Common::String("Funny, no\nresponse!\n");
	}

	// As far as I can tell, about 50% of the time they tell you their
	// name in the introduction
	Response *intro;
	if (xu4_random(2) == 0)
		intro = _dialogue->getIntro();
	else
		intro = _dialogue->getLongIntro();

	cnv->_state = Conversation::TALK;
	Common::String text = processResponse(cnv, intro);

	return text;
}

Common::String Person::processResponse(Conversation *cnv, Response *response) {
	Common::String text;
	const Std::vector<ResponsePart> &parts = response->getParts();
	for (const auto &i : parts) {

		// check for command triggers
		if (i.isCommand())
			runCommand(cnv, i);

		// otherwise, append response part to reply
		else
			text += i;
	}
	return text;
}

void Person::runCommand(Conversation *cnv, const ResponsePart &command) {
	if (command == g_responseParts->ASK) {
		cnv->_question = _dialogue->getQuestion();
		cnv->_state = Conversation::ASK;
	} else if (command == g_responseParts->END) {
		cnv->_state = Conversation::DONE;
	} else if (command == g_responseParts->ATTACK) {
		cnv->_state = Conversation::ATTACK;
	} else if (command == g_responseParts->BRAGGED) {
		g_context->_party->adjustKarma(KA_BRAGGED);
	} else if (command == g_responseParts->HUMBLE) {
		g_context->_party->adjustKarma(KA_HUMBLE);
	} else if (command == g_responseParts->ADVANCELEVELS) {
		cnv->_state = Conversation::ADVANCELEVELS;
	} else if (command == g_responseParts->HEALCONFIRM) {
		cnv->_state = Conversation::CONFIRMATION;
	} else if (command == g_responseParts->STARTMUSIC_LB) {
		g_music->lordBritish();
	} else if (command == g_responseParts->STARTMUSIC_HW) {
		g_music->hawkwind();
	} else if (command == g_responseParts->STOPMUSIC) {
		g_music->playMapMusic();
	} else if (command == g_responseParts->HAWKWIND) {
		g_context->_party->adjustKarma(KA_HAWKWIND);
	} else {
		error("unknown command trigger in dialogue response: %s\n", Common::String(command).c_str());
	}
}

Common::String Person::getResponse(Conversation *cnv, const char *inquiry) {
	Common::String reply;
	Virtue v;
	const ResponsePart &action = _dialogue->getAction();

	reply = "\n";

	/* Does the person take action during the conversation? */
	if (action == g_responseParts->END) {
		runCommand(cnv, action);
		return _dialogue->getPronoun() + " turns away!\n";
	} else if (action == g_responseParts->ATTACK) {
		runCommand(cnv, action);
		return Common::String("\n") + getName() + " says: On guard! Fool!";
	}

	if (_npcType == NPC_TALKER_BEGGAR && scumm_strnicmp(inquiry, "give", 4) == 0) {
		reply.clear();
		cnv->_state = Conversation::GIVEBEGGAR;
	}

	else if (scumm_strnicmp(inquiry, "join", 4) == 0 &&
	         g_context->_party->canPersonJoin(getName(), &v)) {
		CannotJoinError join = g_context->_party->join(getName());

		if (join == JOIN_SUCCEEDED) {
			reply += "I am honored to join thee!";
			g_context->_location->_map->removeObject(this);
			cnv->_state = Conversation::DONE;
		} else {
			reply += "Thou art not ";
			reply += (join == JOIN_NOT_VIRTUOUS) ? getVirtueAdjective(v) : "experienced";
			reply += " enough for me to join thee.";
		}
	}

	else if ((*_dialogue)[inquiry]) {
		Dialogue::Keyword *kw = (*_dialogue)[inquiry];

		reply = processResponse(cnv, kw->getResponse());
	}

	else if (settings._debug && scumm_strnicmp(inquiry, "dump", 4) == 0) {
		Std::vector<Common::String> words = split(inquiry, " \t");
		if (words.size() <= 1)
			reply = _dialogue->dump("");
		else
			reply = _dialogue->dump(words[1]);
	}

	else
		reply += processResponse(cnv, _dialogue->getDefaultAnswer());

	return reply;
}

Common::String Person::talkerGetQuestionResponse(Conversation *cnv, const char *answer) {
	bool valid = false;
	bool yes = false;
	char ans = tolower(answer[0]);

	if (ans == 'y' || ans == 'n') {
		valid = true;
		yes = ans == 'y';
	}

	if (!valid) {
		cnv->_state = Conversation::ASKYESNO;
		return "Yes or no!";
	}

	cnv->_state = Conversation::TALK;
	return "\n" + processResponse(cnv, cnv->_question->getResponse(yes));
}

Common::String Person::beggarGetQuantityResponse(Conversation *cnv, const char *response) {
	Common::String reply;

	cnv->_quant = (int) strtol(response, nullptr, 10);
	cnv->_state = Conversation::TALK;

	if (cnv->_quant > 0) {
		if (g_context->_party->donate(cnv->_quant)) {
			reply = "\n";
			reply += _dialogue->getPronoun();
			reply += " says: Oh Thank thee! I shall never forget thy kindness!\n";
		}

		else
			reply = "\n\nThou hast not that much gold!\n";
	} else
		reply = "\n";

	return reply;
}

Common::String Person::lordBritishGetQuestionResponse(Conversation *cnv, const char *answer) {
	Common::String reply;

	cnv->_state = Conversation::TALK;

	if (tolower(answer[0]) == 'y') {
		reply = "Y\n\nHe says: That is good.\n";
	}

	else if (tolower(answer[0]) == 'n') {
		reply = "N\n\nHe says: Let me heal thy wounds!\n";
		cnv->_state = Conversation::FULLHEAL;
	}

	else
		reply = "\n\nThat I cannot\nhelp thee with.\n";

	return reply;
}

Common::String Person::getQuestion(Conversation *cnv) {
	return "\n" + cnv->_question->getText() + "\n\nYou say: ";
}

/**
 * Returns the number of characters needed to get to
 * the next line of text (based on column width).
 */
int chars_to_next_line(const char *s, int columnmax) {
	int chars = -1;

	if (strlen(s) > 0) {
		int lastbreak = columnmax;
		chars = 0;
		for (const char *str = s; *str; str++) {
			if (*str == '\n')
				return (str - s);
			else if (*str == ' ')
				lastbreak = (str - s);
			else if (++chars >= columnmax)
				return lastbreak;
		}
	}

	return chars;
}

/**
 * Counts the number of lines (of the maximum width given by
 * columnmax) in the Common::String.
 */
int linecount(const Common::String &s, int columnmax) {
	int lines = 0;
	unsigned ch = 0;
	while (ch < s.size()) {
		ch += chars_to_next_line(s.c_str() + ch, columnmax);
		if (ch < s.size())
			ch++;
		lines++;
	}
	return lines;
}


/**
 * Returns the number of characters needed to produce a
 * valid screen of text (given a column width and row height)
 */
int chars_needed(const char *s, int columnmax, int linesdesired, int *real_lines) {
	int chars = 0,
	    totalChars = 0;

	Common::String new_str = s;
	const char *str = new_str.c_str();

	// try breaking text into paragraphs first
	Common::String text = s;
	Common::String paragraphs;
	uint pos;
	int lines = 0;
	while ((pos = text.find("\n\n")) < text.size()) {
		Common::String p = text.substr(0, pos);
		lines += linecount(p.c_str(), columnmax);
		if (lines <= linesdesired)
			paragraphs += p + "\n";
		else
			break;
		text = text.substr(pos + 1);
	}
	// Seems to be some sort of clang compilation bug in this code, that causes this addition
	// to not work correctly.
	int totalPossibleLines = lines + linecount(text.c_str(), columnmax);
	if (totalPossibleLines <= linesdesired)
		paragraphs += text;

	if (!paragraphs.empty()) {
		*real_lines = lines;
		return paragraphs.size();
	} else {
		// reset variables and try another way
		lines = 1;
	}
	// gather all the line breaks
	while ((chars = chars_to_next_line(str, columnmax)) >= 0) {
		if (++lines >= linesdesired)
			break;

		int num_to_move = chars;
		if (*(str + num_to_move) == '\n')
			num_to_move++;

		totalChars += num_to_move;
		str += num_to_move;
	}

	*real_lines = lines;
	return totalChars;
}

} // End of namespace Ultima4
} // End of namespace Ultima
