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 inline;
pub mod key_index;
pub mod node;
pub mod node_iter;
pub mod node_pointer;
pub mod projector;
pub mod reference;
pub mod tree;
pub mod tree_iter;
pub mod writer;

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 = strip_doc_extension(name).to_string();

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

    pub fn from_stripped(key: &str) -> Self {
        Key {
            relative_path: Arc::new(key.to_string()),
        }
    }

    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 = strip_doc_extension(&decoded).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, format: config::Format) -> String {
        format!("{}.{}", self.relative_path, format.extension())
    }

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

pub fn strip_doc_extension(name: &str) -> &str {
    name.strip_suffix(".md")
        .or_else(|| name.strip_suffix(".dj"))
        .unwrap_or(name)
}

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>;
    fn wiki_display(&self, key: &Key, original_url: &str) -> 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(),
    }
}
