/*
 * 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-2020 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 <string.h>

#include "log.h"
#include "parameters.h"
#include "thread.h"
#include "async_event.h"
#include "async_alarm.h"
#include "async_wait.h"
#include "program.h"
#include "tune.h"
#include "notes.h"

static int tuneInitialized = 0;
static AsyncHandle tuneDeviceCloseTimer = NULL;
static int openErrorLevel = LOG_ERR;

static const NoteMethods *noteMethods = NULL;
static NoteDevice *noteDevice = NULL;

static int
flushNoteDevice (void) {
  if (!noteDevice) return 1;
  return noteMethods->flush(noteDevice);
}

static void
closeTuneDevice (void) {
  if (tuneDeviceCloseTimer) {
    asyncCancelRequest(tuneDeviceCloseTimer);
    tuneDeviceCloseTimer = NULL;
  }

  if (noteDevice) {
    noteMethods->destruct(noteDevice);
    noteDevice = NULL;
  }
}

ASYNC_ALARM_CALLBACK(handleTuneDeviceCloseTimeout) {
  if (tuneDeviceCloseTimer) {
    asyncDiscardHandle(tuneDeviceCloseTimer);
    tuneDeviceCloseTimer = NULL;
  }

  closeTuneDevice();
}

static int
openTuneDevice (void) {
  const int timeout = TUNE_DEVICE_CLOSE_DELAY;

  if (noteDevice) {
    asyncResetAlarmIn(tuneDeviceCloseTimer, timeout);
    return 1;
  }

  if (noteMethods) {
    if ((noteDevice = noteMethods->construct(openErrorLevel)) != NULL) {
      asyncNewRelativeAlarm(&tuneDeviceCloseTimer, timeout, handleTuneDeviceCloseTimeout, NULL);
      return 1;
    }
  }

  return 0;
}

static const NoteElement *currentlyPlayingNotes = NULL;
static const ToneElement *currentlyPlayingTones = NULL;

typedef unsigned char TuneSynchronizationMonitor;

typedef enum {
  TUNE_REQ_SET_DEVICE,
  TUNE_REQ_PLAY_NOTES,
  TUNE_REQ_PLAY_TONES,
  TUNE_REQ_WAIT,
  TUNE_REQ_SYNCHRONIZE
} TuneRequestType;

typedef struct {
  TuneRequestType type;

  union {
    struct {
      const NoteMethods *methods;
    } setDevice;

    struct {
      const NoteElement *tune;
    } playNotes;

    struct {
      const ToneElement *tune;
    } playTones;

    struct {
      int time;
    } wait;

    struct {
      TuneSynchronizationMonitor *monitor;
    } synchronize;
  } parameters;
} TuneRequest;

static void
handleTuneRequest_setDevice (const NoteMethods *methods) {
  if (methods != noteMethods) {
    closeTuneDevice();
    noteMethods = methods;
  }
}

static void
handleTuneRequest_playNotes (const NoteElement *tune) {
  while (tune->duration) {
    if (!openTuneDevice()) return;
    if (!noteMethods->note(noteDevice, tune->duration, tune->note)) return;
    tune += 1;
  }

  flushNoteDevice();
}

static void
handleTuneRequest_playTones (const ToneElement *tune) {
  while (tune->duration) {
    if (!openTuneDevice()) return;
    if (!noteMethods->tone(noteDevice, tune->duration, tune->frequency)) return;
    tune += 1;
  }

  flushNoteDevice();
}

static void
handleTuneRequest_wait (int time) {
  asyncWait(time);
}

static void
handleTuneRequest_synchronize (TuneSynchronizationMonitor *monitor) {
  *monitor = 1;
}

static void
handleTuneRequest (TuneRequest *req) {
  if (req) {
    switch (req->type) {
      case TUNE_REQ_SET_DEVICE:
        handleTuneRequest_setDevice(req->parameters.setDevice.methods);
        break;

      case TUNE_REQ_PLAY_NOTES: {
        const NoteElement *tune = req->parameters.playNotes.tune;

        currentlyPlayingNotes = tune;
        handleTuneRequest_playNotes(tune);
        currentlyPlayingNotes = NULL;

        break;
      }

      case TUNE_REQ_PLAY_TONES: {
        const ToneElement *tune = req->parameters.playTones.tune;

        currentlyPlayingTones = tune;
        handleTuneRequest_playTones(tune);
        currentlyPlayingTones = NULL;

        break;
      }

      case TUNE_REQ_WAIT:
        handleTuneRequest_wait(req->parameters.wait.time);
        break;

      case TUNE_REQ_SYNCHRONIZE:
        handleTuneRequest_synchronize(req->parameters.synchronize.monitor);
        break;
    }

    free(req);
  } else {
    closeTuneDevice();
  }
}

#ifdef GOT_PTHREADS
typedef enum {
  TUNE_THREAD_NONE,
  TUNE_THREAD_STARTING,
  TUNE_THREAD_FAILED,

  TUNE_THREAD_RUNNING,
  TUNE_THREAD_STOPPING,
  TUNE_THREAD_STOPPED
} TuneThreadState;

static TuneThreadState tuneThreadState = TUNE_THREAD_NONE;
static pthread_t tuneThreadIdentifier;
static AsyncEvent *tuneRequestEvent = NULL;
static AsyncEvent *tuneMessageEvent = NULL;

static void
setTuneThreadState (TuneThreadState newState) {
  TuneThreadState oldState = tuneThreadState;

  logMessage(LOG_DEBUG, "tune thread state change: %d -> %d", oldState, newState);
  tuneThreadState = newState;
}

ASYNC_CONDITION_TESTER(testTuneThreadStarted) {
  return tuneThreadState != TUNE_THREAD_STARTING;
}

ASYNC_CONDITION_TESTER(testTuneThreadStopping) {
  return tuneThreadState == TUNE_THREAD_STOPPING;
}

ASYNC_CONDITION_TESTER(testTuneThreadStopped) {
  return tuneThreadState == TUNE_THREAD_STOPPED;
}

typedef enum {
  TUNE_MSG_SET_STATE
} TuneMessageType;

typedef struct {
  TuneMessageType type;

  union {
    struct {
      TuneThreadState state;
    } setState;
  } parameters;
} TuneMessage;

static void
handleTuneMessage (TuneMessage *msg) {
  switch (msg->type) {
    case TUNE_MSG_SET_STATE:
      setTuneThreadState(msg->parameters.setState.state);
      break;
  }

  free(msg);
}

ASYNC_EVENT_CALLBACK(handleTuneMessageEvent) {
  TuneMessage *msg = parameters->signalData;

  if (msg) handleTuneMessage(msg);
}

static int
sendTuneMessage (TuneMessage *msg) {
  return asyncSignalEvent(tuneMessageEvent, msg);
}

static TuneMessage *
newTuneMessage (TuneMessageType type) {
  TuneMessage *msg;

  if ((msg = malloc(sizeof(*msg)))) {
    memset(msg, 0, sizeof(*msg));
    msg->type = type;
    return msg;
  } else {
    logMallocError();
  }

  return NULL;
}

static void
sendTuneThreadState (TuneThreadState state) {
  TuneMessage *msg;

  if ((msg = newTuneMessage(TUNE_MSG_SET_STATE))) {
    msg->parameters.setState.state = state;
    if (!sendTuneMessage(msg)) free(msg);
  }
}

static void
finishTuneRequest_stop (void) {
  setTuneThreadState(TUNE_THREAD_STOPPING);
}

static void
finishTuneRequest_synchronize (void) {
  sendTuneMessage(NULL);
}

ASYNC_EVENT_CALLBACK(handleTuneRequestEvent) {
  TuneRequest *req = parameters->signalData;
  void (*finish) (void) = NULL;

  if (req) {
    switch (req->type) {
      case TUNE_REQ_SYNCHRONIZE:
        finish = finishTuneRequest_synchronize;
        break;

      default:
        break;
    }
  } else {
    finish = finishTuneRequest_stop;
  }

  handleTuneRequest(req);
  if (finish) finish();
}

THREAD_FUNCTION(runTuneThread) {
  if ((tuneRequestEvent = asyncNewEvent(handleTuneRequestEvent, NULL))) {
    sendTuneThreadState(TUNE_THREAD_RUNNING);
    asyncWaitFor(testTuneThreadStopping, NULL);

    asyncDiscardEvent(tuneRequestEvent);
    tuneRequestEvent = NULL;
  }

  sendTuneThreadState(TUNE_THREAD_STOPPED);
  return NULL;
}

static int
startTuneThread (void) {
  if (tuneThreadState == TUNE_THREAD_NONE) {
    setTuneThreadState(TUNE_THREAD_STARTING);

    if ((tuneMessageEvent = asyncNewEvent(handleTuneMessageEvent, NULL))) {
      int creationError = createThread("tune-thread", &tuneThreadIdentifier,
                                       NULL, runTuneThread, NULL);

      if (!creationError) {
        asyncWaitFor(testTuneThreadStarted, NULL);
        if (tuneThreadState == TUNE_THREAD_RUNNING) return 1;
      } else {
        logActionError(creationError, "tune thread creation");
        setTuneThreadState(TUNE_THREAD_FAILED);
      }

      asyncDiscardEvent(tuneMessageEvent);
      tuneMessageEvent = NULL;
    }
  }

  return tuneThreadState == TUNE_THREAD_RUNNING;
}
#endif /* GOT_PTHREADS */

static int
sendTuneRequest (TuneRequest *req) {
#ifdef GOT_PTHREADS
  if (startTuneThread()) {
    return asyncSignalEvent(tuneRequestEvent, req);
  }
#endif /* GOT_PTHREADS */

  handleTuneRequest(req);
  return 1;
}

static void
exitTunes (void *data) {
  sendTuneRequest(NULL);

#ifdef GOT_PTHREADS
  if (tuneThreadState >= TUNE_THREAD_RUNNING) {
    asyncWaitFor(testTuneThreadStopped, NULL);
  }

  tuneThreadState = TUNE_THREAD_NONE;
#endif /* GOT_PTHREADS */

  tuneInitialized = 0;
}
 
static TuneRequest *
newTuneRequest (TuneRequestType type) {
  TuneRequest *req;

  if (!tuneInitialized) {
    tuneInitialized = 1;
    onProgramExit("tunes", exitTunes, NULL);
  }

  if ((req = malloc(sizeof(*req)))) {
    memset(req, 0, sizeof(*req));
    req->type = type;
    return req;
  } else {
    logMallocError();
  }

  return NULL;
}

int
tuneSetDevice (TuneDevice device) {
  const NoteMethods *methods;

  switch (device) {
    default:
      return 0;

#ifdef HAVE_BEEP_SUPPORT
    case tdBeeper:
      methods = &beepNoteMethods;
      break;
#endif /* HAVE_BEEP_SUPPORT */

#ifdef HAVE_PCM_SUPPORT
    case tdPcm:
      methods = &pcmNoteMethods;
      break;
#endif /* HAVE_PCM_SUPPORT */

#ifdef HAVE_MIDI_SUPPORT
    case tdMidi:
      methods = &midiNoteMethods;
      break;
#endif /* HAVE_MIDI_SUPPORT */

#ifdef HAVE_FM_SUPPORT
    case tdFm:
      methods = &fmNoteMethods;
      break;
#endif /* HAVE_FM_SUPPORT */
  }

  {
    TuneRequest *req;

    if ((req = newTuneRequest(TUNE_REQ_SET_DEVICE))) {
      req->parameters.setDevice.methods = methods;
      if (!sendTuneRequest(req)) free(req);
    }
  }

  return 1;
}

void
tunePlayNotes (const NoteElement *tune) {
  if (tune != currentlyPlayingNotes) {
    TuneRequest *req;

    if ((req = newTuneRequest(TUNE_REQ_PLAY_NOTES))) {
      req->parameters.playNotes.tune = tune;
      if (!sendTuneRequest(req)) free(req);
    }
  }
}

void
tunePlayTones (const ToneElement *tune) {
  if (tune != currentlyPlayingTones) {
    TuneRequest *req;

    if ((req = newTuneRequest(TUNE_REQ_PLAY_TONES))) {
      req->parameters.playTones.tune = tune;
      if (!sendTuneRequest(req)) free(req);
    }
  }
}

void
tuneWait (int time) {
  TuneRequest *req;

  if ((req = newTuneRequest(TUNE_REQ_WAIT))) {
    req->parameters.wait.time = time;
    if (!sendTuneRequest(req)) free(req);
  }
}

ASYNC_CONDITION_TESTER(testTuneSynchronizationMonitor) {
  TuneSynchronizationMonitor *monitor = data;

  return !!*monitor;
}

void
tuneSynchronize (void) {
  TuneRequest *req;

  if ((req = newTuneRequest(TUNE_REQ_SYNCHRONIZE))) {
    TuneSynchronizationMonitor monitor = 0;
    req->parameters.synchronize.monitor = &monitor;

    if (sendTuneRequest(req)) {
      asyncWaitFor(testTuneSynchronizationMonitor, &monitor);
    } else {
      free(req);
    }
  }
}

void
suppressTuneDeviceOpenErrors (void) {
  openErrorLevel = LOG_DEBUG;
}
