/*
 * Audio driver handler. This file is part of Shairport.
 * Copyright (c) James Laird 2013
 * Modifications (c) Mike Brady 2014--2025
 * All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#include "audio.h"
#include "common.h"
#include "config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef CONFIG_JACK
extern audio_output audio_jack;
#endif
#ifdef CONFIG_SNDIO
extern audio_output audio_sndio;
#endif
#ifdef CONFIG_AO
extern audio_output audio_ao;
#endif
#ifdef CONFIG_SOUNDIO
extern audio_output audio_soundio;
#endif
#ifdef CONFIG_PIPEWIRE
extern audio_output audio_pw;
#endif
#ifdef CONFIG_PULSEAUDIO
extern audio_output audio_pa;
#endif
#ifdef CONFIG_ALSA
extern audio_output audio_alsa;
#endif
#ifdef CONFIG_DUMMY
extern audio_output audio_dummy;
#endif
#ifdef CONFIG_PIPE
extern audio_output audio_pipe;
#endif
#ifdef CONFIG_STDOUT
extern audio_output audio_stdout;
#endif

static audio_output *outputs[] = {
#ifdef CONFIG_ALSA
    &audio_alsa,
#endif
#ifdef CONFIG_SNDIO
    &audio_sndio,
#endif
#ifdef CONFIG_PIPEWIRE
    &audio_pw,
#endif
#ifdef CONFIG_PULSEAUDIO
    &audio_pa,
#endif
#ifdef CONFIG_JACK
    &audio_jack,
#endif
#ifdef CONFIG_AO
    &audio_ao,
#endif
#ifdef CONFIG_SOUNDIO
    &audio_soundio,
#endif
#ifdef CONFIG_PIPE
    &audio_pipe,
#endif
#ifdef CONFIG_STDOUT
    &audio_stdout,
#endif
#ifdef CONFIG_DUMMY
    &audio_dummy,
#endif
    NULL};

audio_output *audio_get_output(const char *name) {
  audio_output **out;

  // default to the first
  if (!name)
    return outputs[0];

  for (out = outputs; *out; out++)
    if (!strcasecmp(name, (*out)->name))
      return *out;

  return NULL;
}

void audio_ls_outputs(void) {
  audio_output **out;

  printf("Available audio backends:\n");
  for (out = outputs; *out; out++)
    printf("    %s%s\n", (*out)->name, out == outputs ? " (default)" : "");

  for (out = outputs; *out; out++) {
    printf("\n");
    if ((*out)->help) {
      printf("Settings and options for the audio backend \"%s\":\n", (*out)->name);
      (*out)->help();
    } else {
      printf("There are no settings or options for the audio backend \"%s\".\n", (*out)->name);
    }
  }
}

void parse_audio_options(const char *named_stanza, uint32_t default_format_set,
                         uint32_t default_rate_set, uint32_t default_channel_set) {
  /* this must be called after the output device has been initialised, so that the default values
   * are set before any options are chosen */
  int value;
  double dvalue;
  const char *str = 0;
  if (config.cfg != NULL) {

    /* Get the desired buffer size setting (deprecated). */
    if (config_lookup_int(config.cfg, "general.audio_backend_buffer_desired_length", &value)) {
      inform("The setting general.audio_backend_buffer_desired_length is no longer supported. "
             "Please use general.audio_backend_buffer_desired_length_in_seconds instead.");
    }

    /* Get the desired backend buffer size setting in seconds. */
    /* This is the size of the buffer in the output system, e.g. in the DAC itself in ALSA */
    if (config_lookup_float(config.cfg, "general.audio_backend_buffer_desired_length_in_seconds",
                            &dvalue)) {
      if (dvalue < 0) {
        die("Invalid audio_backend_buffer_desired_length_in_seconds value: \"%f\". It "
            "should be 0.0 or greater."
            " The default is %.3f seconds",
            dvalue, config.audio_backend_buffer_desired_length);
      } else {
        config.audio_backend_buffer_desired_length = dvalue;
      }
    }

    /* Get the desired decoded buffer size setting in seconds. */
    /* This is the size of the buffer of decoded and deciphered audio held in the player's output
     * queue prior to sending it to the output system */
    if (config_lookup_float(config.cfg, "general.audio_decoded_buffer_desired_length_in_seconds",
                            &dvalue)) {
      if (dvalue < 0) {
        die("Invalid audio_decoded_buffer_desired_length_in_seconds value: \"%f\". It "
            "should be 0.0 or greater."
            " The default is %.3f seconds",
            dvalue, config.audio_decoded_buffer_desired_length);
      } else {
        config.audio_decoded_buffer_desired_length = dvalue;
      }
    }

    /* Get the minimum buffer size for fancy interpolation setting in seconds. */
    if (config_lookup_float(config.cfg,
                            "general.audio_backend_buffer_interpolation_threshold_in_seconds",
                            &dvalue)) {
      if ((dvalue < 0) || (dvalue > config.audio_backend_buffer_desired_length)) {
        die("Invalid audio_backend_buffer_interpolation_threshold_in_seconds value: \"%f\". It "
            "should be between 0 and "
            "audio_backend_buffer_desired_length_in_seconds of %.3f, default is %.3f seconds",
            dvalue, config.audio_backend_buffer_desired_length,
            config.audio_backend_buffer_interpolation_threshold_in_seconds);
      } else {
        config.audio_backend_buffer_interpolation_threshold_in_seconds = dvalue;
      }
    }

    /* Get the latency offset (deprecated). */
    if (config_lookup_int(config.cfg, "general.audio_backend_latency_offset", &value)) {

      inform("The setting general.audio_backend_latency_offset is no longer supported. "
             "Please use general.audio_backend_latency_offset_in_seconds instead.");
    }

    /* Get the latency offset in seconds. */
    if (config_lookup_float(config.cfg, "general.audio_backend_latency_offset_in_seconds",
                            &dvalue)) {
      config.audio_backend_latency_offset = dvalue;
    }

    /* Check if the length of the silent lead-in ia set to \"auto\". */
    if (config_lookup_string(config.cfg, "general.audio_backend_silent_lead_in_time", &str)) {
      if (strcasecmp(str, "auto") == 0) {
        config.audio_backend_silent_lead_in_time_auto = 1;
      } else {
        if (config.audio_backend_silent_lead_in_time_auto == 1)
          warn("Invalid audio_backend_silent_lead_in_time \"%s\". It should be \"auto\" or the "
               "lead-in time in seconds. "
               "It remains set to \"auto\". Note: numbers should not be placed in quotes.",
               str);
        else
          warn("Invalid output rate \"%s\". It should be \"auto\" or the lead-in time in seconds. "
               "It remains set to %f. Note: numbers should not be placed in quotes.",
               str, config.audio_backend_silent_lead_in_time);
      }
    }

    /* Get the desired length of the silent lead-in. */
    if (config_lookup_float(config.cfg, "general.audio_backend_silent_lead_in_time", &dvalue)) {
      if ((dvalue < 0.0) || (dvalue > 4)) {
        if (config.audio_backend_silent_lead_in_time_auto == 1)
          warn("Invalid audio_backend_silent_lead_in_time \"%f\". It "
               "must be between 0.0 and 4.0 seconds. Omit the setting to use the automatic value. "
               "The setting remains at \"auto\".",
               dvalue);
        else
          warn("Invalid audio_backend_silent_lead_in_time \"%f\". It "
               "must be between 0.0 and 4.0 seconds. Omit the setting to use the automatic value. "
               "It remains set to %f.",
               dvalue, config.audio_backend_silent_lead_in_time);
      } else {
        config.audio_backend_silent_lead_in_time = dvalue;
        config.audio_backend_silent_lead_in_time_auto = 0;
      }
    }
  }

  if (named_stanza != NULL) {
    config.format_set = get_format_settings(named_stanza, "output_format");
    config.rate_set = get_rate_settings(named_stanza, "output_rate");
    config.channel_set = get_channel_settings(named_stanza, "output_channels");
  }
  // use the supplied defaults if no settings were given
  if (config.format_set == 0)
    config.format_set = default_format_set;
  if (config.rate_set == 0)
    config.rate_set = default_rate_set;
  if (config.channel_set == 0)
    config.channel_set = default_channel_set;
}

uint32_t get_format_settings(const char *stanza_name, const char *setting_name) {
  uint32_t format_set = 0;
  if (config.cfg != NULL) {
    char setting_path[256];
    snprintf(setting_path, sizeof(setting_path) - 1, "%s.%s", stanza_name, setting_name);
    // get any format settings -- can be "auto", a single format e.g. "S16" or a list of formats
    config_setting_t *format_setting = config_lookup(config.cfg, setting_path);
    if (format_setting != NULL) {
      const char **format_settings;
      int format_settings_count =
          config_get_string_settings_as_string_array(format_setting, &format_settings);
      if (format_settings_count > 0) {
        int i;
        for (i = 0; i < format_settings_count; i++) {
          debug(3, "format setting %u: \"%s\".", i, format_settings[i]);
          if (strcmp(format_settings[i], "auto") == 0) {
            if (format_settings_count != 1)
              warn("in the \"%s\" setting in the \"%s\" section of the configuration file, "
                   "multiple formats, including \"auto\", "
                   "are specified, but \"auto\" includes all "
                   "formats anyway...",
                   setting_name, stanza_name);
            format_set = SPS_FORMAT_SET; // all valid formats
          } else if (strcmp(format_settings[i], "S16") == 0) {
            format_set |= (1 << SPS_FORMAT_S16_LE) | (1 << SPS_FORMAT_S16_BE);
          } else if (strcmp(format_settings[i], "S24") == 0) {
            format_set |= (1 << SPS_FORMAT_S24_LE) | (1 << SPS_FORMAT_S24_BE) |
                          (1 << SPS_FORMAT_S24_3LE) | (1 << SPS_FORMAT_S24_3BE);
          } else if (strcmp(format_settings[i], "S32") == 0) {
            format_set |= (1 << SPS_FORMAT_S32_LE) | (1 << SPS_FORMAT_S32_BE);
          } else {
            sps_format_t f;
            int valid = 0;
            for (f = SPS_FORMAT_LOWEST; f <= SPS_FORMAT_HIGHEST_NATIVE; f++) {
              if (strcmp(format_settings[i], sps_format_description_string(f)) == 0) {
                format_set |= (1 << f);
                valid = 1;
              }
            }
            if (valid == 0) {
              warn("in the \"%s\" setting in the \"%s\" section of the configuration file, an "
                   "invalid format: \"%s\" has been detected.",
                   setting_name, stanza_name, format_settings[i]);
            }
          }
        }
        free(format_settings);
      } else {
        debug(1,
              "in the \"%s\" setting in the \"%s\" section of the configuration file, an error has "
              "been detected at argument %d.",
              setting_name, stanza_name, -format_settings_count);
      }
    }
  }
  sps_format_t f;
  uint32_t t_format_set = format_set;
  char buf[256];
  char *p = buf;
  for (f = SPS_FORMAT_UNKNOWN; f <= SPS_FORMAT_S32_BE; f++) {
    if ((t_format_set & (1 << f)) != 0) {
      snprintf(p, sizeof(buf) - (p - buf) - 1, "%s", sps_format_description_string(f));
      p = p + strlen(sps_format_description_string(f));
      t_format_set -= (1 << f);
      if (t_format_set != 0) {
        snprintf(p, sizeof(buf) - (p - buf) - 1, ", ");
        p = p + strlen(", ");
      }
    }
  }
  if (p != buf) {
    *p = '.';
    p++;
  }
  *p = '\0';
  if (strlen(buf) == 0)
    debug(3, "No \"%s\" output format settings.", stanza_name);
  else
    debug(3, "The \"%s\" output format settings are: \"%s\".", stanza_name, buf);
  return format_set;
}

uint32_t get_rate_settings(const char *stanza_name, const char *setting_name) {
  uint32_t rate_set = 0;
  if (config.cfg != NULL) {
    char setting_path[256];
    snprintf(setting_path, sizeof(setting_path) - 1, "%s.%s", stanza_name, setting_name);
    config_setting_t *rate_setting = config_lookup(config.cfg, setting_path);
    if (rate_setting != NULL) {
      if (config_setting_type(rate_setting) == CONFIG_TYPE_STRING) {
        // see if it is "auto"
        if (strcmp(config_setting_get_string(rate_setting), "auto") == 0) {
#ifdef CONFIG_FFMPEG
          rate_set = SPS_RATE_SET; // all valid rates
#else
          rate_set = SPS_RATE_NON_FFMPEG_SET;
#endif
        } else {
          warn("In the \"%s\" setting in the \"%s\" section of the configuration file, an invalid "
               "character string -- \"%s\" -- has been detected. (Note that numbers must not be "
               "enclosed in quotes.)",
               setting_name, stanza_name, config_setting_get_string(rate_setting));
        }
      } else {
        int *rates;
        int rate_settings_count = config_get_int_settings_as_int_array(rate_setting, &rates);
        if (rate_settings_count > 0) {
          debug(3, "%d rate settings found.", rate_settings_count);
          int i;
          for (i = 0; i < rate_settings_count; i++) {
            debug(3, "rate setting %d: %d.", i, rates[i]);
            sps_rate_t r;
            int valid = 0;
            for (r = SPS_RATE_5512; r <= SPS_RATE_384000; r++) {
              if ((unsigned int)rates[i] == sps_rate_actual_rate(r)) {
                valid = 1;

#ifdef CONFIG_FFMPEG
                rate_set |= (1 << r);
#else
                if (((1 << r) & SPS_RATE_NON_FFMPEG_SET) != 0) {
                  rate_set |= (1 << r);
                } else {
                  warn("In the \"%s\" setting in the \"%s\" section of the configuration file, "
                       "the rate selected -- %d -- can not be used because Shairport Sync has been "
                       "built without FFmpeg support.",
                       setting_name, stanza_name, rates[i]);
                }
#endif
              }
            }
            if (valid == 0) {
              warn("In the \"%s\" setting in the \"%s\" section of the configuration file, an "
                   "invalid rate: %d has been detected.",
                   setting_name, stanza_name, rates[i]);
            }
          }
          free(rates);
        } else {
          warn("in the \"%s\" setting in the \"%s\" section of the configuration file, an error "
               "has been detected at argument %d. (Note that numbers must not be enclosed in "
               "quotes.)",
               setting_name, stanza_name, -rate_settings_count);
        }
      }
    }
  }
  sps_rate_t r;
  char buf[256];
  char *p = buf;
  uint32_t t_rate_set = rate_set;
  char numbuf[32];
  for (r = SPS_RATE_UNKNOWN; r <= SPS_RATE_384000; r++) {
    if ((t_rate_set & (1 << r)) != 0) {
      snprintf(numbuf, sizeof(numbuf) - 1, "%u", sps_rate_actual_rate(r));
      snprintf(p, sizeof(buf) - (p - buf) - 1, "%s", numbuf);
      p = p + strlen(numbuf);
      t_rate_set -= (1 << r);
      if (t_rate_set != 0) {
        snprintf(p, sizeof(buf) - (p - buf) - 1, ", ");
        p = p + strlen(", ");
      }
    }
  }
  if (p != buf) {
    *p = '.';
    p++;
  }
  *p = '\0';
  if (strlen(buf) == 0)
    debug(3, "No \"%s\" output rate settings.", stanza_name);
  else
    debug(3, "The \"%s\" output rate settings are: \"%s\".", stanza_name, buf);
  return rate_set;
}

uint32_t get_channel_settings(const char *stanza_name, const char *setting_name) {
  uint32_t channel_set = 0;
  if (config.cfg != NULL) {
    char setting_path[256];
    snprintf(setting_path, sizeof(setting_path) - 1, "%s.%s", stanza_name, setting_name);
    // now get the channels count set
    config_setting_t *channels_setting = config_lookup(config.cfg, setting_path);
    if (channels_setting != NULL) {
      if (config_setting_type(channels_setting) == CONFIG_TYPE_STRING) {
        // see if it is "auto"
        if (strcmp(config_setting_get_string(channels_setting), "auto") == 0) {
#ifdef CONFIG_FFMPEG
          channel_set = SPS_CHANNEL_SET; // all valid channels
#else
          channel_set = SPS_CHANNNEL_NON_FFMPEG_SET; // just two channels
#endif
        } else {
          warn("in the \"%s\" setting in the \"%s\" section of the configuration file, an invalid "
               "setting: \"%s\" has been detected.",
               setting_name, stanza_name, config_setting_get_string(channels_setting));
        }
      } else {
        int *channel_counts;
        int channel_counts_count =
            config_get_int_settings_as_int_array(channels_setting, &channel_counts);
        if (channel_counts_count > 0) {
          debug(3, "%d channel count settings found.", channel_counts_count);
          int i;
          for (i = 0; i < channel_counts_count; i++) {
            debug(3, "channel count setting %d: %d.", i, channel_counts[i]);

            if ((channel_counts[i] >= 1) && (channel_counts[i] <= 8)) {
#ifdef CONFIG_FFMPEG
              channel_set |= (1 << channel_counts[i]);
#else
              if (((1 << channel_counts[i]) & SPS_CHANNNEL_NON_FFMPEG_SET) != 0) {
                channel_set |= (1 << channel_counts[i]);
              } else {
                warn("in the \"%s\" setting in the \"%s\" section of the configuration file, "
                     "the channel count selected -- %d -- can not be used because Shairport Sync "
                     "has been built without FFmpeg support.",
                     setting_name, stanza_name, channel_counts[i]);
              }
#endif
            } else {
              warn("in the \"%s\" setting in the \"%s\" section of the configuration file, an "
                   "invalid channel count: %d has been detected.",
                   setting_name, stanza_name, channel_counts[i]);
            }
          }
          free(channel_counts);
        } else {
          debug(1,
                "in the \"%s\" setting in the \"%s\" section of the configuration file, an error "
                "has been detected at argument %d.",
                setting_name, stanza_name, -channel_counts_count);
        }
      }
    }
  }
  char buf[256];
  char *p = buf;
  char numbuf[32];
  unsigned int c;
  uint32_t t_channel_set = channel_set;
  for (c = 0; c <= 8; c++) {
    if ((t_channel_set & (1 << c)) != 0) {
      snprintf(numbuf, sizeof(numbuf) - 1, "%u", c);
      snprintf(p, sizeof(buf) - (p - buf) - 1, "%s", numbuf);
      p = p + strlen(numbuf);
      t_channel_set -= (1 << c);
      if (t_channel_set != 0) {
        snprintf(p, sizeof(buf) - (p - buf) - 1, ", ");
        p = p + strlen(", ");
      }
    }
  }
  if (p != buf) {
    *p = '.';
    p++;
  }
  *p = '\0';
  if (strlen(buf) == 0)
    debug(3, "No \"%s\" output channel settings.", stanza_name);
  else
    debug(3, "The \"%s\" output channel settings are: \"%s\".", stanza_name, buf);
  return channel_set;
}

sps_format_t check_configuration_with_formats(
    unsigned int channels, unsigned int rate, sps_format_t format,
    int (*check_configuration)(unsigned int channels, unsigned int rate, unsigned int format)) {
  sps_format_t response = SPS_FORMAT_UNKNOWN; // this is encoded as zero

  // first, check that the channels are permissible...
  if ((config.channel_set & (1 << channels)) != 0) {
    // now check the rate...
    sps_rate_t r = SPS_RATE_LOWEST;
    int rate_is_permissible = 0;
    while ((rate_is_permissible == 0) && (r <= SPS_RATE_HIGHEST)) {
      if (((config.rate_set & (1 << r)) != 0) && (rate == sps_rate_actual_rate(r)))
        rate_is_permissible = 1;
      else
        r++;
    }
    if (rate_is_permissible != 0) {
      // check the actual requested format first with the initial_search array
      // and if it fails, then check in the subsequent_search array.
      // the choice of subsequent search array is determined by the machine's endianness

      // clang-format off
      sps_format_t ordered_initial_search_U8[] = {SPS_FORMAT_U8, SPS_FORMAT_S8};
      sps_format_t ordered_initial_search_S8[] = {SPS_FORMAT_S8, SPS_FORMAT_U8};
      sps_format_t ordered_subsequent_search_8_LE[] = {SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE, SPS_FORMAT_S24_LE, SPS_FORMAT_S24_BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE};
      sps_format_t ordered_subsequent_search_8_BE[] = {SPS_FORMAT_S16_BE, SPS_FORMAT_S16_LE, SPS_FORMAT_S24_BE, SPS_FORMAT_S24_LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S32_LE};
      
      sps_format_t ordered_initial_search_S16_LE[] = {SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE};
      sps_format_t ordered_initial_search_S16_BE[] = {SPS_FORMAT_S16_BE, SPS_FORMAT_S16_LE};
      sps_format_t ordered_subsequent_search_S16_LE[] = {SPS_FORMAT_S24_LE, SPS_FORMAT_S24_BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S8, SPS_FORMAT_U8};
      sps_format_t ordered_subsequent_search_S16_BE[] = {SPS_FORMAT_S24_BE, SPS_FORMAT_S24_LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S32_LE, SPS_FORMAT_S8, SPS_FORMAT_U8};
      
      sps_format_t ordered_initial_search_S24_LE[] = {SPS_FORMAT_S24_LE, SPS_FORMAT_S24_BE};
      sps_format_t ordered_initial_search_S24_BE[] = {SPS_FORMAT_S24_BE, SPS_FORMAT_S24_LE};
      sps_format_t ordered_subsequent_search_S24_LE[] = {SPS_FORMAT_S24_3LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE, SPS_FORMAT_S8, SPS_FORMAT_U8};
      sps_format_t ordered_subsequent_search_S24_BE[] = {SPS_FORMAT_S24_3BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S32_LE, SPS_FORMAT_S16_BE, SPS_FORMAT_S16_LE, SPS_FORMAT_S8, SPS_FORMAT_U8};
      
      sps_format_t ordered_initial_search_S24_3le[] = {SPS_FORMAT_S24_3LE, SPS_FORMAT_S24_3BE};
      sps_format_t ordered_initial_search_S24_3be[] = {SPS_FORMAT_S24_3BE, SPS_FORMAT_S24_3LE};
      sps_format_t ordered_subsequent_search_S24_3le[] = {SPS_FORMAT_S24_LE, SPS_FORMAT_S24_BE, SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE, SPS_FORMAT_S8, SPS_FORMAT_U8};
      sps_format_t ordered_subsequent_search_S24_3be[] = {SPS_FORMAT_S24_BE, SPS_FORMAT_S24_LE, SPS_FORMAT_S32_BE, SPS_FORMAT_S32_LE, SPS_FORMAT_S16_BE, SPS_FORMAT_S16_LE, SPS_FORMAT_S8, SPS_FORMAT_U8};
      
      sps_format_t ordered_initial_search_S32_LE[] = {SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE};
      sps_format_t ordered_initial_search_S32_BE[] = {SPS_FORMAT_S32_BE, SPS_FORMAT_S32_LE};
      sps_format_t ordered_subsequent_search_S32_LE[] = {SPS_FORMAT_S24_LE, SPS_FORMAT_S24_BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE, SPS_FORMAT_S8, SPS_FORMAT_U8};
      sps_format_t ordered_subsequent_search_S32_BE[] = {SPS_FORMAT_S24_BE, SPS_FORMAT_S24_LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S16_BE, SPS_FORMAT_S16_LE, SPS_FORMAT_S8, SPS_FORMAT_U8};
      // clang-format on

      // set up the initial_search pointer
      sps_format_t *initial_formats_to_check = NULL;
      unsigned int number_of_initial_formats_to_check = 0;

      // set up the subsequent_search pointer
      sps_format_t *subsequent_formats_to_check = NULL;
      unsigned int number_of_subsequent_formats_to_check = 0;

      // Set up the initial and subsequent search pointers.

      // Searching is done with an initial search and, if necessary, a subsequent search.
      // Searching stops when a suitable format is confirmed as being acceptable.

      // The initial search checks the requested format first, followed by its equivalents of the
      // same bit size. The subsequent search puts the formats to check in order of suitability but
      // with the native-endian format first. SPS_FORMAT_S16, SPS_FORMAT_S24 and SPS_FORMAT_S24 are
      // treated the same as their equivalent with native endianness. AUTO is treated as
      // SPS_FORMAT_S24 with native endianness.

      switch (format) {
      case SPS_FORMAT_U8:
      case SPS_FORMAT_S8:
        if (config.endianness == SS_LITTLE_ENDIAN) {
          subsequent_formats_to_check = ordered_subsequent_search_8_LE;
          number_of_subsequent_formats_to_check =
              sizeof(ordered_subsequent_search_8_LE) / sizeof(sps_format_t);
        } else {
          subsequent_formats_to_check = ordered_subsequent_search_8_BE;
          number_of_subsequent_formats_to_check =
              sizeof(ordered_subsequent_search_8_BE) / sizeof(sps_format_t);
        }
        if (format == SPS_FORMAT_S8) {
          initial_formats_to_check = ordered_initial_search_S8;
          number_of_initial_formats_to_check =
              sizeof(ordered_initial_search_S8) / sizeof(sps_format_t);
        } else {
          initial_formats_to_check = ordered_initial_search_U8;
          number_of_initial_formats_to_check =
              sizeof(ordered_initial_search_U8) / sizeof(sps_format_t);
        }
        break;
      case SPS_FORMAT_S16:
      case SPS_FORMAT_S16_LE:
      case SPS_FORMAT_S16_BE:
        if (config.endianness == SS_LITTLE_ENDIAN) {
          subsequent_formats_to_check = ordered_subsequent_search_S16_LE;
          number_of_subsequent_formats_to_check =
              sizeof(ordered_subsequent_search_S16_LE) / sizeof(sps_format_t);
          if (format == SPS_FORMAT_S16) {
            initial_formats_to_check = ordered_initial_search_S16_LE;
            number_of_initial_formats_to_check =
                sizeof(ordered_initial_search_S16_LE) / sizeof(sps_format_t);
          }
        } else {
          subsequent_formats_to_check = ordered_subsequent_search_S16_BE;
          number_of_subsequent_formats_to_check =
              sizeof(ordered_subsequent_search_S16_BE) / sizeof(sps_format_t);
          if (format == SPS_FORMAT_S16) {
            initial_formats_to_check = ordered_initial_search_S16_BE;
            number_of_initial_formats_to_check =
                sizeof(ordered_initial_search_S16_BE) / sizeof(sps_format_t);
          }
        }
        if (format == SPS_FORMAT_S16_LE) {
          initial_formats_to_check = ordered_initial_search_S16_LE;
          number_of_initial_formats_to_check =
              sizeof(ordered_initial_search_S16_LE) / sizeof(sps_format_t);
        } else if (format == SPS_FORMAT_S16_BE) {
          initial_formats_to_check = ordered_initial_search_S16_BE;
          number_of_initial_formats_to_check =
              sizeof(ordered_initial_search_S16_BE) / sizeof(sps_format_t);
        }
        break;
      case SPS_FORMAT_S24:
      case SPS_FORMAT_S24_LE:
      case SPS_FORMAT_S24_BE:
        if (config.endianness == SS_LITTLE_ENDIAN) {
          subsequent_formats_to_check = ordered_subsequent_search_S24_LE;
          number_of_subsequent_formats_to_check =
              sizeof(ordered_subsequent_search_S24_LE) / sizeof(sps_format_t);
          if (format == SPS_FORMAT_S24) {
            initial_formats_to_check = ordered_initial_search_S24_LE;
            number_of_initial_formats_to_check =
                sizeof(ordered_initial_search_S24_LE) / sizeof(sps_format_t);
          }
        } else {
          subsequent_formats_to_check = ordered_subsequent_search_S24_BE;
          number_of_subsequent_formats_to_check =
              sizeof(ordered_subsequent_search_S24_BE) / sizeof(sps_format_t);
          if (format == SPS_FORMAT_S24) {
            initial_formats_to_check = ordered_initial_search_S24_BE;
            number_of_initial_formats_to_check =
                sizeof(ordered_initial_search_S24_BE) / sizeof(sps_format_t);
          }
        }
        if (format == SPS_FORMAT_S24_LE) {
          initial_formats_to_check = ordered_initial_search_S24_LE;
          number_of_initial_formats_to_check =
              sizeof(ordered_initial_search_S24_LE) / sizeof(sps_format_t);
        } else if (format == SPS_FORMAT_S24_BE) {
          initial_formats_to_check = ordered_initial_search_S24_BE;
          number_of_initial_formats_to_check =
              sizeof(ordered_initial_search_S24_BE) / sizeof(sps_format_t);
        }
        break;
      case SPS_FORMAT_S32:
      case SPS_FORMAT_S32_LE:
      case SPS_FORMAT_S32_BE:
        //      case SPS_FORMAT_AUTO:
        if (config.endianness == SS_LITTLE_ENDIAN) {
          subsequent_formats_to_check = ordered_subsequent_search_S32_LE;
          number_of_subsequent_formats_to_check =
              sizeof(ordered_subsequent_search_S32_LE) / sizeof(sps_format_t);
          if ((format == SPS_FORMAT_S32) || (format == SPS_FORMAT_AUTO)) {
            initial_formats_to_check = ordered_initial_search_S32_LE;
            number_of_initial_formats_to_check =
                sizeof(ordered_initial_search_S32_LE) / sizeof(sps_format_t);
          }
        } else {
          subsequent_formats_to_check = ordered_subsequent_search_S32_BE;
          number_of_subsequent_formats_to_check =
              sizeof(ordered_subsequent_search_S32_BE) / sizeof(sps_format_t);
          if ((format == SPS_FORMAT_S32) || (format == SPS_FORMAT_AUTO)) {
            initial_formats_to_check = ordered_initial_search_S32_BE;
            number_of_initial_formats_to_check =
                sizeof(ordered_initial_search_S32_BE) / sizeof(sps_format_t);
          }
        }
        if (format == SPS_FORMAT_S32_LE) {
          initial_formats_to_check = ordered_initial_search_S32_LE;
          number_of_initial_formats_to_check =
              sizeof(ordered_initial_search_S32_LE) / sizeof(sps_format_t);
        } else if (format == SPS_FORMAT_S32_BE) {
          initial_formats_to_check = ordered_initial_search_S32_BE;
          number_of_initial_formats_to_check =
              sizeof(ordered_initial_search_S32_BE) / sizeof(sps_format_t);
        }
        break;
      case SPS_FORMAT_S24_3LE:
      case SPS_FORMAT_S24_3BE:
        if (config.endianness == SS_LITTLE_ENDIAN) {
          subsequent_formats_to_check = ordered_subsequent_search_S24_3le;
          number_of_subsequent_formats_to_check =
              sizeof(ordered_subsequent_search_S24_3le) / sizeof(sps_format_t);
        } else {
          subsequent_formats_to_check = ordered_subsequent_search_S24_3be;
          number_of_subsequent_formats_to_check =
              sizeof(ordered_subsequent_search_S24_3be) / sizeof(sps_format_t);
        }
        if (format == SPS_FORMAT_S24_3LE) {
          initial_formats_to_check = ordered_initial_search_S24_3le;
          number_of_initial_formats_to_check =
              sizeof(ordered_initial_search_S24_3le) / sizeof(sps_format_t);
        } else {
          initial_formats_to_check = ordered_initial_search_S24_3be;
          number_of_initial_formats_to_check =
              sizeof(ordered_initial_search_S24_3be) / sizeof(sps_format_t);
        }
        break;
      default:
        debug(1, "unknown format %u to check.", format);
        break;
      }

      if ((initial_formats_to_check == NULL) || (subsequent_formats_to_check == NULL)) {
        debug(1, "error initialising for format %s.", sps_format_description_string(format));
      }

      // here, we know the channel and rate are permissible according to the settings,
      // but we don't yet know about each of the formats
      // do initial search
      unsigned int i = 0;
      while ((i < number_of_initial_formats_to_check) && (response == SPS_FORMAT_UNKNOWN)) {
        // only call check_configuration() if the format is permissible
        if ((((1 << initial_formats_to_check[i]) & config.format_set) != 0) &&
            ((check_configuration == NULL) ||
             (check_configuration(channels, rate, initial_formats_to_check[i]) == 0)))
          response = initial_formats_to_check[i];
        else
          i++;
      }
      // if no joy, do subsequent search
      if (response == 0) {
        i = 0;
        while ((i < number_of_subsequent_formats_to_check) && (response == SPS_FORMAT_UNKNOWN)) {
          // only call check_configuration() if the format is permissible
          if ((((1 << subsequent_formats_to_check[i]) & config.format_set) != 0) &&
              ((check_configuration == NULL) ||
               (check_configuration(channels, rate, subsequent_formats_to_check[i]) == 0)))
            response = subsequent_formats_to_check[i];
          else
            i++;
        }
      }
    }
  }
  return response;
}

/*

sps_format_t all_formats_to_check_le[] = {SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE,  SPS_FORMAT_S24_LE,
                                          SPS_FORMAT_S24_BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S24_3BE,
                                          SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE};
// look at little-endian formats first
sps_format_t all_formats_to_check_le[] = {SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE,  SPS_FORMAT_S24_LE,
                                          SPS_FORMAT_S24_BE, SPS_FORMAT_S24_3LE, SPS_FORMAT_S24_3BE,
                                          SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE};
// look at big-endian formats first
sps_format_t all_formats_to_check_be[] = {SPS_FORMAT_S32_BE, SPS_FORMAT_S32_LE,  SPS_FORMAT_S24_BE,
                                          SPS_FORMAT_S24_LE, SPS_FORMAT_S24_3BE, SPS_FORMAT_S24_3LE,
                                          SPS_FORMAT_S16_BE, SPS_FORMAT_S16_LE};
// look at little-endian formats first
sps_format_t s32_formats_to_check_le[] = {SPS_FORMAT_S32_LE, SPS_FORMAT_S32_BE};
// look at big-endian formats first
sps_format_t s32_formats_to_check_be[] = {SPS_FORMAT_S32_BE, SPS_FORMAT_S32_LE};
// look at little-endian formats first
sps_format_t s24_formats_to_check_le[] = {
    SPS_FORMAT_S24_LE,
    SPS_FORMAT_S24_BE,
    SPS_FORMAT_S24_3LE,
    SPS_FORMAT_S24_3BE,
};
// look at big-endian formats first
sps_format_t s24_formats_to_check_be[] = {
    SPS_FORMAT_S24_BE,
    SPS_FORMAT_S24_LE,
    SPS_FORMAT_S24_3BE,
    SPS_FORMAT_S24_3LE,
};
// look at little-endian formats first
sps_format_t s16_formats_to_check_le[] = {SPS_FORMAT_S16_LE, SPS_FORMAT_S16_BE};
// look at big-endian formats first
sps_format_t s16_formats_to_check_be[] = {SPS_FORMAT_S16_BE, SPS_FORMAT_S16_LE};

sps_format_t single_format_to_check[1];

sps_format_t check_configuration_with_formats(
    unsigned int channels, unsigned int rate, sps_format_t format,
    int (*check_configuration)(unsigned int channels, unsigned int rate, unsigned int format)) {
  sps_format_t response = SPS_FORMAT_UNKNOWN; // this is encoded as zero

  // assume there is one single format to check
  unsigned int number_of_formats_to_check = 1;
  sps_format_t single_format_to_check[] = {format};
  sps_format_t *formats_to_check = single_format_to_check;

  // now, check to see if there are multiple formats to check, e.g. if the format is given as AUTO
  // or if it doesn't specify _LE or _BE. Try to favour the host endianness:

  if (config.endianness == SS_LITTLE_ENDIAN) {
    switch (format) {
    case SPS_FORMAT_AUTO:
    case SPS_FORMAT_S32:
    case SPS_FORMAT_32_LE:
    case SPS_FORMAT_32_BE:
      formats_to_check = all_formats_to_check_le;
      number_of_formats_to_check = sizeof(all_formats_to_check_le) / sizeof(sps_format_t);
      break;
    case SPS_FORMAT_S24:
    case SPS_FORMAT_S24_LE:
    case SPS_FORMAT_S24_BE:
      formats_to_check = s32_formats_to_check_le;
      number_of_formats_to_check = sizeof(s32_formats_to_check_le) / sizeof(sps_format_t);
      break;
    case SPS_FORMAT_S24:
      formats_to_check = s24_formats_to_check_le;
      number_of_formats_to_check = sizeof(s24_formats_to_check_le) / sizeof(sps_format_t);
      break;
    case SPS_FORMAT_S16:
      formats_to_check = s16_formats_to_check_le;
      number_of_formats_to_check = sizeof(s16_formats_to_check_le) / sizeof(sps_format_t);
      break;
    default:
      break;
    };
  } else { // big-endian
    switch (format) {
    case SPS_FORMAT_AUTO:
      formats_to_check = all_formats_to_check_be;
      number_of_formats_to_check = sizeof(all_formats_to_check_be) / sizeof(sps_format_t);
      break;
    case SPS_FORMAT_S32:
      formats_to_check = s32_formats_to_check_be;
      number_of_formats_to_check = sizeof(s32_formats_to_check_be) / sizeof(sps_format_t);
      break;
    case SPS_FORMAT_S24:
      formats_to_check = s24_formats_to_check_be;
      number_of_formats_to_check = sizeof(s24_formats_to_check_be) / sizeof(sps_format_t);
      break;
    case SPS_FORMAT_S16:
      formats_to_check = s16_formats_to_check_be;
      number_of_formats_to_check = sizeof(s16_formats_to_check_be) / sizeof(sps_format_t);
      break;
    default:
      break;
    };
  }

  unsigned int i = 0;
  while ((i < number_of_formats_to_check) && (response == SPS_FORMAT_UNKNOWN)) {
    if (check_configuration(channels, rate, formats_to_check[i]) == 0)
      response = formats_to_check[i];
    else
      i++;
  }

  return response;
}

*/

// This looks for a suitable configuration in the right order and checks possible
// configurations using the check_configuration method passed in.

// If it's the same as the most recent request, it sends back the cached reply.
// The cache is keyed to the address of the checker method in order to distinguish
// between requests from different backends. It's moot, as only one back end can be
// active, but anyway... ya never just know.

// but it does mean that it only remembers the last response, whichever backend it was for

typedef struct {
  int32_t encoded_request;  // this will have the channels and the rate requested but not the format
  int32_t encoded_response; // this will have the channels, rate and format suggested
  int (*check_configuration)(unsigned int channels, unsigned int rate, unsigned int format);
} configuration_request_and_response_t;

static configuration_request_and_response_t search_for_suitable_configuration_last_response = {
    0, 0, NULL};

// First it looks for an exact match of channels, rate or with a rate that is an even multiple.
// If that fails it will look for an exact match of channels with the next highest rate
// If that fails it will look for an exact match of channels with the next lowest rate
// If that fails it increases the number channels, if available, and checks all over again
// Finally, if that fails it reduces the number channels, if possible, and checks all over again.

// Regarding the requested format: ask for the exact format only if no audio processing will be
// done in the software or (as far as can be known) in the hardware mixer.
// So, if ignore_volume_control is set and volume_max_db is
// not set, and the number of output channels is greater than or equal to those requested,
// and mixing to mono is not requested and the rate being checked is the rate requested,
// then look for the exact rate format.
// Otherwise ask for the deepest format, which could be useful
// for attenuation, mixing, transcoding or other audio processing.
// (Haven't included checking for convolution here.)

int32_t search_for_suitable_configuration(unsigned int channels, unsigned int rate,
                                          unsigned int format,
                                          int (*check_configuration)(unsigned int in_channels,
                                                                     unsigned int in_rate,
                                                                     unsigned int in_format)) {
  int32_t reply = 0; // means nothing was found
  if ((channels == 0) || (rate == 0))
    debug(1, "invalid channel %u or rate %u parameters to search_for_suitable_configuration",
          channels, rate);
  int32_t encoded_request = CHANNELS_TO_ENCODED_FORMAT(channels) | RATE_TO_ENCODED_FORMAT(rate) |
                            FORMAT_TO_ENCODED_FORMAT(format);

  if ((search_for_suitable_configuration_last_response.check_configuration ==
       check_configuration) && // same method (implies same backend)
      (search_for_suitable_configuration_last_response.encoded_request ==
       encoded_request)) { // same request
    reply = search_for_suitable_configuration_last_response
                .encoded_response; // provide cached response...
  }
  if (reply == 0) { // no luck with the last response generated, if any...
    // check for native number of channels or more... and then, if not successful, with fewer
    // channels
#ifdef CONFIG_FFMPEG
    unsigned int channel_count_check_sequence[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1};
    unsigned int rates[] = {44100, 48000, 88200, 96000, 176400, 192000, 352800, 384000,
                            64000, 32000, 22050, 16000, 11025,  8000,   5512};
#else
    unsigned int channel_count_check_sequence[] = {0, 1, 2};
//    unsigned int rates[] = {44100};
#endif

    unsigned int channel_count_index = channels;
    // start looking with the required number of channels
    unsigned int local_channels;
    unsigned int local_rate;
    sps_format_t local_format = SPS_FORMAT_UNKNOWN;
    uint32_t channel_check_bit_field = 0; // set (1 << channel_count) when check done.
    // Use it to stop checking the same channel count on the way down as well as on the way up...
    while ((local_format == SPS_FORMAT_UNKNOWN) &&
           (channel_count_index < (sizeof(channel_count_check_sequence) / sizeof(unsigned int)))) {
      local_channels = channel_count_check_sequence[channel_count_index];
      // if we haven't already checked....
      if ((channel_check_bit_field & (1 << local_channels)) == 0) {
        channel_check_bit_field |= (1 << local_channels); // flag that we have checked it
        int rate_multiplier = 2;
        local_rate = rate; // begin with the requested rate
        debug(3, "check for an exact multiple of %u with %u channels.", rate, local_channels);
        while ((local_rate <= 384000) && (local_format == SPS_FORMAT_UNKNOWN)) {
          if (
              // clang-format off
            // check for the exact format only under these conditions, otherwise look for the best
            (config.ignore_volume_control != 0) &&
            (config.volume_max_db_set == 0) &&
            (local_rate == rate) &&
            (local_channels >= channels) &&
            (config.playback_mode != ST_mono)
              // clang-format on
          ) {
            // debug(1, "check exact");
            local_format = check_configuration_with_formats(
                local_channels, local_rate, (sps_format_t)format, check_configuration);
          } else {
            // debug(1, "check best");
            local_format = check_configuration_with_formats(local_channels, local_rate,
                                                            SPS_FORMAT_S32, check_configuration);
          }

          if (local_format == SPS_FORMAT_UNKNOWN) {
            local_rate = rate * rate_multiplier;
            rate_multiplier = rate_multiplier * 2;
          }
        }
#ifdef CONFIG_FFMPEG
        if (local_format == SPS_FORMAT_UNKNOWN) {
          debug(3, "check for the next highest rate above %u with %u channels.", rate,
                local_channels);
          unsigned int rate_pointer = 0;
          while ((rate_pointer < sizeof(rates) / sizeof(unsigned int)) &&
                 (local_format == SPS_FORMAT_UNKNOWN)) {
            local_rate = rates[rate_pointer];
            if (local_rate > rate) {
              local_format = check_configuration_with_formats(
                  local_channels, local_rate, (sps_format_t)format, check_configuration);
            }
            if (local_format == SPS_FORMAT_UNKNOWN) {
              rate_pointer++;
            }
          }
        }

        if (local_format == SPS_FORMAT_UNKNOWN) {
          int rate_pointer = (int)(sizeof(rates) / sizeof(unsigned int) - 1);
          debug(3, "check for the next lowest rate below %u with %u channels.", rate,
                local_channels);
          while ((rate_pointer >= 0) && (local_format == SPS_FORMAT_UNKNOWN)) {
            local_rate = rates[rate_pointer];
            if (local_rate < rate) {
              local_format = check_configuration_with_formats(
                  local_channels, local_rate, (sps_format_t)format, check_configuration);
            }
            if (local_format == SPS_FORMAT_UNKNOWN) {
              rate_pointer--;
            }
          }
        }
#endif
      }
      // if unsuccessful, try with, firstly, more channels, and then, later, with fewer channels
      if (local_format == SPS_FORMAT_UNKNOWN)
        channel_count_index++;
    }
    if (local_format != SPS_FORMAT_UNKNOWN) {
      reply = CHANNELS_TO_ENCODED_FORMAT(local_channels) | RATE_TO_ENCODED_FORMAT(local_rate) |
              FORMAT_TO_ENCODED_FORMAT(local_format);

      // cache the response
      search_for_suitable_configuration_last_response.check_configuration = check_configuration;
      search_for_suitable_configuration_last_response.encoded_request = encoded_request;
      search_for_suitable_configuration_last_response.encoded_response = reply;

      debug(3,
            "output configuration search for request: %s/%u/%u, succeeded with response: %u/%s/%u.",
            sps_format_description_string(format), rate, channels,
            RATE_FROM_ENCODED_FORMAT(reply),                                  // rate
            sps_format_description_string(FORMAT_FROM_ENCODED_FORMAT(reply)), // format
            CHANNELS_FROM_ENCODED_FORMAT(reply)                               // channels
      );

    } else {
      debug(1, "output configuration search for request: %s/%u/%u failed.",
            sps_format_description_string(format), rate, channels);
    }
  }
  return reply;
}
