nonogrid 0.7.3

Efficient nonogram solver
Documentation
use std::{any::Any, fmt, fs, io, num::ParseIntError};

use hashbrown::{HashMap, HashSet};
use log::info;

use crate::{
    block::{
        base::{
            clues_from_solution,
            color::{ColorId, ColorPalette, ColorValue},
        },
        binary::BinaryBlock,
        Block, Description,
    },
    board::Board,
    utils::{iter::FindOk, product, rc::MutRc, split_sections},
};

pub use self::{ini::MyFormat, xml::WebPbn};

#[derive(Debug)]
pub struct ParseError(pub String);

pub trait BoardParser: fmt::Debug {
    fn with_content(content: &str) -> Result<Self, ParseError>
    where
        Self: Sized;

    fn parse<B>(&self) -> Board<B>
    where
        B: Block;

    fn parse_rc<B>(&self) -> MutRc<Board<B>>
    where
        B: Block,
    {
        MutRc::new(self.parse())
    }

    fn infer_scheme(&self) -> PuzzleScheme;
}

impl From<io::Error> for ParseError {
    fn from(err: io::Error) -> Self {
        Self(format!("{:?}", err))
    }
}

pub trait LocalReader: BoardParser {
    fn read_local(file_name: &str) -> Result<Self, ParseError>
    where
        Self: Sized,
    {
        let content = Self::file_content(file_name)?;
        Self::with_content(&content)
    }
    fn file_content(file_name: &str) -> io::Result<String> {
        fs::read_to_string(file_name)
    }
}

#[cfg(feature = "web")]
impl From<reqwest::Error> for ParseError {
    fn from(err: reqwest::Error) -> Self {
        Self(format!("{:?}", err))
    }
}

pub trait NetworkReader: BoardParser {
    fn read_remote(file_name: &str) -> Result<Self, ParseError>
    where
        Self: Sized,
    {
        let content = Self::http_content(file_name)?;
        Self::with_content(&content)
    }

    #[cfg(feature = "web")]
    fn http_content(url: &str) -> Result<String, reqwest::Error> {
        info!("Requesting {} ...", url);
        let response = reqwest::blocking::get(url)?;
        response.text()
    }

    #[cfg(not(feature = "web"))]
    fn http_content(url: &str) -> Result<String, ParseError> {
        info!("Requesting {} ...", url);
        Err(ParseError(format!(
            "Cannot request url {}: no support for web client (hint: add --features=web)",
            url
        )))
    }
}

pub trait Paletted {
    fn get_colors(&self) -> Vec<(String, char, String)>;
    fn get_colors_sorted(&self) -> Vec<(String, char, String)> {
        let mut colors = self.get_colors();
        colors.sort_unstable_by(|(name1, ..), (name2, ..)| name1.cmp(name2));
        colors
    }

    fn default_palette(&self, white_name: &str, black_name: &str) -> ColorPalette {
        let mut palette = ColorPalette::with_white_and_black(white_name, black_name);

        for (name, symbol, value) in &self.get_colors_sorted() {
            let val = ColorValue::parse(value);
            palette.color_with_name_value_and_symbol(name, val, *symbol);
        }

        palette
    }
    fn get_palette(&self) -> ColorPalette;
}

#[derive(Debug, PartialEq, Clone, Copy)]
pub enum PuzzleScheme {
    BlackAndWhite,
    MultiColor,
}

#[cfg(feature = "ini")]
mod ini {
    use serde::Deserialize;

    use super::{
        Block, Board, BoardParser, ColorPalette, Description, LocalReader, Paletted, ParseError,
        PuzzleScheme,
    };

    #[derive(Debug, Deserialize)]
    struct Clues {
        rows: String,
        columns: String,
    }

    #[derive(Debug, Deserialize)]
    struct Colors {
        defs: Option<Vec<String>>,
    }

    #[derive(Debug, Deserialize)]
    pub struct MyFormat {
        clues: Clues,
        colors: Option<Colors>,
    }

    impl LocalReader for MyFormat {}

    impl From<toml::de::Error> for ParseError {
        fn from(err: toml::de::Error) -> Self {
            Self(format!("{:?}", err))
        }
    }

    impl BoardParser for MyFormat {
        fn with_content(content: &str) -> Result<Self, ParseError> {
            Ok(toml::from_str(content)?)
        }

        fn parse<B>(&self) -> Board<B>
        where
            B: Block,
        {
            let clues = &self.clues;
            let palette = self.get_palette();
            Board::with_descriptions_and_palette(
                Self::parse_clues(&clues.rows, &palette),
                Self::parse_clues(&clues.columns, &palette),
                Some(palette),
            )
        }

        fn infer_scheme(&self) -> PuzzleScheme {
            if let Some(colors) = &self.colors {
                if let Some(defs) = &colors.defs {
                    if !defs.is_empty() {
                        return PuzzleScheme::MultiColor;
                    }
                }
            }

            PuzzleScheme::BlackAndWhite
        }
    }

    impl MyFormat {
        fn parse_block<B>(block: &str, palette: &ColorPalette) -> B
        where
            B: Block,
        {
            let mut as_chars = block.chars();
            let value_color_pos = as_chars.position(|c| !c.is_digit(10));
            #[allow(clippy::option_if_let_else)]
            let (value, block_color) = if let Some(pos) = value_color_pos {
                let (value, color) = block.split_at(pos);
                (value, Some(color))
            } else {
                (block, palette.get_default())
            };

            let color_id = block_color.and_then(|name| palette.id_by_name(name));
            B::from_str_and_color(value, color_id)
        }

        fn parse_line<B>(descriptions: &str, palette: &ColorPalette) -> Option<Vec<Description<B>>>
        where
            B: Block,
        {
            let descriptions = descriptions.trim();
            let non_comment: &str = descriptions
                .split(&['#', ';'][..])
                .next()
                .expect("Split returned empty");

            if non_comment.is_empty() {
                return None;
            }

            Some(
                non_comment
                    .split(',')
                    .filter_map(|row| {
                        let row = row.trim().trim_matches(&['\'', '"'][..]);
                        if row.is_empty() {
                            None
                        } else {
                            Some(Description::new(
                                row.split_whitespace()
                                    .map(|block| Self::parse_block(block, palette))
                                    .collect(),
                            ))
                        }
                    })
                    .collect(),
            )
        }

        pub(super) fn parse_clues<B>(
            descriptions: &str,
            palette: &ColorPalette,
        ) -> Vec<Description<B>>
        where
            B: Block,
        {
            descriptions
                .lines()
                .flat_map(|line| Self::parse_line(line, palette).unwrap_or_default())
                .collect()
        }

        ///```
        /// # use nonogrid::parser::MyFormat;
        ///
        /// let s = "b = (blue) *";
        /// let def = MyFormat::parse_color_def(s);
        /// assert_eq!(def, ("b".to_string(), '*', "blue".to_string()));
        /// ```
        pub fn parse_color_def(color_def: impl AsRef<str>) -> (String, char, String) {
            let parts: Vec<_> = color_def.as_ref().split('=').map(str::trim).collect();
            let name = parts[0];
            let mut desc = parts[1].to_string();
            let symbol = desc.pop().expect("Empty color description in definition");

            desc = desc.trim().trim_matches(&['(', ')'][..]).to_string();
            (name.to_string(), symbol, desc)
        }
    }

    impl Paletted for MyFormat {
        fn get_colors(&self) -> Vec<(String, char, String)> {
            if let Some(colors) = &self.colors {
                if let Some(defs) = &colors.defs {
                    return defs.iter().map(Self::parse_color_def).collect();
                }
            }

            vec![]
        }

        fn get_palette(&self) -> ColorPalette {
            self.default_palette("W", "B")
        }
    }
}

#[cfg(not(feature = "ini"))]
mod ini {
    //! dummy definitions
    use super::{Block, Board, BoardParser, ParseError, PuzzleScheme};

    #[derive(Debug, Clone, Copy)]
    pub struct MyFormat;

    impl MyFormat {
        const NO_FEATURE_ENABLED_MSG: &'static str =
            "Cannot parse TOML-based puzzles: no support for TOML (hint: add --features=ini)";
    }

    impl BoardParser for MyFormat {
        fn with_content(_content: &str) -> Result<Self, ParseError>
        where
            Self: Sized,
        {
            Err(ParseError(Self::NO_FEATURE_ENABLED_MSG.to_string()))
        }

        fn parse<B>(&self) -> Board<B>
        where
            B: Block,
        {
            unimplemented!("{}", Self::NO_FEATURE_ENABLED_MSG)
        }

        fn infer_scheme(&self) -> PuzzleScheme {
            unimplemented!("{}", Self::NO_FEATURE_ENABLED_MSG)
        }
    }
}

#[cfg(feature = "xml")]
mod xml {
    use sxd_document as xml;
    use sxd_xpath::{
        evaluate_xpath,
        nodeset::{Node, Nodeset},
        Value,
    };

    use crate::utils::rc::{mutate_ref, read_ref, InteriorMutableRef};

    use super::{
        Block, Board, BoardParser, ColorPalette, Description, LocalReader, NetworkReader, Paletted,
        ParseError, PuzzleScheme,
    };

    #[derive(Debug)]
    pub struct WebPbn {
        package: xml::Package,
        cached_colors: InteriorMutableRef<Option<Vec<(String, char, String)>>>,
        cached_palette: InteriorMutableRef<Option<ColorPalette>>,
    }

    impl LocalReader for WebPbn {}

    impl NetworkReader for WebPbn {
        fn read_remote(file_name: &str) -> Result<Self, ParseError> {
            let url = format!("{}/XMLpuz.cgi?id={}", Self::BASE_URL, file_name);

            let content = Self::http_content(&url)?;
            Self::with_content(&content)
        }
    }

    impl From<xml::parser::Error> for ParseError {
        fn from(err: xml::parser::Error) -> Self {
            Self(format!("{:?}", err))
        }
    }

    impl BoardParser for WebPbn {
        fn with_content(content: &str) -> Result<Self, ParseError> {
            let package = xml::parser::parse(content)?;

            Ok(Self {
                package,
                cached_colors: InteriorMutableRef::new(None),
                cached_palette: InteriorMutableRef::new(None),
            })
        }

        fn parse<B>(&self) -> Board<B>
        where
            B: Block,
        {
            Board::with_descriptions_and_palette(
                self.parse_clues("rows"),
                self.parse_clues("columns"),
                Some(self.get_palette()),
            )
        }

        fn infer_scheme(&self) -> PuzzleScheme {
            let colors = self.get_colors_sorted();
            let names: Vec<_> = colors.iter().map(|(name, ..)| name).collect();
            if names.is_empty() || names == ["black", "white"] {
                return PuzzleScheme::BlackAndWhite;
            }

            PuzzleScheme::MultiColor
        }
    }

    impl WebPbn {
        const BASE_URL: &'static str = "http://webpbn.com";

        fn parse_block<B>(block: &Node<'_>, palette: &ColorPalette) -> B
        where
            B: Block,
        {
            let value = block.string_value();

            let block_color = if let Node::Element(e) = block {
                e.attribute("color")
                    .map(|color| color.value())
                    .or_else(|| palette.get_default())
            } else {
                None
            };

            let color_id = block_color.and_then(|name| palette.id_by_name(name));
            B::from_str_and_color(&value, color_id)
        }

        fn parse_line<B>(description: &Node<'_>, palette: &ColorPalette) -> Description<B>
        where
            B: Block,
        {
            Description::new(
                description
                    .children()
                    .iter()
                    .filter_map(|child| {
                        if let Node::Text(_text) = child {
                            // ignore newlines and whitespaces
                            None
                        } else {
                            Some(Self::parse_block(child, palette))
                        }
                    })
                    .collect(),
            )
        }

        fn get_clues<B>(descriptions: &Nodeset<'_>, palette: &ColorPalette) -> Vec<Description<B>>
        where
            B: Block,
        {
            descriptions
                .document_order()
                .iter()
                .map(|line_node| Self::parse_line(line_node, palette))
                .collect()
        }

        fn parse_clues<B>(&self, type_: &str) -> Vec<Description<B>>
        where
            B: Block,
        {
            let document = self.package.as_document();
            let value = evaluate_xpath(&document, &format!(".//clues[@type='{}']/line", type_))
                .expect("XPath evaluation failed");

            if let Value::Nodeset(ns) = value {
                Self::get_clues(&ns, &self.get_palette())
            } else {
                vec![]
            }
        }
    }

    impl WebPbn {
        fn _get_colors(&self) -> Vec<(String, char, String)> {
            let document = self.package.as_document();
            let value = evaluate_xpath(&document, ".//color").expect("XPath evaluation failed");

            if let Value::Nodeset(ns) = value {
                ns.iter()
                    .filter_map(|color_node| {
                        let value = color_node.string_value();
                        if let Node::Element(e) = color_node {
                            let name = e
                                .attribute("name")
                                .expect("Not found 'name' attribute in the 'color' element")
                                .value();
                            let symbol = e
                                .attribute("char")
                                .expect("Not found 'char' attribute in the 'color' element")
                                .value();
                            let symbol: char = symbol.as_bytes()[0] as char;
                            Some((name.to_string(), symbol, value))
                        } else {
                            None
                        }
                    })
                    .collect()
            } else {
                vec![]
            }
        }

        fn get_default_color(&self) -> Option<String> {
            let document = self.package.as_document();
            let value = evaluate_xpath(&document, ".//puzzle[@type='grid']")
                .expect("XPath evaluation failed");
            if let Value::Nodeset(ns) = value {
                let first_node = ns.iter().next();
                if let Some(Node::Element(e)) = first_node {
                    return e
                        .attribute("defaultcolor")
                        .map(|color| color.value().to_string());
                }
            }
            None
        }

        fn _get_palette(&self) -> ColorPalette {
            let mut palette = self.default_palette("white", "black");

            if let Some(default_color) = self.get_default_color() {
                palette.set_default(&default_color).unwrap();
            }
            palette
        }
    }

    impl Paletted for WebPbn {
        fn get_colors(&self) -> Vec<(String, char, String)> {
            if let Some(colors) = read_ref(&self.cached_colors).as_ref() {
                return colors.clone();
            }

            let result = self._get_colors();
            let mut cache = mutate_ref(&self.cached_colors);
            *cache = Some(result.clone());
            result
        }

        fn get_palette(&self) -> ColorPalette {
            if let Some(palette) = read_ref(&self.cached_palette).as_ref() {
                return palette.clone();
            }

            let result = self._get_palette();
            let mut cache = mutate_ref(&self.cached_palette);
            *cache = Some(result.clone());
            result
        }
    }
}

#[cfg(not(feature = "xml"))]
mod xml {
    //! dummy definitions
    use super::{Block, Board, BoardParser, NetworkReader, ParseError, PuzzleScheme};

    #[derive(Debug, Clone, Copy)]
    pub struct WebPbn;

    impl WebPbn {
        const NO_FEATURE_ENABLED_MSG: &'static str =
            "Cannot parse XML-based puzzles: no support for XML (hint: add --features=xml)";
    }

    impl BoardParser for WebPbn {
        fn with_content(_content: &str) -> Result<Self, ParseError>
        where
            Self: Sized,
        {
            Err(ParseError(Self::NO_FEATURE_ENABLED_MSG.to_string()))
        }

        fn parse<B>(&self) -> Board<B>
        where
            B: Block,
        {
            unimplemented!("{}", Self::NO_FEATURE_ENABLED_MSG)
        }

        fn infer_scheme(&self) -> PuzzleScheme {
            unimplemented!("{}", Self::NO_FEATURE_ENABLED_MSG)
        }
    }

    impl NetworkReader for WebPbn {}
}

type EncodedInt = u16;

#[derive(Debug)]
pub struct NonogramsOrg {
    encoded: Vec<Vec<EncodedInt>>,
}

impl NonogramsOrg {
    const URLS: [&'static str; 2] = ["http://www.nonograms.ru/", "http://www.nonograms.org/"];
    const PATHS: [(PuzzleScheme, &'static str); 2] = [
        (PuzzleScheme::BlackAndWhite, "nonograms"),
        (PuzzleScheme::MultiColor, "nonograms2"),
    ];
    const CYPHER_PREFIX: &'static str = r"var d=";
    const CYPHER_SUFFIX: char = ';';

    fn extract_encoded_json(html: &str) -> Option<&str> {
        #[allow(unused_imports)]
        use crate::utils::Stripper; // for Rust<1.45

        html.lines().find_map(|line| {
            line.strip_prefix(Self::CYPHER_PREFIX)
                .map(|line| line.trim_end_matches(Self::CYPHER_SUFFIX))
        })
    }

    fn parse_line(line: &str) -> Vec<EncodedInt> {
        line.split(',')
            .map(|x| x.parse().expect("The items should be positive integers"))
            .collect()
    }

    fn parse_json(array: &str) -> Vec<Vec<EncodedInt>> {
        array
            .trim_start_matches('[')
            .trim_end_matches(']')
            .split("],[")
            .map(Self::parse_line)
            .collect()
    }

    /// Reverse engineered version of the part of the script
    /// <http://www.nonograms.org/js/nonogram.min.059.js>
    /// that produces a nonogram solution for the given cyphered solution
    /// (it can be found in puzzle HTML in the form 'var d=[...]').
    #[allow(clippy::shadow_unrelated)]
    #[allow(unknown_lints)]
    #[allow(clippy::no_effect_underscore_binding)]
    pub fn decipher(&self) -> (Vec<String>, Vec<Vec<ColorId>>) {
        let cyphered = self.encoded();

        let x = &cyphered[1];
        let width = (x[0] % x[3] + x[1] % x[3] - x[2] % x[3]) as usize;

        let x = &cyphered[2];
        let height = (x[0] % x[3] + x[1] % x[3] - x[2] % x[3]) as usize;

        let x = &cyphered[3];
        let colors_number = (x[0] % x[3] + x[1] % x[3] - x[2] % x[3]) as usize;

        let x = &cyphered[4];
        let colors: Vec<_> = (0..colors_number)
            .map(|c| {
                let color_x = &cyphered[c + 5];
                let a = color_x[0] - x[1];
                let b = u32::from(color_x[1] - x[0]);
                let c = u32::from(color_x[2] - x[3]);
                let _unknown_flag = color_x[3] - a - x[2];
                let a = &format!("{:x}", a + 256)[1..];
                let b = &format!("{:x}", ((b + 256) << 8) + c)[1..];
                a.to_string() + b
            })
            .collect();

        let mut solution = vec![vec![0; width]; height];
        let z = colors_number + 5;
        let x = &cyphered[z];
        let solution_size = (x[0] % x[3] * (x[0] % x[3]) + x[1] % x[3] * 2 + x[2] % x[3]) as usize;

        let x = &cyphered[z + 1];
        for i in 0..solution_size {
            let y = &cyphered[z + 2 + i];
            let vv = y[0] - x[0] - 1;

            for j in 0..(y[1] - x[1]) {
                let v = (j + vv) as usize;
                let xx = y[3] - x[3] - 1;
                solution[xx as usize][v] = ColorId::from(y[2] - x[2]);
            }
        }

        (colors, solution)
    }

    pub fn encoded(&self) -> &[Vec<EncodedInt>] {
        &self.encoded
    }

    fn get_solution_matrix(&self) -> Vec<Vec<ColorId>> {
        let (_colors, solution_matrix) = self.decipher();
        let palette = self.get_palette();

        let mut mapping_cache = HashMap::new();
        solution_matrix
            .iter()
            .map(|row| {
                row.iter()
                    .map(|&item| {
                        *mapping_cache.entry(item).or_insert_with(|| {
                            palette
                                .id_by_name(&Self::color_name_by_id(item))
                                .unwrap_or(0)
                        })
                    })
                    .collect()
            })
            .collect()
    }

    fn color_name_by_id(id: ColorId) -> String {
        format!("color-{}", id)
    }
}

impl LocalReader for NonogramsOrg {}

impl Default for ParseError {
    fn default() -> Self {
        Self("Unknown parser error".to_string())
    }
}

impl NetworkReader for NonogramsOrg {
    fn read_remote(file_name: &str) -> Result<Self, ParseError> {
        product(&Self::URLS, &Self::PATHS)
            .iter()
            .first_ok(|(base_url, (_scheme, path))| {
                let url = format!("{}{}/i/{}", base_url, path, file_name);
                let content = Self::http_content(&url)?;
                Self::with_content(&content)
            })
    }
}

impl BoardParser for NonogramsOrg {
    fn with_content(content: &str) -> Result<Self, ParseError> {
        let json = Self::extract_encoded_json(content)
            .ok_or_else(|| ParseError("Not found cypher in HTML content".to_string()))?;

        Ok(Self {
            encoded: Self::parse_json(json),
        })
    }

    fn parse<B>(&self) -> Board<B>
    where
        B: Block,
    {
        let solution_matrix = self.get_solution_matrix();
        let (columns, rows) = clues_from_solution(&solution_matrix, 0);

        Board::with_descriptions_and_palette(rows, columns, Some(self.get_palette()))
    }

    fn infer_scheme(&self) -> PuzzleScheme {
        let (colors, _solution) = self.decipher();
        if colors.len() == 1 {
            assert_eq!(colors, ["000000"]);
            return PuzzleScheme::BlackAndWhite;
        }

        PuzzleScheme::MultiColor
    }
}

impl Paletted for NonogramsOrg {
    #[allow(clippy::cast_possible_truncation)]
    fn get_colors(&self) -> Vec<(String, char, String)> {
        let (colors, _solution) = self.decipher();
        colors
            .into_iter()
            .enumerate()
            // enumerating starts with 1
            .map(|(i, rgb)| (Self::color_name_by_id((i + 1) as ColorId), '?', rgb))
            .collect()
    }

    fn get_palette(&self) -> ColorPalette {
        let mut palette = ColorPalette::with_white("W");

        for (name, _dumb_symbol, value) in &self.get_colors() {
            let val = ColorValue::parse(value);
            palette.color_with_name_and_value(name, val);
        }

        palette
    }
}

#[derive(Debug)]
enum ParserKind {
    Toml,
    WebPbn,
    NonogramsOrg,
    Olsak,
    Simple,
}

pub struct DetectedParser {
    parser_kind: ParserKind,
    inner: Box<dyn Any>,
}

impl DetectedParser {
    fn cast<T>(&self) -> &T
    where
        T: BoardParser + 'static,
    {
        let expect_msg = format!("Parser should be created with {:?}", self.parser_kind);
        self.inner.downcast_ref::<T>().expect(&expect_msg)
    }
}

impl BoardParser for DetectedParser {
    fn with_content(content: &str) -> Result<Self, ParseError> {
        let trim_content = content.trim();
        Ok(if trim_content.starts_with("<?xml") {
            Self {
                parser_kind: ParserKind::WebPbn,
                inner: Box::new(WebPbn::with_content(content)?),
            }
        } else if ["<!DOCTYPE HTML", "<html", NonogramsOrg::CYPHER_PREFIX]
            .iter()
            .any(|&prefix| trim_content.starts_with(prefix))
        {
            Self {
                parser_kind: ParserKind::NonogramsOrg,
                inner: Box::new(NonogramsOrg::with_content(content)?),
            }
        } else {
            let lines: Vec<_> = trim_content.lines().map(str::trim).collect();
            if lines.contains(&"[clues]") {
                Self {
                    parser_kind: ParserKind::Toml,
                    inner: Box::new(MyFormat::with_content(content)?),
                }
            } else if lines.contains(&": rows") {
                Self {
                    parser_kind: ParserKind::Olsak,
                    inner: Box::new(OlsakParser::with_content(content)?),
                }
            } else {
                Self {
                    parser_kind: ParserKind::Simple,
                    inner: Box::new(SimpleParser::with_content(content)?),
                }
            }
        })
    }

    //noinspection RsTypeCheck
    fn parse<B>(&self) -> Board<B>
    where
        B: Block,
    {
        match self.parser_kind {
            ParserKind::Toml => self.cast::<MyFormat>().parse::<B>(),
            ParserKind::WebPbn => self.cast::<WebPbn>().parse::<B>(),
            ParserKind::NonogramsOrg => self.cast::<NonogramsOrg>().parse::<B>(),
            ParserKind::Olsak => self.cast::<OlsakParser>().parse::<B>(),
            ParserKind::Simple => self.cast::<SimpleParser>().parse::<B>(),
        }
    }

    fn infer_scheme(&self) -> PuzzleScheme {
        match self.parser_kind {
            ParserKind::Toml => self.cast::<MyFormat>().infer_scheme(),
            ParserKind::WebPbn => self.cast::<WebPbn>().infer_scheme(),
            ParserKind::NonogramsOrg => self.cast::<NonogramsOrg>().infer_scheme(),
            ParserKind::Olsak => self.cast::<OlsakParser>().infer_scheme(),
            ParserKind::Simple => self.cast::<SimpleParser>().infer_scheme(),
        }
    }
}

impl fmt::Debug for DetectedParser {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
        let inner = match self.parser_kind {
            ParserKind::Toml => format!("{:?}", self.cast::<MyFormat>()),
            ParserKind::WebPbn => format!("{:?}", self.cast::<WebPbn>()),
            ParserKind::NonogramsOrg => format!("{:?}", self.cast::<NonogramsOrg>()),
            ParserKind::Olsak => format!("{:?}", self.cast::<OlsakParser>()),
            ParserKind::Simple => format!("{:?}", self.cast::<SimpleParser>()),
        };

        f.debug_struct("DetectedParser")
            .field("parser_kind", &self.parser_kind)
            .field("inner", &inner)
            .finish()
    }
}

#[derive(Debug, PartialEq)]
pub struct OlsakColor {
    pub block_name: String,
    pub symbol: char,
    pub rgb: String,
    pub name: String,
}

impl OlsakColor {
    ///```
    /// # use nonogrid::parser::OlsakColor;
    ///
    /// let s = "0:   #FFFFFF   white";
    /// let color = OlsakColor::parse(s);
    /// assert_eq!(color.block_name, "0");
    /// assert_eq!(color.symbol, ' ');
    /// assert_eq!(color.rgb, "#FFFFFF");
    /// assert_eq!(color.name, "white");
    ///
    /// let s = "n:%  #00B000   green";
    /// let color = OlsakColor::parse(s);
    /// assert_eq!(color.block_name, "n");
    /// assert_eq!(color.symbol, '%');
    /// assert_eq!(color.rgb, "#00B000");
    /// assert_eq!(color.name, "green");
    /// ```
    pub fn parse(color_def: &str) -> Self {
        let parts: Vec<_> = color_def.split_whitespace().collect();
        let block_name_and_symbol: Vec<_> = parts[0].split(':').collect();

        let block_name = block_name_and_symbol[0];
        let symbol = block_name_and_symbol[1].chars().next().unwrap_or(' ');

        Self {
            block_name: block_name.to_string(),
            symbol,
            rgb: parts[1].to_string(),
            name: parts[2].to_string(),
        }
    }
}

#[derive(Debug)]
struct OlsakParser {
    rows: Vec<Vec<String>>,
    columns: Vec<Vec<String>>,
    colors: HashMap<String, OlsakColor>,
}

impl From<String> for ParseError {
    fn from(err: String) -> Self {
        Self(err)
    }
}

impl BoardParser for OlsakParser {
    fn with_content(content: &str) -> Result<Self, ParseError>
    where
        Self: Sized,
    {
        let names = [": rows", ": columns", "#d"];
        let mut sections = split_sections(content, &names, false, None)?;
        let mut splitted: HashMap<_, _> = sections
            .iter()
            .map(|(&name, lines)| {
                (
                    name,
                    lines
                        .iter()
                        .map(|&line| line.split_whitespace().map(ToString::to_string).collect())
                        .collect(),
                )
            })
            .collect();

        let colors = sections.remove(names[2]).unwrap_or_default();

        Ok(Self {
            rows: splitted.remove(names[0]).expect("Rows section not found"),
            columns: splitted
                .remove(names[1])
                .expect("Columns section not found"),
            colors: colors
                .into_iter()
                .map(|line| {
                    let color = OlsakColor::parse(line);
                    (color.block_name.clone(), color)
                })
                .collect(),
        })
    }

    fn parse<B>(&self) -> Board<B>
    where
        B: Block,
    {
        let palette = self.get_palette();
        Board::with_descriptions_and_palette(
            self.parse_clues(&self.rows, &palette),
            self.parse_clues(&self.columns, &palette),
            Some(palette),
        )
    }

    fn infer_scheme(&self) -> PuzzleScheme {
        if !self.colors.is_empty() {
            let mut names: Vec<_> = self.colors.values().map(|x| &x.name).collect();
            names.sort_unstable();

            if names != ["black", "white"] {
                return PuzzleScheme::MultiColor;
            }
        }

        PuzzleScheme::BlackAndWhite
    }
}

impl OlsakParser {
    fn parse_block<B>(&self, block: &str, palette: &ColorPalette) -> B
    where
        B: Block,
    {
        let mut as_chars = block.chars();
        let value_color_pos = as_chars.position(|c| !c.is_digit(10));

        #[allow(clippy::option_if_let_else)]
        let (value, block_color) = if let Some(pos) = value_color_pos {
            let (value, color) = block.split_at(pos);
            (value, Some(color))
        } else {
            (block, None)
        };

        let color_name = block_color
            .and_then(|block_color| self.colors.get(block_color))
            .map(|color| &color.name);

        let color_id = color_name.and_then(|name| palette.id_by_name(name));
        B::from_str_and_color(value, color_id)
    }

    fn parse_line<B>(&self, descriptions: &[String], palette: &ColorPalette) -> Description<B>
    where
        B: Block,
    {
        Description::new(
            descriptions
                .iter()
                .map(|block| self.parse_block(block, palette))
                .collect(),
        )
    }

    fn parse_clues<B>(
        &self,
        descriptions: &[Vec<String>],
        palette: &ColorPalette,
    ) -> Vec<Description<B>>
    where
        B: Block,
    {
        descriptions
            .iter()
            .map(|line| self.parse_line(line, palette))
            .collect()
    }
}

impl Paletted for OlsakParser {
    fn get_colors(&self) -> Vec<(String, char, String)> {
        self.colors
            .values()
            .map(|x| (x.name.clone(), x.symbol, x.rgb.clone()))
            .collect()
    }

    fn get_palette(&self) -> ColorPalette {
        self.default_palette("white", "black")
    }
}

#[derive(Debug)]
/// This kind of parser only valid for Black-and-White puzzles.
/// See the full list of formats here <https://webpbn.com/export.cgi>.
struct SimpleParser {
    rows: Vec<Vec<String>>,
    columns: Vec<Vec<String>>,
}

impl From<ParseIntError> for ParseError {
    fn from(err: ParseIntError) -> Self {
        Self(format!("{}", err))
    }
}

impl SimpleParser {
    fn parse_clues<B>(descriptions: &[Vec<String>]) -> Vec<Description<B>>
    where
        B: Block,
    {
        descriptions
            .iter()
            .map(|line| {
                Description::new(
                    line.iter()
                        .filter_map(|block| {
                            let block = block.trim();
                            if block.is_empty() {
                                None
                            } else {
                                Some(B::from_str_and_color(block, None))
                            }
                        })
                        .collect(),
                )
            })
            .collect()
    }

    fn split_into_blocks(lines: &[&str]) -> Vec<Vec<String>> {
        lines
            .iter()
            .filter_map(|&line| {
                if line.is_empty() {
                    None
                } else {
                    Some(
                        // 'ish' and 'ss' has comma-separated blocks
                        line.split(&[' ', ','][..])
                            .map(ToString::to_string)
                            .collect(),
                    )
                }
            })
            .collect()
    }

    fn remove_comments(text: &str) -> String {
        let lines: Vec<_> = text
            .lines()
            .map(|line| {
                // 'ish', 'mk' and 'syro' can have '#' comments
                // 'makhorin' has a '*' comments and '&' rows-columns delimiter
                if line.starts_with(&['#', '*'][..]) || line == "&" {
                    ""
                } else {
                    // every line in 'syro' terminated with '0' block
                    line.trim_end_matches(" 0")
                }
            })
            .collect();
        lines.join("\n").trim().to_string()
    }
}

impl BoardParser for SimpleParser {
    fn with_content(content: &str) -> Result<Self, ParseError>
    where
        Self: Sized,
    {
        let symbols: HashSet<_> = content
            .lines()
            .flat_map(|line| line.trim().chars())
            .collect();
        let solution_matrix_chars = ['0', '1'].iter().copied().collect();

        // only '1' and '0' as the solution matrix
        if symbols == solution_matrix_chars {
            let solution_matrix: Vec<_> = content
                .lines()
                .map(|line| {
                    line.chars()
                        .map(|ch| ch.to_digit(10).expect("not a decimal digit"))
                        .collect()
                })
                .collect();
            let (columns, rows) = clues_from_solution(&solution_matrix, 0);
            return Ok(Self {
                rows: rows
                    .into_iter()
                    .map(|d| d.vec.iter().map(BinaryBlock::to_string).collect())
                    .collect(),
                columns: columns
                    .into_iter()
                    .map(|d| d.vec.iter().map(BinaryBlock::to_string).collect())
                    .collect(),
            });
        }

        let content = Self::remove_comments(content);

        let (rows, columns) = {
            // 'faase' and 'ss' formats
            let names = ["rows", "columns"];

            let mut sections = split_sections(&content, &names, false, None);
            if let Ok(sections) = sections.as_mut() {
                (
                    sections.remove(names[0]).expect("Cannot find rows"),
                    sections.remove(names[1]).expect("Cannot find rows"),
                )
            } else {
                // try to find two blocks separated by empty line
                // 'ish', 'keen', 'makhorin', 'syro' formats
                let rows_section = "rows go first";
                let columns_section = [""];
                let mut sections =
                    split_sections(&content, &columns_section, true, Some(rows_section));

                if let Ok(sections) = sections.as_mut() {
                    (
                        sections.remove(rows_section).expect("Cannot find rows"),
                        sections
                            .remove(columns_section[0])
                            .expect("Cannot find columns"),
                    )
                } else {
                    // no empty lines, 'nin' format
                    let mut content_iter = content.lines();
                    let dimensions: Result<Vec<usize>, _> = content_iter
                        .next()
                        .expect("Empty content")
                        .split_whitespace()
                        .map(str::parse)
                        .collect();

                    let dimensions = dimensions?;
                    if dimensions.len() == 2 {
                        let (width, height) = (dimensions[0], dimensions[1]);
                        let rows = content_iter.by_ref().take(height).collect();
                        let columns = content_iter.take(width).collect();
                        (rows, columns)
                    } else {
                        unimplemented!("This puzzle format is not supported")
                    }
                }
            }
        };

        Ok(Self {
            rows: Self::split_into_blocks(&rows),
            columns: Self::split_into_blocks(&columns),
        })
    }

    fn parse<B>(&self) -> Board<B>
    where
        B: Block,
    {
        let palette = self.get_palette();
        Board::with_descriptions_and_palette(
            Self::parse_clues(&self.rows),
            Self::parse_clues(&self.columns),
            Some(palette),
        )
    }

    fn infer_scheme(&self) -> PuzzleScheme {
        PuzzleScheme::BlackAndWhite
    }
}

impl Paletted for SimpleParser {
    fn get_colors(&self) -> Vec<(String, char, String)> {
        vec![]
    }

    fn get_palette(&self) -> ColorPalette {
        self.default_palette("white", "black")
    }
}

#[cfg(test)]
#[cfg(feature = "ini")]
mod tests {
    use crate::block::{base::color::ColorPalette, binary::BinaryBlock, Description};

    use super::{BoardParser, MyFormat, Paletted, PuzzleScheme};

    const fn block(n: usize) -> BinaryBlock {
        BinaryBlock(n)
    }

    fn palette() -> ColorPalette {
        ColorPalette::with_white_and_black("W", "B")
    }

    #[test]
    fn parse_single() {
        assert_eq!(
            MyFormat::parse_clues(&String::from("1"), &palette()),
            vec![Description::new(vec![block(1)])]
        )
    }

    #[test]
    fn parse_two_lines() {
        assert_eq!(
            MyFormat::parse_clues(&String::from("1\n2"), &palette()),
            vec![
                Description::new(vec![block(1)]),
                Description::new(vec![block(2)])
            ]
        )
    }

    #[test]
    fn parse_two_rows_same_line() {
        assert_eq!(
            MyFormat::parse_clues(&String::from("1, 2"), &palette()),
            vec![
                Description::new(vec![block(1)]),
                Description::new(vec![block(2)])
            ]
        )
    }

    #[test]
    fn parse_two_rows_with_commas() {
        assert_eq!(
            MyFormat::parse_clues(&String::from("1, 2,\n3"), &palette()),
            vec![
                Description::new(vec![block(1)]),
                Description::new(vec![block(2)]),
                Description::new(vec![block(3)]),
            ]
        )
    }

    #[test]
    fn parse_two_blocks() {
        assert_eq!(
            MyFormat::parse_clues(&String::from("1 2"), &palette()),
            vec![Description::new(vec![block(1), block(2)]),]
        )
    }

    #[test]
    fn parse_quotes() {
        assert_eq!(
            MyFormat::parse_clues(&String::from("'1 2'"), &palette()),
            vec![Description::new(vec![block(1), block(2)]),]
        )
    }

    #[test]
    fn parse_double_quotes() {
        assert_eq!(
            MyFormat::parse_clues(&String::from("1 2\n\"3 4\"\n"), &palette()),
            vec![
                Description::new(vec![block(1), block(2)]),
                Description::new(vec![block(3), block(4)]),
            ]
        )
    }

    #[test]
    fn parse_comment_end_of_line() {
        assert_eq!(
            MyFormat::parse_clues(&String::from("1 2  # the comment"), &palette()),
            vec![Description::new(vec![block(1), block(2)]),]
        )
    }

    #[test]
    fn parse_comment_semicolon() {
        assert_eq!(
            MyFormat::parse_clues(&String::from("1 2  ; another comment"), &palette()),
            vec![Description::new(vec![block(1), block(2)]),]
        )
    }

    #[test]
    fn parse_comments_in_the_middle() {
        assert_eq!(
            MyFormat::parse_clues(
                &String::from("1 2 \n # the multi-line \n # comment \n 3, 4"),
                &palette(),
            ),
            vec![
                Description::new(vec![block(1), block(2)]),
                Description::new(vec![block(3)]),
                Description::new(vec![block(4)]),
            ]
        )
    }

    #[test]
    fn infer_black_and_white_no_colors_section() {
        let s = r"
        [clues]
        rows = '1'
        columns = '1'
        ";

        assert_eq!(
            MyFormat::with_content(s).unwrap().infer_scheme(),
            PuzzleScheme::BlackAndWhite
        )
    }

    #[test]
    fn infer_black_and_white_empty_colors_section() {
        let s = r"
        [clues]
        rows = '1'
        columns = '1'

        [colors]
        ";

        assert_eq!(
            MyFormat::with_content(s).unwrap().infer_scheme(),
            PuzzleScheme::BlackAndWhite
        )
    }

    #[test]
    fn infer_black_and_white_empty_defs_in_colors_section() {
        let s = r"
        [clues]
        rows = '1'
        columns = '1'

        [colors]
        defs = []
        ";

        assert_eq!(
            MyFormat::with_content(s).unwrap().infer_scheme(),
            PuzzleScheme::BlackAndWhite
        )
    }

    #[test]
    fn infer_multi_color() {
        let s = r"
        [clues]
        rows = '1'
        columns = '1'

        [colors]
        defs = ['g=(0, 204, 0) %']
        ";

        assert_eq!(
            MyFormat::with_content(s).unwrap().infer_scheme(),
            PuzzleScheme::MultiColor
        )
    }

    #[test]
    fn parse_colors() {
        let s = r"
        [clues]
        rows = '1'
        columns = '1g'

        [colors]
        defs = ['g=(0, 204, 0) %']
        ";

        let f = MyFormat::with_content(s).unwrap();
        let colors = vec![("g".to_string(), '%', "0, 204, 0".to_string())];
        assert_eq!(f.get_colors(), colors)
    }
}