Framework for embedding localizations into Rust types
use crate::macro_impl::derive;
use std::collections::HashMap;
use std::num::ParseIntError;

use camino::Utf8PathBuf;
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)]
#[error("unable to parse Fluent function")]
pub enum FunctionError {
    InvalidName {
        name: String,
        #[source_code]
        source_code: NamedSource<String>,
        // TODO: blocked on https://github.com/projectfluent/fluent-rs/pull/373
        // #[label("This key isn't in the `{canonical_locale}` locale")]
        // span: SourceSpan,
    },
    IncorrectPositionalArgumentCount {
        expected_len: usize,
        actual_len: usize,
        #[source_code]
        source_code: NamedSource<String>,
        // TODO: blocked on https://github.com/projectfluent/fluent-rs/pull/373
        // #[label("This key isn't in the `{canonical_locale}` locale")]
        // span: SourceSpan,
    },
    UnexpectedNamedArguments {
        #[source_code]
        source_code: NamedSource<String>,
        // TODO: blocked on https://github.com/projectfluent/fluent-rs/pull/373
        // #[label("This key isn't in the `{canonical_locale}` locale")]
        // span: SourceSpan,
    },
    NonVariableReference {
        #[source_code]
        source_code: NamedSource<String>,
        // TODO: blocked on https://github.com/projectfluent/fluent-rs/pull/373
        // #[label("This key isn't in the `{canonical_locale}` locale")]
        // span: SourceSpan,
    },
}

#[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>,
        // TODO: blocked on https://github.com/projectfluent/fluent-rs/pull/373
        // #[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>,
        // TODO: blocked on https://github.com/projectfluent/fluent-rs/pull/373
        // #[label("This key isn't in the `{canonical_locale}` locale")]
        // span: SourceSpan,
    },
    #[error("invalid number literal")]
    InvalidNumberLiteral {
        invalid_literal: String,
        parse_error: ParseIntError,
        #[source_code]
        source_code: NamedSource<String>,
        // TODO: blocked on https://github.com/projectfluent/fluent-rs/pull/373
        // #[label("This can't be parsed as an unsigned 128-bit integer")]
        // span: SourceSpan,
    },
    #[error("invalid identifier type")]
    InvalidIdentifierType {
        invalid_identifier: String,
        expected_type: ast::VariantType,
        found_type: ast::VariantType,
        #[source_code]
        source_code: NamedSource<String>,
        // TODO: blocked on https://github.com/projectfluent/fluent-rs/pull/373
        // #[label("This can't be parsed as an unsigned 128-bit integer")]
        // span: SourceSpan,
    },
    #[error(transparent)]
    #[diagnostic(transparent)]
    Function(FunctionError),
    #[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: Utf8PathBuf) -> Result<Self, Error> {
        let file_contents = std::fs::read_to_string(&path).unwrap();

        let named_source = NamedSource::new(path.to_string(), 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(), 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.clone();
                    // 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(_) => (),
            }
        }

        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)
    }
}