use super::*;

pub(super) struct TableState<'a> {
    /// Alignment markers for HTML rendering
    /// * :-: center alignment
    /// * :-- left alignment
    /// * --: right alignmet
    /// * --- no alignmet
    alignment: Vec<Alignment>,
    /// Table headers
    headers: Vec<Cow<'a, str>>,
    /// Keep track of the widest cell in each column.
    /// Will use this info to align columns when rewriting
    max_column_width: Vec<usize>,
    /// Table rows
    body: Vec<Vec<Cow<'a, str>>>,
    /// Keep track of whether or not we're writing to the headers
    write_to_body: bool,
    /// Keep track of Which cell we're currently operating on.
    col_index: usize,
}

impl<'a> Write for TableState<'a> {
    fn write_str(&mut self, s: &str) -> std::fmt::Result {
        self.write(s.to_owned().into());
        Ok(())
    }

    fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) -> std::fmt::Result {
        if let Some(text) = args.as_str() {
            self.write(text.into());
        } else {
            self.write(args.to_string().into());
        }
        Ok(())
    }
}

impl<'a> TableState<'a> {
    pub(super) fn new(alignment: Vec<Alignment>) -> Self {
        let capacity = alignment.len();
        Self {
            alignment,
            headers: Vec::with_capacity(capacity),
            max_column_width: vec![3; capacity],
            body: vec![],
            write_to_body: false,
            col_index: 0,
        }
    }

    /// Check if we've written anything to the table state yet
    pub(super) fn is_empty(&self) -> bool {
        self.headers.is_empty() && self.body.is_empty()
    }

    /// Write some values to the table state.
    pub(super) fn write(&mut self, value: Cow<'a, str>) {
        if self.write_to_body {
            // push or update the body
            self.write_cell(value)
        } else {
            // push or update the headers
            self.write_header(value)
        }
    }

    /// Allows the caller to advance the internal col_index.
    /// All subsequent calls to `write` will affect the next cell
    pub(super) fn increment_col_index(&mut self) {
        self.col_index += 1;
    }

    /// Update the table state to start writing to the table body
    pub(super) fn push_row(&mut self) {
        self.body.push(Vec::with_capacity(self.alignment.len()));
        self.write_to_body = true;
        self.col_index = 0;
    }

    fn write_header(&mut self, text: Cow<'a, str>) {
        let header_width = unicode_str_width(&text);
        if let Some(column_header) = self.headers.get_mut(self.col_index) {
            *column_header += text;

            let current_width = self
                .max_column_width
                .get_mut(self.col_index)
                .expect("should be set");
            *current_width += header_width;
        } else {
            self.headers.push(text);
            self.update_column_width(self.col_index, header_width);
        }
    }

    fn write_cell(&mut self, text: Cow<'a, str>) {
        let row = self
            .body
            .last_mut()
            .expect("can only write cells after push_row called");

        if let Some(cell_value) = row.get_mut(self.col_index) {
            let current_width = unicode_str_width(cell_value) + unicode_str_width(&text);
            *cell_value += text;
            self.update_column_width(self.col_index, current_width);
        } else {
            let cell_width = unicode_str_width(&text);
            row.push(text);
            self.update_column_width(self.col_index, cell_width);
        }
    }

    fn update_column_width(&mut self, index: usize, column_width: usize) {
        if let Some(old_column_width) = self.max_column_width.get_mut(index) {
            if *old_column_width < column_width {
                *old_column_width = column_width
            }
        }
    }

    pub(super) fn format(self) -> Result<String, std::fmt::Error> {
        let mut result = String::new();
        self.rewrite_header(&mut result)?;
        self.rewrite_alignment(&mut result)?;
        self.rewrite_body(&mut result)?;
        Ok(result)
    }

    fn write_wth_padding(buffer: &mut String, value: &str, mut size: usize) -> std::fmt::Result {
        let offset = UnicodeSegmentation::graphemes(value, true)
            .map(|grapheme| unicode_str_width(grapheme).saturating_sub(1))
            .sum();
        size = size.saturating_sub(offset);
        write!(buffer, " {value:<0$} |", size)
    }

    fn rewrite_header(&self, buffer: &mut String) -> std::fmt::Result {
        for (header, width) in self.headers.iter().zip(self.max_column_width.iter()) {
            Self::write_wth_padding(buffer, header, *width)?;
        }
        Ok(())
    }

    fn rewrite_alignment(&self, buffer: &mut String) -> std::fmt::Result {
        writeln!(buffer)?;
        for (alignment, width) in self.alignment.iter().zip(self.max_column_width.iter()) {
            let alignment = match alignment {
                Alignment::Center => {
                    // :-:
                    // (- 2 to account for 2 `:`)
                    format!(":{:-^1$}:", "-", width - 2)
                }
                Alignment::Left => {
                    // :--
                    format!("{:-<1$}", ":", width)
                }
                Alignment::Right => {
                    // --:
                    format!("{:->1$}", ":", width)
                }
                Alignment::None => {
                    // ---
                    "-".repeat(*width)
                }
            };
            Self::write_wth_padding(buffer, &alignment, *width)?;
        }
        Ok(())
    }

    fn rewrite_body(&self, buffer: &mut String) -> std::fmt::Result {
        for row in self.body.iter() {
            writeln!(buffer)?;
            for either_or_both in row.iter().zip_longest(self.max_column_width.iter()) {
                match either_or_both {
                    EitherOrBoth::Both(cell, width) => {
                        Self::write_wth_padding(buffer, cell, *width)?;
                    }
                    EitherOrBoth::Right(width) => {
                        Self::write_wth_padding(buffer, "", *width)?;
                    }
                    EitherOrBoth::Left(_) => {
                        // There may be fewer cells in a row, but there should never be more cells.
                        // See https://github.github.com/gfm/#example-204 and the preceding text
                        unreachable!()
                    }
                }
            }
        }
        Ok(())
    }
}
