use either::Either;
use hir::{HasSource, HirDisplay, Semantics, VariantId, db::ExpandDatabase};
use ide_db::text_edit::TextEdit;
use ide_db::{EditionedFileId, RootDatabase, source_change::SourceChange};
use syntax::{
    AstNode,
    ast::{self, edit::IndentLevel, make},
};

use crate::{
    Assist, Diagnostic, DiagnosticCode, DiagnosticsContext, fix,
    handlers::private_field::field_is_private_fixes,
};

// Diagnostic: no-such-field
//
// This diagnostic is triggered if created structure does not have field provided in record.
pub(crate) fn no_such_field(ctx: &DiagnosticsContext<'_, '_>, d: &hir::NoSuchField) -> Diagnostic {
    let (code, message) = if d.private.is_some() {
        ("E0451", "field is private")
    } else if let VariantId::EnumVariantId(_) = d.variant {
        ("E0559", "no such field")
    } else {
        ("E0560", "no such field")
    };

    let node = d.field.map(Into::into);
    Diagnostic::new_with_syntax_node_ptr(ctx, DiagnosticCode::RustcHardError(code), message, node)
        .stable()
        .with_fixes(fixes(ctx, d))
}

fn fixes(ctx: &DiagnosticsContext<'_, '_>, d: &hir::NoSuchField) -> Option<Vec<Assist>> {
    // FIXME: quickfix for pattern
    let root = ctx.sema.db.parse_or_expand(d.field.file_id);
    match &d.field.value.to_node(&root) {
        Either::Left(node) => {
            if let Some(private_field) = d.private {
                field_is_private_fixes(
                    &ctx.sema,
                    d.field.file_id.original_file(ctx.sema.db),
                    private_field,
                    ctx.sema.original_range(node.syntax()).range,
                )
            } else {
                missing_record_expr_field_fixes(
                    &ctx.sema,
                    d.field.file_id.original_file(ctx.sema.db),
                    node,
                )
            }
        }
        _ => None,
    }
}

fn missing_record_expr_field_fixes(
    sema: &Semantics<'_, RootDatabase>,
    usage_file_id: EditionedFileId,
    record_expr_field: &ast::RecordExprField,
) -> Option<Vec<Assist>> {
    let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?;
    let def_id = sema.resolve_variant(record_lit)?;
    let module;
    let def_file_id;
    let record_fields = match def_id {
        hir::Variant::Struct(s) => {
            module = s.module(sema.db);
            let source = s.source(sema.db)?;
            def_file_id = source.file_id;
            let fields = source.value.field_list()?;
            record_field_list(fields)?
        }
        hir::Variant::Union(u) => {
            module = u.module(sema.db);
            let source = u.source(sema.db)?;
            def_file_id = source.file_id;
            source.value.record_field_list()?
        }
        hir::Variant::EnumVariant(e) => {
            module = e.module(sema.db);
            let source = e.source(sema.db)?;
            def_file_id = source.file_id;
            let fields = source.value.field_list()?;
            record_field_list(fields)?
        }
    };
    let def_file_id = def_file_id.original_file(sema.db);

    let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?.adjusted();
    if new_field_type.is_unknown() {
        return None;
    }
    let new_field = make::record_field(
        None,
        make::name(record_expr_field.field_name()?.ident_token()?.text()),
        make::ty(&new_field_type.display_source_code(sema.db, module.into(), true).ok()?),
    );

    let (indent, offset, postfix, needs_comma) =
        if let Some(last_field) = record_fields.fields().last() {
            let indent = IndentLevel::from_node(last_field.syntax());
            let offset = last_field.syntax().text_range().end();
            let needs_comma = !last_field.to_string().ends_with(',');
            (indent, offset, String::new(), needs_comma)
        } else {
            let indent = IndentLevel::from_node(record_fields.syntax());
            let offset = record_fields.l_curly_token()?.text_range().end();
            let postfix = if record_fields.syntax().text().contains_char('\n') {
                ",".into()
            } else {
                format!(",\n{indent}")
            };
            (indent + 1, offset, postfix, false)
        };

    let mut new_field = new_field.to_string();
    // FIXME: check submodule instead of FileId
    let vis = if usage_file_id != def_file_id && !matches!(def_id, hir::Variant::EnumVariant(_)) {
        "pub(crate) "
    } else {
        ""
    };
    let comma = if needs_comma { "," } else { "" };
    new_field = format!("{comma}\n{indent}{vis}{new_field}{postfix}");

    let source_change = SourceChange::from_text_edit(
        def_file_id.file_id(sema.db),
        TextEdit::insert(offset, new_field),
    );

    return Some(vec![fix(
        "create_field",
        "Create field",
        source_change,
        sema.original_range(record_expr_field.syntax()).range,
    )]);

    fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
        match field_def_list {
            ast::FieldList::RecordFieldList(it) => Some(it),
            ast::FieldList::TupleFieldList(_) => None,
        }
    }
}

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

    #[test]
    fn dont_work_for_field_with_disabled_cfg() {
        check_diagnostics(
            r#"
struct Test {
    #[cfg(feature = "hello")]
    test: u32,
    other: u32
}

fn main() {
    let a = Test {
        #[cfg(feature = "hello")]
        test: 1,
        other: 1
    };

    let Test {
        #[cfg(feature = "hello")]
        test,
        mut other,
        ..
    } = a;

    other += 1;
}
"#,
        );
    }

    #[test]
    fn no_such_field_diagnostics() {
        check_diagnostics(
            r#"
struct S { foo: i32, bar: () }
impl S {
    fn new(
        s@S {
        //^ 💡 error: missing structure fields:
        //|    - bar
            foo,
            baz: baz2,
          //^^^^^^^^^ error: no such field
            qux
          //^^^ error: no such field
        }: S
    ) -> S {
        S {
      //^ 💡 error: missing structure fields:
      //|    - bar
            foo,
            baz: baz2,
          //^^^^^^^^^ error: no such field
            qux
          //^^^ error: no such field
        } = s;
        S {
      //^ 💡 error: missing structure fields:
      //|    - bar
            foo: 92,
            baz: 62,
          //^^^^^^^ 💡 error: no such field
            qux
          //^^^ error: no such field
        }
    }
}
"#,
        );
    }
    #[test]
    fn no_such_field_with_feature_flag_diagnostics() {
        check_diagnostics(
            r#"
//- /lib.rs crate:foo cfg:feature=foo
struct MyStruct {
    my_val: usize,
    #[cfg(feature = "foo")]
    bar: bool,
}

impl MyStruct {
    #[cfg(feature = "foo")]
    pub(crate) fn new(my_val: usize, bar: bool) -> Self {
        Self { my_val, bar }
    }
    #[cfg(not(feature = "foo"))]
    pub(crate) fn new(my_val: usize, _bar: bool) -> Self {
        Self { my_val }
    }
}
"#,
        );
    }

    #[test]
    fn no_such_field_enum_with_feature_flag_diagnostics() {
        check_diagnostics(
            r#"
//- /lib.rs crate:foo cfg:feature=foo
enum Foo {
    #[cfg(not(feature = "foo"))]
    Buz,
    #[cfg(feature = "foo")]
    Bar,
    Baz
}

fn test_fn(f: Foo) {
    match f {
        Foo::Bar => {},
        Foo::Baz => {},
    }
}
"#,
        );
    }

    #[test]
    fn no_such_field_with_feature_flag_diagnostics_on_struct_lit() {
        check_diagnostics(
            r#"
//- /lib.rs crate:foo cfg:feature=foo
struct S {
    #[cfg(feature = "foo")]
    foo: u32,
    #[cfg(not(feature = "foo"))]
    bar: u32,
}

impl S {
    #[cfg(feature = "foo")]
    fn new(foo: u32) -> Self {
        Self { foo }
    }
    #[cfg(not(feature = "foo"))]
    fn new(bar: u32) -> Self {
        Self { bar }
    }
    fn new2(bar: u32) -> Self {
        #[cfg(feature = "foo")]
        { Self { foo: bar } }
        #[cfg(not(feature = "foo"))]
        { Self { bar } }
    }
    fn new2(val: u32) -> Self {
        Self {
            #[cfg(feature = "foo")]
            foo: val,
            #[cfg(not(feature = "foo"))]
            bar: val,
        }
    }
}
"#,
        );
    }

    #[test]
    fn no_such_field_with_type_macro() {
        check_diagnostics(
            r#"
macro_rules! Type { () => { u32 }; }
struct Foo { bar: Type![] }

impl Foo {
    fn new() -> Self {
        Foo { bar: 0 }
    }
}
"#,
        );
    }

    #[test]
    fn test_add_field_from_usage() {
        check_fix(
            r"
fn main() {
    Foo { bar: 3, baz$0: false};
}
struct Foo {
    bar: i32
}
",
            r"
fn main() {
    Foo { bar: 3, baz: false};
}
struct Foo {
    bar: i32,
    baz: bool
}
",
        )
    }

    #[test]
    fn test_add_field_from_usage_with_empty_struct() {
        check_fix(
            r"
fn main() {
    Foo { bar$0: false };
}
struct Foo {}
",
            r"
fn main() {
    Foo { bar: false };
}
struct Foo {
    bar: bool,
}
",
        );

        check_fix(
            r"
fn main() {
    Foo { bar$0: false };
}
struct Foo {
}
",
            r"
fn main() {
    Foo { bar: false };
}
struct Foo {
    bar: bool,
}
",
        );
    }

    #[test]
    fn test_add_field_in_other_file_from_usage() {
        check_fix(
            r#"
//- /main.rs
mod foo;

fn main() {
    foo::Foo { bar: 3, $0baz: false};
}
//- /foo.rs
pub struct Foo {
    bar: i32
}
"#,
            r#"
pub struct Foo {
    bar: i32,
    pub(crate) baz: bool
}
"#,
        )
    }

    #[test]
    fn test_add_enum_variant_field_in_other_file_from_usage() {
        check_fix(
            r#"
//- /main.rs
mod foo;

fn main() {
    foo::Foo::Variant { bar: 3, $0baz: false};
}
//- /foo.rs
pub enum Foo {
    Variant {
        bar: i32
    }
}
"#,
            r#"
pub enum Foo {
    Variant {
        bar: i32,
        baz: bool
    }
}
"#,
        )
    }

    #[test]
    fn test_tuple_field_on_record_struct() {
        check_no_fix(
            r#"
struct Struct {}
fn main() {
    Struct {
        0$0: 0
    }
}
"#,
        )
    }

    #[test]
    fn test_struct_field_private() {
        check_diagnostics(
            r#"
mod m {
    pub struct Struct {
        field: u32,
        field2: u32,
    }
}
fn f(s@m::Struct {
    field: f,
  //^^^^^^^^ error: field is private
    field2
  //^^^^^^ error: field is private
}: m::Struct) {
    // assignee expression
    m::Struct {
        field: 0,
      //^^^^^^^^ 💡 error: field is private
        field2
      //^^^^^^ 💡 error: field is private
    } = s;
    m::Struct {
        field: 0,
      //^^^^^^^^ 💡 error: field is private
        field2
      //^^^^^^ 💡 error: field is private
    };
}
"#,
        )
    }

    #[test]
    fn test_struct_field_private_same_crate_fix() {
        check_diagnostics(
            r#"
mod m {
    pub struct Struct {
        field: u32,
    }
}
fn f() {
    let _ = m::Struct {
        field: 0,
      //^^^^^^^^ 💡 error: field is private
    };
}
"#,
        );

        check_fix(
            r#"
mod m {
    pub struct Struct {
        field: u32,
    }
}
fn f() {
    let _ = m::Struct {
        field$0: 0,
    };
}
"#,
            r#"
mod m {
    pub struct Struct {
        pub(crate) field: u32,
    }
}
fn f() {
    let _ = m::Struct {
        field: 0,
    };
}
"#,
        );
    }

    #[test]
    fn test_struct_field_private_other_crate_fix() {
        check_fix(
            r#"
//- /lib.rs crate:another_crate
pub struct Struct {
    field: u32,
}
//- /lib.rs crate:this_crate deps:another_crate
use another_crate;

fn f() {
    let _ = another_crate::Struct {
        field$0: 0,
    };
}
"#,
            r#"
pub struct Struct {
    pub field: u32,
}
"#,
        );
    }

    #[test]
    fn editions_between_macros() {
        check_diagnostics(
            r#"
//- /edition2015.rs crate:edition2015 edition:2015
#[macro_export]
macro_rules! pass_expr_thorough {
    ($e:expr) => { $e };
}

//- /edition2018.rs crate:edition2018 deps:edition2015 edition:2018
async fn bar() {}
async fn foo() {
    edition2015::pass_expr_thorough!(bar().await);
}
        "#,
        );
        check_diagnostics(
            r#"
//- /edition2018.rs crate:edition2018 edition:2018
pub async fn bar() {}
#[macro_export]
macro_rules! make_await {
    () => { async { $crate::bar().await }; };
}

//- /edition2015.rs crate:edition2015 deps:edition2018 edition:2015
fn foo() {
    edition2018::make_await!();
}
        "#,
        );
    }
}
