/*
** intermission.cpp
** Framework for intermissions (text screens, slideshows, etc)
**
**---------------------------------------------------------------------------
** Copyright 2010 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 "doomtype.h"
#include "g_game.h"
#include "d_event.h"
#include "filesystem.h"
#include "gi.h"
#include "v_video.h"
#include "d_main.h"
#include "gstrings.h"
#include "intermission/intermission.h"
#include "actor.h"
#include "d_player.h"
#include "r_state.h"
#include "c_bind.h"
#include "p_conversation.h"
#include "menu.h"
#include "d_net.h"
#include "g_levellocals.h"
#include "utf8.h"

#include "s_music.h"
#include "texturemanager.h"
#include "v_draw.h"
#include "doommenu.h"
#include "sbar.h"
#include "screenjob.h"
#include "vm.h"
#include "i_time.h"

FIntermissionDescriptorList IntermissionDescriptors;

IMPLEMENT_CLASS(DIntermissionScreen, false, false)
IMPLEMENT_CLASS(DIntermissionScreenFader, false, false)
IMPLEMENT_CLASS(DIntermissionScreenText, false, false)
IMPLEMENT_CLASS(DIntermissionScreenCast, false, false)
IMPLEMENT_CLASS(DIntermissionScreenScroller, false, false)
IMPLEMENT_CLASS(DIntermissionScreenCutscene, false, true)
IMPLEMENT_CLASS(DIntermissionController, false, true)

IMPLEMENT_POINTERS_START(DIntermissionController)
	IMPLEMENT_POINTER(mScreen)
IMPLEMENT_POINTERS_END

IMPLEMENT_POINTERS_START(DIntermissionScreenCutscene)
	IMPLEMENT_POINTER(mScreenJobRunner)
IMPLEMENT_POINTERS_END

extern int		NoWipe;

CVAR(Bool, nointerscrollabort, false, CVAR_ARCHIVE | CVAR_GLOBALCONFIG);

CVAR(Bool, inter_classic_scaling, true, CVAR_ARCHIVE);

DEFINE_ACTION_FUNCTION(_Screen, GetTextScreenSize)
{
	auto scale = active_con_scaletext(twod, generic_ui);
	int hudwidth = twod->GetWidth() / scale;
	int hudheight = twod->GetHeight() / scale;
	ACTION_RETURN_VEC2(DVector2(hudwidth, hudheight));
}

//==========================================================================
//
// This also gets used by the title loop.
//
//==========================================================================

void DrawFullscreenSubtitle(FFont* font, const char *text)
{
	if (!text || !*text || !inter_subtitles) return;
	text = GStrings.localize(text);

	// This uses the same scaling as regular HUD messages
	auto scale = active_con_scaletext(twod, generic_ui);
	int hudwidth = twod->GetWidth() / scale;
	int hudheight = twod->GetHeight() / scale;

	int linelen = hudwidth < 640 ? Scale(hudwidth, 9, 10) - 40 : 560;
	auto lines = V_BreakLines(font, linelen, text);
	int height = 20;

	for (unsigned i = 0; i < lines.Size(); i++) height += font->GetHeight();

	int x, y, w;

	if (linelen < 560)
	{
		x = hudwidth / 20;
		y = hudheight * 9 / 10 - height;
		w = hudwidth - 2 * x;
	}
	else
	{
		x = (hudwidth >> 1) - 300;
		y = hudheight * 9 / 10 - height;
		if (y < 0) y = 0;
		w = 600;
	}
	Dim(twod, 0, 0.5f, Scale(x, twod->GetWidth(), hudwidth), Scale(y, twod->GetHeight(), hudheight),
		Scale(w, twod->GetWidth(), hudwidth), Scale(height, twod->GetHeight(), hudheight));
	x += 20;
	y += 10;
	for (const FBrokenLines &line : lines)
	{
		DrawText(twod, font, CR_UNTRANSLATED, x, y, line.Text.GetChars(),
			DTA_KeepRatio, true,
			DTA_VirtualWidth, hudwidth, DTA_VirtualHeight, hudheight, TAG_DONE);
		y += font->GetHeight();
	}
}

//==========================================================================
//
//
//
//==========================================================================

void DIntermissionScreen::Init(FIntermissionAction *desc, bool first)
{
	if (!first && desc->mMusic.IsNotEmpty())
	{
		S_ChangeMusic (desc->mMusic.GetChars(), desc->mMusicOrder, desc->mMusicLooping);
	}
	mDuration = desc->mDuration;

	const char *texname = desc->mBackground.GetChars();
	if (*texname == '@')
	{
		char *pp;
		unsigned int v = strtoul(texname+1, &pp, 10) - 1;
		if (*pp == 0 && v < gameinfo.finalePages.Size())
		{
			texname = gameinfo.finalePages[v].GetChars();
		}
		else if (gameinfo.finalePages.Size() > 0)
		{
			texname = gameinfo.finalePages[0].GetChars();
		}
		else
		{
			texname = gameinfo.TitlePage.GetChars();
		}
	}
	else if (*texname == '$')
	{
		texname = GStrings.GetString(texname+1);
	}
	if (texname[0] != 0)
	{
		mBackground = TexMan.CheckForTexture(texname, ETextureType::MiscPatch);
		mFlatfill = desc->mFlatfill;
	}
	S_Sound (CHAN_VOICE, CHANF_UI, desc->mSound, 1.0f, ATTN_NONE);
	mOverlays.Resize(desc->mOverlays.Size());
	for (unsigned i=0; i < mOverlays.Size(); i++)
	{
		mOverlays[i].x = desc->mOverlays[i].x;
		mOverlays[i].y = desc->mOverlays[i].y;
		mOverlays[i].mCondition = desc->mOverlays[i].mCondition;
		mOverlays[i].mPic = TexMan.CheckForTexture(desc->mOverlays[i].mName.GetChars(), ETextureType::MiscPatch);
	}
	mTicker = 0;
	mSubtitle = desc->mSubtitle;
	mFirst = first;
	// If this is the first element of an intermission we must delay starting the music until Start() is called.
	mMusic = desc->mMusic;
	mMusicLooping = desc->mMusicLooping;
	mMusicOrder = desc->mMusicOrder;
}

void DIntermissionScreen::Start()
{
	if (mFirst)
	{
		if (mMusic.IsEmpty())
		{
			S_ChangeMusic(gameinfo.finaleMusic.GetChars(), gameinfo.finaleOrder, mMusicLooping);
		}
		else
		{
			S_ChangeMusic(mMusic.GetChars(), mMusicOrder, mMusicLooping);
		}
	}
}

int DIntermissionScreen::Responder (FInputEvent *ev)
{
	if (ev->Type == EV_KeyDown)
	{
		return -1;
	}
	return 0;
}

int DIntermissionScreen::Ticker ()
{
	if (++mTicker >= mDuration && mDuration > 0) return -1;
	return 0;
}

bool DIntermissionScreen::CheckOverlay(int i)
{
	if (mOverlays[i].mCondition == NAME_Multiplayer && !multiplayer) return false;
	else if (mOverlays[i].mCondition != NAME_None)
	{
		if (multiplayer || players[0].mo == NULL) return false;
		const PClass *cls = PClass::FindClass(mOverlays[i].mCondition);
		if (cls == NULL) return false;
		if (!players[0].mo->IsKindOf(cls)) return false;
	}
	return true;
}

void DIntermissionScreen::Drawer ()
{
	if (mBackground.isValid())
	{
		if (!mFlatfill)
		{
			DrawTexture(twod, mBackground, false, 0, 0, DTA_Fullscreen, true, TAG_DONE);
		}
		else
		{
			twod->AddFlatFill(0,0, twod->GetWidth(), twod->GetHeight(), TexMan.GetGameTexture(mBackground), (inter_classic_scaling ? -1 : 0));
		}
	}
	else
	{
		ClearRect(twod, 0, 0, twod->GetWidth(), twod->GetHeight(), 0, 0);
	}
	for (unsigned i=0; i < mOverlays.Size(); i++)
	{
		if (CheckOverlay(i))
			DrawTexture(twod, mOverlays[i].mPic, false, mOverlays[i].x, mOverlays[i].y, DTA_320x200, true, TAG_DONE);
	}
	FFont* font = generic_ui ? NewSmallFont : SmallFont;
	DrawFullscreenSubtitle(font, mSubtitle.GetChars());
}

void DIntermissionScreen::OnDestroy()
{
	S_StopSound(CHAN_VOICE);
	Super::OnDestroy();
}

//==========================================================================
//
//
//
//==========================================================================

void DIntermissionScreenCutscene::Init(FIntermissionAction *desc, bool first)
{
	Super::Init(desc, first);
	mScreenJobRunner = CreateRunner(false);
	if(first) NoWipe++;
	CreateCutscene(&static_cast<FIntermissionActionCutscene*>(desc)->scn, mScreenJobRunner, nullptr);
}

int DIntermissionScreenCutscene::Responder (FInputEvent *ev)
{
	ScaleOverrider ovr(twod);
	IFVIRTUALPTRNAME(mScreenJobRunner, NAME_ScreenJobRunner, OnEvent)
	{
		FInputEvent evt = *ev;
		int result = 0;
		VMValue parm[] = { mScreenJobRunner, &evt };
		VMReturn ret(&result);
		VMCall(func, parm, 2, &ret, 1);
		return result ? -1 : 0;
	}
	return Super::Responder(ev);
}

int DIntermissionScreenCutscene::Ticker()
{
	++mTicker;
	ScaleOverrider ovr(twod);
	IFVIRTUALPTRNAME(mScreenJobRunner, NAME_ScreenJobRunner, OnTick)
	{
		int result = 0;
		VMValue parm[] = { mScreenJobRunner };
		VMReturn ret(&result);
		VMCall(func, parm, 1, &ret, 1);
		return result ? -1 : 0;
	}
	return -1;
}

void DIntermissionScreenCutscene::Drawer ()
{
	ScaleOverrider ovr(twod);
	IFVIRTUALPTRNAME(mScreenJobRunner, NAME_ScreenJobRunner, RunFrame)
	{
		int res = 0;
		VMValue parm[] = { mScreenJobRunner, I_GetTimeFrac() };
		VMReturn ret[] = { &res };
		VMCall(func, parm, 2, ret, 1);
	}
}

void DIntermissionScreenCutscene::OnDestroy()
{
	mScreenJobRunner->Destroy();
	Super::OnDestroy();
}

//==========================================================================
//
//
//
//==========================================================================

void DIntermissionScreenFader::Init(FIntermissionAction *desc, bool first)
{
	Super::Init(desc, first);
	mType = static_cast<FIntermissionActionFader*>(desc)->mFadeType;
}

//===========================================================================
//
// FadePic
//
//===========================================================================

int DIntermissionScreenFader::Responder (FInputEvent *ev)
{
	if (ev->Type == EV_KeyDown)
	{
		return -1;
	}
	return Super::Responder(ev);
}

int DIntermissionScreenFader::Ticker ()
{
	if (mFlatfill || !mBackground.isValid()) return -1;
	return Super::Ticker();
}

void DIntermissionScreenFader::Drawer ()
{
	if (!mFlatfill && mBackground.isValid())
	{
		double factor = clamp(double(mTicker) / mDuration, 0., 1.);
		if (mType == FADE_In) factor = 1.0 - factor;
		int color = MAKEARGB(int(factor*255), 0,0,0);

		DrawTexture(twod, mBackground, false, 0, 0, DTA_Fullscreen, true, DTA_ColorOverlay, color, TAG_DONE);
		for (unsigned i=0; i < mOverlays.Size(); i++)
		{
			if (CheckOverlay(i))
				DrawTexture(twod, mOverlays[i].mPic, false, mOverlays[i].x, mOverlays[i].y, DTA_320x200, true, DTA_ColorOverlay, color, TAG_DONE);
		}
	}
}

//==========================================================================
//
//
//
//==========================================================================

void DIntermissionScreenText::Init(FIntermissionAction *desc, bool first)
{
	Super::Init(desc, first);
	mText = static_cast<FIntermissionActionTextscreen*>(desc)->mText;
	if (mText[0] == '$') mText = GStrings.GetString(&mText[1]);

	auto lines = mText.Split("\n");
	mText = "";
	for (auto& line : lines)
	{
		line.StripRight();
		mText << line << "\n";
	}

	mTextSpeed = static_cast<FIntermissionActionTextscreen*>(desc)->mTextSpeed;
	mTextX = static_cast<FIntermissionActionTextscreen*>(desc)->mTextX;
	usesDefault = mTextX < 0;
	if (mTextX < 0) mTextX =gameinfo.TextScreenX;
	mTextY = static_cast<FIntermissionActionTextscreen*>(desc)->mTextY;
	if (mTextY < 0) mTextY =gameinfo.TextScreenY;

	if (!generic_ui)
	{
		// Todo: Split too long texts

		// If the text is too wide, center it so that it works better on widescreen displays.
		// On 4:3 it'd still be cut off, though.
		int width = SmallFont->StringWidth(mText);
		if (usesDefault && mTextX + width > 320 - mTextX)
		{
			mTextX = (320 - width) / 2;
		}
	}
	else
	{
		// Todo: Split too long texts

		mTextX *= 2;
		mTextY *= 2;
		int width = NewSmallFont->StringWidth(mText);
		if (usesDefault && mTextX + width > 640 - mTextX)
		{
			mTextX = (640 - width) / 2;
		}
	}


	mTextLen = (int)mText.CharacterCount();
	mTextDelay = static_cast<FIntermissionActionTextscreen*>(desc)->mTextDelay;
	mTextColor = static_cast<FIntermissionActionTextscreen*>(desc)->mTextColor;
	// For text screens, the duration only counts when the text is complete.
	if (mDuration > 0) mDuration += mTextDelay + mTextSpeed * mTextLen;
}

int DIntermissionScreenText::Responder (FInputEvent *ev)
{
	if (ev->Type == EV_KeyDown)
	{
		if (mTicker < mTextDelay + (mTextLen * mTextSpeed))
		{
			mTicker = mTextDelay + (mTextLen * mTextSpeed);
			return 1;
		}
	}
	return Super::Responder(ev);
}

void DIntermissionScreenText::Drawer ()
{
	Super::Drawer();
	if (mTicker >= mTextDelay)
	{
		int w;
		size_t count;
		int c;
		const uint8_t *ch = (const uint8_t*)mText.GetChars();

		// Count number of rows in this text. Since it does not word-wrap, we just count
		// line feed characters.
		int numrows;
		auto font = generic_ui ? NewSmallFont : SmallFont;
		auto fontscale = max(generic_ui ? min(twod->GetWidth() / 640, twod->GetHeight() / 400) : min(twod->GetWidth() / 400, twod->GetHeight() / 250), 1);
		int cleanwidth = twod->GetWidth() / fontscale;
		int cleanheight = twod->GetHeight() / fontscale;
		int refwidth = generic_ui ? 640 : 320;
		int refheight = generic_ui ? 400 : 200;
		const int kerning = font->GetDefaultKerning();

		for (numrows = 1, c = 0; ch[c] != '\0'; ++c)
		{
			numrows += (ch[c] == '\n');
		}

		int rowheight = font->GetHeight() * fontscale;
		int rowpadding = (generic_ui? 2 : ((gameinfo.gametype & (GAME_DoomStrifeChex) ? 3 : -1))) * fontscale;

		int cx = (mTextX - refwidth/2) * fontscale + twod->GetWidth() / 2;
		int cy = (mTextY - refheight/2) * fontscale + twod->GetHeight() / 2;
		cx = max<int>(0, cx);
		int startx = cx;

		if (usesDefault)
		{
			int allheight;
			while ((allheight = numrows * (rowheight + rowpadding)), allheight > twod->GetHeight() && rowpadding > 0)
			{
				rowpadding--;
			}
			allheight = numrows * (rowheight + rowpadding);
			if (twod->GetHeight() - cy - allheight < cy)
			{
				cy = (twod->GetHeight() - allheight) / 2;
				if (cy < 0) cy = 0;
			}
		}
		else
		{
			// Does this text fall off the end of the screen? If so, try to eliminate some margins first.
			while (rowpadding > 0 && cy + numrows * (rowheight + rowpadding) - rowpadding > twod->GetHeight())
			{
				rowpadding--;
			}
			// If it's still off the bottom, you are screwed if the origin is fixed.
		}
		rowheight += rowpadding;

		// draw some of the text onto the screen
		count = (mTicker - mTextDelay) / mTextSpeed;

		for ( ; count > 0 ; count-- )
		{
			c = GetCharFromString(ch);
			if (!c)
				break;
			if (c == '\n')
			{
				cx = startx;
				cy += rowheight;
				continue;
			}

			w = font->GetCharWidth(c);
			w += kerning;
			w *= fontscale;
			if (cx + w > twod->GetWidth())
				continue;

			DrawChar(twod, font, mTextColor, cx/fontscale, cy/fontscale, c, DTA_KeepRatio, true, DTA_VirtualWidth, cleanwidth, DTA_VirtualHeight, cleanheight, TAG_DONE);
			cx += w;
		}
	}
}

//==========================================================================
//
//
//
//==========================================================================

void DIntermissionScreenCast::Init(FIntermissionAction *desc, bool first)
{
	Super::Init(desc, first);
	mName = static_cast<FIntermissionActionCast*>(desc)->mName.GetChars();
	mClass = PClass::FindActor(static_cast<FIntermissionActionCast*>(desc)->mCastClass);
	if (mClass != NULL) mDefaults = GetDefaultByType(mClass);
	else
	{
		mDefaults = NULL;
		caststate = NULL;
		return;
	}

	mCastSounds.Resize(static_cast<FIntermissionActionCast*>(desc)->mCastSounds.Size());
	for (unsigned i=0; i < mCastSounds.Size(); i++)
	{
		mCastSounds[i].mSequence = static_cast<FIntermissionActionCast*>(desc)->mCastSounds[i].mSequence;
		mCastSounds[i].mIndex = static_cast<FIntermissionActionCast*>(desc)->mCastSounds[i].mIndex;
		mCastSounds[i].mSound = S_FindSound(static_cast<FIntermissionActionCast*>(desc)->mCastSounds[i].mSound);
	}
	caststate = mDefaults->SeeState;
	if (mClass->IsDescendantOf(NAME_PlayerPawn))
	{
		advplayerstate = mDefaults->MissileState;
		casttranslation = TRANSLATION(TRANSLATION_Players, consoleplayer);
	}
	else
	{
		advplayerstate = NULL;
		casttranslation = NO_TRANSLATION;
		if (mDefaults->Translation != NO_TRANSLATION)
		{
			casttranslation = mDefaults->Translation;
		}
	}
	castdeath = false;
	castframes = 0;
	castonmelee = 0;
	castattacking = false;
	if (mDefaults->SeeSound.isvalid())
	{
		S_Sound (CHAN_VOICE, CHANF_UI, mDefaults->SeeSound, 1, ATTN_NONE);
	}
}

int DIntermissionScreenCast::Responder (FInputEvent *ev)
{
	if (ev->Type != EV_KeyDown) return 0;

	if (castdeath)
		return 1;					// already in dying frames

	castdeath = true;

	if (mClass != NULL)
	{
		FName label[] = {NAME_Death, NAME_Cast};
		caststate = mClass->FindState(2, label);
		if (caststate == NULL) return -1;

		casttics = caststate->GetTics();
		castframes = 0;
		castattacking = false;

		if (mClass->IsDescendantOf(NAME_PlayerPawn))
		{
			auto snd = S_FindSkinnedSound(players[consoleplayer].mo, S_FindSound("*death"));
			if (snd != NO_SOUND) S_Sound (CHAN_VOICE, CHANF_UI, snd, 1, ATTN_NONE);
		}
		else if (mDefaults->DeathSound.isvalid())
		{
			S_Sound (CHAN_VOICE, CHANF_UI, mDefaults->DeathSound, 1, ATTN_NONE);
		}
	}
	return true;
}

void DIntermissionScreenCast::PlayAttackSound()
{
	// sound hacks....
	if (caststate != NULL && castattacking)
	{
		for (unsigned i = 0; i < mCastSounds.Size(); i++)
		{
			if ((!!mCastSounds[i].mSequence) == (basestate != mDefaults->MissileState) &&
				(caststate == basestate + mCastSounds[i].mIndex))
			{
				S_StopAllChannels ();
				S_Sound (CHAN_WEAPON, CHANF_UI, mCastSounds[i].mSound, 1, ATTN_NONE);
				return;
			}
		}
	}

}

int DIntermissionScreenCast::Ticker ()
{
	Super::Ticker();

	if (--casttics > 0 && caststate != NULL)
		return 0; 				// not time to change state yet

	if (caststate == NULL || caststate->GetTics() == -1 || caststate->GetNextState() == NULL ||
		(caststate->GetNextState() == caststate && castdeath))
	{
		return -1;
	}
	else
	{
		// just advance to next state in animation
		if (caststate == advplayerstate)
			goto stopattack;	// Oh, gross hack!

		caststate = caststate->GetNextState();

		PlayAttackSound();
		castframes++;
	}

	if (castframes == 12 && !castdeath)
	{
		// go into attack frame
		castattacking = true;
		if (!mClass->IsDescendantOf(NAME_PlayerPawn))
		{
			if (castonmelee)
				basestate = caststate = mDefaults->MeleeState;
			else
				basestate = caststate = mDefaults->MissileState;
			castonmelee ^= 1;
			if (caststate == NULL)
			{
				if (castonmelee)
					basestate = caststate = mDefaults->MeleeState;
				else
					basestate = caststate = mDefaults->MissileState;
			}
		}
		else
		{
			// The players use the melee state differently so it can't be used here
			basestate = caststate = mDefaults->MissileState;
		}
		PlayAttackSound();
	}

	if (castattacking)
	{
		if (castframes == 24 || caststate == mDefaults->SeeState )
		{
		  stopattack:
			castattacking = false;
			castframes = 0;
			caststate = mDefaults->SeeState;
		}
	}

	casttics = caststate->GetTics();
	if (casttics == -1)
		casttics = 15;
	return 0;
}

void DIntermissionScreenCast::Drawer ()
{
	spriteframe_t*		sprframe;

	Super::Drawer();

	const char *name = mName;
	if (name != NULL)
	{
		auto font = generic_ui ? NewSmallFont : SmallFont;
		if (*name == '$') name = GStrings.GetString(name+1);
		DrawText(twod, font, CR_UNTRANSLATED,
			(twod->GetWidth() - font->StringWidth (name) * CleanXfac)/2,
			(twod->GetHeight() * 180) / 200,
			name,
			DTA_CleanNoMove, true, TAG_DONE);
	}

	// draw the current frame in the middle of the screen
	if (caststate != NULL)
	{
		DVector2 castscale = mDefaults->Scale;

		int castsprite = caststate->sprite;

		if (!(mDefaults->flags4 & MF4_NOSKIN) &&
			mDefaults->SpawnState != NULL && caststate->sprite == mDefaults->SpawnState->sprite &&
			mClass->IsDescendantOf(NAME_PlayerPawn) &&
			Skins.Size() > 0)
		{
			// Only use the skin sprite if this class has not been removed from the
			// PlayerClasses list.
			for (unsigned i = 0; i < PlayerClasses.Size(); ++i)
			{
				if (PlayerClasses[i].Type == mClass)
				{
					FPlayerSkin *skin = &Skins[players[consoleplayer].userinfo.GetSkin()];
					castsprite = skin->sprite;

					if (!(mDefaults->flags4 & MF4_NOSKIN))
					{
						castscale = skin->Scale;
					}

				}
			}
		}

		sprframe = &SpriteFrames[sprites[castsprite].spriteframes + caststate->GetFrame()];
		auto pic = TexMan.GetGameTexture(sprframe->Texture[0], true);

		DrawTexture(twod, pic, 160, 170,
			DTA_320x200, true,
			DTA_FlipX, sprframe->Flip & 1,
			DTA_DestHeightF, pic->GetDisplayHeight() * castscale.Y,
			DTA_DestWidthF, pic->GetDisplayWidth() * castscale.X,
			DTA_RenderStyle, mDefaults->RenderStyle,
			DTA_Alpha, mDefaults->Alpha,
			DTA_TranslationIndex, casttranslation.index(),
			TAG_DONE);
	}
}

//==========================================================================
//
//
//
//==========================================================================

void DIntermissionScreenScroller::Init(FIntermissionAction *desc, bool first)
{
	Super::Init(desc, first);
	mFirstPic = mBackground;
	mSecondPic = TexMan.CheckForTexture(static_cast<FIntermissionActionScroller*>(desc)->mSecondPic.GetChars(), ETextureType::MiscPatch);
	mScrollDelay = static_cast<FIntermissionActionScroller*>(desc)->mScrollDelay;
	mScrollTime = static_cast<FIntermissionActionScroller*>(desc)->mScrollTime;
	mScrollDir = static_cast<FIntermissionActionScroller*>(desc)->mScrollDir;
}

int DIntermissionScreenScroller::Responder (FInputEvent *ev)
{
	int res = Super::Responder(ev);

	if (res == -1)
	{
		if (nointerscrollabort) {
			// Consume the key press to prevent advancing the scroller
			return 0;
		}

		mBackground = mSecondPic;
		mTicker = mScrollDelay + mScrollTime;
	}

	return res;
}

void DIntermissionScreenScroller::Drawer ()
{
	auto tex = TexMan.GetGameTexture(mFirstPic);
	auto tex2 = TexMan.GetGameTexture(mSecondPic);

	// These must round down to the nearest full pixel to cover seams between the two textures.
	int fwidth = (int)tex->GetDisplayWidth();
	int fheight = (int)tex->GetDisplayHeight();
	int fwidth2 = (int)tex2->GetDisplayWidth();
	int fheight2 = (int)tex2->GetDisplayHeight();

	double xpos1 = 0, ypos1 = 0, xpos2 = 0, ypos2 = 0;

	int aheight = fheight == 200 ? 240 : fheight == 400 ? 480 : fheight;
	DoubleRect drect;

	if (mScrollDir == SCROLL_Left || mScrollDir == SCROLL_Right)
	{
		// guesstimate the intended aspect ratio.
		int awidth = aheight * 4 / 3;
		int atotalwidth = fwidth + fwidth2;
		int sidespace = (atotalwidth - 2*awidth);
		// Now set a clipping rectangle for the intended viewport
		double displayratio = atotalwidth / double(aheight) - 4./3.;
		double displaywidth = aheight * displayratio;
		GetFullscreenRect(displaywidth, aheight, FSMode_ScaleToFit43, &drect);
		twod->SetClipRect(int(drect.left), int(drect.top), int(drect.width), int(drect.height));

		int ticker = clamp(mTicker - mScrollDelay, 0, mScrollTime);

		switch (mScrollDir)
		{
		case SCROLL_Left:
		default:
			xpos2 = -awidth + double(ticker) * awidth / mScrollTime;
			xpos1 = xpos2 + fwidth2;
			break;

		case SCROLL_Right:
			xpos1 = -double(ticker) * awidth / mScrollTime;
			xpos2 = xpos1 + fwidth;
			break;
		}
		double scale = drect.height / aheight;
		xpos1 *= scale;
		xpos2 *= scale;

		DrawTexture(twod, tex, xpos1 + drect.left, drect.top,
			DTA_DestWidthF, fwidth * scale,
			DTA_DestHeightF, aheight * scale,
			DTA_Masked, false,
			TAG_DONE);
		DrawTexture(twod, tex2, xpos2 + drect.left, drect.top,
			DTA_DestWidthF, fwidth2 * scale,
			DTA_DestHeightF, aheight * scale,
			DTA_Masked, false,
			TAG_DONE);


		twod->ClearClipRect();
	}
	else
	{
		// guesstimate the intended aspect ratio.
		GetFullscreenRect(fwidth, aheight, FSMode_ScaleToFit43, &drect);
		twod->SetClipRect(int(drect.left), int(drect.top), int(drect.width), int(drect.height));

		int ticker = clamp(mTicker - mScrollDelay, 0, mScrollTime);

		switch (mScrollDir)
		{
		case SCROLL_Up:
		default:
			ypos1 = double(ticker) * aheight / mScrollTime;
			ypos2 = ypos1 - aheight;
			break;

		case SCROLL_Down:
			ypos1 = -double(ticker) * aheight / mScrollTime;
			ypos2 = ypos1 + aheight;
			break;
		}
		double scale = drect.height / aheight;
		ypos1 *= scale;
		ypos2 *= scale;

		DrawTexture(twod, tex, drect.left, drect.top + ypos1,
			DTA_DestWidthF, fwidth * scale,
			DTA_DestHeightF, aheight * scale,
			DTA_Masked, false,
			TAG_DONE);
		DrawTexture(twod, tex2, drect.left, drect.top + ypos2,
			DTA_DestWidthF, fwidth2 * scale,
			DTA_DestHeightF, aheight * scale,
			DTA_Masked, false,
			TAG_DONE);


		twod->ClearClipRect();
	}
	mBackground = mSecondPic;
}

//==========================================================================
//
//
//
//==========================================================================

DIntermissionController::DIntermissionController(FIntermissionDescriptor *Desc, bool DeleteDesc, bool ending)
{
	mDesc = Desc;
	mDeleteDesc = DeleteDesc;
	mIndex = 0;
	mAdvance = false;
	mSentAdvance = false;
	mScreen = nullptr;
	mFirst = true;
	mEndGame = ending;
}

bool DIntermissionController::NextPage ()
{
	FTextureID bg;
	bool fill = false;

	if (mIndex == (int)mDesc->mActions.Size() && mDesc->mLink == NAME_None)
	{
		// last page (must block when ending the game.)
		return mEndGame;
	}
	bg.SetInvalid();

	if (mScreen != NULL)
	{
		bg = mScreen->GetBackground(&fill);
		mScreen->Destroy();
	}
again:
	while ((unsigned)mIndex < mDesc->mActions.Size())
	{
		FIntermissionAction *action = mDesc->mActions[mIndex++];
		if (action->mClass == WIPER_ID)
		{
			wipegamestate = static_cast<FIntermissionActionWiper*>(action)->mWipeType;
		}
		else if (action->mClass == TITLE_ID)
		{
			return false;
		}
		else
		{
			// create page here
			mScreen = (DIntermissionScreen*)action->mClass->CreateNew();
			mScreen->SetBackground(bg, fill);	// copy last screen's background before initializing
			mScreen->Init(action, mFirst);
			mFirst = false;
			return true;
		}
	}
	if (mDesc->mLink != NAME_None)
	{
		FIntermissionDescriptor **pDesc = IntermissionDescriptors.CheckKey(mDesc->mLink);
		if (pDesc != NULL)
		{
			if (mDeleteDesc) delete mDesc;
			mDeleteDesc = false;
			mIndex = 0;
			mDesc = *pDesc;
			goto again;
		}
	}
	return false;
}

void DIntermissionController::Start()
{
	if (mScreen) mScreen->Start();
}

bool DIntermissionController::Responder (FInputEvent *ev)
{
	if (mScreen != NULL)
	{
		if (ev->Type == EV_KeyDown)
		{
			const char *cmd = Bindings.GetBind (ev->KeyScan);

			if (cmd != nullptr)
			{
				if (!stricmp(cmd, "toggleconsole") || !stricmp(cmd, "screenshot"))
				{
					return false;
				}
				// The following is needed to be able to enter main menu with a controller,
				// by pressing buttons that are usually assigned to this action, Start and Back by default
				else if (!stricmp(cmd, "menu_main") || !stricmp(cmd, "pause"))
				{
					M_StartControlPanel(true);
					M_SetMenu(NAME_Mainmenu, -1);
					return true;
				}
			}
		}

		if (mScreen->mTicker < 2) return false;	// prevent some leftover events from auto-advancing
		int res = mScreen->Responder(ev);
		if (res == -1)
		{
			mAdvance = true;
		}
		return !!res;
	}
	return false;
}

bool DIntermissionController::Ticker ()
{
	if (mAdvance)
	{
		mSentAdvance = false;
	}
	if (mScreen != NULL)
	{
		mAdvance |= (mScreen->Ticker() == -1);
	}
	if (mAdvance)
	{
		mAdvance = false;
		if (!NextPage())
		{
			return false;
		}
	}
	return true;
}

void DIntermissionController::Drawer ()
{
	if (mScreen != NULL)
	{
		twod->ClearScreen();
		mScreen->Drawer();
	}
}

void DIntermissionController::OnDestroy ()
{
	Super::OnDestroy();
	if (mScreen != NULL) mScreen->Destroy();
	if (mDeleteDesc) delete mDesc;
	mDesc = NULL;
}


//==========================================================================
//
// starts a new intermission
//
//==========================================================================

DIntermissionController* F_StartIntermission(FIntermissionDescriptor *desc, int state, bool deleteme, bool ending)
{
	ScaleOverrider s(twod);
	S_StopAllChannels ();
	gameaction = ga_nothing;
	gamestate = GS_FINALE;
	if (state == FSTATE_InLevelNoWipe) wipegamestate = GS_FINALE;	// don't wipe when within a level.
	auto CurrentIntermission = Create<DIntermissionController>(desc, deleteme, ending);

	// If the intermission finishes straight away then cancel the wipe.
	if (!CurrentIntermission->NextPage())
	{
		CurrentIntermission->Destroy();
		return nullptr;
	}

	GC::WriteBarrier(CurrentIntermission);
	return CurrentIntermission;
}


//==========================================================================
//
// starts a new intermission
//
//==========================================================================

DIntermissionController* F_StartIntermission(FName seq, int state)
{
	FIntermissionDescriptor **pdesc = IntermissionDescriptors.CheckKey(seq);
	if (pdesc == nullptr || (*pdesc)->mActions.Size() == 0)
	{
		return nullptr;
	}
	else
	{
		auto desc = *pdesc;
		auto action = desc->mActions[0];
		if (action->mClass == TITLE_ID)
		{
			// is handled elsewhere.
			return nullptr;
		}

		return F_StartIntermission(desc, state, false);
	}
}


DEFINE_ACTION_FUNCTION(DIntermissionController, Responder)
{
	PARAM_SELF_PROLOGUE(DIntermissionController);
	PARAM_POINTER(evt, FInputEvent);
	ACTION_RETURN_BOOL(self->Responder(evt));
}

DEFINE_ACTION_FUNCTION(DIntermissionController, Ticker)
{
	PARAM_SELF_PROLOGUE(DIntermissionController);
	ACTION_RETURN_BOOL(self->Ticker());
}

DEFINE_ACTION_FUNCTION(DIntermissionController, Start)
{
	PARAM_SELF_PROLOGUE(DIntermissionController);
	self->Start();
	return 0;
}

DEFINE_ACTION_FUNCTION(DIntermissionController, Drawer)
{
	PARAM_SELF_PROLOGUE(DIntermissionController);
	self->Drawer();
	return 0;
}

DEFINE_ACTION_FUNCTION(DIntermissionController, NextPage)
{
	PARAM_SELF_PROLOGUE(DIntermissionController);
	ACTION_RETURN_BOOL(self->NextPage());
}

#include "c_dispatch.h"

CCMD(measureintermissions)
{
	static const char *intermissions[] = {
		"E1TEXT", "E2TEXT", "E3TEXT", "E4TEXT",
		"C1TEXT", "C2TEXT", "C3TEXT", "C4TEXT", "C5TEXT",
		"P1TEXT", "P2TEXT", "P3TEXT", "P4TEXT", "P5TEXT",
		"T1TEXT", "T2TEXT", "T3TEXT", "T4TEXT", "T5TEXT", "NERVETEXT",
		"HE1TEXT", "HE2TEXT", "HE3TEXT", "HE4TEXT", "HE5TEXT",
		"TXT_HEXEN_CLUS1MSG", "TXT_HEXEN_CLUS2MSG","TXT_HEXEN_CLUS3MSG","TXT_HEXEN_CLUS4MSG",
		"TXT_HEXEN_WIN1MSG", "TXT_HEXEN_WIN2MSG","TXT_HEXEN_WIN3MSG",
		"TXT_HEXDD_CLUS1MSG", "TXT_HEXDD_CLUS2MSG",
		"TXT_HEXDD_WIN1MSG", "TXT_HEXDD_WIN2MSG","TXT_HEXDD_WIN3MSG" };

	static const char *languages[] = { "", "cz", "de", "eng", "es", "esm", "fr", "hu", "it", "pl", "pt", "ro", "ru", "sr" };

	for (auto l : languages)
	{
		int langid = *l ? MAKE_ID(l[0], l[1], l[2], 0) : FStringTable::default_table;
		for (auto t : intermissions)
		{
			auto text = GStrings.GetLanguageString(t, langid);
			if (text)
			{
				auto ch = text;
				int numrows, c;
				for (numrows = 1, c = 0; ch[c] != '\0'; ++c)
				{
					numrows += (ch[c] == '\n');
				}
				int width = SmallFont->StringWidth(text);
				if (width > 360 || numrows > 20)
					Printf("%s, %s: %d x %d\n", t, l, width, numrows);
			}
		}
	}
}
