#include <dpmi.h>
#include "quakedef.h"
#include "dosisms.h"

extern  cvar_t  bgmvolume;

#define ADDRESS_MODE_HSG    0
#define ADDRESS_MODE_RED_BOOK 1

#define STATUS_ERROR_BIT  0x8000
#define STATUS_BUSY_BIT   0x0200
#define STATUS_DONE_BIT   0x0100
#define STATUS_ERROR_MASK 0x00ff

#define ERROR_WRITE_PROTECT   0
#define ERROR_UNKNOWN_UNIT    1
#define ERROR_DRIVE_NOT_READY 2
#define ERROR_UNKNOWN_COMMAND 3
#define ERROR_CRC_ERROR     4
#define ERROR_BAD_REQUEST_LEN 5
#define ERROR_SEEK_ERROR    6
#define ERROR_UNKNOWN_MEDIA   7
#define ERROR_SECTOR_NOT_FOUND  8
#define ERROR_OUT_OF_PAPER    9
#define ERROR_WRITE_FAULT   10
#define ERROR_READ_FAULT    11
#define ERROR_GENERAL_FAILURE 12
#define ERROR_RESERVED_13   13
#define ERROR_RESERVED_14   14
#define ERROR_BAD_DISK_CHANGE 15

#define COMMAND_READ      3
#define COMMAND_WRITE     12
#define COMMAND_PLAY_AUDIO    132
#define COMMAND_STOP_AUDIO    133
#define COMMAND_RESUME_AUDIO  136

#define READ_REQUEST_AUDIO_CHANNEL_INFO   4
#define READ_REQUEST_DEVICE_STATUS      6
#define READ_REQUEST_MEDIA_CHANGE     9
#define READ_REQUEST_AUDIO_DISK_INFO    10
#define READ_REQUEST_AUDIO_TRACK_INFO   11
#define READ_REQUEST_AUDIO_STATUS     15

#define WRITE_REQUEST_EJECT         0
#define WRITE_REQUEST_RESET         2
#define WRITE_REQUEST_AUDIO_CHANNEL_INFO  3

#define STATUS_DOOR_OPEN          0x00000001
#define STATUS_DOOR_UNLOCKED        0x00000002
#define STATUS_RAW_SUPPORT          0x00000004
#define STATUS_READ_WRITE         0x00000008
#define STATUS_AUDIO_SUPPORT        0x00000010
#define STATUS_INTERLEAVE_SUPPORT     0x00000020
#define STATUS_BIT_6_RESERVED       0x00000040
#define STATUS_PREFETCH_SUPPORT       0x00000080
#define STATUS_AUDIO_MANIPLUATION_SUPPORT 0x00000100
#define STATUS_RED_BOOK_ADDRESS_SUPPORT   0x00000200

#define MEDIA_NOT_CHANGED   1
#define MEDIA_STATUS_UNKNOWN  0
#define MEDIA_CHANGED     -1

#define AUDIO_CONTROL_MASK        0xd0
#define AUDIO_CONTROL_DATA_TRACK    0x40
#define AUDIO_CONTROL_AUDIO_2_TRACK   0x00
#define AUDIO_CONTROL_AUDIO_2P_TRACK  0x10
#define AUDIO_CONTROL_AUDIO_4_TRACK   0x80
#define AUDIO_CONTROL_AUDIO_4P_TRACK  0x90

#define AUDIO_STATUS_PAUSED       0x0001

#pragma pack(1)

struct playAudioRequest
{
  char  addressingMode;
  int   startLocation;
  int   sectors;
};

struct readRequest
{
  char  mediaDescriptor;
  short bufferOffset;
  short bufferSegment;
  short length;
  short startSector;
  int   volumeID;
};

struct writeRequest
{
  char  mediaDescriptor;
  short bufferOffset;
  short bufferSegment;
  short length;
  short startSector;
  int   volumeID;
};

struct cd_request
{
  char  headerLength;
  char  unit;
  char  command;
  short status;
  char  reserved[8];
  union
  {
    struct  playAudioRequest  playAudio;
    struct  readRequest     read;
    struct  writeRequest    write;
  } x;
};


struct audioChannelInfo_s
{
  char  code;
  char  channel0input;
  char  channel0volume;
  char  channel1input;
  char  channel1volume;
  char  channel2input;
  char  channel2volume;
  char  channel3input;
  char  channel3volume;
};

struct deviceStatus_s
{
  char  code;
  int   status;
};

struct mediaChange_s
{
  char  code;
  char  status;
};

struct audioDiskInfo_s
{
  char  code;
  char  lowTrack;
  char  highTrack;
  int   leadOutStart;
};

struct audioTrackInfo_s
{
  char  code;
  char  track;
  int   start;
  char  control;
};

struct audioStatus_s
{
  char  code;
  short status;
  int   PRstartLocation;
  int   PRendLocation;
};

struct reset_s
{
  char  code;
};

union readInfo_u
{
  struct audioChannelInfo_s audioChannelInfo;
  struct deviceStatus_s   deviceStatus;
  struct mediaChange_s    mediaChange;
  struct audioDiskInfo_s    audioDiskInfo;
  struct audioTrackInfo_s   audioTrackInfo;
  struct audioStatus_s    audioStatus;
  struct reset_s        reset;
};

#pragma pack()

#define MAXIMUM_TRACKS      32

typedef struct
{
  int     start;
  int     length;
  qboolean  isData;
} track_info;

typedef struct
{
  qboolean  valid;
  int     leadOutAddress;
  track_info  track[MAXIMUM_TRACKS];
  byte    lowTrack;
  byte    highTrack;
} cd_info;

static struct cd_request  *cdRequest;
static union readInfo_u   *readInfo;
static cd_info        cd;

static qboolean playing = false;
static qboolean wasPlaying = false;
static qboolean mediaCheck = false;
static qboolean initialized = false;
static qboolean enabled = true;
static qboolean playLooping = false;
static short  cdRequestSegment;
static short  cdRequestOffset;
static short  readInfoSegment;
static short  readInfoOffset;
static byte   remap[256];
static byte   cdrom;
static byte   playTrack;
static byte   cdvolume;


static int RedBookToSector(int rb)
{
  byte  minute;
  byte  second;
  byte  frame;

  minute = (rb >> 16) & 0xff;
  second = (rb >> 8) & 0xff;
  frame = rb & 0xff;
  return minute * 60 * 75 + second * 75 + frame;
}


static void CDAudio_Reset(void)
{
  cdRequest->headerLength = 13;
  cdRequest->unit = 0;
  cdRequest->command = COMMAND_WRITE;
  cdRequest->status = 0;

  cdRequest->x.write.mediaDescriptor = 0;
  cdRequest->x.write.bufferOffset = readInfoOffset;
  cdRequest->x.write.bufferSegment = readInfoSegment;
  cdRequest->x.write.length = sizeof(struct reset_s);
  cdRequest->x.write.startSector = 0;
  cdRequest->x.write.volumeID = 0;

  readInfo->reset.code = WRITE_REQUEST_RESET;

  regs.x.ax = 0x1510;
  regs.x.cx = cdrom;
  regs.x.es = cdRequestSegment;
  regs.x.bx = cdRequestOffset;
  dos_int86 (0x2f);
}


static void CDAudio_Eject(void)
{
  cdRequest->headerLength = 13;
  cdRequest->unit = 0;
  cdRequest->command = COMMAND_WRITE;
  cdRequest->status = 0;

  cdRequest->x.write.mediaDescriptor = 0;
  cdRequest->x.write.bufferOffset = readInfoOffset;
  cdRequest->x.write.bufferSegment = readInfoSegment;
  cdRequest->x.write.length = sizeof(struct reset_s);
  cdRequest->x.write.startSector = 0;
  cdRequest->x.write.volumeID = 0;

  readInfo->reset.code = WRITE_REQUEST_EJECT;

  regs.x.ax = 0x1510;
  regs.x.cx = cdrom;
  regs.x.es = cdRequestSegment;
  regs.x.bx = cdRequestOffset;
  dos_int86 (0x2f);
}


static int CDAudio_GetAudioTrackInfo(byte track, int *start)
{
  byte  control;

  cdRequest->headerLength = 13;
  cdRequest->unit = 0;
  cdRequest->command = COMMAND_READ;
  cdRequest->status = 0;

  cdRequest->x.read.mediaDescriptor = 0;
  cdRequest->x.read.bufferOffset = readInfoOffset;
  cdRequest->x.read.bufferSegment = readInfoSegment;
  cdRequest->x.read.length = sizeof(struct audioTrackInfo_s);
  cdRequest->x.read.startSector = 0;
  cdRequest->x.read.volumeID = 0;

  readInfo->audioTrackInfo.code = READ_REQUEST_AUDIO_TRACK_INFO;
  readInfo->audioTrackInfo.track = track;

  regs.x.ax = 0x1510;
  regs.x.cx = cdrom;
  regs.x.es = cdRequestSegment;
  regs.x.bx = cdRequestOffset;
  dos_int86 (0x2f);

  if (cdRequest->status & STATUS_ERROR_BIT)
  {
    Con_DPrintf("CDAudio_GetAudioTrackInfo %04x\n", cdRequest->status &   0xffff);
    return -1;
  }

  *start = readInfo->audioTrackInfo.start;
  control = readInfo->audioTrackInfo.control & AUDIO_CONTROL_MASK;
  return (control & AUDIO_CONTROL_DATA_TRACK);
}


static int CDAudio_GetAudioDiskInfo(void)
{
  int n;

  cdRequest->headerLength = 13;
  cdRequest->unit = 0;
  cdRequest->command = COMMAND_READ;
  cdRequest->status = 0;

  cdRequest->x.read.mediaDescriptor = 0;
  cdRequest->x.read.bufferOffset = readInfoOffset;
  cdRequest->x.read.bufferSegment = readInfoSegment;
  cdRequest->x.read.length = sizeof(struct audioDiskInfo_s);
  cdRequest->x.read.startSector = 0;
  cdRequest->x.read.volumeID = 0;

  readInfo->audioDiskInfo.code = READ_REQUEST_AUDIO_DISK_INFO;

  regs.x.ax = 0x1510;
  regs.x.cx = cdrom;
  regs.x.es = cdRequestSegment;
  regs.x.bx = cdRequestOffset;
  dos_int86 (0x2f);

  if (cdRequest->status & STATUS_ERROR_BIT)
  {
    Con_DPrintf("CDAudio_GetAudioDiskInfo %04x\n", cdRequest->status &  0xffff);
    return -1;
  }

  cd.valid = true;
  cd.lowTrack = readInfo->audioDiskInfo.lowTrack;
  cd.highTrack = readInfo->audioDiskInfo.highTrack;
  cd.leadOutAddress = readInfo->audioDiskInfo.leadOutStart;

  for (n = cd.lowTrack; n <= cd.highTrack; n++)
  {
    cd.track[n].isData = CDAudio_GetAudioTrackInfo (n, &cd.track[n].start);
    if (n > cd.lowTrack)
    {
      cd.track[n-1].length = RedBookToSector(cd.track[n].start) - RedBookToSector(cd.track[n-1].start);
      if (n == cd.highTrack)
        cd.track[n].length = RedBookToSector(cd.leadOutAddress) - RedBookToSector(cd.track[n].start);
    }
  }

  return 0;
}


static int CDAudio_GetAudioStatus(void)
{
  cdRequest->headerLength = 13;
  cdRequest->unit = 0;
  cdRequest->command = COMMAND_READ;
  cdRequest->status = 0;

  cdRequest->x.read.mediaDescriptor = 0;
  cdRequest->x.read.bufferOffset = readInfoOffset;
  cdRequest->x.read.bufferSegment = readInfoSegment;
  cdRequest->x.read.length = sizeof(struct audioStatus_s);
  cdRequest->x.read.startSector = 0;
  cdRequest->x.read.volumeID = 0;

  readInfo->audioDiskInfo.code = READ_REQUEST_AUDIO_STATUS;

  regs.x.ax = 0x1510;
  regs.x.cx = cdrom;
  regs.x.es = cdRequestSegment;
  regs.x.bx = cdRequestOffset;
  dos_int86 (0x2f);

  if (cdRequest->status & STATUS_ERROR_BIT)
    return -1;
  return 0;
}


static int CDAudio_MediaChange(void)
{
  cdRequest->headerLength = 13;
  cdRequest->unit = 0;
  cdRequest->command = COMMAND_READ;
  cdRequest->status = 0;

  cdRequest->x.read.mediaDescriptor = 0;
  cdRequest->x.read.bufferOffset = readInfoOffset;
  cdRequest->x.read.bufferSegment = readInfoSegment;
  cdRequest->x.read.length = sizeof(struct mediaChange_s);
  cdRequest->x.read.startSector = 0;
  cdRequest->x.read.volumeID = 0;

  readInfo->mediaChange.code = READ_REQUEST_MEDIA_CHANGE;

  regs.x.ax = 0x1510;
  regs.x.cx = cdrom;
  regs.x.es = cdRequestSegment;
  regs.x.bx = cdRequestOffset;
  dos_int86 (0x2f);

  return readInfo->mediaChange.status;
}


byte CDAudio_GetVolume (void)
{
  return cdvolume;
}


// we set the volume to 0 first and then to the desired volume
// some cd-rom drivers seem to need it done this way
void CDAudio_SetVolume (byte volume)
{
  if (!initialized || !enabled)
    return;

  cdRequest->headerLength = 13;
  cdRequest->unit = 0;
  cdRequest->command = COMMAND_WRITE;
  cdRequest->status = 0;

  cdRequest->x.read.mediaDescriptor = 0;
  cdRequest->x.read.bufferOffset = readInfoOffset;
  cdRequest->x.read.bufferSegment = readInfoSegment;
  cdRequest->x.read.length = sizeof(struct audioChannelInfo_s);
  cdRequest->x.read.startSector = 0;
  cdRequest->x.read.volumeID = 0;

  readInfo->audioChannelInfo.code = WRITE_REQUEST_AUDIO_CHANNEL_INFO;
  readInfo->audioChannelInfo.channel0input = 0;
  readInfo->audioChannelInfo.channel0volume = 0;
  readInfo->audioChannelInfo.channel1input = 1;
  readInfo->audioChannelInfo.channel1volume = 0;
  readInfo->audioChannelInfo.channel2input = 2;
  readInfo->audioChannelInfo.channel2volume = 0;
  readInfo->audioChannelInfo.channel3input = 3;
  readInfo->audioChannelInfo.channel3volume = 0;

  regs.x.ax = 0x1510;
  regs.x.cx = cdrom;
  regs.x.es = cdRequestSegment;
  regs.x.bx = cdRequestOffset;
  dos_int86 (0x2f);

  readInfo->audioChannelInfo.channel0volume = volume;
  readInfo->audioChannelInfo.channel1volume = volume;

  regs.x.ax = 0x1510;
  regs.x.cx = cdrom;
  regs.x.es = cdRequestSegment;
  regs.x.bx = cdRequestOffset;
  dos_int86 (0x2f);

  cdvolume = volume;
}


void CDAudio_Play(byte track, qboolean looping)
{
  if (!initialized || !enabled)
    return;
  
  if (!cd.valid)
    return;

  track = remap[track];

  if (playing)
  {
    if (playTrack == track)
      return;
    CDAudio_Stop();
  }

  playLooping = looping;

  if (track < cd.lowTrack || track > cd.highTrack)
  {
    Con_DPrintf("CDAudio_Play: Bad track number %u.\n", track);
    return;
  }

  playTrack = track;

  if (cd.track[track].isData)
  {
    Con_DPrintf("CDAudio_Play: Can not play data.\n");
    return;
  }

  cdRequest->headerLength = 13;
  cdRequest->unit = 0;
  cdRequest->command = COMMAND_PLAY_AUDIO;
  cdRequest->status = 0;

  cdRequest->x.playAudio.addressingMode = ADDRESS_MODE_RED_BOOK;
  cdRequest->x.playAudio.startLocation = cd.track[track].start;
  cdRequest->x.playAudio.sectors = cd.track[track].length;

  regs.x.ax = 0x1510;
  regs.x.cx = cdrom;
  regs.x.es = cdRequestSegment;
  regs.x.bx = cdRequestOffset;
  dos_int86 (0x2f);

  if (cdRequest->status & STATUS_ERROR_BIT)
  {
    Con_DPrintf("CDAudio_Play: track %u failed\n", track);
    cd.valid = false;
    playing = false;
    return;
  }

  playing = true;
}


void CDAudio_Stop(void)
{
  if (!initialized || !enabled)
    return;
  
  cdRequest->headerLength = 13;
  cdRequest->unit = 0;
  cdRequest->command = COMMAND_STOP_AUDIO;
  cdRequest->status = 0;

  regs.x.ax = 0x1510;
  regs.x.cx = cdrom;
  regs.x.es = cdRequestSegment;
  regs.x.bx = cdRequestOffset;
  dos_int86 (0x2f);

  wasPlaying = playing;
  playing = false;
}


void CDAudio_Resume(void)
{
  if (!initialized || !enabled)
    return;
  
  if (!cd.valid)
    return;

  if (!wasPlaying)
    return;
  
  cdRequest->headerLength = 13;
  cdRequest->unit = 0;
  cdRequest->command = COMMAND_RESUME_AUDIO;
  cdRequest->status = 0;

  regs.x.ax = 0x1510;
  regs.x.cx = cdrom;
  regs.x.es = cdRequestSegment;
  regs.x.bx = cdRequestOffset;
  dos_int86 (0x2f);

  playing = true;
}


static void CD_f (void)
{
  char  *command;
  int   ret;
  int   n;
  int   startAddress;

  if (Cmd_Argc() < 2)
    return;

  command = Cmd_Argv (1);

  if (Q_strcasecmp(command, "on") == 0)
  {
    enabled = true;
    return;
  }

  if (Q_strcasecmp(command, "off") == 0)
  {
    if (playing)
      CDAudio_Stop();
    enabled = false;
    return;
  }

  if (Q_strcasecmp(command, "reset") == 0)
  {
    enabled = true;
    if (playing)
      CDAudio_Stop();
    for (n = 0; n < 256; n++)
      remap[n] = n;
    CDAudio_Reset();
    CDAudio_GetAudioDiskInfo();
    return;
  }

  if (Q_strcasecmp(command, "remap") == 0)
  {
    ret = Cmd_Argc() - 2;
    if (ret <= 0)
    {
      for (n = 1; n < 256; n++)
        if (remap[n] != n)
          Con_Printf("  %u -> %u\n", n, remap[n]);
      return;
    }
    for (n = 1; n <= ret; n++)
      remap[n] = Q_atoi(Cmd_Argv (n+1));
    return;
  }

  if (!cd.valid)
  {
    Con_Printf("No CD in player.\n");
    return;
  }

  if (Q_strcasecmp(command, "play") == 0)
  {
    CDAudio_Play(Q_atoi(Cmd_Argv (2)), false);
    return;
  }

  if (Q_strcasecmp(command, "loop") == 0)
  {
    CDAudio_Play(Q_atoi(Cmd_Argv (2)), true);
    return;
  }

  if (Q_strcasecmp(command, "stop") == 0)
  {
    CDAudio_Stop();
    return;
  }

  if (Q_strcasecmp(command, "resume") == 0)
  {
    CDAudio_Resume();
    return;
  }

  if (Q_strcasecmp(command, "eject") == 0)
  {
    if (playing)
      CDAudio_Stop();
    CDAudio_Eject();
    cd.valid = false;
    return;
  }

  if (Q_strcasecmp(command, "info") == 0)
  {
    Con_Printf("%u tracks\n", cd.highTrack - cd.lowTrack + 1);
    for (n = cd.lowTrack; n <= cd.highTrack; n++)
    {
      ret = CDAudio_GetAudioTrackInfo (n, &startAddress);
      Con_Printf("Track %2u: %s at %2u:%02u\n", n, ret ? "data " : "music", (startAddress >> 16) & 0xff, (startAddress >> 8) & 0xff);
    }
    if (playing)
      Con_Printf("Currently %s track %u\n", playLooping ? "looping" : "playing", playTrack);
    Con_Printf("Volume is %u\n", cdvolume);
    CDAudio_MediaChange();
    Con_Printf("Status %04x\n", cdRequest->status & 0xffff);
    return;
  }
}


void CDAudio_Update(void)
{
  int   ret;
  int   newVolume;
  static  double lastUpdate;

  if (!initialized || !enabled)
    return;

  if ((realtime - lastUpdate) < 0.25)
    return;
  lastUpdate = realtime;

  if (mediaCheck)
  {
    static  double lastCheck;

    if ((realtime - lastCheck) < 5.0)
      return;
    lastCheck = realtime;

    ret = CDAudio_MediaChange();
    if (ret == MEDIA_CHANGED)
    {
      Con_DPrintf("CDAudio: media changed\n");
      playing = false;
      wasPlaying = false;
      cd.valid = false;
      CDAudio_GetAudioDiskInfo();
      return;
    }
  }

  newVolume = (int)(bgmvolume.value * 255.0);
  if (newVolume < 0)
  {
    Cvar_SetValue ("bgmvolume", 0.0);
    newVolume = 0;
  }
  else if (newVolume > 255)
  {
    Cvar_SetValue ("bgmvolume", 1.0);
    newVolume = 255;
  }
  if (cdvolume != newVolume)
    CDAudio_SetVolume (newVolume);

  if (playing)
  {
    CDAudio_GetAudioStatus();
    if ((cdRequest->status & STATUS_BUSY_BIT) == 0)
    {
      playing = false;
      if (playLooping)
        CDAudio_Play(playTrack, true);
    }
  }
}


qboolean CDAudio_Playing(void)
{
  return playing;
}


int CDAudio_Init(void)
{
  char  *memory;
  int   n;

  if (cls.state == ca_dedicated)
    return -1;

  if (COM_CheckParm("-nocdaudio"))
    return -1;

  if (COM_CheckParm("-cdmediacheck"))
    mediaCheck = true;

  regs.x.ax = 0x1500;
  regs.x.bx = 0;
  dos_int86 (0x2f);
  if (regs.x.bx == 0)
  {
    Con_NotifyBox (
      "MSCDEX not loaded, music is\n"
      "disabled.  Use \"-nocdaudio\" if you\n"
      "wish to avoid this message in the\n"
      "future.  See README.TXT for help.\n"
      );      
    return -1;
  }
  if (regs.x.bx > 1)
    Con_DPrintf("CDAudio_Init: First CD-ROM drive will be used\n");
  cdrom = regs.x.cx;

  regs.x.ax = 0x150c;
  regs.x.bx = 0;
  dos_int86 (0x2f);
  if (regs.x.bx == 0)
  {
    Con_NotifyBox (
      "MSCDEX version 2.00 or later\n"
      "required for music. See README.TXT\n"
      "for help.\n"
      );      
    Con_DPrintf("CDAudio_Init: MSCDEX version 2.00 or later required.\n");
    return -1;
  }

  memory = dos_getmemory(sizeof(struct cd_request
) + sizeof(union readInfo_u));
  if (memory == NULL)
  {
    Con_DPrintf("CDAudio_Init: Unable to allocate low memory.\n");
    return -1;
  }

  cdRequest = (struct cd_request *)memory;
  cdRequestSegment = ptr2real(cdRequest) >> 4;
  cdRequestOffset = ptr2real(cdRequest) & 0xf;

  readInfo = (union readInfo_u *)(memory + sizeof(struct cd_request));
  readInfoSegment = ptr2real(readInfo) >> 4;
  readInfoOffset = ptr2real(readInfo) & 0xf;

  for (n = 0; n < 256; n++)
    remap[n] = n;
  initialized = true;

  CDAudio_SetVolume (255);
  if (CDAudio_GetAudioDiskInfo())
  {
    Con_Printf("CDAudio_Init: No CD in player.\n");
    enabled = false;
  }

  Cmd_AddCommand ("cd", CD_f);

  Con_Printf("CD Audio Initialized\n");

  return 0;
}


void CDAudio_Shutdown(void)
{
  if (!initialized)
    return;
  CDAudio_Stop();
}
