/*
Copyright (C) 1997-2008 ZSNES Team ( zsKnight, _Demo_, pagefault, Nach )

http://www.zsnes.com
http://sourceforge.net/projects/zsnes
https://zsnes.bountysource.com

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
version 2 as published by the Free Software Foundation.

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, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

#include "types.h" /* IGNORE_RESULT */

#ifdef __UNIXSDL__
#include "gblhdr.h"
#include <signal.h>
#define WRITE_BINARY "w"
#define NULL_FILE "/dev/null"
#include "unix/safelib.h"
#else
#define _POSIX_
#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <zlib.h>
#define signal(x, y)
#ifdef __WIN32__
#include <direct.h>
#undef _POSIX_
#include <io.h>
#define ftruncate chsize
#include "win/safelib.h"
#endif
#define WRITE_BINARY "wb"
#define NULL_FILE "nul"
#endif
#ifndef _MSC_VER
#include <unistd.h>
#endif
#include "c_init.h"
#include "cfg.h"
#include "cpu/dspproc.h"
#include "gui/c_guiwindp.h"
#include "gui/guimisc.h"
#include "initc.h"
#include "input.h"
#include "md.h"
#include "ui.h"
#include "video/procvid.h"
#include "zdir.h"
#include "zmovie.h"
#include "zpath.h"
#include "zstate.h"

#define NUMCONV_FR2
#define NUMCONV_FR3
#define NUMCONV_FR4
#define NUMCONV_FW2
#define NUMCONV_FW3
#define NUMCONV_FW4
#include "numconv.h"

extern uint32_t versionNumber, CRC32;
extern size_t cur_zst_size; /* size_t in zstate.c */
extern uint8_t GUIReset, GUIQuit;
bool MovieWaiting = false;

extern uint8_t device1, device2;
extern uint16_t latchx, latchy;
#define IS_MOUSE_1() (device1 == 1)
#define IS_MOUSE_2() (device2 == 1)
#define IS_SCOPE() (device2 == 2)

#define SET_MOUSE_1(x) (device1 = (x) ? 1 : 0)
#define SET_MOUSE_2(x) (device2 = (x) ? 1 : 0)
#define SET_SCOPE(x) (device2 = (x) ? 2 : 0)

void powercycle(bool, bool);
void zst_sram_load(FILE*);
void zst_sram_load_compressed(FILE*);
void zst_save(FILE*, bool, bool);
bool zst_load(FILE*, size_t);
bool zst_compressed_loader(FILE*);

u1 lameExists = 0;
u1 mencoderExists = 0;

#ifdef DEBUG_INPUT
#define debug_input_start useda = usedb = usedc = usedd = usede = 0;
#define debug_input              \
    print_bin(JoyAOrig, &useda); \
    printf(" ");                 \
    print_bin(JoyBOrig, &usedb); \
    printf(" ");                 \
    print_bin(JoyCOrig, &usedc); \
    printf(" ");                 \
    print_bin(JoyDOrig, &usedd); \
    printf(" ");                 \
    print_bin(JoyEOrig, &usede); \
    printf("\n");

static uint32_t useda, usedb, usedc, usedd, usede;
static void print_bin(uint32_t num, uint32_t* used)
{
    uint_fast32_t mask = BIT(31);
    while (mask) {
        *used |= num & mask;
        printf(num & mask ? "1" : "0");
        mask >>= 1;
    }
}

#define debug_input_used      \
    printf("Used: ");         \
    print_bin(useda, &useda); \
    printf(" ");              \
    print_bin(usedb, &usedb); \
    printf(" ");              \
    print_bin(usedc, &usedc); \
    printf(" ");              \
    print_bin(usedd, &usedd); \
    printf(" ");              \
    print_bin(usede, &usede); \
    printf("\n");

#else
#define debug_input_start
#define debug_input
#define debug_input_used
#endif

/*
ZMV Format

Header

3 bytes  -  "ZMV"
2 bytes  -  ZMV Version # (version of ZSNES)
4 bytes  -  CRC32 of ROM
4 bytes  -  Number of frames in this movie
4 bytes  -  Number of rerecords
4 bytes  -  Number of frames removed by rerecord
4 bytes  -  Number of frames advanced step-by-step
1 byte   -  Average recording FPS (includes dropped frames) x4-5 for precision
4 bytes  -  Number of key combos
2 bytes  -  Number of internal chapters
2 bytes  -  Length of author name
3 bytes  -  Uncompressed ZST size
2 bytes  -  Initial input configuration
  1 bit  -   Input 1 enabled
  1 bit  -   Input 2 enabled
  1 bit  -   Input 3 enabled
  1 bit  -   Input 4 enabled
  1 bit  -   Input 5 enabled
  1 bit  -   Mouse in first port
  1 bit  -   Mouse in second port
  1 bit  -   Super Scope in second port
  8 bits -   Reserved
1 byte   -  Flag Byte
  2 bits -   Start from ZST/Power On/Reset/Power On + Clear SRAM
  1 bit  -   NTSC or PAL
  5 bits -   Reserved
3 bytes  -  1 bit for compressed or not, 23 bits for size
ZST size -  ZST (no thumbnail)

Key input  -  Repeated for all input / internal chapters

1 byte   - Flag Byte
  1 bit  -  Controller 1 changed
  1 bit  -  Controller 2 changed
  1 bit  -  Controller 3 changed
  1 bit  -  Controller 4 changed
  1 bit  -  Controller 5 changed
  1 bit  -  Chapter instead of input here
  1 bit  -  RLE instead of input
  1 bit  -  Command here

-If Command-
Remaining 7 bits of flag determine command

-Else If RLE-
4 bytes  -  Frame # to repeat previous input to

-Else If Chapter-

3 bytes  -  1 bit for compressed or not, 23 bits for size
ZST size -  ZST
4 bytes  -  Frame #
2 bytes  -  Controller Status
9 bytes  -  Maximum previous input (1 Scope [20] + 4 Regular [12*4] + 4 padded bits)

-Else-

variable - Input

  12 bits per regular controller, 18 per mouse, 20 for scope.
     Input changed padded to next full byte size
  Minimum 2 bytes (12 controller bits + 4 padded bits)
  Maximum 9 bytes (20 scope controller bits + 48 regular controller bits [12*4] + 4 padded bits)

Internal chapter offsets  -  Repeated for all internal chapters

4 bytes  -  Offset to chapter from beginning of file (after input flag byte for ZST)

External chapters  -  Repeated for all external chapters

ZST Size -  ZST (never compressed)
4 bytes  -  Frame #
2 bytes  -  Controller Status
9 bytes  -  Maximum previous input (1 Scope [20] + 4 Regular [12*4] + 4 padded bits)
4 bytes  -  Offset to input for current chapter from beginning of file

External chapter count

2 bytes  - Number of external chapters

Author name

Name Len - Author's name

*/

/* ZMV header types, vars, and functions */

enum zmv_start_methods { zmv_sm_zst,
    zmv_sm_power,
    zmv_sm_reset,
    zmv_sm_clear_all };
enum zmv_video_modes { zmv_vm_ntsc,
    zmv_vm_pal };
enum zmv_commands { zmv_command_reset };

#define INT_CHAP_SIZE(offset) (internal_chapter_length(offset) + 4 + 2 + 9)
#define EXT_CHAP_SIZE (cur_zst_size + 4 + 2 + 9 + 4)

#define INT_CHAP_INDEX_SIZE (zmv_vars.header.internal_chapters * 4)
#define EXT_CHAP_BLOCK_SIZE (zmv_open_vars.external_chapter_count * EXT_CHAP_SIZE + 2)

#define EXT_CHAP_END_DIST (EXT_CHAP_BLOCK_SIZE + (size_t)zmv_vars.header.author_len)
#define INT_CHAP_END_DIST (INT_CHAP_INDEX_SIZE + EXT_CHAP_END_DIST)

#define EXT_CHAP_COUNT_END_DIST ((size_t)zmv_vars.header.author_len + 2)

struct zmv_header {
    char magic[3];
    uint16_t zsnes_version;
    uint32_t rom_crc32;
    uint32_t frames;
    uint32_t rerecords;
    uint32_t removed_frames;
    uint32_t incr_frames;
    uint8_t average_fps;
    uint32_t key_combos;
    uint16_t internal_chapters;
    uint16_t author_len;
    uint32_t zst_size; // We only read/write 3 bytes for this
    uint16_t initial_input;
    struct
    {
        enum zmv_start_methods start_method;
        enum zmv_video_modes video_mode;
    } zmv_flag;
};

static void zmv_header_write(struct zmv_header* zmv_head, FILE* fp)
{
    uint8_t flag = 0;

    fwrite(zmv_head->magic, 3, 1, fp);
    fwrite2(zmv_head->zsnes_version, fp);
    fwrite4(zmv_head->rom_crc32, fp);
    fwrite4(zmv_head->frames, fp);
    fwrite4(zmv_head->rerecords, fp);
    fwrite4(zmv_head->removed_frames, fp);
    fwrite4(zmv_head->incr_frames, fp);
    fwrite(&zmv_head->average_fps, 1, 1, fp);
    fwrite4(zmv_head->key_combos, fp);
    fwrite2(zmv_head->internal_chapters, fp);
    fwrite2(zmv_head->author_len, fp);
    fwrite3(zmv_head->zst_size, fp);
    fwrite2(zmv_head->initial_input, fp);

    switch (zmv_head->zmv_flag.start_method) {
    case zmv_sm_zst:
        flag &= ~BIT(7);
        flag &= ~BIT(6);
        break;

    case zmv_sm_power:
        flag |= BIT(7);
        flag &= ~BIT(6);
        break;

    case zmv_sm_reset:
        flag &= ~BIT(7);
        flag |= BIT(6);
        break;

    case zmv_sm_clear_all:
        flag |= BIT(7);
        flag |= BIT(6);
        break;
    }

    switch (zmv_head->zmv_flag.video_mode) {
    case zmv_vm_ntsc:
        flag &= ~BIT(5);
        break;

    case zmv_vm_pal:
        flag |= BIT(5);
        break;
    }

    // Not needed, but oh well, it makes it easier to read for some.
    // Reserved bits:
    flag &= ~BIT(4);
    flag &= ~BIT(3);
    flag &= ~BIT(2);
    flag &= ~BIT(1);
    flag &= ~BIT(0);

    fwrite(&flag, 1, 1, fp);
}

static bool zmv_header_read(struct zmv_header* zmv_head, FILE* fp)
{
    uint8_t flag;

    IGNORE_RESULT(fread(zmv_head->magic, 3, 1, fp));
    zmv_head->zsnes_version = fread2(fp);
    zmv_head->rom_crc32 = fread4(fp);
    zmv_head->frames = fread4(fp);
    zmv_head->rerecords = fread4(fp);
    zmv_head->removed_frames = fread4(fp);
    zmv_head->incr_frames = fread4(fp);
    IGNORE_RESULT(fread(&zmv_head->average_fps, 1, 1, fp));
    zmv_head->key_combos = fread4(fp);
    zmv_head->internal_chapters = fread2(fp);
    zmv_head->author_len = fread2(fp);
    zmv_head->zst_size = fread3(fp);
    zmv_head->initial_input = fread2(fp);
    IGNORE_RESULT(fread(&flag, 1, 1, fp));

    if (feof(fp)) {
        return (false);
    }

    switch (flag & (BIT(7) | BIT(6))) {
    case 0:
        zmv_head->zmv_flag.start_method = zmv_sm_zst;
        break;

    case BIT(7):
        zmv_head->zmv_flag.start_method = zmv_sm_power;
        break;

    case BIT(6):
        zmv_head->zmv_flag.start_method = zmv_sm_reset;
        break;

    case BIT(7) | BIT(6):
        zmv_head->zmv_flag.start_method = zmv_sm_clear_all;
        break;
    }

    switch (flag & BIT(5)) {
    case 0:
        zmv_head->zmv_flag.video_mode = zmv_vm_ntsc;
        break;

    case BIT(5):
        zmv_head->zmv_flag.video_mode = zmv_vm_pal;
        break;
    }

    if (flag & (BIT(4) | BIT(3) | BIT(2) | BIT(1) | BIT(0))) {
        return (false);
    }

    return (true);
}

/* Internal chapter types, vars, and functions */

#define INTERNAL_CHAPTER_BUF_LIM 16
struct internal_chapter_buf {
    size_t offsets[INTERNAL_CHAPTER_BUF_LIM];
    uint8_t used;
    struct internal_chapter_buf* next;
};

static void internal_chapter_add_offset(struct internal_chapter_buf* icb, size_t offset)
{
    while (icb->next) {
        icb = icb->next;
    }

    if (icb->used == INTERNAL_CHAPTER_BUF_LIM) {
        icb->next = (struct internal_chapter_buf*)malloc(sizeof(struct internal_chapter_buf));
        icb = icb->next;
        memset(icb, 0, sizeof(struct internal_chapter_buf));
    }

    icb->offsets[icb->used] = offset;
    icb->used++;
}

static void internal_chapter_free_chain(struct internal_chapter_buf* icb)
{
    if (icb) {
        if (icb->next) {
            internal_chapter_free_chain(icb->next);
        }
        free(icb);
    }
}

static void internal_chapter_write(struct internal_chapter_buf* icb, FILE* fp)
{
    uint_fast8_t i;
    for (i = 0; i < icb->used; i++) {
        fwrite4(icb->offsets[i], fp);
    }
    if (icb->next) {
        internal_chapter_write(icb->next, fp);
    }
}

static void internal_chapter_read(struct internal_chapter_buf* icb, FILE* fp, size_t count)
{
    while (count--) {
        internal_chapter_add_offset(icb, fread4(fp));
    }
}

size_t internal_chapter_pos(struct internal_chapter_buf* icb, size_t offset)
{
    size_t pos = 0;
    do {
        uint_fast8_t i;
        for (i = 0; i < icb->used; i++, pos++) {
            if (icb->offsets[i] == offset) {
                return (pos);
            }
        }
    } while ((icb = icb->next));
    return (~0);
}

static size_t internal_chapter_greater(struct internal_chapter_buf* icb, size_t offset)
{
    size_t greater = ~0;
    do {
        uint_fast8_t i;
        for (i = 0; i < icb->used; i++) {
            if ((icb->offsets[i] > offset) && (icb->offsets[i] < greater)) {
                greater = icb->offsets[i];
            }
        }
    } while ((icb = icb->next));
    return ((greater == 0xFFFFFFFF) ? offset : greater);
}

static size_t internal_chapter_lesser(struct internal_chapter_buf* icb, size_t offset)
{
    size_t lesser = 0;
    do {
        uint_fast8_t i;
        for (i = 0; i < icb->used; i++) {
            if ((icb->offsets[i] < offset) && (icb->offsets[i] > lesser)) {
                lesser = icb->offsets[i];
            }
        }
    } while ((icb = icb->next));
    return ((lesser == 0) ? offset : lesser);
}

static size_t internal_chapter_delete_after(struct internal_chapter_buf* icb, size_t offset)
{
    if (icb->used) {
        size_t last_offset = 0;
        if (icb->offsets[0] == offset) {
            last_offset = offset;
        } else {
            last_offset = internal_chapter_lesser(icb, offset);
            if (last_offset == offset) {
                internal_chapter_free_chain(icb->next);
                icb->next = 0;
                icb->used = 0;
                return (0);
            }
            if (internal_chapter_greater(icb, last_offset) == offset) {
                last_offset = offset;
            }
        }

        if (last_offset) {
            size_t buffer_pos = internal_chapter_pos(icb, last_offset);
            size_t link = buffer_pos / INTERNAL_CHAPTER_BUF_LIM;

            while (link--) {
                icb = icb->next;
            }

            internal_chapter_free_chain(icb->next);
            icb->next = 0;
            icb->used = (buffer_pos % INTERNAL_CHAPTER_BUF_LIM) + 1;
            return (buffer_pos + 1);
        }
    }
    return (0);
}

static size_t internal_chapter_count_until(struct internal_chapter_buf* icb, size_t offset)
{
    size_t chapter_count = 0;
    uint_fast8_t i;

    do {
        for (i = 0; i < icb->used; i++) {
            if (icb->offsets[i] >= offset) {
                break;
            }
            chapter_count++;
        }
    } while ((i == icb->used) && (icb = icb->next));
    return (chapter_count);
}

/* Bit Encoder and Decoder */

/* Bit n lives in byte n>>3, at bit 7 - (n & 7). */
size_t bit_encoder(uint32_t data, uint32_t mask, uint8_t* buffer, size_t skip_bits)
{
    uint_fast8_t bit_loop;

    for (bit_loop = 31;; bit_loop--) {
        if (mask & BIT(bit_loop)) {
            if (data & BIT(bit_loop)) {
                buffer[skip_bits >> 3] |= BIT(7 - (skip_bits & 7));
            } else {
                buffer[skip_bits >> 3] &= ~BIT(7 - (skip_bits & 7));
            }
            skip_bits++;
        }

        if (!bit_loop) {
            break;
        }
    }

    return (skip_bits);
}

size_t bit_decoder(uint32_t* data, uint32_t mask, uint8_t* buffer, size_t skip_bits)
{
    uint_fast8_t bit_loop;
    *data = 0;

    for (bit_loop = 31;; bit_loop--) {
        if (mask & BIT(bit_loop)) {
            if (buffer[skip_bits >> 3] & BIT(7 - (skip_bits & 7))) {
                *data |= BIT(bit_loop);
            }
            skip_bits++;
        }

        if (!bit_loop) {
            break;
        }
    }

    return (skip_bits);
}

/* Shared var between record/replay functions */

#define WRITE_BUFFER_SIZE 1024
static struct
{
    struct zmv_header header;
    FILE* fp;
    struct
    {
        uint32_t A;
        uint32_t B;
        uint32_t C;
        uint32_t D;
        uint32_t E;
        uint16_t latchx;
        uint16_t latchy;
    } last_joy_state;
    uint16_t inputs_enabled;
    uint8_t write_buffer[WRITE_BUFFER_SIZE];
    size_t write_buffer_loc;
    struct internal_chapter_buf internal_chapters;
    size_t last_internal_chapter_offset;
    char* filename;
    size_t rle_count;
} zmv_vars;

#define GAMEPAD_MASK 0xFFF00000
#define MOUSE_MASK 0x00C0FFFF
#define SCOPE_MASK 0xF0000000

#define GAMEPAD_ENABLE 0x00008000
#define MOUSE_ENABLE 0x00010000
#define SCOPE_ENABLE 0x00FF0000

static size_t pad_bit_encoder(uint8_t pad, uint8_t* buffer, size_t skip_bits)
{
    uint32_t last_state = 0;

    switch (pad) {
    case 1:
        last_state = zmv_vars.last_joy_state.A;
        break;

    case 2:
        last_state = zmv_vars.last_joy_state.B;
        break;

    case 3:
        last_state = zmv_vars.last_joy_state.C;
        break;

    case 4:
        last_state = zmv_vars.last_joy_state.D;
        break;

    case 5:
        last_state = zmv_vars.last_joy_state.E;
        break;
    }

    switch (pad) {
    case 2:
        if ((zmv_vars.inputs_enabled & BIT(0x8))) // Super Scope
        {
            uint32_t xdata = (zmv_vars.last_joy_state.latchx - 40) & 0xFF;
            uint32_t ydata = zmv_vars.last_joy_state.latchy & 0xFF;

            skip_bits = bit_encoder(last_state, SCOPE_MASK, buffer, skip_bits);
            skip_bits = bit_encoder(xdata, 0x000000FF, buffer, skip_bits);
            skip_bits = bit_encoder(ydata, 0x000000FF, buffer, skip_bits);

            break;
        }

    case 1:
        if ((zmv_vars.inputs_enabled & ((pad == 1) ? BIT(0xA) : BIT(0x9)))) // Mouse ?
        {
            skip_bits = bit_encoder(last_state, MOUSE_MASK, buffer, skip_bits);
        } else {
            skip_bits = bit_encoder(last_state, GAMEPAD_MASK, buffer, skip_bits);
        }
        break;

    case 3:
    case 4:
    case 5:
        // No multitap if both ports use special devices
        if ((zmv_vars.inputs_enabled & (BIT(0xA) | BIT(0x9) | BIT(0x8))) <= BIT(0xA)) {
            skip_bits = bit_encoder(last_state, GAMEPAD_MASK, buffer, skip_bits);
        }
        break;
    }
    return (skip_bits);
}

static size_t pad_bit_decoder(uint8_t pad, uint8_t* buffer, size_t skip_bits)
{
    uint32_t* last_state = 0;
    uint16_t input_enable_mask = 0;

    switch (pad) {
    case 1:
        last_state = &zmv_vars.last_joy_state.A;
        input_enable_mask = BIT(0xF);
        break;

    case 2:
        last_state = &zmv_vars.last_joy_state.B;
        input_enable_mask = BIT(0xE);
        break;

    case 3:
        last_state = &zmv_vars.last_joy_state.C;
        input_enable_mask = BIT(0xD);
        break;

    case 4:
        last_state = &zmv_vars.last_joy_state.D;
        input_enable_mask = BIT(0xC);
        break;

    case 5:
        last_state = &zmv_vars.last_joy_state.E;
        input_enable_mask = BIT(0xB);
        break;
    }

    switch (pad) {
    case 2:
        if ((zmv_vars.inputs_enabled & BIT(0x8))) // Super Scope
        {
            uint32_t xdata, ydata;

            skip_bits = bit_decoder(last_state, SCOPE_MASK, buffer, skip_bits);
            skip_bits = bit_decoder(&xdata, 0x000000FF, buffer, skip_bits);
            skip_bits = bit_decoder(&ydata, 0x000000FF, buffer, skip_bits);
            *last_state |= SCOPE_ENABLE;

            zmv_vars.last_joy_state.latchx = (uint16_t)(xdata + 40);
            zmv_vars.last_joy_state.latchy = (uint16_t)ydata;

            break;
        }

    case 1:
        if (zmv_vars.inputs_enabled & ((pad == 1) ? BIT(0xA) : BIT(0x9))) // Mouse ?
        {
            skip_bits = bit_decoder(last_state, MOUSE_MASK, buffer, skip_bits);
            *last_state |= MOUSE_ENABLE;
        } else {
            skip_bits = bit_decoder(last_state, GAMEPAD_MASK, buffer, skip_bits);
            *last_state |= (zmv_vars.inputs_enabled & input_enable_mask) ? GAMEPAD_ENABLE : 0;
        }
        break;

    case 3:
    case 4:
    case 5:
        // No multitap if both ports use special devices
        if ((zmv_vars.inputs_enabled & (BIT(0xA) | BIT(0x9) | BIT(0x8))) > BIT(0xA)) {
            *last_state = 0;
        } else {
            skip_bits = bit_decoder(last_state, GAMEPAD_MASK, buffer, skip_bits);
            *last_state |= (zmv_vars.inputs_enabled & input_enable_mask) ? GAMEPAD_ENABLE : 0;
        }
        break;
    }
    return (skip_bits);
}

static void save_last_joy_state(uint8_t* buffer)
{
    size_t skip_bits = 16;
    memcpy(buffer, uint16_to_bytes(zmv_vars.inputs_enabled), 2);

    skip_bits = pad_bit_encoder(1, buffer, skip_bits);
    skip_bits = pad_bit_encoder(2, buffer, skip_bits);
    skip_bits = pad_bit_encoder(3, buffer, skip_bits);
    skip_bits = pad_bit_encoder(4, buffer, skip_bits);
    skip_bits = pad_bit_encoder(5, buffer, skip_bits);
}

static void load_last_joy_state(uint8_t* buffer)
{
    size_t skip_bits = 16;
    zmv_vars.inputs_enabled = bytes_to_uint16(buffer);

    skip_bits = pad_bit_decoder(1, buffer, skip_bits);
    skip_bits = pad_bit_decoder(2, buffer, skip_bits);
    skip_bits = pad_bit_decoder(3, buffer, skip_bits);
    skip_bits = pad_bit_decoder(4, buffer, skip_bits);
    skip_bits = pad_bit_decoder(5, buffer, skip_bits);
}

// These things are a total of 11 bytes (2 byte enable field, up to 9 bytes for input bits)
static void write_last_joy_state(FILE* fp)
{
    save_last_joy_state(zmv_vars.write_buffer);
    fwrite(zmv_vars.write_buffer, 11, 1, fp);
}

static void read_last_joy_state(FILE* fp)
{
    IGNORE_RESULT(fread(zmv_vars.write_buffer, 11, 1, fp));
    load_last_joy_state(zmv_vars.write_buffer);
}

static void flush_input_buffer()
{
    if (zmv_vars.write_buffer_loc) {
        fwrite(zmv_vars.write_buffer, zmv_vars.write_buffer_loc, 1, zmv_vars.fp);
        zmv_vars.write_buffer_loc = 0;
    }

    if (zmv_vars.rle_count) {
        if (zmv_vars.rle_count > 5) {
            zmv_vars.write_buffer[0] = BIT(1); // RLE bit
            fwrite(zmv_vars.write_buffer, 1, 1, zmv_vars.fp);
            fwrite4(zmv_vars.header.frames, zmv_vars.fp);
        } else {
            memset(zmv_vars.write_buffer, 0, zmv_vars.rle_count);
            fwrite(zmv_vars.write_buffer, zmv_vars.rle_count, 1, zmv_vars.fp);
        }
        zmv_vars.rle_count = 0;
    }

    zmv_vars.header.author_len = 0; // If we're writing, then author is erased if there
}

static void flush_input_if_needed()
{
    if (zmv_vars.write_buffer_loc > WRITE_BUFFER_SIZE - 15) // 14 is a RLE buffer (5) + flag (1) + largest input (9)
    {
        flush_input_buffer();
    }
}

// For various ZMV calculations, the length of the last chapter needs to be known
static size_t internal_chapter_length(size_t offset)
{
    size_t current_loc = ftell(zmv_vars.fp);
    size_t icl = 0;

    fseek(zmv_vars.fp, offset, SEEK_SET);
    icl = fread3(zmv_vars.fp) & 0x007FFFFF; // The upper 9 bits are not part of the length
    icl += 3; // Add 3 for the header which says how long it is

    fseek(zmv_vars.fp, current_loc, SEEK_SET);
    return (icl);
}

/* Create and record ZMV */

static bool zmv_create(char* filename)
{
    memset(&zmv_vars, 0, sizeof(zmv_vars));
    if ((zmv_vars.fp = fopen_dir(ZMoviePath, filename, "w+b"))) {
        size_t filename_len = strlen(filename);
        memcpy(zmv_vars.header.magic, "ZMV", 3);
        zmv_vars.header.zsnes_version = versionNumber & 0xFFFF;
        zmv_vars.header.rom_crc32 = CRC32;
        zmv_vars.header.zst_size = cur_zst_size;
        zmv_vars.header.zmv_flag.start_method = (enum zmv_start_methods)MovieStartMethod;
        zmv_vars.header.zmv_flag.video_mode = romispal ? zmv_vm_pal : zmv_vm_ntsc;
        zmv_vars.header.average_fps = romispal ? 250 : 240;
        zmv_vars.header.initial_input = (pl1contrl ? BIT(0xF) : 0) | (pl2contrl ? BIT(0xE) : 0) | (pl3contrl ? BIT(0xD) : 0) | (pl4contrl ? BIT(0xC) : 0) | (pl5contrl ? BIT(0xB) : 0) | (IS_MOUSE_1() ? BIT(0xA) : 0) | (IS_MOUSE_2() ? BIT(0x9) : 0) | (IS_SCOPE() ? BIT(0x8) : 0);

        zmv_header_write(&zmv_vars.header, zmv_vars.fp);

        zmv_vars.inputs_enabled = zmv_vars.header.initial_input;

        switch (zmv_vars.header.zmv_flag.start_method) {
        case zmv_sm_zst:
            break;
        case zmv_sm_power:
            MovieWaiting = true;
            powercycle(true, false);
            break;
        case zmv_sm_reset:
            MovieWaiting = true;
            GUIReset = 1;
            GUIDoReset();
            ReturnFromSPCStall = 0;
            break;
        case zmv_sm_clear_all:
            MovieWaiting = true;
            powercycle(false, false);
            break;
        }

        zst_save(zmv_vars.fp, false, true);
        zmv_vars.filename = (char*)malloc(filename_len + 1); //+1 for null
        strcpy(zmv_vars.filename, filename);

        debug_input_start;

        return (true);
    }
    return (false);
}

static void zmv_rle_flush()
{
    if (zmv_vars.rle_count) {
        if (zmv_vars.rle_count > 5) {
            zmv_vars.write_buffer[zmv_vars.write_buffer_loc++] = BIT(1); // RLE bit
            memcpy(zmv_vars.write_buffer + zmv_vars.write_buffer_loc, uint32_to_bytes(zmv_vars.header.frames), 4);
            zmv_vars.write_buffer_loc += 4;
        } else {
            memset(zmv_vars.write_buffer + zmv_vars.write_buffer_loc, 0, zmv_vars.rle_count);
            zmv_vars.write_buffer_loc += zmv_vars.rle_count;
        }
        zmv_vars.rle_count = 0;
    }
}

static void zmv_record_command(enum zmv_commands command)
{
    zmv_rle_flush();

    zmv_vars.write_buffer[zmv_vars.write_buffer_loc++] = (command << 1) | BIT(0);

    flush_input_if_needed();
}

static void record_pad(uint8_t pad, uint8_t* flag, uint8_t* buffer, size_t* skip_bits)
{
    uint32_t *last_state = 0, current_state = 0;
    uint8_t bit_mask = 0;

    switch (pad) {
    case 1:
        last_state = &zmv_vars.last_joy_state.A;
        current_state = JoyAOrig;
        bit_mask = BIT(7);
        break;

    case 2:
        last_state = &zmv_vars.last_joy_state.B;
        current_state = JoyBOrig;
        bit_mask = BIT(6);
        break;

    case 3:
        last_state = &zmv_vars.last_joy_state.C;
        current_state = JoyCOrig;
        bit_mask = BIT(5);
        break;

    case 4:
        last_state = &zmv_vars.last_joy_state.D;
        current_state = JoyDOrig;
        bit_mask = BIT(4);
        break;

    case 5:
        last_state = &zmv_vars.last_joy_state.E;
        current_state = JoyEOrig;
        bit_mask = BIT(3);
        break;
    }

    if ((current_state != *last_state) || ((zmv_vars.inputs_enabled & BIT(0x8)) && ((zmv_vars.last_joy_state.latchx != latchx) || (zmv_vars.last_joy_state.latchy != latchy)))) {
        zmv_vars.last_joy_state.latchx = latchx;
        zmv_vars.last_joy_state.latchy = latchy;

        *last_state = current_state;
        *flag |= bit_mask;
        *skip_bits = pad_bit_encoder(pad, buffer, *skip_bits);
    }
}

static void zmv_record(bool pause, uint8_t combos_used, uint8_t slow)
{
    uint8_t flag = 0;
    uint8_t press_buf[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0 };
    size_t skip_bits = 0;

    static float average = 0.0f;
    if (!average) {
        average = (float)zmv_vars.header.average_fps / ((romispal) ? 250.0f : 240.0f);
    }

    if (pause) {
        zmv_vars.header.incr_frames++;
    }

    zmv_vars.header.key_combos += combos_used;

    debug_input;

    record_pad(1, &flag, press_buf, &skip_bits);
    record_pad(2, &flag, press_buf, &skip_bits);
    record_pad(3, &flag, press_buf, &skip_bits);
    record_pad(4, &flag, press_buf, &skip_bits);
    record_pad(5, &flag, press_buf, &skip_bits);

    if (flag) {
        uint8_t buffer_used = (skip_bits >> 3) + ((skip_bits & 7) ? 1 : 0);

        zmv_rle_flush();

        zmv_vars.write_buffer[zmv_vars.write_buffer_loc++] = flag;
        memcpy(zmv_vars.write_buffer + zmv_vars.write_buffer_loc, press_buf, buffer_used);
        zmv_vars.write_buffer_loc += buffer_used;

        flush_input_if_needed();
    } else {
        zmv_vars.rle_count++;
    }

    // it's the right formula, don't waste time busting your brain on it
    average = (average * (zmv_vars.header.frames + zmv_vars.header.removed_frames) + 1.0f / (float)(slow + 1)) / (float)(zmv_vars.header.frames + zmv_vars.header.removed_frames + 1);
    // 1/4 precision for NTSC, 1/5 precision for PAL
    zmv_vars.header.average_fps = (uint8_t)(average * ((romispal) ? 250 : 240));
    zmv_vars.header.frames++;
}

static bool zmv_insert_chapter()
{
    if ((zmv_vars.header.internal_chapters < 65535) && zmv_vars.header.frames && (zmv_vars.last_internal_chapter_offset != (ftell(zmv_vars.fp) + zmv_vars.write_buffer_loc - INT_CHAP_SIZE(zmv_vars.last_internal_chapter_offset)))) {
        uint8_t flag = BIT(2);

        flush_input_buffer();

        fwrite(&flag, 1, 1, zmv_vars.fp);

        internal_chapter_add_offset(&zmv_vars.internal_chapters, ftell(zmv_vars.fp));
        zmv_vars.header.internal_chapters++;
        zmv_vars.last_internal_chapter_offset = ftell(zmv_vars.fp);

        zst_save(zmv_vars.fp, false, true);
        fwrite4(zmv_vars.header.frames, zmv_vars.fp);
        write_last_joy_state(zmv_vars.fp);

        return (true);
    }
    return (false);
}

static void zmv_record_finish()
{
    FILE* fp;

    flush_input_buffer();

    // Now write the save for append data
    mzt_chdir_up();
    if ((fp = fopen_dir(ZMoviePath, "append.zst", "wb"))) {
        zst_save(fp, false, false);
        fclose(fp);

        if ((fp = fopen_dir(ZMoviePath, "append.mzi", "wb"))) {
            fwrite4(zmv_vars.header.frames, fp);
            write_last_joy_state(fp);
            fwrite4(ftell(zmv_vars.fp), fp);
            fclose(fp);
        }
    }
    mzt_chdir_down();

    // Finish up writing the ZMV
    internal_chapter_write(&zmv_vars.internal_chapters, zmv_vars.fp);
    internal_chapter_free_chain(zmv_vars.internal_chapters.next);

    if (!zmv_vars.filename) {
        free(zmv_vars.filename);
        zmv_vars.filename = 0;
    }

    fwrite2(0, zmv_vars.fp); // External chapter count

    rewind(zmv_vars.fp);
    zmv_header_write(&zmv_vars.header, zmv_vars.fp);

    if (zmv_vars.fp) {
        fclose(zmv_vars.fp);
        zmv_vars.fp = 0;
    }

    debug_input_used;
}

static size_t zmv_frames_recorded()
{
    return (zmv_vars.header.frames);
}

/* Open and replay ZMV */

typedef struct internal_chapter_buf external_chapter_buf;

static struct
{
    external_chapter_buf external_chapters;
    uint16_t external_chapter_count;
    uint32_t frames_replayed;
    size_t last_chapter_frame;
    size_t first_chapter_pos;
    size_t input_start_pos;
} zmv_open_vars; // Additional vars for open/replay of a ZMV

static bool zmv_open(char* filename)
{
    memset(&zmv_vars, 0, sizeof(zmv_vars));
    memset(&zmv_open_vars, 0, sizeof(zmv_open_vars));

    zmv_vars.fp = fopen_dir(ZMoviePath, filename, "r+b");
    if (zmv_vars.fp && zmv_header_read(&zmv_vars.header, zmv_vars.fp) && !strncmp(zmv_vars.header.magic, "ZMV", 3)) {
        uint_fast16_t i;
        size_t filename_len = strlen(filename);

        MovieStartMethod = (unsigned char)zmv_vars.header.zmv_flag.start_method;
        zmv_vars.inputs_enabled = zmv_vars.header.initial_input;
        zmv_open_vars.first_chapter_pos = ftell(zmv_vars.fp);

        if (zmv_vars.header.zsnes_version != (versionNumber & 0xFFFF)) {
            zst_compressed_loader(zmv_vars.fp);
            zmv_open_vars.input_start_pos = ftell(zmv_vars.fp);
            Msgptr = "ZSNES VERSION MISMATCH.";
        } else {
            switch (zmv_vars.header.zmv_flag.start_method) {
            case zmv_sm_zst:
                zst_compressed_loader(zmv_vars.fp);
                break;
            case zmv_sm_power:
                MovieWaiting = true;
                powercycle(false, false);
                zst_sram_load_compressed(zmv_vars.fp);
                break;
            case zmv_sm_reset:
                GUIReset = 1;
                GUIDoReset();
                ReturnFromSPCStall = 0;
                zst_sram_load_compressed(zmv_vars.fp);
                break;
            case zmv_sm_clear_all:
                MovieWaiting = true;
                powercycle(false, false);
                fseek(zmv_vars.fp, internal_chapter_length(ftell(zmv_vars.fp)), SEEK_CUR);
                break;
            }

            zmv_open_vars.input_start_pos = ftell(zmv_vars.fp);
            Msgptr = "MOVIE STARTED.";
        }

        if (zmv_vars.header.rom_crc32 != CRC32) {
            static char buffer[29]; //"ROM MISMATCH. NEED: 01234567"
            sprintf(buffer, "ROM MISMATCH. NEED: %08X", (unsigned int)zmv_vars.header.rom_crc32);
            Msgptr = buffer;
        }

        pl1contrl = (zmv_vars.header.initial_input & BIT(0xF)) ? 1 : 0;
        pl2contrl = (zmv_vars.header.initial_input & BIT(0xE)) ? 1 : 0;
        pl3contrl = (zmv_vars.header.initial_input & BIT(0xD)) ? 1 : 0;
        pl4contrl = (zmv_vars.header.initial_input & BIT(0xC)) ? 1 : 0;
        pl5contrl = (zmv_vars.header.initial_input & BIT(0xB)) ? 1 : 0;

        SET_MOUSE_1(zmv_vars.header.initial_input & BIT(0xA));
        SET_MOUSE_2(zmv_vars.header.initial_input & BIT(0x9));
        SET_SCOPE(zmv_vars.header.initial_input & BIT(0x8));

        fseek(zmv_vars.fp, -((signed)EXT_CHAP_COUNT_END_DIST), SEEK_END);
        zmv_open_vars.external_chapter_count = fread2(zmv_vars.fp);

        fseek(zmv_vars.fp, -((signed)INT_CHAP_END_DIST), SEEK_END);

        internal_chapter_read(&zmv_vars.internal_chapters, zmv_vars.fp, zmv_vars.header.internal_chapters);

        for (i = 0; i < zmv_open_vars.external_chapter_count; i++) {
            // Seek to 4 bytes before end of chapter, since last 4 bytes is where it contains offset value
            fseek(zmv_vars.fp, EXT_CHAP_SIZE - 4, SEEK_CUR);
            internal_chapter_add_offset(&zmv_open_vars.external_chapters, fread4(zmv_vars.fp));
        }

        fseek(zmv_vars.fp, zmv_open_vars.input_start_pos, SEEK_SET);

        zmv_vars.filename = (char*)malloc(filename_len + 1); //+1 for null
        strcpy(zmv_vars.filename, filename);

        debug_input_start;

        return (true);
    }
    return (false);
}

static bool zmv_replay_command(enum zmv_commands command)
{
    switch (command) {

    default:
        break;
    }
    return (false);
}

static void replay_pad(uint8_t pad, uint8_t flag, uint8_t* buffer, size_t* skip_bits)
{
    uint32_t *last_state = 0, *current_state = 0;
    uint8_t bit_mask = 0;

    switch (pad) {
    case 1:
        last_state = &zmv_vars.last_joy_state.A;
        current_state = &JoyAOrig;
        bit_mask = BIT(7);
        break;

    case 2:
        last_state = &zmv_vars.last_joy_state.B;
        current_state = &JoyBOrig;
        bit_mask = BIT(6);
        break;

    case 3:
        last_state = &zmv_vars.last_joy_state.C;
        current_state = &JoyCOrig;
        bit_mask = BIT(5);
        break;

    case 4:
        last_state = &zmv_vars.last_joy_state.D;
        current_state = &JoyDOrig;
        bit_mask = BIT(4);
        break;

    case 5:
        last_state = &zmv_vars.last_joy_state.E;
        current_state = &JoyEOrig;
        bit_mask = BIT(3);
        break;
    }

    if (flag & bit_mask) {
        size_t bits_needed = pad_bit_decoder(pad, buffer, 0);
        if (bits_needed) {
            size_t leftover_bits = (8 - (*skip_bits & 7)) & 7;
            bits_needed -= leftover_bits;

            IGNORE_RESULT(fread(buffer + (*skip_bits >> 3) + ((*skip_bits & 7) ? 1 : 0), 1, (bits_needed >> 3) + ((bits_needed & 7) ? 1 : 0), zmv_vars.fp));
            *skip_bits = pad_bit_decoder(pad, buffer, *skip_bits);
        }
    }
    *current_state = *last_state;
    latchx = zmv_vars.last_joy_state.latchx;
    latchy = zmv_vars.last_joy_state.latchy;
}

static bool zmv_replay()
{
    if (zmv_open_vars.frames_replayed < zmv_vars.header.frames) {
        if (zmv_vars.rle_count) {
            JoyAOrig = zmv_vars.last_joy_state.A;
            JoyBOrig = zmv_vars.last_joy_state.B;
            JoyCOrig = zmv_vars.last_joy_state.C;
            JoyDOrig = zmv_vars.last_joy_state.D;
            JoyEOrig = zmv_vars.last_joy_state.E;
            latchx = zmv_vars.last_joy_state.latchx;
            latchy = zmv_vars.last_joy_state.latchy;
            zmv_vars.rle_count--;

            debug_input;
        } else {
            uint8_t flag = 0;
            zmv_vars.rle_count = 0;

            IGNORE_RESULT(fread(&flag, 1, 1, zmv_vars.fp));

            if (flag & BIT(0)) // Command
            {
                uint8_t command = flag >> 1;
                if (command == zmv_command_reset) {
                    GUIReset = 1;
                    ReturnFromSPCStall = 0;
                    return (true);
                }
                if (zmv_replay_command(command)) {
                    return (zmv_replay());
                }
                return (false);
            }

            else if (flag & BIT(1)) // RLE
            {
                zmv_vars.rle_count = fread4(zmv_vars.fp) - zmv_open_vars.frames_replayed;
                return (zmv_replay());
            }

            else if (flag & BIT(2)) // Internal Chapter
            {
                fseek(zmv_vars.fp, INT_CHAP_SIZE(ftell(zmv_vars.fp)), SEEK_CUR);
                return (zmv_replay());
            }

            else {
                uint8_t press_buf[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0 };
                size_t skip_bits = 0;

                replay_pad(1, flag, press_buf, &skip_bits);
                replay_pad(2, flag, press_buf, &skip_bits);
                replay_pad(3, flag, press_buf, &skip_bits);
                replay_pad(4, flag, press_buf, &skip_bits);
                replay_pad(5, flag, press_buf, &skip_bits);

                debug_input;
            }
        }

        zmv_open_vars.frames_replayed++;
        return (true);
    }

    return (false);
}

static bool zmv_next_chapter()
{
    size_t current_loc = ftell(zmv_vars.fp);

    size_t next_internal = internal_chapter_greater(&zmv_vars.internal_chapters, current_loc);
    size_t next_external = internal_chapter_greater(&zmv_open_vars.external_chapters, current_loc);

    size_t next = 0;

    if (next_internal != current_loc) {
        next = next_internal;
    } else {
        next_internal = ~0;
    }
    if ((next_external != current_loc) && next_external < next_internal) {
        next = next_external;
    }

    if (next) {
        if (next == next_internal) {
            fseek(zmv_vars.fp, next_internal, SEEK_SET);
            zst_compressed_loader(zmv_vars.fp);
            zmv_open_vars.frames_replayed = fread4(zmv_vars.fp);
            read_last_joy_state(zmv_vars.fp);
        } else {
            size_t ext_chapter_loc = EXT_CHAP_END_DIST - internal_chapter_pos(&zmv_open_vars.external_chapters, next) * EXT_CHAP_SIZE;
            fseek(zmv_vars.fp, -((signed)ext_chapter_loc), SEEK_END);
            zst_load(zmv_vars.fp, 0);
            zmv_open_vars.frames_replayed = fread4(zmv_vars.fp);
            read_last_joy_state(zmv_vars.fp);

            fseek(zmv_vars.fp, next_external, SEEK_SET);
        }

        zmv_vars.rle_count = 0;
        return (true);
    }

    return (false);
}

// Have playback start movie from beginning
static void zmv_rewind_playback()
{
    fseek(zmv_vars.fp, zmv_open_vars.first_chapter_pos, SEEK_SET);
    zst_compressed_loader(zmv_vars.fp);
    zmv_open_vars.frames_replayed = 0;
    zmv_open_vars.last_chapter_frame = 0;
    zmv_vars.rle_count = 0;
    zmv_vars.inputs_enabled = zmv_vars.header.initial_input;
    memset(&zmv_vars.last_joy_state, 0, sizeof(zmv_vars.last_joy_state));
}

static void zmv_prev_chapter()
{
    size_t current_loc = ftell(zmv_vars.fp);

    size_t prev_internal = internal_chapter_lesser(&zmv_vars.internal_chapters, current_loc);
    size_t prev_external = internal_chapter_lesser(&zmv_open_vars.external_chapters, current_loc);

    size_t prev = 0;

    if (prev_internal != current_loc) {
        prev = prev_internal;
    } else {
        prev_internal = 0;
    }
    if ((prev_external != current_loc) && prev_external > prev_internal) {
        prev = prev_external;
    }

    if (!prev) {
        zmv_rewind_playback();
        return;
    }

    // Code to go back before the previous chapter if the previous chapter was loaded recently
    if ((zmv_open_vars.frames_replayed - zmv_open_vars.last_chapter_frame) < 5 * ((unsigned int)EmuSpeed + 1)) { // 2.5 seconds NTSC when in 100% speed - altered to make slowmo nicer
        size_t pprev = prev - 1;
        size_t pprev_internal = internal_chapter_lesser(&zmv_vars.internal_chapters, pprev);
        size_t pprev_external = internal_chapter_lesser(&zmv_open_vars.external_chapters, pprev);

        if ((pprev_internal == pprev) && (pprev_external == pprev)) {
            zmv_rewind_playback();
            return;
        }

        if (pprev_internal != pprev) {
            prev = prev_internal = pprev_internal;
        } else {
            pprev_internal = 0;
        }
        if ((pprev_external != pprev) && pprev_external > pprev_internal) {
            prev = prev_external = pprev_external;
        }
    }

    if (prev == prev_internal) {
        fseek(zmv_vars.fp, prev_internal, SEEK_SET);
        zst_compressed_loader(zmv_vars.fp);
        zmv_open_vars.frames_replayed = fread4(zmv_vars.fp);
        read_last_joy_state(zmv_vars.fp);
    } else {
        size_t ext_chapter_loc = EXT_CHAP_END_DIST - internal_chapter_pos(&zmv_open_vars.external_chapters, prev) * EXT_CHAP_SIZE;
        fseek(zmv_vars.fp, -((signed)ext_chapter_loc), SEEK_END);
        zst_load(zmv_vars.fp, 0);
        zmv_open_vars.frames_replayed = fread4(zmv_vars.fp);
        read_last_joy_state(zmv_vars.fp);

        fseek(zmv_vars.fp, prev_external, SEEK_SET);
    }
    zmv_open_vars.last_chapter_frame = zmv_open_vars.frames_replayed;
    zmv_vars.rle_count = 0;
}

// External chapter
static void zmv_add_chapter()
{
    if ((zmv_open_vars.external_chapter_count < 65535) && zmv_open_vars.frames_replayed) {
        size_t current_loc = ftell(zmv_vars.fp);

        // Check if there is external here already - should possibly modify this to use
        // intern_chapter_lesser() to see if an internal chapter was made right before this
        // point in the stream
        if ((internal_chapter_pos(&zmv_open_vars.external_chapters, current_loc)) == 0xFFFFFFFF) {
            // Check if we have internal right here
            uint8_t flag;
            IGNORE_RESULT(fread(&flag, 1, 1, zmv_vars.fp));

            if (!(flag & BIT(2))) {
                char* author = 0;

                internal_chapter_add_offset(&zmv_open_vars.external_chapters, current_loc);
                zmv_open_vars.external_chapter_count++;

                if (zmv_vars.header.author_len) {
                    if ((author = (char*)malloc(zmv_vars.header.author_len))) {
                        fseek(zmv_vars.fp, -(zmv_vars.header.author_len), SEEK_END);
                        IGNORE_RESULT(fread(author, zmv_vars.header.author_len, 1, zmv_vars.fp));
                    }
                }

                fseek(zmv_vars.fp, -((signed)EXT_CHAP_COUNT_END_DIST), SEEK_END);
                zst_save(zmv_vars.fp, false, false);
                fwrite4(zmv_open_vars.frames_replayed, zmv_vars.fp);
                write_last_joy_state(zmv_vars.fp);
                fwrite4(current_loc, zmv_vars.fp);

                fwrite2(zmv_open_vars.external_chapter_count, zmv_vars.fp);

                if (author) {
                    fwrite(author, zmv_vars.header.author_len, 1, zmv_vars.fp);
                    free(author);
                }

                fseek(zmv_vars.fp, current_loc, SEEK_SET);
            } else // Just skip the internal
            {
                fseek(zmv_vars.fp, INT_CHAP_SIZE(ftell(zmv_vars.fp)), SEEK_CUR);
            }
        }
    }
}

static void zmv_replay_finished()
{
    internal_chapter_free_chain(zmv_vars.internal_chapters.next);
    internal_chapter_free_chain(zmv_open_vars.external_chapters.next);
    if (!zmv_vars.filename) {
        free(zmv_vars.filename);
        zmv_vars.filename = 0;
    }
    if (zmv_vars.fp) {
        fclose(zmv_vars.fp);
        zmv_vars.fp = 0;
    }
}

static void zmv_replay_to_record()
{
    internal_chapter_free_chain(zmv_open_vars.external_chapters.next);
    zmv_vars.header.rerecords++;
    zmv_vars.header.removed_frames += zmv_vars.header.frames - zmv_open_vars.frames_replayed;
    zmv_vars.header.frames = zmv_open_vars.frames_replayed;
    zmv_vars.header.author_len = 0;
    zmv_vars.header.internal_chapters = internal_chapter_delete_after(&zmv_vars.internal_chapters, ftell(zmv_vars.fp));
    zmv_vars.last_internal_chapter_offset = internal_chapter_lesser(&zmv_vars.internal_chapters, ~0);

    if (zmv_vars.rle_count) {
        fseek(zmv_vars.fp, -4, SEEK_CUR);
        fwrite4(zmv_vars.header.frames, zmv_vars.fp);
        zmv_vars.rle_count = 0;
    }

    IGNORE_RESULT(ftruncate(fileno(zmv_vars.fp), ftell(zmv_vars.fp)));
}

static size_t zmv_frames_replayed()
{
    return (zmv_open_vars.frames_replayed);
}

static bool zmv_append(char* filename)
{
    if (zmv_open(filename)) {
        FILE* fp;

        mzt_chdir_up();
        if ((fp = fopen_dir(ZMoviePath, "append.zst", "rb"))) {
            zst_load(fp, 0);
            fclose(fp);

            if ((fp = fopen_dir(ZMoviePath, "append.mzi", "rb"))) {
                zmv_open_vars.frames_replayed = fread4(fp);
                read_last_joy_state(fp);
                fseek(zmv_vars.fp, fread4(fp), SEEK_SET);
                fclose(fp);

                zmv_vars.rle_count = 0;
                mzt_chdir_down();

                zmv_replay_to_record();
                zmv_vars.header.rerecords--; // Remove the rerecord count added by replay to record
                return (true);
            }
        }
        mzt_chdir_down();
        zmv_replay_finished();
    }
    return (false);
}
/* Rewind related functions and vars */

struct zmv_rewind {
    uint8_t last_joy_state[10];
    size_t file_pos;
    uint32_t frames;
    size_t rle_count;
};

struct zmv_rewind* zmv_rewind_buffer = 0;

static void zmv_alloc_rewind_buffer(uint8_t rewind_states)
{
    zmv_rewind_buffer = (struct zmv_rewind*)malloc(sizeof(struct zmv_rewind) * rewind_states);
}

static void zmv_dealloc_rewind_buffer()
{
    if (zmv_rewind_buffer) {
        free(zmv_rewind_buffer);
        zmv_rewind_buffer = 0;
    }
}

void zmv_rewind_save(size_t state, bool playback)
{
    save_last_joy_state(zmv_rewind_buffer[state].last_joy_state);
    zmv_rewind_buffer[state].file_pos = ftell(zmv_vars.fp) + zmv_vars.write_buffer_loc;
    zmv_rewind_buffer[state].frames = playback ? zmv_open_vars.frames_replayed : zmv_vars.header.frames;
    zmv_rewind_buffer[state].rle_count = zmv_vars.rle_count;
}

void zmv_rewind_load(size_t state, bool playback)
{
    size_t file_pos = zmv_rewind_buffer[state].file_pos;
    load_last_joy_state(zmv_rewind_buffer[state].last_joy_state);

    if (playback) {
        zmv_open_vars.frames_replayed = zmv_rewind_buffer[state].frames;
        fseek(zmv_vars.fp, file_pos, SEEK_SET);
        zmv_vars.rle_count = zmv_rewind_buffer[state].rle_count;
    } else {
        flush_input_buffer();

        zmv_vars.header.rerecords++;
        zmv_vars.header.removed_frames += zmv_vars.header.frames - zmv_rewind_buffer[state].frames;
        zmv_vars.header.frames = zmv_rewind_buffer[state].frames;
        zmv_vars.rle_count = zmv_rewind_buffer[state].rle_count;

        fseek(zmv_vars.fp, file_pos, SEEK_SET);
        IGNORE_RESULT(ftruncate(fileno(zmv_vars.fp), file_pos));

        zmv_vars.header.internal_chapters = internal_chapter_delete_after(&zmv_vars.internal_chapters, file_pos);
        zmv_vars.last_internal_chapter_offset = internal_chapter_lesser(&zmv_vars.internal_chapters, ~0);
    }
}

/* Save and load MZT */

size_t mzt_filename_generate()
{
    size_t filename_len = strlen(zmv_vars.filename);
    memcpy(zmv_vars.filename + filename_len - 3, "mz", 2);
    if (!isdigit(zmv_vars.filename[filename_len - 1])) {
        zmv_vars.filename[filename_len - 1] = 't';
    }
    if (access_dir(ZMoviePath, zmv_vars.filename, F_OK)) {
        mkdir_dir(ZMoviePath, zmv_vars.filename);
    }
    return (filename_len);
}

void mzt_chdir_up()
{
    mzt_filename_generate();
    strcat(ZMoviePath, zmv_vars.filename);
    strcatslash(ZMoviePath);
}

void mzt_chdir_down()
{
    strdirname(ZMoviePath);
    strcatslash(ZMoviePath);
}

bool mzt_save(int position, bool thumb, bool playback)
{
    FILE* fp;
    bool mzt_saved = false;
    char name_buf[7];

    if (!playback) {
        flush_input_buffer();
    }
    mzt_chdir_up();

    sprintf(name_buf, "%.2d.zst", position);

    if ((fp = fopen_dir(ZMoviePath, name_buf, "wb"))) {
        zst_save(fp, thumb, false);
        fclose(fp);

        setextension(name_buf, "mzi");
        if ((fp = fopen_dir(ZMoviePath, name_buf, "wb"))) {
            gzFile gzp = 0;
            size_t rewind_point = ftell(zmv_vars.fp);
            ;

            fwrite4((playback) ? zmv_open_vars.frames_replayed : zmv_vars.header.frames, fp);
            write_last_joy_state(fp);
            fwrite4(rewind_point, fp);
            fclose(fp);

            setextension(name_buf, "zmv");
            if ((gzp = gzopen_dir(ZMoviePath, name_buf, "wb9"))) {
                if (playback) {
                    size_t amount_written,
                        int_chap_seek_point = INT_CHAP_END_DIST,
                        internal_chapter_count = internal_chapter_count_until(&zmv_vars.internal_chapters, rewind_point),
                        internal_chapters_size = internal_chapter_count << 2,
                        frames = zmv_vars.header.frames,
                        internal_chapters = zmv_vars.header.internal_chapters,
                        author_len = zmv_vars.header.author_len;

                    //*Correct* the header for the MZT ZMV file
                    zmv_vars.header.frames = zmv_open_vars.frames_replayed;
                    zmv_vars.header.internal_chapters = internal_chapter_count;
                    zmv_vars.header.author_len = 0;

                    rewind(zmv_vars.fp);
                    zmv_header_write(&zmv_vars.header, zmv_vars.fp);
                    rewind(zmv_vars.fp);

                    // Copy the real ZMV to the MZT GZipped ZMV
                    for (amount_written = 0; amount_written < rewind_point;) {
                        size_t amount = rewind_point - amount_written;
                        if (amount > WRITE_BUFFER_SIZE) {
                            amount = WRITE_BUFFER_SIZE;
                        }

                        amount = fread(zmv_vars.write_buffer, 1, amount, zmv_vars.fp);
                        gzwrite(gzp, zmv_vars.write_buffer, amount);
                        amount_written += amount;
                    }
                    fseek(zmv_vars.fp, -((signed)int_chap_seek_point), SEEK_END);
                    for (amount_written = 0; amount_written < internal_chapters_size;) {
                        size_t amount = internal_chapters_size - amount_written;
                        if (amount > WRITE_BUFFER_SIZE) {
                            amount = WRITE_BUFFER_SIZE;
                        }

                        amount = fread(zmv_vars.write_buffer, 1, amount, zmv_vars.fp);
                        gzwrite(gzp, zmv_vars.write_buffer, amount);
                        amount_written += amount;
                    }
                    // External chapter count
                    gzputc(gzp, 0);
                    gzputc(gzp, 0);
                    // Done
                    gzclose(gzp);

                    // Now fix data for the real ZMV file's header since we destroyed it
                    zmv_vars.header.frames = frames;
                    zmv_vars.header.internal_chapters = internal_chapters;
                    zmv_vars.header.author_len = author_len;

                    rewind(zmv_vars.fp);
                    zmv_header_write(&zmv_vars.header, zmv_vars.fp);
                } else // During record is much simpler
                {
                    internal_chapter_write(&zmv_vars.internal_chapters, zmv_vars.fp);
                    fwrite2(0, zmv_vars.fp); // External chapter count

                    rewind(zmv_vars.fp);
                    zmv_header_write(&zmv_vars.header, zmv_vars.fp);
                    rewind(zmv_vars.fp);

                    while (!feof(zmv_vars.fp)) {
                        size_t amount_read = fread(zmv_vars.write_buffer, 1, WRITE_BUFFER_SIZE, zmv_vars.fp);
                        gzwrite(gzp, zmv_vars.write_buffer, amount_read);
                    }
                    gzclose(gzp);
                }
                mzt_saved = true;
            }
            fseek(zmv_vars.fp, rewind_point, SEEK_SET);
        }
    }
    mzt_chdir_down();
    return (mzt_saved);
}

bool mzt_load(int position, bool playback)
{
    FILE* fp;
    bool mzt_loaded = false;
    char name_buf[7];

    mzt_chdir_up();

    sprintf(name_buf, "%.2d.zst", position);

    if ((fp = fopen_dir(ZMoviePath, name_buf, "rb"))) {
        zst_load(fp, 0);
        fclose(fp);

        setextension(name_buf, "mzi");
        if ((fp = fopen_dir(ZMoviePath, name_buf, "rb"))) {
            size_t rewind_point;

            size_t current_frame = fread4(fp);
            read_last_joy_state(fp);
            rewind_point = fread4(fp);
            fclose(fp);

            zmv_vars.rle_count = 0;

            if (!playback) {
                gzFile gzp = 0;

                setextension(name_buf, "zmv");
                if ((gzp = gzopen_dir(ZMoviePath, name_buf, "rb"))) {
                    size_t rerecords = zmv_vars.header.rerecords + 1;
                    size_t removed_frames = zmv_vars.header.removed_frames + (zmv_vars.header.frames - current_frame);

                    internal_chapter_free_chain(zmv_vars.internal_chapters.next);
                    memset(&zmv_vars.internal_chapters, 0, sizeof(struct internal_chapter_buf));

                    rewind(zmv_vars.fp);
                    while (!gzeof(gzp)) {
                        size_t amount_read = gzread(gzp, zmv_vars.write_buffer, WRITE_BUFFER_SIZE);
                        fwrite(zmv_vars.write_buffer, 1, amount_read, zmv_vars.fp);
                    }
                    gzclose(gzp);

                    rewind(zmv_vars.fp);
                    zmv_header_read(&zmv_vars.header, zmv_vars.fp);
                    zmv_vars.header.removed_frames = removed_frames;
                    zmv_vars.header.rerecords = rerecords;
                    zmv_vars.header.author_len = 0;
                    zmv_vars.write_buffer_loc = 0;

                    fseek(zmv_vars.fp, rewind_point, SEEK_SET);
                    internal_chapter_read(&zmv_vars.internal_chapters, zmv_vars.fp, zmv_vars.header.internal_chapters);

                    fseek(zmv_vars.fp, rewind_point, SEEK_SET);
                    zmv_vars.last_internal_chapter_offset = internal_chapter_lesser(&zmv_vars.internal_chapters, ~0);
                    IGNORE_RESULT(ftruncate(fileno(zmv_vars.fp), ftell(zmv_vars.fp)));
                }
            } else {
                zmv_open_vars.frames_replayed = current_frame;
                fseek(zmv_vars.fp, rewind_point, SEEK_SET);
            }

            mzt_loaded = true;
        }
    }
    mzt_chdir_down();
    return (mzt_loaded);
}

/* Code for dumping raw video */

#define RAW_WIDTH 256
#define RAW_HEIGHT 224
#define RAW_FRAME_SIZE (RAW_WIDTH * RAW_HEIGHT * 3)

// NTSC FPS is  59.948743718592964824120603015060 in AVI that's a fraction of 59649/995
// which equals 59.948743718592964824120603015075, so videos should not desync for several millenia

// FPS = Rate*Stereo / Samples per Frame

// These two numbers help with calculating how many samples are needed per frame
// 59.948743718592964824120603015060 = SAMPLE_NTSC_LO*Rate/SAMPLE_NTSC_HI
// Samples per Frame = SAMPLE_NTSC_HI/SAMPLE_NTSC_LO *Stereo

#define SAMPLE_NTSC_HI_SCALE 995ULL
#define SAMPLE_NTSC_LO 59649ULL

//* PAL is much simpler, FPS is 50.0000000000000000000 which is a fraction of 50/1
#define SAMPLE_PAL_HI_SCALE 1ULL
#define SAMPLE_PAL_LO 50ULL

// Code using this by Bisqwit
// Used by raw videos for calculating sample rate

/* Matches what the mixer renders at (unix/audio.c). */
#define RATE 32000

// 0 = None; 1 Logging, but not now, 2 Log now
uint8_t AudioLogging;

extern uint8_t ZMVRawDump;

/* Replace the first rep_len bytes of str with new_str, capped at n. Returns the
   length the result needs; <= n means it was written. */
static size_t string_replace(char* str, size_t rep_len, const char* new_str, size_t n)
{
    size_t str_len = strlen(str);
    size_t new_str_len = strlen(new_str);
    size_t replaced_len = (str_len - rep_len) + new_str_len + 1;
    if (replaced_len <= n) {
        if (new_str_len == rep_len) {
            memcpy(str, new_str, rep_len);
        } else {
            memmove(str + new_str_len, str + rep_len, (str_len - rep_len) + 1);
            memcpy(str, new_str, new_str_len);
        }
    }
    return (replaced_len);
}

#define PICK_HELP(var)                               \
    if (!strncmp(str, "$" #var, strlen(#var) + 1)) { \
        *len = strlen(#var) + 1;                     \
        return (var);                                \
    }

static const char* pick_var(char* str, size_t* len)
{
    const char* null = NULL_FILE;

    PICK_HELP(md_prog);
    PICK_HELP(md_raw);
    PICK_HELP(md_other);
    PICK_HELP(md_file);
    PICK_HELP(md_sound);
    PICK_HELP(md_no_sound);
    PICK_HELP(md_pcm_audio);
    PICK_HELP(md_compressed_audio);

    if (MovieVideoMode == 5) {
        PICK_HELP(md_custom_temp1);
        PICK_HELP(md_custom_temp2);
        PICK_HELP(md_custom_temp3);
        PICK_HELP(null);
    }

    if (!strncmp(str, "$md_video_rate", strlen("$md_video_rate"))) {
        *len = strlen("$md_video_rate");
        return (romispal ? md_pal : md_ntsc);
    }
    if (!strncmp(str, "$md_vcodec", strlen("$md_vcodec"))) {
        *len = strlen("$md_vcodec");
        switch (MovieVideoMode) {
        case 2:
            return (md_ffv1);
            break;
        case 3:
            return (md_x264);
            break;
        case 4:
            return (md_xvid);
            break;
        }
        return ("");
    }
    if (!strncmp(str, "$md_smode", strlen("$md_smode"))) {
        *len = strlen("$md_smode");
        if (MovieAudioCompress) {
            return (md_compressed_sound);
        } else if (MovieAudio) {
            return (md_sound);
        } else {
            return (md_no_sound);
        }
    }
    fprintf(stderr, "Unknown Variable: %s\n", str);
    *len = 0;
    return (0);
}

static char* encode_command(char* p)
{
    static char command[1024];
    strncpy(command, p, sizeof(command));
    command[sizeof(command) - 1] = 0;

    for (p = command; *p;) {
        char* var = strchr(p, '$');
        if (var) {
            size_t var_len;
            const char* replace = pick_var(var, &var_len);
            if (var_len) {
                size_t remaining = sizeof(command) - (var - command);
                if (string_replace(var, var_len, replace, remaining) > remaining) {
                    fputs("Encoding command has been cut off, check your settings in zmovie.cfg", stderr);
                    break;
                }
            } else {
                p = var + 1;
            }
        } else {
            break;
        }
    }

#ifndef DEBUG // Debug mode prints out commands used in system_dir and popen_dir anyway
    puts(command);
#endif

    return (command);
}

static char* encode_command_custom(uint8_t pass)
{
    char* p;
    if (pass == md_custom_passes) {
        p = md_custom_last_pass;
    } else if (pass == 1) {
        p = md_custom_first_pass;
    } else {
        p = md_custom_middle_passes;
    }
    return (encode_command(p));
}

#ifdef __UNIXSDL__
static void broken_pipe(int sig)
{
    Msgptr = "BROKEN PIPE!";
    MessageOn = MsgCount;
}
#endif

struct
{
    FILE* vp;
    FILE* ap;

    size_t aud_dsize_pos;

    uint64_t sample_hi;
    uint64_t sample_lo;
    uint64_t sample_balance;
} raw_vid;

static void raw_embed_logo(bool audio)
{
    gzFile gzp;
    if ((gzp = gzopen_dir(ZCfgPath, md_logo, "rb"))) {
        uint8_t logo_buffer[RAW_FRAME_SIZE];
        while (!gzeof(gzp)) {
            if (RAW_FRAME_SIZE == gzread(gzp, logo_buffer, RAW_FRAME_SIZE)) {
                fwrite(logo_buffer, RAW_FRAME_SIZE, 1, raw_vid.vp);

                if (audio) {
                    // Thanks Bisqwit for this algorithm
                    uint32_t samples = (uint32_t)((raw_vid.sample_balance / raw_vid.sample_lo) << StereoSound);
                    raw_vid.sample_balance %= raw_vid.sample_lo;
                    raw_vid.sample_balance += raw_vid.sample_hi;
                    while (samples--) {
                        fwrite2(0, raw_vid.ap);
                    }
                }
            }
        }
    }
}

static uint8_t movie_current_pass = 0;

static void raw_video_close()
{
    bool audio_and_video = raw_vid.vp && raw_vid.ap;

    if (raw_vid.vp) {
        switch (MovieVideoMode) {
        case 1:
            fclose(raw_vid.vp);
            break;
        case 2:
        case 3:
        case 4:
        case 5:
            pclose(raw_vid.vp);
            break;
        }

        raw_vid.vp = 0;
    }

    if (raw_vid.ap) {
        size_t file_size = ftell(raw_vid.ap); // Get file size
        if (!fseek(raw_vid.ap, 4, SEEK_SET)) // Seek to after RIFF header
        {
            fwrite4(file_size - 8, raw_vid.ap); // Don't include header or this write, -8
        }
        if (!fseek(raw_vid.ap, raw_vid.aud_dsize_pos, SEEK_SET)) // Seek to where the audio data size goes
        {
            // Data size is remainder of file, which is file size, less current position, plus
            // The 4 bytes needed to hold the data size
            fwrite4(file_size - (raw_vid.aud_dsize_pos + 4), raw_vid.ap);
        }
        if (MovieAudioCompress) {
            pclose(raw_vid.ap);
        } else {
            fclose(raw_vid.ap);
        }
        raw_vid.ap = 0;
        AudioLogging = 0;
    }

    if (movie_current_pass == md_custom_passes) {
        remove_dir(ZCfgPath, MovieAudioCompress ? md_compressed_audio : md_pcm_audio);
        remove_dir(ZCfgPath, md_custom_pass_log_file);
        remove_dir(ZCfgPath, md_custom_pass_log_file2);
    }

    if ((MovieVideoMode != 5) && (audio_and_video && MovieVideoAudio)) {
        if (mencoderExists) {
            system_dir(ZCfgPath, encode_command(md_merge));
        }
        remove_dir(ZCfgPath, MovieAudioCompress ? md_compressed_audio : md_pcm_audio);
        remove_dir(ZCfgPath, md_file);
    }
    signal(SIGPIPE, SIG_IGN);
}

static bool raw_video_open()
{
    if (ZMVRawDump) // Command line
    {
        MovieVideoMode = ZMVRawDump;
    }

    switch (MovieVideoMode) {
    case 0:
        break;

    case 1:
        raw_vid.vp = fopen_dir(ZCfgPath, md_raw_file, "wb");
        MovieVideoAudio = false;
        break;

    case 2:
    case 3:
    case 4:
        signal(SIGPIPE, broken_pipe);
        mencoderExists = (raw_vid.vp = popen_dir(ZCfgPath, encode_command(md_command), WRITE_BINARY)) ? 1 : 0;
        break;

    case 5:
        signal(SIGPIPE, broken_pipe);
        mencoderExists = (raw_vid.vp = popen_dir(ZCfgPath, encode_command_custom(movie_current_pass), WRITE_BINARY)) ? 1 : 0;
        if (movie_current_pass == 1) {
            MovieAudio = 1;
        } else {
            if (*md_logo) {
                raw_embed_logo(false);
            }
            return (true);
        }
        break;

    default:
        return (false);
        break;
    }

    if (!MovieAudio && raw_vid.vp) {
        if (*md_logo) {
            raw_embed_logo(false);
        }
        return (true);
    }

    if ((!MovieVideoMode || raw_vid.vp) && MovieAudio) {
        if (MovieAudioCompress) {
            signal(SIGPIPE, broken_pipe);
            lameExists = (raw_vid.ap = popen_dir(ZCfgPath, encode_command(md_audio_compress), WRITE_BINARY)) ? 1 : 0;
        } else {
            raw_vid.ap = fopen_dir(ZCfgPath, md_pcm_audio, "wb");
        }
        if (raw_vid.ap) {
            uint16_t ba = (16 / 8) << StereoSound; // block align (SignificantBitsPerSample / 8 * NumChannels)

            fputs("RIFF", raw_vid.ap); // header
            fwrite4(~0, raw_vid.ap); // file size - unknown till file close
            fputs("WAVEfmt ", raw_vid.ap); // format
            fwrite4(0x12, raw_vid.ap); // fmt size
            fwrite2(1, raw_vid.ap); // fmt type (PCM)
            fwrite2(StereoSound + 1, raw_vid.ap); // channels
            fwrite4(RATE, raw_vid.ap); // sample rate
            fwrite4(RATE * ba, raw_vid.ap); // byte rate (sample rate*block align)
            fwrite2(ba, raw_vid.ap); // calculated above
            fwrite2(16, raw_vid.ap); // Significant bits per sample
            fwrite2(0, raw_vid.ap); // Extra format bytes
            fputs("data", raw_vid.ap); // data header
            raw_vid.aud_dsize_pos = ftell(raw_vid.ap); // Save current position for use later
            fwrite4(~0, raw_vid.ap); // data size - unknown till file close

            if (romispal) {
                raw_vid.sample_hi = SAMPLE_PAL_HI_SCALE * RATE;
                raw_vid.sample_lo = SAMPLE_PAL_LO;
            } else {
                raw_vid.sample_hi = SAMPLE_NTSC_HI_SCALE * RATE;
                raw_vid.sample_lo = SAMPLE_NTSC_LO;
            }
            raw_vid.sample_balance = raw_vid.sample_hi;

            AudioLogging = 1;
            if (MovieVideoMode && *md_logo) {
                raw_embed_logo(true);
            }
            return (true);
        }
    }

    raw_video_close();
    return (false);
}

static void raw_audio_write(uint32_t samples)
{
    extern int DSPBuffer[1280];
    extern uint32_t BufferSizeB, BufferSizeW;
    int *d = DSPBuffer, *d_end;

    while (samples > 1280) // This is in a loop for future proofing if we ever add above 48KHz
    {
        raw_audio_write(1280);
        samples -= 1280;
    }

    BufferSizeB = samples;
    BufferSizeW = samples << 1;

    ProcessSoundBuffer();

    for (d_end = DSPBuffer + samples; d < d_end; d++) {
        if ((unsigned int)(*d + 0x7FFF) < 0xFFFF) {
            fwrite2((short)*d, raw_vid.ap);
            continue;
        }
        if (*d > 0x7FFF) {
            fwrite2(0x7FFF, raw_vid.ap);
        } else {
            fwrite2(0x8001, raw_vid.ap);
        }
    }
}

static void raw_video_write_frame()
{
    if (raw_vid.vp) {
        size_t x, y;

        // Convert 16 bit image to 24 bit image
        for (y = 0; y < RAW_HEIGHT; y++) {
            for (x = 0; x < RAW_WIDTH; x++) {
                uint16_t pixel = ((u2 const*)vidbuffer)[(y + 1) * 288 + x + 16];
                fwrite3((((pixel & 0xF800) << 8) + ((pixel & 0xE000) << 3)) | (((pixel & 0x07E0) << 5) + ((pixel & 0x0600) >> 1)) | (((pixel & 0x001F) << 3) + ((pixel & 0x001C) >> 2)), raw_vid.vp);
            }
        }
    }

    if (raw_vid.ap) {
        // Thanks Bisqwit for this algorithm
        uint32_t samples = (uint32_t)((raw_vid.sample_balance / raw_vid.sample_lo) << StereoSound);
        raw_vid.sample_balance %= raw_vid.sample_lo;
        raw_vid.sample_balance += raw_vid.sample_hi;
        // printf("Samples: %u\n", samples);

        AudioLogging = 2;
        raw_audio_write(samples);
        AudioLogging = 1;
    }
}

/* Movie subtitles. gamename.zmv takes gamename.sub beside it, gamename.zm1
   takes gamename.su1, and so on. One "startframe:duration:message" per line:

     1:180:Hi how are you?
     2500:375:Kill 'em! */

static struct
{
    FILE* fp;
    char linebuf[256];
    size_t message_start;
    size_t message_duration;
} MovieSub;

static void MovieSub_Open(const char* filename)
{
    memset(&MovieSub, 0, sizeof(MovieSub));
    MovieSub.fp = fopen_dir(ZMoviePath, filename, "r");
}

static void MovieSub_Close()
{
    if (MovieSub.fp) {
        fclose(MovieSub.fp);
        MovieSub.fp = 0;
    }
}

static char* MovieSub_GetData(size_t frame_count)
{
    if (MovieSub.fp) {
        char *i, *num;

        if (frame_count > MovieSub.message_start + MovieSub.message_duration) {
            MovieSub.message_duration = 0;
            do {
                if (!fgets(MovieSub.linebuf, 256, MovieSub.fp)) {
                    return (0);
                }
                if (!(num = strtok(MovieSub.linebuf, ":"))) {
                    return (0);
                }
                for (i = num; *i; i++) {
                    if (!isdigit(*i)) {
                        return (0);
                    }
                }
                MovieSub.message_start = atoi(num);
            } while (MovieSub.message_start < zmv_frames_replayed());
            if (!(num = strtok(0, ":"))) {
                return (0);
            }
            for (i = num; *i; i++) {
                if (!isdigit(*i)) {
                    MovieSub.message_start = 0;
                    return (0);
                }
            }
            MovieSub.message_duration = atoi(num);
        }

        if (frame_count == MovieSub.message_start) {
            return (strtok(0, ":"));
        }
    }
    return (0);
}

static void MovieSub_ResetStream()
{
    if (MovieSub.fp) {
        rewind(MovieSub.fp);
        MovieSub.message_start = 0;
        MovieSub.message_duration = 0;
    }
}

static size_t MovieSub_GetDuration()
{
    return (MovieSub.message_duration);
}

bool RawDumpInProgress = false;
bool PrevSRAMState;

extern uint8_t AllocatedRewindStates;
extern uint8_t SloMo, EMUPause;
char MovieFrameStr[10];
u1 MovieForcedLengthEnabled = 0;
uint32_t MovieForcedLength = 0, MovieForcedLengthInternal;
uint8_t MoviePassWaiting = 0;
unsigned char MovieProcessing;
uint8_t MovieRecordWinVal;

struct
{
    FILE* fp;
    size_t frames_replayed;
    struct
    {
        uint32_t A;
        uint32_t B;
        uint32_t C;
        uint32_t D;
        uint32_t E;
    } last_joy_state;
} old_movie;

static void DumpVideoFrame(bool playback_over)
{
    if (RawDumpInProgress) {
        if (playback_over && MovieForcedLengthEnabled) {
            SetMovieMode(MOVIE_ENDING_DUMPING);
            // Disable Input
            JoyAOrig = JoyBOrig = JoyCOrig = JoyDOrig = JoyEOrig = 0;
        }

        if ((playback_over && !MovieForcedLengthEnabled) || (MovieForcedLengthEnabled && (MovieForcedLengthInternal >= MovieForcedLength))) {
            switch (MovieProcessing) {
            case MOVIE_DUMPING_NEW:
                zmv_replay_finished();
                MovieSub_Close();
                break;
            case MOVIE_DUMPING_OLD:
                fclose(old_movie.fp);
                MovieSub_Close();
                break;
            }
            raw_video_close();
            RawDumpInProgress = false;
            SetMovieMode(MOVIE_OFF);
            if ((MovieVideoMode == 5) && (movie_current_pass < md_custom_passes)) {
                movie_current_pass++;
                MoviePassWaiting = 1;
            } else {
                movie_current_pass = 0;
                MovieForcedLengthEnabled = 0;
            }
        } else {
            raw_video_write_frame();
            MovieForcedLengthInternal++;
            if (MovieForcedLengthEnabled == 2) {
                MovieForcedLength++;
            }
        }
    }
}

/* Code to playback old ZMVs */

static void OldMovieReplay()
{
    uint8_t byte;

    if (fread(&byte, 1, 1, old_movie.fp)) {
        if (byte < 2) // 1 or 0 are correct values
        {
            char* sub;

            if (byte == 0) // 0 means the input has changed
            {
                IGNORE_RESULT(fread(&old_movie.last_joy_state.A, 1, 4, old_movie.fp));
                IGNORE_RESULT(fread(&old_movie.last_joy_state.B, 1, 4, old_movie.fp));
                IGNORE_RESULT(fread(&old_movie.last_joy_state.C, 1, 4, old_movie.fp));
                IGNORE_RESULT(fread(&old_movie.last_joy_state.D, 1, 4, old_movie.fp));
                IGNORE_RESULT(fread(&old_movie.last_joy_state.E, 1, 4, old_movie.fp));
            }

            JoyAOrig = old_movie.last_joy_state.A;
            JoyBOrig = old_movie.last_joy_state.B;
            JoyCOrig = old_movie.last_joy_state.C;
            JoyDOrig = old_movie.last_joy_state.D;
            JoyEOrig = old_movie.last_joy_state.E;

            if ((sub = MovieSub_GetData(old_movie.frames_replayed))) {
                Msgptr = sub;
                MessageOn = MovieSub_GetDuration();
            }

            DumpVideoFrame(false);
            old_movie.frames_replayed++;
        } else // anything else is bad - the file isn't a movie.
        {
            SetMovieMode(MOVIE_OFF);
            MessageOn = 0;
            fclose(old_movie.fp);
            MovieSub_Close();
        }
    } else {
        if (old_movie.frames_replayed) {
            Msgptr = "MOVIE FINISHED.";
        } else {
            Msgptr = "STATE LOADED.";
        }

        MessageOn = MsgCount;
        SetMovieMode(MOVIE_OFF);

        fclose(old_movie.fp);
        MovieSub_Close();

        DumpVideoFrame(true);
    }
}

static uint8_t oldframeskip = 0;
static uint8_t oldmaxskip = 0;

static void OldMoviePlay(FILE* fp)
{
    uint8_t RecData[16];
    extern uint8_t NextLineCache;
    void loadstate2();

    memset(&old_movie, 0, sizeof(old_movie));
    old_movie.fp = fp;

    SetMovieMode(MOVIE_OLD_PLAY);
    loadstate2();
    SetMovieMode(MOVIE_OFF);

    fseek(fp, Totalbyteloaded, SEEK_SET);
    IGNORE_RESULT(fread(RecData, 1, 16, fp));
    printf("Movie made with version: %d\n", RecData[1]);

    if (RecData[2] == 1) {
        timer2upd = bytes_to_uint32(RecData + 3);
        curexecstate = bytes_to_uint32(RecData + 7);
        nmiprevaddrl = 0;
        nmiprevaddrh = 0;
        nmirept = 0;
        nmiprevline = 224;
        nmistatus = 0;
        spcnumread = 0;
        NextLineCache = 0;
    }

    if (soundon == RecData[0]) {
        if (ramsize) {
            IGNORE_RESULT(fread(sram, 1, ramsize, fp));
        }

        SetMovieMode(MOVIE_OLD_PLAY);
        sramsavedis = 1;
        DSPMem[0x08] = 0;
        DSPMem[0x18] = 0;
        DSPMem[0x28] = 0;
        DSPMem[0x38] = 0;
        DSPMem[0x48] = 0;
        DSPMem[0x58] = 0;
        DSPMem[0x68] = 0;
        DSPMem[0x78] = 0;

        Msgptr = "OLD MOVIE REPLAYING.";

        oldframeskip = frameskip;
        oldmaxskip = maxskip;
        frameskip = 0;
        maxskip = 0;
    } else {
        Msgptr = (!soundon) ? "MUST PLAY WITH SOUND ON." : "MUST PLAY WITH SOUND OFF.";
        fclose(fp);
    }
    MessageOn = MsgCount;
}

void MovieInsertChapter(void)
{
    switch (MovieProcessing) {
    case MOVIE_PLAYBACK:
    case MOVIE_DUMPING_NEW: // replaying - external
        zmv_add_chapter();
        Msgptr = "EXTERNAL CHAPTER ADDED.";
        break;
    case MOVIE_RECORD: // recording - internal
        if (zmv_insert_chapter()) {
            Msgptr = "INTERNAL CHAPTER ADDED.";
        } else {
            Msgptr = "";
        }
        break;
    case MOVIE_OLD_PLAY:
        Msgptr = "OLD MOVIES DO NOT SUPPORT CHAPTERS.";
        break;
    default: // no movie processing
        Msgptr = "NO MOVIE PROCESSING.";
    }

    MessageOn = MsgCount;
}

void MovieSeekAhead(void)
{
    switch (MovieProcessing) {
    case MOVIE_PLAYBACK: // replay seeking ok
        if (zmv_next_chapter()) {
            Msgptr = "NEXT CHAPTER LOADED.";
        } else {
            Msgptr = "NO CHAPTERS AHEAD.";
        }
        break;
    case MOVIE_RECORD: // record will use MZTs
        Msgptr = "NO SEEKING DURING RECORD.";
        break;
    case MOVIE_OLD_PLAY:
        Msgptr = "OLD MOVIES DO NOT SUPPORT CHAPTERS.";
        break;
    default:
        Msgptr = "NO MOVIE PROCESSING.";
    }

    MessageOn = MsgCount;
}

void MovieSeekBehind(void)
{
    switch (MovieProcessing) {
    case MOVIE_PLAYBACK: // replay seeking ok
        zmv_prev_chapter();
        MovieSub_ResetStream();
        Msgptr = "PREVIOUS CHAPTER LOADED.";
        break;
    case MOVIE_RECORD: // record will use MZTs
        Msgptr = "NO SEEKING DURING RECORD.";
        break;
    case MOVIE_OLD_PLAY:
        Msgptr = "OLD MOVIES DO NOT SUPPORT CHAPTERS.";
        break;
    default:
        Msgptr = "NO MOVIE PROCESSING.";
    }

    MessageOn = MsgCount;
}

void Replay()
{
    if (zmv_replay()) {
        char* sub;
        if ((sub = MovieSub_GetData(zmv_frames_replayed()))) {
            Msgptr = sub;
            MessageOn = MovieSub_GetDuration();
        }

        DumpVideoFrame(false);
    } else {
        if (zmv_frames_replayed()) {
            Msgptr = "MOVIE FINISHED.";
        } else {
            Msgptr = "STATE LOADED.";
        }
        MessageOn = MsgCount;
        SetMovieMode(MOVIE_OFF);

        zmv_replay_finished();
        zmv_dealloc_rewind_buffer();
        MovieSub_Close();

        DumpVideoFrame(true);

        SRAMState = PrevSRAMState;
    }
}

void ProcessMovies()
{
    switch (MovieProcessing) {
    case MOVIE_PLAYBACK:
    case MOVIE_DUMPING_NEW:
        Replay();
        break;
    case MOVIE_RECORD:
        zmv_record(EMUPause ? true : false, ComboCounter, SloMo);
        break;
    case MOVIE_OLD_PLAY:
    case MOVIE_DUMPING_OLD:
        OldMovieReplay();
        break;
    case MOVIE_ENDING_DUMPING:
        DumpVideoFrame(true);
        break;
    }
}

void ResetDuringMovie(void)
{
    zmv_record_command(zmv_command_reset);
    SetMovieMode(MOVIE_OFF);
    GUIDoReset();
    SetMovieMode(MOVIE_RECORD);
}

void SkipMovie(void)
{
    MovieRecordWinVal = 0;
}

void MovieStop()
{
    if (MovieProcessing && !MovieWaiting) {
        if ((MovieForcedLengthEnabled == 2) || movie_current_pass) {
            MovieForcedLengthEnabled = 1;
            MovieForcedLength = MovieForcedLengthInternal;
        }

        switch (MovieProcessing) {
        case MOVIE_PLAYBACK:
        case MOVIE_DUMPING_NEW:
            zmv_replay_finished();
            MovieSub_Close();
            MovieForcedLengthInternal = MovieForcedLength;
            DumpVideoFrame(true);
            MessageOn = 0;
            break;

        case MOVIE_RECORD:
            zmv_record_finish();
            if (!zmv_frames_recorded()) {
                Msgptr = "STATE SAVED.";
                MessageOn = MsgCount;
            }
            break;
        case MOVIE_OLD_PLAY:
        case MOVIE_DUMPING_OLD:
            fclose(old_movie.fp);
            MovieSub_Close();
            MovieForcedLengthInternal = MovieForcedLength;
            DumpVideoFrame(true);
            MessageOn = 0;
            break;
        case MOVIE_ENDING_DUMPING:
            MovieForcedLengthInternal = MovieForcedLength;
            DumpVideoFrame(true);
            break;
        }

        zmv_dealloc_rewind_buffer();
        SetMovieMode(MOVIE_OFF);
        SRAMState = PrevSRAMState;

        frameskip = oldframeskip;
        maxskip = oldmaxskip;
    }
    MovieWaiting = false;
}

void InitRewindVarsForMovie();

void MoviePlay()
{
    if (!MovieProcessing) {
        size_t fname_len = strlen(ZSaveName);
        FILE* fp;

        PrevSRAMState = SRAMState;
        SRAMState = true;

        GUIQuit = 2;
        setextension(ZSaveName, "zmv");
        ZSaveName[fname_len - 1] = CMovieExt;

        if ((fp = fopen_dir(ZMoviePath, ZSaveName, "rb"))) {
            char header_buf[3];
            IGNORE_RESULT(fread(header_buf, 3, 1, fp));

            if (!strncmp("ZMV", header_buf, 3)) // New Enhanced Format
            {
                fclose(fp);

                if (zmv_open(ZSaveName)) {
                    InitRewindVarsForMovie();
                    zmv_alloc_rewind_buffer(AllocatedRewindStates);
                    SetMovieMode(MOVIE_PLAYBACK);
                    setextension(ZSaveName, "sub");
                    if (isdigit(CMovieExt)) {
                        ZSaveName[fname_len - 1] = CMovieExt;
                    }
                    MovieSub_Open(ZSaveName);
                    MessageOn = MsgCount;

                    oldframeskip = frameskip;
                    oldmaxskip = maxskip;
                    frameskip = 0;
                    maxskip = 0;
                } else {
                    Msgptr = "MOVIE COULD NOT BE STARTED.";
                    MessageOn = MsgCount;
                }
            } else // Old Pathetic Format
            {
                OldMoviePlay(fp);
            }
        } else {
            Msgptr = "MOVIE COULD NOT BE OPENED.";
            MessageOn = MsgCount;
        }
    }
}

void MovieRecord()
{
    if (MovieProcessing == MOVIE_PLAYBACK) {
        zmv_replay_to_record();
        MovieProcessing = MOVIE_RECORD;
    }

    if (!MovieProcessing) {
        size_t fname_len = strlen(ZSaveName);

        setextension(ZSaveName, "zmv");
        ZSaveName[fname_len - 1] = CMovieExt;

        if (MovieRecordWinVal == 1) {
            // Erase old ZMV
            remove_dir(ZMoviePath, ZSaveName);
            MovieRecordWinVal = 0;
        }

        if (access_dir(ZMoviePath, ZSaveName, F_OK)) {
            DIR* dir;

            PrevSRAMState = SRAMState;
            SRAMState = true;

            SetMovieMode(MOVIE_RECORD);
            if (zmv_create(ZSaveName)) {
                InitRewindVarsForMovie();
                zmv_alloc_rewind_buffer(AllocatedRewindStates);
                Msgptr = "MOVIE RECORDING.";
                MessageOn = MsgCount;

                oldframeskip = frameskip;
                oldmaxskip = maxskip;
                frameskip = 0;
                maxskip = 0;

                // Cleanup old MZTs
                mzt_chdir_up();
                if ((dir = opendir(ZMoviePath))) {
                    struct dirent* entry;
                    while ((entry = readdir(dir))) {
                        if (*entry->d_name != '.') {
                            remove_dir(ZMoviePath, entry->d_name);
                        }
                    }
                    closedir(dir);
                }
                mzt_chdir_down();
            } else {
                SetMovieMode(MOVIE_OFF);
            }
        } else {
            MovieRecordWinVal = 1;
        }
    }
}

void MovieAppend()
{
    if (!MovieProcessing) {
        size_t fname_len = strlen(ZSaveName);

        setextension(ZSaveName, "zmv");
        ZSaveName[fname_len - 1] = CMovieExt;

        if (zmv_append(ZSaveName)) {
            PrevSRAMState = SRAMState;
            SRAMState = true;

            SetMovieMode(MOVIE_RECORD);
            InitRewindVarsForMovie();
            zmv_alloc_rewind_buffer(AllocatedRewindStates);
            Msgptr = "MOVIE APPENDING.";
            MessageOn = MsgCount;

            oldframeskip = frameskip;
            oldmaxskip = maxskip;
            frameskip = 0;
            maxskip = 0;
        }
    }
}

void GetMovieFrameStr(void)
{
    *MovieFrameStr = 0;
    switch (MovieProcessing) {
    case MOVIE_PLAYBACK:
    case MOVIE_DUMPING_NEW:
        sprintf(MovieFrameStr, "%u", (unsigned int)zmv_frames_replayed());
        break;
    case MOVIE_RECORD:
        sprintf(MovieFrameStr, "%u", (unsigned int)zmv_frames_recorded());
        break;
    case MOVIE_OLD_PLAY:
    case MOVIE_DUMPING_OLD:
        sprintf(MovieFrameStr, "%u", (unsigned int)(old_movie.frames_replayed));
        break;
    }
}

void MovieDumpRaw(void)
{
    MoviePassWaiting = 0;
    if (!MovieProcessing) {
        MoviePlay();
        if ((MovieVideoMode == 5) && !movie_current_pass) {
            movie_current_pass = 1;
        }
        if (MovieForcedLengthEnabled == 2) {
            MovieForcedLength = 1;
        }
        MovieForcedLengthInternal = 0;
        RawDumpInProgress = raw_video_open();

        switch (MovieProcessing) {
        case MOVIE_PLAYBACK:
            SetMovieMode(MOVIE_DUMPING_NEW);
            break;
        case MOVIE_OLD_PLAY:
            SetMovieMode(MOVIE_DUMPING_OLD);
            break;
        }
    }
}

bool MovieInProgress()
{
    return ((MovieProcessing == MOVIE_PLAYBACK) || (MovieProcessing == MOVIE_RECORD));
}
