/*
 * Copyright (C) 2002-2024 D2X Project
 *
 * 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/>.
 */

#define AUDIO

#include <chrono>
#include <cstring>
#include <deque>
#include <memory>
#include <thread>

#include "byteswap.h"
#include "chrono_timer.h"
#include "decoders.h"
#include "mvelib.h"
#include "mve_audio.h"

#ifdef AUDIO
#include "movie_sound.h"
#endif

#define MVE_OPCODE_ENDOFSTREAM 0x00           // mcmd_end
#define MVE_OPCODE_ENDOFCHUNK 0x01            // mcmd_next
#define MVE_OPCODE_CREATETIMER 0x02           // mcmd_syncInit
#define MVE_OPCODE_INITAUDIOBUFFERS 0x03      // mcmd_sndConfigure
#define MVE_OPCODE_STARTSTOPAUDIO 0x04        // mcmd_sndSync
#define MVE_OPCODE_INITVIDEOBUFFERS 0x05      // mcmd_nfConfig
#define MVE_OPCODE_UNKNOWN_06 0x06            // mcmd_nfDecomp
#define MVE_OPCODE_DISPLAYVIDEO 0x07          // mcmd_sfShowFrame
#define MVE_OPCODE_AUDIOFRAMEDATA 0x08        // mcmd_sndAdd
#define MVE_OPCODE_AUDIOFRAMESILENCE 0x09     // mcmd_sndSilence
#define MVE_OPCODE_INITVIDEOMODE 0x0A         // mcmd_gfxMode
#define MVE_OPCODE_UNKNOWN_0B 0x0B            // mcmd_palMakeSynthPalette
#define MVE_OPCODE_SETPALETTE 0x0C            // mcmd_palLoadPalette
#define MVE_OPCODE_SETPALETTECOMPRESSED 0x0D  // mcmd_palLoadCompPalette
#define MVE_OPCODE_UNKNOWN_0E 0x0E            // mcmd_nfChanges
#define MVE_OPCODE_SETDECODINGMAP 0x0F        // mcmd_nfParms
#define MVE_OPCODE_UNKNOWN_10 0x10            // mcmd_nfDecompChg
#define MVE_OPCODE_VIDEODATA 0x11             // mcmd_nfPkDecomp

#define MVE_AUDIO_FLAGS_STEREO 1
#define MVE_AUDIO_FLAGS_16BIT 2
#define MVE_AUDIO_FLAGS_COMPRESSED 4

int g_spdFactorNum = 0;
static int g_spdFactorDenom = 10;
static int g_frameUpdated = 0;

static short get_short(const unsigned char *data) {
  return D3::convert_le<int16_t>(data[0] | (data[1] << 8));
}

static unsigned short get_ushort(const unsigned char *data) {
  return D3::convert_le<uint16_t>(data[0] | (data[1] << 8));
}

static int get_int(const unsigned char *data) {
  return D3::convert_le<int32_t>(data[0] | (data[1] << 8) | (data[2] << 16) | (data[3] << 24));
}

static unsigned int unhandled_chunks[32 * 256];

static int default_seg_handler(unsigned char major, unsigned char minor, unsigned char *data, int len, void *context) {
  unhandled_chunks[major << 8 | minor]++;
  return 1;
}

/*************************
 * general handlers
 *************************/
static int end_movie_handler(unsigned char major, unsigned char minor, unsigned char *data, int len, void *context) {
  return 0;
}

/*************************
 * timer handlers
 *************************/

/*
 * timer variables
 */
static int timer_created = 0;
static int micro_frame_delay = 0;
static int timer_started = 0;
static unsigned long int timer_expire = 0;

unsigned long int timer_getmicroseconds() {
  return D3::ChronoTimer::GetTimeUS();
}

void timer_sleepmicroseconds(unsigned long int usec) {
  std::this_thread::sleep_for(std::chrono::microseconds(usec));
}

static int create_timer_handler(unsigned char major, unsigned char minor, unsigned char *data, int len, void *context) {
  long temp;

  if (timer_created)
    return 1;
  else
    timer_created = 1;

  micro_frame_delay = get_int(data) * (int)get_short(data + 4);
  if (g_spdFactorNum != 0) {
    temp = micro_frame_delay;
    temp *= g_spdFactorNum;
    temp /= g_spdFactorDenom;
    micro_frame_delay = (int)temp;
  }

  return 1;
}

static void timer_stop() {
  timer_expire = 0;
  timer_started = 0;
}

static void timer_start() {
  timer_expire = timer_getmicroseconds();
  timer_expire += micro_frame_delay;
  timer_started = 1;
}

#ifdef AUDIO
static std::unique_ptr<D3::MovieSoundDevice> snd_ds;
static int mve_audio_enabled = 1;
#else
static int mve_audio_enabled = 0;
#endif

static void do_timer_wait() {
  uint64_t ts;
  uint64_t tv;

  if (!timer_started)
    return;

  tv = timer_getmicroseconds();
  if (tv > timer_expire)
    goto end;

  ts = timer_expire - tv;

  timer_sleepmicroseconds(ts);

end:
  timer_expire += micro_frame_delay;
}

/*************************
 * audio handlers
 *************************/

static int create_audiobuf_handler(unsigned char major, unsigned char minor, unsigned char *data, int len,
                                   void *context) {
#ifdef AUDIO
  if (!mve_audio_enabled)
    return 1;

  int flags = get_ushort(data + 2);
  int sample_rate = get_ushort(data + 4);
  int desired_buffer = get_int(data + 6);

  int channels = (flags & MVE_AUDIO_FLAGS_STEREO) ? 2 : 1;
  int sample_size = (flags & MVE_AUDIO_FLAGS_16BIT) ? 2 : 1;

  bool is_compressed = false;
  if (minor > 0 && (flags & MVE_AUDIO_FLAGS_COMPRESSED)) {
    is_compressed = true;
  }

  snd_ds = std::make_unique<D3::MovieSoundDevice>(sample_rate, sample_size, channels, 4096, is_compressed);
#endif

  return 1;
}

static int play_audio_handler(unsigned char major, unsigned char minor, unsigned char *data, int len, void *context) {
#ifdef AUDIO
  if (snd_ds && snd_ds->IsInitialized()) {
    snd_ds->Play();
  }
#endif
  return 1;
}

static int audio_data_handler(unsigned char major, unsigned char minor, unsigned char *data, int len, void *context) {
#ifdef AUDIO
  static const int selected_chan = 1;
  if (snd_ds && snd_ds->IsInitialized()) {
    int chan = get_ushort(data + 2);
    int size = get_ushort(data + 4);
    if (chan & selected_chan) {
      void *buf = malloc(size);
      if (major == MVE_OPCODE_AUDIOFRAMEDATA) {
        mveaudio_process((char *)buf, data, snd_ds->GetSampleSize(), snd_ds->IsCompressed());
      } else {
        // SILENCE, MORTALS!
        memset(data, 0, size);
      }
      snd_ds->FillBuffer((char *)buf, size);
      free(buf);
    }
  }
#endif

  return 1;
}

/*************************
 * video handlers
 *************************/

static int videobuf_created = 0;
static int video_initialized = 0;
int g_width, g_height;
void *g_vBuffers = nullptr, *g_vBackBuf1, *g_vBackBuf2;

static int g_destX, g_destY;
static int g_screenWidth, g_screenHeight;
static unsigned char *g_pCurMap = nullptr;
static int g_nMapLength = 0;
static int g_truecolor;

static int create_videobuf_handler(unsigned char major, unsigned char minor, unsigned char *data, int len,
                                   void *context) {
  short w, h;
  short count, truecolor;

  if (videobuf_created)
    return 1;
  else
    videobuf_created = 1;

  w = get_short(data);
  h = get_short(data + 2);

  if (minor > 0) {
    count = get_short(data + 4);
  } else {
    count = 1;
  }

  if (minor > 1) {
    truecolor = get_short(data + 6);
  } else {
    truecolor = 0;
  }

  g_width = w << 3;
  g_height = h << 3;

  /* TODO: * 4 causes crashes on some files */
  /* only malloc once */
  if (g_vBuffers == nullptr)
    g_vBackBuf1 = g_vBuffers = mve_alloc(g_width * g_height * 8);
  if (truecolor) {
    g_vBackBuf2 = (unsigned short *)g_vBackBuf1 + (g_width * g_height);
  } else {
    g_vBackBuf2 = (unsigned char *)g_vBackBuf1 + (g_width * g_height);
  }

  memset(g_vBackBuf1, 0, g_width * g_height * 4);

  g_truecolor = truecolor;

  return 1;
}

static int display_video_handler(unsigned char major, unsigned char minor, unsigned char *data, int len,
                                 void *context) {
  if (g_destX == -1) // center it
    g_destX = (g_screenWidth - g_width) >> 1;
  if (g_destY == -1) // center it
    g_destY = (g_screenHeight - g_height) >> 1;

  mve_showframe((unsigned char *)g_vBackBuf1, g_width, g_height, 0, 0, g_width, g_height, g_destX, g_destY,
                g_truecolor);

  g_frameUpdated = 1;

  return 1;
}

static int init_video_handler(unsigned char major, unsigned char minor, unsigned char *data, int len, void *context) {
  short width, height;

  if (video_initialized)
    return 1; /* maybe we actually need to change width/height here? */
  else
    video_initialized = 1;

  width = get_short(data);
  height = get_short(data + 2);
  g_screenWidth = width;
  g_screenHeight = height;

  return 1;
}

static int video_palette_handler(unsigned char major, unsigned char minor, unsigned char *data, int len,
                                 void *context) {
  short start, count;
  unsigned char *p;

  start = get_short(data);
  count = get_short(data + 2);

  p = data + 4;

  mve_setpalette(p - 3 * start, start, count);

  return 1;
}

static int video_codemap_handler(unsigned char major, unsigned char minor, unsigned char *data, int len,
                                 void *context) {
  g_pCurMap = data;
  g_nMapLength = len;
  return 1;
}

static int video_data_handler(unsigned char major, unsigned char minor, unsigned char *data, int len, void *context) {
  unsigned char *temp;

  short nFrameHot = get_short(data);
  short nFrameCold = get_short(data + 2);
  short nXoffset = get_short(data + 4);
  short nYoffset = get_short(data + 6);
  short nXsize = get_short(data + 8);
  short nYsize = get_short(data + 10);
  unsigned short nFlags = get_ushort(data + 12);

  if (nFlags & 1) {
    temp = (unsigned char *)g_vBackBuf1;
    g_vBackBuf1 = g_vBackBuf2;
    g_vBackBuf2 = temp;
  }

  /* convert the frame */
  if (g_truecolor) {
    decodeFrame16((unsigned char *)g_vBackBuf1, g_pCurMap, g_nMapLength, data + 14, len - 14);
  } else {
    decodeFrame8((unsigned char *)g_vBackBuf1, g_pCurMap, g_nMapLength, data + 14, len - 14);
  }

  return 1;
}

static int end_chunk_handler(unsigned char major, unsigned char minor, unsigned char *data, int len, void *context) {
  g_pCurMap = nullptr;
  return 1;
}

void MVE_ioCallbacks(mve_cb_Read io_read) { mve_read = io_read; }

void MVE_memCallbacks(mve_cb_Alloc mem_alloc, mve_cb_Free mem_free) {
  mve_alloc = mem_alloc;
  mve_free = mem_free;
}

void MVE_sfCallbacks(mve_cb_ShowFrame showframe) { mve_showframe = showframe; }

void MVE_palCallbacks(mve_cb_SetPalette setpalette) { mve_setpalette = setpalette; }

MVESTREAM* MVE_rmPrepMovie(void *src, int x, int y, int track) {
  MVESTREAM *mve;
  mve = mve_open(src);

  if (!mve)
    return nullptr;

  g_destX = x;
  g_destY = y;

  for (int i = 0; i < 32; i++)
    mve_set_handler(mve, i, default_seg_handler);

  mve_set_handler(mve, MVE_OPCODE_ENDOFSTREAM, end_movie_handler);
  mve_set_handler(mve, MVE_OPCODE_ENDOFCHUNK, end_chunk_handler);
  mve_set_handler(mve, MVE_OPCODE_CREATETIMER, create_timer_handler);
  mve_set_handler(mve, MVE_OPCODE_INITAUDIOBUFFERS, create_audiobuf_handler);
  mve_set_handler(mve, MVE_OPCODE_STARTSTOPAUDIO, play_audio_handler);
  mve_set_handler(mve, MVE_OPCODE_INITVIDEOBUFFERS, create_videobuf_handler);

  mve_set_handler(mve, MVE_OPCODE_DISPLAYVIDEO, display_video_handler);
  mve_set_handler(mve, MVE_OPCODE_AUDIOFRAMEDATA, audio_data_handler);
  mve_set_handler(mve, MVE_OPCODE_AUDIOFRAMESILENCE, audio_data_handler);
  mve_set_handler(mve, MVE_OPCODE_INITVIDEOMODE, init_video_handler);

  mve_set_handler(mve, MVE_OPCODE_SETPALETTE, video_palette_handler);
  mve_set_handler(mve, MVE_OPCODE_SETPALETTECOMPRESSED, default_seg_handler);

  mve_set_handler(mve, MVE_OPCODE_SETDECODINGMAP, video_codemap_handler);

  mve_set_handler(mve, MVE_OPCODE_VIDEODATA, video_data_handler);

  mve_play_next_chunk(mve); /* video initialization chunk */
  if (mve_audio_enabled)
    mve_play_next_chunk(mve); /* audio initialization chunk */

  return mve;
}

void MVE_getVideoSpec(MVE_videoSpec *vSpec) {
  vSpec->screenWidth = g_screenWidth;
  vSpec->screenHeight = g_screenHeight;
  vSpec->width = g_width;
  vSpec->height = g_height;
  vSpec->truecolor = g_truecolor;
}

int MVE_rmStepMovie(MVESTREAM *mve) {
  static int init_timer = 0;
  int cont = 1;

  if (!timer_started)
    timer_start();

  while (cont && !g_frameUpdated) // make a "step" be a frame, not a chunk...
    cont = mve_play_next_chunk(mve);
  g_frameUpdated = 0;

  if (!cont)
    return MVE_ERR_EOF;

  if (micro_frame_delay && !init_timer) {
    timer_start();
    init_timer = 1;
  }

  do_timer_wait();

  return 0;
}

void MVE_rmEndMovie(MVESTREAM *mve) {
  timer_stop();
  timer_created = 0;

#ifdef AUDIO
  snd_ds.reset();
#endif

  mve_free(g_vBuffers);
  g_vBuffers = nullptr;
  g_pCurMap = nullptr;
  g_nMapLength = 0;
  videobuf_created = 0;
  video_initialized = 0;

  mve_close(mve);
  mve = nullptr;
}

void MVE_rmHoldMovie() { timer_started = 0; }

void MVE_sndInit(const bool enable) {
#ifdef AUDIO
  if (enable) {
    mve_audio_enabled = 1;
  } else {
    mve_audio_enabled = 0;
  }
#endif
}
