Experimenting with more structured ways to handle command-line input/output in Rust
use super::{
    attribute,
    fluent::{self, GroupError},
    MacroError, ParseError, UnsupportedError, UnsupportedReason,
};

use proc_macro2::TokenStream;
use proc_macro_error2::{emit_call_site_error, emit_error};
use syn::spanned::Spanned;

fn attribute(error: attribute::Error, attribute_stream: TokenStream) {
    match error {
        attribute::Error::Build(build_error) => {
            for located_error in build_error.locations() {
                let error_source = attribute_stream.span();

                emit_error! { error_source, "invalid glob";
                    note = located_error.to_string();
                };
            }
        }
        attribute::Error::Walk(walk_error) => {
            // Generate help text
            let help = if let Some(path) = walk_error.path() {
                let path_name = path.to_str().unwrap();
                // Might hit an error if file exists but insufficient permissions
                match path.try_exists() {
                    Ok(true) => {
                        format!("the path `{path_name}` exists, but unable to access it")
                    }
                    _ => format!("the path `{path_name}` doesn't seem to exist"),
                }
            } else {
                String::from("no associated path")
            };

            emit_error! { attribute_stream, "error at depth {} while walking path", walk_error.depth();
                help = help;
            };
        }
        attribute::Error::NoMatchesInEntry {
            path,
            complete_match,
        } => {
            // Validate the assumption that the user has provided an exact path
            assert!(path.exists());
            assert_eq!(path.to_string_lossy(), complete_match);

            emit_error! { attribute_stream, "cannot match against an exact path";
                help = "The attribute should use glob syntax to match against multiple files";
                note = "For example, you can:\n{}\n{}",
                "- Match against directories: locale/**/errors.ftl",
                "- Match against files: locale/*.ftl";
            };
        }
        attribute::Error::NoMatchesInGlob => {
            emit_error! { attribute_stream, "no matches found for glob";
                help = "The attribute should use glob syntax to match against multiple files";
                note = "For example, you can:\n{}\n{}",
                "- Match against directories: locale/**/errors.ftl",
                "- Match against files: locale/*.ftl";
            };
        }
        attribute::Error::InvalidLocale {
            path,
            locale,
            source,
        } => {
            emit_error! { attribute_stream, "invalid locale in file path";
                help = "Expected `{}` to be a valid locale identifier while reading `{}`", locale, path.display();
                note = source;
            }
        }
    }
}

fn fluent(error: fluent::Error) {
    // It doesn't seem like you can reference non-Rust source files
    // in `proc_macro::Span`, so use Miette to pretty-print our own reports.
    eprintln!("{:?}", miette::Error::new(error));

    // Make sure compilation fails
    emit_call_site_error!("invalid Fluent source code, see above for details");
}

fn group(error: GroupError, attribute_stream: TokenStream) {
    match error {
        GroupError::Fluent(error) => fluent(error),
        GroupError::InvalidMessage {
            fluent_name,
            rust_name,
            span,
        } => {
            emit_error! { span.unwrap(), "no matching Fluent message in canonical locale";
                help = "The field `{}` implies the Fluent message `{}`", rust_name, fluent_name
            };
        }
        GroupError::MissingCanonicalLocale {
            canonical_locale,
            matched_locales,
        } => emit_error! { attribute_stream, "missing canonical locale `{}`", canonical_locale;
            help = "{}", matched_locales;
        },
    }
}

fn unsupported(error: UnsupportedError) {
    match error.reason {
        UnsupportedReason::Union => emit_error! { error.span, "unions are not supported";
            help = "Use a `struct` or `enum` instead"
        },
        UnsupportedReason::UnnamedFields => {
            emit_error! { error.span, "only named fields are supported";
                help = "Each field needs a name so it can be referenced by Fluent code";
                note = "There must be at least one named field (unit structs are unsupported!)";
            }
        }
    }
}

fn parse(error: ParseError, attribute_stream: TokenStream, derive_input_stream: TokenStream) {
    match error {
        ParseError::InvalidAttribute(invalid_attribute) => {
            emit_error! { attribute_stream, invalid_attribute;
                help = "Expected a path glob, for example {}",
                r#"#[localize("i18n/**/strings.ftl")]"#;
            }
        }
        ParseError::InvalidDeriveInput(invalid_derive_input) => {
            emit_error! { derive_input_stream, invalid_derive_input;
                help = "This macro can only be used on structs or enums";
            }
        }
    }
}

pub fn emit(error: MacroError, attribute_stream: TokenStream, derive_input_stream: TokenStream) {
    match error {
        MacroError::Attribute(error) => attribute(error, attribute_stream),
        MacroError::Group(error) => group(error, attribute_stream),
        MacroError::Unsupported(error) => unsupported(error),
        MacroError::ParseError(error) => parse(error, attribute_stream, derive_input_stream),
    }
}