pub mod str {
    use std::io::{Result, Write};

    use crate::Output;

    #[derive(Debug)]
    pub struct StringWriter {
        buf: Vec<u8>,
    }

    impl Default for StringWriter {
        fn default() -> Self {
            Self::new()
        }
    }

    impl StringWriter {
        pub fn new() -> StringWriter {
            StringWriter {
                buf: Vec::with_capacity(8 * 1024),
            }
        }

        pub fn into_string(self) -> String {
            String::from_utf8(self.buf).unwrap_or_default()
        }
    }

    impl Write for StringWriter {
        fn write(&mut self, buf: &[u8]) -> Result<usize> {
            self.buf.extend_from_slice(buf);
            Ok(buf.len())
        }

        fn flush(&mut self) -> Result<()> {
            Ok(())
        }
    }

    /// See https://github.com/handlebars-lang/handlebars.js/blob/37411901da42200ced8e1a7fc2f67bf83526b497/lib/handlebars/utils.js#L1
    pub fn escape_html(s: &str) -> String {
        let mut output = String::new();
        for c in s.chars() {
            match c {
                '<' => output.push_str("&lt;"),
                '>' => output.push_str("&gt;"),
                '"' => output.push_str("&quot;"),
                '&' => output.push_str("&amp;"),
                '\'' => output.push_str("&#x27;"),
                '`' => output.push_str("&#x60;"),
                '=' => output.push_str("&#x3D;"),
                _ => output.push(c),
            }
        }
        output
    }

    /// add indent for lines but last
    pub fn with_indent(s: &str, indent: &str) -> String {
        let mut output = String::new();

        let mut it = s.chars().peekable();
        while let Some(c) = it.next() {
            output.push(c);
            // check if c is not the last character, we don't append
            // indent for last line break
            if c == '\n' && it.peek().is_some() {
                output.push_str(indent);
            }
        }

        output
    }

    /// like `with_indent`, but writing straight into the output
    pub fn write_indented(s: &str, indent: &str, w: &mut dyn Output) -> std::io::Result<()> {
        let mut i = 0;
        let len = s.len();
        loop {
            let Some(next_newline) = s[i..].find('\n') else {
                w.write(&s[i..])?;
                return Ok(());
            };
            w.write(&s[i..i + next_newline + 1])?;
            i += next_newline + 1;
            if i == len {
                return Ok(());
            }
            w.write(indent)?;
        }
    }

    #[inline]
    pub(crate) fn whitespace_matcher(c: char) -> bool {
        c == ' ' || c == '\t'
    }

    #[inline]
    pub(crate) fn newline_matcher(c: char) -> bool {
        c == '\n' || c == '\r'
    }

    #[inline]
    pub(crate) fn strip_first_newline(s: &str) -> &str {
        if let Some(s) = s.strip_prefix("\r\n") {
            s
        } else if let Some(s) = s.strip_prefix('\n') {
            s
        } else {
            s
        }
    }

    pub(crate) fn find_trailing_whitespace_chars(s: &str) -> Option<&str> {
        let trimmed = s.trim_end_matches(whitespace_matcher);
        if trimmed.len() == s.len() {
            None
        } else {
            Some(&s[trimmed.len()..])
        }
    }

    pub(crate) fn ends_with_empty_line(text: &str) -> bool {
        let s = text.trim_end_matches(whitespace_matcher);
        // also matches when text is just whitespaces
        s.ends_with(newline_matcher) || s.is_empty()
    }

    pub(crate) fn starts_with_empty_line(text: &str) -> bool {
        text.trim_start_matches(whitespace_matcher)
            .starts_with(newline_matcher)
    }

    #[cfg(test)]
    mod test {
        use crate::support::str::StringWriter;
        use std::io::Write;

        #[test]
        fn test_string_writer() {
            let mut sw = StringWriter::new();

            let _ = sw.write("hello".to_owned().into_bytes().as_ref());
            let _ = sw.write("world".to_owned().into_bytes().as_ref());

            let s = sw.into_string();
            assert_eq!(s, "helloworld".to_string());
        }
    }
}
