/* 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/>.
 *
 */

#ifndef SCI_SOUND_DECODERS_ROBOT_H
#define SCI_SOUND_DECODERS_ROBOT_H

#include "audio/audiostream.h"           // for AudioStream
#include "audio/rate.h"                  // for st_sample_t
#include "common/array.h"                // for Array
#include "common/mutex.h"                // for StackLock, Mutex
#include "common/rect.h"                 // for Point, Rect (ptr only)
#include "common/scummsys.h"             // for int16, int32, byte, uint16
#include "sci/engine/vm_types.h"         // for NULL_REG, reg_t
#include "sci/graphics/helpers.h"        // for GuiResourceId
#include "sci/graphics/screen_item32.h"  // for ScaleInfo, ScreenItem (ptr o...

namespace Common { class SeekableSubReadStreamEndian; }
namespace Sci {
class Plane;
class SegManager;

// There were 3 different Robot video versions, used in the following games:
// - v4: PQ:SWAT demo
// - v5: KQ7 DOS, Phantasmagoria, PQ:SWAT, Lighthouse
// - v6: RAMA
//
// Notes on Robot v5/v6 format:
//
// Robot is a packetized streaming AV format that encodes multiple bitmaps +
// positioning data, plus synchronised audio, for rendering in the SCI graphics
// system.
//
// Unlike traditional AV formats, Robot videos almost always require playback
// within the game engine because certain information (like the resolution of
// the Robot coordinates and the background for the video) is dependent on data
// that does not exist within the Robot file itself. In version 6, robots could
// also participate in palette remapping by drawing remap pixels, and the
// information for processing these pixels is also not stored within the Robot
// file.
//
// The Robot container consists of a file header, an optional primer audio
// section, an optional colour palette, a frame seek index, a set of cuepoints,
// and variable-sized packets of compressed video+audio data.
//
// Integers in Robot files are coded using native endianness (LSB for x86
// versions, MSB for 68k/PPC versions).
//
// Robot video coding is a relatively simple variable-length compression with no
// interframe compression. Each cel in a frame is constructed from multiple
// contiguous data blocks, each of which can be independently compressed with
// LZS or left uncompressed. An entire cel can also be line decimated, where
// lines are deleted from the source bitmap at compression time and are
// reconstructed by decompression using line interpolation. Each cel also
// includes coordinates where it should be placed within the video frame,
// relative to the top-left corner of the frame.
//
// Audio coding is fixed-length, and all audio blocks except for the primer
// audio are the same size. Audio is encoded with Sierra SOL DPCM16 compression,
// and is split into two channels ('even' and 'odd'), each at a 11025Hz sample
// rate. The original signal is restored by interleaving samples from the two
// channels together. Channel packets are 'even' if they have an ''absolute
// position of audio'' that is evenly divisible by 2; otherwise, they are 'odd'.
// Because the channels use DPCM compression, there is an 8-byte runway at the
// start of every audio block that is never written to the output stream, which
// is used to move the signal to the correct location by the 9th sample.
//
// File header (v5/v6):
//
//  byte | description
//     0 | signature 0x16
//     1 | unused
//   2-5 | signature 'SOL\0'
//   6-7 | version (4, 5, and 6 are the only known versions)
//   8-9 | size of audio blocks
// 10-11 | primer is compressed flag
// 12-13 | unused
// 14-15 | total number of video frames
// 16-17 | embedded palette size, in bytes
// 18-19 | primer reserved size
// 20-21 | coordinate X-resolution (if 0, uses game coordinates)
// 22-23 | coordinate Y-resolution (if 0, uses game coordinates)
//    24 | if non-zero, Robot includes a palette
//    25 | if non-zero, Robot includes audio
// 26-27 | unused
// 28-29 | the frame rate, in frames per second
// 30-31 | coordinate conversion flag; if true, screen item coordinates
//       | from the robot should be used as-is with NO conversion when
//       | explicitly displaying a specific frame
// 32-33 | the maximum number of packets that can be skipped without causing
//       | audio drop-out
// 34-35 | the maximum possible number of cels that will be displayed in any
//       | frame of the robot
// 36-39 | the maximum possible size, in bytes, of the first fixed cel
// 40-43 | the maximum possible size, in bytes, of the second fixed cel
// 44-47 | the maximum possible size, in bytes, of the third fixed cel
// 48-51 | the maximum possible size, in bytes, of the fourth fixed cel
// 52-59 | unused
//
// If the ''file includes audio'' flag is false, seek ''primer reserved size''
// bytes from the end of the file header to get past a padding zone.
//
// If the ''file includes audio'' flag is true, and the ''primer reserved size''
// is not zero, the data immediately after the file header consists of an audio
// primer header plus compressed audio data:
//
// Audio primer header:
//
//  byte | description
//   0-3 | the size, in bytes, of the entire primer audio section
//   4-5 | the compression format of the primer audio (must be zero)
//   6-9 | the size, in bytes, of the "even" primer
// 10-13 | the size, in bytes, of the "odd" primer
//
// If the combined sizes of the even and odd primers do not match the ''primer
// reserved size'', the next header block can be found ''primer reserved size''
// bytes from the *start* of the audio primer header.
//
// Otherwise, if the Robot has audio, and the ''primer reserved size'' is zero,
// and the ''primer is compressed flag'' is set, the "even" primer size is
// 19922, the "odd" primer size is 21024, and the "even" and "odd" buffers
// should be zero-filled.
//
// Any other combination of these flags is an error.
//
// If the Robot has a palette, the next ''palette size'' bytes should be read
// as a SCI HunkPalette. Otherwise, seek ''palette size'' bytes from the current
// position to get to the frame index.
//
// The next section of the Robot is the video frame size index. In version 5
// robots, read ''total number of frames'' 16-bit integers to get the size of
// the compressed video for each frame. For version 6 robots, use 32-bit
// integers.
//
// The next section of the Robot is the packet size index (combined compressed
// size of video + audio for each frame). In version 5 Robots, read ''total
// number of frames'' 16-bit integers. In version 6 robots, use 32-bit integers.
//
// The next section of the Robot is the cue times index. Read 256 32-bit
// integers, which represent the number of ticks from the start of playback that
// the given cue point falls on.
//
// The next section of the Robot is the cue values index. Read 256 16-bit
// integers, which represent the actual cue values that will be passed back to
// the game engine when a cue is requested.
//
// Finally, to get to the first frame packet, seek from the current position to
// the start of the next 2048-byte-aligned sector.
//
// Frame packet:
//
//  byte | description
//  0..n | video data (size is in the ''video frame size index'')
// n+1.. | optional audio data (size is ''size of audio blocks'')
//
// Video data:
//
//  byte | description
//   0-2 | number of cels in the frame (max 10)
//  3..n | cels
//
// Cel:
//
//  0-17 | cel header
// 18..n | data chunks
//
// Cel header:
//
//  byte | description
//     0 | unused
//     1 | vertical scale factor, in percent decimation (100 = no decimation,
//       | 50 = 50% of lines were removed)
//   2-3 | cel width
//   4-5 | cel height
//   6-9 | unused
// 10-11 | cel x-position, in Robot coordinates
// 12-13 | cel y-position, in Robot coordinates
// 14-15 | cel total data chunk size, in bytes
// 16-17 | number of data chunks
//
// Cel data chunk:
//
//   0-9 | cel data chunk header
// 10..n | cel data
//
// Cel data chunk header:
//
//  byte | description
//   0-3 | compressed size
//   4-7 | decompressed size
//   8-9 | compression type (0 = LZS, 2 = uncompressed)
//
// Random frame seeking can be done by calculating the address of the frame
// packet by adding up the ''packet size index'' entries up to the current
// frame. This will normally disable audio playback, as audio data in a packet
// does not correspond to the video in the same packet.
//
// Audio data is placed immediately after the end of the video data in a packet,
// and consists of an audio header plus compressed audio data:
//
// Audio data:
//
//  byte | description
//   0-7 | audio data header
//  8-15 | DPCM runway
// 16..n | compressed audio data
//
// Audio data header:
//
//  byte | description
//   0-3 | absolute position of audio in the audio stream
//   4-7 | the size of the audio block, excluding the header
//
// When a block of audio is processed, first check to ensure that the
// decompressed audio block's `position * 2 + length * 4` runs past the end of
// the last packet of the same evenness/oddness. Discard the audio block
// entirely if data has already been written past the end of this block for this
// channel, or if the read head has already read past the end of this audio
// block.
//
// If the block is not discarded, apply DPCM decompression to the entire block,
// starting from beginning of the DPCM runway, using an initial sample value of
// 0. Then, copy every sample from the decompressed source outside of the DPCM
// runway into every *other* sample of the final audio buffer (1 -> 2, 2 -> 4,
// 3 -> 6, etc.).
//
// Finally, for any skipped samples where the opposing (even/odd) channel did
// not yet write, interpolate the skipped areas by adding together the
// neighbouring samples from this audio block and dividing by two. (This allows
// the audio quality to degrade to 11kHz in case it takes too long to decode all
// the frames in the stream). Interpolated samples must not be written on top of
// true data from the opposing channel. Audio from later packets must also not
// be written on top of data in the same channel that was already written by an
// earlier packet, in particular because the first 8 bytes of the next packet
// are garbage data used to move the waveform to the correct position (due to
// the use of DPCM compression).

#pragma mark -
#pragma mark RobotAudioStream

/**
 * A Robot audio stream is a simple loop buffer that accepts audio blocks from
 * the Robot engine.
 */
class RobotAudioStream : public Audio::AudioStream {
public:
	enum {
		/**
		 * The sample rate used for all robot audio.
		 */
		kRobotSampleRate = 22050,

		/**
		 * Multiplier for the size of a packet that is being expanded by writing
		 * to every other byte of the target buffer.
		 */
		kEOSExpansion    = 2
	};

	/**
	 * Playback state information. Used for framerate calculation.
	 */
	struct StreamState {
		/**
		 * The current position of the read head of the audio stream.
		 */
		int bytesPlaying;

		/**
		 * The sample rate of the audio stream. Always 22050.
		 */
		uint16 rate;

		/**
		 * The bit depth of the audio stream. Always 16.
		 */
		uint8 bits;
	};

	/**
	 * A single packet of compressed audio from a Robot data stream.
	 */
	struct RobotAudioPacket {
		/**
		 * Raw DPCM-compressed audio data.
		 */
		byte *data;

		/**
		 * The size of the compressed audio data,
		 * in bytes.
		 */
		int dataSize;

		/**
		 * The uncompressed, file-relative position of this audio packet.
		 */
		int position;

		RobotAudioPacket(byte *data_, const int dataSize_, const int position_) :
			data(data_), dataSize(dataSize_), position(position_) {}
	};

	RobotAudioStream(const int32 bufferSize);
	~RobotAudioStream() override;

	/**
	 * Adds a new audio packet to the stream.
	 * @returns `true` if the audio packet was fully consumed, otherwise
	 * `false`.
	 */
	bool addPacket(const RobotAudioPacket &packet);

	/**
	 * Prevents any additional audio packets from being added to the audio
	 * stream.
	 */
	void finish();

	/**
	 * Returns the current status of the audio stream.
	 */
	StreamState getStatus() const;

private:
	Common::Mutex _mutex;

	/**
	 * Loop buffer for playback. Contains decompressed 16-bit PCM samples.
	 */
	byte *_loopBuffer;

	/**
	 * The size of the loop buffer, in bytes.
	 */
	int32 _loopBufferSize;

	/**
	 * The position of the read head within the loop buffer, in bytes.
	 */
	int32 _readHead;

	/**
	 * The lowest file position that can be buffered, in uncompressed bytes.
	 */
	int32 _readHeadAbs;

	/**
	 * The highest file position that can be buffered, in uncompressed bytes.
	 */
	int32 _maxWriteAbs;

	/**
	 * The highest file position, in uncompressed bytes, that has been written
	 * to the stream. This is different from `_maxWriteAbs`, which is the
	 * highest uncompressed position which *can* be written right now.
	 */
	int32 _writeHeadAbs;

	/**
	 * The highest file position, in uncompressed bytes, that has been written
	 * to the even & odd sides of the stream.
	 *
	 * Index 0 corresponds to the 'even' side; index 1 corresponds to the 'odd'
	 * side.
	 */
	int32 _jointMin[2];

	/**
	 * When `true`, the stream is waiting for all primer blocks to be received
	 * before allowing playback to begin.
	 */
	bool _waiting;

	/**
	 * When `true`, the stream will accept no more audio blocks.
	 */
	bool _finished;

	/**
	 * The uncompressed position of the first packet of robot data. Used to
	 * decide whether all primer blocks have been received and the stream should
	 * be started.
	 */
	int32 _firstPacketPosition;

	/**
	 * Decompression buffer, used to temporarily store an uncompressed block of
	 * audio data.
	 */
	byte *_decompressionBuffer;

	/**
	 * The size of the decompression buffer, in bytes.
	 */
	int32 _decompressionBufferSize;

	/**
	 * The position of the packet currently in the decompression buffer. Used to
	 * avoid re-decompressing audio data that has already been decompressed
	 * during a partial packet read.
	 */
	int32 _decompressionBufferPosition;

	/**
	 * Calculates the absolute ranges for new fills into the loop buffer.
	 */
	void fillRobotBuffer(const RobotAudioPacket &packet, const int8 bufferIndex);

	/**
	 * Interpolates `numSamples` samples from the read head, if no true samples
	 * were written for one (or both) of the joint channels.
	 */
	void interpolateMissingSamples(const int32 numSamples);

#pragma mark -
#pragma mark RobotAudioStream - AudioStream implementation
public:
	int readBuffer(Audio::st_sample_t *outBuffer, int numSamples) override;
	bool isStereo() const override { return false; };
	int getRate() const override { return 22050; };
	bool endOfData() const override {
		Common::StackLock lock(_mutex);
		return _readHeadAbs >= _writeHeadAbs;
	};
	bool endOfStream() const override {
		Common::StackLock lock(_mutex);
		return _finished && endOfData();
	}
};

#pragma mark -
#pragma mark RobotDecoder

/**
 * RobotDecoder implements the logic required for Robot animations.
 */
class RobotDecoder {
public:
	RobotDecoder(SegManager *segMan);
	~RobotDecoder();

	GuiResourceId getResourceId() const {
		return _robotId;
	}

private:
	SegManager *_segMan;

	/**
	 * The ID of the currently loaded robot.
	 */
	GuiResourceId _robotId;

#pragma mark Constants
public:
	/**
	 * The playback status of the robot.
	 */
	enum RobotStatus {
		kRobotStatusUninitialized = 0,
		kRobotStatusPlaying       = 1,
		kRobotStatusEnd           = 2,
		kRobotStatusPaused        = 3
	};

	enum {
		// Special high value used to represent parameters that should be left
		// unchanged when calling `showFrame`
		kUnspecified = 50000
	};

private:
	enum {
		/**
		 * Maximum number of on-screen screen items.
		 */
		kScreenItemListSize    = 10,

		/**
		 * Maximum number of queued audio blocks.
		 */
		kAudioListSize         = 10,

		/**
		 * Maximum number of samples used for frame timing.
		 */
		kDelayListSize         = 10,

		/**
		 * Maximum number of cues.
		 */
		kCueListSize           = 256,

		/**
		 * Maximum number of 'fixed' cels that never change for the duration of
		 * a robot.
		 */
		kFixedCelListSize      = 4,

		/**
		 * The size of a hunk palette in the Robot stream.
		 */
		kRawPaletteSize        = 1200,

		/**
		 * The size of a frame of Robot data. This value was used to align the
		 * first block of data after the main Robot header to the next CD
		 * sector.
		 */
		kRobotFrameSize        = 2048,

		/**
		 * The size of a block of zero-compressed audio. Used to fill audio when
		 * the size of an audio packet does not match the expected packet size.
		 */
		kRobotZeroCompressSize = 2048,

		/**
		 * The size of the audio block header, in bytes. The audio block header
		 * consists of the compressed size of the audio in the record, plus the
		 * position of the audio in the compressed data stream.
		 */
		kAudioBlockHeaderSize  = 8,

		/**
		 * The size of a Robot cel header, in bytes.
		 */
		kCelHeaderSize         = 22,

		/**
		 * The maximum amount that the frame rate is allowed to drift from the
		 * nominal frame rate in order to correct for AV drift or slow playback.
		 */
		kMaxFrameRateDrift     = 1
	};

	/**
	 * The version number for the currently loaded robot.
	 *
	 * There are several known versions of robot:
	 *
	 * v2: before Nov 1994; no known examples
	 * v3: before Nov 1994; no known examples
	 * v4: Jan 1995; KQ7 1.65, PQ:SWAT demo
	 * v5: Mar 1995; SCI2.1 and SCI3 games
	 * v6: SCI3 games
	 */
	uint16 _version;

#pragma mark -
#pragma mark Initialisation
private:
	/**
	 * Sets up the read stream for the robot.
	 */
	void initStream(const GuiResourceId robotId);

	/**
	 * Sets up the initial values for playback control.
	 */
	void initPlayback();

	/**
	 * Sets up the initial values for audio decoding.
	 */
	void initAudio();

	/**
	 * Sets up the initial values for video rendering.
	 */
	void initVideo(const int16 x, const int16 y, const int16 scale, const reg_t plane, const bool hasPalette, const uint16 paletteSize);

	/**
	 * Sets up the robot's data record and cue positions.
	 */
	void initRecordAndCuePositions();

#pragma mark -
#pragma mark Playback
public:
	/**
	 * Opens a robot file for playback. Newly opened robots are paused by
	 * default.
	 */
	void open(const GuiResourceId robotId, const reg_t plane, const int16 priority, const int16 x, const int16 y, const int16 scale);

	/**
	 * Closes the currently open robot file.
	 */
	void close();

	/**
	 * Pauses the robot. Once paused, the audio for a robot is disabled until
	 * the end of playback.
	 */
	void pause();

	/**
	 * Resumes a paused robot.
	 */
	void resume();

	/**
	 * Moves robot to the specified frame and pauses playback.
	 *
	 * @note Called DisplayFrame in SSCI.
	 */
	void showFrame(const uint16 frameNo, const uint16 newX, const uint16 newY, const uint16 newPriority);

	/**
	 * Retrieves the value associated with the current cue point.
	 */
	int16 getCue() const;

	/**
	 * Gets the currently displayed frame.
	 */
	int16 getFrameNo() const;

	/**
	 * Gets the playback status of the player.
	 */
	RobotStatus getStatus() const;

private:
	/**
	 * The read stream containing raw robot data.
	 */
	Common::SeekableSubReadStreamEndian *_stream;

	/**
	 * The current status of the player.
	 */
	RobotStatus _status;

	typedef Common::Array<int> PositionList;

	/**
	 * A map of frame numbers to byte offsets within `_stream`.
	 */
	PositionList _recordPositions;

	/**
	 * The offset of the Robot file within a resource bundle.
	 */
	int32 _fileOffset;

	/**
	 * A list of cue times that is updated to prevent earlier cue values from
	 * being given to the game more than once.
	 */
	mutable int32 _cueTimes[kCueListSize];

	/**
	 * The original list of cue times from the raw Robot data.
	 */
	int32 _masterCueTimes[kCueListSize];

	/**
	 * The list of values to provide to a game when a cue value is requested.
	 */
	int32 _cueValues[kCueListSize];

	/**
	 * The current playback frame rate.
	 */
	int16 _frameRate;

	/**
	 * The nominal playback frame rate.
	 */
	int16 _normalFrameRate;

	/**
	 * The minimal playback frame rate. Used to correct for AV sync drift when
	 * the video is more than one frame ahead of the audio.
	 */
	int16 _minFrameRate;

	/**
	 * The maximum playback frame rate. Used to correct for AV sync drift when
	 * the video is more than one frame behind the audio.
	 */
	int16 _maxFrameRate;

	/**
	 * The maximum number of record blocks that can be skipped without causing
	 * audio to drop out.
	 */
	int16 _maxSkippablePackets;

	/**
	 * The currently displayed frame number.
	 */
	int _currentFrameNo;

	/**
	 * The last displayed frame number.
	 */
	int _previousFrameNo;

	/**
	 * The time, in ticks, when the robot was last started or resumed.
	 */
	int32 _startTime;

	/**
	 * The first frame displayed when the robot was resumed.
	 */
	int32 _startFrameNo;

	/**
	 * The last frame displayed when the robot was resumed.
	 */
	int32 _startingFrameNo;

	/**
	 * Seeks the raw data stream to the record for the given frame number.
	 */
	bool seekToFrame(const int frameNo);

	/**
	 * Sets the start time and frame of the robot when the robot is started or
	 * resumed.
	 */
	void setRobotTime(const int frameNo);

#pragma mark -
#pragma mark Timing
private:
	/**
	 * This class tracks the amount of time it takes for a frame of robot
	 * animation to be rendered. This information is used by the player to
	 * speculatively skip rendering of future frames to keep the animation in
	 * sync with the robot audio.
	 */
	class DelayTime {
	public:
		DelayTime(RobotDecoder *decoder);

		/**
		 * Starts performance timing.
		 */
		void startTiming();

		/**
		 * Ends performance timing.
		 */
		void endTiming();

		/**
		 * Returns whether or not timing is currently in progress.
		 */
		bool timingInProgress() const;

		/**
		 * Returns the median time, in ticks, of the currently stored timing
		 * samples.
		 */
		int predictedTicks() const;

	private:
		RobotDecoder *_decoder;

		/**
		 * The start time, in ticks, of the current timing loop. If no loop is
		 * in progress, the value is 0.
		 *
		 * @note This is slightly different than SSCI where the not-timing value
		 * was -1.
		 */
		uint32 _startTime;

		/**
		 * A sorted list containing the timing data for the last
		 * `kDelayListSize` frames, in ticks.
		 */
		int _delays[kDelayListSize];

		/**
		 * A list of monotonically increasing identifiers used to identify and
		 * replace the oldest sample in the `_delays` array when finishing the
		 * next timing operation.
		 */
		uint _timestamps[kDelayListSize];

		/**
		 * The identifier of the oldest timing.
		 */
		uint _oldestTimestamp;

		/**
		 * The identifier of the newest timing.
		 */
		uint _newestTimestamp;

		/**
		 * Sorts the list of timings.
		 */
		void sortList();
	};

	/**
	 * Calculates the next frame number that needs to be rendered, using the
	 * timing data collected by DelayTime.
	 */
	uint16 calculateNextFrameNo(const uint32 extraTicks = 0) const;

	/**
	 * Calculates and returns the number of frames that should be rendered in
	 * `ticks` time, according to the current target frame rate of the robot.
	 */
	uint32 ticksToFrames(const uint32 ticks) const;

	/**
	 * Gets the current game time, in ticks.
	 */
	uint32 getTickCount() const;

	/**
	 * The performance timer for the robot.
	 */
	DelayTime _delayTime;

#pragma mark -
#pragma mark Audio
private:
	enum {
		/**
		 * The number of ticks that should elapse between each AV sync check.
		 */
		kAudioSyncCheckInterval = 5 * 60 /* 5 seconds */
	};

	/**
	 * The status of the audio track of a Robot animation.
	 */
	enum RobotAudioStatus {
		kRobotAudioReady    = 1,
		kRobotAudioStopped  = 2,
		kRobotAudioPlaying  = 3,
		kRobotAudioPaused   = 4,
		kRobotAudioStopping = 5
	};

#pragma mark -
#pragma mark Audio - AudioList
private:
	/**
	 * This class manages packetized audio playback for robots.
	 */
	class AudioList {
	public:
		AudioList();

		/**
		 * Starts playback of robot audio.
		 */
		void startAudioNow();

		/**
		 * Stops playback of robot audio, allowing any queued audio to finish
		 * playing back.
		 */
		void stopAudio();

		/**
		 * Stops playback of robot audio immediately.
		 */
		void stopAudioNow();

		/**
		 * Submits as many blocks of audio as possible to the audio engine.
		 */
		void submitDriverMax();

		/**
		 * Adds a new AudioBlock to the queue.
		 *
		 * @param position The absolute position of the audio for the block, in
		 *                 compressed bytes.
		 * @param size The size of the buffer.
		 * @param buffer A pointer to compressed audio data that will be copied
		 *               into the new AudioBlock.
		 */
		void addBlock(const int position, const int size, const byte *buffer);

		/**
		 * Immediately stops any active playback and purges all audio data in
		 * the audio list.
		 */
		void reset();

		/**
		 * Pauses the robot audio channel in preparation for the first block of
		 * audio data to be read.
		 */
		void prepareForPrimer();

		/**
		 * Sets the audio offset which is used to offset the position of audio
		 * packets sent to the audio stream.
		 */
		void setAudioOffset(const int offset);

#pragma mark -
#pragma mark Audio - AudioList - AudioBlock

	private:
		/**
		 * AudioBlock represents a block of audio from the Robot's audio track.
		 */
		class AudioBlock {
		public:
			AudioBlock(const int position, const int size, const byte *const data);
			~AudioBlock();

			/**
			 * Submits the block of audio to the audio manager.
			 * @returns true if the block was fully read, or false if the block
			 * was not read or only partially read.
			 */
			bool submit(const int startOffset);

		private:
			/**
			 * The absolute position, in compressed bytes, of this audio block's
			 * audio data in the audio stream.
			 */
			int _position;

			/**
			 * The compressed size, in bytes, of this audio block's audio data.
			 */
			int _size;

			/**
			 * A buffer containing raw SOL-compressed audio data.
			 */
			byte *_data;
		};

		/**
		 * The list of compressed audio blocks submitted for playback.
		 */
		AudioBlock *_blocks[kAudioListSize];

		/**
		 * The number of blocks in `_blocks` that are ready to be submitted.
		 */
		uint8 _blocksSize;

		/**
		 * The index of the oldest submitted audio block.
		 */
		uint8 _oldestBlockIndex;

		/**
		 * The index of the newest submitted audio block.
		 */
		uint8 _newestBlockIndex;

		/**
		 * The offset used when sending packets to the audio stream.
		 */
		int _startOffset;

		/**
		 * The status of robot audio playback.
		 */
		RobotAudioStatus _status;

		/**
		 * Frees all audio blocks in the `_blocks` list.
		 */
		void freeAudioBlocks();
	};

	/**
	 * Whether or not this robot animation has
	 * an audio track.
	 */
	bool _hasAudio;

	/**
	 * The audio list for the current robot.
	 */
	AudioList _audioList;

	/**
	 * The size, in bytes, of a block of audio data, excluding the audio block
	 * header.
	 */
	uint16 _audioBlockSize;

	/**
	 * The expected size of a block of audio data, in bytes, excluding the audio
	 * block header.
	 */
	int16 _expectedAudioBlockSize;

	/**
	 * The number of compressed audio bytes that are needed per frame to fill
	 * the audio buffer without causing audio to drop out.
	 */
	int16 _audioRecordInterval;

	/**
	 * If true, primer audio buffers should be filled with silence instead of
	 * trying to read buffers from the Robot data.
	 */
	uint16 _primerZeroCompressFlag;

	/**
	 * The size, in bytes, of the primer audio in the Robot, including any extra
	 * alignment padding.
	 */
	uint16 _primerReservedSize;

	/**
	 * The combined size, in bytes, of the even and odd primer channels.
	 */
	int32 _totalPrimerSize;

	/**
	 * The absolute offset of the primer audio data in the robot data stream.
	 */
	int32 _primerPosition;

	/**
	 * The size, in bytes, of the even primer.
	 */
	int32 _evenPrimerSize;

	/**
	 * The size, in bytes, of the odd primer.
	 */
	int32 _oddPrimerSize;

	/**
	 * The absolute position in the audio stream of the first audio packet.
	 */
	int32 _firstAudioRecordPosition;

	/**
	 * A temporary buffer used to hold one frame of raw (DPCM-compressed) audio
	 * when reading audio records from the robot stream.
	 */
	byte *_audioBuffer;

	/**
	 * The next tick count when AV sync should be checked and framerate
	 * adjustments made, if necessary.
	 */
	uint32 _checkAudioSyncTime;

	/**
	 * Primes the audio buffer with the first frame of audio data.
	 *
	 * @note `primeAudio` was `InitAudio` in SSCI
	 */
	bool primeAudio(const uint32 startTick);

	/**
	 * Reads primer data from the robot data stream and puts it into the given
	 * buffers.
	 */
	bool readPrimerData(byte *outEvenBuffer, byte *outOddBuffer);

	/**
	 * Reads audio data for the given frame number into the given buffer.
	 *
	 * @param outAudioPosition The position of the audio, in compressed bytes,
	 *                         in the data stream.
	 * @param outAudioSize The size of the audio data, in compressed bytes.
	 */
	bool readAudioDataFromRecord(const int frameNo, byte *outBuffer, int &outAudioPosition, int &outAudioSize);

	/**
	 * Submits part of the audio packet of the given frame to the audio list,
	 * starting `startPosition` bytes into the audio.
	 */
	bool readPartialAudioRecordAndSubmit(const int startFrame, const int startPosition);

#pragma mark -
#pragma mark Rendering
public:
	/**
	 * Gets the plane used to render the robot.
	 */
	const reg_t getPlaneId() const {
		return _planeId;
	}

	/**
	 * Gets the origin of the robot.
	 */
	Common::Point getPosition() const {
		return _position;
	}

	/**
	 * Gets the scale of the robot.
	 */
	int16 getScale() const {
		return _scaleInfo.x;
	}

	/**
	 * Puts the current dimensions of the robot, in game script coordinates,
	 * into the given rect, and returns the total number of frames in the robot
	 * animation.
	 */
	uint16 getFrameSize(Common::Rect &outRect) const;

	/**
	 * Pumps the robot player for the next frame of video. This is the main
	 * rendering function.
	 */
	void doRobot();

	/**
	 * Submits any outstanding audio blocks that should be added to the queue
	 * before the robot frame becomes visible.
	 */
	void frameAlmostVisible();

	/**
	 * Evaluates frame drift and makes modifications to the player in order to
	 * ensure that future frames will arrive on time.
	 */
	void frameNowVisible();

	/**
	 * Scales a vertically compressed cel to its original uncompressed
	 * dimensions.
	 */
	void expandCel(byte *target, const byte* source, const int16 celWidth, const int16 celHeight) const;

	int16 getPriority() const;

	/**
	 * Sets the visual priority of the robot.
	 * @see Plane::_priority
	 */
	void setPriority(const int16 newPriority);

private:
	enum CompressionType {
		kCompressionLZS  = 0,
		kCompressionNone = 2
	};

	/**
	 * Describes the state of a Robot video cel.
	 */
	struct CelHandleInfo {
		/**
		 * The persistence level of Robot cels.
		 */
		enum CelHandleLifetime {
			kNoCel         = 0,
			kFrameLifetime = 1,
			kRobotLifetime = 2
		};

		/**
		 * A reg_t pointer to an in-memory bitmap containing the cel.
		 */
		reg_t bitmapId;

		/**
		 * The lifetime of the cel, either just for this frame or for the entire
		 * duration of the robot playback.
		 */
		CelHandleLifetime status;

		/**
		 * The size, in pixels, of the decompressed cel.
		 */
		int area;

		CelHandleInfo() : bitmapId(NULL_REG), status(kNoCel), area(0) {}
	};

	typedef Common::Array<ScreenItem *> RobotScreenItemList;
	typedef Common::Array<CelHandleInfo> CelHandleList;
	typedef Common::Array<int> VideoSizeList;
	typedef Common::Array<uint> MaxCelAreaList;
	typedef Common::Array<reg_t> FixedCelsList;
	typedef Common::Array<Common::Point> CelPositionsList;
	typedef Common::Array<byte> ScratchMemory;

	/**
	 * Renders a version 5/6 robot frame.
	 */
	void doVersion5(const bool shouldSubmitAudio = true);

	/**
	 * Creates screen items for a version 5/6 robot.
	 */
	void createCels5(const byte *rawVideoData, const int16 numCels, const bool usePalette);

	/**
	 * Creates a single screen item for a cel in a version 5/6 robot.
	 *
	 * Returns the size, in bytes, of the raw cel data.
	 */
	uint32 createCel5(const byte *rawVideoData, const int16 screenItemIndex, const bool usePalette);

	/**
	 * Preallocates memory for the next `numCels` cels in the robot data stream.
	 */
	void preallocateCelMemory(const byte *rawVideoData, const int16 numCels);

	/**
	 * The decompressor for LZS-compressed cels.
	 */
	DecompressorLZS _decompressor;

	/**
	 * The ID of the robot plane.
	 */
	reg_t _planeId;

	/**
	 * The origin of the robot animation, in screen coordinates.
	 */
	Common::Point _position;

	/**
	 * Global scaling applied to the robot.
	 */
	ScaleInfo _scaleInfo;

	/**
	 * The native resolution of the robot.
	 */
	int16 _xResolution, _yResolution;

	/**
	 * Whether or not the coordinates read from robot data are high resolution.
	 */
	bool _isHiRes;

	/**
	 * The maximum number of cels that will be rendered on any given frame in
	 * this robot. Used for preallocation of cel memory.
	 */
	int16 _maxCelsPerFrame;

	/**
	 * The maximum areas, in pixels, for each of the fixed cels in the robot.
	 * Used for preallocation of cel memory.
	 */
	MaxCelAreaList _maxCelArea;

	/**
	 * The hunk palette to use when rendering the current frame, if the
	 * `usePalette` flag was set in the robot header.
	 */
	uint8 *_rawPalette;

	/**
	 * A list of the raw video data sizes, in bytes, for each frame of the
	 * robot.
	 */
	VideoSizeList _videoSizes;

	/**
	 * A list of cels that will be present for the entire duration of the robot
	 * animation.
	 */
	FixedCelsList _fixedCels;

	/**
	 * A list of handles for each cel in the current frame.
	 */
	CelHandleList _celHandles;

	/**
	 * Scratch memory used to temporarily store decompressed cel data for
	 * vertically squashed cels.
	 */
	ScratchMemory _celDecompressionBuffer;

	/**
	 * The size, in bytes, of the squashed cel decompression buffer.
	 */
	int _celDecompressionArea;

	/**
	 * If true, the robot just started playing and is awaiting output for the
	 * first frame.
	 */
	bool _syncFrame;

	/**
	 * Scratch memory used to store the compressed robot video data for the
	 * current frame.
	 */
	ScratchMemory _doVersion5Scratch;

	/**
	 * When set to a non-negative value, forces the next call to doRobot to
	 * render the given frame number instead of whatever frame would have
	 * normally been rendered.
	 */
	mutable int _cueForceShowFrame;

	/**
	 * The plane where the robot animation will be drawn.
	 */
	Plane *_plane;

	/**
	 * A list of pointers to ScreenItems used by the robot.
	 */
	RobotScreenItemList _screenItemList;

	/**
	 * The positions of the various screen items in this robot, in screen
	 * coordinates.
	 */
	Common::Array<int16> _screenItemX, _screenItemY;

	/**
	 * The raw position values from the cel header for each screen item
	 * currently on-screen.
	 */
	Common::Array<int16> _originalScreenItemX, _originalScreenItemY;

	/**
	 * The duration of the current robot, in frames.
	 */
	uint16 _numFramesTotal;

	/**
	 * The screen priority of the video.
	 * @see ScreenItem::_priority
	 */
	int16 _priority;

	/**
	 * The amount of visual vertical compression applied to the current cel. A
	 * value of 100 means no compression; a value above 100 indicates how much
	 * the cel needs to be scaled along the y-axis to return to its original
	 * dimensions.
	 */
	uint8 _verticalScaleFactor;
};
} // end of namespace Sci
#endif
