
// SPDX-License-Identifier: CC-BY-NC-SA-4.0
//
// Copyright (C) 2026 Bit by Bit Signal Processing LLC (https://bxbsp.com)
//
// This work is placed under the "Creative Commons Attribution
// NonCommercial ShareAlike 4.0 International" license, known
// by the shortened acronym "CC-BY-NC-SA-4.0".
//
// This work 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.
//
// A CC-BY-NC-SA-4.0 license allows you to use this work for
// noncommercial purposes so long as attribution is made to the
// original author.  Modified versions of this work may be distributed,
// but only under the same license.  For further details, see the
// Creative Commons License "CC-BY-NC-SA-4.0".
//
// You should have received a copy of the CC-BY-NC-SA-4.0 license
// along with this work. If not, see
// <https://creativecommons.org/licenses/by-nc-sa/4.0/>.
//

#include "beep_manager.hh"

//
// Create a beep manager on first use.
//
beep_manager* audio_beep_manager()
{
  static beep_manager* bm = 0;

  if(!bm)
    bm = new beep_manager();

  return bm;
}


beep_manager::beep_manager()
{
  handle = 0;
  zero_data = 0;
  next_job = 0;

  // Just to make sure everything is initialized, give these a value here.
  num_periods     = 2;
  buffer_length   = 512;
  period_length   = 256;
  
  // Start the thread, beginning in the run() method, to do the output to the audio device.
  start();
}


bool beep_manager::start_audio()
{
  int err;
  int err1;
  int err2;

  handle          = 0;
  
  //
  // This original_home stuff is simply to get rid of a "cannot open home directory"
  // error mesage that comes from the audio subsystem, and might mislead one into thinking
  // there is a real problem.  It is done by temporarily changing the HOME environment
  // variable.
  //
  
  //char* original_home = getenv("HOME");
  setenv("HOME", "/tmp", true);

  //
  // Newer pipewire attachments handling alsa opens require the environment variable
  // XDG_RUNTIME_DIR to be set to something like /run/user/1000, if the user ID
  // running the current X session is 1000.  This is for when this program is run
  // from inside X11 or XWayland (for testing).  So make sure this is defined, if
  // we can.
  //
  char* xdg_run = getenv("XDG_RUNTIME_DIR");

  if(!xdg_run)
    {
      char* uid = getenv("SUDO_UID");

      if(uid)
        {
          char temp[1000];
          int nid = atoi(uid);

          sprintf(temp, "/run/user/%d", nid);
          
          setenv("XDG_RUNTIME_DIR", temp, true);
        }
    }
  
  if(handle)
    {
      snd_pcm_drain(handle);
      snd_pcm_close(handle);
      //snd_config_update_free_global();
    }
  
  err = snd_pcm_open(&handle, device1, SND_PCM_STREAM_PLAYBACK, 0);
  if(err>=0)
    {
      printf("Opened audio device \"%s\".\n", device1);
    }
  else
    {
      err1 = err;
      err = snd_pcm_open(&handle, device2, SND_PCM_STREAM_PLAYBACK, 0);
      if(err>=0)
        {
          printf("Opened audio device \"%s\".\n", device2);
        }
      else
        {
          err2 = err;
          printf("Audio open errors of audio devices \"%s\" and \"%s\": \"%s\" and \"%s\"\n", device1, device2, snd_strerror(err1), snd_strerror(err2));
          handle = 0;
          sleep(1);
          return false;
        }
    }




  //
  // Set hardware parameters
  //

  snd_pcm_hw_params_alloca(&hwparams);

  err = snd_pcm_hw_params_any(handle, hwparams);
  if(err<0)
    {
      printf("Broken configuration for playback (no configurations available): %s\n", snd_strerror(err));
      return false;
    }

  err = snd_pcm_hw_params_set_access(handle, hwparams, SND_PCM_ACCESS_RW_INTERLEAVED);
  if(err<0)
    {
      printf("Audio hardware interleaved parameter setting error: %s\n", snd_strerror(err));
      return false;
    }

  err = snd_pcm_hw_params_set_format(handle, hwparams, SND_PCM_FORMAT_S16_LE);
  if(err<0)
    {
      printf("Audio hardware S16 format parameter setting error: %s\n", snd_strerror(err));
      return false;
    }

  err = snd_pcm_hw_params_set_channels(handle, hwparams, num_channels);
  if(err<0)
    {
      printf("Audio channel count %d not available for playbacks: %s\n", num_channels, snd_strerror(err));
      return false;
    }

  err = snd_pcm_hw_params_set_rate(handle, hwparams, sample_rate_hz, 0);
  if(err<0)
    {
      printf("Audio sample rate %dHz not available for playback: %s\n", sample_rate_hz, snd_strerror(err));
      return false;
    }

  snd_pcm_uframes_t actual_buffer_length = requested_buffer_length;
  err = snd_pcm_hw_params_set_buffer_size_near(handle, hwparams, &actual_buffer_length);
  if(err<0)
    {
      printf("Audio error setting buffer length %ld for playback: %s\n",
	     (long)requested_buffer_length, snd_strerror(err));
      return false;
    }
  if( (long) actual_buffer_length != (long) requested_buffer_length)
    {
      printf("Audio set buffer size %ld for playback when %ld was requested: Status %s\n",
	     (long)actual_buffer_length, (long)requested_buffer_length, snd_strerror(err));
      //return false;
    }

  buffer_length = (int)actual_buffer_length;

  unsigned int actual_num_periods = requested_num_periods;
  err = snd_pcm_hw_params_set_periods_near(handle, hwparams, &actual_num_periods, NULL);
  if(err<0)
    {
      printf("Audio error setting num periods %d for playback: %s\n",
	     requested_num_periods, snd_strerror(err));
      return false;
    }

  if(actual_num_periods != requested_num_periods)
    {
      printf("Audio set num_periods %ld for playback when %ld was requested: Status %s\n",
	     (long)actual_num_periods, (long)requested_num_periods, snd_strerror(err));
      //return false;
    }

  num_periods = (int)actual_num_periods;
  period_length = buffer_length / num_periods;

  if(period_length * num_periods != buffer_length)
    {
      printf("Audio setup error.  Period length %d doesn't evenly divide buffer length %d.\n",
	     period_length, buffer_length);
      return false;
    }
  
  printf("Audio buffer length is %ld, evenly divided into %ld periods.\n", (long)buffer_length, (long)num_periods);

  // Write the settings to the device
  err = snd_pcm_hw_params(handle, hwparams);
  if(err<0)
    {
      printf("Audio unable to set hw params for playback: %s\n", snd_strerror(err));
      return false;
    }




  //
  // Set software parameters
  
  snd_pcm_sw_params_alloca(&swparams);

  err = snd_pcm_sw_params_current(handle, swparams);
  if(err<0)
    {
      printf("Audio: Unable to determine current swparams for playback: %s\n", snd_strerror(err));
      return false;
    }

  // Start the transfer after a buffer is full
  err = snd_pcm_sw_params_set_start_threshold(handle, swparams, buffer_length);
  if(err<0)
    {
      printf("Audio: Unable to set start threshold mode for playback: %s\n", snd_strerror(err));
      return false;
    }

  // Allow a transfer when period_length samples can be processed
  err = snd_pcm_sw_params_set_avail_min(handle, swparams, period_length);
  if(err<0)
    {
      printf("Audio: Unable to set avail min for playback: %s\n", snd_strerror(err));
      return false;
    }

  // Write the settings to the playback device
  err = snd_pcm_sw_params(handle, swparams);
  if(err<0)
    {
      printf("Unable to set sw params for playback: %s\n", snd_strerror(err));
      return false;
    }

  //if(original_home)
  // setenv("HOME", original_home, true);

   if(zero_data)
     delete [] zero_data;
   
   zero_data = new channels[period_length];
   for(int i = 0; i<period_length; i++)
     {
       zero_data[i].right  = 0;
       zero_data[i].left   = 0;
     }
   
   return true;
}
  


beep_manager::~beep_manager()
{
  cancel();
  wait_for_completion();
  
  if(handle)
    {
      snd_pcm_drain(handle);
      snd_pcm_close(handle);
      snd_config_update_free_global();
    }

  if(zero_data)
    {
      delete [] zero_data;
    }
}

bool beep_manager::send_job(channels* data, int length)
{
  int num_to_send = length;
  int num_sent = 0;
  int num_to_send_each_time = period_length;
  int error_count = 0;
  
  while(num_to_send>0)
    {
      if(num_to_send<num_to_send_each_time)
	num_to_send_each_time = num_to_send;
      
      int sent_this_time = snd_pcm_writei(handle, &data[num_sent], num_to_send_each_time);

      if(sent_this_time == -EAGAIN)
        {
          snd_pcm_wait(handle, 500);
          continue;
        }
      else if(sent_this_time == -EPIPE)
        {
          printf("Audio underrun:  \"%s\".\n", snd_strerror(sent_this_time));
          snd_pcm_prepare(handle);
          continue;
        }
      else if(sent_this_time == -ESTRPIPE)
        {
          for(;;)
            {
              int err = snd_pcm_resume(handle);
              if(err == -EAGAIN)
                {
                  sleep(1);
                  continue;
                }
              if(err<0)
                snd_pcm_prepare(handle);
              break;
            }
        }
      
      if(sent_this_time<0)
	{
	  printf("Audio error sending data: %s\n", snd_strerror(sent_this_time));

	  if(error_count++>10)
	    return true;  // error

	  snd_pcm_recover(handle, sent_this_time, false);
	  continue;
	}

      error_count = 0;
      
      num_to_send -= sent_this_time;
      num_sent += sent_this_time;

      //printf("Sent %ld of %ld\n", (long) num_sent, (long) length);
    }

  return false;  // No error
}


void beep_manager::run()
{
  for(;;)
    {
      
      bool success = start_audio();

      if(!success)
	{
	  usleep(100000);
	  continue;
	}

      bool error = false;
      
      while(!error)
	{
	  // Need to send a new frame
	  
	  beep_job* job = next_job;
	  
	  if(job)
	    {
	      // Send data from the job
	      //printf("Beginning audio job.\n");
	      
	      error = send_job(job->data, job->data_length);
	      
	      // job is finished.  Remove it and wait for another.
	      next_job = 0;
	    }
	  else
	    {
	      // Send a zero period
	      error = send_job(zero_data, period_length);
	    }
	}
    }
}





//
// Old code snippets from when exclusion was used.
//
// If we have a beep to do, wait until it's over to cancel the thread.
//
//synchronized_data<bool> beeper::audio_exclusion;
//
//
//pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, 0);
//audio_exclusion.lock();
//
//  for(;;)
//    {
//      beep_command.lock();
//      beep_command.wait_for_change();
//      beep_command.unlock();
//
//      do_beep();
//    }
//
//audio_exclusion.unlock();
//pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, 0);
//
//
//
//
//  // Don't actually need to touch the data.  Just the fact that
//  // it might have changed will kick off the run method.
//  beep_command.lock();
//  beep_command.unlock();
