use std::path::PathBuf;

use syn::{spanned::Spanned, visit::Visit, Pat};

use crate::{
    issues::{Issue, IssueReport},
    metadata::{
        AnyLoopInfo, ClosureInfo, ComponentInfo, ConditionalInfo, FnInfo, ForInfo, HookInfo,
        IfInfo, LoopInfo, MatchInfo, Span, WhileInfo,
    },
};

struct VisitHooks {
    issues: Vec<Issue>,
    context: Vec<Node>,
}

impl VisitHooks {
    const fn new() -> Self {
        Self {
            issues: vec![],
            context: vec![],
        }
    }
}

/// Checks a Dioxus file for issues.
pub fn check_file(path: PathBuf, file_content: &str) -> IssueReport {
    let file = syn::parse_file(file_content).unwrap();
    let mut visit_hooks = VisitHooks::new();
    visit_hooks.visit_file(&file);
    IssueReport::new(
        path,
        std::env::current_dir().unwrap_or_default(),
        file_content.to_string(),
        visit_hooks.issues,
    )
}

#[derive(Debug, Clone)]
enum Node {
    If(IfInfo),
    Match(MatchInfo),
    For(ForInfo),
    While(WhileInfo),
    Loop(LoopInfo),
    Closure(ClosureInfo),
    ComponentFn(ComponentInfo),
    HookFn(HookInfo),
    OtherFn(FnInfo),
}

fn returns_element(ty: &syn::ReturnType) -> bool {
    match ty {
        syn::ReturnType::Default => false,
        syn::ReturnType::Type(_, ref ty) => {
            if let syn::Type::Path(ref path) = **ty {
                if let Some(segment) = path.path.segments.last() {
                    if segment.ident == "Element" {
                        return true;
                    }
                }
            }
            false
        }
    }
}

fn is_hook_ident(ident: &syn::Ident) -> bool {
    ident.to_string().starts_with("use_")
}

fn is_component_fn(item_fn: &syn::ItemFn) -> bool {
    returns_element(&item_fn.sig.output)
}

fn get_closure_hook_body(local: &syn::Local) -> Option<&syn::Expr> {
    if let Pat::Ident(ident) = &local.pat {
        if is_hook_ident(&ident.ident) {
            if let Some((_, expr)) = &local.init {
                if let syn::Expr::Closure(closure) = &**expr {
                    return Some(&closure.body);
                }
            }
        }
    }

    None
}

fn fn_name_and_name_span(item_fn: &syn::ItemFn) -> (String, Span) {
    let name = item_fn.sig.ident.to_string();
    let name_span = item_fn.sig.ident.span().into();
    (name, name_span)
}

impl<'ast> syn::visit::Visit<'ast> for VisitHooks {
    fn visit_expr_call(&mut self, i: &'ast syn::ExprCall) {
        if let syn::Expr::Path(ref path) = *i.func {
            if let Some(segment) = path.path.segments.last() {
                if is_hook_ident(&segment.ident) {
                    let hook_info = HookInfo::new(
                        i.span().into(),
                        segment.ident.span().into(),
                        segment.ident.to_string(),
                    );
                    let mut container_fn: Option<Node> = None;
                    for node in self.context.iter().rev() {
                        match node {
                            Node::If(if_info) => {
                                let issue = Issue::HookInsideConditional(
                                    hook_info.clone(),
                                    ConditionalInfo::If(if_info.clone()),
                                );
                                self.issues.push(issue);
                            }
                            Node::Match(match_info) => {
                                let issue = Issue::HookInsideConditional(
                                    hook_info.clone(),
                                    ConditionalInfo::Match(match_info.clone()),
                                );
                                self.issues.push(issue);
                            }
                            Node::For(for_info) => {
                                let issue = Issue::HookInsideLoop(
                                    hook_info.clone(),
                                    AnyLoopInfo::For(for_info.clone()),
                                );
                                self.issues.push(issue);
                            }
                            Node::While(while_info) => {
                                let issue = Issue::HookInsideLoop(
                                    hook_info.clone(),
                                    AnyLoopInfo::While(while_info.clone()),
                                );
                                self.issues.push(issue);
                            }
                            Node::Loop(loop_info) => {
                                let issue = Issue::HookInsideLoop(
                                    hook_info.clone(),
                                    AnyLoopInfo::Loop(loop_info.clone()),
                                );
                                self.issues.push(issue);
                            }
                            Node::Closure(closure_info) => {
                                let issue = Issue::HookInsideClosure(
                                    hook_info.clone(),
                                    closure_info.clone(),
                                );
                                self.issues.push(issue);
                            }
                            Node::ComponentFn(_) | Node::HookFn(_) | Node::OtherFn(_) => {
                                container_fn = Some(node.clone());
                                break;
                            }
                        }
                    }

                    if let Some(Node::OtherFn(_)) = container_fn {
                        let issue = Issue::HookOutsideComponent(hook_info);
                        self.issues.push(issue);
                    }
                }
            }
        }
    }

    fn visit_item_fn(&mut self, i: &'ast syn::ItemFn) {
        let (name, name_span) = fn_name_and_name_span(i);
        if is_component_fn(i) {
            self.context.push(Node::ComponentFn(ComponentInfo::new(
                i.span().into(),
                name,
                name_span,
            )));
        } else if is_hook_ident(&i.sig.ident) {
            self.context.push(Node::HookFn(HookInfo::new(
                i.span().into(),
                i.sig.ident.span().into(),
                name,
            )));
        } else {
            self.context
                .push(Node::OtherFn(FnInfo::new(i.span().into(), name, name_span)));
        }
        syn::visit::visit_item_fn(self, i);
        self.context.pop();
    }

    fn visit_local(&mut self, i: &'ast syn::Local) {
        if let Some(body) = get_closure_hook_body(i) {
            // if the closure is a hook, we only visit the body of the closure.
            // this prevents adding a ClosureInfo node to the context
            syn::visit::visit_expr(self, body);
        } else {
            // otherwise visit the whole local
            syn::visit::visit_local(self, i);
        }
    }

    fn visit_expr_if(&mut self, i: &'ast syn::ExprIf) {
        self.context.push(Node::If(IfInfo::new(
            i.span().into(),
            i.if_token
                .span()
                .join(i.cond.span())
                .unwrap_or_else(|| i.span())
                .into(),
        )));
        syn::visit::visit_expr_if(self, i);
        self.context.pop();
    }

    fn visit_expr_match(&mut self, i: &'ast syn::ExprMatch) {
        self.context.push(Node::Match(MatchInfo::new(
            i.span().into(),
            i.match_token
                .span()
                .join(i.expr.span())
                .unwrap_or_else(|| i.span())
                .into(),
        )));
        syn::visit::visit_expr_match(self, i);
        self.context.pop();
    }

    fn visit_expr_for_loop(&mut self, i: &'ast syn::ExprForLoop) {
        self.context.push(Node::For(ForInfo::new(
            i.span().into(),
            i.for_token
                .span()
                .join(i.expr.span())
                .unwrap_or_else(|| i.span())
                .into(),
        )));
        syn::visit::visit_expr_for_loop(self, i);
        self.context.pop();
    }

    fn visit_expr_while(&mut self, i: &'ast syn::ExprWhile) {
        self.context.push(Node::While(WhileInfo::new(
            i.span().into(),
            i.while_token
                .span()
                .join(i.cond.span())
                .unwrap_or_else(|| i.span())
                .into(),
        )));
        syn::visit::visit_expr_while(self, i);
        self.context.pop();
    }

    fn visit_expr_loop(&mut self, i: &'ast syn::ExprLoop) {
        self.context
            .push(Node::Loop(LoopInfo::new(i.span().into())));
        syn::visit::visit_expr_loop(self, i);
        self.context.pop();
    }

    fn visit_expr_closure(&mut self, i: &'ast syn::ExprClosure) {
        self.context
            .push(Node::Closure(ClosureInfo::new(i.span().into())));
        syn::visit::visit_expr_closure(self, i);
        self.context.pop();
    }
}

#[cfg(test)]
mod tests {
    use crate::metadata::{
        AnyLoopInfo, ClosureInfo, ConditionalInfo, ForInfo, HookInfo, IfInfo, LineColumn, LoopInfo,
        MatchInfo, Span, WhileInfo,
    };
    use indoc::indoc;
    use pretty_assertions::assert_eq;

    use super::*;

    #[test]
    fn test_no_hooks() {
        let contents = indoc! {r#"
            fn App(cx: Scope) -> Element {
                rsx! {
                    p { "Hello World" }
                }
            }
        "#};

        let report = check_file("app.rs".into(), contents);

        assert_eq!(report.issues, vec![]);
    }

    #[test]
    fn test_hook_correctly_used_inside_component() {
        let contents = indoc! {r#"
            fn App(cx: Scope) -> Element {
                let count = use_state(cx, || 0);
                rsx! {
                    p { "Hello World: {count}" }
                }
            }
        "#};

        let report = check_file("app.rs".into(), contents);

        assert_eq!(report.issues, vec![]);
    }

    #[test]
    fn test_hook_correctly_used_inside_hook_fn() {
        let contents = indoc! {r#"
            fn use_thing(cx: Scope) -> UseState<i32> {
                use_state(cx, || 0)
            }
        "#};

        let report = check_file("use_thing.rs".into(), contents);

        assert_eq!(report.issues, vec![]);
    }

    #[test]
    fn test_hook_correctly_used_inside_hook_closure() {
        let contents = indoc! {r#"
            fn App(cx: Scope) -> Element {
                let use_thing = || {
                    use_state(cx, || 0)
                };
                let count = use_thing();
                rsx! {
                    p { "Hello World: {count}" }
                }
            }
        "#};

        let report = check_file("app.rs".into(), contents);

        assert_eq!(report.issues, vec![]);
    }

    #[test]
    fn test_conditional_hook_if() {
        let contents = indoc! {r#"
            fn App(cx: Scope) -> Element {
                if you_are_happy && you_know_it {
                    let something = use_state(cx, || "hands");
                    println!("clap your {something}")
                }
            }
        "#};

        let report = check_file("app.rs".into(), contents);

        assert_eq!(
            report.issues,
            vec![Issue::HookInsideConditional(
                HookInfo::new(
                    Span::new_from_str(
                        r#"use_state(cx, || "hands")"#,
                        LineColumn { line: 3, column: 24 },
                    ),
                    Span::new_from_str(
                        r#"use_state"#,
                        LineColumn { line: 3, column: 24 },
                    ),
                    "use_state".to_string()
                ),
                ConditionalInfo::If(IfInfo::new(
                    Span::new_from_str(
                        "if you_are_happy && you_know_it {\n        let something = use_state(cx, || \"hands\");\n        println!(\"clap your {something}\")\n    }",
                        LineColumn { line: 2, column: 4 },
                    ),
                    Span::new_from_str(
                        "if you_are_happy && you_know_it",
                        LineColumn { line: 2, column: 4 }
                    )
                ))
            )],
        );
    }

    #[test]
    fn test_conditional_hook_match() {
        let contents = indoc! {r#"
            fn App(cx: Scope) -> Element {
                match you_are_happy && you_know_it {
                    true => {
                        let something = use_state(cx, || "hands");
                        println!("clap your {something}")
                    }
                    false => {}
                }
            }
        "#};

        let report = check_file("app.rs".into(), contents);

        assert_eq!(
            report.issues,
            vec![Issue::HookInsideConditional(
                HookInfo::new(
                    Span::new_from_str(r#"use_state(cx, || "hands")"#, LineColumn { line: 4, column: 28 }),
                    Span::new_from_str(r#"use_state"#, LineColumn { line: 4, column: 28 }),
                    "use_state".to_string()
                ),
                ConditionalInfo::Match(MatchInfo::new(
                    Span::new_from_str(
                        "match you_are_happy && you_know_it {\n        true => {\n            let something = use_state(cx, || \"hands\");\n            println!(\"clap your {something}\")\n        }\n        false => {}\n    }",
                        LineColumn { line: 2, column: 4 },
                    ),
                    Span::new_from_str("match you_are_happy && you_know_it", LineColumn { line: 2, column: 4 })
                ))
            )]
        );
    }

    #[test]
    fn test_for_loop_hook() {
        let contents = indoc! {r#"
            fn App(cx: Scope) -> Element {
                for _name in &names {
                    let is_selected = use_state(cx, || false);
                    println!("selected: {is_selected}");
                }
            }
        "#};

        let report = check_file("app.rs".into(), contents);

        assert_eq!(
            report.issues,
            vec![Issue::HookInsideLoop(
                HookInfo::new(
                    Span::new_from_str(
                        "use_state(cx, || false)",
                        LineColumn { line: 3, column: 26 },
                    ),
                    Span::new_from_str(
                        "use_state",
                        LineColumn { line: 3, column: 26 },
                    ),
                    "use_state".to_string()
                ),
                AnyLoopInfo::For(ForInfo::new(
                    Span::new_from_str(
                        "for _name in &names {\n        let is_selected = use_state(cx, || false);\n        println!(\"selected: {is_selected}\");\n    }",
                        LineColumn { line: 2, column: 4 },
                    ),
                    Span::new_from_str(
                        "for _name in &names",
                        LineColumn { line: 2, column: 4 },
                    )
                ))
            )]
        );
    }

    #[test]
    fn test_while_loop_hook() {
        let contents = indoc! {r#"
            fn App(cx: Scope) -> Element {
                while true {
                    let something = use_state(cx, || "hands");
                    println!("clap your {something}")
                }
            }
        "#};

        let report = check_file("app.rs".into(), contents);

        assert_eq!(
            report.issues,
            vec![Issue::HookInsideLoop(
                HookInfo::new(
                    Span::new_from_str(
                        r#"use_state(cx, || "hands")"#,
                        LineColumn { line: 3, column: 24 },
                    ),
                    Span::new_from_str(
                        "use_state",
                        LineColumn { line: 3, column: 24 },
                    ),
                    "use_state".to_string()
                ),
                AnyLoopInfo::While(WhileInfo::new(
                    Span::new_from_str(
                        "while true {\n        let something = use_state(cx, || \"hands\");\n        println!(\"clap your {something}\")\n    }",
                        LineColumn { line: 2, column: 4 },
                    ),
                    Span::new_from_str(
                        "while true",
                        LineColumn { line: 2, column: 4 },
                    )
                ))
            )],
        );
    }

    #[test]
    fn test_loop_hook() {
        let contents = indoc! {r#"
            fn App(cx: Scope) -> Element {
                loop {
                    let something = use_state(cx, || "hands");
                    println!("clap your {something}")
                }
            }
        "#};

        let report = check_file("app.rs".into(), contents);

        assert_eq!(
            report.issues,
            vec![Issue::HookInsideLoop(
                HookInfo::new(
                    Span::new_from_str(
                        r#"use_state(cx, || "hands")"#,
                        LineColumn { line: 3, column: 24 },
                    ),
                    Span::new_from_str(
                        "use_state",
                        LineColumn { line: 3, column: 24 },
                    ),
                    "use_state".to_string()
                ),
                AnyLoopInfo::Loop(LoopInfo::new(Span::new_from_str(
                    "loop {\n        let something = use_state(cx, || \"hands\");\n        println!(\"clap your {something}\")\n    }",
                    LineColumn { line: 2, column: 4 },
                )))
            )],
        );
    }

    #[test]
    fn test_conditional_okay() {
        let contents = indoc! {r#"
            fn App(cx: Scope) -> Element {
                let something = use_state(cx, || "hands");
                if you_are_happy && you_know_it {
                    println!("clap your {something}")
                }
            }
        "#};

        let report = check_file("app.rs".into(), contents);

        assert_eq!(report.issues, vec![]);
    }

    #[test]
    fn test_closure_hook() {
        let contents = indoc! {r#"
            fn App(cx: Scope) -> Element {
                let _a = || {
                    let b = use_state(cx, || 0);
                    b.get()
                };
            }
        "#};

        let report = check_file("app.rs".into(), contents);

        assert_eq!(
            report.issues,
            vec![Issue::HookInsideClosure(
                HookInfo::new(
                    Span::new_from_str(
                        "use_state(cx, || 0)",
                        LineColumn {
                            line: 3,
                            column: 16
                        },
                    ),
                    Span::new_from_str(
                        "use_state",
                        LineColumn {
                            line: 3,
                            column: 16
                        },
                    ),
                    "use_state".to_string()
                ),
                ClosureInfo::new(Span::new_from_str(
                    "|| {\n        let b = use_state(cx, || 0);\n        b.get()\n    }",
                    LineColumn {
                        line: 2,
                        column: 13
                    },
                ))
            )]
        );
    }

    #[test]
    fn test_hook_outside_component() {
        let contents = indoc! {r#"
            fn not_component_or_hook(cx: Scope) {
                let _a = use_state(cx, || 0);
            }
        "#};

        let report = check_file("app.rs".into(), contents);

        assert_eq!(
            report.issues,
            vec![Issue::HookOutsideComponent(HookInfo::new(
                Span::new_from_str(
                    "use_state(cx, || 0)",
                    LineColumn {
                        line: 2,
                        column: 13
                    }
                ),
                Span::new_from_str(
                    "use_state",
                    LineColumn {
                        line: 2,
                        column: 13
                    },
                ),
                "use_state".to_string()
            ))]
        );
    }

    #[test]
    fn test_hook_inside_hook() {
        let contents = indoc! {r#"
            fn use_thing(cx: Scope) {
                let _a = use_state(cx, || 0);
            }
        "#};

        let report = check_file("app.rs".into(), contents);

        assert_eq!(report.issues, vec![]);
    }
}
