// $Id: jukebox.cc 5850 2014-12-14 21:09:53Z flaterco $

/*
    Copyright (C) 2006  David Flater.

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

// Play sound files (or whatever) in random order forever.  It will
// play every song in the playlist exactly once before repeating any.
// State is preserved between runs.  Under no circumstance will it
// play a song twice in a row; however, whatever it was playing when
// you last stopped it gets replayed from the beginning on the next
// run.  You must have at least two songs in your playlist.
// Duplicates are suppressed.
//
// Prerequisites:
//
// You must have two shell scripts or executables in your path.
//
// jukebox_prep input-file temp-file
//
//    Do whatever is needed to prepare input-file for playing.  This
//    script is invoked before jukebox_play for a given input-file,
//    but concurrent with jukebox_play for the previous input-file.
//    The purpose is to give you a chance to do any slow preprocessing
//    that would otherwise incur a long pause between songs.  If no
//    such preprocessing is needed, this script can do nothing.
//
//    temp-file is a unique file name that can optionally be used to
//    store the input for jukebox_play.
//
//    Example jukebox_prep:
//
//      #!/bin/sh
//      sox -t wav "$1" -w -t wav -c 2 -r 48000 "$2" polyphase
//      normalize -q --clipping "$2"
//
// jukebox_play input-file temp-file
//
//    Play input-file.  This script is invoked after jukebox_prep for
//    a given file, with temp-file being the same as was given to the
//    corresponding jukebox_prep invocation.
//
//    If temp-file was created by jukebox_prep, it should be deleted
//    by jukebox_play.
//
//    Example jukebox_play:
//
//      #!/bin/sh
//      echo Now playing: $1
//      aplay-rt -q -Dhw:0,4 "$2"
//      rm "$2"
//
// Jukebox will not invoke jukebox_play for a given input-file until
// its corresponding jukebox_prep has terminated.  However, it is not
// guaranteed that there will be no pauses in playback.  There is
// always a pause on the first song and after reshuffling the
// playlist.
//
// Usage:
//
// To create the playlist, pipe the list of wav files (or whatever) to
// standard input and use the -N flag:
//
//   find /share/Music -name \*.wav -print | jukebox -N
//
// Henceforth to resume playing where it left off, just run jukebox
// with no flags.
//
// State is stored in ~/.jukebox.
//
// g++ -O2 -Wall -Wextra -pedantic -s -o jukebox jukebox.cc -ldstr

#include <stdio.h>
#include <Dstr>
#include <set>
#include <vector>
#include <algorithm>
#include <time.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <assert.h>
#include <signal.h>
#include <string.h>

#define prepcmd "jukebox_prep"
#define playcmd "jukebox_play"

// Start command running in a subprocess.  Return is the pid.
//
// argv[0]     The command
// argv[1]...  Arguments
// argv[n]     The list of arguments MUST be terminated by a NULL pointer.
//
pid_t sew (char *const argv[]) {
  pid_t pid = fork();
  switch (pid) {
  case -1:
    // failure
    perror ("fork");
    exit (-1);
  case 0:
    // child process
    execvp (argv[0], argv);
    perror (argv[0]);
    exit (-1);
  }
  return pid;
}

// Wait on subprocess to finish and die if it failed in any way.
void reap (pid_t pid) {
  int status;
  pid_t waitret = waitpid (pid, &status, 0);
  assert (waitret == pid);
  if (WIFSIGNALED(status)) {
    fprintf (stderr, "jukebox: script died from signal\n");
    exit (-1);
  }
  if (!WIFEXITED(status)) {
    fprintf (stderr, "jukebox: script did not exit normally\n");
    exit (-1);
  }
  if (WEXITSTATUS(status)) {
    fprintf (stderr, "jukebox: script returned exit status %d\n",
	     WEXITSTATUS(status));
    exit (-1);
  }
}

// Seed drand48 when needed.
void BootRandom () {
  static bool seeded = false;
  if (!seeded) {
    seeded = true;
    long seedval;
    FILE *fp = fopen ("/dev/urandom", "r");
    assert (fp);
    assert (fread (&seedval, sizeof(long), 1, fp) == 1);
    fclose (fp);
    srand48 (seedval);
  }
}

unsigned long RandFunc (unsigned long N) {
  assert (N > 0);
  unsigned long r = (unsigned long)(drand48() * N);
  assert (r < N);
  return r;
}

void shuffle (std::vector<Dstr> &playlist) {
  BootRandom();
  printf ("\n************* Shuffling playlist *************\n\n");
  std::random_shuffle (playlist.begin(), playlist.end(), RandFunc);
}

void reshuffle (std::vector<Dstr> &playlist) {
  Dstr lastback (playlist.back());
  shuffle (playlist);
  if (playlist.front() == lastback) {
    playlist.front() = playlist.back();
    playlist.back() = lastback;
  }
}

void usagebarf () {
  fprintf (stderr, "Usage:  jukebox [-N]\n");
  exit (-1);
}

void stupidbarf () {
  fprintf (stderr, "jukebox: Get a playlist!\n");
  exit (-1);
}

char *statefilename () {
  static Dstr fname;
  if (fname.isNull()) {
    fname = getenv ("HOME");
    assert (fname.length());
    if (fname.back() != '/')
      fname += '/';
    fname += ".jukebox";
  }
  return fname.aschar();
}

volatile sig_atomic_t caught_signal = 0;

void not_a_convenient_time (int sig __attribute__ ((unused))) {
  caught_signal = 1;
}

void critical_enter () {
  signal (SIGINT,  not_a_convenient_time);
  signal (SIGTERM, not_a_convenient_time);
  signal (SIGHUP,  not_a_convenient_time);
  signal (SIGQUIT, not_a_convenient_time);
}

void critical_exit () {
  signal (SIGINT,  SIG_DFL);
  signal (SIGTERM, SIG_DFL);
  signal (SIGHUP,  SIG_DFL);
  signal (SIGQUIT, SIG_DFL);
  if (caught_signal)
    exit (0);
}

void badbad () {
  fprintf (stderr, "\n*** JUKEBOX CATASTROPHIC FAILURE ***\n");
  fprintf (stderr, "UNABLE TO WRITE TO ~/.jukebox\n");
  fprintf (stderr, "FILE IS NOW CORRUPT!\n");
  fprintf (stderr, "Goodbye, cruel world!  AAAAAGGGHH!!!\n");
  exit (-1);
}

void save (std::vector<Dstr> &playlist) {
  critical_enter();
  FILE *fp = fopen (statefilename(), "w");
  if (!fp) {
    perror (statefilename());
    exit (-1);
  }
  if (fprintf (fp, "%40u\n", 0) < 0)
    badbad();
  for (unsigned i=0; i<playlist.size(); ++i)
    if (fprintf (fp, "%s\n", playlist[i].aschar()) < 0)
      badbad();
  if (fclose (fp))
    badbad();
  critical_exit();
}

void update (unsigned nowplaying) {
  critical_enter();
  FILE *fp = fopen (statefilename(), "r+");
  if (!fp) {
    perror (statefilename());
    exit (-1);
  }
  if (fprintf (fp, "%40u", nowplaying) < 0)
    badbad();
  if (fclose (fp))
    badbad();
  critical_exit();
}

void load (std::vector<Dstr> &playlist, unsigned &nowplaying) {
  FILE *fp = fopen (statefilename(), "r");
  if (!fp) {
    perror (statefilename());
    exit (-1);
  }
  Dstr buf;
  buf.getline (fp);
  assert (!buf.isNull());
  assert (sscanf (buf.aschar(), "%u", &nowplaying) == 1);
  while (!buf.getline(fp).isNull())
    playlist.push_back (buf);
  fclose (fp);
  assert (playlist.size() > 1);
  assert (nowplaying < playlist.size());
}

void jtmpnam (unsigned nowplaying, Dstr &fname) {
  fname = "/tmp/jukebox";
  fname += nowplaying;
}

enum Action {Prep, Play};
pid_t dofile (std::vector<Dstr> &playlist, unsigned nowplaying, Action act) {
  Dstr fname;
  jtmpnam (nowplaying, fname);
  char *args[4] = {((act == Prep) ? (char*)prepcmd : (char*)playcmd),
                   playlist[nowplaying].aschar(),
                   fname.aschar(),
                   NULL};
  return sew (args);
}

pid_t prep (std::vector<Dstr> &playlist, unsigned nowplaying) {
  return dofile (playlist, nowplaying, Prep);
}

pid_t play (std::vector<Dstr> &playlist, unsigned nowplaying) {
  return dofile (playlist, nowplaying, Play);
}

int main (int argc, char **argv) {

  if (argc > 2)
    usagebarf();

  // Initialize playlist.
  if (argc == 2) {
    if (strcmp (argv[1], "-N"))
      usagebarf();
    Dstr buf;
    std::set<Dstr> playset;
    while (!(buf.getline(stdin).isNull()))
      playset.insert (buf);
    if (playset.size() < 2)
      stupidbarf();
    std::vector<Dstr> playlist;
    playlist.insert (playlist.end(), playset.begin(), playset.end());
    shuffle (playlist);
    save (playlist);
    printf ("Playlist initialized.\n");
    return 0;
  }

  std::vector<Dstr> playlist;
  unsigned nowplaying;
  load (playlist, nowplaying);
  bool noprep = true;

  while (true) {
    while (nowplaying < playlist.size()) {
      if (noprep) {
        reap (prep (playlist, nowplaying));
        noprep = false;
      }
      bool notlast = (nowplaying+1 < playlist.size());
      pid_t playpid = play (playlist, nowplaying);
      pid_t preppid = 0;
      if (notlast)
        preppid = prep (playlist, nowplaying+1);
      reap (playpid);
      ++nowplaying;
      if (notlast) {
        update (nowplaying);
        reap (preppid);
      }
    }
    reshuffle (playlist);
    save (playlist);
    nowplaying = 0;
    noprep = true;
  }
}
