// SPDX-FileCopyrightText: 2025 Jonah Brüchert <jbb@kaidan.im
//
// SPDX-License-Identifier: MIT

use std::{
    collections::HashMap,
    fs::{self, File},
    hash::Hash,
    io::{BufReader, ErrorKind, Write},
    rc::Rc,
    time::{Duration, SystemTime},
};

use geo::{Distance, Haversine, Point};
use serde::{Deserialize, Serialize};

use translit::Transliterator;
use unicode_normalization::UnicodeNormalization;

#[derive(Debug)]
pub enum OsmError {
    Download(ureq::Error),
    Parsing(quick_xml::DeError),
    Serializing(quick_xml::SeError),
    Io(std::io::Error),
}

impl std::fmt::Display for OsmError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            OsmError::Download(e) => {
                write!(f, "Error while downloading data from Overpass: {e}")
            }
            OsmError::Parsing(e) => {
                write!(f, "Error while parsing OSM data: {e}")
            }
            OsmError::Serializing(e) => {
                write!(f, "Error while serializing OSM data for cache: {e}")
            }
            OsmError::Io(e) => {
                write!(f, "io error: {e}")
            }
        }
    }
}

impl std::error::Error for OsmError {}

impl From<ureq::Error> for OsmError {
    fn from(value: ureq::Error) -> Self {
        OsmError::Download(value)
    }
}

impl From<quick_xml::DeError> for OsmError {
    fn from(value: quick_xml::DeError) -> Self {
        OsmError::Parsing(value)
    }
}

impl From<quick_xml::SeError> for OsmError {
    fn from(value: quick_xml::SeError) -> Self {
        OsmError::Serializing(value)
    }
}

impl From<std::io::Error> for OsmError {
    fn from(value: std::io::Error) -> Self {
        OsmError::Io(value)
    }
}

/// Station position
#[derive(Deserialize, Serialize, Clone)]
pub struct Position {
    pub lat: f64,
    pub lon: f64,
}

/// A station in the structure of an OpenStreetMap Node
pub struct Node {
    pub id: u64,
    pub position: Position,
    pub tags: HashMap<String, String>,
}

#[derive(Debug, Deserialize, Serialize)]
struct Osm {
    #[serde(rename = "node", default)]
    nodes: Vec<OsmNode>,

    #[serde(rename = "way", default)]
    ways: Vec<OsmWay>,
}

#[derive(Debug, Deserialize, Serialize)]
struct OsmNode {
    #[serde(rename = "@id")]
    id: u64,

    #[serde(rename = "@lat")]
    lat: f64,

    #[serde(rename = "@lon")]
    lon: f64,

    #[serde(rename = "tag", default)]
    tags: Vec<Tag>,
}

#[derive(Debug, Deserialize, Serialize)]
struct OsmWay {
    #[serde(rename = "@id")]
    id: u64,

    #[serde(rename = "nd", default)]
    nodes: Vec<WayNodeRef>,

    #[serde(rename = "tag", default)]
    tags: Vec<Tag>,
}

#[derive(Debug, Deserialize, Serialize)]
struct WayNodeRef {
    #[serde(rename = "@ref")]
    node_id: u64,
}

#[derive(Debug, Deserialize, Serialize)]
struct Tag {
    #[serde(rename = "@k")]
    key: String,

    #[serde(rename = "@v")]
    value: String,
}

/// A rule that matched stations must fulfil
#[derive(Clone)]
pub enum MatchRule {
    /// Every matched stop must be at max m meters away from the given position
    Position { max_distance_m_default: u32 },
    /// Every matched stop must have exactly the same name
    Name,
    /// Every matched stop must have a name that contains the individual words of the search name
    NameSubstring,
    /// Every matched stop must have a name that is similar to the search name
    NameSimilar {
        /// Number between 0.0 (entirely different) and 1.0 (identical)
        min_similarity: f64,
    },
    /// Every matched stop must have a name that the search name is an abbreviation of (or the other way around)
    NameAbbreviation,
    /// The specified OSM tag needs to contain a value specified in StationFacts
    CustomTag { name: String },
    /// Every matched stop needs to fulfil both of the given requirements
    Both(Box<MatchRule>, Box<MatchRule>),
    /// The first matching node is returned
    FirstMatch(Vec<MatchRule>),
}

/// Configure the strategy for matching stations
///
/// Example:
/// Here the matcher is configured to only return result results that are less than 50km from a specified point,
/// and either have a matching name, a name that contains the requested name as a substring, or a name with a similarity of 60%.
/// ```
/// use gtfs_generator::osm;
///
/// let precedence = vec![
///     "name".to_string(),
///     "alt_name".to_string(),
///     "name:sr-Latn".to_string(),
///     "name:en".to_string(),
/// ];
///
/// osm::StationMatcherBuilder::new()
///        .match_on(osm::MatchRule::Both(
///            Box::from(osm::MatchRule::FirstMatch(vec![
///                osm::MatchRule::Name,
///                osm::MatchRule::NameSubstring,
///                osm::MatchRule::NameSimilar {
///                    min_similarity: 0.6,
///                },
///            ])),
///            Box::from(osm::MatchRule::Position {
///                max_distance_m_default: 50 * 1000,
///            }),
///        ))
///        .name_tag_precedence(precedence)
///        .download_stations(&["ME"]).unwrap();
///```
#[derive(Default)]
pub struct StationMatcherBuilder {
    match_precedence: Option<MatchRule>,
    name_tags: Vec<String>,
    transliteration_lang: Option<TransliterationLanguage>,
    tags: Vec<(String, String)>,
}

impl StationMatcherBuilder {
    /// Start configuring a [`StationMatcher`]
    pub fn new() -> StationMatcherBuilder {
        StationMatcherBuilder {
            tags: vec![
                ("railway".to_string(), "station".to_string()),
                ("railway".to_string(), "halt".to_string()),
            ],
            ..Default::default()
        }
    }

    /// Set a list of rules that a matched station must fulfil
    pub fn match_on(mut self, match_precedence: MatchRule) -> StationMatcherBuilder {
        self.match_precedence = Some(match_precedence);
        self
    }

    /// Name tags the matcher should try to match. The order decides the order the priority the tags are tried in
    pub fn name_tag_precedence(mut self, name_tags: Vec<String>) -> StationMatcherBuilder {
        self.name_tags = name_tags;
        self
    }

    /// Configure the matcher to try transliterating names from the specified language to latin
    pub fn transliteration_lang(mut self, lang: TransliterationLanguage) -> StationMatcherBuilder {
        self.transliteration_lang = Some(lang);
        self
    }

    /// Configure tag=value pairs that features need to match in order to be included in the downloaded data.
    pub fn download_filter(mut self, tags: Vec<(String, String)>) -> StationMatcherBuilder {
        self.tags = tags;
        self
    }

    /// Create a [`StationMatcher`] matching to custom stations provided by the caller
    pub fn with_stations(self, stations: Vec<Node>) -> StationMatcher {
        StationMatcher {
            nodes: stations
                .into_iter()
                .map(|node| (node.id, Rc::from(node)))
                .collect(),
            match_precedence: self.match_precedence.unwrap(),
            cache: HashMap::new(),
            name_tag_precedence: self.name_tags,
            transliteration_lang: self.transliteration_lang,
        }
    }

    /// Download the list of stations for a specified area from OpenStreetMap (Overpass Turbo)
    ///
    /// Country codes need to be specified as ISO3166-1.
    pub fn download_stations(self, country_codes: &[&str]) -> Result<StationMatcher, OsmError> {
        let download = match fs::metadata("stations.xml") {
            Ok(meta) => {
                let last_modified = meta.modified()?;
                let age = SystemTime::now().duration_since(last_modified).unwrap();

                age > Duration::from_hours(24 * 14)
            }
            Err(e) if e.kind() == ErrorKind::NotFound => true,
            Err(e) => Err(e)?,
        };

        let document: Osm = if download {
            let country_codes_regex = country_codes.join("|");
            let tag_queries = self
                .tags
                .iter()
                .map(|(tag, value)| format!(r#"nwr["{tag}"="{value}"](area.countries);"#))
                .collect::<Vec<String>>()
                .join("\n");

            let query = format!(
                r#"
[out:xml];
(
    area["ISO3166-1"~"{country_codes_regex}"] -> .countries;
);
(
    {tag_queries}
);
(._;>;);
out body;"#
            );
            let xml = ureq::put("https://overpass-api.de/api/interpreter")
                .header("User-Agent", "gtfs-generator <jbb@kaidan.im>")
                .send(query)?
                .body_mut()
                .read_to_string()?;

            let document: Osm = quick_xml::de::from_str(&xml)?;

            let mut stations_file = File::create("stations.xml")?;
            let json = quick_xml::se::to_string(&document)?;
            stations_file.write_all(json.as_bytes())?;

            document
        } else {
            let buf = BufReader::new(File::open("stations.xml")?);
            quick_xml::de::from_reader(buf)?
        };

        let node_positions: HashMap<u64, Position> = document
            .nodes
            .iter()
            .map(|n| {
                (
                    n.id,
                    Position {
                        lat: n.lat,
                        lon: n.lon,
                    },
                )
            })
            .collect();

        let mut stations = HashMap::new();

        for node in document.nodes {
            let tags = node.tags.into_iter().map(|t| (t.key, t.value)).collect();

            stations.insert(
                node.id,
                Rc::from(Node {
                    id: node.id,
                    position: Position {
                        lat: node.lat,
                        lon: node.lon,
                    },
                    tags,
                }),
            );
        }

        for way in document.ways {
            let tags: HashMap<String, String> =
                way.tags.into_iter().map(|t| (t.key, t.value)).collect();

            let coords: Vec<_> = way
                .nodes
                .iter()
                .filter_map(|n| node_positions.get(&n.node_id))
                .collect();

            if coords.is_empty() {
                continue;
            }

            let lat = coords.iter().map(|p| p.lat).sum::<f64>() / coords.len() as f64;

            let lon = coords.iter().map(|p| p.lon).sum::<f64>() / coords.len() as f64;

            stations.insert(
                way.id,
                Rc::from(Node {
                    id: way.id,
                    position: Position { lat, lon },
                    tags,
                }),
            );
        }

        Ok(StationMatcher {
            match_precedence: self.match_precedence.unwrap(),
            nodes: stations,
            cache: HashMap::new(),
            name_tag_precedence: self.name_tags,
            transliteration_lang: self.transliteration_lang,
        })
    }
}

/// Already configured object for matching stations to OSM nodes.
///
/// See [`StationMatcherBuilder`] for how to create one.
pub struct StationMatcher {
    nodes: HashMap<u64, Rc<Node>>,
    match_precedence: MatchRule,
    cache: HashMap<StationFacts, Option<Rc<Node>>>,
    name_tag_precedence: Vec<String>,
    transliteration_lang: Option<TransliterationLanguage>,
}

/// Language to transliterate to latin from
#[derive(Deserialize, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum TransliterationLanguage {
    /// Ukrainian
    Uk,
    /// Russian
    Ru,
    /// Belarusian
    Be,
    /// Bulgarian
    Bg,
    /// Macedonian
    Mk,
    /// Greek
    El,
}

fn greek_to_latin_rules() -> Vec<(&'static str, &'static str)> {
    vec![
        // Digraphs (must come first)
        ("αι", "ai"),
        ("ει", "ei"),
        ("οι", "oi"),
        ("υι", "yi"),
        ("ου", "ou"),
        ("αυ", "av"),
        ("ευ", "ev"),
        ("ηυ", "iv"),
        // Uppercase digraphs
        ("ΑΙ", "AI"),
        ("ΕΙ", "EI"),
        ("ΟΙ", "OI"),
        ("ΥΙ", "YI"),
        ("ΟΥ", "OU"),
        ("ΑΥ", "AV"),
        ("ΕΥ", "EV"),
        ("ΗΥ", "IV"),
        // Consonant digraphs
        ("μπ", "b"),
        ("ντ", "d"),
        ("γκ", "g"),
        ("γγ", "ng"),
        ("ΜΠ", "B"),
        ("ΝΤ", "D"),
        ("ΓΚ", "G"),
        ("ΓΓ", "NG"),
        // Single letters
        ("α", "a"),
        ("ά", "a"),
        ("β", "v"),
        ("γ", "g"),
        ("δ", "d"),
        ("ε", "e"),
        ("έ", "e"),
        ("ζ", "z"),
        ("η", "i"),
        ("ή", "i"),
        ("θ", "th"),
        ("ι", "i"),
        ("ί", "i"),
        ("ϊ", "i"),
        ("ΐ", "i"),
        ("κ", "k"),
        ("λ", "l"),
        ("μ", "m"),
        ("ν", "n"),
        ("ξ", "x"),
        ("ο", "o"),
        ("ό", "o"),
        ("π", "p"),
        ("ρ", "r"),
        ("σ", "s"),
        ("ς", "s"),
        ("τ", "t"),
        ("υ", "y"),
        ("ύ", "y"),
        ("ϋ", "y"),
        ("ΰ", "y"),
        ("φ", "f"),
        ("χ", "ch"),
        ("ψ", "ps"),
        ("ω", "o"),
        ("ώ", "o"),
        // Uppercase
        ("Α", "A"),
        ("Ά", "A"),
        ("Β", "V"),
        ("Γ", "G"),
        ("Δ", "D"),
        ("Ε", "E"),
        ("Έ", "E"),
        ("Ζ", "Z"),
        ("Η", "I"),
        ("Ή", "I"),
        ("Θ", "TH"),
        ("Ι", "I"),
        ("Ί", "I"),
        ("Ϊ", "I"),
        ("Κ", "K"),
        ("Λ", "L"),
        ("Μ", "M"),
        ("Ν", "N"),
        ("Ξ", "X"),
        ("Ο", "O"),
        ("Ό", "O"),
        ("Π", "P"),
        ("Ρ", "R"),
        ("Σ", "S"),
        ("Τ", "T"),
        ("Υ", "Y"),
        ("Ύ", "Y"),
        ("Ϋ", "Y"),
        ("Φ", "F"),
        ("Χ", "CH"),
        ("Ψ", "PS"),
        ("Ω", "O"),
        ("Ώ", "O"),
    ]
}

fn transliterate_name(name: &str, lang: TransliterationLanguage) -> String {
    use translit::{BulgarianOfficial, Gost779B, Language as L, MacedonianOfficial, ToLatin};

    let transliterator: Box<dyn ToLatin> = match lang {
        TransliterationLanguage::Uk | TransliterationLanguage::Ru | TransliterationLanguage::Be => {
            Box::from(Gost779B::new(match lang {
                TransliterationLanguage::Uk => L::Ua,
                TransliterationLanguage::Ru => L::Ru,
                TransliterationLanguage::Be => L::By,
                _ => unreachable!(),
            }))
        }
        TransliterationLanguage::Bg => Box::from(BulgarianOfficial::new()),
        TransliterationLanguage::Mk => Box::from(MacedonianOfficial::new()),
        TransliterationLanguage::El => Box::from(Transliterator::new(greek_to_latin_rules())),
    };

    transliterator.to_latin(name)
}

/// Already known information about a station.
///
/// Which attributes are used depends on the configured matching strategy.
#[derive(Default, Debug, PartialEq, Clone)]
pub struct StationFacts {
    pub name: Option<String>,
    pub pos: Option<(f64, f64)>,
    pub max_distance_m: Option<u32>,
    pub values: HashMap<String, String>,
}

impl Eq for StationFacts {}

impl Hash for StationFacts {
    fn hash<H: std::hash::Hasher>(&self, hasher: &mut H) {
        self.name.hash(hasher);
        for (key, value) in &self.values {
            key.hash(hasher);
            value.hash(hasher);
        }
        if let Some((lat, lon)) = self.pos {
            let lat = (lat * (10.0_f64.powi(6)).round()) as u64;
            let lon = (lon * (10.0_f64.powi(6)).round()) as u64;
            lat.hash(hasher);
            lon.hash(hasher);
        }
    }
}

fn get_node_names<'a>(node: &'a Node, name_tag_precedence: &[String]) -> Vec<&'a str> {
    let mut names = Vec::new();

    for tag in name_tag_precedence {
        if let Some(value) = node.tags.get(tag) {
            for name in value.split(';') {
                names.push(name);
            }
        }
    }

    names
}

fn normalize_numerals(text: &str) -> String {
    fn is_roman_numeral(text: &str) -> bool {
        !text.is_empty()
            && text
                .chars()
                .all(|c| ['I', 'V', 'X', 'M', 'C', 'L'].contains(&c.to_ascii_uppercase()))
    }

    fn is_all_digits(text: &str) -> bool {
        !text.is_empty() && text.chars().all(|c| c.is_digit(10))
    }

    fn parse_roman_numeral(text: &str) -> u32 {
        let (v, _): (u32, u32) = text.chars().rev().fold((0, 0), |(acc, last), c| {
            let v = match c.to_ascii_uppercase() {
                'M' => 1000,
                'D' => 500,
                'C' => 100,
                'L' => 50,
                'X' => 10,
                'V' => 5,
                'I' => 1,
                _ => unreachable!(),
            };

            if last > v {
                (acc.saturating_sub(v), v)
            } else {
                (acc.saturating_add(v), v)
            }
        });

        v
    }

    text.split(&[' ', '-'])
        .map(|word| {
            if is_roman_numeral(word) {
                parse_roman_numeral(word).to_string()
            } else if is_all_digits(word) {
                // Remove leading zeros
                u32::from_str_radix(word, 10).unwrap().to_string()
            } else {
                word.to_string()
            }
        })
        .collect::<Vec<String>>()
        .join(" ")
}

#[test]
fn test_numerals() {
    assert_eq!(
        normalize_numerals("Старокостянтинів-II"),
        "Старокостянтинів 2"
    );
    assert_eq!(
        normalize_numerals("Старокостянтинів 02"),
        "Старокостянтинів 2"
    );
    assert_eq!(normalize_numerals("IV"), "4");
    assert_eq!(normalize_numerals("MMCLIV"), "2154");

    assert_eq!(
        make_comparable_name("Samtredia-1", None),
        make_comparable_name("Samtredia   I", None)
    )
}

fn make_comparable_name(
    name: &str,
    transliteration_lang: Option<TransliterationLanguage>,
) -> String {
    let name = match transliteration_lang {
        Some(lang) => &transliterate_name(name, lang),
        None => name,
    };

    normalize_numerals(
        &name
            .to_lowercase()
            .nfd()
            .filter(char::is_ascii)
            .collect::<String>()
            .replace('-', " ")
            .split_whitespace()
            .filter(|w| !w.is_empty())
            .collect::<Vec<_>>()
            .join(" "),
    )
}

fn contains_word(a: &str, b: &str) -> bool {
    let words_a = a.split(' ').collect::<Vec<_>>();

    let words_b = b.split(' ').collect::<Vec<_>>();

    words_a.iter().all(|w| words_b.contains(&w)) || words_b.iter().all(|w| words_a.contains(&w))
}

fn match_property(
    nodes: &[Rc<Node>],
    name_tag_precedence: &[String],
    transliteration_lang: Option<TransliterationLanguage>,
    property: &MatchRule,
    known: &StationFacts,
) -> Vec<Rc<Node>> {
    match *property {
        MatchRule::Position {
            ref max_distance_m_default,
        } => {
            if let Some((lat, lon)) = known.pos {
                let mut nodes = nodes
                    .iter()
                    .flat_map(|node| {
                        let dist = Haversine.distance(
                            Point::new(lon, lat),
                            Point::new(node.position.lon, node.position.lat),
                        );

                        if dist <= known.max_distance_m.unwrap_or(*max_distance_m_default) as f64 {
                            Some((dist, node.clone()))
                        } else {
                            None
                        }
                    })
                    .collect::<Vec<(f64, Rc<Node>)>>();

                nodes.sort_by(|(dist_a, _), (dist_b, _)| dist_a.total_cmp(dist_b));
                return nodes
                    .iter()
                    .map(|(_, node)| node.clone())
                    .collect::<Vec<Rc<Node>>>();
            } else {
                return nodes.to_vec();
            }
        }
        MatchRule::Name => {
            if let Some(known_name) = &known.name {
                let known_name_ascii = make_comparable_name(known_name, transliteration_lang);

                return nodes
                    .iter()
                    .filter(|station| {
                        for name in get_node_names(station, name_tag_precedence) {
                            let osm_name = make_comparable_name(name, transliteration_lang);

                            if osm_name == known_name_ascii {
                                return true;
                            }
                        }

                        false
                    })
                    .cloned()
                    .collect::<Vec<_>>();
            }
        }
        MatchRule::NameSubstring => {
            if let Some(known_name) = &known.name {
                let partial_matches: Vec<_> = nodes
                    .iter()
                    .filter(|station| {
                        for name in get_node_names(station, name_tag_precedence) {
                            let value = make_comparable_name(name, transliteration_lang);
                            let known = make_comparable_name(known_name, transliteration_lang);
                            if contains_word(&value, &known) {
                                return true;
                            }
                        }

                        false
                    })
                    .cloned()
                    .collect();

                return partial_matches;
            }
        }
        MatchRule::NameSimilar { min_similarity } => {
            if let Some(known_name) = &known.name {
                let mut partial_matches: Vec<_> = nodes
                    .iter()
                    .flat_map(|station| {
                        let closest_string_score = get_node_names(station, name_tag_precedence)
                            .into_iter()
                            .map(|v| {
                                strsim::normalized_damerau_levenshtein(
                                    &make_comparable_name(known_name, transliteration_lang),
                                    &make_comparable_name(v, transliteration_lang),
                                )
                            })
                            .filter(|similarity| *similarity >= min_similarity)
                            .max_by(|a, b| a.total_cmp(b));

                        closest_string_score.map(|score| (score, station))
                    })
                    .collect();
                partial_matches.sort_by(|(score_a, _), (score_b, _)| score_a.total_cmp(score_b));

                return partial_matches
                    .iter()
                    .map(|(_, node)| (*node).clone())
                    .collect();
            }
        }
        MatchRule::NameAbbreviation => {
            if let Some(known_name) = &known.name {
                let partial_matches: Vec<_> = nodes
                    .iter()
                    .filter(|station| {
                        for name in get_node_names(station, name_tag_precedence) {
                            if compare_abbreviations(&name, &known_name, transliteration_lang) {
                                return true;
                            }
                        }

                        false
                    })
                    .cloned()
                    .collect();

                return partial_matches;
            }
        }
        MatchRule::CustomTag { ref name } => {
            if let Some(known_value) = &known.values.get(name) {
                return nodes
                    .iter()
                    .filter(|station| {
                        station
                            .tags
                            .get(name)
                            .map(|v| v == *known_value)
                            .unwrap_or(false)
                    })
                    .cloned()
                    .collect();
            }
        }
        MatchRule::Both(ref a, ref b) => {
            let nodes_a =
                match_property(nodes, name_tag_precedence, transliteration_lang, a, known);
            let nodes_b = match_property(
                &nodes_a,
                name_tag_precedence,
                transliteration_lang,
                b,
                known,
            );
            return nodes_b;
        }
        MatchRule::FirstMatch(ref rules) => {
            for rule in rules {
                let m = match_property(
                    nodes,
                    name_tag_precedence,
                    transliteration_lang,
                    rule,
                    known,
                );
                if !m.is_empty() {
                    return m;
                }
            }
        }
    }

    Vec::new()
}

impl StationMatcher {
    /// Look up a station based on the configured matching rules and the known information
    ///
    /// On success, the function returns the coordinates of the station.
    pub fn find_matching_station(&mut self, known: &StationFacts) -> Option<Rc<Node>> {
        let cached_result = self.cache.get(known);
        if let Some(cached_match) = cached_result {
            return cached_match.clone();
        }

        let maybe_match = match_property(
            &self.nodes.values().cloned().collect::<Vec<Rc<Node>>>(),
            &self.name_tag_precedence,
            self.transliteration_lang,
            &self.match_precedence,
            known,
        )
        .first()
        .cloned();
        self.cache.insert(known.clone(), maybe_match.clone());
        if let Some(m) = maybe_match {
            return Some(m.clone());
        }

        None
    }

    pub fn get_node_by_id(&self, id: u64) -> Option<&Rc<Node>> {
        self.nodes.get(&id)
    }
}

#[test]
fn comparable_name() {
    let name = make_comparable_name("Bucureşti Basarab", Some(TransliterationLanguage::Bg));
    assert_eq!(name, "bucuresti basarab");

    assert_eq!(
        make_comparable_name("Старокостянтинів 2", Some(TransliterationLanguage::Uk)),
        make_comparable_name("Старокостянтинів-II", Some(TransliterationLanguage::Uk))
    );

    assert_eq!(
        make_comparable_name("РУСТАВИ-ГРУЗ", Some(TransliterationLanguage::Ru)),
        make_comparable_name("Рустави-Груз", Some(TransliterationLanguage::Ru))
    )
}

fn compare_abbreviations(
    name_a: &str,
    name_b: &str,
    lang: Option<TransliterationLanguage>,
) -> bool {
    let check_contains_chars_in_correct_order = |string: &str, chars: &str| {
        if string.is_empty() || chars.is_empty() {
            return false;
        }

        let mut pos = None;
        for ch in chars.chars() {
            pos = match pos {
                Some(pos) => string[pos..].find(ch),
                None => string.find(ch),
            };
            if pos.is_none() {
                return false;
            }
        }

        return true;
    };

    let compare_word = |a: &str, b: &str| {
        if a.ends_with('.') {
            let abbreviation_chars = &a[0..a.len() - 1];
            if b.starts_with(abbreviation_chars) {
                return true;
            }

            if check_contains_chars_in_correct_order(b, abbreviation_chars) {
                return true;
            }
        }

        if a.contains(' ') {
            let pos = a.find(' ').unwrap();
            return b.starts_with(&a[0..pos])
                && b.ends_with(&a[pos + 1..a.len()].trim_end_matches('.'));
        }

        a == b
    };

    let compare = |name_a: &str, name_b: &str| -> bool {
        let words_a: Vec<&str> = name_a
            .split(' ')
            .flat_map(|word| word.split_inclusive('.'))
            .flat_map(|word| word.split('+'))
            .filter(|word| !word.is_empty())
            .collect();
        let words_b: Vec<&str> = name_b
            .split(' ')
            .flat_map(|word| word.split_inclusive('.'))
            .flat_map(|word| word.split('+'))
            .filter(|word| !word.is_empty())
            .collect();

        if words_a.len() != words_b.len() {
            return false;
        }

        return words_a.iter().enumerate().all(|(i, word_a)| {
            let a = make_comparable_name(word_a, lang);
            let b = make_comparable_name(&words_b[i], lang);
            compare_word(&a, &b) || compare_word(&b, &a)
        });
    };

    compare(name_a, name_b)
}

#[test]
fn abbreviation() {
    let name = "Gen. Gh. Avramescu";
    let known = "General Gheorghe Avramescu";
    assert!(compare_abbreviations(name, known, None));

    let a = "General Nicolae Dăscălescu";
    let b = "G-ral N. Dăscălescu";
    assert!(compare_abbreviations(a, b, None));

    let a = "General Eremia Grigorescu";
    let b = "G-ral. E. Grigorescu";
    assert!(compare_abbreviations(a, b, None));

    let a = "General Traian Moșoiu";
    let b = "G-ral T.Moşoiu";
    assert!(compare_abbreviations(a, b, None));

    let a = "Drobeta Tr.Sev. Est";
    let b = "Drobeta Turnu Severin Est";
    assert!(compare_abbreviations(a, b, None));

    let a = "Drobeta Tr.Sev. Est";
    let b = "Drobeta Tr. Sev. Est";
    assert!(compare_abbreviations(a, b, None));

    let a = "Km 74 + 600";
    let b = "Km. 74+600";
    assert!(compare_abbreviations(a, b, None));
}

#[test]
fn node_names() {
    let node = Node {
        id: 0,
        position: Position { lat: 0.1, lon: 0.2 },
        tags: HashMap::from_iter([
            ("name".to_string(), "Gara Obor".to_string()),
            (
                "alt_name".to_string(),
                "Gara de Est;București Est;București Obor".to_string(),
            ),
        ]),
    };

    assert_eq!(
        get_node_names(&node, &["name".to_string(), "alt_name".to_string()]),
        [
            "Gara Obor",
            "Gara de Est",
            "București Est",
            "București Obor"
        ]
    );
}

#[test]
fn test_bucuresti() {
    let a = make_comparable_name("Bucureşti Obor", None);
    let b = make_comparable_name("București Obor", None);
    assert_eq!(a, b);

    let a = make_comparable_name("Gălănești", None);
    let b = make_comparable_name("Gălăneşti", None);
    assert_eq!(a, b)
}

#[test]
fn test_word() {
    let a = make_comparable_name("Săvinești hc", None);
    let b = make_comparable_name("Săvineşti", None);
    assert!(contains_word(&a, &b));

    let a = make_comparable_name("Costișa Hm", None);
    let b = make_comparable_name("Costişa", None);
    assert!(contains_word(&a, &b));

    let a = make_comparable_name("Dragalina Hm", None);
    let b = make_comparable_name("Dragalina", None);
    assert!(contains_word(&a, &b));
}

//#[test]
fn test() {
    use crate::osm;
    let name_tag_precedence = vec![
        "name:lt".to_string(),
        "name:lv".to_string(),
        "name:et".to_string(),
        "name:en".to_string(),
        "name".to_string(),
    ];
    let mut matcher = osm::StationMatcherBuilder::new()
        .match_on(osm::MatchRule::Both(
            Box::new(osm::MatchRule::Name),
            Box::new(osm::MatchRule::Position {
                max_distance_m_default: 2000,
            }),
        ))
        .name_tag_precedence(name_tag_precedence)
        .download_stations(&["LT", "LV", "PL", "EE"])
        .unwrap();

    let known = StationFacts {
        name: Some("Ligatne".to_string()),
        pos: Some((57.182496, 25.03361)),
        values: HashMap::from_iter([("ref:ibnr".to_string(), "2509510".to_string())]),
        ..Default::default()
    };

    matcher.find_matching_station(&known).unwrap();
}
