/*
 * 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-2021 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>.
 */

/* Theta/speech.c - Speech library
 * For the Theta text to speech package
 * Maintained by Dave Mielke <dave@mielke.cc>
 */

#include "prologue.h"

#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <signal.h>
#include <sys/wait.h>

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

typedef enum {
  PARM_AGE,
  PARM_GENDER,
  PARM_LANGUAGE,
  PARM_NAME,
  PARM_PITCH
} DriverParameter;
#define SPKPARMS "age", "gender", "language", "name", "pitch"

#include "spk_driver.h"

struct theta_sfx_block_ops { /* used but not defined in header */
  char dummy;
};
#include <theta.h>

static cst_voice *voice = NULL;
static pid_t child = -1;
static int pipeDescriptors[2];
static const int *pipeOutput = &pipeDescriptors[0];
static const int *pipeInput = &pipeDescriptors[1];

static void
initializeTheta (void) {
  static int initialized = 0;
  if (!initialized) {
    {
      const char *directory = THETA_ROOT "/voices";
      setenv("THETA_VOXPATH", directory, 0);
    }

    setenv("THETA_HOME", THETA_ROOT, 0);
    theta_init(NULL);
    initialized = 1;
  }
}

static void
loadVoice (theta_voice_desc *descriptor) {
  if ((voice = theta_load_voice(descriptor))) {
    logMessage(LOG_INFO, "Voice: %s(%s,%d)",
               theta_voice_human(voice),
               theta_voice_gender(voice),
               theta_voice_age(voice));
  } else {
    logMessage(LOG_WARNING, "Voice load error: %s [%s]",
               descriptor->human, descriptor->voxname);
  }
}

static int
doChild (void) {
  FILE *stream = fdopen(*pipeOutput, "r");
  char buffer[0X400];
  char *line;
  while ((line = fgets(buffer, sizeof(buffer), stream))) {
    theta_tts(line, voice);
  }
  return 0;
}

static void
spk_say (SpeechSynthesizer *spk, const unsigned char *buffer, size_t length, size_t count, const unsigned char *attributes) {
  if (voice) {
    if (child != -1) goto ready;

    if (pipe(pipeDescriptors) != -1) {
      if ((child = fork()) == -1) {
        logSystemError("fork");
      } else if (child == 0) {
        _exit(doChild());
      } else
      ready: {
        unsigned char text[length + 1];
        memcpy(text, buffer, length);
        text[length] = '\n';
        write(*pipeInput, text, sizeof(text));
        return;
      }

      close(*pipeInput);
      close(*pipeOutput);
    } else {
      logSystemError("pipe");
    }
  }
}

static void
spk_mute (SpeechSynthesizer *spk) {
  if (child != -1) {
    close(*pipeInput);
    close(*pipeOutput);

    kill(child, SIGKILL);
    waitpid(child, NULL, 0);
    child = -1;
  }
}

static void
spk_setVolume (SpeechSynthesizer *spk, unsigned char setting) {
  theta_set_rescale(voice, getFloatSpeechVolume(setting), NULL);
}

static void
spk_setRate (SpeechSynthesizer *spk, unsigned char setting) {
  theta_set_rate_stretch(voice, 1.0/getFloatSpeechRate(setting), NULL);
}

static int
spk_construct (SpeechSynthesizer *spk, char **parameters) {
  theta_voice_search criteria;

  memset(&criteria, 0, sizeof(criteria));
  initializeTheta();

  spk->setVolume = spk_setVolume;
  spk->setRate = spk_setRate;

  if (*parameters[PARM_GENDER]) {
    const char *const choices[] = {"male", "female", "neuter", NULL};
    unsigned int choice;
    if (validateChoice(&choice, parameters[PARM_GENDER], choices)) {
      criteria.gender = (char *)choices[choice];
    } else {
      logMessage(LOG_WARNING, "%s: %s", "invalid gender specification", parameters[PARM_GENDER]);
    }
  }

  if (*parameters[PARM_LANGUAGE]) criteria.lang = parameters[PARM_LANGUAGE];

  if (*parameters[PARM_AGE]) {
    const char *word = parameters[PARM_AGE];
    int value;
    int younger;
    static const int minimumAge = 1;
    static const int maximumAge = 99;
    if ((younger = word[0] == '-') && word[1]) ++word;
    if (validateInteger(&value, word, &minimumAge, &maximumAge)) {
      if (younger) value = -value;
      criteria.age = value;
    } else {
      logMessage(LOG_WARNING, "%s: %s", "invalid age specification", word);
    }
  }

  {
    const char *name = parameters[PARM_NAME];
    if (name && (*name == '/')) {
      theta_voice_desc *descriptor = theta_try_voxdir(name, &criteria);
      if (descriptor) {
        loadVoice(descriptor);
        theta_free_voice_desc(descriptor);
      }
    } else {
      theta_voice_desc *descriptors = theta_enum_voices(theta_voxpath, &criteria);
      if (descriptors) {
        theta_voice_desc *descriptor;
        for (descriptor=descriptors; descriptor; descriptor=descriptor->next) {
          if (*name)
            if (strcasecmp(name, descriptor->human) != 0)
              continue;
          loadVoice(descriptor);
          if (voice) break;
        }
        theta_free_voicelist(descriptors);
      }
    }
  }

  if (voice) {
    {
      float pitch = 0.0;
      static const float minimumPitch = -2.0;
      static const float maximumPitch = 2.0;
      if (validateFloat(&pitch, parameters[PARM_PITCH], &minimumPitch, &maximumPitch)) {
        theta_set_pitch_shift(voice, pitch, NULL);
      } else {
        logMessage(LOG_WARNING, "%s: %s", "invalid pitch shift specification", parameters[PARM_PITCH]);
      }
    }

    logMessage(LOG_INFO, "Theta Engine: version %s", theta_version);
    return 1;
  }

  logMessage(LOG_WARNING, "No voices found.");
  return 0;
}

static void
spk_destruct (SpeechSynthesizer *spk) {
  spk_mute(spk);

  if (voice) {
    theta_unload_voice(voice);
    voice = NULL;
  }
}
