use clap::{App, Arg, SubCommand};
use std::cmp;
use std::io::{self, Read, Seek};
use time::OffsetDateTime;

fn pad(mut string: String, fill: char, width: usize) -> String {
    while string.len() < width {
        string.push(fill);
    }
    string
}

fn print_summary_info<F>(package: &msi::Package<F>) {
    println!("Package type: {:?}", package.package_type());
    let is_signed = package.has_digital_signature();
    let summary_info = package.summary_info();
    let codepage = summary_info.codepage();
    println!("   Code page: {} ({})", codepage.id(), codepage.name());
    if let Some(title) = summary_info.title() {
        println!("       Title: {title}");
    }
    if let Some(subject) = summary_info.subject() {
        println!("     Subject: {subject}");
    }
    if let Some(author) = summary_info.author() {
        println!("      Author: {author}");
    }
    if let Some(uuid) = summary_info.uuid() {
        println!("        UUID: {}", uuid.hyphenated());
    }
    if let Some(arch) = summary_info.arch() {
        println!("        Arch: {arch}");
    }
    let languages = summary_info.languages();
    if !languages.is_empty() {
        let tags: Vec<&str> =
            languages.iter().map(msi::Language::tag).collect();
        println!("    Language: {}", tags.join(", "));
    }
    if let Some(timestamp) = summary_info.creation_time() {
        println!("  Created at: {}", OffsetDateTime::from(timestamp));
    }
    if let Some(app_name) = summary_info.creating_application() {
        println!("Created with: {app_name}");
    }
    println!("      Signed: {}", if is_signed { "yes" } else { "no" });
    if let Some(comments) = summary_info.comments() {
        println!("Comments:");
        for line in comments.lines() {
            println!("  {line}");
        }
    }
}

fn print_table_description(table: &msi::Table) {
    println!("{}", table.name());
    for column in table.columns() {
        println!(
            "  {:<16} {}{}{}",
            column.name(),
            if column.is_primary_key() { '*' } else { ' ' },
            column.coltype(),
            if column.is_nullable() { "?" } else { "" }
        );
    }
}

fn print_table_contents<F: Read + Seek>(
    package: &mut msi::Package<F>,
    table_name: &str,
) {
    let mut col_widths: Vec<usize> = package
        .get_table(table_name)
        .unwrap()
        .columns()
        .iter()
        .map(|column| column.name().len())
        .collect();
    let rows: Vec<Vec<String>> = package
        .select_rows(msi::Select::table(table_name))
        .expect("select")
        .map(|row| {
            let mut strings = Vec::with_capacity(row.len());
            for index in 0..row.len() {
                let string = row[index].to_string();
                col_widths[index] = cmp::max(col_widths[index], string.len());
                strings.push(string);
            }
            strings
        })
        .collect();
    {
        let mut line = String::new();
        for (index, column) in
            package.get_table(table_name).unwrap().columns().iter().enumerate()
        {
            let string =
                pad(column.name().to_string(), ' ', col_widths[index]);
            line.push_str(&string);
            line.push_str("  ");
        }
        println!("{line}");
    }
    {
        let mut line = String::new();
        for &width in &col_widths {
            let string = pad(String::new(), '-', width);
            line.push_str(&string);
            line.push_str("  ");
        }
        println!("{line}");
    }
    for row in rows {
        let mut line = String::new();
        for (index, value) in row.into_iter().enumerate() {
            let string = pad(value, ' ', col_widths[index]);
            line.push_str(&string);
            line.push_str("  ");
        }
        println!("{line}");
    }
}

fn main() {
    let matches = App::new("msiinfo")
        .version("0.1")
        .author("Matthew D. Steele <mdsteele@alum.mit.edu>")
        .about("Inspects MSI files")
        .subcommand(
            SubCommand::with_name("describe")
                .about("Prints schema for a table in an MSI file")
                .arg(Arg::with_name("path").required(true))
                .arg(Arg::with_name("table").required(true)),
        )
        .subcommand(
            SubCommand::with_name("export")
                .about("Prints all rows for a table in an MSI file")
                .arg(Arg::with_name("path").required(true))
                .arg(Arg::with_name("table").required(true)),
        )
        .subcommand(
            SubCommand::with_name("extract")
                .about("Extract a binary stream from an MSI file")
                .arg(Arg::with_name("path").required(true))
                .arg(Arg::with_name("stream").required(true)),
        )
        .subcommand(
            SubCommand::with_name("streams")
                .about("Lists binary streams in an MSI file")
                .arg(Arg::with_name("path").required(true)),
        )
        .subcommand(
            SubCommand::with_name("summary")
                .about("Prints summary information for an MSI file")
                .arg(Arg::with_name("path").required(true)),
        )
        .subcommand(
            SubCommand::with_name("tables")
                .about("Lists database tables in an MSI file")
                .arg(Arg::with_name("path").required(true)),
        )
        .get_matches();
    if let Some(submatches) = matches.subcommand_matches("describe") {
        let path = submatches.value_of("path").unwrap();
        let table_name = submatches.value_of("table").unwrap();
        let package = msi::open(path).expect("open package");
        if let Some(table) = package.get_table(table_name) {
            print_table_description(table);
        } else {
            println!("No table {table_name:?} exists in the database.");
        }
    } else if let Some(submatches) = matches.subcommand_matches("export") {
        let path = submatches.value_of("path").unwrap();
        let table_name = submatches.value_of("table").unwrap();
        let mut package = msi::open(path).expect("open package");
        print_table_contents(&mut package, table_name);
    } else if let Some(submatches) = matches.subcommand_matches("extract") {
        let path = submatches.value_of("path").unwrap();
        let stream_name = submatches.value_of("stream").unwrap();
        let mut package = msi::open(path).expect("open package");
        let mut stream = package.read_stream(stream_name).expect("read");
        io::copy(&mut stream, &mut io::stdout()).expect("extract");
    } else if let Some(submatches) = matches.subcommand_matches("streams") {
        let path = submatches.value_of("path").unwrap();
        let package = msi::open(path).expect("open package");
        for stream_name in package.streams() {
            println!("{stream_name}");
        }
    } else if let Some(submatches) = matches.subcommand_matches("summary") {
        let path = submatches.value_of("path").unwrap();
        let package = msi::open(path).expect("open package");
        print_summary_info(&package);
    } else if let Some(submatches) = matches.subcommand_matches("tables") {
        let path = submatches.value_of("path").unwrap();
        let package = msi::open(path).expect("open package");
        for table in package.tables() {
            println!("{}", table.name());
        }
    }
}
