//! Abstracts over expense objects.

use std::cmp::{
    Ordering,
    Ord,
};

use chrono::{
    Weekday,
    NaiveDate,
    Datelike,
};

use inquire::{
    DateSelect,
    Text,
    Select,
    Password,
};

use crate::error::Error;

/// Truncates a `String` of arbitrary length into a `Vec<u8>` of exactly `n` characters.
pub fn truncate(input: String, n: usize) -> Vec<u8> {
    let mut bytes = input.as_bytes()
        .iter()
        .map(|x| *x)
        .collect::<Vec<u8>>();
    bytes.truncate(n);
    
    while bytes.len() < n {
        bytes.push(0);
    }

    bytes
}

/// Reads a `String` from a slice of bytes (`&[u8]`).
pub fn to_string(binary: &[u8]) -> String {
    let mut output = String::new();

    for &byte in binary {
        if byte == 0 {
            break;
        }
        output.push(byte as char);
    }

    output
}

/// Represents account types.
pub const ACCOUNTS: [&str; 4] = ["Debit", "Credit", "TechCash", "Gift Card"];

#[derive(Clone, Debug)]
/// Represents an expense.
pub struct Expense {
    /// Unique identifier of the transaction.
    id: u32,
    /// Date of transaction.
    date: NaiveDate,
    /// Name of vendor.
    vendor: String,
    /// Transaction amount in dollars.
    amount: f32,
    /// Account from which money is withdrawn.
    account: String,
    /// Description of the transaction.
    desc: String,
}

impl Expense {
    /// Reads an expense from the command-line interface.
    pub fn get(id: u32) -> Self {
        let date = get_date();

        let vendor = get_text("Input vendor name.".to_owned());

        let raw_dollar_amount = get_text("Input transaction amount in dollars.".to_owned());
        let amount = match str::parse::<f32>(&raw_dollar_amount) {
            Ok(a) => a,
            Err(_) => (Error::InvalidDollarAmount (raw_dollar_amount)).throw(),
        };

        let account_options = Vec::from(ACCOUNTS);
        let account = get_selection("Input account type.".to_owned(), account_options).to_string();

        let desc = get_text("Input transaction description.".to_owned());

        Self {
            id,
            date,
            vendor,
            amount,
            account,
            desc,
        }
    }

    /// Converts binary into an expense object.
    pub fn from_binary(binary: Vec<u8>) -> Self {
        // Note: it's OK to use `Result::unwrap()` in this function because we know
        // that the slices are of the proper length.
        let id = u32::from_be_bytes(binary[0..4].try_into().unwrap());
        let date = {
            let year = i32::from_be_bytes(binary[4..8].try_into().unwrap());
            let month = u32::from_be_bytes(binary[8..12].try_into().unwrap());
            let day = u32::from_be_bytes(binary[12..16].try_into().unwrap());

            match NaiveDate::from_ymd_opt(year, month, day) {
                Some(d) => d,
                None => Error::InvalidDecryptionKey.throw(),
            }
        };
        let vendor = to_string(&binary[16..36]);
        let amount = f32::from_be_bytes(binary[36..40].try_into().unwrap());
        let account = to_string(&binary[40..60]);
        let desc = to_string(&binary[60..100]);

        Self {
            id,
            date,
            vendor,
            amount,
            account,
            desc,
        }
    }

    /// Converts an expense object into binary.
    pub fn to_binary(&self) -> Vec<u8> {
        // Convert each field into bytes
        let mut id = self.id.to_be_bytes().to_vec();
        let mut year = self.date.year().to_be_bytes().to_vec();
        let mut month = self.date.month().to_be_bytes().to_vec();
        let mut day = self.date.day().to_be_bytes().to_vec();
        let mut vendor = truncate(self.vendor.to_owned(), 20);
        let mut amount = self.amount.to_be_bytes().to_vec();
        let mut account = truncate(self.account.to_owned(), 20);
        let mut desc = truncate(self.desc.to_owned(), 40);

        // Initialize an empty output vector and grow it with the necessary bytes.
        let mut output = Vec::new();
        output.append(&mut id);
        output.append(&mut year);
        output.append(&mut month);
        output.append(&mut day);
        output.append(&mut vendor);
        output.append(&mut amount);
        output.append(&mut account);
        output.append(&mut desc);

        output
    }

    /// Gets the unique identifier of the expense.
    pub fn id(&self) -> u32 {
        self.id
    }

    /// Gets the month of the expense.
    pub fn month(&self) -> u32 {
        self.date.month()
    }

    /// Gets the day of the expense.
    pub fn day(&self) -> u32 {
        self.date.day()
    }

    /// Gets the year of the expense.
    pub fn year(&self) -> i32 {
        self.date.year()
    }

    /// Gets the weekday of the expense.
    pub fn weekday(&self) -> Weekday {
        self.date.weekday()
    }

    /// Gets the dollar amount of the expense.
    pub fn amount(&self) -> f32 {
        self.amount
    }

    /// Gets the vendor of the expense.
    pub fn vendor(&self) -> String {
        self.vendor.to_owned()
    }

    /// Gets the description of the expense.
    pub fn desc(&self) -> String {
        self.desc.to_owned()
    }

    /// Gets the account of the expense.
    pub fn account(&self) -> String {
        self.account.to_owned()
    }

    /// Exports the expense into raw LaTeX.
    pub fn to_latex(&self) -> String {
        let mut output = String::from("\\section*{");

        // Add an expense date, vendor, and amount
        output.push_str(&format!(
            "{} {:02}/{:02}/{:04} -- \\${:.2} -- {}",
            self.weekday(),
            self.month(),
            self.day(),
            self.year(),
            self.amount(),
            self.vendor(),
        ));
        output.push_str("}\n");

        // Add an event ID
        output.push_str(&format!(
            "\\textbf{{ID}}: {}\\newline\n",
            self.id(),
        ));

        // Add the account type
        output.push_str(&format!(
            "\\textbf{{Account}}: {}\\newline\n",
            self.account(),
        ));

        // Add an event description
        output.push_str(&format!(
            "\\textbf{{Description}}: {}\n",
            self.desc(),
        ));

        output.to_owned()
    }

    /// Exports the expense object into a one-line string format.
    pub fn to_str(&self) -> String {
        format!(
            "ID {} | {:02}/{:02}/{:04} | ${:.2} | {}",
            self.id(),
            self.month(),
            self.day(),
            self.year(),
            self.amount(),
            self.vendor(),
        )
    }

    /// Exports the expense object into a verbose multi-line string format.
    pub fn to_verbose(&self) -> String {
        format!(
            "ID {}\nDate: {} {:02}/{:02}/{:04}\nVendor: {}\nAmount: ${:.2}\nAccount: {}\nDescription: {}\n",
            self.id(),
            self.weekday(),
            self.month(),
            self.day(),
            self.year(),
            self.vendor(),
            self.amount(),
            self.account(),
            self.desc(),
        )
    }

    /// Sorts a list of expenses by purchase date.
    pub fn sort_by_date(vector: &mut Vec<Self>) {
        vector.sort_by(|a, b|
            if a.date.num_days_from_ce() < b.date.num_days_from_ce() {
                Ordering::Less
            } else if a.date.num_days_from_ce() == b.date.num_days_from_ce() {
                Ordering::Equal
            } else {
                Ordering::Greater
            }
        );
    }

    /// Sorts a list of expenses by amount spent.
    pub fn sort_by_amount(vector: &mut Vec<Self>) {
        vector.sort_by(|a, b|
            if a.amount() < b.amount() {
                Ordering::Less
            } else if a.amount() == b.amount() {
                Ordering::Equal
            } else {
                Ordering::Greater
            }
        );
    }

    /// Sorts a list of expenses by vendor name.
    pub fn sort_by_vendor(vector: &mut Vec<Self>) {
        vector.sort_by(|a, b|
            if Ord::max(a.vendor(), b.vendor()) == b.vendor() {
                Ordering::Less
            } else {
                Ordering::Greater
            }
        );
    }

    /// Sorts a list of expenses by ID.
    pub fn sort_by_id(vector: &mut Vec<Self>) {
        vector.sort_by(|a, b|
            if a.id() < b.id() {
                Ordering::Less
            } else {
                Ordering::Greater
            }
        );
    }
}


/// Reads a date from the CLI.
pub fn get_date() -> NaiveDate {
    // Establish default date
    let now = chrono::offset::Local::now();
    let now_naive = now.date_naive();

    let date = DateSelect::new("Input transaction date.")
        .with_default(now_naive)
        .with_week_start(Weekday::Mon)
        .prompt();
    
    match date {
        Ok(d) => d,
        Err(_) => Error::InvalidDate.throw(),
    }
}


/// Reads text from the CLI.
pub fn get_text(prompt: String) -> String {
    let string = Text::new(&prompt).prompt();
    
    match string {
        Ok(s) => s,
        Err(_) => Error::InvalidText.throw(),
    }
}


/// Reads a password from the CLI.
pub fn get_password(prompt: String) -> String {
    let string = Password::new(&prompt).prompt();
    
    match string {
        Ok(s) => s,
        Err(_) => Error::InvalidText.throw(),
    }
}


/// Reads a selection from the CLI.
pub fn get_selection(prompt: String, options: Vec<&str>) -> &str {
    let selection = Select::new(&prompt, options).prompt();

    match selection {
        Ok(s) => s,
        Err(_) => Error::InvalidSelection.throw(),
    }
}