/*
** strifedialogue.cpp
** loads Strife style conversation dialogs
**
**---------------------------------------------------------------------------
** Copyright 2004-2008 Randy Heit
** Copyright 2006-2019 Christoph Oelckers
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
**    notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
**    notice, this list of conditions and the following disclaimer in the
**    documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
**    derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/

#include <assert.h>

#include "actor.h"
#include "p_conversation.h"
#include "filesystem.h"
#include "cmdlib.h"
#include "v_text.h"
#include "gi.h"
#include "a_keys.h"
#include "p_enemy.h"
#include "gstrings.h"
#include "p_setup.h"
#include "d_net.h"
#include "d_event.h"
#include "doomstat.h"
#include "c_console.h"
#include "g_levellocals.h"
#include "maploader.h"

// The conversations as they exist inside a SCRIPTxx lump.
struct Response
{
	int32_t GiveType;
	int32_t Item[3];
	int32_t Count[3];
	char Reply[32];
	char Yes[80];
	int32_t Link;
	uint32_t Log;
	char No[80];
};

struct Speech
{
	uint32_t SpeakerType;
	int32_t DropType;
	int32_t ItemCheck[3];
	int32_t Link;
	char Name[16];
	char Sound[8];
	char Backdrop[8];
	char Dialogue[320];
	Response Responses[5];
};

// The Teaser version of the game uses an older version of the structure
struct TeaserSpeech
{
	uint32_t SpeakerType;
	int32_t DropType;
	uint32_t VoiceNumber;
	char Name[16];
	char Dialogue[320];
	Response Responses[5];
};

//============================================================================
//
// P_LoadStrifeConversations
//
// Loads the SCRIPT00 and SCRIPTxx files for a corresponding map.
//
//============================================================================

void MapLoader::LoadStrifeConversations (MapData *map, const char *mapname)
{
	if (map->Size(ML_CONVERSATION) > 0)
	{
		LoadScriptFile (nullptr, map->lumpnum, map->Reader(ML_CONVERSATION), map->Size(ML_CONVERSATION), false, 0);
	}
	else
	{
		// additive dialogues via MAPINFO
		bool addedDialogues = false;
		for (const FString &addd : gameinfo.AddDialogues)
		{
			if (!LoadScriptFile(addd, true, 0))
			{
				continue;
			}
			else
			{
				addedDialogues = true;
			}
		}

		if (strnicmp (mapname, "MAP", 3) == 0)
		{
			char scriptname_b[9] = { 'S','C','R','I','P','T',mapname[3],mapname[4],0 };
			char scriptname_t[9] = { 'D','I','A','L','O','G',mapname[3],mapname[4],0 };

			if (   LoadScriptFile(scriptname_t, false, 2)
				|| LoadScriptFile(scriptname_b, false, 1))
			{
				return;
			}
		}

		if (gameinfo.Dialogue.IsNotEmpty())
		{
			if (LoadScriptFile(gameinfo.Dialogue, false, 0))
			{
				if (addedDialogues)
				{
					Printf(TEXTCOLOR_RED "Warning! Dialogue was mixed with AddDialogues! Previous AddDialogues have been overwritten\n");
				}
				return;
			}
		}

		LoadScriptFile("SCRIPT00", false, 1);
	}
}

//============================================================================
//
// LoadScriptFile
//
// Loads a SCRIPTxx file and converts it into a more useful internal format.
//
//============================================================================

bool MapLoader::LoadScriptFile (const char *name, bool include, int type)
{
	int lumpnum = fileSystem.CheckNumForName (name);
	const bool found = lumpnum >= 0
		|| (lumpnum = fileSystem.CheckNumForFullName (name)) >= 0;

	if (!found)
	{
		if (type == 0)
		{
			Printf(TEXTCOLOR_RED "Could not find dialog file %s\n", name);
		}

		return false;
	}
	FileReader lump = fileSystem.ReopenFileReader (lumpnum);

	auto fn = fileSystem.GetFileContainer(lumpnum);
	auto wadname = fileSystem.GetResourceFileName(fn);
	if (stricmp(wadname, "STRIFE0.WAD") && stricmp(wadname, "STRIFE1.WAD") && stricmp(wadname, "SVE.WAD")) name = nullptr;	// Only localize IWAD content.

	bool res = LoadScriptFile(name, lumpnum, lump, fileSystem.FileLength(lumpnum), include, type);
	return res;
}

bool MapLoader::LoadScriptFile(const char *name, int lumpnum, FileReader &lump, int numnodes, bool include, int type)
{
	int i;
	uint32_t prevSpeakerType;
	FStrifeDialogueNode *node;
	char buffer[4];

	lump.Read(buffer, 4);
	lump.Seek(-4, FileReader::SeekCur);

	// The binary format is so primitive that this check is enough to detect it.
	bool isbinary = (buffer[0] == 0 || buffer[1] == 0 || buffer[2] == 0 || buffer[3] == 0);

	if ((type == 1 && !isbinary) || (type == 2 && isbinary))
	{
		DPrintf(DMSG_ERROR, "Incorrect data format for conversation script in %s.\n", fileSystem.GetFileFullName(lumpnum));
		return false;
	}

	if (!isbinary)
	{
		ParseUSDF(lumpnum, lump, numnodes);
	}
	else 
	{
		if (!include)
		{
			LoadScriptFile("SCRIPT00", true, 1);
		}
		if (!(gameinfo.flags & GI_SHAREWARE))
		{
			// Strife scripts are always a multiple of 1516 bytes because each entry
			// is exactly 1516 bytes long.
			if (numnodes % 1516 != 0)
			{
				DPrintf(DMSG_ERROR, "Incorrect data format for conversation script in %s.\n", fileSystem.GetFileFullName(lumpnum));
				return false;
			}
			numnodes /= 1516;
		}
		else
		{
			// And the teaser version has 1488-byte entries.
			if (numnodes % 1488 != 0)
			{
				DPrintf(DMSG_ERROR, "Incorrect data format for conversation script in %s.\n", fileSystem.GetFileFullName(lumpnum));
				return false;
			}
			numnodes /= 1488;
		}

		prevSpeakerType = 0;

		for (i = 0; i < numnodes; ++i)
		{
			if (!(gameinfo.flags & GI_SHAREWARE))
			{
				node = ReadRetailNode (name, lump, prevSpeakerType);
			}
			else
			{
				node = ReadTeaserNode (name, lump, prevSpeakerType);
			}
			node->ThisNodeNum = Level->StrifeDialogues.Push(node);
		}
	}
	return true;
}

//============================================================================
//
// ReadRetailNode
//
// Converts a single dialogue node from the Retail version of Strife.
//
//============================================================================

static FString TokenFromString(const char *speech)
{
	FString token = speech;
	token.ToUpper();
	token.ReplaceChars(".,-+!?'", ' ');
	token.Substitute(" ", "");
	token.Truncate(5);
	return token;
}

FStrifeDialogueNode *MapLoader::ReadRetailNode (const char *name, FileReader &lump, uint32_t &prevSpeakerType)
{
	FStrifeDialogueNode *node;
	Speech speech;
	char fullsound[16];
	PClassActor *type;
	int j;

	node = new FStrifeDialogueNode;

	auto pos = lump.Tell();
	lump.Read (&speech, sizeof(speech));

	// Byte swap all the ints in the original data
	speech.SpeakerType = LittleLong(speech.SpeakerType);
	speech.DropType = LittleLong(speech.DropType);
	speech.Link = LittleLong(speech.Link);

	// Assign the first instance of a conversation as the default for its
	// actor, so newly spawned actors will use this conversation by default.
	type = GetStrifeType (speech.SpeakerType);
	node->SpeakerType = type;

	if ((signed)(speech.SpeakerType) >= 0 && prevSpeakerType != speech.SpeakerType)
	{
		if (type != NULL)
		{
			Level->ClassRoots[type->TypeName] = Level->StrifeDialogues.Size();
		}
		Level->DialogueRoots[speech.SpeakerType] = Level->StrifeDialogues.Size();
		prevSpeakerType = speech.SpeakerType;
	}

	// Convert the rest of the data to our own internal format.

	if (name && strncmp(speech.Dialogue, "RANDOM_", 7))
	{
		FStringf label("$TXT_DLG_%s_d%d_%s", name, int(pos), TokenFromString(speech.Dialogue).GetChars());
		node->Dialogue = GStrings.exists(label.GetChars()+1)? label : FString(speech.Dialogue);
	}
	else
	{
		node->Dialogue = speech.Dialogue;
	}

	// The speaker's portrait, if any.
	speech.Dialogue[0] = 0; 	//speech.Backdrop[8] = 0;
	node->Backdrop = speech.Backdrop;

	// The speaker's voice for this node, if any.
	speech.Backdrop[0] = 0; 	//speech.Sound[8] = 0;
	mysnprintf (fullsound, countof(fullsound), "svox/%s", speech.Sound);
	node->SpeakerVoice = fullsound;

	// The speaker's name, if any.
	speech.Sound[0] = 0; 		//speech.Name[16] = 0;
	if (name && speech.Name[0])
	{
		FString label = speech.Name;
		label.ReplaceChars(' ', '_');
		label.ReplaceChars('\'', '_');
		node->SpeakerName.Format("$TXT_SPEAKER_%s", label.GetChars());
		if (!GStrings.exists(node->SpeakerName.GetChars() + 1)) node->SpeakerName = speech.Name;

	}
	else
	{
		node->SpeakerName = speech.Name;
	}

	// The item the speaker should drop when killed.
	node->DropType = GetStrifeType(speech.DropType);

	// Items you need to have to make the speaker use a different node.
	node->ItemCheck.Resize(3);
	for (j = 0; j < 3; ++j)
	{
		auto inv = GetStrifeType(speech.ItemCheck[j]);
		if (!inv->IsDescendantOf(NAME_Inventory)) inv = nullptr;
		node->ItemCheck[j].Item = inv;
		node->ItemCheck[j].Amount = -1;
	}
	node->ItemCheckNode = speech.Link;
	node->Children = NULL;

	ParseReplies (name, int(pos), &node->Children, &speech.Responses[0]);

	return node;
}

//============================================================================
//
// ReadTeaserNode
//
// Converts a single dialogue node from the Teaser version of Strife.
//
//============================================================================

FStrifeDialogueNode *MapLoader::ReadTeaserNode (const char *name, FileReader &lump, uint32_t &prevSpeakerType)
{
	FStrifeDialogueNode *node;
	TeaserSpeech speech;
	char fullsound[16];
	PClassActor *type;
	int j;

	node = new FStrifeDialogueNode;

	auto pos = lump.Tell() * 1516 / 1488;
	lump.Read (&speech, sizeof(speech));

	// Byte swap all the ints in the original data
	speech.SpeakerType = LittleLong(speech.SpeakerType);
	speech.DropType = LittleLong(speech.DropType);

	// Assign the first instance of a conversation as the default for its
	// actor, so newly spawned actors will use this conversation by default.
	type = GetStrifeType(speech.SpeakerType);
	node->SpeakerType = type;

	if ((signed)speech.SpeakerType >= 0 && prevSpeakerType != speech.SpeakerType)
	{
		if (type != NULL)
		{
			Level->ClassRoots[type->TypeName] = Level->StrifeDialogues.Size();
		}
		Level->DialogueRoots[speech.SpeakerType] = Level->StrifeDialogues.Size();
		prevSpeakerType = speech.SpeakerType;
	}

	// Convert the rest of the data to our own internal format.
	if (name && strncmp(speech.Dialogue, "RANDOM_", 7))
	{
		FStringf label("$TXT_DLG_%s_d%d_%s", name, pos, TokenFromString(speech.Dialogue).GetChars());
		node->Dialogue = GStrings.exists(label.GetChars() + 1)? label : FString(speech.Dialogue);
	}
	else
	{
		node->Dialogue = speech.Dialogue;
	}

	// The Teaser version doesn't have portraits.
	node->Backdrop = "";

	// The speaker's voice for this node, if any.
	if (speech.VoiceNumber != 0)
	{
		mysnprintf (fullsound, countof(fullsound), "svox/voc%u", speech.VoiceNumber);
		node->SpeakerVoice = fullsound;
	}
	else
	{
		node->SpeakerVoice = 0;
	}

	// The speaker's name, if any.
	speech.Dialogue[0] = 0; 	//speech.Name[16] = 0;
	if ((name && speech.Name[0]))
	{
		FString label = speech.Name;
		label.ReplaceChars(' ', '_');
		label.ReplaceChars('\'', '_');
		node->SpeakerName.Format("$TXT_SPEAKER_%s", label.GetChars());
		if (!GStrings.exists(node->SpeakerName.GetChars() + 1)) node->SpeakerName = speech.Name;
	}
	else
	{
		node->SpeakerName = speech.Name;
	}

	// The item the speaker should drop when killed.
	node->DropType = GetStrifeType (speech.DropType);

	// Items you need to have to make the speaker use a different node.
	node->ItemCheck.Resize(3);
	for (j = 0; j < 3; ++j)
	{
		node->ItemCheck[j].Item = NULL;
		node->ItemCheck[j].Amount = -1;
	}
	node->ItemCheckNode = 0;
	node->Children = NULL;

	ParseReplies (name, int(pos), &node->Children, &speech.Responses[0]);

	return node;
}

//============================================================================
//
// ParseReplies
//
// Convert PC responses. Rather than being stored inside the main node, they
// hang off it as a singly-linked list, so no space is wasted on replies that
// don't even matter.
//
//============================================================================

void MapLoader::ParseReplies (const char *name, int pos, FStrifeDialogueReply **replyptr, Response *responses)
{
	FStrifeDialogueReply *reply;
	int j, k;

	// Byte swap first.
	for (j = 0; j < 5; ++j)
	{
		responses[j].GiveType = LittleLong(responses[j].GiveType);
		responses[j].Link = LittleLong(responses[j].Link);
		responses[j].Log = LittleLong(responses[j].Log);
		for (k = 0; k < 3; ++k)
		{
			responses[j].Item[k] = LittleLong(responses[j].Item[k]);
			responses[j].Count[k] = LittleLong(responses[j].Count[k]);
		}
	}

	for (j = 0; j < 5; ++j)
	{
		Response *rsp = &responses[j];

		// If the reply has no text and goes nowhere, then it doesn't
		// need to be remembered.
		if (rsp->Reply[0] == 0 && rsp->Link == 0)
		{
			continue;
		}
		reply = new FStrifeDialogueReply;

		// The next node to use when this reply is chosen.
		reply->NextNode = rsp->Link;
		if (reply->NextNode < 0)
		{
			reply->NextNode *= -1;
			reply->CloseDialog = false;
		}

		// The message to record in the log for this reply.
		reply->LogNumber = rsp->Log;
		reply->LogString = "";

		// The item to receive when this reply is used.
		reply->GiveType = GetStrifeType (rsp->GiveType);
		reply->ActionSpecial = 0;

		// Do you need anything special for this reply to succeed?
		reply->ItemCheck.Resize(3);
		for (k = 0; k < 3; ++k)
		{
			auto inv = GetStrifeType(rsp->Item[k]);
			if (!inv->IsDescendantOf(NAME_Inventory)) inv = nullptr;
			reply->ItemCheck[k].Item = inv;
			reply->ItemCheck[k].Amount = rsp->Count[k];
		}
		reply->PrintAmount = reply->ItemCheck[0].Amount;
		reply->ItemCheckRequire.Clear();
		reply->ItemCheckExclude.Clear();

		if (name)
		{
			FStringf label("$TXT_RPLY%d_%s_d%d_%s", j, name, pos, TokenFromString(rsp->Reply).GetChars());
			reply->Reply = GStrings.exists(label.GetChars() + 1)? label : FString(rsp->Reply);
		}
		else
		{
			reply->Reply = rsp->Reply;
		}


		// If the first item check has a positive amount required, then
		// add that to the reply string. Otherwise, use the reply as-is.
		reply->NeedsGold = (rsp->Count[0] > 0);

		// QuickYes messages are shown when you meet the item checks.
		// QuickNo messages are shown when you don't.
		// Note that empty nodes contain a '_' in retail Strife, a '.' in the teasers and an empty string in SVE.
		if (((rsp->Yes[0] == '_' || rsp->Yes[0] == '.') && rsp->Yes[1] == 0) || rsp->Yes[0] == 0)
		{
			reply->QuickYes = "";
		}
		else
		{
			if (name)
			{
				FStringf label("$TXT_RYES%d_%s_d%d_%s", j, name, pos, TokenFromString(rsp->Yes).GetChars());
				reply->QuickYes = GStrings.exists(label.GetChars() + 1)? label : FString(rsp->Yes);
			}
			else
			{
				reply->QuickYes = rsp->Yes;
			}
		}
		if (reply->ItemCheck[0].Item != 0)
		{
			FStringf label("$TXT_RNO%d_%s_d%d_%s", j, name, pos, TokenFromString(rsp->No).GetChars());
			reply->QuickNo = GStrings.exists(label.GetChars() + 1)? label : FString(rsp->No);
		}
		else
		{
			reply->QuickNo = "";
		}
		reply->Next = *replyptr;
		*replyptr = reply;
		replyptr = &reply->Next;
	}
}

