use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::rc::Rc;

use super::frontend::{SourceFile, SourceId};

#[derive(Debug, Default)]
pub(crate) struct SourceSet {
    files: Vec<Option<SourceFile>>,
    failed_sources: BTreeMap<SourceId, FailedSource>,
    canonical_paths: Vec<PathBuf>,
    by_canonical_path: BTreeMap<PathBuf, SourceId>,
}

#[derive(Debug)]
struct FailedSource {
    logical_path: PathBuf,
    text: Rc<str>,
}

impl SourceSet {
    pub(crate) fn insert(
        &mut self,
        canonical_path: PathBuf,
        file: SourceFile,
    ) -> Result<SourceId, SourceId> {
        if let Some(id) = self.by_canonical_path.get(&canonical_path) {
            return Err(*id);
        }
        let id = file.id();
        debug_assert_eq!(id.index(), self.files.len());
        self.by_canonical_path.insert(canonical_path.clone(), id);
        self.canonical_paths.push(canonical_path);
        self.files.push(Some(file));
        Ok(id)
    }

    pub(crate) fn insert_failed(
        &mut self,
        canonical_path: PathBuf,
        source: SourceId,
        logical_path: PathBuf,
        text: Rc<str>,
    ) -> Result<(), SourceId> {
        if let Some(id) = self.by_canonical_path.get(&canonical_path) {
            return Err(*id);
        }
        debug_assert_eq!(source.index(), self.files.len());
        self.by_canonical_path
            .insert(canonical_path.clone(), source);
        self.canonical_paths.push(canonical_path);
        self.files.push(None);
        self.failed_sources
            .insert(source, FailedSource { logical_path, text });
        Ok(())
    }

    pub(crate) fn next_id(&self) -> SourceId {
        SourceId::new(u32::try_from(self.files.len()).expect("source count exceeds u32"))
    }

    pub(crate) fn get(&self, id: SourceId) -> Option<&SourceFile> {
        self.files.get(id.index()).and_then(Option::as_ref)
    }

    pub(crate) fn logical_path(&self, id: SourceId) -> Option<&Path> {
        self.get(id).map(SourceFile::logical_path).or_else(|| {
            self.failed_sources
                .get(&id)
                .map(|source| source.logical_path.as_path())
        })
    }

    pub(crate) fn line_column(&self, id: SourceId, byte: u32) -> Option<(usize, usize)> {
        if let Some(file) = self.get(id) {
            return file.line_column(byte);
        }
        self.failed_sources
            .get(&id)
            .and_then(|source| line_column(&source.text, byte))
    }

    pub(crate) fn canonical_path(&self, id: SourceId) -> Option<&Path> {
        self.canonical_paths.get(id.index()).map(PathBuf::as_path)
    }

    pub(crate) fn canonical_paths(&self) -> impl Iterator<Item = &Path> {
        self.canonical_paths.iter().map(PathBuf::as_path)
    }

    pub(crate) fn id_for_canonical_path(&self, path: &Path) -> Option<SourceId> {
        self.by_canonical_path.get(path).copied()
    }

    pub(crate) fn iter(&self) -> impl Iterator<Item = &SourceFile> {
        self.files.iter().filter_map(Option::as_ref)
    }

    pub(crate) fn len(&self) -> usize {
        self.files.len() - self.failed_sources.len()
    }

    pub(crate) fn is_empty(&self) -> bool {
        self.len() == 0
    }
}

fn line_column(text: &str, byte: u32) -> Option<(usize, usize)> {
    let byte = byte as usize;
    if byte > text.len() || !text.is_char_boundary(byte) {
        return None;
    }
    let prefix = &text[..byte];
    let line = prefix
        .bytes()
        .filter(|character| *character == b'\n')
        .count()
        + 1;
    let line_start = prefix.rfind('\n').map_or(0, |index| index + 1);
    let column = text[line_start..byte].chars().count();
    Some((line, column))
}
