/*
 * BRLTTY - A background process providing access to the console screen (when in
 *          text mode) for a blind person using a refreshable braille display.
 *
 * Copyright (C) 1995-2026 by The BRLTTY Developers.
 *
 * BRLTTY comes with ABSOLUTELY NO WARRANTY.
 *
 * This is free software, placed under the terms of the
 * GNU Lesser General Public License, as published by the Free Software
 * Foundation; either version 2.1 of the License, or (at your option) any
 * later version. Please see the file LICENSE-LGPL for details.
 *
 * Web Page: http://brltty.app/
 *
 * This software is maintained by Dave Mielke <dave@mielke.cc>.
 */

#include "prologue.h"

#include "log.h"
#include "parse.h"
#include "pcm.h"

struct PcmDeviceStruct {
  HWAVEOUT handle;
  UINT deviceID;
  WAVEFORMATEX format;
  HANDLE done;
  WAVEHDR waveHdr;
  size_t bufSize;
};

static WAVEFORMATEX defaultFormat = { WAVE_FORMAT_PCM, 1, 11025, 11025, 1, 8, 0 };

static void
recomputeWaveOutFormat(WAVEFORMATEX *format) {
  format->nBlockAlign = format->nChannels * ((format->wBitsPerSample + 7) / 8);
  format->nAvgBytesPerSec = format->nBlockAlign * format->nSamplesPerSec;
}

static WAVEHDR initWaveHdr = { NULL, 0, 0, 0, 0, 1, NULL, 0 };

static void
LogWaveOutError(MMRESULT error, int errorLevel, const char *action) {
  char msg[MAXERRORLENGTH];
  waveOutGetErrorText(error, msg, sizeof(msg));
  logMessage(errorLevel, "%s error %d: %s.", action, error, msg);
}

PcmDevice *
openPcmDevice (int errorLevel, const char *device) {
  PcmDevice *pcm;
  MMRESULT mmres;
  WAVEOUTCAPS caps;
  int id = 0;

  if (*device) {
    if (!isInteger(&id, device) || (id < 0) || (id >= waveOutGetNumDevs())) {
      logMessage(errorLevel, "invalid PCM device number: %s", device);
      return NULL;
    }
  }

  if (!(pcm = malloc(sizeof(*pcm)))) {
    logSystemError("PCM device allocation");
    return NULL;
  }
  pcm->deviceID = id;

  if ((waveOutGetDevCaps(pcm->deviceID, &caps, sizeof(caps))) != MMSYSERR_NOERROR)
    pcm->format = defaultFormat;
  else {
    logMessage(errorLevel, "PCM device %d is %s", pcm->deviceID, caps.szPname);
    pcm->format.wFormatTag = WAVE_FORMAT_PCM;
    if (caps.dwFormats & 
	(WAVE_FORMAT_1S08
	|WAVE_FORMAT_1S16
	|WAVE_FORMAT_2S08
	|WAVE_FORMAT_2S16
	|WAVE_FORMAT_4S08
	|WAVE_FORMAT_4S16))
      pcm->format.nChannels = 2;
    else
      pcm->format.nChannels = 1;
    if (caps.dwFormats &
	(WAVE_FORMAT_4M08
	|WAVE_FORMAT_4M16
	|WAVE_FORMAT_4S08
	|WAVE_FORMAT_4S16))
      pcm->format.nSamplesPerSec = 44100;
    else if (caps.dwFormats &
	(WAVE_FORMAT_2M08
	|WAVE_FORMAT_2M16
	|WAVE_FORMAT_2S08
	|WAVE_FORMAT_2S16))
      pcm->format.nSamplesPerSec = 22050;
    else if (caps.dwFormats &
	(WAVE_FORMAT_1M08
	|WAVE_FORMAT_1M16
	|WAVE_FORMAT_1S08
	|WAVE_FORMAT_1S16))
      pcm->format.nSamplesPerSec = 11025;
    else {
      logMessage(errorLevel, "unknown PCM capability %#lx", caps.dwFormats);
      goto out;
    }
    if (caps.dwFormats &
	(WAVE_FORMAT_1M16
	|WAVE_FORMAT_1S16
	|WAVE_FORMAT_2M16
	|WAVE_FORMAT_2S16
	|WAVE_FORMAT_4M16
	|WAVE_FORMAT_4S16))
      pcm->format.wBitsPerSample = 16;
    else if (caps.dwFormats &
	(WAVE_FORMAT_1M08
	|WAVE_FORMAT_1S08
	|WAVE_FORMAT_2M08
	|WAVE_FORMAT_2S08
	|WAVE_FORMAT_4M08
	|WAVE_FORMAT_4S08))
      pcm->format.wBitsPerSample = 8;
    else {
      logMessage(LOG_ERR, "unknown PCM capability %#lx", caps.dwFormats);
      goto out;
    }
    recomputeWaveOutFormat(&pcm->format);
    pcm->format.cbSize = 0;
  }

  if (!(pcm->done = CreateEvent(NULL, FALSE, TRUE, NULL))) {
    logWindowsSystemError("creating PCM completion event");
    goto out;
  }

  pcm->waveHdr = initWaveHdr;
  pcm->bufSize = 0;

  if ((mmres = waveOutOpen(&pcm->handle, pcm->deviceID,
	  &pcm->format, (DWORD_PTR) pcm->done, 0, CALLBACK_EVENT)) != MMSYSERR_NOERROR) {
    LogWaveOutError(mmres, errorLevel, "opening PCM device");
    goto outEvent;
  }
  return pcm;

outEvent:
  CloseHandle(pcm->done);
out:
  free(pcm);
  return NULL;
}

static int
unprepareHeader(PcmDevice *pcm) {
  MMRESULT mmres;
  awaitPcmOutput(pcm);
  if ((mmres = waveOutUnprepareHeader(pcm->handle, &pcm->waveHdr, sizeof(pcm->waveHdr))) != MMSYSERR_NOERROR) {
    LogWaveOutError(mmres, LOG_ERR, "unpreparing PCM data header");
    return 0;
  }
  return 1;
}

static int
updateWaveOutFormat(PcmDevice *pcm, WAVEFORMATEX *format, const char *errmsg) {
  MMRESULT mmres;
  recomputeWaveOutFormat(format);
  if (!(unprepareHeader(pcm))) return 0;
  if (waveOutOpen(NULL, pcm->deviceID, format, 0, 0, WAVE_FORMAT_QUERY) == MMSYSERR_NOERROR) {
    waveOutClose(pcm->handle);
    pcm->handle = INVALID_HANDLE_VALUE;
    if ((mmres = waveOutOpen(&pcm->handle, pcm->deviceID, format,
	    (DWORD_PTR) pcm->done, 0, CALLBACK_EVENT)) == MMSYSERR_NOERROR) {
      pcm->format = *format;
      return 1;
    }
    LogWaveOutError(mmres, LOG_ERR, errmsg);
  }
  return 0;
}

void
closePcmDevice (PcmDevice *pcm) {
  CloseHandle(pcm->done);
  unprepareHeader(pcm);
  waveOutClose(pcm->handle);
  free(pcm->waveHdr.lpData);
  free(pcm);
}

int
writePcmData (PcmDevice *pcm, const unsigned char *buffer, int count) {
  MMRESULT mmres;
  void *newBuf;
  if (!count) return 1;
  if (count > pcm->bufSize) {
    if (!(unprepareHeader(pcm))) return 0;
    if (!(newBuf = realloc(pcm->waveHdr.lpData, 2 * count))) {
      logSystemError("allocating PCM data buffer");
      return 0;
    }
    pcm->waveHdr.lpData = newBuf;
    pcm->waveHdr.dwFlags = 0;
    pcm->waveHdr.dwBufferLength = pcm->bufSize = 2 * count;
  }
  awaitPcmOutput(pcm);
  if (!(pcm->waveHdr.dwFlags & WHDR_PREPARED))
    if ((mmres = waveOutPrepareHeader(pcm->handle, &pcm->waveHdr, sizeof(pcm->waveHdr))) != MMSYSERR_NOERROR) {
      LogWaveOutError(mmres, LOG_ERR, "preparing PCM data header");
      return 0;
    }
  pcm->waveHdr.dwBufferLength = count;
  memcpy(pcm->waveHdr.lpData, buffer, count);
  ResetEvent(pcm->done);
  if ((mmres = waveOutWrite(pcm->handle, &pcm->waveHdr, sizeof(pcm->waveHdr))) != MMSYSERR_NOERROR) {
    SetEvent(pcm->done);
    LogWaveOutError(mmres, LOG_ERR, "writing PCM data");
    return 0;
  }
  return 1;
}

int
getPcmBlockSize (PcmDevice *pcm) {
  return 0X10000;
}

int
getPcmSampleRate (PcmDevice *pcm) {
  return pcm->format.nSamplesPerSec;
}

int
setPcmSampleRate (PcmDevice *pcm, int rate) {
  WAVEFORMATEX format = pcm->format;
  format.nSamplesPerSec = rate;
  if (!updateWaveOutFormat(pcm, &format, "setting PCM sample rate"))
    return getPcmSampleRate(pcm);
  else
    return rate;
}

int
getPcmChannelCount (PcmDevice *pcm) {
  return pcm->format.nChannels;
}

int
setPcmChannelCount (PcmDevice *pcm, int channels) {
  WAVEFORMATEX format = pcm->format;
  format.nChannels = channels;
  if (!updateWaveOutFormat(pcm, &format, "setting PCM channel count"))
    return getPcmChannelCount(pcm);
  else
    return channels;
}

PcmAmplitudeFormat
getPcmAmplitudeFormat (PcmDevice *pcm) {
  if (pcm->format.wBitsPerSample == 8) return PCM_FMT_U8;
  if (pcm->format.wBitsPerSample == 16) return PCM_FMT_S16L;
  return PCM_FMT_UNKNOWN;
}

PcmAmplitudeFormat
setPcmAmplitudeFormat (PcmDevice *pcm, PcmAmplitudeFormat format) {
  WAVEFORMATEX newFormat = pcm->format;
  if (format == PCM_FMT_U8) newFormat.wBitsPerSample = 8;
  else if (format == PCM_FMT_S16L) newFormat.wBitsPerSample = 16;
  else return getPcmAmplitudeFormat(pcm);
  if (!updateWaveOutFormat(pcm, &newFormat, "setting PCM amplitude format"))
    return getPcmAmplitudeFormat(pcm);
  else
    return format;
}

void
pushPcmOutput (PcmDevice *pcm) {
}

void
awaitPcmOutput (PcmDevice *pcm) {
  while ((pcm->waveHdr.dwFlags & WHDR_PREPARED)
		  && !(pcm->waveHdr.dwFlags & WHDR_DONE))
    WaitForSingleObject(pcm->done, INFINITE);
  SetEvent(pcm->done);
}

void
cancelPcmOutput (PcmDevice *pcm) {
  waveOutReset(pcm->handle);
}
