use std::{borrow::Cow, io::Read};

use flate2::read::GzDecoder;
use itertools::Itertools;
use once_cell::sync::Lazy;
use rustc_hash::FxHashMap;
use serde::Deserialize;

#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Database<'a> {
    #[serde(rename = "components", borrow)]
    packages: Vec<Package<'a>>,

    #[serde(borrow)]
    metadata: Vec<Metadata<'a>>,

    #[serde(skip)]
    lookup_packages: FxHashMap<&'a str, usize>,

    #[serde(skip)]
    lookup_metadata: FxHashMap<&'a str, usize>,

    #[serde(skip)]
    lookup_kernel: usize,
}

#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Package<'a> {
    #[serde(borrow)]
    pub file_names: Vec<&'a str>,

    #[serde(borrow)]
    pub references: Vec<&'a str>,

    #[serde(borrow)]
    pub commands: Vec<Command<'a>>,

    #[serde(borrow)]
    pub environments: Vec<&'a str>,
}

#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Command<'a> {
    pub name: Cow<'a, str>,

    #[serde(borrow)]
    pub image: Option<&'a str>,

    #[serde(borrow)]
    pub glyph: Option<Cow<'a, str>>,

    #[serde(borrow)]
    pub parameters: Vec<Parameter<'a>>,
}

#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Parameter<'a>(#[serde(borrow)] pub Vec<Argument<'a>>);

#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Argument<'a> {
    pub name: &'a str,

    #[serde(borrow)]
    pub image: Option<&'a str>,
}

#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Metadata<'a> {
    pub name: &'a str,

    #[serde(borrow)]
    pub caption: Option<Cow<'a, str>>,

    #[serde(borrow)]
    pub description: Option<Cow<'a, str>>,
}

impl<'a> Database<'a> {
    pub fn iter(&self) -> impl Iterator<Item = &Package> + '_ {
        self.packages.iter()
    }

    pub fn find(&self, name: &str) -> Option<&Package> {
        self.lookup_packages
            .get(name)
            .map(|index| &self.packages[*index])
    }

    pub fn meta(&self, name: &str) -> Option<&Metadata> {
        self.lookup_metadata
            .get(name)
            .map(|index| &self.metadata[*index])
    }

    pub fn kernel(&self) -> &Package {
        &self.packages[self.lookup_kernel]
    }
}

const JSON_GZ: &[u8] = include_bytes!("../data/completion.json.gz");

pub static DATABASE: Lazy<Database<'static>> = Lazy::new(|| {
    let mut decoder = GzDecoder::new(JSON_GZ);
    let json = Box::leak(Box::default());
    decoder.read_to_string(json).unwrap();
    let mut db: Database = serde_json::from_str(json).unwrap();
    db.lookup_packages = db
        .packages
        .iter()
        .enumerate()
        .flat_map(|(i, pkg)| pkg.file_names.iter().map(move |name| (*name, i)))
        .collect();

    db.lookup_metadata = db
        .metadata
        .iter()
        .enumerate()
        .unique_by(|(_, meta)| meta.name)
        .map(|(i, meta)| (meta.name, i))
        .collect();

    db.lookup_kernel = db
        .packages
        .iter()
        .position(|package| package.file_names.is_empty())
        .unwrap();

    db
});
