//! This module contains definition of table/row cells stuff

use super::format::Alignment;
use super::utils::display_width;
use super::utils::print_align;
use super::{color, Attr, Terminal};
use std::io::{Error, Write};
use std::string::ToString;
use std::str::FromStr;

/// Represent a table cell containing a string.
///
/// Once created, a cell's content cannot be modified.
/// The cell would have to be replaced by another one
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct Cell {
    content: Vec<String>,
    width: usize,
    align: Alignment,
    style: Vec<Attr>,
    hspan: usize,
}

impl Cell {
    /// Create a new `Cell` initialized with content from `string`.
    /// Text alignment in cell is configurable with the `align` argument
    pub fn new_align(string: &str, align: Alignment) -> Cell {
        let content: Vec<String> = string.lines().map(|x| x.to_string()).collect();
        let mut width = 0;
        for cont in &content {
            let l = display_width(&cont[..]);
            if l > width {
                width = l;
            }
        }
        Cell {
            content: content,
            width: width,
            align: align,
            style: Vec::new(),
            hspan: 1,
        }
    }

    /// Create a new `Cell` initialized with content from `string`.
    /// By default, content is align to `LEFT`
    pub fn new(string: &str) -> Cell {
        Cell::new_align(string, Alignment::LEFT)
    }

    /// Set text alignment in the cell
    pub fn align(&mut self, align: Alignment) {
        self.align = align;
    }

    /// Add a style attribute to the cell
    pub fn style(&mut self, attr: Attr) {
        self.style.push(attr);
    }

    /// Add a style attribute to the cell. Can be chained
    pub fn with_style(mut self, attr: Attr) -> Cell {
        self.style(attr);
        self
    }

    /// Add horizontal spanning to the cell
    pub fn with_hspan(mut self, hspan: usize) -> Cell {
        self.set_hspan(hspan);
        self
    }

    /// Remove all style attributes and reset alignment to default (LEFT)
    pub fn reset_style(&mut self) {
        self.style.clear();
        self.align(Alignment::LEFT);
    }

    /// Set the cell's style by applying the given specifier string
    ///
    /// # Style spec syntax
    ///
    /// The syntax for the style specifier looks like this :
    /// **FrBybl** which means **F**oreground **r**ed **B**ackground **y**ellow **b**old **l**eft
    ///
    /// ### List of supported specifiers :
    ///
    /// * **F** : **F**oreground (must be followed by a color specifier)
    /// * **B** : **B**ackground (must be followed by a color specifier)
    /// * **H** : **H**orizontal span (must be followed by a number)
    /// * **b** : **b**old
    /// * **i** : **i**talic
    /// * **u** : **u**nderline
    /// * **c** : Align **c**enter
    /// * **l** : Align **l**eft
    /// * **r** : Align **r**ight
    /// * **d** : **d**efault style
    ///
    /// ### List of color specifiers :
    ///
    /// * **r** : Red
    /// * **b** : Blue
    /// * **g** : Green
    /// * **y** : Yellow
    /// * **c** : Cyan
    /// * **m** : Magenta
    /// * **w** : White
    /// * **d** : Black
    ///
    /// And capital letters are for **bright** colors.
    /// Eg :
    ///
    /// * **R** : Bright Red
    /// * **B** : Bright Blue
    /// * ... and so on ...
    pub fn style_spec(mut self, spec: &str) -> Cell {
        self.reset_style();
        let mut foreground = false;
        let mut background = false;
        let mut it = spec.chars().peekable();
        while let Some(c) = it.next() {
            if foreground || background {
                let color = match c {
                    'r' => color::RED,
                    'R' => color::BRIGHT_RED,
                    'b' => color::BLUE,
                    'B' => color::BRIGHT_BLUE,
                    'g' => color::GREEN,
                    'G' => color::BRIGHT_GREEN,
                    'y' => color::YELLOW,
                    'Y' => color::BRIGHT_YELLOW,
                    'c' => color::CYAN,
                    'C' => color::BRIGHT_CYAN,
                    'm' => color::MAGENTA,
                    'M' => color::BRIGHT_MAGENTA,
                    'w' => color::WHITE,
                    'W' => color::BRIGHT_WHITE,
                    'd' => color::BLACK,
                    'D' => color::BRIGHT_BLACK,
                    _ => {
                        // Silently ignore unknown tags
                        foreground = false;
                        background = false;
                        continue;
                    }
                };
                if foreground {
                    self.style(Attr::ForegroundColor(color));
                } else if background {
                    self.style(Attr::BackgroundColor(color));
                }
                foreground = false;
                background = false;
            } else {
                match c {
                    'F' => foreground = true,
                    'B' => background = true,
                    'b' => self.style(Attr::Bold),
                    'i' => self.style(Attr::Italic(true)),
                    'u' => self.style(Attr::Underline(true)),
                    'c' => self.align(Alignment::CENTER),
                    'l' => self.align(Alignment::LEFT),
                    'r' => self.align(Alignment::RIGHT),
                    'H' => {
                        let mut span_s = String::new();
                        while let Some('0'..='9') = it.peek() {
                            span_s.push(it.next().unwrap());
                        }
                        let span = usize::from_str(&span_s).unwrap();
                        self.set_hspan(span);
                    }
                    _ => { /* Silently ignore unknown tags */ }
                }
            }
        }
        self
    }

    /// Return the height of the cell
    #[deprecated(since="0.8.0", note="Will become private in future release. See [issue #87](https://github.com/phsym/prettytable-rs/issues/87)")]
    pub fn get_height(&self) -> usize {
        self.content.len()
    }

    /// Return the width of the cell
    #[deprecated(since="0.8.0", note="Will become private in future release. See [issue #87](https://github.com/phsym/prettytable-rs/issues/87)")]
    pub fn get_width(&self) -> usize {
        self.width
    }

    /// Set horizontal span for this cell (must be > 0)
    pub fn set_hspan(&mut self, hspan: usize) {
        self.hspan = if hspan <= 0 {1} else {hspan};
    }

    /// Get horizontal span of this cell (> 0)
    pub fn get_hspan(&self) -> usize {
        self.hspan
    }

    /// Return a copy of the full string contained in the cell
    pub fn get_content(&self) -> String {
        self.content.join("\n")
    }

    /// Print a partial cell to `out`. Since the cell may be multi-lined,
    /// `idx` is the line index to print. `col_width` is the column width used to
    /// fill the cells with blanks so it fits in the table.
    /// If `ìdx` is higher than this cell's height, it will print empty content
    #[deprecated(since="0.8.0", note="Will become private in future release. See [issue #87](https://github.com/phsym/prettytable-rs/issues/87)")]
    pub fn print<T: Write + ?Sized>(
        &self,
        out: &mut T,
        idx: usize,
        col_width: usize,
        skip_right_fill: bool,
    ) -> Result<(), Error> {
        let c = self.content.get(idx).map(|s| s.as_ref()).unwrap_or("");
        print_align(out, self.align, c, ' ', col_width, skip_right_fill)
    }

    /// Apply style then call `print` to print the cell into a terminal
    #[deprecated(since="0.8.0", note="Will become private in future release. See [issue #87](https://github.com/phsym/prettytable-rs/issues/87)")]
    pub fn print_term<T: Terminal + ?Sized>(
        &self,
        out: &mut T,
        idx: usize,
        col_width: usize,
        skip_right_fill: bool,
    ) -> Result<(), Error> {
        for a in &self.style {
            match out.attr(*a) {
                Ok(..) | Err(::term::Error::NotSupported) | Err(::term::Error::ColorOutOfRange) => {
                    ()
                } // Ignore unsupported atrributes
                Err(e) => return Err(term_error_to_io_error(e)),
            };
        }
        self.print(out, idx, col_width, skip_right_fill)?;
        match out.reset() {
            Ok(..) | Err(::term::Error::NotSupported) | Err(::term::Error::ColorOutOfRange) => {
                Ok(())
            }
            Err(e) => Err(term_error_to_io_error(e)),
        }
    }
}

fn term_error_to_io_error(te: ::term::Error) -> Error {
    match te {
        ::term::Error::Io(why) => why,
        _ => Error::new(::std::io::ErrorKind::Other, te),
    }
}

impl<'a, T: ToString> From<&'a T> for Cell {
    fn from(f: &T) -> Cell {
        Cell::new(&f.to_string())
    }
}

impl ToString for Cell {
    fn to_string(&self) -> String {
        self.get_content()
    }
}

impl Default for Cell {
    /// Return a cell initialized with a single empty `String`, with LEFT alignment
    fn default() -> Cell {
        Cell {
            content: vec!["".to_string(); 1],
            width: 0,
            align: Alignment::LEFT,
            style: Vec::new(),
            hspan: 1,
        }
    }
}

/// This macro simplifies `Cell` creation
///
/// Support 2 syntax : With and without style specification.
/// # Syntax
/// ```text
/// cell!(value);
/// ```
/// or
///
/// ```text
/// cell!(spec->value);
/// ```
/// Value must implement the `std::string::ToString` trait
///
/// For details about style specifier syntax, check doc for [`Cell::style_spec`](cell/struct.Cell.html#method.style_spec) method
/// # Example
/// ```
/// # #[macro_use] extern crate prettytable;
/// # fn main() {
/// let cell = cell!("value");
/// // Do something with the cell
/// # drop(cell);
/// // Create a cell with style (Red foreground, Bold, aligned to left);
/// let styled = cell!(Frbl->"value");
/// # drop(styled);
/// # }
/// ```
#[macro_export]
macro_rules! cell {
    () => {
        $crate::Cell::default()
    };
    ($value:expr) => {
        $crate::Cell::new(&$value.to_string())
    };
    ($style:ident -> $value:expr) => {
        cell!($value).style_spec(stringify!($style))
    };
}

#[cfg(test)]
mod tests {
    use Cell;
    use format::Alignment;
    use term::{color, Attr};
    use utils::StringWriter;

    #[test]
    fn get_content() {
        let cell = Cell::new("test");
        assert_eq!(cell.get_content(), "test");
    }

    #[test]
    fn print_ascii() {
        let ascii_cell = Cell::new("hello");
        assert_eq!(ascii_cell.get_width(), 5);

        let mut out = StringWriter::new();
        let _ = ascii_cell.print(&mut out, 0, 10, false);
        assert_eq!(out.as_string(), "hello     ");
    }

    #[test]
    fn print_unicode() {
        let unicode_cell = Cell::new("привет");
        assert_eq!(unicode_cell.get_width(), 6);

        let mut out = StringWriter::new();
        let _ = unicode_cell.print(&mut out, 0, 10, false);
        assert_eq!(out.as_string(), "привет    ");
    }

    #[test]
    fn print_cjk() {
        let unicode_cell = Cell::new("由系统自动更新");
        assert_eq!(unicode_cell.get_width(), 14);
        let mut out = StringWriter::new();
        let _ = unicode_cell.print(&mut out, 0, 20, false);
        assert_eq!(out.as_string(), "由系统自动更新      ");
    }

    #[test]
    fn align_left() {
        let cell = Cell::new_align("test", Alignment::LEFT);
        let mut out = StringWriter::new();
        let _ = cell.print(&mut out, 0, 10, false);
        assert_eq!(out.as_string(), "test      ");
    }

    #[test]
    fn align_center() {
        let cell = Cell::new_align("test", Alignment::CENTER);
        let mut out = StringWriter::new();
        let _ = cell.print(&mut out, 0, 10, false);
        assert_eq!(out.as_string(), "   test   ");
    }

    #[test]
    fn align_right() {
        let cell = Cell::new_align("test", Alignment::RIGHT);
        let mut out = StringWriter::new();
        let _ = cell.print(&mut out, 0, 10, false);
        assert_eq!(out.as_string(), "      test");
    }

    #[test]
    fn style_spec() {
        let mut cell = Cell::new("test").style_spec("FrBBbuic");
        assert_eq!(cell.style.len(), 5);
        assert!(cell.style.contains(&Attr::Underline(true)));
        assert!(cell.style.contains(&Attr::Italic(true)));
        assert!(cell.style.contains(&Attr::Bold));
        assert!(cell.style.contains(&Attr::ForegroundColor(color::RED)));
        assert!(
            cell.style
                .contains(&Attr::BackgroundColor(color::BRIGHT_BLUE))
        );
        assert_eq!(cell.align, Alignment::CENTER);

        cell = cell.style_spec("FDBwr");
        assert_eq!(cell.style.len(), 2);
        assert!(
            cell.style
                .contains(&Attr::ForegroundColor(color::BRIGHT_BLACK))
        );
        assert!(cell.style.contains(&Attr::BackgroundColor(color::WHITE)));
        assert_eq!(cell.align, Alignment::RIGHT);

        // Test with invalid sepcifier chars
        cell = cell.clone();
        cell = cell.style_spec("FzBr");
        assert!(cell.style.contains(&Attr::BackgroundColor(color::RED)));
        assert_eq!(cell.style.len(), 1);
        cell = cell.style_spec("zzz");
        assert!(cell.style.is_empty());
        assert_eq!(cell.get_hspan(), 1);
        cell = cell.style_spec("FDBwH03r");
        assert_eq!(cell.get_hspan(), 3);
    }

    #[test]
    fn reset_style() {
        let mut cell = Cell::new("test")
            .with_style(Attr::ForegroundColor(color::BRIGHT_BLACK))
            .with_style(Attr::BackgroundColor(color::WHITE));
        cell.align(Alignment::RIGHT);

        //style_spec("FDBwr");
        assert_eq!(cell.style.len(), 2);
        assert_eq!(cell.align, Alignment::RIGHT);
        cell.reset_style();
        assert_eq!(cell.style.len(), 0);
        assert_eq!(cell.align, Alignment::LEFT);
    }

    #[test]
    fn default_empty_cell() {
        let cell = Cell::default();
        assert_eq!(cell.align, Alignment::LEFT);
        assert!(cell.style.is_empty());
        assert_eq!(cell.get_content(), "");
        assert_eq!(cell.to_string(), "");
        assert_eq!(cell.get_height(), 1);
        assert_eq!(cell.get_width(), 0);
    }
}
