//! Provides an abstract interface for reading from and writing to files.

use std::{
    path::Path,
    fs::{
        write,
        read,
        read_to_string,
    },
};

use tectonic;

use crate::{
    error::Error,
    expense::{
        Expense,
        get_password,
    },
    config::Config,
    crypt::{
        decrypt,
        encrypt,
    },
};


/// Writes raw LaTeX (in `&str` format) to a PDF file.
pub fn latex_to_pdf(filename: &str, latex: &str) {
    // Generate PDF raw bytes
    let pdf_data: Vec<u8> = match tectonic::latex_to_pdf(latex) {
        Ok(pdf) => pdf,
        Err(_) => Error::CouldNotCompile.throw(),
    };

    // Write the PDF bytes to a file
    match write(filename, pdf_data) {
        Ok(_) => (),
        Err(_) => Error::CouldNotWriteToFile.throw(),
    };
}


/// Reads a list of expenses from a binary file.
pub fn expenses_from_bin(filename: &str, encrypted: bool) -> (Vec<Expense>, Option<String>) {
    // If the file does not exist, return an empty vector
    if !Path::new(filename).exists() {
        return (Vec::new(), None);
    }

    let mut data: Vec<u8> = match read(filename) {
        Ok(d) => d,
        Err(_) => Error::CouldNotReadFile.throw(),
    };

    // Decrypt the data if necessary
    let mut password: Option<String> = None;
    if encrypted {
        let key = get_password("Please input decryption key.".to_string());
        password = Some(key.to_owned());
        data = decrypt(data, key);
    }

    // The number of bytes should be a multiple of 100 as each expense is 100 bytes
    let count: usize = match data.len() % 100 {
        0 => data.len()/100,
        _ => Error::InvalidBinary.throw(),
    };

    let mut expenses = Vec::new();

    for i in 0..count {
        let expense = Expense::from_binary(data[i*100..(i + 1)*100].to_vec());
        expenses.push(expense);
    }

    // Sort the expenses by ID
    Expense::sort_by_id(&mut expenses);

    (expenses, password)
}


/// Writes a list of expenses to a file.
pub fn expenses_to_bin(filename: &str, mut expenses: Vec<Expense>, encrypted: bool) {
    let mut data = Vec::new();

    // Sort the expenses in ascending order by ID
    Expense::sort_by_id(&mut expenses);

    for expense in expenses {
        let mut binary = expense.to_binary();
        data.append(&mut binary);
    }

    // Encrypt the data if necessary
    if encrypted {
        let key = get_password("Please input encryption key.".to_string());
        let key2 = get_password("Please confirm encryption key.".to_string());
        if key == key2 {
            data = encrypt(data, key);
        } else {
            Error::KeysDontMatch.throw();
        }
    }

    match write(filename, data) {
        Ok(_) => (),
        Err(_) => Error::CouldNotWriteToFile.throw(),
    };
}


/// Writes a list of expenses to a file, given an encryption key.
pub fn expenses_to_bin_using_key(filename: &str, mut expenses: Vec<Expense>, key: String) {
    let mut data = Vec::new();

    // Sort the expenses in ascending order by ID
    Expense::sort_by_id(&mut expenses);

    for expense in expenses {
        let mut binary = expense.to_binary();
        data.append(&mut binary);
    }

    // Encrypt the data
    data = encrypt(data, key);

    match write(filename, data) {
        Ok(_) => (),
        Err(_) => Error::CouldNotWriteToFile.throw(),
    };
}


/// Reads config data from a config file.
pub fn config_from_file() -> Config {
    let filename = ".agconfig";

    // If the configuration file does not exist, return the default
    if !Path::new(filename).exists() {
        return Config::default();
    }

    let data: String = match read_to_string(filename) {
        Ok(d) => d,
        Err(_) => Error::CouldNotReadFile.throw(),
    };

    Config::from_ascii(data)
}


/// Writes config data to a file.
pub fn config_to_file(config: Config) {
    let filename = ".agconfig";

    // Convert the `Config` struct into bytes
    let data = config.to_ascii()
        .as_bytes()
        .iter()
        .map(|x| *x)
        .collect::<Vec<u8>>();

    // Write bytes to a file
    match write(filename, data) {
        Ok(_) => (),
        Err(_) => Error::CouldNotWriteToFile.throw(),
    };
}