use lazy_static::lazy_static;
use serde_json::Value;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
use std::fs;
use std::io;
use std::path::Path;

use tonic::{Response, Status};
use tracing::info;

use crate::gitaly::{
    commit_languages_response, CommitLanguagesRequest, CommitLanguagesResponse, Repository,
};
use crate::repository::RequestWithRepo;

lazy_static! {
    static ref LANGUAGE_COLORS: HashMap<String, String> = {
        let raw = include_str!("languages.json");
        let mut colors = HashMap::new();
        // TODO maybe use expect() to panic at startup, but do not forget to read the
        // lazy static before the server starts (otherwise we'd get the panic while it is
        // running, perhaps hours after startup).
        if let Ok(Some(Value::Object(map))) = serde_json::from_str(raw) {
            for (name, details) in map.into_iter() {
                if let Value::String(color) = &details["color"]  {
                    colors.insert(name, color.to_owned());
                }
            }
        }
        colors
    };
}

pub struct CommitLanguagesTracingRequest<'a>(pub &'a CommitLanguagesRequest);

impl Debug for CommitLanguagesTracingRequest<'_> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("CommitLanguagesRequest")
            .field("repository", &self.0.repository)
            .field("revision", &String::from_utf8_lossy(&self.0.revision))
            .finish()
    }
}

impl RequestWithRepo for CommitLanguagesRequest {
    fn repository_ref(&self) -> Option<&Repository> {
        self.repository.as_ref()
    }
}

fn language_color(lang: &str) -> String {
    if let Some(s) = LANGUAGE_COLORS.get(lang) {
        return s.clone();
    }

    let mut hasher = Sha256::new();
    hasher.update(lang);
    let hash = hasher.finalize();
    format!("#{:02x}{:02x}{:02x}", hash[0], hash[1], hash[2])
}

fn aggregate_languages(
    languages: tokei::Languages,
) -> io::Result<Vec<commit_languages_response::Language>> {
    let mut resp_langs = Vec::new();
    let mut total_bytes = 0u64;
    for (lang_type, lang) in languages.iter() {
        let mut lang_bytes = 0u64;
        for report in lang.reports.iter() {
            lang_bytes += fs::metadata(&report.name)?.len();
        }
        total_bytes += lang_bytes;

        let lang_name = lang_type.to_string();
        resp_langs.push(commit_languages_response::Language {
            color: language_color(&lang_name),
            bytes: lang_bytes,
            name: lang_name,
            ..Default::default()
        })
    }
    for lang in resp_langs.iter_mut() {
        lang.share = 1e2 * (lang.bytes as f32) / (total_bytes as f32);
    }
    resp_langs.sort_unstable_by(|a, b| {
        a.share
            .partial_cmp(&b.share)
            .expect("Percentage should not be a NaN")
            .reverse()
    });
    Ok(resp_langs)
}

pub fn at_path(path: &Path) -> Result<Response<CommitLanguagesResponse>, Status> {
    let config = tokei::Config::default();
    let mut languages = tokei::Languages::new();
    let excluded = &[".hg"];
    languages.get_statistics(&[path], excluded, &config);
    info!(
        "commit_languages::at_path({}) found {} languages",
        path.display(),
        languages.len()
    );

    Ok(Response::new(CommitLanguagesResponse {
        languages: aggregate_languages(languages).unwrap(), // TODO unwrap
    }))
}
