/*
Copyright 2022-2023, Savanni D'Gerinel <savanni@luminescent-dreams.com>

This file is part of the Luminescent Dreams Tools.

Luminescent Dreams Tools is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

Luminescent Dreams Tools is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with Luminescent Dreams Tools. If not, see <https://www.gnu.org/licenses/>.
*/

use crate::{hex::AxialAddr, Error};
use nom::{
    bytes::complete::tag,
    character::complete::alphanumeric1,
    multi::many1,
    sequence::{delimited, separated_pair},
    Finish, IResult, Parser,
};
use std::{
    collections::{hash_map::Entry, HashMap},
    path::PathBuf,
};

#[derive(Clone, Debug, PartialEq)]
pub struct Map<A> {
    cells: HashMap<AxialAddr, A>,
}

impl<A: Default + Clone> Map<A> {
    pub fn new_hexagonal(radius: usize) -> Map<A> {
        let cells = vec![(AxialAddr::origin(), Default::default())]
            .into_iter()
            .chain(
                AxialAddr::origin()
                    .addresses(radius)
                    .map(|addr| (addr, Default::default())),
            )
            .collect::<HashMap<AxialAddr, A>>();
        Map { cells }
    }

    pub fn contains(&self, coordinate: AxialAddr) -> bool {
        self.cells.contains_key(&coordinate)
    }

    pub fn cell(&mut self, addr: AxialAddr) -> Entry<AxialAddr, A> {
        self.cells.entry(addr)
    }

    pub fn get(&self, addr: &AxialAddr) -> Option<&A> {
        self.cells.get(addr)
    }
}

pub fn read_file<A: Default + From<String>>(path: PathBuf) -> Result<Map<A>, Error> {
    let data = std::fs::read_to_string(path)?;
    Ok(parse_data(data.lines()))
}

pub fn write_file<A: Clone>(path: PathBuf, map: Map<A>) -> Result<(), Error>
where
    String: From<A>,
{
    std::fs::write(path, write_data(map).join("\n")).map_err(Error::from)
}

pub fn parse_data<'a, A: Default + From<String>>(
    data: impl Iterator<Item = &'a str> + 'a,
) -> Map<A> {
    fn parse_line<A: From<String>>(input: &str) -> Result<(AxialAddr, A), nom::error::Error<&str>> {
        fn parse<A: From<String>>(input: &str) -> IResult<&str, (AxialAddr, A)> {
            let (input, addr) = parse_address(input)?;
            let (input, value) = cell_value::<A>(input)?;
            Ok((input, (addr, value)))
        }

        parse(input).finish().map(|(_, pair)| pair)
    }

    let cells = data
        .map(|line| parse_line::<A>(line).unwrap())
        .collect::<Vec<(AxialAddr, A)>>();
    let cells = cells.into_iter().collect::<HashMap<AxialAddr, A>>();
    Map { cells }
}

fn write_data<A: Clone>(map: Map<A>) -> Vec<String>
where
    String: From<A>,
{
    map.cells
        .iter()
        .map(|(addr, val)| format!("[{}, {}] {}", addr.q(), addr.r(), String::from(val.clone())))
        .collect()
}

fn parse_address(input: &str) -> IResult<&str, AxialAddr> {
    let (input, (q, r)) = delimited(
        tag("["),
        separated_pair(
            nom::character::complete::i32,
            tag(", "),
            nom::character::complete::i32,
        ),
        tag("] "),
    )(input)?;
    Ok((input, (AxialAddr::new(q, r))))
}

fn cell_value<A: From<String>>(input: &str) -> IResult<&str, A> {
    many1(alphanumeric1)
        .or(tag(".").map(|v| vec![v]))
        .map(|v| v.join(""))
        .map(|v| A::from(v))
        .parse(input)
}

#[cfg(test)]
mod test {
    use std::collections::HashSet;

    use super::*;

    #[derive(Clone, Debug, PartialEq)]
    struct MapVal(char);
    impl Default for MapVal {
        fn default() -> MapVal {
            MapVal('.')
        }
    }
    impl From<MapVal> for String {
        fn from(v: MapVal) -> String {
            format!("{}", v.0)
        }
    }
    impl From<String> for MapVal {
        fn from(s: String) -> MapVal {
            MapVal(s.chars().next().unwrap())
        }
    }

    #[test]
    fn it_can_modify_a_map() {
        let mut map: Map<MapVal> = Map::new_hexagonal(1);
        map.cell(AxialAddr::new(0, 0))
            .and_modify(|cell| *cell = MapVal('L'));
        assert_eq!(map.get(&AxialAddr::new(0, 0)), Some(&MapVal('L')));
        assert_eq!(map.get(&AxialAddr::new(0, 1)), Some(&MapVal('.')));
    }

    #[test]
    fn it_can_serialize_a_map() {
        let mut map: Map<MapVal> = Map::new_hexagonal(1);
        map.cell(AxialAddr::new(0, 0))
            .and_modify(|cell| *cell = MapVal('L'));
        map.cell(AxialAddr::new(1, 0))
            .and_modify(|cell| *cell = MapVal('A'));

        let mut expected: HashSet<String> = HashSet::new();
        expected.insert("[0, 0] L".to_owned());
        expected.insert("[1, 0] A".to_owned());
        expected.insert("[0, 1] .".to_owned());
        expected.insert("[-1, 1] .".to_owned());
        expected.insert("[-1, 0] .".to_owned());
        expected.insert("[0, -1] .".to_owned());
        expected.insert("[1, -1] .".to_owned());

        let map_rows = write_data(map);
        assert_eq!(map_rows.len(), expected.len());
        map_rows
            .iter()
            .for_each(|row| assert!(expected.contains(row)));
        expected
            .iter()
            .for_each(|expected| assert!(map_rows.contains(expected)));
    }

    #[test]
    fn it_can_deserialize_a_map() {
        let map_data = "[0, 0] L
[1, 0] A
[0, 1] .
[-1, 1] .
[-1, 0] .
[0, -1] .
[1, -1] .";
        let map: Map<MapVal> = parse_data(map_data.lines());
        assert_eq!(map.get(&AxialAddr::new(0, 0)), Some(&MapVal('L')));
        assert_eq!(map.get(&AxialAddr::new(1, 0)), Some(&MapVal('A')));
        assert_eq!(map.get(&AxialAddr::new(0, 1)), Some(&MapVal('.')));
    }
}
