Documentation
/*
==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--==--

SJ

Copyright (C) 2019-2025  Anonymous

There are several releases over multiple years,
they are listed as ranges, such as: "2019-2025".

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.

::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--::--
*/

//! # Formatter

#![cfg(feature="std")]

use {
    std::io::Write,
    crate::{Array, IoResult, Json, Map, NumberWriter, bytes},
};

/// # Default tab width
pub (super) const DEFAULT_TAB_WIDTH: u8 = 4;

const WHITE_SPACE: &[u8] = &[b' '];

const MAX_TAB_WIDTH: u8 = 32;
const MAX_TAB_LEVEL: usize = 65_535;

const LINE_BREAK: &[u8] = &[b'\n'];
const QUOTATION_MARK: &[u8] = &[b'"'];

const TAB_32: [u8; 32] = [b' '; 32];

#[test]
fn tests() {
    assert!(TAB_32.iter().all(|b| b == &b' '));
}

/// # Formatter
#[derive(Debug)]
pub (super) struct Formatter {
    tab_width: Option<u8>,
    tab_level: Option<usize>,
    number_writer: NumberWriter,
}

impl Formatter {

    /// # Makes new instance
    pub fn new(tab_width: Option<u8>) -> Self {
        let has_tab_width = tab_width.is_some();
        Self {
            tab_width: tab_width.map(|w| w.min(MAX_TAB_WIDTH)),
            tab_level: if has_tab_width { Some(0) } else { None },
            number_writer: NumberWriter::new(),
        }
    }

    /// # Formats a value
    ///
    /// ## Notes
    ///
    /// - The stream is used as-is. You might want to consider using [`BufWriter`][std::io/BufWriter].
    /// - This function does **not** flush the stream when done.
    ///
    /// [std::io/BufWriter]: https://doc.rust-lang.org/std/io/struct.BufWriter.html
    pub fn format<W>(&mut self, value: &Json, stream: &mut W) -> IoResult<()> where W: Write {
        match value {
            Json::String(s) => format_string(s, stream)?,
            Json::Number(n) => self.number_writer.write(n, stream)?,
            Json::Boolean(true) => stream.write_all(b"true")?,
            Json::Boolean(false) => stream.write_all(b"false")?,
            Json::Null => stream.write_all(b"null")?,
            Json::Object(object) => {
                stream.write_all(&[b'{'])?;

                increment_tab_level(self.tab_level.as_mut());
                self.format_object_content(object, stream)?;
                decrement_tab_level(self.tab_level.as_mut());

                if object.is_empty() == false {
                    write_pad(make_pad(self.tab_width.as_ref(), self.tab_level.as_ref()), stream)?
                }
                stream.write_all(&[b'}'])?;
            },
            Json::Array(array) => {
                stream.write_all(&[b'['])?;

                increment_tab_level(self.tab_level.as_mut());
                self.format_array_content(array, stream)?;
                decrement_tab_level(self.tab_level.as_mut());

                if array.is_empty() == false {
                    write_pad(make_pad(self.tab_width.as_ref(), self.tab_level.as_ref()), stream)?;
                }
                stream.write_all(&[b']'])?;
            },
        };

        if self.tab_width.is_some() && self.tab_level == Some(0) {
            // Add a final empty line for nice format
            stream.write_all(LINE_BREAK)
        } else {
            Ok(())
        }
    }

    /// # Formats object content
    fn format_object_content<W>(&mut self, map: &Map, stream: &mut W) -> IoResult<()> where W: Write {
        let pad = make_pad(self.tab_width.as_ref(), self.tab_level.as_ref());

        for (i, (k, v)) in map.iter().enumerate() {
            match i {
                0 => if pad.is_some() {
                    stream.write_all(LINE_BREAK)?;
                },
                _ => {
                    stream.write_all(&[b','])?;
                    if pad.is_some() {
                        stream.write_all(LINE_BREAK)?;
                    }
                },
            };

            write_pad(pad, stream)?;

            stream.write_all(QUOTATION_MARK)?;
            stream.write_all(k.as_bytes())?;
            stream.write_all(&[b'"', b':'])?;
            if pad.is_some() {
                stream.write_all(WHITE_SPACE)?;
            }

            self.format(v, stream)?;
        }

        if pad.is_some() && map.is_empty() == false {
            stream.write_all(LINE_BREAK)
        } else {
            Ok(())
        }
    }

    /// # Formats array content
    fn format_array_content<W>(&mut self, array: &Array, stream: &mut W) -> IoResult<()> where W: Write {
        let pad = make_pad(self.tab_width.as_ref(), self.tab_level.as_ref());

        for (i, v) in array.iter().enumerate() {
            match i {
                0 => if pad.is_some() {
                    stream.write_all(LINE_BREAK)?;
                },
                _ => {
                    stream.write_all(&[b','])?;
                    if pad.is_some() {
                        stream.write_all(LINE_BREAK)?;
                    }
                },
            };

            write_pad(pad, stream)?;
            self.format(v, stream)?;
        }

        if pad.is_some() && array.is_empty() == false {
            stream.write_all(LINE_BREAK)
        } else {
            Ok(())
        }
    }

}

/// # Writes pad to stream
fn write_pad<W>(pad: Option<usize>, stream: &mut W) -> IoResult<()> where W: Write {
    match pad {
        Some(mut pad) => loop {
            let tabs = match pad {
                0 => return Ok(()),
                1 => return stream.write_all(WHITE_SPACE),
                2..=3 => &TAB_32[..2],
                4..=5 => &TAB_32[..4],
                6..=7 => &TAB_32[..6],
                8..=11 => &TAB_32[..8],
                12..=15 => &TAB_32[..12],
                16..=19 => &TAB_32[..16],
                20..=23 => &TAB_32[..20],
                24..=27 => &TAB_32[..24],
                28..=31 => &TAB_32[..28],
                _ => &TAB_32,
            };
            stream.write_all(tabs)?;
            pad -= tabs.len();
        },
        None => Ok(()),
    }
}

/// # Formats string
fn format_string<W>(s: &str, stream: &mut W) -> IoResult<()> where W: Write {
    stream.write_all(QUOTATION_MARK)?;

    fn write<W>(s: &str, start: &mut Option<usize>, end: usize, stream: &mut W) -> IoResult<()> where W: Write {
        if let Some(start_idx) = start {
            stream.write_all(&s.as_bytes()[*start_idx..end])?;
            *start = None;
        }
        Ok(())
    }

    let mut last_idx = None;
    for (i, b) in s.as_bytes().iter().enumerate() {
        match b {
            b'"' | b'\\' => {
                write(s, &mut last_idx, i, stream)?;
                stream.write_all(&[b'\\', *b])?;
            },
            &bytes::BACKSPACE => {
                write(s, &mut last_idx, i, stream)?;
                stream.write_all(&[b'\\', b'b'])?;
            },
            &bytes::FORM_FEED => {
                write(s, &mut last_idx, i, stream)?;
                stream.write_all(&[b'\\', b'f'])?;
            },
            b'\n' => {
                write(s, &mut last_idx, i, stream)?;
                stream.write_all(&[b'\\', b'n'])?;
            },
            b'\r' => {
                write(s, &mut last_idx, i, stream)?;
                stream.write_all(&[b'\\', b'r'])?;
            },
            b'\t' => {
                write(s, &mut last_idx, i, stream)?;
                stream.write_all(&[b'\\', b't'])?;
            },
            _ => if last_idx.is_none() {
                last_idx = Some(i);
            },
        };
    }
    if let Some(last_idx) = last_idx {
        stream.write_all(&s.as_bytes()[last_idx..])?;
    }

    stream.write_all(QUOTATION_MARK)
}

/// # Makes pad
fn make_pad(tab_width: Option<&u8>, tab_level: Option<&usize>) -> Option<usize> {
    match (tab_width, tab_level) {
        (Some(width), Some(level)) if width > &0 && level > &0 => Some(usize::from(*width).saturating_mul(*level)),
        _ => None,
    }
}

/// # Increments tab level
fn increment_tab_level(tab_level: Option<&mut usize>) {
    tab_level.map(|l| *l = l.saturating_add(1).min(MAX_TAB_LEVEL));
}

/// # Decrements tab level
fn decrement_tab_level(tab_level: Option<&mut usize>) {
    tab_level.map(|l| *l = l.saturating_sub(1));
}

/// # Estimates format size
pub (super) fn estimate_format_size(value: &Json, tab: Option<u8>, level: Option<usize>) -> usize {
    let mut level = match tab.as_ref() {
        Some(tab) if tab > &0 => match level {
            Some(_) => level,
            None => Some(0),
        },
        _ => None,
    };
    let result = match value {
        Json::String(s) => s.len().saturating_add(2),
        Json::Number(n) => match u128::try_from(n) {
            Ok(0) => 1,
            Ok(n) => ((n as f64).log10().ceil() as usize).max(1),
            Err(_) => 15,
        },
        Json::Boolean(_) => 5,
        Json::Null => 4,
        Json::Object(map) => {
            let mut result: usize = 2;

            increment_tab_level(level.as_mut());
            let pad = make_pad(tab.as_ref(), level.as_ref());
            for (k, v) in map.iter() {
                // "":,
                result = result.saturating_add(4).saturating_add(k.len()).saturating_add(estimate_format_size(v, tab, level));
                if let Some(pad) = pad.as_ref() {
                    // White space + line break
                    result = result.saturating_add(2).saturating_add(*pad);
                }
            }
            decrement_tab_level(level.as_mut());

            if map.is_empty() == false {
                if let Some(pad) = make_pad(tab.as_ref(), level.as_ref()) {
                    result = result.saturating_add(1).saturating_add(pad);
                }
            }

            result
        },
        Json::Array(array) => {
            let mut result: usize = 2;

            increment_tab_level(level.as_mut());
            let pad = make_pad(tab.as_ref(), level.as_ref());
            for v in array {
                result = result.saturating_add(1).saturating_add(estimate_format_size(v, tab, level));
                if let Some(pad) = pad.as_ref() {
                    result = result.saturating_add(1).saturating_add(*pad);
                }
            }
            decrement_tab_level(level.as_mut());

            if array.is_empty() == false {
                if let Some(pad) = make_pad(tab.as_ref(), level.as_ref()) {
                    result = result.saturating_add(1).saturating_add(pad);
                }
            }

            result
        },
    };

    // Add a final empty line for nice format
    if tab.is_some() {
        result.saturating_add(LINE_BREAK.len())
    } else {
        result
    }
}