use std::fmt;

use hir::{Field, HirDisplay, Layout, Semantics, Type};
use ide_db::{
    defs::Definition,
    helpers::{get_definition, pick_best_token},
    RootDatabase,
};
use span::Edition;
use syntax::{AstNode, SyntaxKind};

use crate::FilePosition;

pub struct MemoryLayoutNode {
    pub item_name: String,
    pub typename: String,
    pub size: u64,
    pub alignment: u64,
    pub offset: u64,
    pub parent_idx: i64,
    pub children_start: i64,
    pub children_len: u64,
}

pub struct RecursiveMemoryLayout {
    pub nodes: Vec<MemoryLayoutNode>,
}

// NOTE: this is currently strictly for testing and so isn't super useful as a visualization tool, however it could be adapted to become one?
impl fmt::Display for RecursiveMemoryLayout {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        fn process(
            fmt: &mut fmt::Formatter<'_>,
            nodes: &Vec<MemoryLayoutNode>,
            idx: usize,
            depth: usize,
        ) -> fmt::Result {
            let mut out = "\t".repeat(depth);
            let node = &nodes[idx];
            out += &format!(
                "{}: {} (size: {}, align: {}, field offset: {})\n",
                node.item_name, node.typename, node.size, node.alignment, node.offset
            );
            write!(fmt, "{out}")?;
            if node.children_start != -1 {
                for j in nodes[idx].children_start
                    ..(nodes[idx].children_start + nodes[idx].children_len as i64)
                {
                    process(fmt, nodes, j as usize, depth + 1)?;
                }
            }
            Ok(())
        }

        process(fmt, &self.nodes, 0, 0)
    }
}

#[derive(Copy, Clone)]
enum FieldOrTupleIdx {
    Field(Field),
    TupleIdx(usize),
}

impl FieldOrTupleIdx {
    fn name(&self, db: &RootDatabase) -> String {
        match *self {
            FieldOrTupleIdx::Field(f) => f.name(db).as_str().to_owned(),
            FieldOrTupleIdx::TupleIdx(i) => format!(".{i}"),
        }
    }
}

// Feature: View Memory Layout
//
// Displays the recursive memory layout of a datatype.
//
// |===
// | Editor  | Action Name
//
// | VS Code | **rust-analyzer: View Memory Layout**
// |===
pub(crate) fn view_memory_layout(
    db: &RootDatabase,
    position: FilePosition,
) -> Option<RecursiveMemoryLayout> {
    let sema = Semantics::new(db);
    let file = sema.parse_guess_edition(position.file_id);
    let edition = sema
        .attach_first_edition(position.file_id)
        .map(|it| it.edition())
        .unwrap_or(Edition::CURRENT);
    let token =
        pick_best_token(file.syntax().token_at_offset(position.offset), |kind| match kind {
            SyntaxKind::IDENT => 3,
            _ => 0,
        })?;

    let def = get_definition(&sema, token)?;

    let ty = match def {
        Definition::Adt(it) => it.ty(db),
        Definition::TypeAlias(it) => it.ty(db),
        Definition::BuiltinType(it) => it.ty(db),
        Definition::SelfType(it) => it.self_ty(db),
        Definition::Local(it) => it.ty(db),
        Definition::Field(it) => it.ty(db),
        Definition::Const(it) => it.ty(db),
        Definition::Static(it) => it.ty(db),
        _ => return None,
    };

    fn read_layout(
        nodes: &mut Vec<MemoryLayoutNode>,
        db: &RootDatabase,
        ty: &Type,
        layout: &Layout,
        parent_idx: usize,
        edition: Edition,
    ) {
        let mut fields = ty
            .fields(db)
            .into_iter()
            .map(|(f, ty)| (FieldOrTupleIdx::Field(f), ty))
            .chain(
                ty.tuple_fields(db)
                    .into_iter()
                    .enumerate()
                    .map(|(i, ty)| (FieldOrTupleIdx::TupleIdx(i), ty)),
            )
            .collect::<Vec<_>>();

        if fields.is_empty() {
            return;
        }

        fields.sort_by_key(|&(f, _)| match f {
            FieldOrTupleIdx::Field(f) => layout.field_offset(f).unwrap_or(0),
            FieldOrTupleIdx::TupleIdx(f) => layout.tuple_field_offset(f).unwrap_or(0),
        });

        let children_start = nodes.len();
        nodes[parent_idx].children_start = children_start as i64;
        nodes[parent_idx].children_len = fields.len() as u64;

        for (field, child_ty) in fields.iter() {
            if let Ok(child_layout) = child_ty.layout(db) {
                nodes.push(MemoryLayoutNode {
                    item_name: field.name(db),
                    typename: child_ty.display(db, edition).to_string(),
                    size: child_layout.size(),
                    alignment: child_layout.align(),
                    offset: match *field {
                        FieldOrTupleIdx::Field(f) => layout.field_offset(f).unwrap_or(0),
                        FieldOrTupleIdx::TupleIdx(f) => layout.tuple_field_offset(f).unwrap_or(0),
                    },
                    parent_idx: parent_idx as i64,
                    children_start: -1,
                    children_len: 0,
                });
            } else {
                nodes.push(MemoryLayoutNode {
                    item_name: field.name(db)
                        + format!("(no layout data: {:?})", child_ty.layout(db).unwrap_err())
                            .as_ref(),
                    typename: child_ty.display(db, edition).to_string(),
                    size: 0,
                    offset: 0,
                    alignment: 0,
                    parent_idx: parent_idx as i64,
                    children_start: -1,
                    children_len: 0,
                });
            }
        }

        for (i, (_, child_ty)) in fields.iter().enumerate() {
            if let Ok(child_layout) = child_ty.layout(db) {
                read_layout(nodes, db, child_ty, &child_layout, children_start + i, edition);
            }
        }
    }

    ty.layout(db)
        .map(|layout| {
            let item_name = match def {
                // def is a datatype
                Definition::Adt(_)
                | Definition::TypeAlias(_)
                | Definition::BuiltinType(_)
                | Definition::SelfType(_) => "[ROOT]".to_owned(),

                // def is an item
                def => def.name(db).map(|n| n.as_str().to_owned()).unwrap_or("[ROOT]".to_owned()),
            };

            let typename = ty.display(db, edition).to_string();

            let mut nodes = vec![MemoryLayoutNode {
                item_name,
                typename,
                size: layout.size(),
                offset: 0,
                alignment: layout.align(),
                parent_idx: -1,
                children_start: -1,
                children_len: 0,
            }];
            read_layout(&mut nodes, db, &ty, &layout, 0, edition);

            RecursiveMemoryLayout { nodes }
        })
        .ok()
}

#[cfg(test)]
mod tests {
    use super::*;

    use crate::fixture;
    use expect_test::expect;

    fn make_memory_layout(ra_fixture: &str) -> Option<RecursiveMemoryLayout> {
        let (analysis, position, _) = fixture::annotations(ra_fixture);

        view_memory_layout(&analysis.db, position)
    }

    #[test]
    fn view_memory_layout_none() {
        assert!(make_memory_layout(r#"$0"#).is_none());
        assert!(make_memory_layout(r#"stru$0ct Blah {}"#).is_none());
    }

    #[test]
    fn view_memory_layout_primitive() {
        expect![[r#"
            foo: i32 (size: 4, align: 4, field offset: 0)
        "#]]
        .assert_eq(
            &make_memory_layout(
                r#"
fn main() {
    let foo$0 = 109; // default i32
}
"#,
            )
            .unwrap()
            .to_string(),
        );
    }

    #[test]
    fn view_memory_layout_constant() {
        expect![[r#"
            BLAH: bool (size: 1, align: 1, field offset: 0)
        "#]]
        .assert_eq(
            &make_memory_layout(
                r#"
const BLAH$0: bool = 0;
"#,
            )
            .unwrap()
            .to_string(),
        );
    }

    #[test]
    fn view_memory_layout_static() {
        expect![[r#"
            BLAH: bool (size: 1, align: 1, field offset: 0)
        "#]]
        .assert_eq(
            &make_memory_layout(
                r#"
static BLAH$0: bool = 0;
"#,
            )
            .unwrap()
            .to_string(),
        );
    }

    #[test]
    fn view_memory_layout_tuple() {
        expect![[r#"
            x: (f64, u8, i64) (size: 24, align: 8, field offset: 0)
            	.0: f64 (size: 8, align: 8, field offset: 0)
            	.1: u8 (size: 1, align: 1, field offset: 8)
            	.2: i64 (size: 8, align: 8, field offset: 16)
        "#]]
        .assert_eq(
            &make_memory_layout(
                r#"
fn main() {
    let x$0 = (101.0, 111u8, 119i64);
}
"#,
            )
            .unwrap()
            .to_string(),
        );
    }

    #[test]
    fn view_memory_layout_c_struct() {
        expect![[r#"
            [ROOT]: Blah (size: 16, align: 4, field offset: 0)
            	a: u32 (size: 4, align: 4, field offset: 0)
            	b: (i32, u8) (size: 8, align: 4, field offset: 4)
            		.0: i32 (size: 4, align: 4, field offset: 0)
            		.1: u8 (size: 1, align: 1, field offset: 4)
            	c: i8 (size: 1, align: 1, field offset: 12)
        "#]]
        .assert_eq(
            &make_memory_layout(
                r#"
#[repr(C)]
struct Blah$0 {
    a: u32,
    b: (i32, u8),
    c: i8,
}
"#,
            )
            .unwrap()
            .to_string(),
        );
    }

    #[test]
    fn view_memory_layout_struct() {
        expect![[r#"
            [ROOT]: Blah (size: 16, align: 4, field offset: 0)
            	b: (i32, u8) (size: 8, align: 4, field offset: 0)
            		.0: i32 (size: 4, align: 4, field offset: 0)
            		.1: u8 (size: 1, align: 1, field offset: 4)
            	a: u32 (size: 4, align: 4, field offset: 8)
            	c: i8 (size: 1, align: 1, field offset: 12)
        "#]]
        .assert_eq(
            &make_memory_layout(
                r#"
struct Blah$0 {
    a: u32,
    b: (i32, u8),
    c: i8,
}
"#,
            )
            .unwrap()
            .to_string(),
        );
    }

    #[test]
    fn view_memory_layout_member() {
        expect![[r#"
            a: bool (size: 1, align: 1, field offset: 0)
        "#]]
        .assert_eq(
            &make_memory_layout(
                r#"
#[repr(C)]
struct Oof {
    a$0: bool,
}
"#,
            )
            .unwrap()
            .to_string(),
        );
    }

    #[test]
    fn view_memory_layout_alias() {
        let ml_a = make_memory_layout(
            r#"
struct X {
    a: u32,
    b: i8,
    c: (f32, f32),
}

type Foo$0 = X;
"#,
        )
        .unwrap();

        let ml_b = make_memory_layout(
            r#"
struct X$0 {
    a: u32,
    b: i8,
    c: (f32, f32),
}
"#,
        )
        .unwrap();

        assert_eq!(ml_a.to_string(), ml_b.to_string());
    }
}
