use core::panic;
use std::process::Command;

use sha1::{Digest, Sha1};

const B64TABLE: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._";

// credit to https://dev.to/vvkkumar06/converting-a-string-to-base64-manually-4ln2
// for showing how to make a base64 converter (guide, i wrote the code).
fn b64(data: &[u8]) -> String {
    let binary: Vec<u8> = data
        .iter()
        .flat_map(|b| (0..8).rev().map(move |i| (b >> i) & 1))
        .collect();
    let mut chunks: Vec<Vec<u8>> = binary.chunks(6).map(|s| s.to_vec()).collect();
    let last = chunks.last_mut().unwrap();
    if last.len() < 6 {
        for _ in last.len()..6 {
            last.push(0);
        }
    }
    let decimal: Vec<u8> = chunks
        .iter()
        .map(|bits| bits.iter().fold(0, |acc, &bit| (acc << 1) | bit))
        .collect();
    let mut converted: Vec<String> = Vec::new();

    for dec in decimal {
        converted.push(B64TABLE.chars().nth(dec as usize).unwrap().to_string());
    }
    match data.len() % 3 {
        1 => {
            converted.push("--".to_string());
        }
        2 => {
            converted.push("-".to_string());
        }
        _ => {}
    }
    converted.join("")
}
pub fn compute_disc_id(first_track: u8, last_track: u8, leadout: u32, offsets: &[u32]) -> String {
    let mut toc = String::new();
    toc.push_str(&format!("{first_track:02X}{last_track:02X}{leadout:08X}"));
    for i in 0..99 {
        let offset = offsets.get(i).copied().unwrap_or(0);
        toc.push_str(&format!("{:08X}", offset));
    }
    let hash = Sha1::digest(toc.as_bytes());
    b64(&hash)
}
pub fn get_disc_id() -> String {
    let output = Command::new("cdparanoia")
        .arg("-Q")
        .output()
        .expect("couldn't run cdparanoia :( is it installed?\nyou can probably look up how to install it\nfor your distro :)\n");

    let toc_text = String::from_utf8_lossy(&output.stderr);

    if !output.status.success() {
        panic!("so cdparanoia got upset, idk?\nrun cdparanoia -Q yourself and see what happens\n");
    }
    let mut in_toc = false;
    let mut first_track = 0;
    let mut last_track = 0;
    let mut leadout = 0;
    let mut offsets: Vec<u32> = Vec::new();
    for line in toc_text.lines() {
        // i am assuming you are trying to read this because it is failing.
        // don't read it.
        // if you want it to work, install the following version of cdparanoia, and ONLY that version
        // cdparanoia III release 10.2 (September 11, 2008)
        // make sure that running cdparanoia -Q with a drive in, after the track number like 1.,
        // there should be 4 spaces (5 characters, the period+4 spaces)
        // if there isn't,
        // make your own parser that outputs in the right format and overwrite the normal cdparanoia command with it
        // shouldn't be that bad.
        let line = line.trim();
        if line.contains("TOTAL") {
            leadout = line
                .split_once("TOTAL")
                .unwrap() // what's error handling?
                .1
                .trim()
                .split_once(' ')
                .unwrap() // what's error handling?
                .0
                .parse()
                .unwrap(); // what's error handling?
            leadout += 150;
            break;
        }
        if in_toc {
            let track_num: u8 = line.split('.').next().unwrap().parse().unwrap();
            if first_track == 0 {
                // before this method, i did it by counting the line number, subtracting 7 to get to the first parts
                // of useful info, which WORKED for my build, but i replaced it, never forget, i-=7
                first_track = track_num;
            }
            last_track = track_num;

            offsets.push(
                line.split_once(".    ")
                    .unwrap() // what's error handling?
                    .1
                    .split_once(']')
                    .unwrap() // what's error handling?
                    .1
                    .trim()
                    .split_once(' ')
                    .unwrap() // what's error handling?
                    .0
                    .parse::<u32>()
                    .unwrap() // what's error handling?
                    + 150,
            );
        }
        if line.contains("===") {
            in_toc = true;
        }
    }
    //println!(
    //    "first track: {first_track} last track: {last_track} leadout: {leadout} offsets: {offsets:?}"
    //);
    let id = compute_disc_id(first_track, last_track, leadout, &offsets);
    //println!("disc id: {id}");
    id
    // finally that function is over!
    // still, what's error handling?
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn known_example() {
        let first_track = 1;
        let last_track = 12;
        let leadout = 224556;
        let offsets = [
            150, 9078, 13528, 34182, 53768, 70987, 96424, 118425, 136793, 159514, 179777, 198006,
        ];
        assert_eq!(
            compute_disc_id(first_track, last_track, leadout, &offsets),
            "T_prJXQSrqbnH8OE.dgOKsHm5Uw-"
        );
    }
    #[test]
    fn base64() {
        assert_eq!(b64("Man".as_bytes()), "TWFu".to_string());
        assert_eq!(b64("M".as_bytes()), "TQ--".to_string());
        assert_eq!(b64("Ma".as_bytes()), "TWE-".to_string());
    }
}
