use std::path::{Path, PathBuf};

use tower_lsp::lsp_types::{DocumentLink, Position, Range};

use crate::Uri;
use crate::consts::TREESITTER_CMAKE_LANGUAGE;
use crate::utils::query::get_normal_commands;
use crate::utils::{NeoStrExt, gen_module_pattern, include_is_module};

const LINK_NODE_KIND: &[&str] = &["include", "add_subdirectory"];

const NEED_TO_CHECK_EXTENSION: &[&str] = &[".h", ".hpp", ".c", ".cpp", ".cmake"];

pub fn document_link_search<P: AsRef<Path>>(
    source: &str,
    current_file: P,
) -> Option<Vec<DocumentLink>> {
    let mut parse = tree_sitter::Parser::new();
    parse.set_language(&TREESITTER_CMAKE_LANGUAGE).unwrap();
    let thetree = parse.parse(source, None)?;
    let file_parent = current_file.as_ref().parent()?;
    let links = document_link_search_inner(source, thetree.root_node(), &file_parent);
    if links.is_empty() {
        return None;
    }
    Some(links)
}

pub fn document_link_search_inner<P: AsRef<Path>>(
    source: &str,
    node: tree_sitter::Node,
    current_parent: &P,
) -> Vec<DocumentLink> {
    let mut links = vec![];
    let source = source.as_bytes();
    let normal_commands = get_normal_commands(source, node, None);
    for command in normal_commands {
        let name = command.identifier.to_lowercase();
        let arguments = command.args;
        if arguments.is_empty() {
            continue;
        }
        if !LINK_NODE_KIND.contains(&name.as_str()) {
            for arg_node in arguments {
                let start_h = arg_node.start_position().row;
                let x = arg_node.start_position().column;
                let y = arg_node.end_position().column;
                let arg = arg_node.utf8_text(source).unwrap();
                if !NEED_TO_CHECK_EXTENSION
                    .iter()
                    .any(|extension| arg.ends_with(extension))
                {
                    continue;
                }
                let Some(filename) = arg.try_replace_placeholders() else {
                    continue;
                };
                let file_path = current_parent.as_ref().join(filename);
                if !file_path.exists() {
                    continue;
                }
                let range = Range {
                    start: Position {
                        line: start_h as u32,
                        character: x as u32,
                    },
                    end: Position {
                        line: start_h as u32,
                        character: y as u32,
                    },
                };
                links.push(DocumentLink {
                    range,
                    target: Some(Uri::from_file_path(file_path).unwrap()),
                    tooltip: None,
                    data: None,
                });
            }
            continue;
        }
        let is_subdirectory = name == "add_subdirectory";
        let file_node = arguments[0];
        let start_h = file_node.start_position().row;
        let x = file_node.start_position().column;
        let y = file_node.end_position().column;
        let Some(first_arg) = command.first_arg else {
            continue;
        };
        let Some(filename) = first_arg.try_replace_placeholders() else {
            continue;
        };
        let (final_uri, builtin) = if is_subdirectory {
            (
                current_parent
                    .as_ref()
                    .join(filename)
                    .join("CMakeLists.txt"),
                false,
            )
        } else {
            let Some((cmake_path, builtin)) = convert_include_cmake(&filename, current_parent)
            else {
                continue;
            };
            (cmake_path, builtin)
        };
        if !final_uri.exists() {
            continue;
        }
        let tooltip = if builtin {
            Some(format!("builtin module, link: {}", final_uri.display()))
        } else {
            Some(format!("link: {}", final_uri.display()))
        };
        let range = Range {
            start: Position {
                line: start_h as u32,
                character: x as u32,
            },
            end: Position {
                line: start_h as u32,
                character: y as u32,
            },
        };
        links.push(DocumentLink {
            range,
            target: Some(Uri::from_file_path(final_uri).unwrap()),
            tooltip,
            data: None,
        });
    }
    links
}

fn convert_include_cmake<P: AsRef<Path>>(name: &str, current_parent: P) -> Option<(PathBuf, bool)> {
    if !include_is_module(name) {
        return Some((current_parent.as_ref().join(name), false));
    }
    let global_pattern = gen_module_pattern(name)?;
    Some((
        glob::glob(&global_pattern)
            .into_iter()
            .flatten()
            .flatten()
            .next()?,
        true,
    ))
}

#[cfg(test)]
mod tests {
    use std::fs;
    use std::fs::File;
    use std::io::Write;

    use tempfile::tempdir;

    use super::*;
    use crate::fileapi::cache::Cache;
    use crate::fileapi::set_cache_data;

    #[test]
    fn test_document_link_search() {
        let dir = tempdir().unwrap();

        let json_value = serde_json::json!({
            "kind": "cache",
            "version": {
                "major": 2,
                "minor": 0
            },
            "entries": [
                {
                    "name": "ROOT_DIR",
                    "properties": [],
                    "type": "FILEPATH",
                    "value": &dir.path().display().to_string()
                }
            ]
        });
        let template_cache: Cache = serde_json::from_value(json_value).unwrap();
        set_cache_data(template_cache);
        #[cfg(not(windows))]
        let jump_file_src = r#"
set(ABCD 1234)
message(INFO "${ABCD}")
set(ROOT_DIR "ABCD" STRING CACHE "ROOTDIR")
include("${ROOT_DIR}/hello.cmake")
add_subdirectory(abcd_test)
"#;
        #[cfg(windows)]
        let jump_file_src = r#"
set(ABCD 1234)
message(INFO "${ABCD}")
set(ROOT_DIR "ABCD" STRING CACHE "ROOTDIR")
include("${ROOT_DIR}\\hello.cmake")
add_subdirectory(abcd_test)
"#;
        let top_cmake = dir.path().join("CMakeLists.txt");
        let hello_cmake = dir.path().join("hello.cmake");
        File::create_new(&hello_cmake).unwrap();
        let mut top_file = File::create_new(&top_cmake).unwrap();
        top_file.write_all(jump_file_src.as_bytes()).unwrap();
        let subdir = dir.path().join("abcd_test");
        fs::create_dir_all(&subdir).unwrap();
        let subdir_file = subdir.join("CMakeLists.txt");
        File::create_new(&subdir_file).unwrap();
        let mut parse = tree_sitter::Parser::new();
        parse.set_language(&TREESITTER_CMAKE_LANGUAGE).unwrap();
        let thetree = parse.parse(jump_file_src, None).unwrap();
        let links = document_link_search_inner(jump_file_src, thetree.root_node(), &dir.path());

        // NOTE: from some report, the order may be different,
        // But we do not need to control this order, so make a change
        let test_links = vec![
            DocumentLink {
                range: Range {
                    start: Position {
                        line: 4,
                        character: 8,
                    },
                    end: Position {
                        line: 4,
                        character: if cfg!(windows) { 34 } else { 33 },
                    },
                },
                target: Some(Uri::from_file_path(&hello_cmake).unwrap()),
                tooltip: Some(format!("link: {}", hello_cmake.display())),
                data: None,
            },
            DocumentLink {
                range: Range {
                    start: Position {
                        line: 5,
                        character: 17,
                    },
                    end: Position {
                        line: 5,
                        character: 26,
                    },
                },
                target: Some(Uri::from_file_path(&subdir_file).unwrap()),
                tooltip: Some(format!("link: {}", subdir_file.display())),
                data: None,
            },
        ];
        assert_eq!(links.len(), test_links.len());
        for link in test_links {
            assert!(links.contains(&link));
        }
    }
}
