//! This module contains a [`CompactTable`] table.

use core::cmp::max;
use core::fmt;

use crate::{
    grid::{
        config::{AlignmentHorizontal, CompactConfig, Indent, Sides},
        dimension::{ConstDimension, ConstSize, Dimension},
        records::{
            into_records::{LimitColumns, LimitRows},
            IntoRecords, IterRecords,
        },
        util::string::get_line_width,
        CompactGrid,
    },
    settings::{style::Style, TableOption},
};

/// A table which consumes an [`IntoRecords`] iterator.
/// It assumes that the content has only single line.
///
/// In contrast to [`Table`] [`CompactTable`] does no allocations but it consumes an iterator.
/// It's useful when you don't want to re/allocate a buffer for your data.
///
/// # Example
///
/// It works smoothly with arrays.
///
#[cfg_attr(feature = "std", doc = "```")]
#[cfg_attr(not(feature = "std"), doc = "```ignore")]
/// use tabled::{settings::Style, tables::CompactTable};
///
/// let data = [
///     ["FreeBSD", "1993", "William and Lynne Jolitz", "?"],
///     ["OpenBSD", "1995", "Theo de Raadt", ""],
///     ["HardenedBSD", "2014", "Oliver Pinter and Shawn Webb", ""],
/// ];
///
/// let table = CompactTable::from(data)
///     .with(Style::psql())
///     .to_string();
///
/// assert_eq!(
///     table,
///     concat!(
///         " FreeBSD     | 1993 | William and Lynne Jolitz     | ? \n",
///         " OpenBSD     | 1995 | Theo de Raadt                |   \n",
///         " HardenedBSD | 2014 | Oliver Pinter and Shawn Webb |   ",
///     )
/// );
/// ```
///
/// But it's default creation requires to be given an estimated cell width, and the amount of columns.
///
#[cfg_attr(feature = "std", doc = "```")]
#[cfg_attr(not(feature = "std"), doc = "```ignore")]
/// use tabled::{settings::Style, tables::CompactTable};
///
/// let data = [
///     ["FreeBSD", "1993", "William and Lynne Jolitz", "?"],
///     ["OpenBSD", "1995", "Theo de Raadt", ""],
///     ["HardenedBSD", "2014", "Oliver Pinter and Shawn Webb", ""],
/// ];
///
/// // See what will happen if the given width is too narrow
///
/// let table = CompactTable::new(&data)
///     .columns(4)
///     .width(5)
///     .with(Style::ascii())
///     .to_string();
///
/// assert_eq!(
///     table,
///     "+-----+-----+-----+-----+\n\
///      | FreeBSD | 1993 | William and Lynne Jolitz | ?   |\n\
///      |-----+-----+-----+-----|\n\
///      | OpenBSD | 1995 | Theo de Raadt |     |\n\
///      |-----+-----+-----+-----|\n\
///      | HardenedBSD | 2014 | Oliver Pinter and Shawn Webb |     |\n\
///      +-----+-----+-----+-----+"
/// );
/// ```
///
/// [`Table`]: crate::Table
#[derive(Debug, Clone)]
pub struct CompactTable<I, D> {
    records: I,
    cfg: CompactConfig,
    dims: D,
    count_columns: usize,
    count_rows: Option<usize>,
}

impl<I> CompactTable<I, ConstDimension<0, 0>> {
    /// Creates a new [`CompactTable`] structure with a width dimension for all columns.
    pub const fn new(iter: I) -> Self
    where
        I: IntoRecords,
    {
        Self {
            records: iter,
            cfg: create_config(),
            count_columns: 0,
            count_rows: None,
            dims: ConstDimension::new(ConstSize::Value(2), ConstSize::Value(1)),
        }
    }
}

impl<I, const ROWS: usize, const COLS: usize> CompactTable<I, ConstDimension<COLS, ROWS>> {
    /// Set a height for each row.
    pub fn height<S: Into<ConstSize<COUNT_ROWS>>, const COUNT_ROWS: usize>(
        self,
        size: S,
    ) -> CompactTable<I, ConstDimension<COLS, COUNT_ROWS>> {
        let (width, _) = self.dims.into();
        CompactTable {
            dims: ConstDimension::new(width, size.into()),
            records: self.records,
            cfg: self.cfg,
            count_columns: self.count_columns,
            count_rows: self.count_rows,
        }
    }

    /// Set a width for each column.
    pub fn width<S: Into<ConstSize<COUNT_COLUMNS>>, const COUNT_COLUMNS: usize>(
        self,
        size: S,
    ) -> CompactTable<I, ConstDimension<COUNT_COLUMNS, ROWS>> {
        let (_, height) = self.dims.into();
        CompactTable {
            dims: ConstDimension::new(size.into(), height),
            records: self.records,
            cfg: self.cfg,
            count_columns: self.count_columns,
            count_rows: self.count_rows,
        }
    }
}

impl<I, D> CompactTable<I, D> {
    /// Creates a new [`CompactTable`] structure with a known dimension.
    ///
    /// Notice that the function wont call [`Estimate`].
    ///
    /// [`Estimate`]: crate::grid::dimension::Estimate
    pub fn with_dimension(iter: I, dimension: D) -> Self
    where
        I: IntoRecords,
    {
        Self {
            records: iter,
            dims: dimension,
            cfg: create_config(),
            count_columns: 0,
            count_rows: None,
        }
    }

    /// With is a generic function which applies options to the [`CompactTable`].
    pub fn with<O>(mut self, option: O) -> Self
    where
        for<'a> O: TableOption<IterRecords<&'a I>, CompactConfig, D>,
    {
        let mut records = IterRecords::new(&self.records, self.count_columns, self.count_rows);
        option.change(&mut records, &mut self.cfg, &mut self.dims);

        self
    }

    /// Limit a number of rows.
    pub const fn rows(mut self, count_rows: usize) -> Self {
        self.count_rows = Some(count_rows);
        self
    }

    /// Limit a number of columns.
    pub const fn columns(mut self, count: usize) -> Self {
        self.count_columns = count;
        self
    }

    /// Returns a table config.
    pub fn get_config(&self) -> &CompactConfig {
        &self.cfg
    }

    /// Returns a table config.
    pub fn get_config_mut(&mut self) -> &mut CompactConfig {
        &mut self.cfg
    }

    /// Format table into [fmt::Write]er.
    pub fn fmt<W>(self, writer: W) -> fmt::Result
    where
        I: IntoRecords,
        I::Cell: AsRef<str>,
        D: Dimension,
        W: fmt::Write,
    {
        build_grid(
            writer,
            self.records,
            self.dims,
            self.cfg,
            self.count_columns,
            self.count_rows,
        )
    }

    /// Format table into a writer.
    #[cfg(feature = "std")]
    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
    pub fn build<W>(self, writer: W) -> std::io::Result<()>
    where
        I: IntoRecords,
        I::Cell: AsRef<str>,
        D: Dimension,
        W: std::io::Write,
    {
        let writer = crate::util::utf8_writer::UTF8Writer::new(writer);
        self.fmt(writer)
            .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
    }

    /// Build a string.
    ///
    /// We can't implement [`std::string::ToString`] cause it does takes `&self` reference.
    #[allow(clippy::inherent_to_string)]
    #[cfg(feature = "std")]
    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
    pub fn to_string(self) -> String
    where
        I: IntoRecords,
        I::Cell: AsRef<str>,
        D: Dimension,
    {
        let mut buf = String::new();
        self.fmt(&mut buf)
            .expect("it's expected to be ok according to doc");

        buf
    }
}

impl<T, const ROWS: usize, const COLS: usize> From<[[T; COLS]; ROWS]>
    for CompactTable<[[T; COLS]; ROWS], ConstDimension<COLS, ROWS>>
where
    T: AsRef<str>,
{
    fn from(mat: [[T; COLS]; ROWS]) -> Self {
        let mut width = [0; COLS];
        for row in mat.iter() {
            for (col, text) in row.iter().enumerate() {
                let text = text.as_ref();
                let text_width = get_line_width(text);
                width[col] = max(width[col], text_width);
            }
        }

        // add padding
        for w in &mut width {
            *w += 2;
        }

        let dims = ConstDimension::new(ConstSize::List(width), ConstSize::Value(1));
        Self::with_dimension(mat, dims).columns(COLS).rows(ROWS)
    }
}

fn build_grid<W, I, D>(
    writer: W,
    records: I,
    dims: D,
    config: CompactConfig,
    cols: usize,
    rows: Option<usize>,
) -> Result<(), fmt::Error>
where
    W: fmt::Write,
    I: IntoRecords,
    I::Cell: AsRef<str>,
    D: Dimension,
{
    match rows {
        Some(limit) => {
            let records = LimitRows::new(records, limit);
            let records = LimitColumns::new(records, cols);
            let records = IterRecords::new(records, cols, rows);
            CompactGrid::new(records, dims, config).build(writer)
        }
        None => {
            let records = LimitColumns::new(records, cols);
            let records = IterRecords::new(records, cols, rows);
            CompactGrid::new(records, dims, config).build(writer)
        }
    }
}

const fn create_config() -> CompactConfig {
    CompactConfig::new()
        .set_padding(Sides::new(
            Indent::spaced(1),
            Indent::spaced(1),
            Indent::zero(),
            Indent::zero(),
        ))
        .set_alignment_horizontal(AlignmentHorizontal::Left)
        .set_borders(Style::ascii().get_borders())
}

impl<R, D> TableOption<R, CompactConfig, D> for CompactConfig {
    fn change(self, _: &mut R, cfg: &mut CompactConfig, _: &mut D) {
        *cfg = self;
    }
}
