mod validation_db;
mod argument;
mod directive;
mod enum_;
mod field;
mod fragment;
mod input_object;
mod interface;
mod object;
mod operation;
mod scalar;
mod schema;
mod selection;
mod union_;
mod value;
mod variable;
use crate::executable::BuildError as ExecutableBuildError;
use crate::schema::BuildError as SchemaBuildError;
use crate::FileId;
use crate::NodeLocation;
use crate::NodeStr;
use crate::SourceFile;
use indexmap::IndexMap;
use indexmap::IndexSet;
use std::cell::RefCell;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::fmt;
use std::io;
use std::sync::Arc;
pub(crate) use validation_db::{ValidationDatabase, ValidationStorage};
pub struct Diagnostics(Box<DiagnosticsBoxed>);
struct DiagnosticsBoxed {
    source_cache: RefCell<SourceCache>,
    errors: Vec<Error>,
}
struct SourceCache {
    sources: IndexMap<FileId, Arc<SourceFile>>,
    cache: HashMap<FileId, ariadne::Source>,
}
struct Error {
    location: Option<NodeLocation>,
    details: Details,
}
#[derive(thiserror::Error, Debug)]
pub(crate) enum Details {
    #[error("{message}")]
    ParserLimit { message: String },
    #[error("syntax error: {message}")]
    SyntaxError { message: String },
    #[error("{0}")]
    SchemaBuildError(SchemaBuildError),
    #[error("{0}")]
    ExecutableBuildError(ExecutableBuildError),
    #[error("syntax error: {0}")]
    CompilerDiagnostic(crate::ApolloDiagnostic),
}
impl Error {
    fn report(&self, color: bool) -> ariadne::Report<'static, NodeLocation> {
        let config = ariadne::Config::default().with_color(color);
        if let Details::CompilerDiagnostic(diagnostic) = &self.details {
            return diagnostic.to_report(config);
        }
        let (id, offset) = if let Some(location) = self.location {
            (location.file_id(), location.offset())
        } else {
            (FileId::NONE, 0)
        };
        let mut report = ariadne::Report::build::<FileId>(ariadne::ReportKind::Error, id, offset)
            .with_config(config);
        let mut colors = ariadne::ColorGenerator::new();
        macro_rules! opt_label {
            ($location: expr, $message: literal $(, $args: expr )* $(,)?) => {
                if let Some(location) = $location {
                    report.add_label(
                        ariadne::Label::new(*location)
                            .with_message(format_args!($message $(, $args)*))
                            .with_color(colors.next()),
                    )
                }
            };
            ($message: literal $(, $args: expr )* $(,)?) => {
                opt_label!(&self.location, $message $(, $args)*)
            };
        }
        report.set_message(&self.details);
        match &self.details {
            Details::CompilerDiagnostic(_) => unreachable!(),
            Details::ParserLimit { message, .. } => opt_label!("{message}"),
            Details::SyntaxError { message, .. } => opt_label!("{message}"),
            Details::SchemaBuildError(err) => match err {
                SchemaBuildError::ExecutableDefinition { .. } => {
                    opt_label!("remove this definition, or use `parse_mixed()`")
                }
                SchemaBuildError::SchemaDefinitionCollision {
                    previous_location, ..
                } => {
                    opt_label!(previous_location, "previous `schema` definition here");
                    opt_label!("`schema` redefined here");
                    report.set_help(
                        "merge this definition with the previous one, or use `extend schema`",
                    );
                }
                SchemaBuildError::DirectiveDefinitionCollision {
                    previous_location,
                    name,
                    ..
                } => {
                    opt_label!(previous_location, "previous definition of `@{name}` here");
                    opt_label!("`@{name}` redefined here");
                    report.set_help("remove or rename one of the definitions");
                }
                SchemaBuildError::TypeDefinitionCollision {
                    previous_location,
                    name,
                    ..
                } => {
                    opt_label!(previous_location, "previous definition of `{name}` here");
                    opt_label!("`{name}` redefined here");
                    report.set_help("remove or rename one of the definitions, or use `extend`");
                }
                SchemaBuildError::BuiltInScalarTypeRedefinition { .. } => {
                    opt_label!("remove this scalar definition");
                }
                SchemaBuildError::OrphanSchemaExtension { .. } => opt_label!("extension here"),
                SchemaBuildError::OrphanTypeExtension { .. } => opt_label!("extension here"),
                SchemaBuildError::TypeExtensionKindMismatch { def_location, .. } => {
                    opt_label!(def_location, "type definition");
                    opt_label!("extension here")
                }
                SchemaBuildError::DuplicateRootOperation {
                    previous_location,
                    operation_type,
                    ..
                } => {
                    opt_label!(
                        previous_location,
                        "previous definition of `{operation_type}` here"
                    );
                    opt_label!("`{operation_type}` redefined here");
                }
                SchemaBuildError::DuplicateImplementsInterfaceInObject {
                    name_at_previous_location,
                    ..
                }
                | SchemaBuildError::DuplicateImplementsInterfaceInInterface {
                    name_at_previous_location,
                    ..
                } => {
                    let previous_location = &name_at_previous_location.location();
                    let name = name_at_previous_location;
                    opt_label!(
                        previous_location,
                        "previous implementation of `{name}` here"
                    );
                    opt_label!("`{name}` implemented again here");
                }
                SchemaBuildError::ObjectFieldNameCollision {
                    name_at_previous_location,
                    ..
                }
                | SchemaBuildError::InterfaceFieldNameCollision {
                    name_at_previous_location,
                    ..
                }
                | SchemaBuildError::EnumValueNameCollision {
                    name_at_previous_location,
                    ..
                }
                | SchemaBuildError::UnionMemberNameCollision {
                    name_at_previous_location,
                    ..
                }
                | SchemaBuildError::InputFieldNameCollision {
                    name_at_previous_location,
                    ..
                } => {
                    let previous_location = &name_at_previous_location.location();
                    let name = name_at_previous_location;
                    opt_label!(previous_location, "previous definition of `{name}` here");
                    opt_label!("`{name}` redefined here");
                }
            },
            Details::ExecutableBuildError(err) => match err {
                ExecutableBuildError::TypeSystemDefinition { .. } => {
                    opt_label!("remove this definition, or use `parse_mixed()`")
                }
                ExecutableBuildError::AmbiguousAnonymousOperation { .. } => {
                    opt_label!("provide a name for this definition");
                    report.set_help(
                        "GraphQL requires operations to be named if the document has more than one",
                    );
                }
                ExecutableBuildError::OperationNameCollision {
                    name_at_previous_location,
                    ..
                }
                | ExecutableBuildError::FragmentNameCollision {
                    name_at_previous_location,
                    ..
                } => {
                    let previous_location = &name_at_previous_location.location();
                    let name = name_at_previous_location;
                    opt_label!(previous_location, "previous definition of `{name}` here");
                    opt_label!("`{name}` redefined here");
                }
                ExecutableBuildError::UndefinedRootOperation { operation_type, .. } => {
                    opt_label!(
                        "`{operation_type}` is not defined in the schema \
                         and is therefore not supported"
                    );
                    report.set_help(format_args!(
                        "consider defining a `{operation_type}` root operation type \
                         in your schema"
                    ))
                }
                ExecutableBuildError::UndefinedTypeInNamedFragmentTypeCondition { .. } => {
                    opt_label!("type condition here")
                }
                ExecutableBuildError::UndefinedTypeInInlineFragmentTypeCondition {
                    path, ..
                } => {
                    opt_label!("type condition here");
                    report.set_note(format_args!("path to the inline fragment: `{path} → ...`"))
                }
                ExecutableBuildError::SubselectionOnScalarType { path, .. }
                | ExecutableBuildError::SubselectionOnEnumType { path, .. } => {
                    opt_label!("remove subselections here");
                    report.set_note(format_args!("path to the field: `{path}`"))
                }
                ExecutableBuildError::UndefinedField {
                    field_name,
                    type_name,
                    path,
                    ..
                } => {
                    opt_label!("field `{field_name}` selected here");
                    opt_label!(&type_name.location(), "type `{type_name}` defined here");
                    report.set_note(format_args!("path to the field: `{path}`"))
                }
            },
        }
        report.finish()
    }
}
impl Diagnostics {
    pub fn to_string_no_color(&self) -> String {
        format!("{self:#}")
    }
    pub(crate) fn new(sources: IndexMap<FileId, Arc<SourceFile>>) -> Self {
        Self(Box::new(DiagnosticsBoxed {
            errors: Vec::new(),
            source_cache: RefCell::new(SourceCache {
                sources,
                cache: HashMap::new(),
            }),
        }))
    }
    pub(crate) fn push(&mut self, location: Option<NodeLocation>, details: Details) {
        self.0.errors.push(Error { location, details })
    }
    pub(crate) fn into_result(mut self) -> Result<(), Self> {
        if self.0.errors.is_empty() {
            Ok(())
        } else {
            self.sort();
            Err(self)
        }
    }
    pub(crate) fn sort(&mut self) {
        self.0
            .errors
            .sort_by_key(|err| err.location.map(|loc| (loc.file_id(), loc.offset())))
    }
    pub(crate) fn is_empty(&self) -> bool {
        self.0.errors.is_empty()
    }
}
impl fmt::Display for Diagnostics {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        struct Adaptor<'a, 'b>(&'a mut fmt::Formatter<'b>);
        impl io::Write for Adaptor<'_, '_> {
            fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
                let s = std::str::from_utf8(buf).map_err(|_| io::ErrorKind::Other)?;
                self.0.write_str(s).map_err(|_| io::ErrorKind::Other)?;
                Ok(buf.len())
            }
            fn flush(&mut self) -> io::Result<()> {
                Ok(())
            }
        }
        let mut cache = self.0.source_cache.borrow_mut();
        let color = !f.alternate();
        for error in &self.0.errors {
            error
                .report(color)
                .write(&mut *cache, Adaptor(f))
                .map_err(|_| fmt::Error)?
        }
        Ok(())
    }
}
impl ariadne::Cache<FileId> for SourceCache {
    fn fetch(&mut self, file_id: &FileId) -> Result<&ariadne::Source, Box<dyn fmt::Debug + '_>> {
        struct NotFound(FileId);
        impl fmt::Debug for NotFound {
            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                write!(f, "source file not found: {:?}", self.0)
            }
        }
        match self.cache.entry(*file_id) {
            Entry::Occupied(entry) => Ok(entry.into_mut()),
            Entry::Vacant(entry) => {
                let source_text = if *file_id == FileId::NONE
                    || *file_id == FileId::HACK_TMP
                    || *file_id == FileId::HACK_TMP_2
                {
                    ""
                } else if let Some(file) = self.sources.get(file_id) {
                    file.source_text()
                } else {
                    return Err(Box::new(NotFound(*file_id)));
                };
                Ok(entry.insert(ariadne::Source::from(source_text)))
            }
        }
    }
    fn display<'a>(&self, file_id: &'a FileId) -> Option<Box<dyn fmt::Display + 'a>> {
        if *file_id != FileId::NONE {
            struct Path(Arc<SourceFile>);
            impl fmt::Display for Path {
                fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                    self.0.path().display().fmt(f)
                }
            }
            let source_file = self.sources.get(file_id)?;
            Some(Box::new(Path(source_file.clone())))
        } else {
            struct NoSourceFile;
            impl fmt::Display for NoSourceFile {
                fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                    f.write_str("(no source file)")
                }
            }
            Some(Box::new(NoSourceFile))
        }
    }
}
impl fmt::Debug for Diagnostics {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Display::fmt(self, f)
    }
}
impl ariadne::Span for NodeLocation {
    type SourceId = FileId;
    fn source(&self) -> &FileId {
        &self.file_id
    }
    fn start(&self) -> usize {
        self.offset()
    }
    fn end(&self) -> usize {
        self.end_offset()
    }
}
struct RecursionStack {
    seen: IndexSet<NodeStr>,
}
impl RecursionStack {
    fn with_root(root: NodeStr) -> Self {
        let mut seen = IndexSet::new();
        seen.insert(root);
        Self { seen }
    }
    pub(crate) fn guard(&mut self) -> RecursionGuard<'_> {
        RecursionGuard(&mut self.seen)
    }
}
struct RecursionGuard<'a>(&'a mut IndexSet<NodeStr>);
impl RecursionGuard<'_> {
    fn push(&mut self, name: &NodeStr) -> RecursionGuard<'_> {
        debug_assert!(
            self.0.insert(name.clone()),
            "cannot push the same name twice to RecursionGuard, check contains() first"
        );
        RecursionGuard(self.0)
    }
    fn contains(&self, name: &NodeStr) -> bool {
        self.0.iter().any(|seen| seen == name)
    }
    fn first(&self) -> Option<&NodeStr> {
        self.0.first()
    }
}
impl Drop for RecursionGuard<'_> {
    fn drop(&mut self) {
        self.0.pop();
    }
}