Documentation
use std::cmp::Ordering;

use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
use indexmap::IndexMap;

use xron::{Error, RonWorksheet, Value};

fn app() -> App<'static, 'static> {
    App::new("xron")
        .about("Converts XLSX files to RON")
        .author("Thomas Schaller")
        .setting(AppSettings::ArgsNegateSubcommands)
        .setting(AppSettings::SubcommandsNegateReqs)
        .arg(
            Arg::with_name("INPUT_FILE")
                .required(true)
                .takes_value(true)
                .value_name("INPUT_FILE")
                .index(1),
        )
        .arg(
            Arg::with_name("reverse")
                .help("Converts RON to XLSX")
                .short("r")
                .long("reverse")
                .requires("output-file"),
        )
        .arg(
            Arg::with_name("skip-empty")
                .help("Does not insert empty rows into XLSX")
                .long("skip-empty")
                .requires("reverse"),
        )
        .arg(
            Arg::with_name("column-width")
                .help("Sets column width for XLSX")
                .long("column-width")
                .requires("reverse")
                .takes_value(true)
                .value_name("COLUMN_WIDTH"),
        )
        .arg(
            Arg::with_name("skip-column")
                .help("Sets a column to skip")
                .long("skip-column")
                .conflicts_with("reverse")
                .takes_value(true),
        )
        .arg(
            Arg::with_name("output-file")
                .short("o")
                .long("output-file")
                .takes_value(true)
                .value_name("OUTPUT_TO_FILE"),
        )
        .subcommand(
            SubCommand::with_name("duplicates")
                .about("Prints duplicated line numbers")
                .arg(
                    Arg::with_name("INPUT_FILE")
                        .required(true)
                        .takes_value(true)
                        .value_name("INPUT_FILE")
                        .index(1),
                )
                .arg(
                    Arg::with_name("column")
                        .long("column")
                        .help("column number to search (zero-based)")
                        .default_value("0")
                        .takes_value(true)
                        .value_name("COLUMN_NUMBER"),
                )
                .arg(
                    Arg::with_name("sheet")
                        .long("sheet")
                        .help("sheet to search [default: first]")
                        .takes_value(true)
                        .value_name("SHEET_NAME"),
                )
                .arg(
                    Arg::with_name("write-unique-xlsx")
                        .long("write-unique-xlsx")
                        .help("Creates a copy without duplicates")
                        .takes_value(true)
                        .conflicts_with("sheet")
                        .value_name("NEW_UNIQUE_XLSX_FILE"),
                ),
        )
        .subcommand(
            SubCommand::with_name("intersect")
                .about("Only keep intersect rows")
                .arg(
                    Arg::with_name("BASE_FILE")
                        .help("Base file to remove rows from")
                        .required(true)
                        .takes_value(true)
                        .value_name("BASE_FILE")
                        .index(1),
                )
                .arg(
                    Arg::with_name("CMP_FILE")
                        .help("File to compare rows to")
                        .required(true)
                        .takes_value(true)
                        .value_name("CMP_FILE")
                        .index(2),
                )
                .arg(
                    Arg::with_name("output-file")
                        .short("o")
                        .long("output-file")
                        .takes_value(true)
                        .value_name("OUTPUT_TO_FILE"),
                ),
        )
        .subcommand(
            SubCommand::with_name("sort")
                .about("Sort XLSX lines by some parameter")
                .long_about(r#"Sort XLSX by some paramter.

Example:
xron sort myfile.xlsx --skip-first 1 -p columns-empty --descending

The above command modifies the lines 2-end of myfile.xlsx to be sorted by how many columns are
empty, descending. In other words, empty rows will be at the beginning and full rows at the end.

Simpler example:
xron sort myfile.xlsx -o myfile-output.xlsx -p first-column

Sorts the myfile.xlsx by the first column (alphabetic order) and outputs the result to myfile-output.xlsx.
"#)
                .arg(
                    Arg::with_name("input-file")
                        .required(true)
                        .takes_value(true)
                        .index(1),
                )
                .arg(
                    Arg::with_name("output-file")
                        .short("o")
                        .long("output-file")
                        .takes_value(true)
                )
                .arg(
                    Arg::with_name("skip-first")
                        .help("Skip the first n lines")
                        .long("skip-first")
                        .takes_value(true)
                )
                .arg(
                    Arg::with_name("parameter")
                        .help("The parameter to sort by")
                        .short("p")
                        .long("parameter")
                        .takes_value(true)
                        .required(true)
                        .possible_values(&["first-column", "columns-empty"])
                )
                .arg(
                    Arg::with_name("descending")
                        .help("Sort descending instead of ascending")
                        .long("descending")
                )
        )
}

fn core(matches: &ArgMatches<'_>) -> Result<(), Error> {
    let file = matches.value_of("INPUT_FILE").unwrap();

    if matches.is_present("reverse") {
        let skip_empty = matches.is_present("skip-empty");
        let column_width = matches.value_of("COLUMN_WIDTH").unwrap_or("60.0");
        let column_width = column_width.parse().unwrap_or(60.0);
        let output_file = matches.value_of("output-file").unwrap();

        let ron_workbook = xron::RonWorkbook::read_ron(file)?;
        ron_workbook.write_xlsx(output_file, column_width, skip_empty)?;
    } else {
        let mut ron_workbook = xron::RonWorkbook::read_xlsx(file)?;

        let skip_column: Option<usize> =
            matches.value_of("skip-column").and_then(|x| x.parse().ok());

        if let Some(skip_column) = skip_column {
            for sheet in ron_workbook.sheets.values_mut() {
                sheet
                    .rows
                    .values_mut()
                    .filter(|row| skip_column < row.len())
                    .for_each(|row| {
                        row.remove(skip_column);
                    })
            }
        }

        let serialized = ron::ser::to_string_pretty(&ron_workbook, Default::default())?;
        println!("{}", serialized);

        if let Some(file) = matches.value_of("output-file") {
            std::fs::write(file, serialized.as_bytes())?;
        }
    }

    Ok(())
}

fn duplicates(matches: &ArgMatches<'_>) -> Result<(), Error> {
    let file = matches.value_of("INPUT_FILE").unwrap();
    let column = matches.value_of("column").unwrap_or("0");
    let column = column.parse().unwrap_or(0usize);

    let mut ron_workbook = xron::RonWorkbook::read_xlsx(file)?;

    let sheet = {
        matches
            .value_of("sheet")
            .and_then(|name| ron_workbook.sheets.get(name))
    };
    let sheet = match sheet {
        Some(x) => x,
        None => ron_workbook.sheets.values().next().expect("No sheets"),
    };

    if let Some(unique_xlsx) = matches.value_of("write-unique-xlsx") {
        ron_workbook.retain_unique(column);
        ron_workbook.write_xlsx(unique_xlsx, 60.0, true)?;
    } else {
        let lines = sheet.find_duplicates(column);
        println!(
            "Duplicated lines: {:?}",
            lines.iter().map(|i| i + 1).collect::<Vec<_>>()
        );
    }

    Ok(())
}

fn unique(matches: &ArgMatches<'_>) -> Result<(), Error> {
    let base_file = matches.value_of("BASE_FILE").unwrap();
    let cmp_file = matches.value_of("CMP_FILE").unwrap();

    let mut base_workbook = xron::RonWorkbook::read_xlsx(base_file)?;
    let cmp_workbook = xron::RonWorkbook::read_xlsx(cmp_file)?;

    base_workbook.retain_intersecting(&cmp_workbook);

    let serialized = ron::ser::to_string_pretty(&base_workbook, Default::default())?;
    println!("{}", serialized);

    if let Some(file) = matches.value_of("output-file") {
        std::fs::write(file, serialized.as_bytes())?;
    }

    Ok(())
}

fn sort(matches: &ArgMatches<'_>) -> Result<(), Error> {
    let input_file = matches.value_of("input-file").expect("Missing input file");
    let output_file = matches.value_of("output-file").unwrap_or(input_file);
    let skip_lines = matches.value_of("skip-first").unwrap_or("0");
    let skip_lines: usize = skip_lines.parse().expect("Not a valid number");
    let parameter = matches.value_of("parameter").unwrap();
    let descending = matches.is_present("descending");

    let workbook = xron::RonWorkbook::read_xlsx(input_file)?;
    let mut result_workbook = xron::RonWorkbook::default();

    for sheet in &workbook.sheets {
        let (sheet_name, worksheet) = sheet;
        let first_lines = worksheet
            .rows
            .iter()
            .map(|x| (*x.0, x.1.clone()))
            .filter(|x| x.0 < skip_lines)
            .collect::<IndexMap<_, _>>();
        let mut rows = worksheet.rows.clone();
        rows.retain(|x, _| *x >= skip_lines);

        match parameter {
            "first-column" => rows.sort_by(|_, a: &Vec<Value>, _, b: &Vec<Value>| {
                a.get(0)
                    .and_then(|a| a.partial_cmp(&b.get(0).unwrap_or(&Value::Empty)))
                    .unwrap_or(Ordering::Less)
            }),

            "columns-empty" => rows.sort_by(|_, a: &Vec<Value>, _, b: &Vec<Value>| {
                to_ordering(
                    a.iter().cloned().filter(Value::is_empty).count() as isize
                        - b.iter().cloned().filter(Value::is_empty).count() as isize,
                )
            }),
            _ => unimplemented!(),
        }

        if descending {
            rows = rows.into_iter().rev().collect();
        }

        let mut sheet = RonWorksheet::default();
        sheet.rows.extend(first_lines);
        sheet.rows.extend(
            rows.into_iter()
                .map(|(_, v)| v)
                .enumerate()
                .map(|(i, v)| (i + skip_lines, v)),
        );

        result_workbook.sheets.insert(sheet_name.clone(), sheet);
    }

    result_workbook.write_xlsx(output_file, 80.0, false)?;

    Ok(())
}

fn to_ordering(x: isize) -> Ordering {
    if x < 0 {
        Ordering::Less
    } else if x > 0 {
        Ordering::Greater
    } else {
        Ordering::Equal
    }
}

fn run() -> Result<(), Error> {
    let app = app();
    let matches = app.get_matches();

    match matches.subcommand() {
        ("duplicates", Some(sm)) => duplicates(sm)?,
        ("intersect", Some(sm)) => unique(sm)?,
        ("sort", Some(sm)) => sort(sm)?,
        _ => core(&matches)?,
    }

    Ok(())
}

fn main() {
    if let Err(e) = run() {
        eprintln!("Error: {}", e);
    }
}