use super::{parse_control, Stanza};
use std::num::NonZeroUsize;
use std::sync::{Arc, Mutex};
use tracing::*;

struct Index_ {
    map: memmap::Mmap,
    lru: Mutex<lru::LruCache<String, Option<(usize, usize)>>>,
}

/// The index of a package repository.
///
/// These files are made of many stanzas sorted in lexicographical
/// order of the package name. Internally this is represented as a
/// memory-mapped file.
#[derive(Clone)]
pub struct Index(Arc<Index_>);

impl Index {
    /// Open an index file.
    pub fn open<P: AsRef<std::path::Path>>(f: P) -> Result<Self, std::io::Error> {
        let file = std::fs::File::open(f)?;
        let mmap = unsafe { memmap::MmapOptions::new().map(&file)? };
        Ok(Index(Arc::new(Index_ {
            map: mmap,
            lru: Mutex::new(lru::LruCache::new(NonZeroUsize::new(256).unwrap())),
        })))
    }

    /// Looks a package up by name. This async version uses
    /// `spawn_blocking` since the process uses mmap and hence does
    /// blocking IO.
    ///
    /// This implementation assumes the packages are in
    /// lexicographical order of their name and uses a binary search.
    pub async fn lookup_async<'a>(&'a self, package: &str) -> Option<Stanza<'a>> {
        let s = self.clone();
        let p = package.to_string();
        tokio::task::spawn_blocking(move || s.lookup_range(&p))
            .await
            .unwrap()
            .map(|(m1, m2)| {
                let s = &*self.0.map;
                let line = std::str::from_utf8(&s[m1..m2]).unwrap().trim();
                parse_control(line).unwrap().1
            })
    }

    /// Look virtual packages up by name. This runs a simple regular
    /// expression on the entire index, and hence may be costly both
    /// in complexity and in IO.
    pub async fn lookup_virtual_async<'a>(&'a self, package: &str) -> Vec<Stanza<'a>> {
        let s = self.clone();
        let r =
            regex::bytes::Regex::new(&format!(r#"\nProvides:[^\n]*[, ]{package}[,\s\n]"#)).unwrap();
        tokio::task::spawn_blocking(move || {
            r.find_iter(&*s.0.map)
                .map(|m| {
                    let map = &*s.0.map;
                    s.frame(0, map.len(), m.start())
                })
                .collect::<Vec<_>>()
        })
        .await
        .unwrap()
        .into_iter()
        .map(|(m1, m2)| {
            let line = std::str::from_utf8(&self.0.map[m1..m2]).unwrap().trim();
            parse_control(line).unwrap().1
        })
        .collect()
    }

    /// Blocking package lookup by name.
    pub fn lookup<'a>(&'a self, package: &str) -> Option<Stanza<'a>> {
        self.lookup_range(package).map(|(m1, m2)| {
            let s = &*self.0.map;
            let line = std::str::from_utf8(&s[m1..m2]).unwrap().trim();
            parse_control(line).unwrap().1
        })
    }

    /// Given a range of bytes `a` (inclusive) to `b` (exclusive)
    /// inside the file, as well as a middle point, find the stanza
    /// containing byte number `m1` in the file.
    ///
    /// This assums a..b is large enough to contain the entire stanza
    /// around m1.
    fn frame(&self, a: usize, b: usize, mut m1: usize) -> (usize, usize) {
        let s = &*self.0.map;
        while m1 > a && (s[m1] != b'\n' || s[m1 + 1] != b'\n') {
            m1 -= 1;
        }

        let mut m2 = m1 + 2;
        while m2 + 1 < b && (s[m2] != b'\n' || s[m2 + 1] != b'\n') {
            m2 += 1;
        }
        if m2 < b {
            m2 += 1
        }

        (m1, m2)
    }

    /// Do the actual binay search.
    fn lookup_range(&self, package: &str) -> Option<(usize, usize)> {
        info!("Looking up {:?}", package);
        let s = &*self.0.map;

        match self.0.lru.lock().unwrap().get(package) {
            Some(Some((m1, m2))) => {
                return Some((*m1, *m2));
            }
            Some(None) => return None,
            None => {}
        }

        let mut a = 0;

        let mut b = s.len();

        // Now, dichotomy.
        loop {
            let (m1, m2) = self.frame(a, b, (a + b) / 2);
            let line = std::str::from_utf8(&s[m1..m2]).unwrap().trim();

            if line.is_empty() {
                // This may happen if b = a + 1.
                self.0.lru.lock().unwrap().put(package.to_string(), None);
                return None;
            }
            if let Ok((_, st)) = parse_control(line) {
                if st.package == package {
                    self.0
                        .lru
                        .lock()
                        .unwrap()
                        .put(package.to_string(), Some((m1, m2)));
                    return Some((m1, m2));
                } else if st.package < package {
                    a = m2
                } else {
                    b = m1
                }
            } else {
                panic!("parse {:?}", line);
            }
            if a >= b {
                break;
            }
        }
        debug!("not found 2: {:?}", package);
        self.0.lru.lock().unwrap().put(package.to_string(), None);
        None
    }
}