use futures_util::{future, FutureExt, StreamExt};
use librespot_playback::player::PlayerEvent;
use log::{error, info, trace, warn};
use sha1::{Digest, Sha1};
use thiserror::Error;
use tokio::sync::mpsc::UnboundedReceiver;
use url::Url;

use librespot::connect::spirc::Spirc;
use librespot::core::authentication::Credentials;
use librespot::core::cache::Cache;
use librespot::core::config::{ConnectConfig, DeviceType, SessionConfig};
use librespot::core::session::Session;
use librespot::core::version;
use librespot::playback::audio_backend::{self, SinkBuilder, BACKENDS};
use librespot::playback::config::{
    AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl,
};
use librespot::playback::dither;
#[cfg(feature = "alsa-backend")]
use librespot::playback::mixer::alsamixer::AlsaMixer;
use librespot::playback::mixer::{self, MixerConfig, MixerFn};
use librespot::playback::player::{coefficient_to_duration, duration_to_coefficient, Player};

mod player_event_handler;
use player_event_handler::{emit_sink_event, run_program_on_events};

use std::env;
use std::ops::RangeInclusive;
use std::path::Path;
use std::pin::Pin;
use std::process::exit;
use std::str::FromStr;
use std::time::Duration;
use std::time::Instant;

fn device_id(name: &str) -> String {
    hex::encode(Sha1::digest(name.as_bytes()))
}

fn usage(program: &str, opts: &getopts::Options) -> String {
    let repo_home = env!("CARGO_PKG_REPOSITORY");
    let desc = env!("CARGO_PKG_DESCRIPTION");
    let version = get_version_string();
    let brief = format!(
        "{}\n\n{}\n\n{}\n\nUsage: {} [<Options>]",
        version, desc, repo_home, program
    );
    opts.usage(&brief)
}

fn setup_logging(quiet: bool, verbose: bool) {
    let mut builder = env_logger::Builder::new();
    match env::var("RUST_LOG") {
        Ok(config) => {
            builder.parse_filters(&config);
            builder.init();

            if verbose {
                warn!("`--verbose` flag overidden by `RUST_LOG` environment variable");
            } else if quiet {
                warn!("`--quiet` flag overidden by `RUST_LOG` environment variable");
            }
        }
        Err(_) => {
            if verbose {
                builder.parse_filters("libmdns=info,librespot=trace");
            } else if quiet {
                builder.parse_filters("libmdns=warn,librespot=warn");
            } else {
                builder.parse_filters("libmdns=info,librespot=info");
            }
            builder.init();

            if verbose && quiet {
                warn!("`--verbose` and `--quiet` are mutually exclusive. Logging can not be both verbose and quiet. Using verbose mode.");
            }
        }
    }
}

fn list_backends() {
    println!("Available backends: ");
    for (&(name, _), idx) in BACKENDS.iter().zip(0..) {
        if idx == 0 {
            println!("- {} (default)", name);
        } else {
            println!("- {}", name);
        }
    }
}

#[derive(Debug, Error)]
pub enum ParseFileSizeError {
    #[error("empty argument")]
    EmptyInput,
    #[error("invalid suffix")]
    InvalidSuffix,
    #[error("invalid number: {0}")]
    InvalidNumber(#[from] std::num::ParseFloatError),
    #[error("non-finite number specified")]
    NotFinite(f64),
}

pub fn parse_file_size(input: &str) -> Result<u64, ParseFileSizeError> {
    use ParseFileSizeError::*;

    let mut iter = input.chars();
    let mut suffix = iter.next_back().ok_or(EmptyInput)?;
    let mut suffix_len = 0;

    let iec = matches!(suffix, 'i' | 'I');

    if iec {
        suffix_len += 1;
        suffix = iter.next_back().ok_or(InvalidSuffix)?;
    }

    let base: u64 = if iec { 1024 } else { 1000 };

    suffix_len += 1;
    let exponent = match suffix.to_ascii_uppercase() {
        '0'..='9' if !iec => {
            suffix_len -= 1;
            0
        }
        'K' => 1,
        'M' => 2,
        'G' => 3,
        'T' => 4,
        'P' => 5,
        'E' => 6,
        'Z' => 7,
        'Y' => 8,
        _ => return Err(InvalidSuffix),
    };

    let num = {
        let mut iter = input.chars();

        for _ in (&mut iter).rev().take(suffix_len) {}

        iter.as_str().parse::<f64>()?
    };

    if !num.is_finite() {
        return Err(NotFinite(num));
    }

    Ok((num * base.pow(exponent) as f64) as u64)
}

fn get_version_string() -> String {
    #[cfg(debug_assertions)]
    const BUILD_PROFILE: &str = "debug";
    #[cfg(not(debug_assertions))]
    const BUILD_PROFILE: &str = "release";

    format!(
        "librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id}, Profile: {build_profile})",
        semver = version::SEMVER,
        sha = version::SHA_SHORT,
        build_date = version::BUILD_DATE,
        build_id = version::BUILD_ID,
        build_profile = BUILD_PROFILE
    )
}

struct Setup {
    format: AudioFormat,
    backend: SinkBuilder,
    device: Option<String>,
    mixer: MixerFn,
    cache: Option<Cache>,
    player_config: PlayerConfig,
    session_config: SessionConfig,
    connect_config: ConnectConfig,
    mixer_config: MixerConfig,
    credentials: Option<Credentials>,
    enable_discovery: bool,
    zeroconf_port: u16,
    player_event_program: Option<String>,
    emit_sink_events: bool,
}

fn get_setup() -> Setup {
    const VALID_INITIAL_VOLUME_RANGE: RangeInclusive<u16> = 0..=100;
    const VALID_VOLUME_RANGE: RangeInclusive<f64> = 0.0..=100.0;
    const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive<f64> = 0.0..=10.0;
    const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive<f64> = -10.0..=10.0;
    const VALID_NORMALISATION_THRESHOLD_RANGE: RangeInclusive<f64> = -10.0..=0.0;
    const VALID_NORMALISATION_ATTACK_RANGE: RangeInclusive<u64> = 1..=500;
    const VALID_NORMALISATION_RELEASE_RANGE: RangeInclusive<u64> = 1..=1000;

    const AP_PORT: &str = "ap-port";
    const AUTOPLAY: &str = "autoplay";
    const BACKEND: &str = "backend";
    const BITRATE: &str = "bitrate";
    const CACHE: &str = "cache";
    const CACHE_SIZE_LIMIT: &str = "cache-size-limit";
    const DEVICE: &str = "device";
    const DEVICE_TYPE: &str = "device-type";
    const DISABLE_AUDIO_CACHE: &str = "disable-audio-cache";
    const DISABLE_CREDENTIAL_CACHE: &str = "disable-credential-cache";
    const DISABLE_DISCOVERY: &str = "disable-discovery";
    const DISABLE_GAPLESS: &str = "disable-gapless";
    const DITHER: &str = "dither";
    const EMIT_SINK_EVENTS: &str = "emit-sink-events";
    const ENABLE_VOLUME_NORMALISATION: &str = "enable-volume-normalisation";
    const FORMAT: &str = "format";
    const HELP: &str = "help";
    const INITIAL_VOLUME: &str = "initial-volume";
    const MIXER_TYPE: &str = "mixer";
    const ALSA_MIXER_DEVICE: &str = "alsa-mixer-device";
    const ALSA_MIXER_INDEX: &str = "alsa-mixer-index";
    const ALSA_MIXER_CONTROL: &str = "alsa-mixer-control";
    const NAME: &str = "name";
    const NORMALISATION_ATTACK: &str = "normalisation-attack";
    const NORMALISATION_GAIN_TYPE: &str = "normalisation-gain-type";
    const NORMALISATION_KNEE: &str = "normalisation-knee";
    const NORMALISATION_METHOD: &str = "normalisation-method";
    const NORMALISATION_PREGAIN: &str = "normalisation-pregain";
    const NORMALISATION_RELEASE: &str = "normalisation-release";
    const NORMALISATION_THRESHOLD: &str = "normalisation-threshold";
    const ONEVENT: &str = "onevent";
    const PASSTHROUGH: &str = "passthrough";
    const PASSWORD: &str = "password";
    const PROXY: &str = "proxy";
    const QUIET: &str = "quiet";
    const SYSTEM_CACHE: &str = "system-cache";
    const USERNAME: &str = "username";
    const VERBOSE: &str = "verbose";
    const VERSION: &str = "version";
    const VOLUME_CTRL: &str = "volume-ctrl";
    const VOLUME_RANGE: &str = "volume-range";
    const ZEROCONF_PORT: &str = "zeroconf-port";

    // Mostly arbitrary.
    const AUTOPLAY_SHORT: &str = "A";
    const AP_PORT_SHORT: &str = "a";
    const BACKEND_SHORT: &str = "B";
    const BITRATE_SHORT: &str = "b";
    const SYSTEM_CACHE_SHORT: &str = "C";
    const CACHE_SHORT: &str = "c";
    const DITHER_SHORT: &str = "D";
    const DEVICE_SHORT: &str = "d";
    const VOLUME_CTRL_SHORT: &str = "E";
    const VOLUME_RANGE_SHORT: &str = "e";
    const DEVICE_TYPE_SHORT: &str = "F";
    const FORMAT_SHORT: &str = "f";
    const DISABLE_AUDIO_CACHE_SHORT: &str = "G";
    const DISABLE_GAPLESS_SHORT: &str = "g";
    const DISABLE_CREDENTIAL_CACHE_SHORT: &str = "H";
    const HELP_SHORT: &str = "h";
    const CACHE_SIZE_LIMIT_SHORT: &str = "M";
    const MIXER_TYPE_SHORT: &str = "m";
    const ENABLE_VOLUME_NORMALISATION_SHORT: &str = "N";
    const NAME_SHORT: &str = "n";
    const DISABLE_DISCOVERY_SHORT: &str = "O";
    const ONEVENT_SHORT: &str = "o";
    const PASSTHROUGH_SHORT: &str = "P";
    const PASSWORD_SHORT: &str = "p";
    const EMIT_SINK_EVENTS_SHORT: &str = "Q";
    const QUIET_SHORT: &str = "q";
    const INITIAL_VOLUME_SHORT: &str = "R";
    const ALSA_MIXER_DEVICE_SHORT: &str = "S";
    const ALSA_MIXER_INDEX_SHORT: &str = "s";
    const ALSA_MIXER_CONTROL_SHORT: &str = "T";
    const NORMALISATION_ATTACK_SHORT: &str = "U";
    const USERNAME_SHORT: &str = "u";
    const VERSION_SHORT: &str = "V";
    const VERBOSE_SHORT: &str = "v";
    const NORMALISATION_GAIN_TYPE_SHORT: &str = "W";
    const NORMALISATION_KNEE_SHORT: &str = "w";
    const NORMALISATION_METHOD_SHORT: &str = "X";
    const PROXY_SHORT: &str = "x";
    const NORMALISATION_PREGAIN_SHORT: &str = "Y";
    const NORMALISATION_RELEASE_SHORT: &str = "y";
    const NORMALISATION_THRESHOLD_SHORT: &str = "Z";
    const ZEROCONF_PORT_SHORT: &str = "z";

    // Options that have different desc's
    // depending on what backends were enabled at build time.
    #[cfg(feature = "alsa-backend")]
    const MIXER_TYPE_DESC: &str = "Mixer to use {alsa|softvol}. Defaults to softvol.";
    #[cfg(not(feature = "alsa-backend"))]
    const MIXER_TYPE_DESC: &str = "Not supported by the included audio backend(s).";
    #[cfg(any(
        feature = "alsa-backend",
        feature = "rodio-backend",
        feature = "portaudio-backend"
    ))]
    const DEVICE_DESC: &str = "Audio device to use. Use ? to list options if using alsa, portaudio or rodio. Defaults to the backend's default.";
    #[cfg(not(any(
        feature = "alsa-backend",
        feature = "rodio-backend",
        feature = "portaudio-backend"
    )))]
    const DEVICE_DESC: &str = "Not supported by the included audio backend(s).";
    #[cfg(feature = "alsa-backend")]
    const ALSA_MIXER_CONTROL_DESC: &str =
        "Alsa mixer control, e.g. PCM, Master or similar. Defaults to PCM.";
    #[cfg(not(feature = "alsa-backend"))]
    const ALSA_MIXER_CONTROL_DESC: &str = "Not supported by the included audio backend(s).";
    #[cfg(feature = "alsa-backend")]
    const ALSA_MIXER_DEVICE_DESC: &str = "Alsa mixer device, e.g hw:0 or similar from `aplay -l`. Defaults to `--device` if specified, default otherwise.";
    #[cfg(not(feature = "alsa-backend"))]
    const ALSA_MIXER_DEVICE_DESC: &str = "Not supported by the included audio backend(s).";
    #[cfg(feature = "alsa-backend")]
    const ALSA_MIXER_INDEX_DESC: &str = "Alsa index of the cards mixer. Defaults to 0.";
    #[cfg(not(feature = "alsa-backend"))]
    const ALSA_MIXER_INDEX_DESC: &str = "Not supported by the included audio backend(s).";
    #[cfg(feature = "alsa-backend")]
    const INITIAL_VOLUME_DESC: &str = "Initial volume in % from 0 - 100. Default for softvol: 50. For the alsa mixer: the current volume.";
    #[cfg(not(feature = "alsa-backend"))]
    const INITIAL_VOLUME_DESC: &str = "Initial volume in % from 0 - 100. Defaults to 50.";
    #[cfg(feature = "alsa-backend")]
    const VOLUME_RANGE_DESC: &str = "Range of the volume control (dB) from 0.0 to 100.0. Default for softvol: 60.0. For the alsa mixer: what the control supports.";
    #[cfg(not(feature = "alsa-backend"))]
    const VOLUME_RANGE_DESC: &str =
        "Range of the volume control (dB) from 0.0 to 100.0. Defaults to 60.0.";

    let mut opts = getopts::Options::new();
    opts.optflag(
        HELP_SHORT,
        HELP,
        "Print this help menu.",
    )
    .optflag(
        VERSION_SHORT,
        VERSION,
        "Display librespot version string.",
    )
    .optflag(
        VERBOSE_SHORT,
        VERBOSE,
        "Enable verbose log output.",
    )
    .optflag(
        QUIET_SHORT,
        QUIET,
        "Only log warning and error messages.",
    )
    .optflag(
        DISABLE_AUDIO_CACHE_SHORT,
        DISABLE_AUDIO_CACHE,
        "Disable caching of the audio data.",
    )
    .optflag(
        DISABLE_CREDENTIAL_CACHE_SHORT,
        DISABLE_CREDENTIAL_CACHE,
        "Disable caching of credentials.",
    )
    .optflag(
        DISABLE_DISCOVERY_SHORT,
        DISABLE_DISCOVERY,
        "Disable zeroconf discovery mode.",
    )
    .optflag(
        DISABLE_GAPLESS_SHORT,
        DISABLE_GAPLESS,
        "Disable gapless playback.",
    )
    .optflag(
        EMIT_SINK_EVENTS_SHORT,
        EMIT_SINK_EVENTS,
        "Run PROGRAM set by `--onevent` before the sink is opened and after it is closed.",
    )
    .optflag(
        AUTOPLAY_SHORT,
        AUTOPLAY,
        "Automatically play similar songs when your music ends.",
    )
    .optflag(
        PASSTHROUGH_SHORT,
        PASSTHROUGH,
        "Pass a raw stream to the output. Only works with the pipe and subprocess backends.",
    )
    .optflag(
        ENABLE_VOLUME_NORMALISATION_SHORT,
        ENABLE_VOLUME_NORMALISATION,
        "Play all tracks at approximately the same apparent volume.",
    )
    .optopt(
        NAME_SHORT,
        NAME,
        "Device name. Defaults to Librespot.",
        "NAME",
    )
    .optopt(
        BITRATE_SHORT,
        BITRATE,
        "Bitrate (kbps) {96|160|320}. Defaults to 160.",
        "BITRATE",
    )
    .optopt(
        FORMAT_SHORT,
        FORMAT,
        "Output format {F64|F32|S32|S24|S24_3|S16}. Defaults to S16.",
        "FORMAT",
    )
    .optopt(
        DITHER_SHORT,
        DITHER,
        "Specify the dither algorithm to use {none|gpdf|tpdf|tpdf_hp}. Defaults to tpdf for formats S16, S24, S24_3 and none for other formats.",
        "DITHER",
    )
    .optopt(
        DEVICE_TYPE_SHORT,
        DEVICE_TYPE,
        "Displayed device type. Defaults to speaker.",
        "TYPE",
    )
    .optopt(
        CACHE_SHORT,
        CACHE,
        "Path to a directory where files will be cached.",
        "PATH",
    )
    .optopt(
        SYSTEM_CACHE_SHORT,
        SYSTEM_CACHE,
        "Path to a directory where system files (credentials, volume) will be cached. May be different from the `--cache` option value.",
        "PATH",
    )
    .optopt(
        CACHE_SIZE_LIMIT_SHORT,
        CACHE_SIZE_LIMIT,
        "Limits the size of the cache for audio files. It's possible to use suffixes like K, M or G, e.g. 16G for example.",
        "SIZE"
    )
    .optopt(
        BACKEND_SHORT,
        BACKEND,
        "Audio backend to use. Use ? to list options.",
        "NAME",
    )
    .optopt(
        USERNAME_SHORT,
        USERNAME,
        "Username used to sign in with.",
        "USERNAME",
    )
    .optopt(
        PASSWORD_SHORT,
        PASSWORD,
        "Password used to sign in with.",
        "PASSWORD",
    )
    .optopt(
        ONEVENT_SHORT,
        ONEVENT,
        "Run PROGRAM when a playback event occurs.",
        "PROGRAM",
    )
    .optopt(
        ALSA_MIXER_CONTROL_SHORT,
        ALSA_MIXER_CONTROL,
        ALSA_MIXER_CONTROL_DESC,
        "NAME",
    )
    .optopt(
        ALSA_MIXER_DEVICE_SHORT,
        ALSA_MIXER_DEVICE,
        ALSA_MIXER_DEVICE_DESC,
        "DEVICE",
    )
    .optopt(
        ALSA_MIXER_INDEX_SHORT,
        ALSA_MIXER_INDEX,
        ALSA_MIXER_INDEX_DESC,
        "NUMBER",
    )
    .optopt(
        MIXER_TYPE_SHORT,
        MIXER_TYPE,
        MIXER_TYPE_DESC,
        "MIXER",
    )
    .optopt(
        DEVICE_SHORT,
        DEVICE,
        DEVICE_DESC,
        "NAME",
    )
    .optopt(
        INITIAL_VOLUME_SHORT,
        INITIAL_VOLUME,
        INITIAL_VOLUME_DESC,
        "VOLUME",
    )
    .optopt(
        VOLUME_CTRL_SHORT,
        VOLUME_CTRL,
        "Volume control scale type {cubic|fixed|linear|log}. Defaults to log.",
        "VOLUME_CTRL"
    )
    .optopt(
        VOLUME_RANGE_SHORT,
        VOLUME_RANGE,
        VOLUME_RANGE_DESC,
        "RANGE",
    )
    .optopt(
        NORMALISATION_METHOD_SHORT,
        NORMALISATION_METHOD,
        "Specify the normalisation method to use {basic|dynamic}. Defaults to dynamic.",
        "METHOD",
    )
    .optopt(
        NORMALISATION_GAIN_TYPE_SHORT,
        NORMALISATION_GAIN_TYPE,
        "Specify the normalisation gain type to use {track|album|auto}. Defaults to auto.",
        "TYPE",
    )
    .optopt(
        NORMALISATION_PREGAIN_SHORT,
        NORMALISATION_PREGAIN,
        "Pregain (dB) applied by volume normalisation from -10.0 to 10.0. Defaults to 0.0.",
        "PREGAIN",
    )
    .optopt(
        NORMALISATION_THRESHOLD_SHORT,
        NORMALISATION_THRESHOLD,
        "Threshold (dBFS) at which point the dynamic limiter engages to prevent clipping from 0.0 to -10.0. Defaults to -2.0.",
        "THRESHOLD",
    )
    .optopt(
        NORMALISATION_ATTACK_SHORT,
        NORMALISATION_ATTACK,
        "Attack time (ms) in which the dynamic limiter reduces gain from 1 to 500. Defaults to 5.",
        "TIME",
    )
    .optopt(
        NORMALISATION_RELEASE_SHORT,
        NORMALISATION_RELEASE,
        "Release or decay time (ms) in which the dynamic limiter restores gain from 1 to 1000. Defaults to 100.",
        "TIME",
    )
    .optopt(
        NORMALISATION_KNEE_SHORT,
        NORMALISATION_KNEE,
        "Knee width (dB) of the dynamic limiter from 0.0 to 10.0. Defaults to 5.0.",
        "KNEE",
    )
    .optopt(
        ZEROCONF_PORT_SHORT,
        ZEROCONF_PORT,
        "The port the internal server advertises over zeroconf 1 - 65535. Ports <= 1024 may require root privileges.",
        "PORT",
    )
    .optopt(
        PROXY_SHORT,
        PROXY,
        "HTTP proxy to use when connecting.",
        "URL",
    )
    .optopt(
        AP_PORT_SHORT,
        AP_PORT,
        "Connect to an AP with a specified port 1 - 65535. If no AP with that port is present a fallback AP will be used. Available ports are usually 80, 443 and 4070.",
        "PORT",
    );

    let args: Vec<_> = std::env::args_os()
        .filter_map(|s| match s.into_string() {
            Ok(valid) => Some(valid),
            Err(s) => {
                eprintln!(
                    "Command line argument was not valid Unicode and will not be evaluated: {:?}",
                    s
                );
                None
            }
        })
        .collect();

    let matches = match opts.parse(&args[1..]) {
        Ok(m) => m,
        Err(e) => {
            eprintln!("Error parsing command line options: {}", e);
            println!("\n{}", usage(&args[0], &opts));
            exit(1);
        }
    };

    let stripped_env_key = |k: &str| {
        k.trim_start_matches("LIBRESPOT_")
            .replace("_", "-")
            .to_lowercase()
    };

    let env_vars: Vec<_> = env::vars_os().filter_map(|(k, v)| match k.into_string() {
        Ok(key) if key.starts_with("LIBRESPOT_") => {
            let stripped_key = stripped_env_key(&key);
            // We only care about long option/flag names.
            if stripped_key.chars().count() > 1 && matches.opt_defined(&stripped_key) {
                match v.into_string() {
                    Ok(value) => Some((key, value)),
                    Err(s) => {
                        eprintln!("Environment variable was not valid Unicode and will not be evaluated: {}={:?}", key, s);
                        None
                    }
                }
            } else {
                None
            }
        },
        _ => None
    })
    .collect();

    let opt_present =
        |opt| matches.opt_present(opt) || env_vars.iter().any(|(k, _)| stripped_env_key(k) == opt);

    let opt_str = |opt| {
        if matches.opt_present(opt) {
            matches.opt_str(opt)
        } else {
            env_vars
                .iter()
                .find(|(k, _)| stripped_env_key(k) == opt)
                .map(|(_, v)| v.to_string())
        }
    };

    if opt_present(HELP) {
        println!("{}", usage(&args[0], &opts));
        exit(0);
    }

    if opt_present(VERSION) {
        println!("{}", get_version_string());
        exit(0);
    }

    setup_logging(opt_present(QUIET), opt_present(VERBOSE));

    info!("{}", get_version_string());

    if !env_vars.is_empty() {
        trace!("Environment variable(s):");

        for (k, v) in &env_vars {
            if matches!(k.as_str(), "LIBRESPOT_PASSWORD" | "LIBRESPOT_USERNAME") {
                trace!("\t\t{}=\"XXXXXXXX\"", k);
            } else if v.is_empty() {
                trace!("\t\t{}=", k);
            } else {
                trace!("\t\t{}=\"{}\"", k, v);
            }
        }
    }

    let args_len = args.len();

    if args_len > 1 {
        trace!("Command line argument(s):");

        for (index, key) in args.iter().enumerate() {
            let opt = key.trim_start_matches('-');

            if index > 0
                && &args[index - 1] != key
                && matches.opt_defined(opt)
                && matches.opt_present(opt)
            {
                if matches!(opt, PASSWORD | PASSWORD_SHORT | USERNAME | USERNAME_SHORT) {
                    // Don't log creds.
                    trace!("\t\t{} \"XXXXXXXX\"", key);
                } else {
                    let value = matches.opt_str(opt).unwrap_or_else(|| "".to_string());
                    if value.is_empty() {
                        trace!("\t\t{}", key);
                    } else {
                        trace!("\t\t{} \"{}\"", key, value);
                    }
                }
            }
        }
    }

    #[cfg(not(feature = "alsa-backend"))]
    for a in &[
        MIXER_TYPE,
        ALSA_MIXER_DEVICE,
        ALSA_MIXER_INDEX,
        ALSA_MIXER_CONTROL,
    ] {
        if opt_present(a) {
            warn!("Alsa specific options have no effect if the alsa backend is not enabled at build time.");
            break;
        }
    }

    let backend_name = opt_str(BACKEND);
    if backend_name == Some("?".into()) {
        list_backends();
        exit(0);
    }

    let invalid_error_msg =
        |long: &str, short: &str, invalid: &str, valid_values: &str, default_value: &str| {
            error!("Invalid `--{}` / `-{}`: \"{}\"", long, short, invalid);

            if !valid_values.is_empty() {
                println!("Valid `--{}` / `-{}` values: {}", long, short, valid_values);
            }

            if !default_value.is_empty() {
                println!("Default: {}", default_value);
            }
        };

    let empty_string_error_msg = |long: &str, short: &str| {
        error!("`--{}` / `-{}` can not be an empty string", long, short);
        exit(1);
    };

    let backend = audio_backend::find(backend_name).unwrap_or_else(|| {
        invalid_error_msg(
            BACKEND,
            BACKEND_SHORT,
            &opt_str(BACKEND).unwrap_or_default(),
            "",
            "",
        );

        list_backends();
        exit(1);
    });

    let format = opt_str(FORMAT)
        .as_deref()
        .map(|format| {
            AudioFormat::from_str(format).unwrap_or_else(|_| {
                let default_value = &format!("{:?}", AudioFormat::default());
                invalid_error_msg(
                    FORMAT,
                    FORMAT_SHORT,
                    format,
                    "F64, F32, S32, S24, S24_3, S16",
                    default_value,
                );

                exit(1);
            })
        })
        .unwrap_or_default();

    let device = opt_str(DEVICE);
    if let Some(ref value) = device {
        if value == "?" {
            backend(device, format);
            exit(0);
        } else if value.is_empty() {
            empty_string_error_msg(DEVICE, DEVICE_SHORT);
        }
    }

    #[cfg(feature = "alsa-backend")]
    let mixer_type = opt_str(MIXER_TYPE);
    #[cfg(not(feature = "alsa-backend"))]
    let mixer_type: Option<String> = None;

    let mixer = mixer::find(mixer_type.as_deref()).unwrap_or_else(|| {
        invalid_error_msg(
            MIXER_TYPE,
            MIXER_TYPE_SHORT,
            &opt_str(MIXER_TYPE).unwrap_or_default(),
            "alsa, softvol",
            "softvol",
        );

        exit(1);
    });

    let is_alsa_mixer = match mixer_type.as_deref() {
        #[cfg(feature = "alsa-backend")]
        Some(AlsaMixer::NAME) => true,
        _ => false,
    };

    #[cfg(feature = "alsa-backend")]
    if !is_alsa_mixer {
        for a in &[ALSA_MIXER_DEVICE, ALSA_MIXER_INDEX, ALSA_MIXER_CONTROL] {
            if opt_present(a) {
                warn!("Alsa specific mixer options have no effect if not using the alsa mixer.");
                break;
            }
        }
    }

    let mixer_config = {
        let mixer_default_config = MixerConfig::default();

        #[cfg(feature = "alsa-backend")]
        let index = if !is_alsa_mixer {
            mixer_default_config.index
        } else {
            opt_str(ALSA_MIXER_INDEX)
                .map(|index| {
                    index.parse::<u32>().unwrap_or_else(|_| {
                        invalid_error_msg(
                            ALSA_MIXER_INDEX,
                            ALSA_MIXER_INDEX_SHORT,
                            &index,
                            "",
                            &mixer_default_config.index.to_string(),
                        );

                        exit(1);
                    })
                })
                .unwrap_or_else(|| match device {
                    // Look for the dev index portion of --device.
                    // Specifically <dev index> when --device is <something>:CARD=<card name>,DEV=<dev index>
                    // or <something>:<card index>,<dev index>.

                    // If --device does not contain a ',' it does not contain a dev index.
                    // In the case that the dev index is omitted it is assumed to be 0 (mixer_default_config.index).
                    // Malformed --device values will also fallback to mixer_default_config.index.
                    Some(ref device_name) if device_name.contains(',') => {
                        // Turn <something>:CARD=<card name>,DEV=<dev index> or <something>:<card index>,<dev index>
                        // into DEV=<dev index> or <dev index>.
                        let dev = &device_name[device_name.find(',').unwrap_or_default()..]
                            .trim_start_matches(',');

                        // Turn DEV=<dev index> into <dev index> (noop if it's already <dev index>)
                        // and then parse <dev index>.
                        // Malformed --device values will fail the parse and fallback to mixer_default_config.index.
                        dev[dev.find('=').unwrap_or_default()..]
                            .trim_start_matches('=')
                            .parse::<u32>()
                            .unwrap_or(mixer_default_config.index)
                    }
                    _ => mixer_default_config.index,
                })
        };

        #[cfg(not(feature = "alsa-backend"))]
        let index = mixer_default_config.index;

        #[cfg(feature = "alsa-backend")]
        let device = if !is_alsa_mixer {
            mixer_default_config.device
        } else {
            match opt_str(ALSA_MIXER_DEVICE) {
                Some(mixer_device) => {
                    if mixer_device.is_empty() {
                        empty_string_error_msg(ALSA_MIXER_DEVICE, ALSA_MIXER_DEVICE_SHORT);
                    }

                    mixer_device
                }
                None => match device {
                    Some(ref device_name) => {
                        // Look for the card name or card index portion of --device.
                        // Specifically <card name> when --device is <something>:CARD=<card name>,DEV=<dev index>
                        // or card index when --device is <something>:<card index>,<dev index>.
                        // --device values like `pulse`, `default`, `jack` may be valid but there is no way to
                        // infer automatically what the mixer should be so they fail auto fallback
                        // so --alsa-mixer-device must be manually specified in those situations.
                        let start_index = device_name.find(':').unwrap_or_default();

                        let end_index = match device_name.find(',') {
                            Some(index) if index > start_index => index,
                            _ => device_name.len(),
                        };

                        let card = &device_name[start_index..end_index];

                        if card.starts_with(':') {
                            // mixers are assumed to be hw:CARD=<card name> or hw:<card index>.
                            "hw".to_owned() + card
                        } else {
                            error!(
                                "Could not find an alsa mixer for \"{}\", it must be specified with `--{}` / `-{}`",
                                &device.unwrap_or_default(),
                                ALSA_MIXER_DEVICE,
                                ALSA_MIXER_DEVICE_SHORT
                            );

                            exit(1);
                        }
                    }
                    None => {
                        error!(
                            "`--{}` / `-{}` or `--{}` / `-{}` \
                            must be specified when `--{}` / `-{}` is set to \"alsa\"",
                            DEVICE,
                            DEVICE_SHORT,
                            ALSA_MIXER_DEVICE,
                            ALSA_MIXER_DEVICE_SHORT,
                            MIXER_TYPE,
                            MIXER_TYPE_SHORT
                        );

                        exit(1);
                    }
                },
            }
        };

        #[cfg(not(feature = "alsa-backend"))]
        let device = mixer_default_config.device;

        #[cfg(feature = "alsa-backend")]
        let control = opt_str(ALSA_MIXER_CONTROL).unwrap_or(mixer_default_config.control);

        #[cfg(feature = "alsa-backend")]
        if control.is_empty() {
            empty_string_error_msg(ALSA_MIXER_CONTROL, ALSA_MIXER_CONTROL_SHORT);
        }

        #[cfg(not(feature = "alsa-backend"))]
        let control = mixer_default_config.control;

        let volume_range = opt_str(VOLUME_RANGE)
            .map(|range| match range.parse::<f64>() {
                Ok(value) if (VALID_VOLUME_RANGE).contains(&value) => value,
                _ => {
                    let valid_values = &format!(
                        "{} - {}",
                        VALID_VOLUME_RANGE.start(),
                        VALID_VOLUME_RANGE.end()
                    );

                    #[cfg(feature = "alsa-backend")]
                    let default_value = &format!(
                        "softvol - {}, alsa - what the control supports",
                        VolumeCtrl::DEFAULT_DB_RANGE
                    );

                    #[cfg(not(feature = "alsa-backend"))]
                    let default_value = &VolumeCtrl::DEFAULT_DB_RANGE.to_string();

                    invalid_error_msg(
                        VOLUME_RANGE,
                        VOLUME_RANGE_SHORT,
                        &range,
                        valid_values,
                        default_value,
                    );

                    exit(1);
                }
            })
            .unwrap_or_else(|| {
                if is_alsa_mixer {
                    0.0
                } else {
                    VolumeCtrl::DEFAULT_DB_RANGE
                }
            });

        let volume_ctrl = opt_str(VOLUME_CTRL)
            .as_deref()
            .map(|volume_ctrl| {
                VolumeCtrl::from_str_with_range(volume_ctrl, volume_range).unwrap_or_else(|_| {
                    invalid_error_msg(
                        VOLUME_CTRL,
                        VOLUME_CTRL_SHORT,
                        volume_ctrl,
                        "cubic, fixed, linear, log",
                        "log",
                    );

                    exit(1);
                })
            })
            .unwrap_or_else(|| VolumeCtrl::Log(volume_range));

        MixerConfig {
            device,
            control,
            index,
            volume_ctrl,
        }
    };

    let cache = {
        let volume_dir = opt_str(SYSTEM_CACHE)
            .or_else(|| opt_str(CACHE))
            .map(|p| p.into());

        let cred_dir = if opt_present(DISABLE_CREDENTIAL_CACHE) {
            None
        } else {
            volume_dir.clone()
        };

        let audio_dir = if opt_present(DISABLE_AUDIO_CACHE) {
            None
        } else {
            opt_str(CACHE)
                .as_ref()
                .map(|p| AsRef::<Path>::as_ref(p).join("files"))
        };

        let limit = if audio_dir.is_some() {
            opt_str(CACHE_SIZE_LIMIT)
                .as_deref()
                .map(parse_file_size)
                .map(|e| {
                    e.unwrap_or_else(|e| {
                        invalid_error_msg(
                            CACHE_SIZE_LIMIT,
                            CACHE_SIZE_LIMIT_SHORT,
                            &e.to_string(),
                            "",
                            "",
                        );

                        exit(1);
                    })
                })
        } else {
            None
        };

        if audio_dir.is_none() && opt_present(CACHE_SIZE_LIMIT) {
            warn!(
                "Without a `--{}` / `-{}` path, and/or if the `--{}` / `-{}` flag is set, `--{}` / `-{}` has no effect.",
                CACHE, CACHE_SHORT, DISABLE_AUDIO_CACHE, DISABLE_AUDIO_CACHE_SHORT, CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT
            );
        }

        match Cache::new(cred_dir, volume_dir, audio_dir, limit) {
            Ok(cache) => Some(cache),
            Err(e) => {
                warn!("Cannot create cache: {}", e);
                None
            }
        }
    };

    let credentials = {
        let cached_creds = cache.as_ref().and_then(Cache::credentials);

        if let Some(username) = opt_str(USERNAME) {
            if username.is_empty() {
                empty_string_error_msg(USERNAME, USERNAME_SHORT);
            }
            if let Some(password) = opt_str(PASSWORD) {
                if password.is_empty() {
                    empty_string_error_msg(PASSWORD, PASSWORD_SHORT);
                }
                Some(Credentials::with_password(username, password))
            } else {
                match cached_creds {
                    Some(creds) if username == creds.username => Some(creds),
                    _ => {
                        let prompt = &format!("Password for {}: ", username);
                        match rpassword::prompt_password_stderr(prompt) {
                            Ok(password) => {
                                if !password.is_empty() {
                                    Some(Credentials::with_password(username, password))
                                } else {
                                    trace!("Password was empty.");
                                    if cached_creds.is_some() {
                                        trace!("Using cached credentials.");
                                    }
                                    cached_creds
                                }
                            }
                            Err(e) => {
                                warn!("Cannot parse password: {}", e);
                                if cached_creds.is_some() {
                                    trace!("Using cached credentials.");
                                }
                                cached_creds
                            }
                        }
                    }
                }
            }
        } else {
            if cached_creds.is_some() {
                trace!("Using cached credentials.");
            }
            cached_creds
        }
    };

    let enable_discovery = !opt_present(DISABLE_DISCOVERY);

    if credentials.is_none() && !enable_discovery {
        error!("Credentials are required if discovery is disabled.");
        exit(1);
    }

    if !enable_discovery && opt_present(ZEROCONF_PORT) {
        warn!(
            "With the `--{}` / `-{}` flag set `--{}` / `-{}` has no effect.",
            DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT, ZEROCONF_PORT, ZEROCONF_PORT_SHORT
        );
    }

    let zeroconf_port = if enable_discovery {
        opt_str(ZEROCONF_PORT)
            .map(|port| match port.parse::<u16>() {
                Ok(value) if value != 0 => value,
                _ => {
                    let valid_values = &format!("1 - {}", u16::MAX);
                    invalid_error_msg(ZEROCONF_PORT, ZEROCONF_PORT_SHORT, &port, valid_values, "");

                    exit(1);
                }
            })
            .unwrap_or(0)
    } else {
        0
    };

    let connect_config = {
        let connect_default_config = ConnectConfig::default();

        let name = opt_str(NAME).unwrap_or_else(|| connect_default_config.name.clone());

        if name.is_empty() {
            empty_string_error_msg(NAME, NAME_SHORT);
            exit(1);
        }

        let initial_volume = opt_str(INITIAL_VOLUME)
            .map(|initial_volume| {
                let volume = match initial_volume.parse::<u16>() {
                    Ok(value) if (VALID_INITIAL_VOLUME_RANGE).contains(&value) => value,
                    _ => {
                        let valid_values = &format!(
                            "{} - {}",
                            VALID_INITIAL_VOLUME_RANGE.start(),
                            VALID_INITIAL_VOLUME_RANGE.end()
                        );

                        #[cfg(feature = "alsa-backend")]
                        let default_value = &format!(
                            "{}, or the current value when the alsa mixer is used.",
                            connect_default_config.initial_volume.unwrap_or_default()
                        );

                        #[cfg(not(feature = "alsa-backend"))]
                        let default_value = &connect_default_config
                            .initial_volume
                            .unwrap_or_default()
                            .to_string();

                        invalid_error_msg(
                            INITIAL_VOLUME,
                            INITIAL_VOLUME_SHORT,
                            &initial_volume,
                            valid_values,
                            default_value,
                        );

                        exit(1);
                    }
                };

                (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16
            })
            .or_else(|| {
                if is_alsa_mixer {
                    None
                } else {
                    cache.as_ref().and_then(Cache::volume)
                }
            });

        let device_type = opt_str(DEVICE_TYPE)
            .as_deref()
            .map(|device_type| {
                DeviceType::from_str(device_type).unwrap_or_else(|_| {
                    invalid_error_msg(
                        DEVICE_TYPE,
                        DEVICE_TYPE_SHORT,
                        device_type,
                        "computer, tablet, smartphone, \
                        speaker, tv, avr, stb, audiodongle, \
                        gameconsole, castaudio, castvideo, \
                        automobile, smartwatch, chromebook, \
                        carthing, homething",
                        DeviceType::default().into(),
                    );

                    exit(1);
                })
            })
            .unwrap_or_default();

        let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed);
        let autoplay = opt_present(AUTOPLAY);

        ConnectConfig {
            name,
            device_type,
            initial_volume,
            has_volume_ctrl,
            autoplay,
        }
    };

    let session_config = SessionConfig {
        user_agent: version::VERSION_STRING.to_string(),
        device_id: device_id(&connect_config.name),
        proxy: opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map(
            |s| {
                match Url::parse(&s) {
                    Ok(url) => {
                        if url.host().is_none() || url.port_or_known_default().is_none() {
                            error!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed");
                            exit(1);
                        }

                        if url.scheme() != "http" {
                            error!("Only unsecure http:// proxies are supported");
                            exit(1);
                        }

                        url
                    },
                    Err(e) => {
                        error!("Invalid proxy URL: \"{}\", only URLs in the format \"http://host:port\" are allowed", e);
                        exit(1);
                    }
                }
            },
        ),
        ap_port: opt_str(AP_PORT).map(|port| match port.parse::<u16>() {
            Ok(value) if value != 0 => value,
            _ => {
                let valid_values = &format!("1 - {}", u16::MAX);
                invalid_error_msg(AP_PORT, AP_PORT_SHORT, &port, valid_values, "");

                exit(1);
            }
        }),
    };

    let player_config = {
        let player_default_config = PlayerConfig::default();

        let bitrate = opt_str(BITRATE)
            .as_deref()
            .map(|bitrate| {
                Bitrate::from_str(bitrate).unwrap_or_else(|_| {
                    invalid_error_msg(BITRATE, BITRATE_SHORT, bitrate, "96, 160, 320", "160");
                    exit(1);
                })
            })
            .unwrap_or(player_default_config.bitrate);

        let gapless = !opt_present(DISABLE_GAPLESS);

        let normalisation = opt_present(ENABLE_VOLUME_NORMALISATION);

        let normalisation_method;
        let normalisation_type;
        let normalisation_pregain_db;
        let normalisation_threshold_dbfs;
        let normalisation_attack_cf;
        let normalisation_release_cf;
        let normalisation_knee_db;

        if !normalisation {
            for a in &[
                NORMALISATION_METHOD,
                NORMALISATION_GAIN_TYPE,
                NORMALISATION_PREGAIN,
                NORMALISATION_THRESHOLD,
                NORMALISATION_ATTACK,
                NORMALISATION_RELEASE,
                NORMALISATION_KNEE,
            ] {
                if opt_present(a) {
                    warn!(
                        "Without the `--{}` / `-{}` flag normalisation options have no effect.",
                        ENABLE_VOLUME_NORMALISATION, ENABLE_VOLUME_NORMALISATION_SHORT,
                    );
                    break;
                }
            }

            normalisation_method = player_default_config.normalisation_method;
            normalisation_type = player_default_config.normalisation_type;
            normalisation_pregain_db = player_default_config.normalisation_pregain_db;
            normalisation_threshold_dbfs = player_default_config.normalisation_threshold_dbfs;
            normalisation_attack_cf = player_default_config.normalisation_attack_cf;
            normalisation_release_cf = player_default_config.normalisation_release_cf;
            normalisation_knee_db = player_default_config.normalisation_knee_db;
        } else {
            normalisation_method = opt_str(NORMALISATION_METHOD)
                .as_deref()
                .map(|method| {
                    warn!(
                        "`--{}` / `-{}` will be deprecated in a future release.",
                        NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT
                    );

                    let method = NormalisationMethod::from_str(method).unwrap_or_else(|_| {
                        invalid_error_msg(
                            NORMALISATION_METHOD,
                            NORMALISATION_METHOD_SHORT,
                            method,
                            "basic, dynamic",
                            &format!("{:?}", player_default_config.normalisation_method),
                        );

                        exit(1);
                    });

                    if matches!(method, NormalisationMethod::Basic) {
                        warn!(
                            "`--{}` / `-{}` {:?} will be deprecated in a future release.",
                            NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, method
                        );
                    }

                    method
                })
                .unwrap_or(player_default_config.normalisation_method);

            normalisation_type = opt_str(NORMALISATION_GAIN_TYPE)
                .as_deref()
                .map(|gain_type| {
                    NormalisationType::from_str(gain_type).unwrap_or_else(|_| {
                        invalid_error_msg(
                            NORMALISATION_GAIN_TYPE,
                            NORMALISATION_GAIN_TYPE_SHORT,
                            gain_type,
                            "track, album, auto",
                            &format!("{:?}", player_default_config.normalisation_type),
                        );

                        exit(1);
                    })
                })
                .unwrap_or(player_default_config.normalisation_type);

            normalisation_pregain_db = opt_str(NORMALISATION_PREGAIN)
                .map(|pregain| match pregain.parse::<f64>() {
                    Ok(value) if (VALID_NORMALISATION_PREGAIN_RANGE).contains(&value) => value,
                    _ => {
                        let valid_values = &format!(
                            "{} - {}",
                            VALID_NORMALISATION_PREGAIN_RANGE.start(),
                            VALID_NORMALISATION_PREGAIN_RANGE.end()
                        );

                        invalid_error_msg(
                            NORMALISATION_PREGAIN,
                            NORMALISATION_PREGAIN_SHORT,
                            &pregain,
                            valid_values,
                            &player_default_config.normalisation_pregain_db.to_string(),
                        );

                        exit(1);
                    }
                })
                .unwrap_or(player_default_config.normalisation_pregain_db);

            normalisation_threshold_dbfs = opt_str(NORMALISATION_THRESHOLD)
                .map(|threshold| match threshold.parse::<f64>() {
                    Ok(value) if (VALID_NORMALISATION_THRESHOLD_RANGE).contains(&value) => value,
                    _ => {
                        let valid_values = &format!(
                            "{} - {}",
                            VALID_NORMALISATION_THRESHOLD_RANGE.start(),
                            VALID_NORMALISATION_THRESHOLD_RANGE.end()
                        );

                        invalid_error_msg(
                            NORMALISATION_THRESHOLD,
                            NORMALISATION_THRESHOLD_SHORT,
                            &threshold,
                            valid_values,
                            &player_default_config
                                .normalisation_threshold_dbfs
                                .to_string(),
                        );

                        exit(1);
                    }
                })
                .unwrap_or(player_default_config.normalisation_threshold_dbfs);

            normalisation_attack_cf = opt_str(NORMALISATION_ATTACK)
                .map(|attack| match attack.parse::<u64>() {
                    Ok(value) if (VALID_NORMALISATION_ATTACK_RANGE).contains(&value) => {
                        duration_to_coefficient(Duration::from_millis(value))
                    }
                    _ => {
                        let valid_values = &format!(
                            "{} - {}",
                            VALID_NORMALISATION_ATTACK_RANGE.start(),
                            VALID_NORMALISATION_ATTACK_RANGE.end()
                        );

                        invalid_error_msg(
                            NORMALISATION_ATTACK,
                            NORMALISATION_ATTACK_SHORT,
                            &attack,
                            valid_values,
                            &coefficient_to_duration(player_default_config.normalisation_attack_cf)
                                .as_millis()
                                .to_string(),
                        );

                        exit(1);
                    }
                })
                .unwrap_or(player_default_config.normalisation_attack_cf);

            normalisation_release_cf = opt_str(NORMALISATION_RELEASE)
                .map(|release| match release.parse::<u64>() {
                    Ok(value) if (VALID_NORMALISATION_RELEASE_RANGE).contains(&value) => {
                        duration_to_coefficient(Duration::from_millis(value))
                    }
                    _ => {
                        let valid_values = &format!(
                            "{} - {}",
                            VALID_NORMALISATION_RELEASE_RANGE.start(),
                            VALID_NORMALISATION_RELEASE_RANGE.end()
                        );

                        invalid_error_msg(
                            NORMALISATION_RELEASE,
                            NORMALISATION_RELEASE_SHORT,
                            &release,
                            valid_values,
                            &coefficient_to_duration(
                                player_default_config.normalisation_release_cf,
                            )
                            .as_millis()
                            .to_string(),
                        );

                        exit(1);
                    }
                })
                .unwrap_or(player_default_config.normalisation_release_cf);

            normalisation_knee_db = opt_str(NORMALISATION_KNEE)
                .map(|knee| match knee.parse::<f64>() {
                    Ok(value) if (VALID_NORMALISATION_KNEE_RANGE).contains(&value) => value,
                    _ => {
                        let valid_values = &format!(
                            "{} - {}",
                            VALID_NORMALISATION_KNEE_RANGE.start(),
                            VALID_NORMALISATION_KNEE_RANGE.end()
                        );

                        invalid_error_msg(
                            NORMALISATION_KNEE,
                            NORMALISATION_KNEE_SHORT,
                            &knee,
                            valid_values,
                            &player_default_config.normalisation_knee_db.to_string(),
                        );

                        exit(1);
                    }
                })
                .unwrap_or(player_default_config.normalisation_knee_db);
        }

        let ditherer_name = opt_str(DITHER);
        let ditherer = match ditherer_name.as_deref() {
            Some(value) => match value {
                "none" => None,
                _ => match format {
                    AudioFormat::F64 | AudioFormat::F32 => {
                        error!("Dithering is not available with format: {:?}.", format);
                        exit(1);
                    }
                    _ => Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| {
                        invalid_error_msg(
                            DITHER,
                            DITHER_SHORT,
                            &opt_str(DITHER).unwrap_or_default(),
                            "none, gpdf, tpdf, tpdf_hp for formats S16, S24, S24_3, S32, none for formats F32, F64",
                            "tpdf for formats S16, S24, S24_3 and none for formats S32, F32, F64",
                        );

                        exit(1);
                    })),
                },
            },
            None => match format {
                AudioFormat::S16 | AudioFormat::S24 | AudioFormat::S24_3 => {
                    player_default_config.ditherer
                }
                _ => None,
            },
        };

        let passthrough = opt_present(PASSTHROUGH);

        PlayerConfig {
            bitrate,
            gapless,
            passthrough,
            normalisation,
            normalisation_type,
            normalisation_method,
            normalisation_pregain_db,
            normalisation_threshold_dbfs,
            normalisation_attack_cf,
            normalisation_release_cf,
            normalisation_knee_db,
            ditherer,
        }
    };

    let player_event_program = opt_str(ONEVENT);
    let emit_sink_events = opt_present(EMIT_SINK_EVENTS);

    Setup {
        format,
        backend,
        device,
        mixer,
        cache,
        player_config,
        session_config,
        connect_config,
        mixer_config,
        credentials,
        enable_discovery,
        zeroconf_port,
        player_event_program,
        emit_sink_events,
    }
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    const RUST_BACKTRACE: &str = "RUST_BACKTRACE";
    const RECONNECT_RATE_LIMIT_WINDOW: Duration = Duration::from_secs(600);
    const RECONNECT_RATE_LIMIT: usize = 5;

    if env::var(RUST_BACKTRACE).is_err() {
        env::set_var(RUST_BACKTRACE, "full")
    }

    let setup = get_setup();

    let mut last_credentials = None;
    let mut spirc: Option<Spirc> = None;
    let mut spirc_task: Option<Pin<_>> = None;
    let mut player_event_channel: Option<UnboundedReceiver<PlayerEvent>> = None;
    let mut auto_connect_times: Vec<Instant> = vec![];
    let mut discovery = None;
    let mut connecting: Pin<Box<dyn future::FusedFuture<Output = _>>> = Box::pin(future::pending());

    if setup.enable_discovery {
        let device_id = setup.session_config.device_id.clone();

        discovery = match librespot::discovery::Discovery::builder(device_id)
            .name(setup.connect_config.name.clone())
            .device_type(setup.connect_config.device_type)
            .port(setup.zeroconf_port)
            .launch()
        {
            Ok(d) => Some(d),
            Err(e) => {
                error!("Discovery Error: {}", e);
                exit(1);
            }
        }
    }

    if let Some(credentials) = setup.credentials {
        last_credentials = Some(credentials.clone());
        connecting = Box::pin(
            Session::connect(
                setup.session_config.clone(),
                credentials,
                setup.cache.clone(),
            )
            .fuse(),
        );
    }

    loop {
        tokio::select! {
            credentials = async {
                match discovery.as_mut() {
                    Some(d) => d.next().await,
                    _ => None
                }
            }, if discovery.is_some() => {
                match credentials {
                    Some(credentials) => {
                        last_credentials = Some(credentials.clone());
                        auto_connect_times.clear();

                        if let Some(spirc) = spirc.take() {
                            spirc.shutdown();
                        }
                        if let Some(spirc_task) = spirc_task.take() {
                            // Continue shutdown in its own task
                            tokio::spawn(spirc_task);
                        }

                        connecting = Box::pin(Session::connect(
                            setup.session_config.clone(),
                            credentials,
                            setup.cache.clone(),
                        ).fuse());
                    },
                    None => {
                        error!("Discovery stopped unexpectedly");
                        exit(1);
                    }
                }
            },
            session = &mut connecting, if !connecting.is_terminated() => match session {
                Ok(session) => {
                    let mixer_config = setup.mixer_config.clone();
                    let mixer = (setup.mixer)(mixer_config);
                    let player_config = setup.player_config.clone();
                    let connect_config = setup.connect_config.clone();

                    let audio_filter = mixer.get_audio_filter();
                    let format = setup.format;
                    let backend = setup.backend;
                    let device = setup.device.clone();
                    let (player, event_channel) =
                        Player::new(player_config, session.clone(), audio_filter, move || {
                            (backend)(device, format)
                        });

                    if setup.emit_sink_events {
                        if let Some(player_event_program) = setup.player_event_program.clone() {
                            player.set_sink_event_callback(Some(Box::new(move |sink_status| {
                                match emit_sink_event(sink_status, &player_event_program) {
                                    Ok(e) if e.success() => (),
                                    Ok(e) => {
                                        if let Some(code) = e.code() {
                                            warn!("Sink event program returned exit code {}", code);
                                        } else {
                                            warn!("Sink event program returned failure");
                                        }
                                    },
                                    Err(e) => {
                                        warn!("Emitting sink event failed: {}", e);
                                    },
                                }
                            })));
                        }
                    };

                    let (spirc_, spirc_task_) = Spirc::new(connect_config, session, player, mixer);

                    spirc = Some(spirc_);
                    spirc_task = Some(Box::pin(spirc_task_));
                    player_event_channel = Some(event_channel);
                },
                Err(e) => {
                    error!("Connection failed: {}", e);
                    exit(1);
                }
            },
            _ = async {
                if let Some(task) = spirc_task.as_mut() {
                    task.await;
                }
            }, if spirc_task.is_some() => {
                spirc_task = None;

                warn!("Spirc shut down unexpectedly");

                let mut reconnect_exceeds_rate_limit = || {
                    auto_connect_times.retain(|&t| t.elapsed() < RECONNECT_RATE_LIMIT_WINDOW);
                    auto_connect_times.len() > RECONNECT_RATE_LIMIT
                };

                match last_credentials.clone() {
                    Some(credentials) if !reconnect_exceeds_rate_limit() => {
                        auto_connect_times.push(Instant::now());

                        connecting = Box::pin(Session::connect(
                            setup.session_config.clone(),
                            credentials,
                            setup.cache.clone(),
                        ).fuse());
                    },
                    _ => {
                        error!("Spirc shut down too often. Not reconnecting automatically.");
                        exit(1);
                    },
                }
            },
            event = async {
                match player_event_channel.as_mut() {
                    Some(p) => p.recv().await,
                    _ => None
                }
            }, if player_event_channel.is_some() => match event {
                Some(event) => {
                    if let Some(program) = &setup.player_event_program {
                        if let Some(child) = run_program_on_events(event, program) {
                            if let Ok(mut child) = child {
                                tokio::spawn(async move {
                                    match child.wait().await {
                                        Ok(e) if e.success() => (),
                                        Ok(e) => {
                                            if let Some(code) = e.code() {
                                                warn!("On event program returned exit code {}", code);
                                            } else {
                                                warn!("On event program returned failure");
                                            }
                                        },
                                        Err(e) => {
                                            warn!("On event program failed: {}", e);
                                        },
                                    }
                                });
                            } else {
                                warn!("On event program failed to start");
                            }
                        }
                    }
                },
                None => {
                    player_event_channel = None;
                }
            },
            _ = tokio::signal::ctrl_c() => {
                break;
            },
            else => break,
        }
    }

    info!("Gracefully shutting down");

    // Shutdown spirc if necessary
    if let Some(spirc) = spirc {
        spirc.shutdown();

        if let Some(mut spirc_task) = spirc_task {
            tokio::select! {
                _ = tokio::signal::ctrl_c() => (),
                _ = spirc_task.as_mut() => (),
                else => (),
            }
        }
    }
}
