//! Parse INI-style configuration files.

/**
 * Copyright (c) 2021  Peter Pentchev <roam@ringlet.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */
use std::collections::HashMap;
use std::error;
use std::fs;
use std::io::{self, Read};

use crate::backend;
use crate::defs;

use expect_exit::ExpectedResult;

/// A backend type for parsing INI-style configuration files.
#[derive(Debug)]
pub struct IniBackend<'a> {
    /// Configuration settings, e.g. filename and section.
    pub config: &'a defs::Config,
}

#[derive(Debug)]
struct State {
    re_comment: regex::Regex,
    re_section: regex::Regex,
    re_variable: regex::Regex,
    filename: String,
    first_section: Option<String>,
    section: String,
    cont: Option<(String, String)>,
    found: bool,
}

impl State {
    fn feed_line(
        self,
        line: &str,
        res: &mut HashMap<String, HashMap<String, String>>,
    ) -> Result<Self, Box<dyn error::Error>> {
        match self.cont {
            Some((name, value)) => match line.strip_suffix('\\') {
                Some(stripped) => Ok(Self {
                    cont: Some((name, format!("{}{}", value, stripped))),
                    ..self
                }),
                None => {
                    match res.get_mut(&self.section) {
                        None => panic!("Internal error: no data for section {}", self.section),
                        Some(data) => {
                            data.insert(name, format!("{}{}", value, line));
                        }
                    };
                    Ok(Self { cont: None, ..self })
                }
            },
            None => match self.re_comment.is_match(line) {
                true => Ok(self),
                false => match self.re_section.captures(line) {
                    Some(caps) => {
                        let name = &caps["name"];
                        match res.contains_key(name) {
                            true => (),
                            false => {
                                res.insert(name.to_owned(), HashMap::new());
                            }
                        };
                        Ok(Self {
                            first_section: match self.first_section.is_none() && !self.found {
                                true => Some(name.to_owned()),
                                false => self.first_section,
                            },
                            section: name.to_owned(),
                            found: true,
                            ..self
                        })
                    }
                    None => match self.re_variable.captures(line) {
                        Some(caps) => {
                            let name = &caps["name"];
                            let value = &caps["value"];
                            let cont = caps.name("cont").is_some();
                            if !cont {
                                match res.get_mut(&self.section) {
                                    None => panic!(
                                        "Internal error: no data for section {}",
                                        self.section
                                    ),
                                    Some(data) => {
                                        data.insert(name.to_owned(), value.to_owned());
                                    }
                                }
                            }
                            Ok(Self {
                                cont: match cont {
                                    false => None,
                                    true => Some((name.to_owned(), value.to_owned())),
                                },
                                found: true,
                                ..self
                            })
                        }
                        None => Err(defs::ConfgetError::boxed(format!(
                            "Unexpected line in {}: {}",
                            self.filename, line
                        ))),
                    },
                },
            },
        }
    }
}

static RE_COMMENT: &str = r"(?x) ^ \s* (?: [\#;] .* )?  $ ";

static RE_SECTION: &str = r"(?x)
    ^ \s*
    \[ \s*
    (?P<name> [^\]]+? )
    \s* \]
    \s* $ ";

static RE_VARIABLE: &str = r"(?x)
    ^ \s*
    (?P<name> [^\s=]+ )
    \s* = \s*
    (?P<value> .*? )
    \s*
    (?P<cont> [\\] )?
    $ ";

impl backend::Backend for IniBackend<'_> {
    fn read_file(&self) -> Result<backend::DataRead, Box<dyn error::Error>> {
        let filename = self
            .config
            .filename
            .as_ref()
            .ok_or_else(|| defs::ConfgetError::boxed("No filename supplied".to_owned()))?;
        let mut res = HashMap::new();
        res.insert("".to_owned(), HashMap::new());

        let init_state = State {
            re_comment: regex::Regex::new(RE_COMMENT).unwrap(),
            re_section: regex::Regex::new(RE_SECTION).unwrap(),
            re_variable: regex::Regex::new(RE_VARIABLE).unwrap(),
            filename: filename.to_string(),
            first_section: match self.config.section_specified {
                true => Some(self.config.section.to_owned()),
                false => None,
            },
            section: "".to_owned(),
            cont: None,
            found: false,
        };

        fn get_file_lines(filename: &str) -> Result<Vec<String>, Box<dyn error::Error>> {
            let mut contents = String::new();
            match filename == "-" {
                true => {
                    io::stdin().lock().read_to_string(&mut contents)?;
                }
                false => {
                    let mut file = fs::File::open(&filename)
                        .expect_result(|| format!("Error opening {} for reading", filename))?;
                    file.read_to_string(&mut contents)
                        .expect_result(|| format!("Could not read from {}", filename))?;
                }
            };
            Ok(contents.lines().map(|line| line.to_owned()).collect())
        }

        let final_state = get_file_lines(filename)?
            .iter()
            .try_fold(init_state, |state, line| state.feed_line(line, &mut res))?;
        match final_state.cont.is_some() {
            true => Err(defs::ConfgetError::boxed(format!(
                "Line continuation on the last line of {}",
                filename
            ))),
            false => Ok((
                res,
                match final_state.first_section {
                    Some(section) => section,
                    None => self.config.section.clone(),
                },
            )),
        }
    }
}
