use base_db::FileId;
use hir::{EnumVariant, Module, ModuleDef, Name};
use ide_db::{defs::Definition, search::Reference, RootDatabase};
use itertools::Itertools;
use rustc_hash::FxHashSet;
use syntax::{
    algo::find_node_at_offset,
    ast::{self, edit::IndentLevel, ArgListOwner, AstNode, NameOwner, VisibilityOwner},
    SourceFile, TextRange, TextSize,
};

use crate::{
    assist_context::AssistBuilder, utils::insert_use, AssistContext, AssistId, AssistKind, Assists,
};
use ast::make;
use insert_use::ImportScope;

// Assist: extract_struct_from_enum_variant
//
// Extracts a struct from enum variant.
//
// ```
// enum A { <|>One(u32, u32) }
// ```
// ->
// ```
// struct One(pub u32, pub u32);
//
// enum A { One(One) }
// ```
pub(crate) fn extract_struct_from_enum_variant(
    acc: &mut Assists,
    ctx: &AssistContext,
) -> Option<()> {
    let variant = ctx.find_node_at_offset::<ast::Variant>()?;
    let field_list = match variant.kind() {
        ast::StructKind::Tuple(field_list) => field_list,
        _ => return None,
    };
    let variant_name = variant.name()?.to_string();
    let variant_hir = ctx.sema.to_def(&variant)?;
    if existing_struct_def(ctx.db(), &variant_name, &variant_hir) {
        return None;
    }
    let enum_ast = variant.parent_enum();
    let visibility = enum_ast.visibility();
    let enum_hir = ctx.sema.to_def(&enum_ast)?;
    let variant_hir_name = variant_hir.name(ctx.db());
    let enum_module_def = ModuleDef::from(enum_hir);
    let current_module = enum_hir.module(ctx.db());
    let target = variant.syntax().text_range();
    acc.add(
        AssistId("extract_struct_from_enum_variant", AssistKind::RefactorRewrite),
        "Extract struct from enum variant",
        target,
        |builder| {
            let definition = Definition::ModuleDef(ModuleDef::EnumVariant(variant_hir));
            let res = definition.usages(&ctx.sema).all();
            let start_offset = variant.parent_enum().syntax().text_range().start();
            let mut visited_modules_set = FxHashSet::default();
            visited_modules_set.insert(current_module);
            for reference in res {
                let source_file = ctx.sema.parse(reference.file_range.file_id);
                update_reference(
                    ctx,
                    builder,
                    reference,
                    &source_file,
                    &enum_module_def,
                    &variant_hir_name,
                    &mut visited_modules_set,
                );
            }
            extract_struct_def(
                builder,
                &enum_ast,
                &variant_name,
                &field_list.to_string(),
                start_offset,
                ctx.frange.file_id,
                &visibility,
            );
            let list_range = field_list.syntax().text_range();
            update_variant(builder, &variant_name, ctx.frange.file_id, list_range);
        },
    )
}

fn existing_struct_def(db: &RootDatabase, variant_name: &str, variant: &EnumVariant) -> bool {
    variant
        .parent_enum(db)
        .module(db)
        .scope(db, None)
        .into_iter()
        .any(|(name, _)| name.to_string() == variant_name.to_string())
}

#[allow(dead_code)]
fn insert_import(
    ctx: &AssistContext,
    builder: &mut AssistBuilder,
    path: &ast::PathExpr,
    module: &Module,
    enum_module_def: &ModuleDef,
    variant_hir_name: &Name,
) -> Option<()> {
    let db = ctx.db();
    let mod_path = module.find_use_path(db, enum_module_def.clone());
    if let Some(mut mod_path) = mod_path {
        mod_path.segments.pop();
        mod_path.segments.push(variant_hir_name.clone());
        let scope = ImportScope::find_insert_use_container(path.syntax(), ctx)?;
        let syntax = scope.as_syntax_node();

        let new_syntax = insert_use(
            &scope,
            make::path_from_text(&mod_path.to_string()),
            ctx.config.insert_use.merge,
        );
        // FIXME: this will currently panic as multiple imports will have overlapping text ranges
        builder.replace(syntax.text_range(), new_syntax.to_string())
    }
    Some(())
}

// FIXME: this should use strongly-typed `make`, rather than string manipulation.
fn extract_struct_def(
    builder: &mut AssistBuilder,
    enum_: &ast::Enum,
    variant_name: &str,
    variant_list: &str,
    start_offset: TextSize,
    file_id: FileId,
    visibility: &Option<ast::Visibility>,
) -> Option<()> {
    let visibility_string = if let Some(visibility) = visibility {
        format!("{} ", visibility.to_string())
    } else {
        "".to_string()
    };
    let indent = IndentLevel::from_node(enum_.syntax());
    let struct_def = format!(
        r#"{}struct {}{};

{}"#,
        visibility_string,
        variant_name,
        list_with_visibility(variant_list),
        indent
    );
    builder.edit_file(file_id);
    builder.insert(start_offset, struct_def);
    Some(())
}

fn update_variant(
    builder: &mut AssistBuilder,
    variant_name: &str,
    file_id: FileId,
    list_range: TextRange,
) -> Option<()> {
    let inside_variant_range = TextRange::new(
        list_range.start().checked_add(TextSize::from(1))?,
        list_range.end().checked_sub(TextSize::from(1))?,
    );
    builder.edit_file(file_id);
    builder.replace(inside_variant_range, variant_name);
    Some(())
}

fn update_reference(
    ctx: &AssistContext,
    builder: &mut AssistBuilder,
    reference: Reference,
    source_file: &SourceFile,
    _enum_module_def: &ModuleDef,
    _variant_hir_name: &Name,
    _visited_modules_set: &mut FxHashSet<Module>,
) -> Option<()> {
    let path_expr: ast::PathExpr = find_node_at_offset::<ast::PathExpr>(
        source_file.syntax(),
        reference.file_range.range.start(),
    )?;
    let call = path_expr.syntax().parent().and_then(ast::CallExpr::cast)?;
    let list = call.arg_list()?;
    let segment = path_expr.path()?.segment()?;
    let _module = ctx.sema.scope(&path_expr.syntax()).module()?;
    let list_range = list.syntax().text_range();
    let inside_list_range = TextRange::new(
        list_range.start().checked_add(TextSize::from(1))?,
        list_range.end().checked_sub(TextSize::from(1))?,
    );
    builder.edit_file(reference.file_range.file_id);
    /* FIXME: this most likely requires AST-based editing, see `insert_import`
    if !visited_modules_set.contains(&module) {
        if insert_import(ctx, builder, &path_expr, &module, enum_module_def, variant_hir_name)
            .is_some()
        {
            visited_modules_set.insert(module);
        }
    }
    */
    builder.replace(inside_list_range, format!("{}{}", segment, list));
    Some(())
}

fn list_with_visibility(list: &str) -> String {
    list.split(',')
        .map(|part| {
            let index = if part.chars().next().unwrap() == '(' { 1usize } else { 0 };
            let mut mod_part = part.trim().to_string();
            mod_part.insert_str(index, "pub ");
            mod_part
        })
        .join(", ")
}

#[cfg(test)]
mod tests {
    use crate::{
        tests::{check_assist, check_assist_not_applicable},
        utils::FamousDefs,
    };

    use super::*;

    #[test]
    fn test_extract_struct_several_fields() {
        check_assist(
            extract_struct_from_enum_variant,
            "enum A { <|>One(u32, u32) }",
            r#"struct One(pub u32, pub u32);

enum A { One(One) }"#,
        );
    }

    #[test]
    fn test_extract_struct_one_field() {
        check_assist(
            extract_struct_from_enum_variant,
            "enum A { <|>One(u32) }",
            r#"struct One(pub u32);

enum A { One(One) }"#,
        );
    }

    #[test]
    fn test_extract_struct_pub_visibility() {
        check_assist(
            extract_struct_from_enum_variant,
            "pub enum A { <|>One(u32, u32) }",
            r#"pub struct One(pub u32, pub u32);

pub enum A { One(One) }"#,
        );
    }

    #[test]
    #[ignore] // FIXME: this currently panics if `insert_import` is used
    fn test_extract_struct_with_complex_imports() {
        check_assist(
            extract_struct_from_enum_variant,
            r#"mod my_mod {
    fn another_fn() {
        let m = my_other_mod::MyEnum::MyField(1, 1);
    }

    pub mod my_other_mod {
        fn another_fn() {
            let m = MyEnum::MyField(1, 1);
        }

        pub enum MyEnum {
            <|>MyField(u8, u8),
        }
    }
}

fn another_fn() {
    let m = my_mod::my_other_mod::MyEnum::MyField(1, 1);
}"#,
            r#"use my_mod::my_other_mod::MyField;

mod my_mod {
    use my_other_mod::MyField;

    fn another_fn() {
        let m = my_other_mod::MyEnum::MyField(MyField(1, 1));
    }

    pub mod my_other_mod {
        fn another_fn() {
            let m = MyEnum::MyField(MyField(1, 1));
        }

        pub struct MyField(pub u8, pub u8);

        pub enum MyEnum {
            MyField(MyField),
        }
    }
}

fn another_fn() {
    let m = my_mod::my_other_mod::MyEnum::MyField(MyField(1, 1));
}"#,
        );
    }

    fn check_not_applicable(ra_fixture: &str) {
        let fixture =
            format!("//- /main.rs crate:main deps:core\n{}\n{}", ra_fixture, FamousDefs::FIXTURE);
        check_assist_not_applicable(extract_struct_from_enum_variant, &fixture)
    }

    #[test]
    fn test_extract_enum_not_applicable_for_element_with_no_fields() {
        check_not_applicable("enum A { <|>One }");
    }

    #[test]
    fn test_extract_enum_not_applicable_if_struct_exists() {
        check_not_applicable(
            r#"struct One;
        enum A { <|>One(u8) }"#,
        );
    }
}