use super::*;

#[derive(Debug, Snafu)]
#[snafu(context(suffix(false)), visibility(pub(crate)))]
pub(crate) enum Error {
  #[snafu(display(
    "app exited with errors{}",
    Self::additional_error_message(additional.len(), " "),
  ))]
  AppExit {
    backtrace: Option<Backtrace>,
    source: Box<Error>,
    additional: Vec<Error>,
  },
  #[snafu(display("failed to run app"))]
  AppRun {
    backtrace: Option<Backtrace>,
    source: winit::error::EventLoopError,
  },
  #[snafu(display("failed to build audio input stream"))]
  AudioBuildInputStream {
    backtrace: Option<Backtrace>,
    source: cpal::BuildStreamError,
  },
  #[snafu(display("failed to get default audio output stream"))]
  AudioBuildOutputStream {
    backtrace: Option<Backtrace>,
    source: rodio::StreamError,
  },
  #[snafu(display("failed to get default audio input device"))]
  AudioDefaultInputDevice { backtrace: Option<Backtrace> },
  #[snafu(display("failed to get default audio output device"))]
  AudioDefaultOutputDevice { backtrace: Option<Backtrace> },
  #[snafu(display("failed to get audio device name"))]
  AudioDeviceName {
    backtrace: Option<Backtrace>,
    source: cpal::DeviceNameError,
  },
  #[snafu(display("failed to enumerate audio devices"))]
  AudioDevices {
    backtrace: Option<Backtrace>,
    source: cpal::DevicesError,
  },
  #[snafu(display("failed to play audio input stream"))]
  AudioPlayStream {
    backtrace: Option<Backtrace>,
    source: cpal::PlayStreamError,
  },
  #[snafu(display("failed to get supported stream config"))]
  AudioSupportedStreamConfig { backtrace: Option<Backtrace> },
  #[snafu(display("failed to get supported stream configs"))]
  AudioSupportedStreamConfigs {
    backtrace: Option<Backtrace>,
    source: cpal::SupportedStreamConfigsError,
  },
  #[snafu(display("failed to flush captured video"))]
  CaptureFlush {
    backtrace: Option<Backtrace>,
    source: io::Error,
  },
  #[snafu(display("failed to invoke capture command"))]
  CaptureInvoke {
    backtrace: Option<Backtrace>,
    source: io::Error,
  },
  #[snafu(display("capture command failed"))]
  CaptureStatus {
    backtrace: Option<Backtrace>,
    status: ExitStatus,
  },
  #[snafu(display("failed to wait for capture command"))]
  CaptureWait {
    backtrace: Option<Backtrace>,
    source: io::Error,
  },
  #[snafu(display("failed to write captured video"))]
  CaptureWrite {
    backtrace: Option<Backtrace>,
    source: io::Error,
  },
  #[snafu(display("failed to deserialize config file at `{path}`"))]
  ConfigDeserialize {
    backtrace: Option<Backtrace>,
    path: Utf8PathBuf,
    source: serde_yaml::Error,
  },
  #[snafu(display("failed to create overlay renderer"))]
  CreateOverlayRenderer {
    backtrace: Option<Backtrace>,
    source: vello::Error,
  },
  #[snafu(display("failed to create surface"))]
  CreateSurface {
    backtrace: Option<Backtrace>,
    source: wgpu::CreateSurfaceError,
  },
  #[snafu(display("failed to create window"))]
  CreateWindow {
    backtrace: Option<Backtrace>,
    source: winit::error::OsError,
  },
  #[snafu(display("failed to get current texture"))]
  CurrentTexture {
    backtrace: Option<Backtrace>,
    source: wgpu::SurfaceError,
  },
  #[snafu(display("failed to open audio file"))]
  DecoderOpen {
    backtrace: Option<Backtrace>,
    path: Utf8PathBuf,
    source: rodio::decoder::DecoderError,
  },
  #[snafu(display("failed to get default config"))]
  DefaultConfig { backtrace: Option<Backtrace> },
  #[snafu(display("failed to build event loop"))]
  EventLoopBuild {
    backtrace: Option<Backtrace>,
    source: winit::error::EventLoopError,
  },
  #[snafu(display("I/O error at `{path}`"))]
  FilesystemIo {
    path: Utf8PathBuf,
    backtrace: Option<Backtrace>,
    source: io::Error,
  },
  #[snafu(display("could not get home directory"))]
  Home { backtrace: Option<Backtrace> },
  #[snafu(display("internal error: {message}"))]
  Internal {
    backtrace: Option<Backtrace>,
    message: String,
  },
  #[snafu(display("failed to initialize MIDI input"))]
  MidiInputInit {
    backtrace: Option<Backtrace>,
    source: midir::InitError,
  },
  #[snafu(display("failed to connect to MIDI port"))]
  MidiInputPortConnect {
    backtrace: Option<Backtrace>,
    source: midir::ConnectError<midir::MidiInput>,
  },
  #[snafu(display("failed to initialize MIDI output"))]
  MidiOutputInit {
    backtrace: Option<Backtrace>,
    source: midir::InitError,
  },
  #[snafu(display("failed to get MIDI port info"))]
  MidiPortInfo {
    backtrace: Option<Backtrace>,
    source: midir::PortInfoError,
  },
  #[snafu(display("no music directory configured"))]
  Music { backtrace: Option<Backtrace> },
  #[snafu(display("path not valid unicode: `{}`", path.display()))]
  PathUnicode {
    backtrace: Option<Backtrace>,
    path: std::path::PathBuf,
    source: camino::FromPathError,
  },
  #[snafu(display("failed to decode PNG at `{path}`"))]
  PngDecode {
    backtrace: Option<Backtrace>,
    path: Utf8PathBuf,
    source: png::DecodingError,
  },
  #[snafu(display("PNG has unsupported format {color_type:?} {bit_depth:?}: `{path}`"))]
  PngDecodeFormat {
    backtrace: Option<Backtrace>,
    bit_depth: png::BitDepth,
    color_type: png::ColorType,
    path: Utf8PathBuf,
  },
  #[snafu(display("PNG too large to fit in memory: `{path}`"))]
  PngDecodeSize {
    backtrace: Option<Backtrace>,
    path: Utf8PathBuf,
  },
  #[snafu(display("failed to encode PNG: `{path}`"))]
  PngEncode {
    backtrace: Option<Backtrace>,
    path: Utf8PathBuf,
    source: png::EncodingError,
  },
  #[snafu(display("failed to invoke recording command"))]
  RecordingInvoke {
    backtrace: Option<Backtrace>,
    source: io::Error,
  },
  #[snafu(display("recording command failed"))]
  RecordingStatus {
    backtrace: Option<Backtrace>,
    status: ExitStatus,
  },
  #[snafu(display(
    "rendering failed: {error}{}",
    Self::additional_error_message(additional.len(), ""),
  ))]
  Render {
    backtrace: Option<Backtrace>,
    error: wgpu::Error,
    additional: Vec<wgpu::Error>,
  },
  #[snafu(display("failed to render overlay"))]
  RenderOverlay {
    backtrace: Option<Backtrace>,
    source: vello::Error,
  },
  #[snafu(display("failed to poll renderer"))]
  RenderPoll {
    backtrace: Option<Backtrace>,
    source: wgpu::PollError,
  },
  #[snafu(display("failed to get adapter"))]
  RequestAdapter {
    backtrace: Option<Backtrace>,
    source: wgpu::RequestAdapterError,
  },
  #[snafu(display("failed to get device"))]
  RequestDevice {
    backtrace: Option<Backtrace>,
    source: wgpu::RequestDeviceError,
  },
  #[snafu(display("sample rate {sample_rate} is not divisible by fps {fps}"))]
  SamplesPerFrame {
    backtrace: Option<Backtrace>,
    fps: Fps,
    sample_rate: u32,
  },
  #[snafu(
    display(
      "more than one match for song: {}",
      matches.iter().map(ToString::to_string).collect::<Vec<String>>().join(", ")
    )
  )]
  SongAmbiguous {
    backtrace: Option<Backtrace>,
    matches: Vec<Utf8PathBuf>,
  },
  #[snafu(display("could not match song `{song}`"))]
  SongMatch {
    backtrace: Option<Backtrace>,
    song: Regex,
  },
  #[snafu(display("invalid song regex"))]
  SongRegex {
    backtrace: Option<Backtrace>,
    source: regex::Error,
  },
  #[snafu(display("I/O error finding song"))]
  SongWalk {
    backtrace: Option<Backtrace>,
    source: walkdir::Error,
  },
  #[snafu(display("I/O error creating tempdir"))]
  TempdirIo {
    backtrace: Option<Backtrace>,
    source: io::Error,
  },
  #[snafu(display("default texture format {texture_format:?} not supported"))]
  UnsupportedTextureFormat {
    backtrace: Option<Backtrace>,
    texture_format: TextureFormat,
  },
  #[snafu(display("failed to create wav writer"))]
  WavCreate {
    backtrace: Option<Backtrace>,
    path: Utf8PathBuf,
    source: hound::Error,
  },
  #[snafu(display("failed to finalize wav writer"))]
  WavFinalize {
    backtrace: Option<Backtrace>,
    path: Utf8PathBuf,
    source: hound::Error,
  },
  #[snafu(display("failed to write wav samples"))]
  WavWrite {
    backtrace: Option<Backtrace>,
    path: Utf8PathBuf,
    source: hound::Error,
  },
}

impl Error {
  fn additional_error_message(additional: usize, prefix: &str) -> String {
    if additional == 0 {
      return String::new();
    }

    let plural = if additional == 1 { "" } else { "s" };

    format!("{prefix}{additional} additional error{plural} suppressed")
  }

  pub(crate) fn internal(message: impl Into<String>) -> Self {
    Internal { message }.build()
  }
}
