use crate::core::*;
use hashbrown::{HashMap, HashSet};
use log::info;
use percent_encoding::percent_decode_str;
use std::fmt::Debug;
use std::sync::Arc;
use tokio::time::Instant;
use tokio_util::sync::CancellationToken;
use tower_lsp::lsp_types::*;
use ustr::Ustr;
pub type Snapshot = Arc<RootGraph>;
#[derive(Hash, PartialEq, Eq, Clone, Copy)]
pub struct FileID(Ustr);
impl FileID {
    pub fn new(i: &str) -> Self {
        assert!(i != "");
        Self((percent_decode_str(i).decode_utf8().unwrap().into_owned()[..]).into())
    }
    pub fn from_uri(uri: &Url) -> FileID {
        Self::new(uri.as_str())
    }
    pub fn max() -> FileID {
        Self("".into())
    }
    pub fn url(&self) -> Url {
        Url::parse(self.0.as_str()).unwrap()
    }
    pub fn is_virtual(&self) -> bool {
        self.0.as_str().ends_with(".VIRTUAL_CONFIG")
    }
    pub fn is_config(&self) -> bool {
        self.0.as_str().ends_with(".json") | self.is_virtual()
    }
    pub fn filepath(&self) -> std::path::PathBuf {
        self.url().to_file_path().unwrap()
    }
    pub fn as_str(&self) -> &str {
        &self.0
    }
}
impl Debug for FileID {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0.as_str().split("/").last().unwrap_or("NONE"))
    }
}
pub type AstFiles = HashMap<FileID, Arc<AstDocument>>;
pub type ConfigFiles = HashMap<FileID, Arc<ConfigDocument>>;
#[derive(Hash, PartialEq, Eq, Debug, Clone, Copy)]
pub struct RootSymbol {
    pub file: FileID,
    pub sym: Symbol,
}
impl std::fmt::Display for RootSymbol {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "{}:{:?}",
            self.file.0.as_str().split("/").last().unwrap(),
            self.sym
        )
    }
}
#[derive(Debug, Clone, Default)]
pub struct RootGraph {
    cache: Cache,
    revision: u64,
    cancel: CancellationToken,
    pub files: AstFiles,
    pub configs: ConfigFiles,
}
impl RootGraph {
    pub fn contains(&self, uri: &Url) -> bool {
        self.contains_id(FileID::new(uri.as_str()))
    }
    pub fn timestamp(&self, uri: &Url) -> Option<Instant> {
        self.config_by_uri(uri)
            .map(|i| i.timestamp)
            .or(self.file_by_uri(uri).map(|i| i.timestamp))
    }
    pub fn contains_id(&self, id: FileID) -> bool {
        self.files.contains_key(&id) || self.configs.contains_key(&id)
    }
    pub fn type_of(&self, sym: RootSymbol) -> Option<Type> {
        if matches!(sym.sym, Symbol::Reference(..)) {
            let file = &self.cache.ast[&sym.file];
            file.resolved
                .get(&sym.sym)
                .and_then(|sym| self.type_of(*sym))
        } else {
            self.file(sym.file).type_of(sym.sym)
        }
    }
    pub fn config_by_uri(&self, name: &Url) -> Option<&ConfigDocument> {
        self.configs.get(&FileID::new(name.as_str())).map(|i| &**i)
    }
    pub fn file_by_uri(&self, name: &Url) -> Option<&AstDocument> {
        self.cache
            .ast
            .get(&FileID::new(name.as_str()))
            .map(|i| &*i.content)
    }
    pub fn file(&self, id: FileID) -> &AstDocument {
        &self.cache.ast[&id].content
    }
    pub fn file_id(&self, name: &Url) -> Option<FileID> {
        let id = FileID::new(name.as_str());
        if self.cache.ast.contains_key(&id) || self.configs.contains_key(&id) {
            Some(id)
        } else {
            None
        }
    }
    pub fn cancellation_token(&self) -> CancellationToken {
        self.cancel.child_token()
    }
    pub fn cancel(&self) {
        self.cancel.cancel();
    }
    pub fn resolve<'a>(
        &'a self,
        origin: FileID,
        path: &'a [Ustr],
    ) -> impl Iterator<Item = RootSymbol> + 'a {
        resolve::resolve(&self.files, &self.cache.fs, origin, path)
    }
    pub fn revision(&self) -> u64 {
        self.revision
    }
    pub fn resolve_with_binding<'a>(
        &'a self,
        origin: FileID,
        path: &'a [Ustr],
    ) -> impl Iterator<Item = Vec<(RootSymbol, usize)>> + 'a {
        resolve::resolve_with_bind(&self.files, &self.cache.fs, origin, path)
    }
    pub fn resolve_attributes<'a, F: FnMut(RootSymbol, &[Ustr])>(
        &'a self,
        origin: FileID,
        context: &'a [Ustr],
        f: F,
    ) {
        resolve::resolve_attributes(&self.files, &self.cache.fs, origin, context, f)
    }
    pub fn fs(&self) -> &FileSystem {
        &self.cache.fs
    }
    pub fn dump(&self) {
        info!("{:#?}", self);
    }
    pub fn cache(&self) -> &Cache {
        &self.cache
    }
    pub fn new(
        files: &HashMap<FileID, Arc<AstDocument>>,
        configs: &HashMap<FileID, Arc<ConfigDocument>>,
        revision: u64,
        old: &Cache,
        err: &mut ErrorsAcc,
        check_state: &mut HashMap<FileID, Instant>,
    ) -> Self {
        let mut dirty = HashSet::new();
        for file in files.values() {
            if check_state
                .get(&file.id)
                .map(|old| old != &file.timestamp)
                .unwrap_or(true)
            {
                dirty.insert(file.id);
                check_state.insert(file.id, file.timestamp);
                err.errors.insert(file.id, file.errors.clone());
            }
        }
        for conf in configs.values() {
            if check_state
                .get(&conf.id)
                .map(|old| old != &conf.timestamp)
                .unwrap_or(true)
            {
                dirty.insert(conf.id);
                check_state.insert(conf.id, conf.timestamp);
                err.errors.insert(conf.id, conf.syntax_errors.clone());
            }
        }
        {
            let mut file_paths = HashSet::new();
            for file in files.values() {
                if !file_paths.insert(file.path.as_slice()) {
                    if let Some(ns) = file.namespace() {
                        if err.errors.contains_key(&file.id) {
                            err.span(ns.range(), file.id, 100, "namespace already defined");
                        }
                    }
                }
            }
        }
        Self {
            cancel: CancellationToken::new(),
            cache: Cache::new(old, files, configs, &dirty, revision, err),
            revision,
            files: files.clone(),
            configs: configs.clone(),
        }
    }
}