#include "ecwolf_web.h"

#include <algorithm>
#include <cstring>
#include <emscripten.h>
#include <SDL.h>
#include <SDL_mixer.h>

#include "g_mapinfo.h"
#include "id_ca.h"
#include "id_in.h"
#include "id_sd.h"
#include "id_us.h"
#include "m_classes.h"
#include "v_video.h"
#include "wl_agent.h"
#include "wl_game.h"
#include "wl_loadsave.h"
#include "wl_main.h"
#include "wl_play.h"

namespace
{
bool destroyed = false;
bool wrapperPaused = false;
bool pauseAfterFirstFrame = true;
bool initializationComplete = false;
bool runtimeReady = false;
bool audioMuted = true;
int savedMusicOffset = 0;
int mouseDx = 0;
int mouseDy = 0;
int mouseButtons = 0;

const char *const resumeFilename = "technicalentropy-resume.bin";

void StopWrapperAudio()
{
	savedMusicOffset = SD_MusicOff();
}

void ResumeWrapperAudio()
{
	if (audioMuted)
		return;
	if (ingame && levelInfo != NULL)
		ContinueMusic(savedMusicOffset);
	else
	{
		Mix_ResumeMusic();
		SD_MusicOn();
	}
}

bool CanSave()
{
	return !destroyed && ingame && players[ConsolePlayer].mo != NULL &&
		playstate == ex_stillplaying && levelInfo != NULL;
}

void NotifyResumeMetadata(bool available)
{
	const int difficulty = gamestate.difficulty == NULL ? -1 :
		static_cast<int>(SkillInfo::GetSkillIndex(*gamestate.difficulty));
	EM_ASM({
		if (Module.onResumeMetadata) {
			Module.onResumeMetadata({
				available: !!$0,
				map: UTF8ToString($1),
				difficulty: $2,
				timeTics: $3
			});
		}
	}, available ? 1 : 0, gamestate.mapname, difficulty, gamestate.TimeCount);
}
}

extern "C" {

EMSCRIPTEN_KEEPALIVE int TE_Healthy()
{
	return !destroyed && initializationComplete && runtimeReady && screen != NULL;
}

EMSCRIPTEN_KEEPALIVE void TE_KeyEvent(int scancode, int pressed)
{
	if (destroyed || scancode <= 0 || scancode >= SDL_NUM_SCANCODES)
		return;
	Keyboard[scancode] = pressed != 0;
	if (pressed)
		LastScan = scancode;
}

EMSCRIPTEN_KEEPALIVE void TE_MouseEvent(int buttons, int dx, int dy)
{
	if (destroyed)
		return;
	mouseButtons = buttons & 7;
	mouseDx = std::max(-2048, std::min(2048, mouseDx + dx));
	mouseDy = std::max(-2048, std::min(2048, mouseDy + dy));
}

EMSCRIPTEN_KEEPALIVE void TE_ClearInput()
{
	std::memset(Keyboard, 0, sizeof(bool) * SDL_NUM_SCANCODES);
	std::memset(control, 0, sizeof(control));
	LastScan = sc_None;
	LastASCII = 0;
	mouseDx = mouseDy = mouseButtons = 0;
}

EMSCRIPTEN_KEEPALIVE int TE_SaveResume()
{
	if (!CanSave())
		return 0;
	const bool ok = GameSave::Save(resumeFilename, "Browser resume");
	NotifyResumeMetadata(ok);
	return ok ? 1 : 0;
}

EMSCRIPTEN_KEEPALIVE int TE_PauseAndSave()
{
	if (destroyed)
		return 0;
	if (wrapperPaused)
		return 1;
	TE_ClearInput();
	wrapperPaused = true;
	StopWrapperAudio();
	if (ingame && !(Paused & 1))
	{
		Paused |= 1;
	}
	return TE_SaveResume();
}

EMSCRIPTEN_KEEPALIVE void TE_Resume()
{
	if (destroyed)
		return;
	TE_ClearInput();
	if (ingame && (Paused & 1))
	{
		Paused &= ~1;
		ResetTimeCount();
	}
	wrapperPaused = false;
	ResumeWrapperAudio();
}

EMSCRIPTEN_KEEPALIVE int TE_LoadResume()
{
	if (destroyed)
		return 0;
	TE_ClearInput();
	loadedgame = true;
	if (!GameSave::Load(resumeFilename))
	{
		loadedgame = false;
		return 0;
	}
	Menu::closeMenus(true);
	NotifyResumeMetadata(true);
	return 1;
}

EMSCRIPTEN_KEEPALIVE void TE_SetAudio(int muted, int volume)
{
	const bool wasMuted = audioMuted;
	audioMuted = muted != 0;
	const int clamped = std::max(0, std::min(MAX_VOLUME, volume));
	AdlibVolume = SoundVolume = MusicVolume = audioMuted ? 0 : clamped;
	Mix_Volume(-1, audioMuted ? 0 : static_cast<int>(128.0 * MULTIPLY_VOLUME(clamped)));
	SD_UpdateMusicVolume(0);
	SD_UpdatePCSpeakerVolume();
	if (audioMuted)
		savedMusicOffset = SD_MusicOff();
	else if (wasMuted && !wrapperPaused)
		ResumeWrapperAudio();
}

EMSCRIPTEN_KEEPALIVE void TE_Destroy()
{
	if (destroyed)
		return;
	destroyed = true;
	wrapperPaused = false;
	TE_ClearInput();
	SD_Shutdown();
}

int TE_WebDestroyed()
{
	return destroyed ? 1 : 0;
}

void TE_WebInitialize()
{
	audioMuted = true;
	AdlibVolume = SoundVolume = MusicVolume = 0;
	Mix_Volume(-1, 0);
	SD_UpdateMusicVolume(0);
	SD_UpdatePCSpeakerVolume();
	// InitGame renders a progress console before the authentic title/menu loop
	// exists. Do not let that temporary screen satisfy the wrapper's startup
	// handshake or it will suspend InitGame halfway through initialization.
	initializationComplete = true;
}

void TE_WebFramePresented(const unsigned char *pixels, int width, int height, int pitch)
{
	if (destroyed || pixels == NULL || width <= 1 || height <= 1 || pitch < width)
		return;
	if (!runtimeReady)
	{
		// Yield while InitGame and the menu setup draw. Besides keeping the page
		// responsive, this lets JavaScript cancellation and its bounded startup
		// watchdog run even on slower browsers.
		emscripten_sleep(0);
		return;
	}
	TE_WebWaitWhilePaused();
}

void TE_WebMarkInteractive()
{
	if (destroyed || !initializationComplete || runtimeReady)
		return;
	runtimeReady = true;
	if (pauseAfterFirstFrame)
	{
		pauseAfterFirstFrame = false;
		wrapperPaused = true;
		StopWrapperAudio();
	}
	TE_WebWaitWhilePaused();
}

void TE_WebWaitWhilePaused()
{
	while (wrapperPaused && !destroyed)
	{
		// The loop remains fully suspended here, but a display-interval poll
		// prevents the first short keyboard or touch tap after Resume from being
		// lost behind a long sleep.
		emscripten_sleep(16);
	}
}

void TE_WebWaitForInput()
{
	// ECWolf's desktop acknowledgement loop blocks in SDL_WaitEvent. Browser
	// controls are forwarded through exported adapter calls instead of SDL's
	// iframe event target, so yield briefly and let those calls update LastScan.
	emscripten_sleep(16);
}

void TE_ConsumeMouse(int *dx, int *dy)
{
	*dx = mouseDx;
	*dy = mouseDy;
	mouseDx = mouseDy = 0;
}

int TE_WebMouseButtons()
{
	return mouseButtons;
}

void TE_WebSaveFinished(const char *filename, int ok)
{
	EM_ASM({
		if (Module.onSaveComplete) {
			Module.onSaveComplete({ hidden: UTF8ToString($0) === 'technicalentropy-resume.bin', ok: !!$1 });
		}
	}, filename, ok);
}

void TE_WebLevelFinished(const char *mapName, int difficulty, int timeTics,
	int kills, int treasures, int secrets)
{
	EM_ASM({
		if (Module.onLevelComplete) {
			Module.onLevelComplete({
				map: UTF8ToString($0), difficulty: $1, timeTics: $2,
				kills: $3, treasures: $4, secrets: $5
			});
		}
	}, mapName, difficulty, timeTics, kills, treasures, secrets);
}

void TE_WebRuntimeFailed()
{
	EM_ASM({
		if (Module.onRuntimeFailure) Module.onRuntimeFailure();
	});
}

}
