use std::path::Path;

use rustpython_parser::ast::{self, Decorator, Stmt};

use ruff_python_ast::call_path::{collect_call_path, CallPath};
use ruff_python_ast::helpers::map_callable;

use crate::model::SemanticModel;

#[derive(Debug, Clone, Copy, is_macro::Is)]
pub enum Visibility {
    Public,
    Private,
}

/// Returns `true` if a function is a "static method".
pub fn is_staticmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
    decorator_list.iter().any(|decorator| {
        semantic
            .resolve_call_path(map_callable(&decorator.expression))
            .map_or(false, |call_path| {
                matches!(call_path.as_slice(), ["", "staticmethod"])
            })
    })
}

/// Returns `true` if a function is a "class method".
pub fn is_classmethod(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
    decorator_list.iter().any(|decorator| {
        semantic
            .resolve_call_path(map_callable(&decorator.expression))
            .map_or(false, |call_path| {
                matches!(call_path.as_slice(), ["", "classmethod"])
            })
    })
}

/// Returns `true` if a function definition is an `@overload`.
pub fn is_overload(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
    decorator_list.iter().any(|decorator| {
        semantic.match_typing_expr(map_callable(&decorator.expression), "overload")
    })
}

/// Returns `true` if a function definition is an `@override` (PEP 698).
pub fn is_override(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
    decorator_list.iter().any(|decorator| {
        semantic.match_typing_expr(map_callable(&decorator.expression), "override")
    })
}

/// Returns `true` if a function definition is an abstract method based on its decorators.
pub fn is_abstract(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
    decorator_list.iter().any(|decorator| {
        semantic
            .resolve_call_path(map_callable(&decorator.expression))
            .map_or(false, |call_path| {
                matches!(
                    call_path.as_slice(),
                    [
                        "abc",
                        "abstractmethod"
                            | "abstractclassmethod"
                            | "abstractstaticmethod"
                            | "abstractproperty"
                    ]
                )
            })
    })
}

/// Returns `true` if a function definition is a `@property`.
/// `extra_properties` can be used to check additional non-standard
/// `@property`-like decorators.
pub fn is_property(
    decorator_list: &[Decorator],
    extra_properties: &[CallPath],
    semantic: &SemanticModel,
) -> bool {
    decorator_list.iter().any(|decorator| {
        semantic
            .resolve_call_path(map_callable(&decorator.expression))
            .map_or(false, |call_path| {
                matches!(
                    call_path.as_slice(),
                    ["", "property"] | ["functools", "cached_property"]
                ) || extra_properties
                    .iter()
                    .any(|extra_property| extra_property.as_slice() == call_path.as_slice())
            })
    })
}

/// Returns `true` if a class is an `final`.
pub fn is_final(decorator_list: &[Decorator], semantic: &SemanticModel) -> bool {
    decorator_list
        .iter()
        .any(|decorator| semantic.match_typing_expr(map_callable(&decorator.expression), "final"))
}

/// Returns `true` if a function is a "magic method".
pub fn is_magic(name: &str) -> bool {
    name.starts_with("__") && name.ends_with("__")
}

/// Returns `true` if a function is an `__init__`.
pub fn is_init(name: &str) -> bool {
    name == "__init__"
}

/// Returns `true` if a function is a `__new__`.
pub fn is_new(name: &str) -> bool {
    name == "__new__"
}

/// Returns `true` if a function is a `__call__`.
pub fn is_call(name: &str) -> bool {
    name == "__call__"
}

/// Returns `true` if a function is a test one.
pub fn is_test(name: &str) -> bool {
    name == "runTest" || name.starts_with("test")
}

/// Returns `true` if a module name indicates public visibility.
fn is_public_module(module_name: &str) -> bool {
    !module_name.starts_with('_') || (module_name.starts_with("__") && module_name.ends_with("__"))
}

/// Returns `true` if a module name indicates private visibility.
fn is_private_module(module_name: &str) -> bool {
    !is_public_module(module_name)
}

/// Return the stem of a module name (everything preceding the last dot).
fn stem(path: &str) -> &str {
    if let Some(index) = path.rfind('.') {
        &path[..index]
    } else {
        path
    }
}

/// A Python module can either be defined as a module path (i.e., the dot-separated path to the
/// module) or, if the module can't be resolved, as a file path (i.e., the path to the file defining
/// the module).
#[derive(Debug)]
pub enum ModuleSource<'a> {
    /// A module path is a dot-separated path to the module.
    Path(&'a [String]),
    /// A file path is the path to the file defining the module, often a script outside of a
    /// package.
    File(&'a Path),
}

impl ModuleSource<'_> {
    /// Return the `Visibility` of the module.
    pub(crate) fn to_visibility(&self) -> Visibility {
        match self {
            Self::Path(path) => {
                if path.iter().any(|m| is_private_module(m)) {
                    return Visibility::Private;
                }
            }
            Self::File(path) => {
                // Check to see if the filename itself indicates private visibility.
                // Ex) `_foo.py` (but not `__init__.py`)
                let mut components = path.iter().rev();
                if let Some(filename) = components.next() {
                    let module_name = filename.to_string_lossy();
                    let module_name = stem(&module_name);
                    if is_private_module(module_name) {
                        return Visibility::Private;
                    }
                }
            }
        }
        Visibility::Public
    }
}

pub(crate) fn function_visibility(stmt: &Stmt) -> Visibility {
    match stmt {
        Stmt::FunctionDef(ast::StmtFunctionDef { name, .. })
        | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { name, .. }) => {
            if name.starts_with('_') {
                Visibility::Private
            } else {
                Visibility::Public
            }
        }
        _ => panic!("Found non-FunctionDef in function_visibility"),
    }
}

pub(crate) fn method_visibility(stmt: &Stmt) -> Visibility {
    match stmt {
        Stmt::FunctionDef(ast::StmtFunctionDef {
            name,
            decorator_list,
            ..
        })
        | Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
            name,
            decorator_list,
            ..
        }) => {
            // Is this a setter or deleter?
            if decorator_list.iter().any(|decorator| {
                collect_call_path(&decorator.expression).map_or(false, |call_path| {
                    call_path.as_slice() == [name, "setter"]
                        || call_path.as_slice() == [name, "deleter"]
                })
            }) {
                return Visibility::Private;
            }

            // Is the method non-private?
            if !name.starts_with('_') {
                return Visibility::Public;
            }

            // Is this a magic method?
            if name.starts_with("__") && name.ends_with("__") {
                return Visibility::Public;
            }

            Visibility::Private
        }
        _ => panic!("Found non-FunctionDef in method_visibility"),
    }
}

pub(crate) fn class_visibility(stmt: &Stmt) -> Visibility {
    match stmt {
        Stmt::ClassDef(ast::StmtClassDef { name, .. }) => {
            if name.starts_with('_') {
                Visibility::Private
            } else {
                Visibility::Public
            }
        }
        _ => panic!("Found non-ClassDef in function_visibility"),
    }
}
