use lazy_static::lazy_static;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
use thiserror::*;
use tracing::*;
mod index;
pub use index::*;

/// A parsed .deb file.
#[derive(Debug)]
pub struct Deb {
    md5sums: HashMap<String, String>,
    control_file: String,
    _global: Header,
    _control: Header,
    data: Header,
    data_offset: u64,
}

/// A "stanza", i.e. a description of a Debian package.
#[derive(Debug, Default, Clone)]
pub struct Stanza<'a> {
    #[allow(missing_docs)]
    pub package: &'a str,
    #[allow(missing_docs)]
    pub version: Version<'a>,
    #[allow(missing_docs)]
    pub architecture: &'a str,
    #[allow(missing_docs)]
    pub depends: Vec<Dep<'a>>,
    #[allow(missing_docs)]
    pub sha256: Option<&'a str>,
    #[allow(missing_docs)]
    pub size: Option<usize>,
    /// Path of the .deb file inside an index.
    pub file_name: Option<&'a str>,
}

/// A dependency, which is either a "simple" dependency, or a list of
/// alternatives.
#[derive(Debug, Clone)]
pub enum Dep<'a> {
    /// A simple dependency.
    Simple(SimpleDep<'a>),
    /// A dependency that can be supplied by multiple packages. This
    /// is an alternative mechanism to virtual packages.
    Alternatives {
        #[allow(missing_docs)]
        alt: Vec<SimpleDep<'a>>,
    },
}

/// A single dependency.
#[derive(Debug, Clone)]
pub struct SimpleDep<'a> {
    /// Name of the dependency.
    pub name: &'a str,
    /// Whether this dependency can have any version.
    pub any: bool,
    /// Constraints on dependencies.
    pub constraints: Vec<(&'a str, Version<'a>)>,
}

fn parse_control(s: &str) -> IResult<&str, Stanza> {
    let mut st = Stanza::default();

    for l in s.lines() {
        if l.is_empty() {
            let (_, b) = s.split_at(l.as_ptr() as usize - s.as_ptr() as usize);
            return Ok((b, st));
        }

        if let Some(i) = l.find(':') {
            let (key, val) = l.split_at(i);
            let (_, val) = val.split_at(1);
            debug!("key {:?} {:?}", key, val);
            match key.trim() {
                "Package" => st.package = val.trim(),
                "Version" => st.version = parse_version(val.trim()).unwrap().1,
                "Architecture" => st.architecture = val.trim(),
                "SHA256" => st.sha256 = Some(val.trim()),
                "Filename" => st.file_name = Some(val.trim()),
                "Depends" | "Pre-Depends" => {
                    let d = parse_deps(val.trim());
                    debug!("{:?}", d);
                    if let Ok(("", a)) = d {
                        st.depends = a
                    } else {
                        error!("error {:?}", val.trim());
                        return IResult::Err(nom::Err::Error(nom::error::make_error(
                            s,
                            nom::error::ErrorKind::Tag,
                        )));
                    }
                }
                _ => {}
            }
        }
    }

    Ok(("", st))
}

#[test]
fn test_grub_common() {
    use tracing_subscriber::prelude::*;
    use tracing_subscriber::util::SubscriberInitExt;
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| String::new().into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .try_init()
        .unwrap();

    let (rest, p) = parse_control("Package: grub-common\nArchitecture: amd64\nVersion: 2.12-1ubuntu7\nBuilt-Using: lzo2 (= 2.10-2build3)\nMulti-Arch: foreign\nPriority: optional\nSection: admin\nSource: grub2\nOrigin: Ubuntu\nMaintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>\nOriginal-Maintainer: GRUB Maintainers <pkg-grub-devel@alioth-lists.debian.net>\nBugs: https://bugs.launchpad.net/ubuntu/+filebug\nInstalled-Size: 12476\nDepends: libc6 (>= 2.38), libdevmapper1.02.1 (>= 2:1.02.36), libefiboot1t64 (>=38), libefivar1t64 (>= 38), libfreetype6 (>= 2.2.1), libfuse3-3 (>= 3.2.3), liblzma5 (>= 5.1.1alpha+20120614), debconf (>= 0.5) | debconf-2.0, gettext-base, lsb-base (>= 3.0-6), python3, python3-apt\nRecommends: os-prober (>= 1.33)\nSuggests: multiboot-doc, grub-emu, mtools, xorriso (>= 0.5.6.pl00), desktop-base (>= 4.0.6), console-setup\nConflicts: init-select\nBreaks: apport (<< 2.1.1), friendly-recovery (<< 0.2.13), lupin-support (<< 0.55), mdadm (<< 2.6.7-2)\nReplaces: grub-coreboot (<< 2.00-4), grub-efi (<< 1.99-1), grub-efi-amd64 (<< 2.00-4), grub-efi-ia32 (<< 2.00-4), grub-efi-ia64 (<< 2.00-4), grub-ieee1275 (<< 2.00-4), grub-linuxbios (<< 1.96+20080831-1), grub-pc (<< 2.00-4), grub-yeeloong (<< 2.00-4), init-select\nFilename: pool/main/g/grub2/grub-common_2.12-1ubuntu7_amd64.deb\nSize: 2119862\nMD5sum: 86e63081ae4ee34d6a4f408ac6dbf0e4\nSHA1: b98f9f88e5480423e041fd647b438ba2f6e6f5ea\nSHA256: d8110b97b6fb6da40449c74b9cb527f02965dffaae8344399a711617f5a69249\nSHA512: 85c63b8d3e6a870df48533ab525df13abe9ec4861ff4b5577def94f65a2ebf6ac44a54a6cd0eca1c825e1c7aa2bd14e4d4ed53f83d381963ac1dc51daa16482a\nHomepage: https://www.gnu.org/software/grub/\nDescription: GRand Unified Bootloader (common files)\nTask: ubuntu-desktop-minimal, ubuntu-desktop, ubuntu-desktop-raspi, kubuntu-desktop, xubuntu-minimal, xubuntu-desktop, lubuntu-desktop, ubuntustudio-desktop-core, ubuntustudio-desktop, ubuntukylin-desktop,ubuntukylin-desktop-minimal, ubuntu-mate-core, ubuntu-mate-desktop, ubuntu-budgie-desktop-minimal, ubuntu-budgie-desktop, ubuntu-budgie-desktop-raspi, ubuntu-unity-desktop, edubuntu-desktop-gnome-minimal, edubuntu-desktop-gnome-raspi, ubuntucinnamon-desktop-minimal, ubuntucinnamon-desktop-raspi\nDescription-md5: 9c75036dc0a0792fedbc58df208ed227").unwrap();

    assert!(p.depends.iter().any(|x| match x {
        Dep::Simple(s) => s.name == "liblzma5",
        _ => false,
    }));

    assert!(rest.is_empty())
}

/// A version of a package.
#[derive(Debug, Clone)]
pub struct Version<'a> {
    /// Epoch, i.e. a Debian mechanism to reorder upstream versions if
    /// necessary, for example if the upstream versioning scheme
    /// changes.
    pub epoch: u32,
    /// Upstream version.
    pub upstream_version: Upstream<'a>,
    /// Debian-specific suffix to distinguish between patches.
    pub debian: Option<&'a str>,
}

impl<'a> Default for Version<'a> {
    fn default() -> Version<'a> {
        Version {
            epoch: 0,
            upstream_version: Upstream(""),
            debian: None,
        }
    }
}

fn parse_constraint<'a>(s: &'a str) -> IResult<&'a str, (&'a str, Version<'a>)> {
    let (s, constraint) = tag("<<")
        .or(tag("<="))
        .or(tag("="))
        .or(tag(">="))
        .or(tag(">>"))
        .parse(s)?;
    debug!("parse_constraint {:?}", constraint);
    let (s, _) = space0(s)?;
    let (s, version) = parse_version(s)?;
    debug!("parse_constraint {:?} {:?}", s, (constraint, &version));
    Ok((s, (constraint, version)))
}

fn parse_dep<'a>(s0: &'a str) -> IResult<&'a str, Dep<'a>> {
    let (s, name) = parse_name(s0)?;
    let (s, any) = if s.starts_with(":any") {
        let (_, b) = s.split_at(4);
        (b, true)
    } else {
        (s, false)
    };
    let (s, _) = space0(s)?;
    let mut constraints = Vec::new();
    if let Ok((s, _)) = tag::<_, _, ()>("(").parse(s) {
        let (s, version) = if let Some(i) = s.find(')') {
            let (a, b) = s.split_at(i);
            let (_, b) = b.split_at(1);
            (b, a)
        } else {
            return IResult::Err(nom::Err::Error(nom::error::make_error(
                s,
                nom::error::ErrorKind::Tag,
            )));
        };

        let mut v = version;
        while let Ok((v_, c)) = parse_constraint(v) {
            constraints.push(c);
            let (v_, _) = space0(v_)?;
            if let Ok((v_, _)) = tag::<_, _, ()>(",").parse(v_) {
                let (v_, _) = space0(v_)?;
                v = v_;
            } else {
                break;
            }
        }

        Ok((
            s,
            Dep::Simple(SimpleDep {
                name,
                any,
                constraints,
            }),
        ))
    } else {
        Ok((
            s,
            Dep::Simple(SimpleDep {
                name,
                any,
                constraints,
            }),
        ))
    }
}

fn parse_name(s: &str) -> IResult<&str, &str> {
    if let Some(i) = s.find(|c| {
        !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '.' || c == '+' || c == '-')
    }) {
        if i > 0 {
            let (a, b) = s.split_at(i);
            return Ok((b, a));
        }
    } else {
        return Ok(("", s));
    }
    error!("ERROR {:?}", s);
    return IResult::Err(nom::Err::Error(nom::error::make_error(
        s,
        nom::error::ErrorKind::Tag,
    )));
}

fn parse_deps<'a>(s: &'a str) -> IResult<&'a str, Vec<Dep<'a>>> {
    let mut d = Vec::new();
    let (mut s, _) = space0(s)?;
    let mut alt_is_open = false;
    while let Ok((s_, d_)) = parse_dep(s) {
        let (s_, _) = space0(s_)?;
        if let Ok((s_, _)) = tag::<_, _, ()>("|").parse(s_) {
            let (s_, _) = space0(s_)?;
            s = s_;
            alt_is_open = true;
            if let Dep::Simple(d_) = d_ {
                if let Some(Dep::Alternatives { ref mut alt }) = d.last_mut() {
                    alt.push(d_)
                } else {
                    d.push(Dep::Alternatives { alt: vec![d_] })
                }
            } else {
                panic!("{:?}", s);
            }
        } else if let Ok((s_, _)) = tag::<_, _, ()>(",").parse(s_) {
            if alt_is_open {
                if let Some(Dep::Alternatives { ref mut alt }) = d.last_mut() {
                    if let Dep::Simple(d_) = d_ {
                        alt.push(d_)
                    } else {
                        panic!("{:?}", s);
                    }
                }
            } else {
                d.push(d_)
            }
            alt_is_open = false;
            s = s_;
        } else {
            if alt_is_open {
                if let Some(Dep::Alternatives { ref mut alt }) = d.last_mut() {
                    if let Dep::Simple(d_) = d_ {
                        alt.push(d_)
                    } else {
                        panic!("{:?}", s);
                    }
                }
            } else {
                d.push(d_)
            }
            s = s_;
            break;
        }
        let (s_, _) = space0(s)?;
        s = s_;
    }
    Ok((s, d))
}

/// The representation of an upstream version. Will implement `Ord`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Upstream<'a>(&'a str);

impl<'a> std::ops::Deref for Upstream<'a> {
    type Target = str;
    fn deref(&self) -> &str {
        self.0
    }
}

impl<'a> PartialOrd for Upstream<'a> {
    /// Following the algorithm described here:
    /// https://www.debian.org/doc/debian-policy/ch-controlfields.html#version
    fn partial_cmp(&self, b: &Upstream<'a>) -> Option<std::cmp::Ordering> {
        fn split_initial(s: &str) -> (&str, &str, &str) {
            // Find `u`, the first digit, and `v > u`, the first non-digit after `u`.
            let mut u = s.len();
            let mut v = s.len();
            for (n, &b) in s.as_bytes().iter().enumerate() {
                if u == s.len() {
                    if b >= b'0' && b <= b'9' {
                        u = n
                    }
                } else if b < b'0' || b > b'9' {
                    v = n;
                    break;
                }
            }
            let (ab, c) = s.split_at(v);
            let (a, b) = ab.split_at(u);
            (a, b, c)
        }
        let mut a = self.0;
        let mut b = b.0;
        use std::cmp::Ordering;
        loop {
            let (a0, a1, a_) = split_initial(a);
            let (b0, b1, b_) = split_initial(b);
            // Compare a0 / b0 using the modified ASCII ordering described in the Debian manual.
            let mut a0i = a0.as_bytes().iter();
            let mut b0i = b0.as_bytes().iter();
            loop {
                match (a0i.next(), b0i.next()) {
                    (None, None) => break,
                    (Some(c), Some(d)) if c == d => continue,
                    (Some(b'~'), _) => return Some(Ordering::Less),
                    (_, Some(b'~')) => return Some(Ordering::Greater),
                    (Some(c), Some(d)) if c.is_ascii_alphabetic() && !d.is_ascii_alphabetic() => {
                        return Some(Ordering::Less);
                    }
                    (Some(c), Some(d)) if d.is_ascii_alphabetic() && !c.is_ascii_alphabetic() => {
                        return Some(Ordering::Greater);
                    }
                    (c, d) => return Some(c.cmp(&d)),
                }
            }
            // If we haven't returned, a0 == b0. Compare the digits part.
            let a1 = if a1.is_empty() {
                0
            } else {
                a1.parse::<u64>().unwrap()
            };
            let b1 = if b1.is_empty() {
                0
            } else {
                b1.parse::<u64>().unwrap()
            };
            let cmp1 = a1.cmp(&b1);
            if let Ordering::Equal = cmp1 {
                a = a_;
                b = b_;
            } else {
                return Some(cmp1);
            }
        }
    }
}

impl<'a> Ord for Upstream<'a> {
    fn cmp(&self, b: &Upstream<'a>) -> std::cmp::Ordering {
        self.partial_cmp(b).unwrap()
    }
}

#[test]
fn test_upstream_ordering() {
    let mut x = [
        Upstream("~~"),
        Upstream("~~a"),
        Upstream("~"),
        Upstream(""),
        Upstream("a"),
    ];
    let y = x.clone();
    x.sort();
    assert_eq!(x, y,);
}

use nom::{
    bytes::tag,
    character::complete::{digit1, newline, space0},
    multi::separated_list1,
    IResult, Parser,
};

/// Parse a version in the Debian version format.
fn parse_version<'a>(s: &'a str) -> IResult<&'a str, Version<'a>> {
    fn epoch(s: &str) -> IResult<&str, u32> {
        let (s, i) = digit1(s)?;
        let (s, _) = tag(":").parse(s)?;
        Ok((s, i.parse().unwrap()))
    }

    let (s, epoch) = epoch(s).unwrap_or((s, 0));
    lazy_static! {
        static ref RE_DASH: regex::Regex =
            regex::Regex::new(r"^[a-z0-9\.~+-]+-([a-z0-9\.~+]+)").unwrap();
        static ref RE: regex::Regex = regex::Regex::new(r"^[a-z0-9\.~+]+").unwrap();
    }

    let (s, v) = if let Some(m) = RE_DASH.find(s) {
        debug!("parse_version RE_DASH {:?}", s);
        let (a, b) = s.split_at(m.end());
        (b, a)
    } else if let Some(m) = RE.find(s) {
        debug!("parse_version RE {:?}", s);
        let (a, b) = s.split_at(m.end());
        (b, a)
    } else {
        debug!("parse_version failed {:?}", s);
        return IResult::Err(nom::Err::Error(nom::error::make_error(
            s,
            nom::error::ErrorKind::Tag,
        )));
    };

    let (upstream_version, debian) = if let Some(i) = v.rfind('-') {
        let (a, b) = v.split_at(i);
        (Upstream(a), Some(b.split_at(1).1))
    } else {
        (Upstream(v), None)
    };

    Ok((
        s,
        Version {
            epoch,
            upstream_version,
            debian,
        },
    ))
}

/// Structure of the "headers" in the debian file. No alignment
/// necessary, the numbers are in decimal. No heap allocation.
#[derive(Debug)]
#[repr(C)]
struct H {
    file_id: [u8; 16],
    timestamp: [u8; 12],
    owner_id: [u8; 6],
    group_id: [u8; 6],
    file_mode: [u8; 8],
    file_size: [u8; 10],
    end_char: [u8; 2],
}

/// Parsed version of `H`, still no heap allocation.
#[allow(dead_code)]
#[derive(Debug)]
struct Header {
    file_id: [u8; 16],
    timestamp: u64,
    owner_id: u32,
    group_id: u32,
    file_mode: u32,
    file_size: u64,
}

#[allow(missing_docs)]
/// Possible errors for .deb files.
#[derive(Debug, Error)]
pub enum Error {
    #[error("Deb file parse error")]
    ParseError,
    #[error(transparent)]
    Utf8(#[from] std::str::Utf8Error),
    #[error(transparent)]
    Int(#[from] std::num::ParseIntError),
    #[error(transparent)]
    IO(#[from] std::io::Error),
    #[error("Unsupported compression algorithm: {file_id}")]
    UnsupportedCompression { file_id: String },
    #[error("Version parse: {version}")]
    Version { version: String },
}

impl Header {
    pub fn file_id(&self) -> &str {
        std::str::from_utf8(&self.file_id).unwrap().trim()
    }
}

impl H {
    fn parse(&self) -> Result<Header, Error> {
        std::str::from_utf8(&self.file_id)?;
        Ok(Header {
            file_id: self.file_id,
            timestamp: std::str::from_utf8(&self.timestamp)?.trim().parse()?,
            owner_id: std::str::from_utf8(&self.owner_id)?.trim().parse()?,
            group_id: std::str::from_utf8(&self.group_id)?.trim().parse().unwrap(),
            file_mode: u32::from_str_radix(std::str::from_utf8(&self.file_mode)?.trim(), 8)?,
            file_size: std::str::from_utf8(&self.file_size)?.trim().parse()?,
        })
    }
}

fn read_hdr<R: Read>(r: &mut R) -> Result<Header, Error> {
    unsafe {
        assert_eq!(size_of::<H>(), HEADER_SIZE as usize);
        let mut b: H = std::mem::zeroed();
        let pb: *mut H = &mut b;
        r.read_exact(&mut std::slice::from_raw_parts_mut(
            pb as *mut u8,
            size_of::<H>(),
        ))?;
        debug!("{:?}", b);
        b.parse()
    }
}

const HEADER_SIZE: u64 = 60;

impl Deb {
    /// Parse the control part, containing the description (stanza) of this package.
    pub fn parse_control(&self) -> Vec<Stanza> {
        if let Ok(("", st)) = separated_list1(newline, parse_control).parse(&self.control_file) {
            st
        } else {
            Vec::new()
        }
    }

    /// Return the md5sums of each file.
    pub fn md5sums(&self) -> &HashMap<String, String> {
        &self.md5sums
    }

    /// Decompress the data part of the deb file into a directory.
    pub fn decompress<R: BufRead + Seek, P: AsRef<std::path::Path>>(
        &self,
        mut r: R,
        path: P,
    ) -> Result<(), Error> {
        r.seek(SeekFrom::Start(self.data_offset))?;
        debug!("decompress {:?}", self.data.file_id());
        if self.data.file_id().ends_with(".tar.gz") {
            unimplemented!()
        } else if self.data.file_id().ends_with(".tar.zst") {
            let mut ar = tar::Archive::new(
                zstd::stream::read::Decoder::new(r.take(self.data.file_size)).unwrap(),
            );
            for e in ar.entries().unwrap() {
                let mut e = e.unwrap();
                debug!("zstd decompress {:?}", e.path());
                // f(&e.path()?);
                e.unpack_in(&path).unwrap();
            }
        } else if self.data.file_id().ends_with(".tar.xz") {
            let mut ar = tar::Archive::new(xz::read::XzDecoder::new(r.take(self.data.file_size)));
            for e in ar.entries().unwrap() {
                let mut e = e.unwrap();
                debug!("xz decompress {:?}", e.path());
                // f(&e.path()?);
                e.unpack_in(&path).unwrap();
            }
        } else if self.data.file_id().ends_with(".tar") {
            let mut ar = tar::Archive::new(r.take(self.data.file_size));
            for e in ar.entries().unwrap() {
                let mut e = e.unwrap();
                debug!("tar extract {:?}", e.path());
                // f(&e.path()?);
                e.unpack_in(&path).unwrap();
            }
        } else {
            unimplemented!()
        };
        Ok(())
    }

    /// Parse a `.deb` file from a buffered reader.
    pub fn read<R: BufRead + Seek>(mut r: R) -> Result<Deb, Error> {
        let mut b = [0; 8];
        r.read_exact(&mut b)?;
        if &b != b"!<arch>\n" {
            return Err(Error::ParseError);
        }

        let global = read_hdr(&mut r)?;
        trace!("GLOBAL {:?}", global);
        assert_eq!(global.file_size, 4);
        r.seek(SeekFrom::Current(global.file_size as i64))?;

        // r.skip_until(b'c')?; // 'control.…'
        // r.seek(SeekFrom::Current(-1))?;
        let control = read_hdr(&mut r)?;
        trace!("CONTROL {:?}", control);

        // Control files are expected to be short, decompress.
        let mut ctrl = vec![0; control.file_size as usize];
        r.read_exact(&mut ctrl)?;

        // gzip xz zstd none
        let ctrl_tar = if control.file_id().ends_with(".tar.gz") {
            unimplemented!()
        } else if control.file_id().ends_with(".tar.zst") {
            zstd::decode_all(&ctrl[..])?
        } else if control.file_id().ends_with(".tar.xz") {
            let mut decompressor = xz::read::XzDecoder::new(&ctrl[..]);
            let mut result = Vec::new();
            decompressor.read_to_end(&mut result)?;
            result
        } else if control.file_id().ends_with(".tar") {
            ctrl
        } else {
            return Err(Error::UnsupportedCompression {
                file_id: control.file_id().to_string(),
            });
        };
        let mut ctrl_tar = tar::Archive::new(&ctrl_tar[..]);

        let mut control_file = String::new();
        let mut md5sums = HashMap::new();
        for e in ctrl_tar.entries().unwrap() {
            let mut e = e.unwrap();
            if e.path()?.file_name() == Some(OsStr::new("control")) {
                e.read_to_string(&mut control_file)?;
            } else if e.path()?.file_name() == Some(OsStr::new("md5sums")) {
                let mut e = BufReader::new(e);
                let mut s = String::new();
                while e.read_line(&mut s)? > 0 {
                    let mut it = s.split(' ').filter(|x| !x.is_empty());
                    if let (Some(hash), Some(file)) = (it.next(), it.next()) {
                        md5sums.insert(file.trim().to_string(), hash.trim().to_string());
                    }
                    s.clear();
                }
            }
        }

        if control.file_size % 2 == 1 {
            r.seek(SeekFrom::Current(1))?;
        }
        let data = read_hdr(&mut r)?;
        debug!("data = {:?}", data);

        Ok(Deb {
            md5sums,
            _global: global,
            _control: control,
            control_file,
            data,
            data_offset: r.seek(SeekFrom::Current(0))?,
        })
    }
}