use crate::*;

use vagabond::*;

use std::collections::HashMap;


pub struct Website {
    pub name: String,
    pub config: HashMap<String, String>,
    pub pages: Vec<Page>,
    pub redirects: Vec<Redirect>,
    pub static_files: Vec<StaticItem>,  // Redirects, !-prefixed-dir contents
    pub static_dirs: Vec<StaticItem>,   // Only !-prefixed static dirs
}

pub struct Page {
    pub name: String,                       // Display name of this page
    pub name_url: String,                   // URL name for this page, no extension
    pub full_url: String,                   // Full URL for this page, no extension
    pub parents: Vec<String>,               // Parent directory components, unsafe
    pub parent_url: String,                 // Base URL for links in this page
    pub source_path: PathBuf,               // Absolute path to source file
    pub document: MarkdownDocument,         // File content parsed as markdown
    pub headings: Vec<Heading>,             // Ordered list of all headings in page
    pub last_modified: Option<SystemTime>,  // last-modified time of source file
}

pub struct Heading {
    pub name: String,
    pub url: String,
    pub level: Level,
}

pub struct StaticItem {
    pub full_url: String,                   // Safe full URL, with extension
    pub source_path: PathBuf,               // Absolute path to source file
    pub last_modified: Option<SystemTime>,  // last-modified time of source file
}

pub struct Redirect {
    pub name: String,                       // Display name of this redirect
    pub full_url: String,                   // Safe full URL, no extension
    pub parents: Vec<String>,               // Parent directory components, unsafe
    pub parent_url: String,                 // Base URL for relative redirects
    pub redirect: String,                   // Page to redirect to, as an internal link
    pub last_modified: Option<SystemTime>,  // last-modified time of source file
}

pub trait LinkFrom {
    fn name(&self) -> &str;
    fn parent_url(&self) -> &str;
    fn parents(&self) -> &[String];
    fn root(&self) -> String {
        let mut root = String::new();
        for _ in self.parents() {
            root.push_str("../");
        }
        return root;
    }
}


impl Page {
    pub fn root(&self) -> String {
        let mut root = String::new();
        for _ in &self.parents {
            root.push_str("../");
        }
        return root;
    }
}

impl LinkFrom for Page {
    fn name(&self) -> &str { &self.name }
    fn parent_url(&self) -> &str { &self.parent_url }
    fn parents(&self) -> &[String] { &self.parents }
}

impl LinkFrom for Redirect {
    fn name(&self) -> &str { &self.name }
    fn parent_url(&self) -> &str { &self.parent_url }
    fn parents(&self) -> &[String] { &self.parents }
}


impl Website {
    pub fn from_path(path: &Path) -> Self {
        let mut new = Self {
            pages: Vec::new(),
            redirects: Vec::new(),
            static_files: Vec::new(),
            static_dirs: Vec::new(),
            name: match Entry::from_path(path) {
                Ok(entry) => entry.name,
                Err(err) => fatal!("Couldn't open {:?}: {:?}", &path, err),
            },
            config: HashMap::new(),
        };
        new.collect_entry(path, path);
        return new;
    }

    fn collect_entry(&mut self, path: &Path, prefix: &Path) {
        let entry = Entry::from_path(path).unwrap();
        // Ignore dotted entries.
        if entry.name.starts_with('.') { return }
        // Get name and extension.
        let (mut name, extension) = entry.split_name();
        if let Some((prefix, suffix)) = name.split_once(' ') {
            if prefix.chars().all(|c| "0123456789-".contains(c)) {
                name = suffix.to_string();
            }
        }
        let name_url = make_url_safe(&name);
        // Get last-modified time.
        let last_modified = entry.last_modified;
        // Generate parent URL, used only for files.
        let source_path = entry.original_path.clone();
        let relative_path = source_path.strip_prefix(prefix).unwrap_or_else(
            |_| fatal!("Path doesn't start with {prefix:?}: {source_path:?}"));
        let mut parents: Vec<_> = relative_path.components()
            .map(|c| c.as_os_str().to_string_lossy().to_string()).collect();
        parents.pop();  // Remove file segment.

        // Process each entry.
        if entry.is_directory() {
            if let Some(stripped) = entry.name.strip_prefix("!") {
                // Track all files inside the static directory.
                for child in traverse_directory(&entry).unwrap() {
                    let source_path = child.original_path;
                    let relative_path = source_path.strip_prefix(&entry.original_path).unwrap_or_else(
                        |_| fatal!("Path doesn't start with {prefix:?}: {source_path:?}"))
                        .as_os_str().to_string_lossy().to_string();
                    let full_url = format!("{stripped}/{relative_path}");
                    self.static_files.push(StaticItem { full_url, source_path, last_modified })
                }
                let full_url = make_url_safe(stripped);
                self.static_dirs.push(StaticItem { full_url, source_path, last_modified });
            } else {
                for child in list_directory(entry.original_path).unwrap() {
                    self.collect_entry(&child.original_path, prefix);
                }
            }
        } else if parents.is_empty() && entry.name.to_lowercase() == "toaster.conf" {
            info!("Reading configuration file at {path:?}");
            // Parse the config file.
            let config = std::fs::read_to_string(&source_path).unwrap();
            let mut key = None;
            let mut value = String::new();
            for line in config.lines() {
                if line.starts_with(" ") {
                    value.push_str(line.trim());
                    value.push('\n');
                } else {
                    if let Some(key) = key {
                        self.config.insert(key, std::mem::take(&mut value));
                    }
                    key = Some(line.trim().to_lowercase().to_string());
                }
            }
            if let Some(key) = key {
                self.config.insert(key, std::mem::take(&mut value));
            }
        } else {
            match extension.as_str() {
                "md" => {
                    let markdown = std::fs::read_to_string(&source_path).unwrap();
                    let document = MarkdownDocument::from_str(&markdown);
                    let mut heading_set = HashSet::new();
                    let mut duplicates = HashSet::new();
                    let headings = document.blocks.iter()
                        .filter_map(|block| if let Block::Heading { line, level } = block {
                            let name = line.to_string();
                            let url = make_url_safe(strip_appendix(&name));
                            let level = level.to_owned();
                            if !heading_set.insert(url.clone()) {
                                duplicates.insert(url.clone());
                            }
                            Some(Heading { name, url, level })
                        } else {
                            None
                        }).collect();
                    for url in duplicates {
                        warn!("Page {name:?} contains multiple headings with ID \"#{url}\"");
                    }
                    if name_url == "+index" {
                        if parents.is_empty() {
                            // This is the index file for the whole site.
                            self.pages.push(Page {
                                name: String::from("Home"),
                                name_url: String::from("index"),
                                full_url: String::from("index"),
                                parents,
                                parent_url: String::from(""),
                                source_path,
                                document,
                                headings,
                                last_modified,
                            });
                        } else {
                            // This is an index file for a directory.
                            let name = parents[parents.len()-1].clone();
                            let name_url = make_url_safe(&name);
                            let mut full_url = String::new();
                            for parent in &parents {
                                full_url.push_str(&make_url_safe(parent));
                                full_url.push('/');
                            }
                            let parent_url = full_url.clone();
                            full_url.pop();
                            parents.pop();
                            self.pages.push(Page {
                                name,
                                name_url,
                                full_url,
                                parents,
                                parent_url,
                                source_path,
                                document,
                                headings,
                                last_modified,
                            });
                        }
                    } else {
                        let mut full_url = String::new();
                        for parent in &parents {
                                full_url.push_str(&make_url_safe(parent));
                                full_url.push('/');
                        }
                        full_url.push_str(&name_url);
                        let mut parent_url = full_url.clone();
                        parent_url.push('/');
                        self.pages.push(Page {
                            name, name_url, full_url,
                            parents, parent_url,
                            source_path,
                            document, headings,
                            last_modified,
                        });
                    }
                },
                "redirect" => {
                    let mut full_url = String::new();
                    for parent in &parents {
                            full_url.push_str(&make_url_safe(parent));
                            full_url.push('/');
                    }
                    let parent_url = full_url.clone();
                    full_url.push_str(&name_url);
                    let redirect = std::fs::read_to_string(&source_path)
                        .unwrap().trim().to_string();
                    self.redirects.push(Redirect {
                        name, full_url, parents, parent_url,
                        redirect, last_modified,
                    });
                }
                _ => {
                    let mut parent_url = String::new();
                    for parent in &parents {
                            parent_url.push_str(&make_url_safe(parent));
                            parent_url.push('/');
                    }
                    let full_url = format!("{parent_url}{name_url}.{extension}");
                    self.static_files.push(StaticItem { full_url, source_path, last_modified });
                },
            }
        }
    }

    // Ext is extension without a dot.
    // Checks if a relative link to an internal page name can be reached from
    // the current page, and returns a resolved absolute link to the page with extension.
    pub fn has_page(&self, from: &impl LinkFrom, path: &str, ext: &str) -> Option<String> {
        // Remove heading fragment and file extension.
        let (path, heading) = match path.rsplit_once('#') {
            Some((path, heading)) => match heading.is_empty() {
                true => (path, None),
                false => (path, Some(heading)),
            }
            None => (path, None),
        };
        let mut path = path.strip_suffix(&format!(".{ext}")).unwrap_or(path).to_string();
        // Attach parent if not an absolute path.
        if !path.starts_with('/') {
            path = format!("{}{path}", from.parent_url());
        }
        let path = make_url_safe(&collapse_path(&path));

        // Find page with this path in website.
        for page in &self.pages {
            if page.full_url == path {
                let root = from.root();
                if let Some(heading) = heading {
                    let heading = make_url_safe(strip_appendix(heading));
                    if !page.headings.iter().any(|h| h.url == heading) {
                        warn!("Page {:?} contains link to nonexistent heading {heading:?} on page {path:?}", from.name());
                    }
                    return Some(format!("{root}{path}.{ext}#{heading}"));
                } else {
                    return Some(format!("{root}{path}.{ext}"));
                }
            }
        }
        return None;
    }

    pub fn has_static(&self, from: &impl LinkFrom, path: &str) -> Option<String> {
        // Attach parent if not an absolute path.
        let path = match !path.starts_with('/') {
            true => collapse_path(&format!("{}{path}", from.parent_url())),
            false => collapse_path(path),
        };
        for file in &self.static_files {
            if file.full_url == path {
                let root = from.root();
                return Some(format!("{root}{path}"));
            }
        }
        return None;
    }

    pub fn has_image(&self, file_name: &str) -> bool {
        let image_path = format!("images/thumb/{file_name}");
        self.static_files.iter().any(|s| s.full_url == image_path)
    }

    pub fn get_config(&self, key: &str) -> String {
        self.config.get(key).map(String::to_owned).unwrap_or_else(String::new)
    }
}


fn collapse_path(path: &str) -> String {
    // Iteratively collapse ".." segments.
    let mut segments: Vec<&str> = path.split('/')
        .filter(|s| !s.is_empty() && *s != ".")
        .collect();
    'outer: loop {
        for i in 0..(segments.len().saturating_sub(1)) {
            if segments[i] == ".." {
                if i == 0 {
                    segments.remove(0);
                } else {
                    segments.remove(i-1);
                    segments.remove(i-1);
                }
                continue 'outer;
            }
        }
        return segments.join("/");
    }
}