use hir::{EditionedFileId, FileRange, HasCrate, HasSource, Semantics};
use ide_db::{RootDatabase, assists::Assist, source_change::SourceChange, text_edit::TextEdit};
use syntax::{
    AstNode, TextRange,
    ast::{HasName, HasVisibility},
};

use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext, fix};

// Diagnostic: private-field
//
// This diagnostic is triggered if the accessed field is not visible from the current module.
pub(crate) fn private_field(ctx: &DiagnosticsContext<'_, '_>, d: &hir::PrivateField) -> Diagnostic {
    Diagnostic::new_with_syntax_node_ptr(
        ctx,
        DiagnosticCode::RustcHardError("E0616"),
        format!(
            "field `{}` of `{}` is private",
            d.field.name(ctx.sema.db).display(ctx.sema.db, ctx.edition),
            d.field.parent_def(ctx.sema.db).name(ctx.sema.db).display(ctx.sema.db, ctx.edition)
        ),
        d.expr.map(|it| it.into()),
    )
    .stable()
    .with_fixes(field_is_private_fixes(
        &ctx.sema,
        d.expr.file_id.original_file(ctx.sema.db),
        d.field,
        ctx.sema.original_range(d.expr.to_node(ctx.sema.db).syntax()).range,
    ))
}

pub(crate) fn field_is_private_fixes(
    sema: &Semantics<'_, RootDatabase>,
    usage_file_id: EditionedFileId,
    private_field: hir::Field,
    fix_range: TextRange,
) -> Option<Vec<Assist>> {
    let def_crate = private_field.krate(sema.db);
    let usage_crate = sema.file_to_module_def(usage_file_id.file_id(sema.db))?.krate(sema.db);
    let mut visibility_text = if usage_crate == def_crate { "pub(crate) " } else { "pub " };

    let source = private_field.source(sema.db)?;
    let existing_visibility = match &source.value {
        hir::FieldSource::Named(it) => it.visibility(),
        hir::FieldSource::Pos(it) => it.visibility(),
    };
    let range = match existing_visibility {
        Some(visibility) => {
            // If there is an existing visibility, don't insert whitespace after.
            visibility_text = visibility_text.trim_end();
            source.with_value(visibility.syntax()).original_file_range_opt(sema.db)?.0
        }
        None => {
            let (range, _) = source
                .map(|it| {
                    Some(match it {
                        hir::FieldSource::Named(it) => {
                            it.unsafe_token().or(it.name()?.ident_token())?.text_range()
                        }
                        hir::FieldSource::Pos(it) => it.ty()?.syntax().text_range(),
                    })
                })
                .transpose()?
                .original_node_file_range_opt(sema.db)?;

            FileRange { file_id: range.file_id, range: TextRange::empty(range.range.start()) }
        }
    };
    let source_change = SourceChange::from_text_edit(
        range.file_id.file_id(sema.db),
        TextEdit::replace(range.range, visibility_text.into()),
    );

    Some(vec![fix(
        "increase_field_visibility",
        "Increase field visibility",
        source_change,
        fix_range,
    )])
}

#[cfg(test)]
mod tests {
    use crate::tests::{check_diagnostics, check_fix};

    #[test]
    fn private_field() {
        check_diagnostics(
            r#"
mod module { pub struct Struct { field: u32 } }
fn main(s: module::Struct) {
    s.field;
  //^^^^^^^ 💡 error: field `field` of `Struct` is private
}
"#,
        );
    }

    #[test]
    fn private_tuple_field() {
        check_diagnostics(
            r#"
mod module { pub struct Struct(u32); }
fn main(s: module::Struct) {
    s.0;
  //^^^ 💡 error: field `0` of `Struct` is private
}
"#,
        );
    }

    #[test]
    fn private_but_shadowed_in_deref() {
        check_diagnostics(
            r#"
//- minicore: deref
mod module {
    pub struct Struct { field: Inner }
    pub struct Inner { pub field: u32 }
    impl core::ops::Deref for Struct {
        type Target = Inner;
        fn deref(&self) -> &Inner { &self.field }
    }
}
fn main(s: module::Struct) {
    s.field;
}
"#,
        );
    }

    #[test]
    fn block_module_madness() {
        check_diagnostics(
            r#"
fn main() {
    let strukt = {
        use crate as ForceParentBlockDefMap;
        {
            pub struct Struct {
                field: (),
            }
            Struct { field: () }
        }
    };
    strukt.field;
}
"#,
        );
    }

    #[test]
    fn block_module_madness2() {
        check_diagnostics(
            r#"
fn main() {
    use crate as ForceParentBlockDefMap;
    let strukt = {
        use crate as ForceParentBlockDefMap;
        {
            pub struct Struct {
                field: (),
            }
            {
                use crate as ForceParentBlockDefMap;
                {
                    Struct { field: () }
                }
            }
        }
    };
    strukt.field;
}
"#,
        );
    }

    #[test]
    fn change_visibility_fix() {
        check_fix(
            r#"
pub mod foo {
    pub mod bar {
        pub struct Struct {
            field: i32,
        }
    }
}

fn foo(v: foo::bar::Struct) {
    v.field$0;
}
            "#,
            r#"
pub mod foo {
    pub mod bar {
        pub struct Struct {
            pub(crate) field: i32,
        }
    }
}

fn foo(v: foo::bar::Struct) {
    v.field;
}
            "#,
        );
    }

    #[test]
    fn change_visibility_with_existing_visibility() {
        check_fix(
            r#"
pub mod foo {
    pub mod bar {
        pub struct Struct {
            pub(super) field: i32,
        }
    }
}

fn foo(v: foo::bar::Struct) {
    v.field$0;
}
            "#,
            r#"
pub mod foo {
    pub mod bar {
        pub struct Struct {
            pub(crate) field: i32,
        }
    }
}

fn foo(v: foo::bar::Struct) {
    v.field;
}
            "#,
        );
    }

    #[test]
    fn change_visibility_of_field_with_doc_comment() {
        check_fix(
            r#"
pub mod foo {
    pub struct Foo {
        /// This is a doc comment
        bar: u32,
    }
}

fn main() {
    let x = foo::Foo { bar: 0 };
    x.bar$0;
}
            "#,
            r#"
pub mod foo {
    pub struct Foo {
        /// This is a doc comment
        pub(crate) bar: u32,
    }
}

fn main() {
    let x = foo::Foo { bar: 0 };
    x.bar;
}
            "#,
        );
    }

    #[test]
    fn change_visibility_of_field_with_line_comment() {
        check_fix(
            r#"
pub mod foo {
    pub struct Foo {
        // This is a line comment
        bar: u32,
    }
}

fn main() {
    let x = foo::Foo { bar: 0 };
    x.bar$0;
}
            "#,
            r#"
pub mod foo {
    pub struct Foo {
        // This is a line comment
        pub(crate) bar: u32,
    }
}

fn main() {
    let x = foo::Foo { bar: 0 };
    x.bar;
}
            "#,
        );
    }

    #[test]
    fn change_visibility_of_field_with_multiple_doc_comments() {
        check_fix(
            r#"
pub mod foo {
    pub struct Foo {
        /// First line
        /// Second line
        bar: u32,
    }
}

fn main() {
    let x = foo::Foo { bar: 0 };
    x.bar$0;
}
            "#,
            r#"
pub mod foo {
    pub struct Foo {
        /// First line
        /// Second line
        pub(crate) bar: u32,
    }
}

fn main() {
    let x = foo::Foo { bar: 0 };
    x.bar;
}
            "#,
        );
    }

    #[test]
    fn change_visibility_of_field_with_attr_and_comment() {
        check_fix(
            r#"
mod foo {
    pub struct Foo {
        #[rustfmt::skip]
        /// First line
        /// Second line
        bar: u32,
    }
}
fn main() {
    foo::Foo { $0bar: 42 };
}
            "#,
            r#"
mod foo {
    pub struct Foo {
        #[rustfmt::skip]
        /// First line
        /// Second line
        pub(crate) bar: u32,
    }
}
fn main() {
    foo::Foo { bar: 42 };
}
            "#,
        );
    }

    #[test]
    fn change_visibility_of_field_with_macro() {
        check_fix(
            r#"
macro_rules! allow_unused {
    ($vis:vis $struct:ident $name:ident { $($fvis:vis $field:ident : $ty:ty,)* }) => {
        $vis $struct $name {
            $(
                #[allow(unused)]
                $fvis $field : $ty,
            )*
        }
    };
}
mod foo {
    allow_unused!(
        pub struct Foo {
            x: i32,
        }
    );
}
fn main() {
    let foo = foo::Foo { x: 2 };
    let _ = foo.$0x
}
            "#,
            r#"
macro_rules! allow_unused {
    ($vis:vis $struct:ident $name:ident { $($fvis:vis $field:ident : $ty:ty,)* }) => {
        $vis $struct $name {
            $(
                #[allow(unused)]
                $fvis $field : $ty,
            )*
        }
    };
}
mod foo {
    allow_unused!(
        pub struct Foo {
            pub(crate) x: i32,
        }
    );
}
fn main() {
    let foo = foo::Foo { x: 2 };
    let _ = foo.x
}
            "#,
        );
    }
}
