/*
	This file is part of Warzone 2100.
	Copyright (C) 2008-2020  Warzone 2100 Project

	Warzone 2100 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 2 of the License, or
	(at your option) any later version.

	Warzone 2100 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 Warzone 2100; if not, write to the Free Software
	Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/

/* This file is derived from the SDL player example as found in the OggTheora
 * software codec source code. In particular this is examples/player_example.c
 * as found in OggTheora 1.0beta3.
 *
 * The copyright to this file was originally owned by and licensed as follows.
 * Please note, however, that *this* file, i.e. the one you are currently
 * reading is not licensed as such anymore.
 *
 * Copyright (C) 2002-2007 Xiph.org Foundation
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - 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.
 *
 * - Neither the name of the Xiph.org Foundation nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``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 FOUNDATION
 * OR CONTRIBUTORS 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 "lib/framework/frame.h"
#include "lib/ivis_opengl/gfx_api.h"
#include "sequence.h"
#include "timer.h"
#include "lib/framework/math_ext.h"
#include "lib/ivis_opengl/piestate.h"
#include "lib/ivis_opengl/pieblitfunc.h"
#include "lib/ivis_opengl/screen.h"
#include "lib/sound/audio.h"
#include "lib/sound/openal_error.h"
#include "lib/sound/mixer.h"
#ifndef GLM_ENABLE_EXPERIMENTAL
	#define GLM_ENABLE_EXPERIMENTAL
#endif
#include <glm/gtx/transform.hpp>
#include "lib/ivis_opengl/pieclip.h"

#include <theora/theora.h>
#include <physfs.h>

#include <vorbis/codec.h>
#include <AL/al.h>

#include "lib/framework/physfs_ext.h"

// stick this in sequence.h perhaps?
struct AudioData
{
	ALuint buffer1;			// buffer 1
	ALuint buffer2;			// buffer 2
	ALuint source;			// source
	int totbufstarted;		// number of buffers started
	int audiofd_fragsize;	// audio fragment size, used to calculate how big audiobuf is
	int audiobuf_fill;		// how full our audio buffer is
};

struct VideoData
{
	ogg_sync_state oy;		// ogg sync state
	ogg_page og;			// ogg page
	ogg_stream_state vo;	// ogg stream state
	ogg_stream_state to;	// ogg stream state
	theora_info ti;			// theora info
	theora_comment tc;		// theora comment
	theora_state td;		// theora state
	vorbis_info vi;			// vorbis info
	vorbis_dsp_state vd;	// vorbis display state
	vorbis_block vb;		// vorbis block
	vorbis_comment vc;		// vorbis comment
};
// stick that in sequence.h perhaps?

// for our audio structure
static AudioData audiodata;
static ALint sourcestate = 0;		//Source state information

// for our video structure
static VideoData videodata;

/// are we playing a theora stream?
static int theora_p = 0;

/// are we playing an ogg vorbis stream?
static int vorbis_p = 0;

static bool stateflag = false;
static bool videoplaying = false;
static bool videobuf_ready = false;		// single frame video buffer ready for processing
static bool audiobuf_ready = false;		// single 'frame' audio buffer ready for processing

// file handle
class VideoProvider;
static std::shared_ptr<VideoProvider> inVideoProvider;

static iV_Image VideoFrameBitmap;				// texture buffer
static SCANLINE_MODE scanMode = SCANLINES_OFF;
static ogg_int16_t *audiobuf = nullptr;			// audio buffer

// For timing
static double audioTime = 0;

static double videobuf_time = 0;
static double sampletimeOffset = 0;
static double basetime = -1;
static double last_time;
static double timer_expire;
static bool timer_started = false;

static ogg_int64_t audiobuf_granulepos = 0;	// time position of last sample
static ogg_int64_t videobuf_granulepos = -1;	// time position of last video frame

// frame & dropped frame counter
static int frames = 0;
static int dropped = 0;

// Screen dimensions
#define NUM_VERTICES 4
static std::unique_ptr<GFX> videoGfx = nullptr;
static gfx_api::gfxFloat vertices[NUM_VERTICES][2];
static gfx_api::gfxFloat Scrnvidpos[3];

static SCANLINE_MODE use_scanlines;
static bool scanlinesDisabled = false;

//// Abstraction for handling loading videos from files, or from memory buffer
class VideoProvider
{
public:
	VideoProvider(const WzString& filename)
	: m_filename(filename)
	{ }
	virtual ~VideoProvider();
public:
	virtual const WzString& filename() const { return m_filename; }
	virtual bool seek_begin() = 0;
	virtual int buffer_data(ogg_sync_state *oy) = 0;
	virtual bool end_of_data() const = 0;
private:
	WzString m_filename;
};

class PhysFSVideoProvider : public VideoProvider
{
public:
	// Takes ownership of the PHYSFS_file*
	PhysFSVideoProvider(PHYSFS_file *in, const WzString& filename);
	~PhysFSVideoProvider();
public:
	virtual bool seek_begin() override;
	virtual int buffer_data(ogg_sync_state *oy) override;
	virtual bool end_of_data() const override;
private:
	PHYSFS_file *in = nullptr;
};

class MemoryBufferVideoProvider : public VideoProvider
{
public:
	MemoryBufferVideoProvider(std::shared_ptr<const std::vector<char>> memoryBuffer, const WzString& filename);
public:
	virtual bool seek_begin() override;
	virtual int buffer_data(ogg_sync_state *oy) override;
	virtual bool end_of_data() const override;
private:
	std::shared_ptr<const std::vector<char>> memoryBuffer;
	size_t currPos = 0;
};

VideoProvider::~VideoProvider()
{ }

// Takes ownership of the PHYSFS_file*
PhysFSVideoProvider::PhysFSVideoProvider(PHYSFS_file *in, const WzString& filename)
: VideoProvider(filename)
, in(in)
{ }
PhysFSVideoProvider::~PhysFSVideoProvider()
{
	if (in && PHYSFS_isInit())
	{
		PHYSFS_close(in);
		in = nullptr;
	}
}
bool PhysFSVideoProvider::seek_begin()
{
	return PHYSFS_seek(in, 0) != 0;
}
int PhysFSVideoProvider::buffer_data(ogg_sync_state *oy)
{
	if (!in) return 0;

	// read in 256K chunks
	const int size = 262144;
	char *buffer = ogg_sync_buffer(oy, size);
	int bytes = static_cast<int>(WZ_PHYSFS_readBytes(in, buffer, size));

	ogg_sync_wrote(oy, bytes);
	return (bytes);
}
bool PhysFSVideoProvider::PhysFSVideoProvider::end_of_data() const
{
	return PHYSFS_eof(in) != 0;
}

MemoryBufferVideoProvider::MemoryBufferVideoProvider(std::shared_ptr<const std::vector<char>> memoryBuffer, const WzString& filename)
: VideoProvider(filename)
, memoryBuffer(memoryBuffer)
{ }
bool MemoryBufferVideoProvider::seek_begin()
{
	currPos = 0;
	return true;
}
int MemoryBufferVideoProvider::buffer_data(ogg_sync_state *oy)
{
	if (!memoryBuffer) return 0;
	if (memoryBuffer->empty()) return 0;
	if (currPos >= memoryBuffer->size()) return 0;

	// read in 256K chunks
	const int size = 262144;
	char *buffer = ogg_sync_buffer(oy, size);

	size_t bytesToRead = std::min<size_t>(size, memoryBuffer->size() - currPos);
	memcpy(buffer, memoryBuffer->data() + currPos, size);
	currPos += bytesToRead;

	int bytes = static_cast<int>(bytesToRead);

	ogg_sync_wrote(oy, bytes);
	return (bytes);
}
bool MemoryBufferVideoProvider::end_of_data() const
{
	if (!memoryBuffer) return true;
	return currPos >= memoryBuffer->size();
}

std::shared_ptr<VideoProvider> makeVideoProvider(PHYSFS_file *in, const WzString& filename)
{
	return std::make_shared<PhysFSVideoProvider>(in, filename);
}

std::shared_ptr<VideoProvider> makeVideoProvider(std::shared_ptr<const std::vector<char>> memoryBuffer, const WzString& filename)
{
	return std::make_shared<MemoryBufferVideoProvider>(memoryBuffer, filename);
}

/** helper: push a page into the appropriate stream
	this can be done blindly; a stream won't accept a page
	that doesn't belong to it
*/
static int queue_page(ogg_page *page)
{
	if (theora_p)
	{
		ogg_stream_pagein(&videodata.to, page);
	}

	if (vorbis_p)
	{
		ogg_stream_pagein(&videodata.vo, page);
	}

	return 0;
}

// sets the frames number we are on
static void seq_SetFrameNumber(int frame)
{
	frames = frame;
}

/// @TODO FIXME:  This routine can & will fail when sources are used up!
static void open_audio(void)
{
	float volume = 1.0;

	audiodata.audiofd_fragsize = static_cast<int>((((videodata.vi.channels * 16) / 8) * videodata.vi.rate));
	audiobuf = (ogg_int16_t *)malloc(audiodata.audiofd_fragsize);

	// FIX ME:  This call will fail, since we have, most likely, already
	// used up all available sources late in the game!
	// openal
	alGenSources(1, &audiodata.source);
	sound_GetError();	// on error, we *need* to free another source!

	// Create an OpenAL buffer and fill it with the decoded data
	alGenBuffers(1, &audiodata.buffer1);
	sound_GetError();
	alGenBuffers(1, &audiodata.buffer2);
	sound_GetError();	// Log and clear error codes

	audiodata.totbufstarted = 0;

	// set the volume of the FMV based on the user's preferences
	volume = sound_GetUIVolume();
	alSourcef(audiodata.source, AL_GAIN, volume);
}

/** Cleans up audio sources & buffers
 */
static void audio_close(void)
{
	// NOTE: sources & buffers deleted in seq_Shutdown()
	audiobuf_granulepos = 0;
//	clear struct
//	memset(&audiodata,0x0,sizeof(audiodata));
	audiodata.audiobuf_fill = 0;
	audiodata.audiofd_fragsize = 0;
	audiodata.source = 0;
	audiodata.buffer1 = audiodata.buffer2 = 0;
	if (audiobuf)
	{
		free(audiobuf);
		audiobuf = nullptr;
	}
}

// Retrieves the current time with millisecond accuracy
static double getTimeNow(void)
{
	return Timer_getElapsedMilliSecs();
}

// get relative time since beginning playback, compensating for A/V drift
static double getRelativeTime(void)
{
	if (basetime == -1)					// check to see if this is first time run
	{
		basetime = getTimeNow();
		timer_expire = Timer_getElapsedMicroSecs();
		timer_expire += (int)((videobuf_time - getTimeNow()) * 1000000.0);
		timer_started = true;
	}
	return ((getTimeNow() - basetime) * .001);
}

const size_t texture_width = 1024;
const size_t texture_height = 1024;

/** Allocates memory to hold the decoded video frame
 */
static void Allocate_videoFrame(void)
{
	scanMode = seq_getScanlinesDisabled() ? SCANLINES_OFF : seq_getScanlineMode();
	const ogg_uint32_t height_factor = (scanMode ? 2 : 1);
	int expected_size = videodata.ti.frame_width * videodata.ti.frame_height * 4 * height_factor;

	VideoFrameBitmap.allocate(videodata.ti.frame_width, videodata.ti.frame_height * height_factor, 4, true);
	ASSERT(VideoFrameBitmap.data_size() == expected_size, "Allocated size does not match expected size!");
}

static void deallocateVideoFrame(void)
{
	VideoFrameBitmap.clear();
}

#ifndef __BIG_ENDIAN__
const int Rshift = 0;
const int Gshift = 8;
const int Bshift = 16;
const int Ashift = 24;
// RGBmask is used only after right-shifting, so ignore the leftmost bit of each byte
const int RGBmask = 0x007f7f7f;
const int Amask = 0xff000000;
#else
const int Rshift = 24;
const int Gshift = 16;
const int Bshift = 8;
const int Ashift = 0;
const int RGBmask = 0x7f7f7f00;
const int Amask = 0x000000ff;
#endif
#define Vclip( x )	( (x > 0) ? ((x < 255) ? x : 255) : 0 )
// main routine to display video on screen.
static void video_write(bool update)
{
	unsigned int x = 0, y = 0;
	const int video_width = videodata.ti.frame_width;
	const int video_height = videodata.ti.frame_height;
	unsigned char* pRGBABitmapData = VideoFrameBitmap.bmp_w();
	yuv_buffer yuv;

	auto setRGBAFramePixel = [pRGBABitmapData](int pixelOffset, uint32_t rgbaValue) {
		memcpy(&pRGBABitmapData[(pixelOffset * 4)], &rgbaValue, sizeof(uint32_t));
	};

	if (update)
	{
		int rgb_offset = 0;
		int y_offset = 0;
		int uv_offset = 0;
		const int half_width = video_width / 2;

		theora_decode_YUVout(&videodata.td, &yuv);

		// fill the RGBA buffer
		for (y = 0; y < video_height; y++)
		{
			y_offset = y * yuv.y_stride;
			uv_offset = (y >> 1) * yuv.uv_stride;

			for (x = 0; x < half_width; x++)
			{
				int Y = yuv.y[y_offset++] - 16;
				const int U = yuv.u[uv_offset] - 128;
				const int V = yuv.v[uv_offset++] - 128;

				int A = 298 * Y;
				const int C = 409 * V;

				int R = Vclip((A + C + 128) >> 8);
				int G = Vclip((A - 100 * U - (C >> 1) + 128) >> 8);
				int B = Vclip((A + 516 * U + 128) >> 8);

				uint32_t rgba = (R << Rshift) | (G << Gshift) | (B << Bshift) | (0xFF << Ashift);

				setRGBAFramePixel(rgb_offset, rgba);
				if (scanMode == SCANLINES_50)
				{
					// halve the rgb values for a dimmed scanline
					setRGBAFramePixel(rgb_offset + video_width, (rgba >> 1 & RGBmask) | Amask);
				}
				else if (scanMode == SCANLINES_BLACK)
				{
					setRGBAFramePixel(rgb_offset + video_width, Amask);
				}
				rgb_offset++;

				// second pixel, U and V (and thus C) are the same as before.
				Y = yuv.y[y_offset++] - 16;
				A = 298 * Y;

				R = Vclip((A + C + 128) >> 8);
				G = Vclip((A - 100 * U - (C >> 1) + 128) >> 8);
				B = Vclip((A + 516 * U + 128) >> 8);

				rgba = (R << Rshift) | (G << Gshift) | (B << Bshift) | (0xFF << Ashift);
				setRGBAFramePixel(rgb_offset, rgba);
				if (scanMode == SCANLINES_50)
				{
					// halve the rgb values for a dimmed scanline
					setRGBAFramePixel(rgb_offset + video_width, (rgba >> 1 & RGBmask) | Amask);
				}
				else if (scanMode == SCANLINES_BLACK)
				{
					setRGBAFramePixel(rgb_offset + video_width, Amask);
				}
				rgb_offset++;
			}
			if (scanMode)
			{
				rgb_offset += video_width;
			}
		}

		videoGfx->updateTexture(VideoFrameBitmap);
	}

	const auto& modelViewProjectionMatrix = glm::ortho(0.f, static_cast<float>(pie_GetVideoBufferWidth()), static_cast<float>(pie_GetVideoBufferHeight()), 0.f) *
	glm::translate(glm::vec3(Scrnvidpos[0], Scrnvidpos[1], Scrnvidpos[2]));

	gfx_api::VideoPSO::get().bind();
	gfx_api::VideoPSO::get().bind_constants({ modelViewProjectionMatrix, glm::vec2(0), glm::vec2(0), glm::vec4(1), 0 });
	videoGfx->draw<gfx_api::VideoPSO>(modelViewProjectionMatrix);
}

// FIXME: perhaps we should use wz's routine for audio?
// loads up the audio buffers, and calculates audio sync time.
static void audio_write(void)
{
	ALint processed = 0;
	ALint queued = 0;

	alGetSourcei(audiodata.source, AL_BUFFERS_PROCESSED, &processed);
	alGetSourcei(audiodata.source, AL_BUFFERS_QUEUED, &queued);
	if ((audiodata.totbufstarted < 2 || processed) && audiodata.audiobuf_fill > 0)
	{
		// we have audiobuf_fill bytes of data
		ALuint oldbuffer = 0;

		if (audiodata.totbufstarted == 0)
		{
			oldbuffer = audiodata.buffer1;
		}
		else if (audiodata.totbufstarted == 1)
		{
			oldbuffer = audiodata.buffer2;
		}
		else
		{
			ALint buffer_size = 0;
			ogg_int64_t current_sample = 0;

			alSourceUnqueueBuffers(audiodata.source, 1, &oldbuffer);
			alGetBufferi(oldbuffer, AL_SIZE, &buffer_size);
			// audio time sync
			audioTime += (double) buffer_size / (videodata.vi.rate * videodata.vi.channels);
			debug(LOG_VIDEO, "Audio sync");
			current_sample = audiobuf_granulepos - audiodata.audiobuf_fill / 2 / videodata.vi.channels;
			sampletimeOffset -= getTimeNow() - 1000 * current_sample / videodata.vi.rate;
		}

		alBufferData(oldbuffer, (videodata.vi.channels == 1 ? AL_FORMAT_MONO16 : AL_FORMAT_STEREO16),
		             audiobuf, audiodata.audiobuf_fill, static_cast<ALsizei>(videodata.vi.rate));

		alSourceQueueBuffers(audiodata.source, 1, &oldbuffer);
		audiodata.totbufstarted++;
		if (audiodata.totbufstarted > 2)
		{
			audiodata.totbufstarted = 2;
		}

		if (sourcestate != AL_PLAYING)
		{
			debug(LOG_VIDEO, "starting source\n");
			alSourcePlay(audiodata.source);
		}

		audiobuf_ready = false;
		audiodata.audiobuf_fill = 0;
	}
}

static void seq_InitOgg(void)
{
	debug(LOG_VIDEO, "seq_InitOgg");

	stateflag = false;
	theora_p = 0;
	vorbis_p = 0;

	videoplaying = false;
	seq_setScanlinesDisabled(false);

	/* single frame video buffering */
	videobuf_ready = false;
	videobuf_granulepos = -1;
	videobuf_time = 0;
	frames = 0;
	dropped = 0;

	/* single audio fragment audio buffering */
	audiodata.audiobuf_fill = 0;
	audiobuf_ready = false;

	audiobuf_granulepos = 0;	/* time position of last sample */
	audioTime = 0;

	sampletimeOffset = 0;

	/* start up Ogg stream synchronization layer */
	ogg_sync_init(&videodata.oy);

	/* init supporting Vorbis structures needed in header parsing */
	vorbis_info_init(&videodata.vi);
	vorbis_comment_init(&videodata.vc);

	/* init supporting Theora structuretotbufstarteds needed in header parsing */
	theora_comment_init(&videodata.tc);
	theora_info_init(&videodata.ti);
	Timer_Init();
	Timer_start();
}

void update_buffers()
{
	if (videoGfx == nullptr)
	{
		return;
	}

	// when using scanlines we need to double the height
	const uint32_t height_factor = ((!seq_getScanlinesDisabled() && seq_getScanlineMode()) ? 2 : 1);
	const gfx_api::gfxFloat vtwidth = (float)videodata.ti.frame_width / (float)texture_width;
	const gfx_api::gfxFloat vtheight = (float)videodata.ti.frame_height * height_factor / (float)texture_height;
	gfx_api::gfxFloat texcoords[NUM_VERTICES * 2] = {0.0f, 0.0f, vtwidth, 0.0f, 0.0f, vtheight, vtwidth, vtheight};
	videoGfx->buffers(NUM_VERTICES, vertices, texcoords);
}

bool seq_Play(std::shared_ptr<VideoProvider> video)
{
	int pp_level_max = 0;
	int pp_level = 0;
	ogg_packet op;

	debug(LOG_VIDEO, "starting playback of: %s", (video) ? video->filename().toUtf8().c_str() : "");

	if (videoplaying)
	{
		debug(LOG_VIDEO, "previous movie is not yet finished");
		seq_Shutdown();
	}

	seq_InitOgg();

	if (!video)
	{
		return false;
	}

	video->seek_begin();

	theora_p = 0;
	vorbis_p = 0;

	/* Ogg file open; parse the headers */
	/* Only interested in Vorbis/Theora streams */
	while (!stateflag)
	{
		int ret = video->buffer_data(&videodata.oy);

		if (ret == 0)
		{
			break;
		}

		while (ogg_sync_pageout(&videodata.oy, &videodata.og) > 0)
		{
			ogg_stream_state test;

			/* is this a mandated initial header? If not, stop parsing */
			if (!ogg_page_bos(&videodata.og))
			{
				/* don't leak the page; get it into the appropriate stream */
				queue_page(&videodata.og);
				stateflag = 1;
				break;
			}

			ogg_stream_init(&test, ogg_page_serialno(&videodata.og));
			ogg_stream_pagein(&test, &videodata.og);
			ogg_stream_packetout(&test, &op);

			/* identify the codec: try theora */
			if (!theora_p && theora_decode_header(&videodata.ti, &videodata.tc, &op) >= 0)
			{
				/* it is theora */
				memcpy(&videodata.to, &test, sizeof(test));
				theora_p = 1;
			}
			else if (!vorbis_p && vorbis_synthesis_headerin(&videodata.vi, &videodata.vc, &op) >= 0)
			{
				/* it is vorbis */
				memcpy(&videodata.vo, &test, sizeof(test));
				vorbis_p = 1;
			}
			else
			{
				/* whatever it is, we don't care about it */
				ogg_stream_clear(&test);
			}
		}
		/* fall through to non-bos page parsing */
	}

	/* we're expecting more header packets. */
	while ((theora_p && theora_p < 3) || (vorbis_p && vorbis_p < 3))
	{
		int ret;

		/* look for further theora headers */
		while (theora_p && (theora_p < 3) && (ret = ogg_stream_packetout(&videodata.to, &op)))
		{
			if (ret < 0)
			{
				debug(LOG_ERROR, "Error parsing Theora stream headers; corrupt stream?\n");
				return false;
			}

			if (theora_decode_header(&videodata.ti, &videodata.tc, &op))
			{
				debug(LOG_ERROR, "Error parsing Theora stream headers; corrupt stream?\n");
				return false;
			}

			theora_p++;
		}

		/* look for more vorbis header packets */
		while (vorbis_p && (vorbis_p < 3) && (ret = ogg_stream_packetout(&videodata.vo, &op)))
		{
			if (ret < 0)
			{
				debug(LOG_ERROR, "Error parsing Vorbis stream headers; corrupt stream?\n");
				return false;
			}

			if (vorbis_synthesis_headerin(&videodata.vi, &videodata.vc, &op))
			{
				debug(LOG_ERROR, "Error parsing Vorbis stream headers; corrupt stream?\n");
				return false;
			}

			vorbis_p++;
		}

		/* The header pages/packets will arrive before anything else we
				care about, or the stream is not obeying spec */
		if (ogg_sync_pageout(&videodata.oy, &videodata.og) > 0)
		{
			queue_page(&videodata.og);	/* demux into the appropriate stream */
		}
		else
		{
			ret = video->buffer_data(&videodata.oy);   /* someone needs more data */

			if (ret == 0)
			{
				debug(LOG_ERROR, "End of file while searching for codec headers.\n");
				return false;
			}
		}
	}

	/* and now we have it all.  initialize decoders */
	if (theora_p)
	{
		theora_decode_init(&videodata.td, &videodata.ti);
		debug(LOG_VIDEO, "Ogg logical stream %x is Theora %dx%d %.02f fps video",
		      (unsigned int) videodata.to.serialno, (int) videodata.ti.width, (int) videodata.ti.height,
		      (double) videodata.ti.fps_numerator / videodata.ti.fps_denominator);
		if (videodata.ti.width != videodata.ti.frame_width || videodata.ti.height != videodata.ti.frame_height)
		{
			debug(LOG_VIDEO, "  Frame content is %dx%d with offset (%d,%d)", videodata.ti.frame_width,
			      videodata.ti.frame_height, videodata.ti.offset_x, videodata.ti.offset_y);
		}

		// hmm
		theora_control(&videodata.td, TH_DECCTL_GET_PPLEVEL_MAX, &pp_level_max, sizeof(pp_level_max));
		pp_level = pp_level_max;
		theora_control(&videodata.td, TH_DECCTL_SET_PPLEVEL, &pp_level, sizeof(pp_level));
	}
	else
	{
		/* tear down the partial theora setup */
		theora_info_clear(&videodata.ti);
		theora_comment_clear(&videodata.tc);
	}

	if (vorbis_p)
	{
		vorbis_synthesis_init(&videodata.vd, &videodata.vi);
		vorbis_block_init(&videodata.vd, &videodata.vb);
		debug(LOG_VIDEO, "Ogg logical stream %x is Vorbis %d channel %d Hz audio",
		      (unsigned int) videodata.vo.serialno, videodata.vi.channels, (int) videodata.vi.rate);
	}
	else
	{
		/* tear down the partial vorbis setup */
		vorbis_info_clear(&videodata.vi);
		vorbis_comment_clear(&videodata.vc);
	}

	/* open audio */
	if (vorbis_p)
	{
		open_audio();
	}

	/* open video */
	videoGfx = std::make_unique<GFX>(GFX_TEXTURE, 2);
	if (theora_p)
	{
		if (videodata.ti.frame_width > texture_width || videodata.ti.frame_height > texture_height)
		{
			debug(LOG_ERROR, "Video size too large, must be below %zu x %zu!",
			      texture_width, texture_height);
			videoGfx = nullptr;
			return false;
		}
		if (videodata.ti.pixelformat != OC_PF_420)
		{
			debug(LOG_ERROR, "Video not in YUV420 format!");
			videoGfx = nullptr;
			return false;
		}
		iV_Image blackFrame;
		blackFrame.allocate(texture_width, texture_height, 4, true);

		// disable scanlines temporarily if the video is too large for the texture or shown too small
		if (videodata.ti.frame_height * 2 > texture_height || vertices[3][1] < videodata.ti.frame_height * 2)
		{
			seq_setScanlinesDisabled(true);
		}

		Allocate_videoFrame();
		videoGfx->makeCompatibleTexture(&blackFrame, "mem::blackframe");
		videoGfx->updateTexture(blackFrame);
		blackFrame.clear();

		update_buffers();
	}

	/* on to the main decode loop.  We assume in this example that audio
		and video start roughly together, and don't begin playback until
		we have a start frame for both.  This is not necessarily a valid
		assumption in Ogg A/V streams! It will always be true of the
		example_encoder (and most streams) though. */
	sampletimeOffset = getTimeNow();
	videoplaying = true;
	inVideoProvider = video; // only set this here (after videoplaying is set), so that it's always freed in seq_Shutdown()
	return true;
}

bool seq_Playing()
{
	return videoplaying;
}

/**
 * Display the next frame and play the sound.
 * \return false if the end of the video is reached.
 */
bool seq_Update()
{
	ogg_packet op;
	int ret;
	int i, j;
	float   **pcm;
	int count, maxsamples;

	/* we want a video and audio frame ready to go at all times.  If
		   we have to buffer incoming, buffer the compressed data (ie, let
		   ogg do the buffering) */
	if (!videoplaying)
	{
		debug(LOG_VIDEO, "no movie playing");
		return false;
	}

	while (vorbis_p && !audiobuf_ready)
	{
		/* if there's pending, decoded audio, grab it */
		if ((ret = vorbis_synthesis_pcmout(&videodata.vd, &pcm)) > 0)
		{
			// we now have float pcm data in pcm
			// going to convert that to int pcm in audiobuf
			count = audiodata.audiobuf_fill / 2;
			maxsamples = (audiodata.audiofd_fragsize - audiodata.audiobuf_fill) / 2 / videodata.vi.channels;

			for (i = 0; i < ret && i < maxsamples; i++)
			{
				for (j = 0; j < videodata.vi.channels; j++)
				{
					int val = static_cast<int>(nearbyint(pcm[j][i] * 32767.f));

					if (val > 32767)
					{
						val = 32767;
					}
					else if (val < -32768)
					{
						val = -32768;
					}
					audiobuf[count++] = val;
				}
			}

			vorbis_synthesis_read(&videodata.vd, i);
			audiodata.audiobuf_fill += i * videodata.vi.channels * 2;

			if (audiodata.audiobuf_fill == audiodata.audiofd_fragsize)
			{
				audiobuf_ready = true;
			}

			if (videodata.vd.granulepos >= 0)
			{
				audiobuf_granulepos = videodata.vd.granulepos - ret + i;
			}
			else
			{
				audiobuf_granulepos += i;
			}
		}
		else
		{
			/* no pending audio; is there a pending packet to decode? */
			if (ogg_stream_packetout(&videodata.vo, &op) > 0)
			{
				if (vorbis_synthesis(&videodata.vb, &op) == 0)
				{
					/* test for success! */
					vorbis_synthesis_blockin(&videodata.vd, &videodata.vb);
				}
			}
			else
			{
				/* we need more data; break out to suck in another page */
				break;
			}
		}
	}

	while (theora_p && !videobuf_ready)
	{
		/* theora is one in, one out... */
		if (ogg_stream_packetout(&videodata.to, &op) > 0)
		{
			double now_time = 0;
			double delay = 0;

			theora_decode_packetin(&videodata.td, &op);
			videobuf_granulepos = videodata.td.granulepos;
			videobuf_time = theora_granule_time(&videodata.td, videobuf_granulepos);

			now_time = getRelativeTime();
			delay = videobuf_time - getRelativeTime();

			if ((delay >= 0.0f) || (now_time - last_time >= 1.0f))
			{
				videobuf_ready = true;
				seq_SetFrameNumber(seq_GetFrameNumber() + 1);
			}
			else
			{
				// running slow, so we skip this frame
				dropped++;
			}
		}
		else
		{
			break;
		}
	}

	alGetSourcei(audiodata.source, AL_SOURCE_STATE, &sourcestate);

	if (inVideoProvider->end_of_data()
		&& !videobuf_ready
		&& ((!audiobuf_ready && (audiodata.audiobuf_fill == 0)) || audio_Disabled())
		&& sourcestate != AL_PLAYING)
	{
		video_write(false);
		seq_Shutdown();
		debug(LOG_VIDEO, "video finished");
		return false;
	}

	if (!videobuf_ready || !audiobuf_ready)
	{
		/* no data yet for somebody.  Grab another page */
		ret = inVideoProvider->buffer_data(&videodata.oy);
		while (ogg_sync_pageout(&videodata.oy, &videodata.og) > 0)
		{
			queue_page(&videodata.og);
		}
	}

	/* If playback has begun, top audio buffer off immediately. */
	if (vorbis_p
	    && stateflag
	    // FIXME : it is possible to crash if people are playing with no sound.
	    && !audio_Disabled())
	{
		// play the data in pcm
		audio_write();
	}

	/* are we at or past time for this video frame? */
	if (stateflag && videobuf_ready && (videobuf_time <= getRelativeTime()))
	{
		video_write(true);
		last_time = getRelativeTime();
		videobuf_ready = false;
	}
	else if (stateflag)
	{
		video_write(false);
	}

	/* if our buffers either don't exist or are ready to go,
		   we can begin playback */
	if ((!theora_p || videobuf_ready) && (!vorbis_p || audiobuf_ready) && !stateflag)
	{
		debug(LOG_VIDEO, "all buffers ready");
		stateflag = true;
	}

	/* same if we've run out of input */
	if (inVideoProvider->end_of_data())
	{
		stateflag = true;
	}

	return true;
}

void seq_Shutdown()
{
	/* tear it all down */
	debug(LOG_VIDEO, "seq_Shutdown");

	if (!videoplaying)
	{
		debug(LOG_VIDEO, "movie is not playing");
		return;
	}
	videoGfx = nullptr;

	if (vorbis_p)
	{
		ogg_stream_clear(&videodata.vo);
		vorbis_block_clear(&videodata.vb);
		vorbis_dsp_clear(&videodata.vd);
		vorbis_comment_clear(&videodata.vc);
		vorbis_info_clear(&videodata.vi);

		alDeleteSources(1, &audiodata.source);
		alDeleteBuffers(1, &audiodata.buffer1);
		alDeleteBuffers(1, &audiodata.buffer2);

		audio_close();
	}

	if (theora_p)
	{
		ogg_stream_clear(&videodata.to);
		theora_clear(&videodata.td);
		theora_comment_clear(&videodata.tc);
		theora_info_clear(&videodata.ti);
		deallocateVideoFrame();
	}

	ogg_sync_clear(&videodata.oy);

	inVideoProvider.reset();

	videoplaying = false;
	seq_setScanlinesDisabled(false);
	Timer_stop();

	audioTime = 0;
	sampletimeOffset = last_time = timer_expire = timer_started = 0;
	basetime = -1;

	debug(LOG_VIDEO, " **** frames = %d dropped = %d ****", frames, dropped);
}

int seq_GetFrameNumber()
{
	return frames;
}

double seq_GetFrameTime()
{
	return videobuf_time;
}

// this controls the size of the video to display on screen
void seq_SetDisplaySize(int sizeX, int sizeY, int posX, int posY)
{
	vertices[0][0] = 0.0f;
	vertices[0][1] = 0.0f;
	vertices[1][0] = sizeX;
	vertices[1][1] = 0.0f;
	vertices[2][0] = 0.0f;
	vertices[2][1] = sizeY;
	vertices[3][0] = sizeX;
	vertices[3][1] = sizeY;

	Scrnvidpos[0] = posX;
	Scrnvidpos[1] = posY;
	Scrnvidpos[2] = 0.0f;

	update_buffers();
}

void seq_setScanlinesDisabled(bool flag)
{
	scanlinesDisabled = flag;
}

bool seq_getScanlinesDisabled()
{
	return scanlinesDisabled;
}

void seq_setScanlineMode(SCANLINE_MODE mode)
{
	use_scanlines = mode;
}

SCANLINE_MODE seq_getScanlineMode(void)
{
	return use_scanlines;
}
