Experimenting with more structured ways to handle command-line input/output in Rust
use crate::macro_impl::derive;
use std::collections::HashMap;
use std::path::PathBuf;

use fluent_syntax::ast::{Entry, Message, Pattern};
use miette::{Diagnostic, NamedSource, SourceSpan};
use thiserror::Error;

mod ast;
mod group;

pub use group::{Group, GroupError};

#[derive(Diagnostic, Debug, Error)]
#[error("couldn't parse Fluent source code")]
pub struct ParserError {
    #[label("{kind}")]
    span: SourceSpan,
    kind: fluent_syntax::parser::ErrorKind,
}

#[derive(Diagnostic, Debug, Error)]
pub enum Error {
    #[error("the reference `${fluent_name}` doesn't match a Rust field `{rust_name}`")]
    InvalidReference {
        fluent_name: String,
        rust_name: String,
        #[source_code]
        source_code: NamedSource<String>,
        #[label("This references `{rust_name}` which doesn't exist")]
        span: SourceSpan,
        #[help]
        valid_references: String,
    },
    #[error(r#"message "{unexpected_key}" from `{locale}` is not in the canonical `{canonical_locale}` locale"#)]
    #[help("the canonical locale must include all keys!")]
    UnexpectedKey {
        unexpected_key: String,
        locale: String,
        canonical_locale: String,
        #[source_code]
        source_code: NamedSource<String>,
        #[label("This key isn't in the `{canonical_locale}` locale")]
        span: SourceSpan,
    },
    #[error("unable to parse Fluent source code")]
    ParserErrors {
        #[source_code]
        source_code: NamedSource<String>,
        #[related]
        related: Vec<ParserError>,
    },
}

#[derive(Clone, Copy, Debug)]
pub struct MessageContext<'context> {
    source: &'context SourceFile,
    pattern: &'context Pattern<String>,
    derive_context: &'context derive::Context,
}

#[derive(Clone, Debug)]
struct SourceFile {
    named_source: NamedSource<String>,
    messages: HashMap<String, Message<String>>,
}

impl SourceFile {
    fn new(path: PathBuf) -> Result<Self, Error> {
        let file_contents = std::fs::read_to_string(&path).unwrap();

        let named_source = NamedSource::new(path.to_string_lossy(), file_contents.clone());

        let syntax_tree = match fluent_syntax::parser::parse(file_contents.clone()) {
            Ok(syntax_tree) => syntax_tree,
            Err((_partial_syntax_tree, parser_errors)) => {
                // Map the `fluent_syntax` errors to `miette::Diagnostic` errors
                let related = parser_errors
                    .into_iter()
                    .map(|error| ParserError {
                        span: SourceSpan::from(error.slice.unwrap_or(error.pos)),
                        kind: error.kind,
                    })
                    .collect();
                return Err(Error::ParserErrors {
                    source_code: NamedSource::new(path.to_string_lossy(), file_contents),
                    related,
                });
            }
        };

        // Keep track of all the messages in our Fluent file
        let mut messages = HashMap::with_capacity(syntax_tree.body.len());

        for entry in &syntax_tree.body {
            match entry {
                Entry::Message(message_entry) => {
                    let message_id = message_entry.id.name.to_owned();
                    // Insert this message id (and make sure it's unique!)
                    let previous_value = messages.insert(message_id, message_entry.clone());
                    assert!(previous_value.is_none());
                }
                Entry::Term(_) => todo!(),
                Entry::Junk { .. } => todo!(),
                // Ignore comments
                Entry::Comment(_) | Entry::GroupComment(_) | Entry::ResourceComment(_) => continue,
            }
        }

        Ok(Self {
            named_source,
            messages,
        })
    }

    /// Unique message IDs, to compare with other locales
    fn message_ids(&self) -> impl Iterator<Item = &str> {
        self.messages.keys().map(String::as_str)
    }

    fn contains_expression(&self, id: &str) -> bool {
        self.messages.contains_key(id)
    }

    fn try_remove_expression(&mut self, id: &str) -> Option<Message<String>> {
        self.messages.remove(id)
    }
}