// License: see LICENSE file at root directory of `master` branch

//! # Formatter

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

use {
    core::convert::TryFrom,
    std::io::Write,

    crate::{
        Array, IoResult, Object, Value,
        bytes,
    },
};

/// # Default tab width
pub 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"                                ";

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

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

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: match has_tab_width {
                true => Some(0),
                false => None,
            },
        }
    }

    /// # Formats a value
    ///
    /// ## Notes
    ///
    /// - The stream is used as-is. For better performance, you _should_ wrap your stream inside a [`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: &Value, stream: &mut W) -> IoResult<()> where W: Write {
        match value {
            Value::String(s) => format_string(s, stream),
            Value::Number(n) => n.write(stream),
            Value::Boolean(true) => stream.write_all(b"true"),
            Value::Boolean(false) => stream.write_all(b"false"),
            Value::Null => stream.write_all(b"null"),
            Value::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'}'])
            },
            Value::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']'])
            },
        }
    }

    /// # Formats object content
    fn format_object_content<W>(&mut self, object: &Object, 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 object.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)?;
        }

        match pad.is_some() && object.is_empty() == false {
            true => stream.write_all(LINE_BREAK),
            false => 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)?;
        }

        match pad.is_some() && array.is_empty() == false {
            true => stream.write_all(LINE_BREAK),
            false => 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 fn estimate_format_size(value: &Value, 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,
    };
    match value {
        Value::String(s) => s.len().saturating_add(2),
        Value::Number(n) => match u128::try_from(n) {
            Ok(0) => 1,
            Ok(n) => ((n as f64).log10().ceil() as usize).max(1),
            Err(_) => 15,
        },
        Value::Boolean(_) => 5,
        Value::Null => 4,
        Value::Object(object) => {
            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 object {
                // "":,
                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 object.is_empty() == false {
                if let Some(pad) = make_pad(tab.as_ref(), level.as_ref()) {
                    result = result.saturating_add(1).saturating_add(pad);
                }
            }

            result
        },
        Value::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
        },
    }
}
