use std::{collections::HashMap, fmt::Display, ops::Range, path::Path, sync::Arc};

use percent_encoding::percent_decode_str;
use relative_path::RelativePath;

pub mod config;
pub mod document;
pub mod graph;
pub mod node;
pub mod projector;
pub mod reference;
pub mod tree;

pub type Markdown = String;

pub type MaybeKey = Option<Key>;

pub type Content = String;
pub type State = HashMap<String, String>;

pub type NodeId = u64;
pub type MaybeNodeId = Option<NodeId>;

pub type LineId = usize;
pub type MaybeLineId = Option<LineId>;

pub type StrId = usize;
pub type MaybeStrId = Option<StrId>;

pub type LineNumber = usize;
pub type LineRange = Range<LineNumber>;

#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Clone, Default, Hash)]
pub struct Key {
    pub relative_path: Arc<String>,
}

impl Display for Key {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.relative_path)
    }
}

impl Key {
    pub fn parent(&self) -> String {
        RelativePath::new(&self.relative_path.to_string())
            .parent()
            .map(|p| p.to_string())
            .unwrap_or_default()
    }

    pub fn source(&self) -> String {
        RelativePath::new(&self.relative_path.to_string())
            .file_name()
            .unwrap_or("")
            .to_string()
    }

    pub fn path(&self) -> Option<String> {
        RelativePath::new(&self.relative_path.to_string())
            .parent()
            .map(|p| p.to_string())
    }

    pub fn combine(parent: &str, id: &str) -> Key {
        let path = RelativePath::new(parent).join(id).to_string();
        Key {
            relative_path: Arc::new(path),
        }
    }

    pub fn name(name: &str) -> Self {
        let key = name.trim_end_matches(".md").to_string();

        Key {
            relative_path: Arc::new(key),
        }
    }

    pub fn from_rel_link_url(url: &str, relative_to: &str) -> Self {
        let decoded = percent_decode_str(url).decode_utf8_lossy().into_owned();
        let key = decoded.trim_end_matches(".md").to_string();
        let path = RelativePath::new(relative_to)
            .join_normalized(key)
            .to_string();
        Key {
            relative_path: Arc::new(path),
        }
    }

    pub fn to_rel_link_url(&self, relative_to: &str) -> String {
        RelativePath::new(relative_to)
            .relative(self.relative_path.to_string())
            .to_string()
    }

    pub fn to_library_url(&self) -> String {
        self.relative_path.to_string()
    }

    pub fn last_url_segment(&self) -> String {
        format!("{}", self.relative_path).to_string()
    }

    pub fn to_path(&self) -> String {
        format!("{}.md", self.relative_path).to_string()
    }

    pub fn from_path(path: &Path) -> Option<Key> {
        let name = path.file_name()?.to_string_lossy().to_string();
        let key = name.trim_end_matches(".md").to_string();
        Some(Key::name(&key))
    }
}

impl From<&str> for Key {
    fn from(value: &str) -> Self {
        Key::name(value)
    }
}

impl From<String> for Key {
    fn from(value: String) -> Self {
        Key::name(&value)
    }
}

#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Default, Hash)]
pub struct Position {
    pub line: usize,
    pub character: usize,
}

impl From<(usize, usize)> for Position {
    fn from(value: (usize, usize)) -> Self {
        Position {
            line: value.0,
            character: value.1,
        }
    }
}

pub type InlineRange = Range<Position>;

pub type NodesMap = Vec<(NodeId, LineRange)>;
pub type DocumentNodesMap = (Key, NodesMap);

pub type Lang = String;
pub type LibraryUrl = String;

pub type Level = u8;
pub type Title = String;

pub trait InlinesContext: Copy {
    fn get_ref_title(&self, key: &Key) -> Option<String>;
}

pub fn is_ref_url(url: &str) -> bool {
    !(url.to_lowercase().starts_with("http://")
        || url.to_lowercase().starts_with("https://")
        || url.to_lowercase().starts_with("mailto:"))
}

pub fn normalize_url(url: &str, extension: &str) -> String {
    if !is_ref_url(url) {
        return url.to_string();
    }
    match url.split_once('#') {
        Some((path, fragment)) => format!(
            "{}#{}",
            path.strip_suffix(extension).unwrap_or(path),
            fragment
        ),
        None => url.strip_suffix(extension).unwrap_or(url).to_string(),
    }
}
