//! Fast string unescaping.
//!
//! # Supported signals
//!
//! - '\t'
//! - '\r'
//! - '\n'
//! - '\''
//! - '\"'
//! - '\\'
//! - '\u{NNNN}'
//! - '\xNN'
//! - '\0'
//! - '\b'
//! - '\f'
//! - '\v'

#![no_std]

#[cfg(feature = "alloc")]
extern crate alloc;

#[cfg(feature = "std")]
extern crate std;

use core::iter::FusedIterator;

use memchr::memchr;

mod error;

#[cfg(feature = "alloc")]
pub use _alloc::*;
pub use error::Error;

/// Unescapes the haystack and feeds the unescaped string slices with given function.
///
/// Failed codepoints will be replaced with [`char::REPLACEMENT_CHARACTER`].
pub fn unescape_with<F>(haystack: &str, mut f: F)
where
    F: FnMut(&str),
{
    Unescaper::new(haystack).for_each(|result| {
        let unescaped = result.unwrap_or(Unescaped::Char(char::REPLACEMENT_CHARACTER));
        match unescaped {
            Unescaped::Slice(slice) => f(slice),
            Unescaped::Char(c) => {
                let mut buf = [0u8; 4];
                f(c.encode_utf8(&mut buf))
            }
        }
    });
}

/// Attempts to unescape the haystack and feeds the unescaped string slices with given function
/// if no failed codepoint persist.
///
/// # Errors
///
/// Errors if there's a codepoint failed to decode from.
pub fn try_unescape_with<F>(haystack: &str, mut f: F) -> Result<(), Error>
where
    F: FnMut(&str),
{
    let mut unescaper = Unescaper::new(haystack);
    while let Some(unescaped) = unescaper.next().transpose()? {
        match unescaped {
            Unescaped::Slice(slice) => f(slice),
            Unescaped::Char(c) => {
                let mut buf = [0u8; 4];
                f(c.encode_utf8(&mut buf))
            }
        }
    }
    Ok(())
}

#[cfg(feature = "alloc")]
mod _alloc {
    use alloc::{
        borrow::{Cow, ToOwned as _},
        string::String,
    };

    use crate::{Error, Unescaped, Unescaper};

    /// Unescapes the haystack into the given buffer.
    ///
    /// This is equivalent to `unescape_with(haystack, |s| buf.push_str(s))`.
    pub fn unescape_into(haystack: &str, buf: &mut String) {
        buf.reserve(haystack.len());
        crate::unescape_with(haystack, |s| buf.push_str(s))
    }

    /// Attempts to unescape the haystack into the given buffer.
    ///
    /// This is equivalent to `try_unescape_with(haystack, |s| buf.push_str(s))`.
    #[allow(clippy::missing_errors_doc)]
    pub fn try_unescape_into(haystack: &str, buf: &mut String) -> Result<(), Error> {
        buf.reserve(haystack.len());
        crate::try_unescape_with(haystack, |s| buf.push_str(s))
    }

    /// Unescapes the haystack.
    ///
    /// Failed codepoints will be replaced with [`char::REPLACEMENT_CHARACTER`].
    pub fn unescape(haystack: &str) -> Cow<'_, str> {
        let mut buf = Cow::Borrowed("");
        Unescaper::new(haystack).for_each(|result| {
            write_to_buf(
                haystack,
                &mut buf,
                result.unwrap_or(Unescaped::Char(char::REPLACEMENT_CHARACTER)),
            );
        });
        buf
    }

    /// Attempts to unescape the haystack.
    ///
    /// # Errors
    ///
    /// Errors if there's a codepoint failed to decode from.
    pub fn try_unescape(haystack: &str) -> Result<Cow<'_, str>, Error> {
        let mut buf = Cow::Borrowed("");
        let mut unescaper = Unescaper::new(haystack);
        while let Some(unescaped) = unescaper.next().transpose()? {
            write_to_buf(haystack, &mut buf, unescaped);
        }
        Ok(buf)
    }

    fn write_to_buf<'a>(haystack: &str, buf: &mut Cow<'a, str>, unescaped: Unescaped<'a>) {
        match (unescaped, &mut *buf) {
            (Unescaped::Slice(slice), Cow::Borrowed(b)) => {
                if b.is_empty() {
                    *b = slice;
                } else if let (naive_start, naive_end) =
                    (b.as_ptr(), unsafe { b.as_ptr().add(b.len()) })
                    && (naive_start..=naive_end).contains(&slice.as_ptr())
                {
                    *b = unsafe {
                        str::from_utf8_unchecked(core::slice::from_raw_parts(
                            naive_start,
                            core::cmp::max(naive_end, slice.as_ptr().add(slice.len()))
                                .offset_from_unsigned(naive_start),
                        ))
                    };
                } else {
                    let mut b = b.to_owned();
                    b.reserve(haystack.len().wrapping_sub(b.len()));
                    b.push_str(slice);
                    *buf = Cow::Owned(b);
                }
            }
            (Unescaped::Slice(slice), Cow::Owned(buf)) => buf.push_str(slice),
            (Unescaped::Char(c), Cow::Borrowed(b)) => {
                let mut b = b.to_owned();
                b.reserve(haystack.len().wrapping_sub(b.len()));
                b.push(c);
                *buf = Cow::Owned(b);
            }
            (Unescaped::Char(c), Cow::Owned(buf)) => buf.push(c),
        }
    }
}

enum Unescaped<'a> {
    Slice(&'a str),
    Char(char),
}

/// Splits the given haystack and needle as (..mid), (mid + 1..)
#[inline]
fn split_once(needle: u8, haystack: &[u8]) -> Option<(&[u8], &[u8])> {
    memchr(needle, haystack).map(|mid| {
        let len = haystack.len();
        let ptr = haystack.as_ptr();
        // SAFETY: memchr guarantees `mid` is on a char boundary.
        unsafe {
            (
                core::slice::from_raw_parts(ptr, mid),
                core::slice::from_raw_parts(ptr.add(mid + 1), len - mid - 1),
            )
        }
    })
}

fn split_once_no_inline(needle: u8, haystack: &[u8]) -> Option<(&[u8], &[u8])> {
    split_once(needle, haystack)
}

#[inline]
fn parse_hex_u32(hex: &[u8]) -> Option<u32> {
    let mut num = 0;
    for &b in hex {
        let digit = match b {
            b'0'..=b'9' => b - b'0',
            b'a'..=b'f' => b - b'a' + 10,
            b'A'..=b'F' => b - b'A' + 10,
            _ => return None,
        };
        num = num * 16 + digit as u32;
    }
    Some(num)
}

fn unescape_prefix(haystack: &[u8], sig: u8) -> Result<(char, &[u8]), Error> {
    let mut remaining = haystack;
    let c = match sig {
        // ascii escape sigs in libcore
        b't' => '\t',
        b'r' => '\r',
        b'n' => '\n',
        b'\'' => '\'',
        b'\"' => '\"',
        b'\\' => '\\',

        // unicode: \u{NNNN}
        b'u' => {
            let [b'{', hs @ ..] = haystack else {
                return Err(Error::UnbracedUnicode);
            };
            let Some((hex, hs)) = split_once(b'}', hs) else {
                return Err(Error::UnbracedUnicode);
            };
            remaining = hs;
            let Some(unicode) = parse_hex_u32(hex).and_then(char::from_u32) else {
                return Err(Error::InvalidUnicodeHex);
            };
            unicode
        }
        // single-byte unicode: \xNN
        b'x' => {
            let [b1, b2, hs @ ..] = haystack else {
                return Err(Error::InvalidUnicodeHex);
            };
            remaining = hs;
            let Some(unicode) = parse_hex_u32(&[*b1, *b2]).and_then(char::from_u32) else {
                return Err(Error::InvalidUnicodeHex);
            };
            unicode
        }

        // misc for compatibility
        b'0' => '\0',       // null
        b'b' => '\u{0008}', // backspace
        b'f' => '\u{000c}', // form feed
        b'v' => '\x0B',     // vertical tab

        invalid => return Err(Error::UnknownEscapeSignal(invalid)),
    };
    Ok((c, remaining))
}

struct Unescaper<'a> {
    haystack: &'a str,
    cbuf: Option<char>,
}

impl<'a> Unescaper<'a> {
    #[inline]
    fn new(haystack: &'a str) -> Self {
        Self {
            haystack,
            cbuf: None,
        }
    }
}

impl FusedIterator for Unescaper<'_> {}

impl<'a> Iterator for Unescaper<'a> {
    type Item = Result<Unescaped<'a>, Error>;

    fn next(&mut self) -> Option<Self::Item> {
        if let Some(c) = self.cbuf.take() {
            return Some(Ok(Unescaped::Char(c)));
        }
        if self.haystack.is_empty() {
            return None;
        }
        let Some((left, remaining)) = split_once_no_inline(b'\\', self.haystack.as_bytes()) else {
            let haystack = self.haystack;
            self.haystack = "";
            return Some(Ok(Unescaped::Slice(haystack)));
        };
        let [sig, remaining @ ..] = remaining else {
            return Some(Err(Error::TrailingBackslash));
        };
        self.haystack = unsafe { str::from_utf8_unchecked(remaining) };

        let (c, remaining) = match unescape_prefix(remaining, *sig) {
            Ok((a, b)) => (a, b),
            Err(err) => return Some(Err(err)),
        };
        self.cbuf = Some(c);

        self.haystack = unsafe { str::from_utf8_unchecked(remaining) };

        Some(Ok(Unescaped::Slice(unsafe {
            str::from_utf8_unchecked(left)
        })))
    }

    fn fold<B, F>(mut self, init: B, mut f: F) -> B
    where
        Self: Sized,
        F: FnMut(B, Self::Item) -> B,
    {
        let mut item = if let Some(c) = self.cbuf.take() {
            f(init, Ok(Unescaped::Char(c)))
        } else {
            init
        };

        let mut haystack = self.haystack.as_bytes();
        while !haystack.is_empty() {
            let (left, remaining) = match split_once(b'\\', haystack) {
                Some((a, b)) => (a, b),
                None => (haystack, b"".as_slice()),
            };
            item = f(
                item,
                Ok(Unescaped::Slice(unsafe { str::from_utf8_unchecked(left) })),
            );
            let [sig, remaining @ ..] = remaining else {
                haystack = remaining;
                item = f(item, Err(Error::TrailingBackslash));
                continue;
            };
            let (c, remaining) = match unescape_prefix(remaining, *sig) {
                Ok((c, remaining)) => (c, remaining),
                Err(err) => {
                    haystack = remaining;
                    item = f(item, Err(err));
                    continue;
                }
            };
            haystack = remaining;
            item = f(item, Ok(Unescaped::Char(c)));
        }

        item
    }
}

#[cfg(all(test, feature = "std"))]
mod tests;
