use core::ffi::{
    c_char, c_int, c_long, c_longlong, c_short, c_uchar, c_uint, c_ulong, c_ulonglong, c_ushort,
    c_void, VaList,
};

use super::{
    char_set::ScanCharSet,
    reader::{GetChar, ScanReader},
    ConversionError,
};

pub enum ScanRadix {
    Decimal,
    Oct,
    Hex,
    Auto,
}

pub enum ScanSpec {
    // %s, a sequence of non-whitespace characters
    Word,
    // %c, a sequence of characters whose length is specified by field width (default 1)
    Chars(ScanCharSet),
    // %[...], nonempty sequence of characters from the specified set
    AnyChar,
    // %d, %u, %x/%X, %o, %i, deprecated, optionally-signed/unsigned decimals
    Signed(ScanRadix),
    Unsigned(ScanRadix),
    // %n, reports consumed count
    ConsumedCount,
    // %p
    Pointer,
    // %%
    Percent,
}

pub enum ScanSize {
    ShortShort,
    Short,
    Normal,
    Long,
    LongLong,
    Size,
}

pub struct ScanOpt {
    pub width: Option<usize>,
    pub size: ScanSize,
    pub discard: bool,
}

impl ScanSize {
    pub fn shorter(self) -> ScanSize {
        match self {
            Self::Normal => Self::Short,
            Self::Short => Self::ShortShort,
            _ => self,
        }
    }

    pub fn longer(self) -> ScanSize {
        match self {
            Self::Normal => Self::Long,
            Self::Long => Self::LongLong,
            _ => self,
        }
    }

    pub unsafe fn write_int(&self, value: i64, ap: &mut VaList) {
        let raw = ap.arg::<*mut c_void>();

        if raw.is_null() {
            panic!();
        }

        match self {
            Self::ShortShort => *(raw as *mut c_char) = value as _,
            Self::Short => *(raw as *mut c_short) = value as _,
            Self::Normal => *(raw as *mut c_int) = value as _,
            Self::Long => *(raw as *mut c_long) = value as _,
            Self::LongLong => *(raw as *mut c_longlong) = value as _,
            Self::Size => *(raw as *mut isize) = value as _,
        }
    }

    pub unsafe fn write_uint(&self, value: u64, ap: &mut VaList) {
        let raw = ap.arg::<*mut c_void>();

        if raw.is_null() {
            panic!();
        }

        match self {
            Self::ShortShort => *(raw as *mut c_uchar) = value as _,
            Self::Short => *(raw as *mut c_ushort) = value as _,
            Self::Normal => *(raw as *mut c_uint) = value as _,
            Self::Long => *(raw as *mut c_ulong) = value as _,
            Self::LongLong => *(raw as *mut c_ulonglong) = value as _,
            Self::Size => *(raw as *mut usize) = value as _,
        }
    }
}

impl Default for ScanOpt {
    fn default() -> Self {
        Self {
            width: None,
            size: ScanSize::Normal,
            discard: false,
        }
    }
}

impl ScanOpt {
    pub fn scan<G: GetChar>(
        self,
        stream: &mut ScanReader<G>,
        spec: ScanSpec,
        ap: &mut VaList,
    ) -> Result<usize, ConversionError> {
        let n = if self.discard { 0 } else { 1 };

        match (spec, self.size) {
            (ScanSpec::Percent, _) => todo!(),
            (ScanSpec::Word, ScanSize::Normal) => {
                let width = self.width.unwrap_or(usize::MAX);
                let dst = if self.discard {
                    None
                } else {
                    let dst = unsafe { ap.arg::<*mut c_char>() };
                    if dst.is_null() {
                        panic!();
                    }
                    Some(dst)
                };

                scan_word(stream, dst, width)?;

                Ok(n)
            }
            (ScanSpec::Chars(set), ScanSize::Normal) => {
                let width = self.width.unwrap_or(usize::MAX);
                let dst = if self.discard {
                    None
                } else {
                    let dst = unsafe { ap.arg::<*mut c_char>() };
                    if dst.is_null() {
                        panic!();
                    }
                    Some(dst)
                };

                scan_char_set(stream, dst, set, width)?;

                Ok(n)
            }
            (ScanSpec::AnyChar, ScanSize::Normal) => {
                let width = self.width.unwrap_or(1);
                let dst = if self.discard {
                    None
                } else {
                    let dst = unsafe { ap.arg::<*mut c_char>() };
                    if dst.is_null() {
                        panic!();
                    }
                    Some(dst)
                };

                scan_chars(stream, dst, width)?;

                Ok(n)
            }
            (ScanSpec::Word, _) => todo!("%ls"),
            (ScanSpec::Chars(_), _) => todo!("%l[...]"),
            (ScanSpec::AnyChar, _) => todo!("%lc"),
            (ScanSpec::Signed(radix), size) => {
                let value = scan_signed(stream, radix)?;

                if !self.discard {
                    unsafe {
                        size.write_int(value, ap);
                    }
                }

                Ok(n)
            }
            (ScanSpec::Unsigned(radix), size) => {
                let value = scan_unsigned(stream, radix)?;

                if !self.discard {
                    unsafe {
                        size.write_uint(value, ap);
                    }
                }

                Ok(n)
            }
            (ScanSpec::Pointer, _) => todo!(),
            (ScanSpec::ConsumedCount, size) => {
                if !self.discard {
                    unsafe {
                        size.write_int(stream.consumed_count().try_into().unwrap(), ap);
                    }
                }
                Ok(0)
            }
        }
    }
}

fn scan_word<G: GetChar>(
    stream: &mut ScanReader<G>,
    mut dst: Option<*mut c_char>,
    limit: usize,
) -> Result<(), ConversionError> {
    // Read until limit reached or whitespace/EOF
    let mut matched = 0;
    while matched < limit {
        let Some(ch) = stream.peek()? else {
            break;
        };

        if ch.is_ascii_whitespace() {
            break;
        }

        if let Some(dst) = dst.as_mut() {
            unsafe {
                **dst = ch as _;
                *dst = dst.add(1);
            }
        }
        matched += 1;

        stream.next()?;
    }

    if matched == 0 {
        return Err(ConversionError::ConversionFailed);
    }

    if let Some(dst) = dst.as_mut() {
        unsafe {
            **dst = 0;
        }
    }
    Ok(())
}

fn scan_chars<G: GetChar>(
    stream: &mut ScanReader<G>,
    mut dst: Option<*mut c_char>,
    width: usize,
) -> Result<(), ConversionError> {
    let mut matched = 0;

    while matched < width {
        let Some(ch) = stream.next()? else {
            break;
        };

        if let Some(dst) = dst.as_mut() {
            unsafe {
                **dst = ch as _;
                *dst = dst.add(1);
            }
        }

        matched += 1;
        stream.next()?;
    }

    if matched == width {
        Ok(())
    } else {
        Err(ConversionError::ConversionFailed)
    }
}

fn scan_char_set<G: GetChar>(
    stream: &mut ScanReader<G>,
    mut dst: Option<*mut c_char>,
    set: ScanCharSet,
    width: usize,
) -> Result<(), ConversionError> {
    let mut matched = 0;

    while matched < width {
        let Some(ch) = stream.peek()? else {
            break;
        };

        if !set.check(ch) {
            break;
        }

        if let Some(dst) = dst.as_mut() {
            unsafe {
                **dst = ch as _;
                *dst = dst.add(1);
            }
        }

        stream.next()?;
        matched += 1;
    }

    if matched == 0 {
        return Err(ConversionError::ConversionFailed);
    }

    if let Some(dst) = dst.as_mut() {
        unsafe {
            **dst = 0;
        }
    }

    Ok(())
}

fn scan_unsigned<G: GetChar>(
    stream: &mut ScanReader<G>,
    radix: ScanRadix,
) -> Result<u64, ConversionError> {
    let mut value = 0;
    let mut matched = 0;

    match radix {
        ScanRadix::Auto => {
            let Some(maybe_0) = stream.peek()? else {
                return Err(ConversionError::ConversionFailed);
            };

            if maybe_0 == b'0' {
                stream.next()?;

                // Maybe x/X or octal
                let Some(maybe_next) = stream.peek()? else {
                    // At least matched a zero
                    return Ok(0);
                };

                return match maybe_next {
                    b'x' | b'X' => {
                        stream.next()?;
                        scan_unsigned(stream, ScanRadix::Hex)
                    }
                    b'0'..=b'9' => scan_unsigned(stream, ScanRadix::Oct),
                    _ => Err(ConversionError::ConversionFailed),
                };
            } else {
                // Anything else is a decimal
                return scan_unsigned(stream, ScanRadix::Decimal);
            }
        }
        ScanRadix::Oct => {
            while let Some(ch) = stream.peek()? {
                let digit = match ch {
                    b'0'..=b'7' => (ch - b'0') as u64,
                    _ => break,
                };

                stream.next()?;

                value <<= 3;
                value |= digit;

                matched += 1;
            }
        }
        ScanRadix::Decimal => {
            while let Some(ch) = stream.peek()? {
                let digit = match ch {
                    b'0'..=b'9' => (ch - b'0') as u64,
                    _ => break,
                };

                stream.next()?;

                value *= 10;
                value += digit;

                matched += 1;
            }
        }
        ScanRadix::Hex => {
            while let Some(ch) = stream.peek()? {
                let digit = match ch {
                    b'0'..=b'9' => (ch - b'0') as u64,
                    b'a'..=b'f' => (ch - b'a') as u64 + 10,
                    b'A'..=b'F' => (ch - b'A') as u64 + 10,
                    _ => break,
                };

                stream.next()?;

                value <<= 4;
                value |= digit;

                matched += 1;
            }
        }
    }

    if matched != 0 {
        Ok(value)
    } else {
        // No characters matched
        Err(ConversionError::ConversionFailed)
    }
}

fn scan_signed<G: GetChar>(
    stream: &mut ScanReader<G>,
    radix: ScanRadix,
) -> Result<i64, ConversionError> {
    let sign = if let Some(ch) = stream.peek()? {
        if ch == b'-' {
            stream.next()?;
            false
        } else {
            true
        }
    } else {
        // EOF
        return Err(ConversionError::ConversionFailed);
    };

    let value: i64 = scan_unsigned(stream, radix)?
        .try_into()
        .map_err(|_| ConversionError::ConversionFailed)?;

    Ok(if sign { value } else { -value })
}
