use crate::audio::io::AudioIO;
use crate::hw::convert_policy;
use nix::libc;
use std::{
    fs::File,
    os::{fd::AsRawFd, unix::fs::OpenOptionsExt},
    sync::{Arc, Mutex},
};

pub use super::midi_hub::MidiHub;

mod audio_core;
mod channel;
mod consts;
mod convert;
mod driver;
mod io_util;
mod ioctl;
mod sync;

pub use self::channel::OSSChannel;
pub use self::consts::*;
pub use self::driver::HwDriver;
pub use self::ioctl::{AudioInfo, BufferInfo, add_to_sync_group, start_sync_group};
pub use crate::hw::options::HwOptions;

use self::audio_core::DoubleBufferedChannel;
use self::convert::*;
use self::io_util::*;
use self::ioctl::*;
use self::sync::{DuplexSync, FrameClock, get_or_create_duplex_sync};

#[cfg(target_endian = "little")]
const AFMT_S16_FOREIGN: u32 = AFMT_S16_BE;
#[cfg(target_endian = "big")]
const AFMT_S16_FOREIGN: u32 = AFMT_S16_LE;
#[cfg(target_endian = "little")]
const AFMT_S24_FOREIGN: u32 = AFMT_S24_BE;
#[cfg(target_endian = "big")]
const AFMT_S24_FOREIGN: u32 = AFMT_S24_LE;
#[cfg(target_endian = "little")]
const AFMT_S32_FOREIGN: u32 = AFMT_S32_BE;
#[cfg(target_endian = "big")]
const AFMT_S32_FOREIGN: u32 = AFMT_S32_LE;

#[derive(Debug)]
pub struct Audio {
    dsp: File,
    pub channels: Vec<Arc<AudioIO>>,
    pub input: bool,
    pub output_gain_linear: f32,
    pub output_balance: f32,
    pub rate: i32,
    pub format: u32,
    pub chsamples: usize,
    buffer: Vec<i32>,
    f32_buffer: Vec<f32>,
    pub buffer_info: BufferInfo,
    frame_size_bytes: usize,
    buffer_frames_cached: i64,
    caps: i32,
    mapped: bool,
    map: *mut libc::c_void,
    map_progress_bytes: usize,
    last_published_balance: i64,
    frame_clock: FrameClock,
    frame_stamp: i64,
    duplex_sync: Arc<Mutex<DuplexSync>>,
    channel: DoubleBufferedChannel,
    last_underrun_count: i32,
    last_overrun_count: i32,
    xrun_count: u64,
    playing: Arc<std::sync::atomic::AtomicBool>,
    was_playing_last_cycle: bool,
    stop_fade_remaining_frames: usize,
    stop_fade_total_frames: usize,
}

impl Audio {
    fn sample_format_candidates(bits: i32) -> Vec<u32> {
        fn add_pair(candidates: &mut Vec<u32>, native: u32, foreign: u32) {
            candidates.push(native);
            candidates.push(foreign);
        }

        let mut candidates = Vec::with_capacity(7);
        match bits {
            32 => {
                add_pair(&mut candidates, AFMT_S32_NE, AFMT_S32_FOREIGN);
                add_pair(&mut candidates, AFMT_S24_NE, AFMT_S24_FOREIGN);
                add_pair(&mut candidates, AFMT_S16_NE, AFMT_S16_FOREIGN);
                candidates.push(AFMT_S8);
            }
            24 => {
                add_pair(&mut candidates, AFMT_S24_NE, AFMT_S24_FOREIGN);
                add_pair(&mut candidates, AFMT_S16_NE, AFMT_S16_FOREIGN);
                candidates.push(AFMT_S8);
            }
            16 => {
                add_pair(&mut candidates, AFMT_S16_NE, AFMT_S16_FOREIGN);
                candidates.push(AFMT_S8);
            }
            8 => candidates.push(AFMT_S8),
            _ => {
                add_pair(&mut candidates, AFMT_S16_NE, AFMT_S16_FOREIGN);
                candidates.push(AFMT_S8);
            }
        }
        candidates
    }

    fn negotiate_sample_format(fd: i32, bits: i32) -> Result<u32, std::io::Error> {
        let candidates = Self::sample_format_candidates(bits);
        let mut last_errno = None;
        let mut last_unsupported = None;
        for candidate in candidates {
            let mut negotiated = candidate;
            let setfmt = unsafe { oss_set_format(fd, &mut negotiated) };
            match setfmt {
                Ok(_) => {
                    if supported_sample_format(negotiated) {
                        return Ok(negotiated);
                    }
                    last_unsupported = Some(negotiated);
                }
                Err(_) => {
                    last_errno = Some(std::io::Error::last_os_error());
                }
            }
        }
        if let Some(format) = last_unsupported {
            return Err(std::io::Error::other(format!(
                "Unsupported OSS sample format after setfmt fallback chain: {format:#x}"
            )));
        }
        Err(last_errno
            .unwrap_or_else(|| std::io::Error::other("OSS setfmt failed for all fallback formats")))
    }

    pub fn fd(&self) -> i32 {
        self.dsp.as_raw_fd()
    }

    pub fn start_trigger(&self) -> std::io::Result<()> {
        if (self.caps & PCM_CAP_TRIGGER) == 0 {
            return Ok(());
        }
        let trig: i32 = if self.input {
            PCM_ENABLE_INPUT
        } else {
            PCM_ENABLE_OUTPUT
        };
        unsafe { oss_set_trigger(self.dsp.as_raw_fd(), &trig) }
            .map(|_| ())
            .map_err(|_| std::io::Error::last_os_error())
    }

    pub fn stop_trigger(&self) -> std::io::Result<()> {
        if (self.caps & PCM_CAP_TRIGGER) == 0 {
            return Ok(());
        }
        let trig: i32 = 0;
        unsafe { oss_set_trigger(self.dsp.as_raw_fd(), &trig) }
            .map(|_| ())
            .map_err(|_| std::io::Error::last_os_error())
    }

    pub fn new(
        path: &str,
        sync_key: &str,
        rate: i32,
        bits: i32,
        input: bool,
        options: HwOptions,
        playing: Arc<std::sync::atomic::AtomicBool>,
    ) -> Result<Audio, std::io::Error> {
        let mut binding = File::options();

        let mut flags = libc::O_NONBLOCK;
        if input {
            flags |= libc::O_RDONLY;
            if options.exclusive {
                flags |= libc::O_EXCL;
            }
            binding.read(true).write(false).custom_flags(flags);
        } else {
            flags |= libc::O_WRONLY;
            if options.exclusive {
                flags |= libc::O_EXCL;
            }
            binding.read(false).write(true).custom_flags(flags);
        }

        let dsp = binding.open(path)?;

        let mut audio_info = AudioInfo::new();
        unsafe {
            oss_get_info(dsp.as_raw_fd(), &mut audio_info)
                .map_err(|_| std::io::Error::last_os_error())?;
        }
        let mut channels = if audio_info.max_channels > 0 {
            audio_info.max_channels
        } else {
            2_i32
        };
        let mut effective_rate = rate;
        let cooked = 0_i32;
        let format = Self::negotiate_sample_format(dsp.as_raw_fd(), bits)?;
        unsafe {
            if options.exclusive {
                oss_set_cooked(dsp.as_raw_fd(), &cooked)
                    .map_err(|_| std::io::Error::last_os_error())?;
            }
            oss_set_channels(dsp.as_raw_fd(), &mut channels)
                .map_err(|_| std::io::Error::last_os_error())?;
            oss_set_speed(dsp.as_raw_fd(), &mut effective_rate)
                .map_err(|_| std::io::Error::last_os_error())?;
        }
        if effective_rate != rate {
            return Err(std::io::Error::other(format!(
                "OSS device forced sample rate {effective_rate} (requested {rate})"
            )));
        }

        let bytes_per_sample = bytes_per_sample(format)
            .ok_or_else(|| std::io::Error::other(format!("Unsupported format: {format:#x}")))?;
        let frame_size = (channels as usize) * bytes_per_sample;
        if !options.ignore_hwbuf {
            let requested_frag_bytes = options.period_frames.saturating_mul(frame_size).max(1);
            let frag_size_pow2 = requested_frag_bytes.next_power_of_two();
            let frag_shift = frag_size_pow2.trailing_zeros() as i32;
            let mut frg = ((options.nperiods.max(1) as i32) << 16) | (frag_shift & 0xFFFF);
            unsafe {
                oss_set_fragment(dsp.as_raw_fd(), &mut frg)
                    .map_err(|_| std::io::Error::last_os_error())?;
            }
        }

        let mut buffer_info = BufferInfo::new();
        unsafe {
            if input {
                oss_input_buffer_info(dsp.as_raw_fd(), &mut buffer_info)
                    .map_err(|_| std::io::Error::last_os_error())?;
            } else {
                oss_output_buffer_info(dsp.as_raw_fd(), &mut buffer_info)
                    .map_err(|_| std::io::Error::last_os_error())?;
            }
        }

        if buffer_info.fragments < 1 {
            buffer_info.fragments = buffer_info.fragstotal;
        }
        if buffer_info.bytes < 1 {
            buffer_info.bytes = buffer_info.fragstotal * buffer_info.fragsize;
        }
        if buffer_info.bytes < 1 {
            return Err(std::io::Error::other("OSS buffer size is invalid"));
        }

        let mut caps = 0_i32;
        unsafe {
            oss_get_caps(dsp.as_raw_fd(), &mut caps)
                .map_err(|_| std::io::Error::last_os_error())?;
        }
        let mut sys = OssSysInfo::default();
        unsafe {
            oss_get_sysinfo(dsp.as_raw_fd(), &mut sys)
                .map_err(|_| std::io::Error::last_os_error())?;
        }
        if (caps & PCM_CAP_MMAP) != 0 {
            let ver = cstr_fixed_prefix(&sys.version);
            if ver.len() >= 7 && ver.as_bytes()[..7].cmp(b"1302000") == std::cmp::Ordering::Less {
                caps &= !PCM_CAP_MMAP;
            }
        }

        let requested_period = options.period_frames.max(1);
        let hw_chsamples = if frame_size > 0 && buffer_info.fragsize > 0 {
            (buffer_info.fragsize as usize) / frame_size
        } else {
            0
        };
        let chsamples = if options.ignore_hwbuf
            || hw_chsamples == 0
            || (requested_period >= hw_chsamples && requested_period.is_multiple_of(hw_chsamples))
        {
            requested_period
        } else {
            hw_chsamples.max(1)
        };

        let buffer_bytes = chsamples * frame_size;
        let channel = if input {
            DoubleBufferedChannel::new_read(buffer_bytes, chsamples as i64)
        } else {
            DoubleBufferedChannel::new_write(buffer_bytes, chsamples as i64)
        };

        let mut map = std::ptr::null_mut();
        let mut mapped = false;
        if (caps & PCM_CAP_MMAP) != 0 {
            let prot = if input {
                libc::PROT_READ
            } else {
                libc::PROT_WRITE
            };
            let addr = unsafe {
                libc::mmap(
                    std::ptr::null_mut(),
                    buffer_info.bytes as usize,
                    prot,
                    libc::MAP_SHARED,
                    dsp.as_raw_fd(),
                    0,
                )
            };
            if addr != libc::MAP_FAILED {
                map = addr;
                mapped = true;
            }
        }

        let mut io_channels = Vec::with_capacity(channels as usize);
        for _ in 0..channels {
            io_channels.push(Arc::new(AudioIO::new(chsamples)));
        }

        let duplex_sync = get_or_create_duplex_sync(sync_key, effective_rate, chsamples);
        let mut frame_clock = FrameClock::default();
        frame_clock.set_sample_rate(effective_rate as u32);
        {
            let mut sync = duplex_sync.lock().expect("duplex sync poisoned");
            if let Some(zero) = sync.clock_zero {
                frame_clock.zero = zero;
            } else {
                let _ = frame_clock.init_clock(effective_rate as u32);
                sync.clock_zero = Some(frame_clock.zero);
            }
        }

        let buffer_frames_cached = (buffer_info.bytes as usize / frame_size) as i64;

        let mut initial_audio = Audio {
            dsp,
            channels: io_channels,
            input,
            output_gain_linear: 1.0,
            output_balance: 0.0,
            rate: effective_rate,
            format,
            chsamples,
            buffer: vec![0_i32; chsamples * (channels as usize)],
            f32_buffer: Vec::new(),
            buffer_info,
            frame_size_bytes: frame_size,
            buffer_frames_cached,
            caps,
            mapped,
            map,
            map_progress_bytes: 0,
            last_published_balance: i64::MIN,
            frame_clock,
            frame_stamp: 0,
            duplex_sync,
            channel,
            last_underrun_count: 0,
            last_overrun_count: 0,
            xrun_count: 0,
            playing,
            was_playing_last_cycle: false,
            stop_fade_remaining_frames: 0,
            stop_fade_total_frames: 0,
        };

        initial_audio.last_underrun_count = initial_audio.get_play_underruns();
        initial_audio.last_overrun_count = initial_audio.get_rec_overruns();

        Ok(initial_audio)
    }

    fn frame_size(&self) -> usize {
        self.frame_size_bytes
    }

    fn buffer_frames(&self) -> i64 {
        self.buffer_frames_cached
    }

    fn stepping(&self) -> i64 {
        self.frame_clock.stepping()
    }

    fn map_pointer(&self) -> usize {
        if self.buffer_info.bytes <= 0 {
            return 0;
        }
        self.map_progress_bytes % (self.buffer_info.bytes as usize)
    }

    fn shared_cycle_end_add(&self, delta: i64) -> i64 {
        let mut sync = self.duplex_sync.lock().expect("duplex sync poisoned");
        sync.cycle_end += delta;
        sync.cycle_end
    }

    fn shared_cycle_end_get(&self) -> i64 {
        self.duplex_sync
            .lock()
            .expect("duplex sync poisoned")
            .cycle_end
    }

    fn publish_balance(&mut self, balance: i64) {
        if self.last_published_balance == balance {
            return;
        }
        self.last_published_balance = balance;
        let mut sync = self.duplex_sync.lock().expect("duplex sync poisoned");
        if self.input {
            sync.capture_balance = Some(balance);
        } else {
            sync.playback_balance = Some(balance);
        }
    }

    fn playback_correction(&self) -> i64 {
        if self.input {
            return 0;
        }
        let mut sync = self.duplex_sync.lock().expect("duplex sync poisoned");
        match (sync.playback_balance, sync.capture_balance) {
            (Some(play), Some(capture)) => sync.correction.correct(play, capture),
            _ => 0,
        }
    }

    fn playback_prefill_frames(&self) -> i64 {
        self.duplex_sync
            .lock()
            .expect("duplex sync poisoned")
            .playback_prefill_frames
    }

    fn update_map_progress_from_count(&mut self, info: &CountInfo) -> Option<usize> {
        if self.buffer_info.bytes <= 0
            || self.buffer_info.fragsize <= 0
            || info.ptr < 0
            || info.blocks < 0
            || (info.ptr as usize) >= self.buffer_info.bytes as usize
            || !(info.ptr as usize).is_multiple_of(self.frame_size())
        {
            return None;
        }
        let buf_bytes = self.buffer_info.bytes as usize;
        let frag_bytes = self.buffer_info.fragsize as usize;
        let ptr = info.ptr as usize;
        let mut delta = (ptr + buf_bytes - self.map_pointer()) % buf_bytes;
        let max_bytes = ((info.blocks as usize).saturating_add(1))
            .saturating_mul(frag_bytes)
            .saturating_sub(1);
        if max_bytes >= delta {
            let mut cycles = max_bytes - delta;
            cycles -= cycles % buf_bytes;
            delta += cycles;
        }
        self.map_progress_bytes = self.map_progress_bytes.saturating_add(delta);
        Some(delta)
    }

    fn read_io(&self, dst: &mut [u8], len: usize, count: &mut usize) -> std::io::Result<()> {
        read_nonblock(self.dsp.as_raw_fd(), dst, len, count)
    }

    fn write_io(&self, src: &mut [u8], len: usize, count: &mut usize) -> std::io::Result<()> {
        write_nonblock(self.dsp.as_raw_fd(), src, len, count)
    }

    fn read_map(&self, dst: &mut [u8], offset: usize, length: usize) -> usize {
        let total = self.buffer_info.bytes.max(0) as usize;
        map_read(self.map, self.mapped, total, dst, offset, length)
    }

    fn write_map(&self, src: Option<&mut [u8]>, offset: usize, length: usize) -> usize {
        let total = self.buffer_info.bytes.max(0) as usize;
        map_write(self.map, self.mapped, total, src, offset, length)
    }

    fn queued_samples(&self) -> i32 {
        let mut ptr = OssCount::default();
        let req = if self.input {
            unsafe { oss_current_iptr(self.dsp.as_raw_fd(), &mut ptr) }
        } else {
            unsafe { oss_current_optr(self.dsp.as_raw_fd(), &mut ptr) }
        };
        if req.is_ok() { ptr.fifo_samples } else { 0 }
    }

    fn get_play_underruns(&self) -> i32 {
        let mut err = AudioErrInfo::default();
        let rc = unsafe { oss_get_error(self.dsp.as_raw_fd(), &mut err) };
        if rc.is_ok() { err.play_underruns } else { 0 }
    }

    fn get_rec_overruns(&self) -> i32 {
        let mut err = AudioErrInfo::default();
        let rc = unsafe { oss_get_error(self.dsp.as_raw_fd(), &mut err) };
        if rc.is_ok() { err.rec_overruns } else { 0 }
    }

    pub(super) fn detect_xrun_enhanced(&mut self) -> i64 {
        let current_underruns = if self.input {
            0
        } else {
            self.get_play_underruns()
        };
        let current_overruns = if self.input {
            self.get_rec_overruns()
        } else {
            0
        };

        if (self.last_underrun_count >= 0 || self.last_overrun_count >= 0)
            && (current_underruns > self.last_underrun_count
                || current_overruns > self.last_overrun_count)
        {
            self.xrun_count += 1;
            let delta_underruns = current_underruns - self.last_underrun_count;
            let delta_overruns = current_overruns - self.last_overrun_count;
            self.last_underrun_count = current_underruns;
            self.last_overrun_count = current_overruns;

            tracing::warn!(
                "OSS hardware xrun detected (#{}, underruns +{}, overruns +{})",
                self.xrun_count,
                delta_underruns,
                delta_overruns
            );

            return self.chsamples as i64;
        }

        self.last_underrun_count = current_underruns;
        self.last_overrun_count = current_overruns;

        0
    }

    pub fn process(&mut self) {
        let num_channels = self.channels.len();
        let all_connected = self
            .channels
            .iter()
            .all(crate::hw::ports::has_audio_connections);

        if self.input {
            let norm_factor = convert_policy::F32_FROM_I32_MAX;
            let total_samples = self.chsamples * num_channels;
            self.f32_buffer.resize(total_samples, 0.0);
            crate::simd::convert_i32_to_f32(
                &self.buffer[..total_samples],
                &mut self.f32_buffer,
                norm_factor,
            );
            crate::hw::ports::fill_ports_from_interleaved_buffer(
                &self.channels,
                self.chsamples,
                !all_connected,
                &self.f32_buffer,
                num_channels,
            );
        } else {
            let playing = self.playing.load(std::sync::atomic::Ordering::Relaxed);
            if self.was_playing_last_cycle && !playing {
                let fade_frames = self.chsamples.max(128);
                self.stop_fade_remaining_frames = fade_frames;
                self.stop_fade_total_frames = fade_frames;
            }
            self.was_playing_last_cycle = playing;
            let data_i32 = self.buffer.as_mut_slice();
            if !playing && self.stop_fade_remaining_frames == 0 {
                data_i32.fill(0);
            } else {
                let scale_factor = convert_policy::F32_TO_I32_MAX;
                let output_gain = self.output_gain_linear;
                if !all_connected {
                    data_i32.fill(0);
                }
                let fade_remaining = self.stop_fade_remaining_frames;
                let fade_total = self.stop_fade_total_frames.max(1);
                crate::hw::ports::write_interleaved_from_ports(
                    &self.channels,
                    self.chsamples,
                    output_gain,
                    self.output_balance,
                    !all_connected,
                    |ch_idx, frame, sample| {
                        let target_idx = frame * num_channels + ch_idx;
                        let fade_gain = if !playing && fade_remaining > 0 {
                            let progressed = self.chsamples.saturating_sub(fade_remaining) + frame;
                            (1.0 - (progressed as f32 / fade_total as f32)).clamp(0.0, 1.0)
                        } else {
                            1.0
                        };
                        data_i32[target_idx] =
                            (sample.clamp(-1.0, 1.0) * fade_gain * scale_factor) as i32;
                    },
                );
                if !playing && self.stop_fade_remaining_frames > 0 {
                    self.stop_fade_remaining_frames = self
                        .stop_fade_remaining_frames
                        .saturating_sub(self.chsamples);
                }
            }
        }
    }

    pub fn force_silence_now(&mut self) {
        if self.input {
            return;
        }
        self.buffer.fill(0);
        for ch in &self.channels {
            ch.buffer.lock().fill(0.0);
        }
        self.stop_fade_remaining_frames = 0;
        self.stop_fade_total_frames = 0;
        self.channel
            .reset_buffers(self.frame_stamp, self.frame_size().max(1));
    }
}

impl Drop for Audio {
    fn drop(&mut self) {
        if self.mapped && !self.map.is_null() && self.buffer_info.bytes > 0 {
            unsafe {
                let _ = libc::munmap(self.map, self.buffer_info.bytes as usize);
            }
            self.map = std::ptr::null_mut();
        }
    }
}
