use std::{fmt, sync::Arc};
use crate::database::hir::{DirectiveLocation, HirNodeLocation};
use crate::database::{InputDatabase, SourceCache};
use crate::FileId;
use thiserror::Error;
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct DiagnosticLocation {
    file_id: FileId,
    offset: usize,
    length: usize,
}
impl ariadne::Span for DiagnosticLocation {
    type SourceId = FileId;
    fn source(&self) -> &FileId {
        &self.file_id
    }
    fn start(&self) -> usize {
        self.offset
    }
    fn end(&self) -> usize {
        self.offset + self.length
    }
}
impl DiagnosticLocation {
    pub fn file_id(&self) -> FileId {
        self.file_id
    }
    pub fn offset(&self) -> usize {
        self.offset
    }
    pub fn node_len(&self) -> usize {
        self.length
    }
}
impl From<(FileId, rowan::TextRange)> for DiagnosticLocation {
    fn from((file_id, range): (FileId, rowan::TextRange)) -> Self {
        Self {
            file_id,
            offset: range.start().into(),
            length: range.len().into(),
        }
    }
}
impl From<(FileId, usize, usize)> for DiagnosticLocation {
    fn from((file_id, offset, length): (FileId, usize, usize)) -> Self {
        Self {
            file_id,
            offset,
            length,
        }
    }
}
impl From<HirNodeLocation> for DiagnosticLocation {
    fn from(location: HirNodeLocation) -> Self {
        Self {
            file_id: location.file_id(),
            offset: location.offset(),
            length: location.node_len(),
        }
    }
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Label {
    pub location: DiagnosticLocation,
    pub text: String,
}
impl Label {
    pub fn new(location: impl Into<DiagnosticLocation>, text: impl Into<String>) -> Self {
        Self {
            location: location.into(),
            text: text.into(),
        }
    }
}
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub struct ApolloDiagnostic {
    cache: Arc<SourceCache>,
    pub location: DiagnosticLocation,
    pub labels: Vec<Label>,
    pub help: Option<String>,
    pub data: Box<DiagnosticData>,
}
impl ApolloDiagnostic {
    pub fn new<DB: InputDatabase + ?Sized>(
        db: &DB,
        location: DiagnosticLocation,
        data: DiagnosticData,
    ) -> Self {
        Self {
            cache: db.source_cache(),
            location,
            labels: vec![],
            help: None,
            data: Box::new(data),
        }
    }
    pub fn help(self, help: impl Into<String>) -> Self {
        Self {
            help: Some(help.into()),
            ..self
        }
    }
    pub fn labels(self, labels: impl Into<Vec<Label>>) -> Self {
        Self {
            labels: labels.into(),
            ..self
        }
    }
    pub fn label(mut self, label: Label) -> Self {
        self.labels.push(label);
        self
    }
}
impl fmt::Display for ApolloDiagnostic {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let mut buf = std::io::Cursor::new(Vec::<u8>::new());
        self.to_report()
            .write(self.cache.as_ref(), &mut buf)
            .unwrap();
        writeln!(f, "{}", std::str::from_utf8(&buf.into_inner()).unwrap())
    }
}
#[derive(Debug, Error, Clone, Hash, PartialEq, Eq)]
#[non_exhaustive]
pub enum DiagnosticData {
    #[error("syntax error: {message}")]
    SyntaxError { message: String },
    #[error("limit exceeded: {message}")]
    LimitExceeded { message: String },
    #[error("expected identifier")]
    MissingIdent,
    #[error("executable documents must not contain {kind}")]
    ExecutableDefinition { kind: &'static str },
    #[error("the {ty} `{name}` is defined multiple times in the document")]
    UniqueDefinition {
        ty: &'static str,
        name: String,
        original_definition: DiagnosticLocation,
        redefined_definition: DiagnosticLocation,
    },
    #[error("the argument `{name}` is defined multiple times")]
    UniqueArgument {
        name: String,
        original_definition: DiagnosticLocation,
        redefined_definition: DiagnosticLocation,
    },
    #[error("the value `{name}` is defined multiple times")]
    UniqueInputValue {
        name: String,
        original_value: DiagnosticLocation,
        redefined_value: DiagnosticLocation,
    },
    #[error("subscription operations can only have one root field")]
    SingleRootField {
        fields: usize,
        subscription: DiagnosticLocation,
    },
    #[error("{ty} root operation type is not defined")]
    UnsupportedOperation {
        ty: &'static str,
    },
    #[error("cannot query `{field}` field")]
    UndefinedField {
        field: String,
    },
    #[error("the argument `{name}` is not supported")]
    UndefinedArgument { name: String },
    #[error("cannot find type `{name}` in this document")]
    UndefinedDefinition {
        name: String,
    },
    #[error("variable `{name}` is not defined")]
    UndefinedVariable {
        name: String,
    },
    #[error("cannot find fragment `{name}` in this document")]
    UndefinedFragment {
        name: String,
    },
    #[error("value `{value}` does not exist on `{definition}` type")]
    UndefinedValue {
        value: String,
        definition: String,
    },
    #[error("type extension for `{name}` is the wrong kind")]
    WrongTypeExtension {
        name: String,
        definition: DiagnosticLocation,
        extension: DiagnosticLocation,
    },
    #[error("`{name}` directive definition cannot reference itself")]
    RecursiveDirectiveDefinition { name: String },
    #[error("interface {name} cannot implement itself")]
    RecursiveInterfaceDefinition { name: String },
    #[error("`{name}` input object cannot reference itself")]
    RecursiveInputObjectDefinition { name: String },
    #[error("`{name}` fragment cannot reference itself")]
    RecursiveFragmentDefinition { name: String },
    #[error("values in an Enum Definition should be capitalized")]
    CapitalizedValue { value: String },
    #[error("fields must be unique in a definition")]
    UniqueField {
        field: String,
        original_definition: DiagnosticLocation,
        redefined_definition: DiagnosticLocation,
    },
    #[error("missing `{field}` field")]
    MissingField {
        field: String,
    },
    #[error("the required argument `{name}` is not provided")]
    RequiredArgument { name: String },
    #[error("type `{ty}` can only implement interface `{interface}` once")]
    DuplicateImplementsInterface { ty: String, interface: String },
    #[error(
        "Transitively implemented interfaces must also be defined on an implementing interface or object"
    )]
    TransitiveImplementedInterfaces {
        missing_interface: String,
    },
    #[error("`{name}` field must return an output type")]
    OutputType {
        name: String,
        ty: &'static str,
    },
    #[error("`${name}` variable must be of an input type")]
    InputType {
        name: String,
        ty: &'static str,
    },
    #[error(
        "custom scalars should provide a scalar specification URL via the @specifiedBy directive"
    )]
    ScalarSpecificationURL,
    #[error("missing query root operation type in schema definition")]
    QueryRootOperationType,
    #[error("built-in scalars must be omitted for brevity")]
    BuiltInScalarDefinition,
    #[error("unused variable: `{name}`")]
    UnusedVariable { name: String },
    #[error("`{name}` field must return an object type")]
    ObjectType {
        name: String,
        ty: &'static str,
    },
    #[error("{name} directive is not supported for {dir_loc} location")]
    UnsupportedLocation {
        name: String,
        dir_loc: DirectiveLocation,
        directive_def: DiagnosticLocation,
    },
    #[error("{ty} cannot be represented by a {value} value")]
    UnsupportedValueType {
        value: String,
        ty: String,
    },
    #[error("int cannot represent non 32-bit signed integer value")]
    IntCoercionError {
        value: String,
    },
    #[error("non-repeatable directive {name} can only be used once per location")]
    UniqueDirective {
        name: String,
        original_call: DiagnosticLocation,
        conflicting_call: DiagnosticLocation,
    },
    #[error("subscription operations can not have an introspection field as a root field")]
    IntrospectionField {
        field: String,
    },
    #[error("subselection set for scalar and enum types must be empty")]
    DisallowedSubselection,
    #[error("interface, union and object types must have a subselection set")]
    MissingSubselection,
    #[error("operation must not select different types using the same field name `{field}`")]
    ConflictingField {
        field: String,
        original_selection: DiagnosticLocation,
        redefined_selection: DiagnosticLocation,
    },
    #[error("fragments must be specified on types that exist in the schema")]
    InvalidFragment {
        ty: Option<String>,
    },
    #[error("fragments can not be declared on primitive types")]
    InvalidFragmentTarget {
        ty: String,
    },
    #[error("fragment cannot be applied to this type")]
    InvalidFragmentSpread {
        name: Option<String>,
        type_name: String,
    },
    #[error("fragment `{name}` must be used in an operation")]
    UnusedFragment {
        name: String,
    },
    #[error(
        "variable `{var_name}` cannot be used for argument `{arg_name}` as their types mismatch"
    )]
    DisallowedVariableUsage {
        var_name: String,
        arg_name: String,
    },
}
impl DiagnosticData {
    pub fn is_error(&self) -> bool {
        !self.is_warning() && !self.is_advice()
    }
    pub fn is_warning(&self) -> bool {
        matches!(self, Self::CapitalizedValue { .. })
    }
    pub fn is_advice(&self) -> bool {
        matches!(self, Self::ScalarSpecificationURL)
    }
}
impl From<Label> for ariadne::Label<DiagnosticLocation> {
    fn from(label: Label) -> Self {
        Self::new(label.location).with_message(label.text)
    }
}
impl ApolloDiagnostic {
    pub fn to_report(&self) -> ariadne::Report<'static, DiagnosticLocation> {
        use ariadne::{ColorGenerator, Report, ReportKind};
        let severity = if self.data.is_advice() {
            ReportKind::Advice
        } else if self.data.is_warning() {
            ReportKind::Warning
        } else {
            ReportKind::Error
        };
        let mut colors = ColorGenerator::new();
        let mut builder = Report::build(severity, self.location.file_id(), self.location.offset())
            .with_message(&self.data);
        builder.add_labels(
            self.labels
                .iter()
                .map(|label| ariadne::Label::from(label.clone()).with_color(colors.next())),
        );
        if let Some(help) = &self.help {
            builder = builder.with_help(help);
        }
        builder.finish()
    }
}