use std::collections::HashMap;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::io::BufRead;

#[derive(Debug, PartialEq)]
/// The different states of the parser's state machine.
enum ParserState {
    Start,
    ParseKey,
    AwaitAssignment,
    AwaitValue,
    ParseValue,
}

#[derive(Debug, PartialEq)]
/// The different errors that parse_line() can return.
enum ParseLineError {
    BeginsWithAssignment,
    IllegalChar(char),
}

impl Display for ParseLineError {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            ParseLineError::BeginsWithAssignment => write!(f, "line begins with assignment"),
            ParseLineError::IllegalChar(c) => write!(f, "illegal character in key: `{}`", c),
        }
    }
}

/// Given a [`BufRead`], parse each line and return them as key-value pairs.
pub fn parse<R: BufRead>(reader: R) -> Result<HashMap<String, String>, String> {
    let mut vars = HashMap::new();

    for (line_number, mut line) in reader.lines().map(|l| l.unwrap()).enumerate() {
        if line.ends_with('\r') {
            line.pop();
        }

        match parse_line(&line) {
            Ok(Some((k, v))) => {
                vars.insert(k.to_owned(), v.to_owned());
            }
            Err(reason) => {
                return Err(format!("error on line {}: {}", line_number + 1, reason));
            }
            _ => (),
        };
    }

    Ok(vars)
}

/// Use a state machine to parse the given line.
fn parse_line(line: &str) -> Result<Option<(&str, &str)>, ParseLineError> {
    let mut k: usize = 0;
    let mut v: usize = 0;
    let mut state = ParserState::Start;

    // call trim() to simplify parsing
    let line = line.trim();

    for (i, c) in line.chars().enumerate() {
        match state {
            // interpret the first character
            ParserState::Start => match c {
                'A'..='Z' | '0'..='9' | '_' => state = ParserState::ParseKey,
                '#' => break,
                '=' => return Err(ParseLineError::BeginsWithAssignment),
                _ => return Err(ParseLineError::IllegalChar(c)),
            },
            // parse the variable name
            ParserState::ParseKey => match c {
                ' ' | '\t' => {
                    k = i;
                    state = ParserState::AwaitAssignment
                }
                '=' => {
                    k = i;
                    state = ParserState::AwaitValue;
                }
                'A'..='Z' | '0'..='9' | '_' => (),
                _ => return Err(ParseLineError::IllegalChar(c)),
            },
            // parse whitespace leading to "="
            ParserState::AwaitAssignment => match c {
                '=' => {
                    state = ParserState::AwaitValue;
                }
                ' ' | '\t' => (),
                _ => return Err(ParseLineError::IllegalChar(c)),
            },
            // parse whitespace after "="
            ParserState::AwaitValue => match c {
                ' ' | '\t' => (),
                _ => {
                    v = i;
                    state = ParserState::ParseValue;
                    break;
                }
            },
            _ => (),
        }
    }

    // ending state
    match state {
        // the line is empty or whitespace only
        ParserState::Start => Ok(None),
        // the line contains just a key
        ParserState::ParseKey => Ok(Some((&line, ""))),
        // the line contains just a key and maybe an empty assignment
        ParserState::AwaitAssignment | ParserState::AwaitValue => {
            Ok(Some((&line[0..k], "")))
        }
        // the line contains a complete key-value pair
        ParserState::ParseValue => Ok(Some((&line[0..k], &line[v..]))),
    }
}

#[cfg(test)]
mod test_parse_line {
    use super::{parse_line, ParseLineError};

    #[test]
    fn empty_line() {
        assert_eq!(parse_line(""), Ok(None));
    }

    #[test]
    fn empty_line_whitespace() {
        assert_eq!(parse_line("       "), Ok(None));
    }

    #[test]
    fn key_only() {
        assert_eq!(parse_line("KEY"), Ok(Some(("KEY", ""))));
    }

    #[test]
    fn key_only_illegal_chars() {
        assert_eq!(
            parse_line("key").unwrap_err(),
            ParseLineError::IllegalChar('k')
        );
    }

    #[test]
    fn key_illegal_pair() {
        assert_eq!(
            parse_line("key = illegal").unwrap_err(),
            ParseLineError::IllegalChar('k')
        );
    }

    #[test]
    fn key_only_numeric() {
        assert_eq!(parse_line("1234567890"), Ok(Some(("1234567890", ""))));
    }

    #[test]
    fn key_only_underscore() {
        assert_eq!(parse_line("_"), Ok(Some(("_", ""))));
    }

    #[test]
    fn key_only_all_legal_chars() {
        assert_eq!(
            parse_line("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"),
            Ok(Some(("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_", "")))
        );
    }

    #[test]
    fn key_only_leading_whitespace() {
        assert_eq!(parse_line("       KEY"), Ok(Some(("KEY", ""))));
    }

    #[test]
    fn key_only_trailing_whitespace() {
        assert_eq!(parse_line("KEY     "), Ok(Some(("KEY", ""))));
    }

    #[test]
    fn key_only_whitespace() {
        assert_eq!(parse_line("       KEY     "), Ok(Some(("KEY", ""))));
    }

    #[test]
    fn val_only() {
        assert!(parse_line("=val").is_err());
    }

    #[test]
    fn val_only_leading_whitespace() {
        assert!(parse_line("      =val").is_err());
    }

    #[test]
    fn val_only_trailing_whitespace() {
        assert!(parse_line("=val             ").is_err());
    }

    #[test]
    fn val_only_surrounding_whitespace() {
        assert!(parse_line("        =val       ").is_err());
    }

    #[test]
    fn val_only_whitespace() {
        assert!(parse_line("        =    val       ").is_err());
    }

    #[test]
    fn key_val() {
        assert_eq!(parse_line("KEY=VAL"), Ok(Some(("KEY", "VAL"))));
    }

    #[test]
    fn key_val_illegal_chars_in_val() {
        assert_eq!(parse_line("KEY=val"), Ok(Some(("KEY", "val"))));
    }

    #[test]
    fn key_val_illegal_whitespace() {
        assert_eq!(
            parse_line("K   Y=VAL").unwrap_err(),
            ParseLineError::IllegalChar('Y')
        );
    }

    #[test]
    fn key_val_leading_whitespace() {
        assert_eq!(parse_line("       KEY=VAL"), Ok(Some(("KEY", "VAL"))));
    }

    #[test]
    fn key_val_trailing_whitespace() {
        assert_eq!(parse_line("KEY=VAL     "), Ok(Some(("KEY", "VAL"))));
    }

    #[test]
    fn key_val_surrounding_whitespace() {
        assert_eq!(parse_line("       KEY=VAL     "), Ok(Some(("KEY", "VAL"))));
    }

    #[test]
    fn key_val_between_whitespace() {
        assert_eq!(parse_line("KEY   =   VAL"), Ok(Some(("KEY", "VAL"))));
    }

    #[test]
    fn key_val_leading_between_whitespace() {
        assert_eq!(parse_line("KEY   =VAL"), Ok(Some(("KEY", "VAL"))));
    }

    #[test]
    fn key_val_trailing_between_whitespace() {
        assert_eq!(parse_line("KEY=        VAL"), Ok(Some(("KEY", "VAL"))));
    }

    #[test]
    fn key_val_whitespace() {
        assert_eq!(
            Ok(Some(("KEY", "VAL"))),
            parse_line("   KEY    =        VAL  ")
        );
    }

    #[test]
    fn key_val_empty() {
        assert_eq!(parse_line("KEY="), Ok(Some(("KEY", ""))));
    }

    #[test]
    fn key_val_empty_leading_between_whitespace() {
        assert_eq!(parse_line("KEY     ="), Ok(Some(("KEY", ""))));
    }

    #[test]
    fn key_val_empty_trailing_whitespace() {
        assert_eq!(parse_line("KEY=      "), Ok(Some(("KEY", ""))));
    }

    #[test]
    fn key_val_empty_leading_whitespace() {
        assert_eq!(parse_line("       KEY="), Ok(Some(("KEY", ""))));
    }

    #[test]
    fn key_val_empty_whitespace() {
        assert_eq!(parse_line("     KEY      =    "), Ok(Some(("KEY", ""))));
    }

    #[test]
    fn key_val_raw_is_removed() {
        assert_eq!(parse_line("KEY=="), Ok(Some(("KEY", "="))));
    }

    #[test]
    fn key_val_raw_is_removed_with_whitespace() {
        assert_eq!(parse_line("KEY == "), Ok(Some(("KEY", "="))));
    }
    
    #[test]
    fn comment() {
        assert_eq!(parse_line("# comment"), Ok(None));
    }

    #[test]
    fn comment_with_whitespace() {
        assert_eq!(parse_line("      # comment       "), Ok(None));
    }

    #[test]
    fn dollar_sign_is_removed() {
        assert_eq!(
            parse_line("$KEY=val").unwrap_err(),
            ParseLineError::IllegalChar('$')
        );
    }
}

#[cfg(test)]
mod test_parse {
    use super::parse;

    #[test]
    fn empty_line() {
        let dotenv = "".as_bytes();
        let result = parse(dotenv).unwrap();

        assert!(result.is_empty());
    }

    #[test]
    fn three_empty_lines() {
        let dotenv = "\n\n".as_bytes();
        let result = parse(dotenv).unwrap();

        assert!(result.is_empty());
    }

    #[test]
    fn three_whitespace_lines() {
        let dotenv = "    \n  \n        ".as_bytes();
        let result = parse(dotenv).unwrap();

        assert!(result.is_empty());
    }

    #[test]
    fn comment_line() {
        let dotenv = "# this is a comment".as_bytes();
        let result = parse(dotenv).unwrap();

        assert!(result.is_empty());
    }

    #[test]
    fn three_comment_lines() {
        let dotenv = "  #COMMENT\n# look on my works, ye mighty!\n#####NEW SECTION#####".as_bytes();
        let result = parse(dotenv).unwrap();

        assert!(result.is_empty());
    }

    #[test]
    fn one_line() {
        let dotenv = "FOO=BAR".as_bytes();
        let result = parse(dotenv).unwrap();

        assert_eq!(result.len(), 1);
        assert_eq!(result.get("FOO").unwrap(), "BAR");
    }

    #[test]
    fn remove_raw_processing() {
        let dotenv = "FOO==BAR".as_bytes();
        let result = parse(dotenv).unwrap();

        assert_eq!(result.len(), 1);
        assert_eq!(result.get("FOO").unwrap(), "=BAR");
    }

    #[test]
    fn two_lines() {
        let dotenv = "FOO=BAR\nBOP=BAZ".as_bytes();
        let result = parse(dotenv).unwrap();

        assert_eq!(result.len(), 2);
        assert_eq!(result.get("FOO").unwrap(), "BAR");
        assert_eq!(result.get("BOP").unwrap(), "BAZ");
    }

    #[test]
    fn three_lines() {
        let dotenv = "FOO=BAR\nBOP=3344\nDUCK=https://duckduckgo.com/".as_bytes();
        let result = parse(dotenv).unwrap();

        assert_eq!(result.len(), 3);
        assert_eq!(result.get("FOO").unwrap(), "BAR");
        assert_eq!(result.get("BOP").unwrap(), "3344");
        assert_eq!(result.get("DUCK").unwrap(), "https://duckduckgo.com/");
    }

    #[test]
    fn three_lines_with_comment() {
        let dotenv = "FOO=BAR\n# search engine\nDUCK=https://duckduckgo.com/".as_bytes();
        let result = parse(dotenv).unwrap();

        assert_eq!(result.len(), 2);
        assert_eq!(result.get("FOO").unwrap(), "BAR");
        assert_eq!(result.get("DUCK").unwrap(), "https://duckduckgo.com/");
    }

    #[test]
    fn example_1() {
        let dotenv = "# My database configuration\n\nDB_PORT=5432\nDB_HOSTNAME=localhost\nDB_USER=postgres\nDB_PASSWORD=postgres\n".as_bytes();
        let result = parse(dotenv).unwrap();

        assert_eq!(result.len(), 4);
        assert_eq!(result.get("DB_PORT").unwrap(), "5432");
        assert_eq!(result.get("DB_HOSTNAME").unwrap(), "localhost");
        assert_eq!(result.get("DB_USER").unwrap(), "postgres");
        assert_eq!(result.get("DB_PASSWORD").unwrap(), "postgres");
    }

    #[test]
    fn example_2() {
        let dotenv =
            "  #------SECTION 1---------#\n\n\nDEBUG\n FILE = /home/user01/test.db      \n"
                .as_bytes();
        let result = parse(dotenv).unwrap();

        assert_eq!(result.len(), 2);
        assert_eq!(result.get("DEBUG").unwrap(), "");
        assert_eq!(result.get("FILE").unwrap(), "/home/user01/test.db");
    }

    #[test]
    fn mixed_example() {
        let dotenv = "#### Minecraft Server ####\n\n\nPORT=25565\nHOST=0.0.0.0\nMOTD=* * * HELLO FANS OF MINECRAFT * * * \nPASSWORD=abc123 def456\n".as_bytes();
        let result = parse(dotenv).unwrap();

        assert_eq!(result.len(), 4);
        assert_eq!(result.get("PORT").unwrap(), "25565");
        assert_eq!(result.get("HOST").unwrap(), "0.0.0.0");
        assert_eq!(
            result.get("MOTD").unwrap(),
            "* * * HELLO FANS OF MINECRAFT * * *"
        );
        assert_eq!(result.get("PASSWORD").unwrap(), "abc123 def456");
    }

    #[test]
    fn illegal_char_error_on_line_1() {
        let dotenv = "database=localhost\nPORT=3344".as_bytes();
        let err = parse(dotenv).unwrap_err();
        assert_eq!(err, "error on line 1: illegal character in key: `d`");
    }

    #[test]
    fn illegal_char_error_on_line_2() {
        let dotenv = "DATABASE=localhost\nport=3344".as_bytes();
        let err = parse(dotenv).unwrap_err();
        assert_eq!(err, "error on line 2: illegal character in key: `p`");
    }

    #[test]
    fn begins_with_assignment_error_on_line_1() {
        let dotenv = "=localhost\nPORT=3344".as_bytes();
        let err = parse(dotenv).unwrap_err();
        assert_eq!(err, "error on line 1: line begins with assignment");
    }

    #[test]
    fn begins_with_assignment_error_on_line_2() {
        let dotenv = "DB=localhost\n=3344".as_bytes();
        let err = parse(dotenv).unwrap_err();
        assert_eq!(err, "error on line 2: line begins with assignment");
    }
}
